From 04b51bc1bad0bee315f795c3bf52fe136e61d25f Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 22 Jan 2025 16:31:51 -0500 Subject: [PATCH] wip: menus --- Cargo.toml | 1 + examples/applet/src/window.rs | 102 +++++-- examples/application/src/main.rs | 101 ++++++- src/app/core.rs | 49 +++- src/app/cosmic.rs | 15 +- src/app/mod.rs | 50 +++- src/applet/mod.rs | 1 + src/theme/style/menu_bar.rs | 8 +- src/theme/style/mod.rs | 2 + src/widget/context_menu.rs | 43 ++- src/widget/dropdown/menu/mod.rs | 242 +++++++++++++--- src/widget/dropdown/mod.rs | 8 +- src/widget/dropdown/widget.rs | 309 ++++++++++++++++++--- src/widget/header_bar.rs | 13 +- src/widget/menu.rs | 1 + src/widget/menu/menu_bar.rs | 11 +- src/widget/menu/menu_inner.rs | 56 ++-- src/widget/menu/menu_tree.rs | 111 ++++---- src/widget/mod.rs | 7 + src/widget/nav_bar.rs | 2 +- src/widget/responsive_container.rs | 293 +++++++++++++++++++ src/widget/segmented_button/widget.rs | 21 +- src/widget/wayland/tooltip/button_popup.rs | 0 src/widget/wayland/tooltip/dropdown.rs | 0 src/widget/wrapper.rs | 155 +++++++++++ 25 files changed, 1358 insertions(+), 243 deletions(-) create mode 100644 src/widget/responsive_container.rs create mode 100644 src/widget/wayland/tooltip/button_popup.rs create mode 100644 src/widget/wayland/tooltip/dropdown.rs create mode 100644 src/widget/wrapper.rs diff --git a/Cargo.toml b/Cargo.toml index 0c75ccc9..8be32689 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ tokio = [ # Wayland window support wayland = [ "ashpd?/wayland", + "autosize", "iced_runtime/wayland", "iced/wayland", "iced_winit/wayland", diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 1d4a9bcd..d62afad9 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -1,32 +1,65 @@ -use std::sync::{Arc, Mutex}; - use cosmic::app::message::{SurfaceMessage, SurfaceMessageHandler}; use cosmic::app::Core; -use cosmic::iced::application; -use cosmic::iced::platform_specific::shell::commands::popup::{destroy_popup, get_popup}; use cosmic::iced::window::Id; use cosmic::iced::{Length, Limits, Task}; use cosmic::iced_runtime::core::window; +use cosmic::iced_runtime::platform_specific::wayland::popup::SctkPopupSettings; use cosmic::iced_runtime::platform_specific::wayland::subsurface; -use cosmic::theme::iced; -use cosmic::widget::{layer_container, list_column, settings, toggler}; -use cosmic::{applet, iced_core, Element, Theme}; +use cosmic::widget::dropdown::DropdownView; +use cosmic::widget::{autosize, dropdown, layer_container, list_column, settings, toggler}; +use cosmic::{iced_core, Element}; +use once_cell::sync::Lazy; const ID: &str = "com.system76.CosmicAppletExample"; -#[derive(Default)] +static SUBSURFACE_ID: Lazy = + Lazy::new(|| cosmic::widget::Id::new("subsurface")); + pub struct Window { core: Core, popup: Option, example_row: bool, - subsurface_id: Option, + selected: Option, + subsurface_id: Id, + dropdown_id: Id, } -#[derive(Clone, Debug)] +impl Default for Window { + fn default() -> Self { + Self { + core: Core::default(), + popup: None, + example_row: false, + selected: None, + subsurface_id: Id::unique(), + dropdown_id: Id::unique(), + } + } +} + +#[derive(Clone)] pub enum Message { PopupClosed(Id), + PopupCloseRequested(Id), ToggleExampleRow(bool), + Selected(usize), Surface(SurfaceMessage), + OpenDropdown(SctkPopupSettings, DropdownView), +} + +impl std::fmt::Debug for Message { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::PopupClosed(arg0) => f.debug_tuple("PopupClosed").field(arg0).finish(), + Self::PopupCloseRequested(arg0) => { + f.debug_tuple("PopupCloseRequested").field(arg0).finish() + } + Self::ToggleExampleRow(arg0) => f.debug_tuple("ToggleExampleRow").field(arg0).finish(), + Self::Selected(arg0) => f.debug_tuple("Selected").field(arg0).finish(), + Self::Surface(arg0) => f.debug_tuple("Surface").field(arg0).finish(), + Self::OpenDropdown(arg0, _) => f.debug_tuple("OpenDropdown").field(arg0).finish(), + } + } } impl SurfaceMessageHandler for Message { @@ -82,6 +115,29 @@ impl cosmic::Application for Window { } Message::Surface(surface_message) => {} + Message::Selected(i) => { + self.selected = Some(i); + return cosmic::task::message(cosmic::app::message::destroy_popup::( + self.dropdown_id, + )); + } + Message::OpenDropdown(sctk_popup_settings, view) => { + self.dropdown_id = sctk_popup_settings.id; + return cosmic::task::message(cosmic::app::message::get_popup::< + Window, + >( + move |_: &mut Window| sctk_popup_settings.clone(), + Some( + move |_: &Window| -> cosmic::Element<'static, cosmic::app::Message> { + view().map(cosmic::app::Message::App) + }, + ), + )); + } + Message::PopupCloseRequested(id) => { + dbg!(id, self.dropdown_id); + return cosmic::task::message(cosmic::app::message::destroy_popup::(id)); + } } Task::none() } @@ -129,8 +185,8 @@ impl cosmic::Application for Window { ) .height(Length::Fixed(50.)), ), - ); - + ).add(dropdown(&["1", "asdf", "hello", "test"], state.selected, Message::Selected).with_popup(state.popup.unwrap_or(Id::NONE), Message::OpenDropdown).on_close_popup(Message::PopupCloseRequested) + ); Element::from( state.core.applet.popup_container(content_list), ) @@ -141,25 +197,29 @@ impl cosmic::Application for Window { ) }, ), - |layout| { - cosmic::app::message::get_subsurface::( + |_| { + cosmic::app::message::get_subsurface( |app: &mut Window| { - let id = window::Id::unique(); - app.subsurface_id = Some(id); - subsurface::SctkSubsurfaceSettings { parent: window::Id::RESERVED, - id, + id: app.subsurface_id, loc: iced_core::Point { x: -100., y: 0. }, - size: Some(iced_core::Size::new(100., 18.)), + size: Some((10., 10.).into()), z: 1, } }, - Some(|app: &Window| layer_container(cosmic::widget::text("hello")).into()), + Some(|_: &Window| { + autosize::autosize( + layer_container(cosmic::widget::text("hello")) + .layer(cosmic::cosmic_theme::Layer::Background), + SUBSURFACE_ID.clone(), + ) + .into() + }), ) }, cosmic::app::message::destroy_subsurface::( - self.subsurface_id.unwrap_or(window::Id::NONE), + self.subsurface_id ), ) .into() diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index a77b9d74..6d937cb7 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -3,12 +3,26 @@ //! Application API example +use std::collections::HashMap; +use std::sync::LazyLock; + +use cosmic::app::message::{SurfaceMessage, SurfaceMessageHandler}; use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::alignment::{Horizontal, Vertical}; use cosmic::iced::widget::column; +use cosmic::iced::Length; use cosmic::iced_core::Size; -use cosmic::widget::nav_bar; +use cosmic::widget::icon::{from_name, Handle}; +use cosmic::widget::menu::KeyBind; +use cosmic::widget::{ + container, + menu::{self, action::MenuAction}, + nav_bar, responsive, +}; use cosmic::{executor, iced, ApplicationExt, Element}; +static MENU_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("menu_id")); + #[derive(Clone, Copy)] pub enum Page { Page1, @@ -28,11 +42,24 @@ impl Page { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Action { + Hi, +} + +impl MenuAction for Action { + type Message = Message; + + fn message(&self) -> Message { + Message::Hi + } +} + /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - let _ = tracing_log::LogTracer::init(); + // tracing_subscriber::fmt::init(); + // let _ = tracing_log::LogTracer::init(); let input = vec![ (Page::Page1, "🖖 Hello from libcosmic.".into()), @@ -56,6 +83,23 @@ pub enum Message { Input2(String), Ignore, ToggleHide, + Surface(SurfaceMessage), + Hi, +} + +impl SurfaceMessageHandler for Message { + fn to_surface_message(self) -> cosmic::app::message::MessageWrapper { + match self { + Message::Surface(msg) => cosmic::app::message::MessageWrapper::Surface(msg), + msg => cosmic::app::message::MessageWrapper::Message(msg), + } + } +} + +impl From for Message { + fn from(value: SurfaceMessage) -> Self { + Message::Surface(value) + } } /// The [`App`] stores application-specific state. @@ -65,6 +109,7 @@ pub struct App { input_1: String, input_2: String, hidden: bool, + keybinds: HashMap, } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -105,6 +150,7 @@ impl cosmic::Application for App { input_1: String::new(), input_2: String::new(), hidden: true, + keybinds: HashMap::new(), }; let command = app.update_title(); @@ -136,6 +182,12 @@ impl cosmic::Application for App { Message::ToggleHide => { self.hidden = !self.hidden; } + Message::Surface(_) => { + unimplemented!() + } + Message::Hi => { + dbg!("hi"); + } } Task::none() } @@ -178,6 +230,49 @@ impl cosmic::Application for App { Element::from(centered) } + + fn header_start(&self) -> Vec> { + vec![self.core.responsive_menu_bar( + MENU_ID.clone(), + vec![ + menu::Tree::with_children( + menu::root("hiiiiiiiiiiiiiiiiiii 1"), + menu::items( + &self.keybinds, + vec![menu::Item::Button("hi", None, Action::Hi)], + ), + ), + menu::Tree::with_children( + menu::root("hiiiiiiiiiiiiiiiiii 2"), + menu::items( + &self.keybinds, + vec![menu::Item::Button("hi 2", None, Action::Hi)], + ), + ), + menu::Tree::with_children( + menu::root("hiiiiiiiiiiiiiiiiiiiii 3"), + menu::items( + &self.keybinds, + vec![menu::Item::Button("hi 3", None, Action::Hi)], + ), + ), + menu::Tree::with_children( + menu::root("hi 3"), + menu::items( + &self.keybinds, + vec![menu::Item::Button("hi 3", None, Action::Hi)], + ), + ), + menu::Tree::with_children( + menu::root("hi 4"), + menu::items( + &self.keybinds, + vec![menu::Item::Button("hi 3", None, Action::Hi)], + ), + ), + ], + )] + } } impl App diff --git a/src/app/core.rs b/src/app/core.rs index 04ab3399..0c9d338a 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -3,17 +3,24 @@ use std::{cell::OnceCell, collections::HashMap}; -use crate::widget::nav_bar; +use crate::{ + theme::menu_bar, + widget::{ + button, icon, + menu::{self, Tree}, + nav_bar, responsive_container, + }, +}; use cosmic_config::CosmicConfigEntry; use cosmic_theme::ThemeMode; -use iced::window; -use iced_core::window::Id; +use iced::{window, Limits, Size}; +use iced_core::{window::Id, Element}; use palette::Srgba; use slotmap::Key; use crate::Theme; -use super::{Element, Task}; +use super::{message::SurfaceMessage, Renderer, Task}; /// Status of the nav bar and its panels. #[derive(Clone)] @@ -97,6 +104,8 @@ pub struct Core { pub(crate) main_window: Option, pub(crate) exit_on_main_window_closed: bool, + + pub(crate) menu_bars: HashMap, } impl Default for Core { @@ -153,6 +162,7 @@ impl Default for Core { portal_is_high_contrast: None, main_window: None, exit_on_main_window_closed: true, + menu_bars: HashMap::new(), } } } @@ -376,4 +386,35 @@ impl Core { std::mem::swap(&mut self.main_window, &mut id); id } + + pub fn responsive_menu_bar<'a, Message: Clone + From + 'static>( + &self, + id: crate::widget::Id, + roots: Vec>, + ) -> crate::Element<'a, Message> { + let menu_bar_size = self.menu_bars.get(&id); + if !menu_bar_size.is_some_and(|(limits, size)| { + let max_size = limits.max(); + + max_size.width < size.width + }) { + crate::Element::from(responsive_container::responsive_container( + menu::bar(roots), + id, + )) + } else { + crate::Element::from( + responsive_container::responsive_container( + menu::bar(vec![menu::Tree::<'a, _, _>::with_children( + button::icon(icon::from_name("open-menu-symbolic")) + .padding([4, 12]) + .class(crate::theme::Button::MenuRoot), + roots, + )]), + id, + ) + .size(menu_bar_size.unwrap().1), + ) + } + } } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index e8f20124..1f7d19ac 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -124,8 +124,7 @@ where let message = match message { super::Message::App(message) => { match super::message::SurfaceMessageHandler::to_surface_message(message) { - super::message::MessageWrapper::Surface(surface_message) => - { + super::message::MessageWrapper::Surface(surface_message) => { #[cfg(feature = "wayland")] match surface_message { super::message::SurfaceMessage::Subsurface(settings, view) => { @@ -160,7 +159,6 @@ where )) } } - #[cfg(feature = "wayland")] super::message::SurfaceMessage::Popup(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -200,7 +198,18 @@ where super::message::SurfaceMessage::DestroySubsurface(id) => { iced_winit::commands::subsurface::destroy_subsurface(id) } + super::message::SurfaceMessage::ResponsiveMenuBar { + menu_bar, + limits, + size, + } => { + let core = self.app.core_mut(); + core.menu_bars.insert(menu_bar, (limits, size)); + iced::Task::none() + } } + #[cfg(not(feature = "wayland"))] + iced::Task::none() } super::message::MessageWrapper::Message(message) => self.app.update(message), } diff --git a/src/app/mod.rs b/src/app/mod.rs index 59d5a118..fbe3f2fe 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -15,6 +15,8 @@ pub(crate) mod multi_window; pub mod settings; pub mod message { + use iced::{Limits, Size}; + pub enum MessageWrapper { Surface(SurfaceMessage), Message(M), @@ -31,6 +33,7 @@ pub mod message { } } + /// Ignore this message in your application. It will be intercepted. #[derive(Clone)] pub enum SurfaceMessage { /// Create a subsurface with a view function @@ -47,20 +50,39 @@ pub mod message { ), /// Destroy a subsurface with a view function DestroyPopup(iced::window::Id), + /// Responsive menu bar update + ResponsiveMenuBar { + /// Id of the menu bar + menu_bar: crate::widget::Id, + /// Limits of the menu bar + limits: Limits, + /// Requested Full Size for expanded menu bar + size: Size, + }, } #[cfg(feature = "wayland")] impl std::fmt::Debug for SurfaceMessage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - SurfaceMessage::Subsurface(any, any1) => f.debug_tuple("Subsurface").finish(), - SurfaceMessage::DestroySubsurface(id) => { - f.debug_tuple("DestroySubsurface").field(id).finish() + Self::Subsurface(arg0, arg1) => { + f.debug_tuple("Subsurface").field(arg0).field(arg1).finish() } - SurfaceMessage::Popup(any, any1) => f.debug_tuple("Popup").finish(), - SurfaceMessage::DestroyPopup(id) => { - f.debug_tuple("DestroyPopup").field(id).finish() + Self::DestroySubsurface(arg0) => { + f.debug_tuple("DestroySubsurface").field(arg0).finish() } + Self::Popup(arg0, arg1) => f.debug_tuple("Popup").field(arg0).field(arg1).finish(), + Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(), + Self::ResponsiveMenuBar { + menu_bar, + limits, + size, + } => f + .debug_struct("ResponsiveMenuBar") + .field("menu_bar", menu_bar) + .field("limits", limits) + .field("size", size) + .finish(), } } } @@ -149,6 +171,20 @@ pub mod message { )) } + // TODO could this somehow be used by widgets? It is the wrong message type though. + // #[cfg(feature = "wayland")] + // pub fn get_simple_popup( + // settings: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings, + // view: Option< + // impl Fn() -> crate::Element<'static, super::Message> + Send + Sync + 'static, + // >, + // ) -> App::Message + // where + // App::Message: SurfaceMessageHandler + From, + // { + // todo!() + // } + #[cfg(feature = "wayland")] pub fn get_subsurface( settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync + 'static, @@ -477,7 +513,7 @@ impl DbusActivation { pub fn run_single_instance(settings: Settings, flags: App::Flags) -> iced::Result where App::Flags: CosmicFlags, - App::Message: Clone + std::fmt::Debug + Send + 'static, + App::Message: Clone + std::fmt::Debug + Send + SurfaceMessageHandler + 'static, { let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok(); diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 6c308646..6e3af6df 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -308,6 +308,7 @@ impl Context { }, parent_size: None, grab: true, + close_with_children: false, } } diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index 5acb0d09..7f99a1a5 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -1,6 +1,8 @@ // From iced_aw, license MIT //! Change the appearance of menu bars and their menus. +use std::sync::Arc; + use crate::Theme; use iced_widget::core::Color; @@ -33,19 +35,19 @@ pub trait StyleSheet { } /// The style of a menu bar and its menus -#[derive(Default)] +#[derive(Default, Clone)] #[allow(missing_debug_implementations)] pub enum MenuBarStyle { /// The default style. #[default] Default, /// A [`Theme`] that uses a `Custom` palette. - Custom(Box>), + Custom(Arc>), } impl From Appearance> for MenuBarStyle { fn from(f: fn(&Theme) -> Appearance) -> Self { - Self::Custom(Box::new(f)) + Self::Custom(Arc::new(f)) } } diff --git a/src/theme/style/mod.rs b/src/theme/style/mod.rs index c62d6be9..60768606 100644 --- a/src/theme/style/mod.rs +++ b/src/theme/style/mod.rs @@ -32,5 +32,7 @@ mod text_input; #[doc(inline)] pub use self::text_input::TextInput; +#[cfg(feature = "wayland")] pub mod tooltip; +#[cfg(feature = "wayland")] pub use tooltip::Tooltip; diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 338775b9..cf174385 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -6,24 +6,30 @@ use crate::widget::menu::{ self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, }; +use crate::widget::ArcElementWrapper; use derive_setters::Setters; use iced::touch::Finger; use iced::{Event, Vector}; use iced_core::widget::{tree, Tree, Widget}; use iced_core::{event, mouse, touch, Length, Point, Size}; use std::collections::HashSet; +use std::sync::{Arc, Mutex}; + +use super::dropdown; /// A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. -pub fn context_menu<'a, Message: 'a>( - content: impl Into> + 'a, +pub fn context_menu( + content: impl Into> + 'static, // on_context: Message, - context_menu: Option>>, -) -> ContextMenu<'a, Message> { + context_menu: Option>>, +) -> ContextMenu<'static, Message> { let mut this = ContextMenu { content: content.into(), context_menu: context_menu.map(|menus| { vec![menu::Tree::with_children( - crate::widget::row::<'static, Message>(), + ArcElementWrapper(Arc::new(Mutex::new(crate::Element::from( + crate::widget::row::<'static, Message>(), + )))), menus, )] }), @@ -43,10 +49,10 @@ pub struct ContextMenu<'a, Message> { #[setters(skip)] content: crate::Element<'a, Message>, #[setters(skip)] - context_menu: Option>>, + context_menu: Option>>, } -impl<'a, Message: Clone> Widget +impl<'a, Message: Clone + 'static> Widget for ContextMenu<'a, Message> { fn tag(&self) -> tree::Tag { @@ -58,6 +64,7 @@ impl<'a, Message: Clone> Widget tree::State::new(LocalState { context_cursor: Point::default(), fingers_pressed: Default::default(), + menu_state: Default::default(), }) } @@ -75,9 +82,12 @@ impl<'a, Message: Clone> Widget .map(|root| { let mut tree = Tree::empty(); let flat = root - .flattern() + .flatten() .iter() - .map(|mt| Tree::new(mt.item.as_widget())) + .map(|mt| { + let guard = mt.item.0.lock().unwrap(); + Tree::new(guard.as_widget()) + }) .collect(); tree.children = flat; tree @@ -214,7 +224,7 @@ impl<'a, Message: Clone> Widget translation: Vector, ) -> Option> { let state = tree.state.downcast_ref::(); - + let menu_state = state.menu_state.clone(); let Some(context_menu) = self.context_menu.as_mut() else { return None; }; @@ -229,8 +239,8 @@ impl<'a, Message: Clone> Widget Some( crate::widget::menu::Menu { - tree: &mut tree.children[1], - menu_roots: context_menu, + tree: menu_state, + menu_roots: std::borrow::Cow::Borrowed(context_menu), bounds_expand: 16, menu_overlays_parent: true, close_condition: CloseCondition { @@ -245,7 +255,7 @@ impl<'a, Message: Clone> Widget cross_offset: 0, root_bounds_list: vec![bounds], path_highlight: Some(PathHighlight::MenuActive), - style: &crate::theme::menu_bar::MenuBarStyle::Default, + style: std::borrow::Cow::Owned(crate::theme::menu_bar::MenuBarStyle::Default), position: Point::new(translation.x, translation.y), } .overlay(), @@ -265,8 +275,10 @@ impl<'a, Message: Clone> Widget } } -impl<'a, Message: Clone + 'a> From> for crate::Element<'a, Message> { - fn from(widget: ContextMenu<'a, Message>) -> Self { +impl From> + for crate::Element<'static, Message> +{ + fn from(widget: ContextMenu<'static, Message>) -> Self { Self::new(widget) } } @@ -285,4 +297,5 @@ fn touch_lifted(event: &Event) -> bool { pub struct LocalState { context_cursor: Point, fingers_pressed: HashSet, + menu_state: dropdown::menu::State, } diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 045a5ef0..b6c5c8c0 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -3,7 +3,11 @@ // SPDX-License-Identifier: MPL-2.0 AND MIT mod appearance; +use std::borrow::Cow; +use std::sync::{Arc, Mutex}; + pub use appearance::{Appearance, StyleSheet}; +use iced::advanced::widget; use crate::widget::{icon, Container}; use iced_core::event::{self, Event}; @@ -21,11 +25,12 @@ use iced_widget::scrollable::Scrollable; pub struct Menu<'a, S, Message> where S: AsRef, + [S]: std::borrow::ToOwned, { - state: &'a mut State, - options: &'a [S], - icons: &'a [icon::Handle], - hovered_option: &'a mut Option, + state: State, + options: Cow<'a, [S]>, + icons: Cow<'a, [icon::Handle]>, + hovered_option: Arc>>, selected_option: Option, on_selected: Box Message + 'a>, on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, @@ -36,14 +41,17 @@ where style: (), } -impl<'a, S: AsRef, Message: 'a> Menu<'a, S, Message> { +impl<'a, S: AsRef, Message: 'a + std::clone::Clone> Menu<'a, S, Message> +where + [S]: std::borrow::ToOwned, +{ /// Creates a new [`Menu`] with the given [`State`], a list of options, and /// the message to produced when an option is selected. pub fn new( - state: &'a mut State, - options: &'a [S], - icons: &'a [icon::Handle], - hovered_option: &'a mut Option, + state: State, + options: Cow<'a, [S]>, + icons: Cow<'a, [icon::Handle]>, + hovered_option: Arc>>, selected_option: Option, on_selected: impl FnMut(usize) -> Message + 'a, on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, @@ -102,20 +110,35 @@ impl<'a, S: AsRef, Message: 'a> Menu<'a, S, Message> { ) -> overlay::Element<'a, Message, crate::Theme, crate::Renderer> { overlay::Element::new(Box::new(Overlay::new(self, target_height, position))) } + + /// Turns the [`Menu`] into a popup [`Element`] at the given target + /// position. + /// + /// The `target_height` will be used to display the menu either on top + /// of the target or under it, depending on the screen position and the + /// dimensions of the [`Menu`]. + #[must_use] + pub fn popup(self, position: Point, target_height: f32) -> crate::Element<'a, Message> { + Overlay::new(self, target_height, position).into() + } } /// The local state of a [`Menu`]. #[must_use] -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct State { - tree: Tree, + pub(crate) tree: Arc>, } +// XXX The tree is only used on one thread, but this is needed to make a message +unsafe impl Send for State {} +unsafe impl Sync for State {} + impl State { /// Creates a new [`State`] for a [`Menu`]. pub fn new() -> Self { Self { - tree: Tree::empty(), + tree: Arc::new(Mutex::new(Tree::empty())), } } } @@ -127,7 +150,7 @@ impl Default for State { } struct Overlay<'a, Message> { - state: &'a mut Tree, + state: Arc>, container: Container<'a, Message, crate::Theme, crate::Renderer>, width: f32, target_height: f32, @@ -140,7 +163,10 @@ impl<'a, Message: 'a> Overlay<'a, Message> { menu: Menu<'a, S, Message>, target_height: f32, position: Point, - ) -> Self { + ) -> Self + where + [S]: ToOwned, + { let Menu { state, options, @@ -172,10 +198,11 @@ impl<'a, Message: 'a> Overlay<'a, Message> { )) .class(crate::style::Container::Dropdown); - state.tree.diff(&mut container as &mut dyn Widget<_, _, _>); - + let mut tree_guard = state.tree.lock().unwrap(); + tree_guard.diff(&mut container as &mut dyn Widget<_, _, _>); + drop(tree_guard); Self { - state: &mut state.tree, + state: state.tree.clone(), container, width, target_height, @@ -183,20 +210,17 @@ impl<'a, Message: 'a> Overlay<'a, Message> { position, } } -} -impl<'a, Message> iced_core::Overlay - for Overlay<'a, Message> -{ - fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { - let position = self.position; - let space_below = bounds.height - (position.y + self.target_height); - let space_above = position.y; + fn _layout(&self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { + let mut guard = self.state.lock().unwrap(); + + let space_below = bounds.height - (self.position.y + self.target_height); + let space_above = self.position.y; let limits = layout::Limits::new( Size::ZERO, Size::new( - bounds.width - position.x, + bounds.width - self.position.x, if space_below > space_above { space_below } else { @@ -206,16 +230,16 @@ impl<'a, Message> iced_core::Overlay ) .width(self.width); - let node = self.container.layout(self.state, renderer, &limits); + let node = self.container.layout(&mut guard, renderer, &limits); node.clone().move_to(if space_below > space_above { - position + Vector::new(0.0, self.target_height) + self.position + Vector::new(0.0, self.target_height) } else { - position - Vector::new(0.0, node.size().height) + self.position - Vector::new(0.0, node.size().height) }) } - fn on_event( + fn _on_event( &mut self, event: Event, layout: Layout<'_>, @@ -225,24 +249,28 @@ impl<'a, Message> iced_core::Overlay shell: &mut Shell<'_, Message>, ) -> event::Status { let bounds = layout.bounds(); + let mut guard = self.state.lock().unwrap(); + let state = &mut *guard; self.container.on_event( - self.state, event, layout, cursor, renderer, clipboard, shell, &bounds, + state, event, layout, cursor, renderer, clipboard, shell, &bounds, ) } - fn mouse_interaction( + fn _mouse_interaction( &self, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &crate::Renderer, ) -> mouse::Interaction { + let guard = self.state.lock().unwrap(); + self.container - .mouse_interaction(self.state, layout, cursor, viewport, renderer) + .mouse_interaction(&guard, layout, cursor, viewport, renderer) } - fn draw( + fn _draw( &self, renderer: &mut crate::Renderer, theme: &crate::Theme, @@ -250,6 +278,8 @@ impl<'a, Message> iced_core::Overlay layout: Layout<'_>, cursor: mouse::Cursor, ) { + let state = self.state.lock().unwrap(); + let appearance = theme.appearance(&self.style); let bounds = layout.bounds(); @@ -267,14 +297,127 @@ impl<'a, Message> iced_core::Overlay ); self.container - .draw(self.state, renderer, theme, style, layout, cursor, &bounds); + .draw(&*state, renderer, theme, style, layout, cursor, &bounds); } } -struct List<'a, S: AsRef, Message> { - options: &'a [S], - icons: &'a [icon::Handle], - hovered_option: &'a mut Option, +impl<'a, Message: 'a> iced_core::Overlay + for Overlay<'a, Message> +{ + fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { + let mut guard = self.state.lock().unwrap(); + + self._layout(renderer, bounds) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &crate::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self._on_event(event, layout, cursor, renderer, clipboard, shell) + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &crate::Renderer, + ) -> mouse::Interaction { + self._mouse_interaction(layout, cursor, viewport, renderer) + } + + fn draw( + &self, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + let guard = self.state.lock().unwrap(); + + self._draw(renderer, theme, style, layout, cursor); + } +} + +impl<'a, Message: 'a> crate::widget::Widget + for Overlay<'a, Message> +{ + fn size(&self) -> Size { + Size::new(Length::Fixed(self.width), Length::Shrink) + } + + fn layout( + &self, + _tree: &mut iced_core::widget::Tree, + renderer: &crate::Renderer, + limits: &iced::Limits, + ) -> layout::Node { + let mut guard = self.state.lock().unwrap(); + + let limits = limits.width(self.width); + + self.container.layout(&mut guard, renderer, &limits) + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &crate::Renderer, + ) -> mouse::Interaction { + self._mouse_interaction(layout, cursor, viewport, renderer) + } + + fn on_event( + &mut self, + _tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &crate::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> event::Status { + self._on_event(event, layout, cursor, renderer, clipboard, shell) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + self._draw(renderer, theme, style, layout, cursor); + } +} + +impl<'a, Message: Clone + 'a> From> for crate::Element<'a, Message> { + fn from(widget: Overlay<'a, Message>) -> Self { + Element::new(widget) + } +} + +struct List<'a, S: AsRef, Message> +where + [S]: std::borrow::ToOwned, +{ + options: Cow<'a, [S]>, + icons: Cow<'a, [icon::Handle]>, + hovered_option: Arc>>, selected_option: Option, on_selected: Box Message + 'a>, on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, @@ -285,6 +428,8 @@ struct List<'a, S: AsRef, Message> { impl<'a, S: AsRef, Message> Widget for List<'a, S, Message> +where + [S]: std::borrow::ToOwned, { fn size(&self) -> Size { Size::new(Length::Fill, Length::Shrink) @@ -330,8 +475,9 @@ impl<'a, S: AsRef, Message> Widget ) -> event::Status { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let hovered_guard = self.hovered_option.lock().unwrap(); if cursor.is_over(layout.bounds()) { - if let Some(index) = *self.hovered_option { + if let Some(index) = *hovered_guard { shell.publish((self.on_selected)(index)); return event::Status::Captured; } @@ -348,14 +494,15 @@ impl<'a, S: AsRef, Message> Widget + self.padding.vertical(); let new_hovered_option = (cursor_position.y / option_height) as usize; + let mut hovered_guard = self.hovered_option.lock().unwrap(); if let Some(on_option_hovered) = self.on_option_hovered { - if *self.hovered_option != Some(new_hovered_option) { + if *hovered_guard != Some(new_hovered_option) { shell.publish(on_option_hovered(new_hovered_option)); } } - *self.hovered_option = Some(new_hovered_option); + *hovered_guard = Some(new_hovered_option); } } Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -367,10 +514,11 @@ impl<'a, S: AsRef, Message> Widget let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) + self.padding.vertical(); + let mut hovered_guard = self.hovered_option.lock().unwrap(); - *self.hovered_option = Some((cursor_position.y / option_height) as usize); + *hovered_guard = Some((cursor_position.y / option_height) as usize); - if let Some(index) = *self.hovered_option { + if let Some(index) = *hovered_guard { shell.publish((self.on_selected)(index)); return event::Status::Captured; } @@ -434,6 +582,8 @@ impl<'a, S: AsRef, Message> Widget height: option_height, }; + let hovered_guard = self.hovered_option.lock().unwrap(); + let (color, font) = if self.selected_option == Some(i) { let item_x = bounds.x + appearance.border_width; let item_width = appearance.border_width.mul_add(-2.0, bounds.width); @@ -471,7 +621,7 @@ impl<'a, S: AsRef, Message> Widget ); (appearance.selected_text_color, crate::font::semibold()) - } else if *self.hovered_option == Some(i) { + } else if *hovered_guard == Some(i) { let item_x = bounds.x + appearance.border_width; let item_width = appearance.border_width.mul_add(-2.0, bounds.width); @@ -538,6 +688,8 @@ impl<'a, S: AsRef, Message> Widget impl<'a, S: AsRef, Message: 'a> From> for Element<'a, Message, crate::Theme, crate::Renderer> +where + [S]: std::borrow::ToOwned, { fn from(list: List<'a, S, Message>) -> Self { Element::new(list) diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index 6e3db648..1821482f 100644 --- a/src/widget/dropdown/mod.rs +++ b/src/widget/dropdown/mod.rs @@ -13,10 +13,14 @@ mod widget; pub use widget::*; /// Displays a list of options in a popover menu on select. -pub fn dropdown<'a, S: AsRef, Message: 'a>( +pub fn dropdown< + 'a, + S: AsRef + std::clone::Clone + Send + Sync + 'static, + Message: 'static + Clone, +>( selections: &'a [S], selected: Option, - on_selected: impl Fn(usize) -> Message + 'a, + on_selected: impl Fn(usize) -> Message + Send + Sync + 'static, ) -> Dropdown<'a, S, Message> { Dropdown::new(selections, selected, on_selected) } diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 26f69cce..eb62150d 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -3,9 +3,10 @@ // SPDX-License-Identifier: MPL-2.0 AND MIT use super::menu::{self, Menu}; -use crate::widget::icon; +use crate::widget::icon::{self, Handle}; +use crate::Element; use derive_setters::Setters; -use iced::Radians; +use iced::{window, Radians}; use iced_core::event::{self, Event}; use iced_core::text::{self, Paragraph, Text}; use iced_core::widget::tree::{self, Tree}; @@ -14,14 +15,23 @@ use iced_core::{ Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, }; use iced_widget::pick_list::{self, Catalog}; +use std::borrow::Cow; use std::ffi::OsStr; use std::hash::{DefaultHasher, Hash, Hasher}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, LazyLock, Mutex}; +pub type DropdownView = Arc Element<'static, Message> + Send + Sync>; +static AUTOSIZE_ID: LazyLock = + 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, Message> { +pub struct Dropdown<'a, S: AsRef + Send + Sync + Clone + 'static, Message> +where + [S]: std::borrow::ToOwned, +{ #[setters(skip)] - on_selected: Box Message + 'a>, + on_selected: Arc Message + Send + Sync>, #[setters(skip)] selections: &'a [S], #[setters] @@ -38,9 +48,29 @@ pub struct Dropdown<'a, S: AsRef, Message> { text_line_height: text::LineHeight, #[setters(strip_option)] font: Option, + #[cfg(feature = "wayland")] + #[setters(skip)] + on_open: Option< + Arc< + dyn Fn( + iced_runtime::platform_specific::wayland::popup::SctkPopupSettings, + DropdownView, + ) -> Message + + 'a, + >, + >, + #[setters(skip)] + on_close_popup: Option Message + 'a>>, + #[setters(strip_option)] + window_id: Option, + #[cfg(feature = "wayland")] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, } -impl<'a, S: AsRef, Message> Dropdown<'a, S, Message> { +impl<'a, S: AsRef + Send + Sync + Clone + 'static, Message: 'static> Dropdown<'a, S, Message> +where + [S]: std::borrow::ToOwned, +{ /// The default gap. pub const DEFAULT_GAP: f32 = 4.0; @@ -52,10 +82,10 @@ impl<'a, S: AsRef, Message> Dropdown<'a, S, Message> { pub fn new( selections: &'a [S], selected: Option, - on_selected: impl Fn(usize) -> Message + 'a, + on_selected: impl Fn(usize) -> Message + 'static + Send + Sync, ) -> Self { Self { - on_selected: Box::new(on_selected), + on_selected: Arc::new(on_selected), selections, icons: &[], selected, @@ -65,12 +95,54 @@ impl<'a, S: AsRef, Message> Dropdown<'a, S, Message> { text_size: None, text_line_height: text::LineHeight::Relative(1.2), font: None, + #[cfg(feature = "wayland")] + on_open: None, + window_id: None, + #[cfg(feature = "wayland")] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), + on_close_popup: None, } } + + #[cfg(feature = "wayland")] + /// Handle dropdown requests for popup creation. + /// Intended to be used with [`crate::app::message::get_popup`] + pub fn with_popup( + mut self, + parent_id: window::Id, + on_open: impl Fn( + iced_runtime::platform_specific::wayland::popup::SctkPopupSettings, + DropdownView, + ) -> Message + + 'a, + ) -> Self { + self.window_id = Some(parent_id); + self.on_open = Some(Arc::new(on_open)); + self + } + + #[cfg(feature = "wayland")] + pub fn with_positioner( + mut self, + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + ) -> Self { + self.positioner = positioner; + self + } + + #[cfg(feature = "wayland")] + /// Handle dropdown requests for popup removal. + /// Intended to be used with [`crate::app::message::destroy_popup`] + pub fn on_close_popup(mut self, on_close: impl Fn(window::Id) -> Message + 'a) -> Self { + self.on_close_popup = Some(Box::new(on_close)); + self + } } -impl<'a, S: AsRef, Message: 'a> Widget - for Dropdown<'a, S, Message> +impl<'a, S: AsRef + Send + Sync + Clone + 'static, Message: 'static + std::clone::Clone> + Widget for Dropdown<'a, S, Message> +where + [S]: std::borrow::ToOwned, { fn tag(&self) -> tree::Tag { tree::Tag::of::() @@ -158,10 +230,22 @@ impl<'a, S: AsRef, Message: 'a> Widget(), + self.window_id, + self.on_close_popup.as_deref(), + self.icons, + self.gap, + self.padding, + self.text_size, + self.font, + self.selected, ) } @@ -211,6 +295,9 @@ impl<'a, S: AsRef, Message: 'a> Widget Option> { + #[cfg(feature = "wayland")] + return None; + let state = tree.state.downcast_mut::(); overlay( @@ -225,7 +312,7 @@ impl<'a, S: AsRef, Message: 'a> Widget, Message: 'a> Widget, Message: 'a> From> - for crate::Element<'a, Message> +impl<'a, S: AsRef + Send + Sync + Clone + 'static, Message: 'static + std::clone::Clone> + From> for crate::Element<'a, Message> +where + [S]: std::borrow::ToOwned, { fn from(pick_list: Dropdown<'a, S, Message>) -> Self { Self::new(pick_list) @@ -251,15 +340,16 @@ impl<'a, S: AsRef, Message: 'a> From> } /// The local state of a [`Dropdown`]. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct State { icon: Option, menu: menu::State, keyboard_modifiers: keyboard::Modifiers, - is_open: bool, - hovered_option: Option, + is_open: Arc, + hovered_option: Arc>>, hashes: Vec, selections: Vec, + popup_id: window::Id, } impl State { @@ -276,10 +366,11 @@ impl State { }, menu: menu::State::default(), keyboard_modifiers: keyboard::Modifiers::default(), - is_open: false, - hovered_option: None, + is_open: Arc::new(AtomicBool::new(false)), + hovered_option: Arc::new(Mutex::new(None)), selections: Vec::new(), hashes: Vec::new(), + popup_id: window::Id::unique(), } } } @@ -349,31 +440,111 @@ pub fn layout( /// Processes an [`Event`] and updates the [`State`] of a [`Dropdown`] /// accordingly. #[allow(clippy::too_many_arguments)] -pub fn update<'a, S: AsRef, Message>( +pub fn update< + 'a, + S: AsRef + Send + Sync + std::clone::Clone + 'static, + Message: Clone + 'static, +>( event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, - on_selected: &dyn Fn(usize) -> Message, + #[cfg(feature = "wayland")] on_open: Option< + Arc< + dyn Fn( + iced_runtime::platform_specific::wayland::popup::SctkPopupSettings, + DropdownView, + ) -> Message + + 'a, + >, + >, + #[cfg(feature = "wayland")] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + on_selected: Arc Message + Send + Sync + 'static>, selected: Option, selections: &[S], state: impl FnOnce() -> &'a mut State, + window_id: Option, + on_close: Option<&'a dyn Fn(window::Id) -> Message>, + icons: &[icon::Handle], + gap: f32, + padding: Padding, + text_size: Option, + font: Option, + selected_option: Option, ) -> event::Status { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { let state = state(); - - if state.is_open { + 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 = false; - + state.is_open.store(false, Ordering::Relaxed); + if let Some(on_close) = on_close { + shell.publish(on_close(state.popup_id)); + } event::Status::Captured } else if cursor.is_over(layout.bounds()) { - state.is_open = true; - state.hovered_option = selected; + 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(feature = "wayland")] + if let Some((on_open, parent)) = on_open.zip(window_id) { + use iced_runtime::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, + }; + let bounds = layout.bounds(); + let anchor_rect = Rectangle { + x: bounds.x as i32, + y: bounds.y as i32, + width: bounds.width as i32, + height: bounds.height as i32, + }; + let icon_width = if icons.is_empty() { 0.0 } else { 24.0 }; + let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 { + selection_paragraph.min_width().round() + }; + let pad_width = padding.horizontal().mul_add(2.0, 16.0); + let selections_width = selections + .iter() + .zip(state.selections.iter_mut()) + .map(|(label, selection)| measure(label.as_ref(), selection.raw())) + .fold(0.0, |next, current| current.max(next)); + + let icons: Cow<'static, [Handle]> = Cow::Owned(icons.to_vec()); + let selections: Cow<'static, [S]> = Cow::Owned(selections.to_vec()); + let state = state.clone(); + + shell.publish(on_open( + SctkPopupSettings { + parent, + id, + positioner: SctkPositioner { + size: Some((selections_width as u32 + gap as u32 + pad_width as u32 + icon_width as u32, 10)), + anchor_rect, + anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::Top, + gravity:cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::Bottom, + reactive: true, + ..Default::default() + }, + parent_size: None, + grab: true, + close_with_children: true + }, + Arc::new(move || { + Element::from( + menu_widget( + bounds, &state, gap, padding,text_size.unwrap_or(14.0), selections.clone(), icons.clone(), selected_option, on_selected.clone() + ) + ) + }), + )); + } event::Status::Captured } else { event::Status::Ignored @@ -383,11 +554,9 @@ pub fn update<'a, S: AsRef, Message>( 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()) - && !state.is_open - { + if state.keyboard_modifiers.command() && cursor.is_over(layout.bounds()) && !is_open { let next_index = selected.map(|index| index + 1).unwrap_or_default(); if selections.len() < next_index { @@ -423,9 +592,70 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In } } +#[cfg(feature = "wayland")] +/// Returns the current menu widget of a [`Dropdown`]. +#[allow(clippy::too_many_arguments)] +pub fn menu_widget< + S: AsRef + Send + Sync + Clone + 'static, + Message: 'static + std::clone::Clone, +>( + bounds: Rectangle, + state: &State, + gap: f32, + padding: Padding, + text_size: f32, + selections: Cow<'static, [S]>, + icons: Cow<'static, [icon::Handle]>, + selected_option: Option, + on_selected: Arc Message + Send + Sync + 'static>, +) -> crate::Element<'static, Message> +where + [S]: std::borrow::ToOwned, +{ + let icon_width = if icons.is_empty() { 0.0 } else { 24.0 }; + let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 { + selection_paragraph.min_width().round() + }; + let selections_width = selections + .iter() + .zip(state.selections.iter()) + .map(|(label, selection)| measure(label.as_ref(), selection.raw())) + .fold(0.0, |next, current| current.max(next)); + let pad_width = padding.horizontal().mul_add(2.0, 16.0); + + let width = selections_width + gap + pad_width + icon_width; + let is_open = state.is_open.clone(); + let menu: Menu<'static, S, Message> = Menu::new( + state.menu.clone(), + selections, + icons, + state.hovered_option.clone(), + selected_option, + move |option| { + is_open.store(false, Ordering::Relaxed); + + (on_selected)(option) + }, + None, + ) + .width(width) + .padding(padding) + .text_size(text_size); + + crate::widget::autosize::autosize( + menu.popup(iced::Point::new(0., 0.), bounds.height), + AUTOSIZE_ID.clone(), + ) + .auto_height(true) + .auto_width(true) + .min_height(1.) + .min_width(width) + .into() +} + /// Returns the current overlay of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] -pub fn overlay<'a, S: AsRef, Message: 'a>( +pub fn overlay<'a, S: AsRef + Send + Sync + Clone + 'static, Message: std::clone::Clone + 'a>( layout: Layout<'_>, _renderer: &crate::Renderer, state: &'a mut State, @@ -439,18 +669,21 @@ pub fn overlay<'a, S: AsRef, Message: 'a>( selected_option: Option, on_selected: &'a dyn Fn(usize) -> Message, translation: Vector, -) -> Option> { - if state.is_open { +) -> Option> +where + [S]: std::borrow::ToOwned, +{ + if state.is_open.load(Ordering::Relaxed) { let bounds = layout.bounds(); let menu = Menu::new( - &mut state.menu, - selections, - icons, - &mut state.hovered_option, + state.menu.clone(), + Cow::Borrowed(selections), + Cow::Borrowed(icons), + state.hovered_option.clone(), selected_option, |option| { - state.is_open = false; + state.is_open.store(false, Ordering::Relaxed); (on_selected)(option) }, diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 1f70220f..93c7354b 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -306,7 +306,10 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { Density::Spacious => 48.0, Density::Standard => 48.0, }; - + let portion = ((start.len().max(end.len()) as f32 / center.len().max(1) as f32).round() + as u16) + .max(1); + let center_empty = center.is_empty() && self.title.is_empty(); // Creates the headerbar widget. let mut widget = widget::row::with_capacity(3) // If elements exist in the start region, append them here. @@ -316,7 +319,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .align_y(iced::Alignment::Center) .apply(widget::container) .align_x(iced::Alignment::Start) - .width(Length::Shrink), + .width(Length::FillPortion(portion)), ) // If elements exist in the center region, use them here. // This will otherwise use the title as a widget if a title was defined. @@ -338,7 +341,11 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .align_y(iced::Alignment::Center) .apply(widget::container) .align_x(iced::Alignment::End) - .width(Length::Shrink), + .width(if center_empty { + Length::Fill + } else { + Length::FillPortion(portion) + }), ) .align_y(iced::Alignment::Center) .height(Length::Fixed(height)) diff --git a/src/widget/menu.rs b/src/widget/menu.rs index d5ea3222..f5c9c461 100644 --- a/src/widget/menu.rs +++ b/src/widget/menu.rs @@ -55,6 +55,7 @@ //! pub mod action; + pub use action::MenuAction as Action; mod flex; diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 1f112925..79c77005 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -84,7 +84,7 @@ where .map(|root| { let mut tree = Tree::empty(); let flat = root - .flattern() + .flatten() .iter() .map(|mt| Tree::new(mt.item.as_widget())) .collect(); @@ -110,7 +110,7 @@ pub(crate) fn menu_roots_diff<'a, Message, Renderer>( .zip(menu_roots.iter()) .for_each(|(t, root)| { let mut flat = root - .flattern() + .flatten() .iter() .map(|mt| { let widget = mt.item.as_widget(); @@ -129,7 +129,7 @@ pub(crate) fn menu_roots_diff<'a, Message, Renderer>( let extended = menu_roots[tree.children.len()..].iter().map(|root| { let mut tree = Tree::empty(); let flat = root - .flattern() + .flatten() .iter() .map(|mt| Tree::new(mt.item.as_widget())) .collect(); @@ -366,6 +366,8 @@ where if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) { state.view_cursor = view_cursor; state.open = true; + // #[cfg(feature = "wayland")] + // TODO emit Message to open menu } } _ => (), @@ -437,6 +439,9 @@ where _renderer: &Renderer, translation: Vector, ) -> Option> { + // #[cfg(feature = "wayland")] + // return None; + let state = tree.state.downcast_ref::(); if !state.open { return None; diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index b64fba2c..616ae441 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1,8 +1,13 @@ // From iced_aw, license MIT //! Menu tree overlay +use std::{ + borrow::Cow, + sync::{Arc, Mutex}, +}; + use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; -use crate::style::menu_bar::StyleSheet; +use crate::{style::menu_bar::StyleSheet, widget::dropdown}; use iced_core::{Border, Shadow}; use iced_widget::core::{ @@ -240,7 +245,7 @@ struct MenuBounds { impl MenuBounds { #[allow(clippy::too_many_arguments)] fn new( - menu_tree: &MenuTree<'_, Message, Renderer>, + menu_tree: &MenuTree, renderer: &Renderer, item_width: ItemWidth, item_height: ItemHeight, @@ -300,7 +305,7 @@ impl MenuState { overlay_offset: Vector, slice: MenuSlice, renderer: &Renderer, - menu_tree: &MenuTree<'_, Message, Renderer>, + menu_tree: &MenuTree, tree: &mut [Tree], ) -> Node where @@ -354,7 +359,7 @@ impl MenuState { overlay_offset: Vector, index: usize, renderer: &Renderer, - menu_tree: &MenuTree<'_, Message, Renderer>, + menu_tree: &MenuTree, tree: &mut Tree, ) -> Node where @@ -427,12 +432,9 @@ impl MenuState { } } -pub(crate) struct Menu<'a, 'b, Message, Renderer> -where - Renderer: renderer::Renderer, -{ - pub(crate) tree: &'b mut Tree, - pub(crate) menu_roots: &'b mut Vec>, +pub(crate) struct Menu<'b, Message: std::clone::Clone> { + pub(crate) tree: dropdown::menu::State, + pub(crate) menu_roots: Cow<'b, Vec>>, pub(crate) bounds_expand: u16, /// Allows menu overlay items to overlap the parent pub(crate) menu_overlays_parent: bool, @@ -444,10 +446,10 @@ where pub(crate) cross_offset: i32, pub(crate) root_bounds_list: Vec, pub(crate) path_highlight: Option, - pub(crate) style: &'b ::Style, + pub(crate) style: Cow<'b, ::Style>, pub(crate) position: Point, } -impl<'a, 'b, Message, Renderer> Menu<'a, 'b, Message, Renderer> +impl<'b, Message: Clone + 'static, Renderer> Menu<'b, Message> where Renderer: renderer::Renderer, { @@ -751,7 +753,7 @@ fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle { } pub(super) fn init_root_menu( - menu: &mut Menu<'_, '_, Message, Renderer>, + menu: &mut Menu<'_, Message>, renderer: &Renderer, shell: &mut Shell<'_, Message>, overlay_cursor: Point, @@ -829,7 +831,7 @@ pub(super) fn init_root_menu( #[allow(clippy::too_many_arguments)] fn process_menu_events<'b, Message, Renderer>( tree: &'b mut Tree, - menu_roots: &'b mut [MenuTree<'_, Message, Renderer>], + menu_roots: &'b mut [MenuTree], event: event::Event, view_cursor: Cursor, renderer: &Renderer, @@ -887,7 +889,7 @@ where #[allow(unused_results)] fn process_overlay_events( - menu: &mut Menu<'_, '_, Message, Renderer>, + menu: &mut Menu<'_, Message>, renderer: &Renderer, viewport_size: Size, overlay_offset: Vector, @@ -897,6 +899,7 @@ fn process_overlay_events( ) -> event::Status where Renderer: renderer::Renderer, + Message: std::clone::Clone, { use event::Status::{Captured, Ignored}; /* @@ -1077,16 +1080,13 @@ where Captured } -fn process_scroll_events( - menu: &mut Menu<'_, '_, Message, Renderer>, +fn process_scroll_events( + menu: &mut Menu<'_, Message>, delta: mouse::ScrollDelta, overlay_cursor: Point, viewport_size: Size, overlay_offset: Vector, -) -> event::Status -where - Renderer: renderer::Renderer, -{ +) -> event::Status { use event::Status::{Captured, Ignored}; use mouse::ScrollDelta; @@ -1155,16 +1155,13 @@ where #[allow(clippy::pedantic)] /// Returns (children_size, child_positions, child_sizes) -fn get_children_layout( - menu_tree: &MenuTree<'_, Message, Renderer>, - renderer: &Renderer, +fn get_children_layout( + menu_tree: &MenuTree, + renderer: &crate::Renderer, item_width: ItemWidth, item_height: ItemHeight, tree: &mut [Tree], -) -> (Size, Vec, Vec) -where - Renderer: renderer::Renderer, -{ +) -> (Size, Vec, Vec) { let width = match item_width { ItemWidth::Uniform(u) => f32::from(u), ItemWidth::Static(s) => f32::from(menu_tree.width.unwrap_or(s)), @@ -1184,7 +1181,8 @@ where .children .iter() .map(|mt| { - let w = mt.item.as_widget(); + let guard = mt.item.0.lock().unwrap(); + let w = guard.as_widget(); match w.size().height { Length::Fixed(f) => Size::new(width, f), Length::Shrink => { diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 01ca3076..96c6a4aa 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -5,13 +5,14 @@ use std::borrow::Cow; use std::collections::HashMap; use std::rc::Rc; +use std::sync::{Arc, Mutex}; use iced_widget::core::{renderer, Element}; use crate::iced_core::{Alignment, Length}; -use crate::widget::icon; use crate::widget::menu::action::MenuAction; use crate::widget::menu::key_bind::KeyBind; +use crate::widget::{icon, ArcElementWrapper}; use crate::{theme, widget}; /// Nested menu is essentially a tree of items, a menu is a collection of items @@ -23,27 +24,27 @@ use crate::{theme, widget}; /// but there's no need to explicitly distinguish them here, if a menu tree /// has children, it's a menu, otherwise it's an item #[allow(missing_debug_implementations)] -pub struct MenuTree<'a, Message, Renderer = crate::Renderer> { +#[derive(Clone)] + +pub struct MenuTree { /// The menu tree will be flatten into a vector to build a linear widget tree, /// the `index` field is the index of the item in that vector pub(crate) index: usize, + // TODO needs to be wrapped in Arc, + pub(crate) item: ArcElementWrapper, /// The children of the menu tree - pub(crate) children: Vec>, + pub(crate) children: Vec>, /// The width of the menu tree pub(crate) width: Option, /// The height of the menu tree pub(crate) height: Option, } -impl<'a, Message, Renderer> MenuTree<'a, Message, Renderer> -where - Renderer: renderer::Renderer, -{ +impl MenuTree { /// Create a new menu tree from a widget - pub fn new(item: impl Into>) -> Self { + pub fn new(item: impl Into>) -> Self { Self { index: 0, item: item.into(), @@ -55,8 +56,8 @@ where /// Create a menu tree from a widget and a vector of sub trees pub fn with_children( - item: impl Into>, - children: Vec>>, + item: impl Into>, + children: Vec>>, ) -> Self { Self { index: 0, @@ -92,7 +93,7 @@ where /// Set the index of each item pub(crate) fn set_index(&mut self) { /// inner counting function. - fn rec(mt: &mut MenuTree<'_, Message, Renderer>, count: &mut usize) { + fn rec(mt: &mut MenuTree, count: &mut usize) { // keep items under the same menu line up mt.children.iter_mut().for_each(|c| { c.index = *count; @@ -109,36 +110,32 @@ where } /// Flatten the menu tree - pub(crate) fn flattern(&'a self) -> Vec<&Self> { + pub(crate) fn flatten(&self) -> Vec { /// Inner flattening function - fn rec<'a, Message, Renderer>( - mt: &'a MenuTree<'a, Message, Renderer>, - flat: &mut Vec<&MenuTree<'a, Message, Renderer>>, + fn rec( + mt: &MenuTree, + flat: &mut Vec>, ) { - mt.children.iter().for_each(|c| { + mt.children.clone().into_iter().for_each(|c| { flat.push(c); }); mt.children.iter().for_each(|c| { - rec(c, flat); + rec(&c, flat); }); } let mut flat = Vec::new(); - flat.push(self); + flat.push(self.clone()); rec(self, &mut flat); flat } } -impl<'a, Message, Renderer> From> - for MenuTree<'a, Message, Renderer> -where - Renderer: renderer::Renderer, -{ - fn from(value: Element<'a, Message, crate::Theme, Renderer>) -> Self { - Self::new(value) +impl From> for MenuTree { + fn from(value: crate::Element<'static, Message>) -> Self { + Self::new(ArcElementWrapper(Arc::new(Mutex::new(value)))) } } @@ -157,6 +154,7 @@ pub fn menu_button<'a, Message: 'a>( .class(theme::Button::MenuItem) } +#[derive(Clone)] /// Represents a menu item that performs an action when selected or a separator between menu items. /// /// - `Action` - Represents a menu item that performs an action when selected. @@ -213,18 +211,13 @@ where /// # Returns /// - A vector of `MenuTree`. pub fn menu_items< - 'a, A: MenuAction, L: Into> + 'static, - Message: 'a, - Renderer: renderer::Renderer + 'a, + Message: 'static + std::clone::Clone, >( key_binds: &HashMap, children: Vec>, -) -> Vec> -where - Element<'a, Message, crate::Theme, Renderer>: From>, -{ +) -> Vec> { fn find_key(action: &A, key_binds: &HashMap) -> String { for (key_bind, key_action) in key_binds { if action == key_action { @@ -259,7 +252,7 @@ where let menu_button = menu_button(items).on_press(action.message()); - trees.push(MenuTree::::new(menu_button)); + trees.push(MenuTree::::from(Element::from(menu_button))); } MenuItem::ButtonDisabled(label, icon, action) => { let key = find_key(&action, key_binds); @@ -277,7 +270,7 @@ where let menu_button = menu_button(items); - trees.push(MenuTree::::new(menu_button)); + trees.push(MenuTree::::from(Element::from(menu_button))); } MenuItem::CheckBox(label, icon, value, action) => { let key = find_key(&action, key_binds); @@ -307,36 +300,40 @@ where items.insert(2, widget::icon::icon(icon).size(14).into()); } - trees.push(MenuTree::new(menu_button(items).on_press(action.message()))); + trees.push(MenuTree::from(Element::from( + menu_button(items).on_press(action.message()), + ))); } MenuItem::Folder(label, children) => { - trees.push(MenuTree::::with_children( - menu_button(vec![ - widget::text(label).into(), - widget::horizontal_space().into(), - widget::icon::from_name("pan-end-symbolic") - .size(16) - .icon() - .into(), - ]) - .class( - // Menu folders have no on_press so they take on the disabled style by default - if children.is_empty() { - // This will make the folder use the disabled style if it has no children - theme::Button::MenuItem - } else { - // This will make the folder use the enabled style if it has children - theme::Button::MenuFolder - }, - ), + trees.push(MenuTree::::with_children( + ArcElementWrapper(Arc::new(Mutex::new(crate::Element::from( + menu_button::<'static, _>(vec![ + widget::text(label).into(), + widget::horizontal_space().into(), + widget::icon::from_name("pan-end-symbolic") + .size(16) + .icon() + .into(), + ]) + .class( + // Menu folders have no on_press so they take on the disabled style by default + if children.is_empty() { + // This will make the folder use the disabled style if it has no children + theme::Button::MenuItem + } else { + // This will make the folder use the enabled style if it has children + theme::Button::MenuFolder + }, + ), + )))), menu_items(key_binds, children), )); } MenuItem::Divider => { if i != size - 1 { - trees.push(MenuTree::::new( + trees.push(MenuTree::::from(Element::from( widget::divider::horizontal::light(), - )); + ))); } } } diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 0b0eeb8f..13a16dd6 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -97,6 +97,9 @@ pub mod aspect_ratio; #[cfg(feature = "autosize")] pub mod autosize; +#[cfg(feature = "winit")] +pub(crate) mod responsive_container; + pub mod button; #[doc(inline)] pub use button::{Button, IconButton, LinkButton, TextButton}; @@ -366,6 +369,10 @@ pub mod warning; #[doc(inline)] pub use warning::*; +pub mod wrapper; +#[doc(inline)] +pub use wrapper::*; + #[cfg(feature = "markdown")] #[doc(inline)] pub use iced::widget::markdown; diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 5a32b4a3..e6bb7037 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -67,7 +67,7 @@ impl<'a, Message: Clone + 'static> NavBar<'a, Message> { self } - pub fn context_menu(mut self, context_menu: Option>>) -> Self { + pub fn context_menu(mut self, context_menu: Option>>) -> Self { self.segmented_button = self.segmented_button.context_menu(context_menu); self } diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs new file mode 100644 index 00000000..32f4bbe0 --- /dev/null +++ b/src/widget/responsive_container.rs @@ -0,0 +1,293 @@ +//! Autosize Container, which will resize the window to its contents. + +use iced::{Limits, Size}; +use iced_core::event::{self, Event}; +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; +use iced_core::renderer; +use iced_core::widget::{tree, Id, Tree}; +use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; + +use crate::app::message::SurfaceMessage; + +pub(crate) fn responsive_container<'a, Message: 'static + From, Theme, E>( + content: E, + id: Id, +) -> ResponsiveContainer<'a, Message, Theme, crate::Renderer> +where + E: Into>, + Theme: iced_widget::container::Catalog, + ::Class<'a>: From>, +{ + ResponsiveContainer::new(content, id) +} + +/// An element decorating some content. +/// +/// It is normally used for alignment purposes. +#[allow(missing_debug_implementations)] +pub struct ResponsiveContainer<'a, Message, Theme, Renderer> +where + Renderer: iced_core::Renderer, +{ + content: Element<'a, Message, Theme, Renderer>, + id: Id, + size: Option, +} + +impl<'a, Message, Theme, Renderer> ResponsiveContainer<'a, Message, Theme, Renderer> +where + Renderer: iced_core::Renderer, +{ + /// Creates an empty [`IdContainer`]. + pub(crate) fn new(content: T, id: Id) -> Self + where + T: Into>, + { + ResponsiveContainer { + content: content.into(), + id, + size: None, + } + } + + pub(crate) fn size(mut self, size: Size) -> Self { + self.size = Some(size); + self + } +} + +impl<'a, Message, Theme, Renderer> Widget + for ResponsiveContainer<'a, Message, Theme, Renderer> +where + Renderer: iced_core::Renderer, + Message: From, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.children[0].diff(&mut self.content); + } + + fn size(&self) -> iced_core::Size { + self.content.as_widget().size() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let state = tree.state.downcast_mut::(); + let unrestricted_size = self.size.unwrap_or_else(|| { + let node = + self.content + .as_widget() + .layout(&mut tree.children[0], renderer, &Limits::NONE); + node.size() + }); + + let max_size = limits.max(); + let old_max = state.limits.max(); + state.needs_update = (unrestricted_size.width > max_size.width) + ^ (state.size.width > old_max.width) + || (unrestricted_size.height > max_size.height) ^ (state.size.height > old_max.height); + if state.needs_update { + state.limits = *limits; + state.size = unrestricted_size; + } + + let node = self + .content + .as_widget() + .layout(&mut tree.children[0], renderer, limits); + let size = node.size(); + layout::Node::with_children(size, vec![node]) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn iced_core::widget::Operation<()>, + ) { + operation.container(Some(&self.id), layout.bounds(), &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let state = tree.state.downcast_mut::(); + + if state.needs_update { + shell.publish(Message::from(SurfaceMessage::ResponsiveMenuBar { + menu_bar: self.id.clone(), + limits: state.limits, + size: state.size, + })); + state.needs_update = false; + } + + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let content_layout = layout.children().next().unwrap(); + self.content.as_widget().mouse_interaction( + &tree.children[0], + content_layout, + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + ) { + let content_layout = layout.children().next().unwrap(); + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + content_layout, + cursor_position, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + translation, + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, + ) { + let content_layout = layout.children().next().unwrap(); + self.content.as_widget().drag_destinations( + &state.children[0], + content_layout, + renderer, + dnd_rectangles, + ); + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: crate::widget::Id) { + self.id = id; + } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + let c_layout = layout.children().next().unwrap(); + let c_state = &state.children[0]; + self.content.as_widget().a11y_nodes(c_layout, c_state, p) + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a + From, + Renderer: 'a + iced_core::Renderer, + Theme: 'a, +{ + fn from( + c: ResponsiveContainer<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(c) + } +} + +#[derive(Debug, Clone, Copy)] +struct State { + limits: Limits, + size: Size, + needs_update: bool, +} + +impl State { + fn new() -> Self { + Self { + limits: Limits::NONE, + size: Size::new(0., 0.), + needs_update: false, + } + } +} diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 7841b3ae..f73c5a6b 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -9,7 +9,7 @@ use crate::widget::menu::{ self, menu_roots_children, menu_roots_diff, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, }; -use crate::widget::{icon, Icon}; +use crate::widget::{dropdown, icon, Icon}; use crate::{Element, Renderer}; use derive_setters::Setters; use iced::clipboard::dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}; @@ -127,7 +127,7 @@ where pub(super) style: Style, /// The context menu to display when a context is activated #[setters(skip)] - pub(super) context_menu: Option>>, + pub(super) context_menu: Option>>, /// Emits the ID of the item that was activated. #[setters(skip)] pub(super) on_activate: Option Message + 'static>>, @@ -197,13 +197,13 @@ where } } - pub fn context_menu(mut self, context_menu: Option>>) -> Self + pub fn context_menu(mut self, context_menu: Option>>) -> Self where - Message: 'static, + Message: Clone + 'static, { self.context_menu = context_menu.map(|menus| { vec![menu::Tree::with_children( - crate::widget::row::<'static, Message>(), + crate::Element::from(crate::widget::row::<'static, Message>()), menus, )] }); @@ -592,6 +592,7 @@ where wheel_timestamp: Default::default(), dnd_state: Default::default(), fingers_pressed: Default::default(), + menu_state: Default::default(), }) } @@ -1537,7 +1538,7 @@ where translation: Vector, ) -> Option> { let state = tree.state.downcast_ref::(); - + let menu_state = state.menu_state.clone(); let Some(entity) = state.show_context else { return None; }; @@ -1565,8 +1566,8 @@ where Some( crate::widget::menu::Menu { - tree: &mut tree.children[0], - menu_roots: context_menu, + tree: menu_state, + menu_roots: Cow::Borrowed(context_menu), bounds_expand: 16, menu_overlays_parent: true, close_condition: CloseCondition { @@ -1581,7 +1582,7 @@ where cross_offset: 0, root_bounds_list: vec![bounds], path_highlight: Some(PathHighlight::MenuActive), - style: &crate::theme::menu_bar::MenuBarStyle::Default, + style: Cow::Owned(crate::theme::menu_bar::MenuBarStyle::Default), position: Point::new(translation.x, translation.y), } .overlay(), @@ -1666,6 +1667,8 @@ pub struct LocalState { pub dnd_state: crate::widget::dnd_destination::State>, /// Tracks multi-touch events fingers_pressed: HashSet, + /// Menu state + pub(crate) menu_state: dropdown::menu::State, } #[derive(Debug, Default, PartialEq)] diff --git a/src/widget/wayland/tooltip/button_popup.rs b/src/widget/wayland/tooltip/button_popup.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/widget/wayland/tooltip/dropdown.rs b/src/widget/wayland/tooltip/dropdown.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs new file mode 100644 index 00000000..cb1fe673 --- /dev/null +++ b/src/widget/wrapper.rs @@ -0,0 +1,155 @@ +use std::sync::{Arc, Mutex}; + +use crate::Element; +use iced::{event, Length, Rectangle, Size}; +use iced_core::{id::Id, widget, widget::tree, Widget}; + +#[derive(Clone)] +pub struct ArcElementWrapper(pub Arc>>); + +impl Widget for ArcElementWrapper { + fn size(&self) -> Size { + self.0.lock().unwrap().as_widget().size() + } + + fn size_hint(&self) -> Size { + self.0.lock().unwrap().as_widget().size_hint() + } + + fn layout( + &self, + tree: &mut tree::Tree, + renderer: &crate::Renderer, + limits: &crate::iced_core::layout::Limits, + ) -> crate::iced_core::layout::Node { + self.0 + .lock() + .unwrap() + .as_widget_mut() + .layout(tree, renderer, limits) + } + + fn draw( + &self, + tree: &tree::Tree, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &crate::iced_core::renderer::Style, + layout: crate::iced_core::Layout<'_>, + cursor: crate::iced_core::mouse::Cursor, + viewport: &Rectangle, + ) { + self.0 + .lock() + .unwrap() + .as_widget() + .draw(tree, renderer, theme, style, layout, cursor, viewport) + } + + fn tag(&self) -> tree::Tag { + self.0.lock().unwrap().as_widget().tag() + } + + fn state(&self) -> tree::State { + self.0.lock().unwrap().as_widget().state() + } + + fn children(&self) -> Vec { + self.0.lock().unwrap().as_widget().children() + } + + fn diff(&mut self, tree: &mut tree::Tree) { + self.0.lock().unwrap().as_widget_mut().diff(tree) + } + + fn operate( + &self, + state: &mut tree::Tree, + layout: crate::iced_core::Layout<'_>, + renderer: &crate::Renderer, + operation: &mut dyn widget::Operation, + ) { + self.0 + .lock() + .unwrap() + .as_widget() + .operate(state, layout, renderer, operation) + } + + fn on_event( + &mut self, + _state: &mut tree::Tree, + _event: crate::iced::Event, + _layout: crate::iced_core::Layout<'_>, + _cursor: crate::iced_core::mouse::Cursor, + _renderer: &crate::Renderer, + _clipboard: &mut dyn crate::iced_core::Clipboard, + _shell: &mut crate::iced_core::Shell<'_, M>, + _viewport: &Rectangle, + ) -> event::Status { + self.0.lock().unwrap().as_widget_mut().on_event( + _state, _event, _layout, _cursor, _renderer, _clipboard, _shell, _viewport, + ) + } + + fn mouse_interaction( + &self, + _state: &tree::Tree, + _layout: crate::iced_core::Layout<'_>, + _cursor: crate::iced_core::mouse::Cursor, + _viewport: &Rectangle, + _renderer: &crate::Renderer, + ) -> crate::iced_core::mouse::Interaction { + self.0 + .lock() + .unwrap() + .as_widget() + .mouse_interaction(_state, _layout, _cursor, _viewport, _renderer) + } + + fn overlay<'a>( + &'a mut self, + _state: &'a mut tree::Tree, + _layout: crate::iced_core::Layout<'_>, + _renderer: &crate::Renderer, + _translation: crate::iced_core::Vector, + ) -> Option> { + // TODO + None + } + + fn id(&self) -> Option { + self.0.lock().unwrap().as_widget().id() + } + + fn set_id(&mut self, _id: Id) { + self.0.lock().unwrap().as_widget_mut().set_id(_id) + } + + fn drag_destinations( + &self, + _state: &tree::Tree, + _layout: crate::iced_core::Layout<'_>, + renderer: &crate::Renderer, + _dnd_rectangles: &mut crate::iced_core::clipboard::DndDestinationRectangles, + ) { + self.0.lock().unwrap().as_widget().drag_destinations( + _state, + _layout, + renderer, + _dnd_rectangles, + ) + } +} + +impl From> for Element<'static, Message> { + fn from(wrapper: ArcElementWrapper) -> Self { + Element::new(wrapper) + } +} + +impl From> for ArcElementWrapper { + fn from(e: Element<'static, Message>) -> Self { + ArcElementWrapper(Arc::new(Mutex::new(e))) + } +}