feat(dropdown): add Id support with custom close, open operations
This commit is contained in:
parent
47cc6dbdbf
commit
7eecbe30d7
3 changed files with 236 additions and 88 deletions
|
|
@ -7,15 +7,17 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
pub mod menu;
|
||||
use iced_core::window;
|
||||
pub use menu::Menu;
|
||||
|
||||
pub mod multi;
|
||||
pub mod operation;
|
||||
|
||||
mod widget;
|
||||
pub use widget::*;
|
||||
|
||||
use crate::surface;
|
||||
pub use iced_core::widget::Id;
|
||||
use iced_core::window;
|
||||
|
||||
/// Displays a list of options in a popover menu on select.
|
||||
pub fn dropdown<
|
||||
|
|
@ -53,3 +55,13 @@ pub fn popup_dropdown<
|
|||
|
||||
dropdown
|
||||
}
|
||||
|
||||
/// Produces a [`Task`] that closes the [`Dropdown`].
|
||||
pub fn close<Message: 'static>(id: Id) -> iced_runtime::Task<Message> {
|
||||
iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::close(id))))
|
||||
}
|
||||
|
||||
/// Produces a [`Task`] that opens the [`Dropdown`].
|
||||
pub fn open<Message: 'static>(id: Id) -> iced_runtime::Task<Message> {
|
||||
iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::open(id))))
|
||||
}
|
||||
|
|
|
|||
72
src/widget/dropdown/operation.rs
Normal file
72
src/widget/dropdown/operation.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2025 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0 AND MIT
|
||||
//! Operate on dropdown widgets.
|
||||
|
||||
use super::State;
|
||||
use iced::Rectangle;
|
||||
use iced_core::widget::{Id, Operation};
|
||||
|
||||
pub trait Dropdown {
|
||||
fn close(&mut self);
|
||||
fn open(&mut self);
|
||||
}
|
||||
|
||||
/// Produces a [`Task`] that closes a [`Dropdown`] popup.
|
||||
pub fn close<T>(id: Id) -> impl Operation<T> {
|
||||
struct Close(Id);
|
||||
|
||||
impl<T> Operation<T> for Close {
|
||||
fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) {
|
||||
if id.map_or(true, |id| id != &self.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(state) = state.downcast_mut::<State>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.close();
|
||||
}
|
||||
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
) {
|
||||
operate_on_children(self)
|
||||
}
|
||||
}
|
||||
|
||||
Close(id)
|
||||
}
|
||||
|
||||
/// Produces a [`Task`] that opens a [`Dropdown`] popup.
|
||||
pub fn open<T>(id: Id) -> impl Operation<T> {
|
||||
struct Open(Id);
|
||||
|
||||
impl<T> Operation<T> for Open {
|
||||
fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) {
|
||||
if id.map_or(true, |id| id != &self.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(state) = state.downcast_mut::<State>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.open();
|
||||
}
|
||||
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
) {
|
||||
operate_on_children(self)
|
||||
}
|
||||
}
|
||||
|
||||
Open(id)
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// Copyright 2019 Héctor Ramón, Iced contributors
|
||||
// SPDX-License-Identifier: MPL-2.0 AND MIT
|
||||
|
||||
use super::Id;
|
||||
use super::menu::{self, Menu};
|
||||
use crate::widget::icon::{self, Handle};
|
||||
use crate::{Element, surface};
|
||||
|
|
@ -18,19 +19,21 @@ use iced_widget::pick_list::{self, Catalog};
|
|||
use std::borrow::Cow;
|
||||
use std::ffi::OsStr;
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
|
||||
pub type DropdownView<Message> = Arc<dyn Fn() -> Element<'static, Message> + Send + Sync>;
|
||||
static AUTOSIZE_ID: LazyLock<crate::widget::Id> =
|
||||
LazyLock::new(|| crate::widget::Id::new("cosmic-applet-autosize"));
|
||||
|
||||
/// A widget for selecting a single value from a list of selections.
|
||||
#[derive(Setters)]
|
||||
pub struct Dropdown<'a, S: AsRef<str> + Send + Sync + Clone + 'static, Message, AppMessage>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
{
|
||||
#[setters(skip)]
|
||||
id: Option<Id>,
|
||||
#[setters(skip)]
|
||||
on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync>,
|
||||
#[setters(skip)]
|
||||
|
|
@ -78,6 +81,7 @@ where
|
|||
on_selected: impl Fn(usize) -> Message + 'static + Send + Sync,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
on_selected: Arc::new(on_selected),
|
||||
selections,
|
||||
icons: Cow::Borrowed(&[]),
|
||||
|
|
@ -100,12 +104,13 @@ where
|
|||
/// Handle dropdown requests for popup creation.
|
||||
/// Intended to be used with [`crate::app::message::get_popup`]
|
||||
pub fn with_popup<NewAppMessage>(
|
||||
mut self,
|
||||
self,
|
||||
parent_id: window::Id,
|
||||
on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static,
|
||||
action_map: impl Fn(Message) -> NewAppMessage + Send + Sync + 'static,
|
||||
) -> Dropdown<'a, S, Message, NewAppMessage> {
|
||||
let Self {
|
||||
id,
|
||||
on_selected,
|
||||
selections,
|
||||
icons,
|
||||
|
|
@ -121,6 +126,7 @@ where
|
|||
} = self;
|
||||
|
||||
Dropdown::<'a, S, Message, NewAppMessage> {
|
||||
id,
|
||||
on_selected,
|
||||
selections,
|
||||
icons,
|
||||
|
|
@ -138,6 +144,11 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub fn id(mut self, id: Id) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
pub fn with_positioner(
|
||||
mut self,
|
||||
|
|
@ -299,6 +310,17 @@ where
|
|||
);
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
_layout: Layout<'_>,
|
||||
_renderer: &crate::Renderer,
|
||||
operation: &mut dyn iced_core::widget::Operation,
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
operation.custom(state, self.id.as_ref());
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
|
|
@ -364,6 +386,8 @@ pub struct State {
|
|||
menu: menu::State,
|
||||
keyboard_modifiers: keyboard::Modifiers,
|
||||
is_open: Arc<AtomicBool>,
|
||||
close_operation: bool,
|
||||
open_operation: bool,
|
||||
hovered_option: Arc<Mutex<Option<usize>>>,
|
||||
hashes: Vec<u64>,
|
||||
selections: Vec<crate::Plain>,
|
||||
|
|
@ -389,6 +413,8 @@ impl State {
|
|||
selections: Vec::new(),
|
||||
hashes: Vec::new(),
|
||||
popup_id: window::Id::unique(),
|
||||
close_operation: false,
|
||||
open_operation: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -399,6 +425,16 @@ impl Default for State {
|
|||
}
|
||||
}
|
||||
|
||||
impl super::operation::Dropdown for State {
|
||||
fn close(&mut self) {
|
||||
self.close_operation = true;
|
||||
}
|
||||
|
||||
fn open(&mut self) {
|
||||
self.open_operation = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the layout of a [`Dropdown`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn layout(
|
||||
|
|
@ -484,29 +520,21 @@ pub fn update<
|
|||
font: Option<crate::font::Font>,
|
||||
selected_option: Option<usize>,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let state = state();
|
||||
let is_open = state.is_open.load(Ordering::Relaxed);
|
||||
if is_open {
|
||||
// Event wasn't processed by overlay, so cursor was clicked either outside it's
|
||||
// bounds or on the drop-down, either way we close the overlay.
|
||||
state.is_open.store(false, Ordering::Relaxed);
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
if let Some(on_close) = on_surface_action {
|
||||
shell.publish(on_close(surface::action::destroy_popup(state.popup_id)));
|
||||
}
|
||||
event::Status::Captured
|
||||
} else if cursor.is_over(layout.bounds()) {
|
||||
|
||||
let open = |shell: &mut Shell<'_, Message>,
|
||||
state: &mut State,
|
||||
on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync + 'static>| {
|
||||
state.is_open.store(true, Ordering::Relaxed);
|
||||
let mut hovered_guard = state.hovered_option.lock().unwrap();
|
||||
*hovered_guard = selected;
|
||||
let id = window::Id::unique();
|
||||
state.popup_id = id;
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
if let Some(((on_surface_action, parent), action_map)) =
|
||||
on_surface_action.zip(_window_id).zip(action_map)
|
||||
if let Some(((on_surface_action, parent), action_map)) = on_surface_action
|
||||
.as_ref()
|
||||
.zip(_window_id)
|
||||
.zip(action_map.clone())
|
||||
{
|
||||
use iced_runtime::platform_specific::wayland::popup::{
|
||||
SctkPopupSettings, SctkPositioner,
|
||||
|
|
@ -580,6 +608,45 @@ pub fn update<
|
|||
);
|
||||
shell.publish(on_surface_action(get_popup_action));
|
||||
}
|
||||
};
|
||||
|
||||
let is_open = state.is_open.load(Ordering::Relaxed);
|
||||
let refresh = state.close_operation && state.open_operation;
|
||||
|
||||
if state.close_operation {
|
||||
state.close_operation = false;
|
||||
state.is_open.store(false, Ordering::SeqCst);
|
||||
if is_open {
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
if let Some(ref on_close) = on_surface_action {
|
||||
shell.publish(on_close(surface::action::destroy_popup(state.popup_id)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.open_operation {
|
||||
state.open_operation = false;
|
||||
state.is_open.store(true, Ordering::SeqCst);
|
||||
if (refresh && is_open) || (!refresh && !is_open) {
|
||||
open(shell, state, on_selected.clone());
|
||||
}
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let is_open = state.is_open.load(Ordering::Relaxed);
|
||||
if is_open {
|
||||
// Event wasn't processed by overlay, so cursor was clicked either outside it's
|
||||
// bounds or on the drop-down, either way we close the overlay.
|
||||
state.is_open.store(false, Ordering::Relaxed);
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
if let Some(on_close) = on_surface_action {
|
||||
shell.publish(on_close(surface::action::destroy_popup(state.popup_id)));
|
||||
}
|
||||
event::Status::Captured
|
||||
} else if cursor.is_over(layout.bounds()) {
|
||||
open(shell, state, on_selected);
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
|
|
@ -588,7 +655,6 @@ pub fn update<
|
|||
Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines { .. },
|
||||
}) => {
|
||||
let state = state();
|
||||
let is_open = state.is_open.load(Ordering::Relaxed);
|
||||
|
||||
if state.keyboard_modifiers.command() && cursor.is_over(layout.bounds()) && !is_open {
|
||||
|
|
@ -604,8 +670,6 @@ pub fn update<
|
|||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
|
||||
let state = state();
|
||||
|
||||
state.keyboard_modifiers = *modifiers;
|
||||
|
||||
event::Status::Ignored
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue