feat(dropdown): add Id support with custom close, open operations

This commit is contained in:
Michael Aaron Murphy 2025-11-18 18:35:27 +01:00 committed by Michael Murphy
parent 47cc6dbdbf
commit 7eecbe30d7
3 changed files with 236 additions and 88 deletions

View file

@ -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))))
}

View 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)
}

View file

@ -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