diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 3c5ce8e..e5ae2f3 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -25,4 +25,5 @@ features = [ "wgpu", "single-instance", "multi-window", + "surface-message", ] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 92c2c24..c70a9d3 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -46,13 +46,19 @@ impl Page { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Action { Hi, + Hi2, + Hi3, } impl MenuAction for Action { type Message = Message; fn message(&self) -> Message { - Message::Hi + match self { + Action::Hi => Message::Hi, + Action::Hi2 => Message::Hi2, + Action::Hi3 => Message::Hi3, + } } } @@ -86,6 +92,8 @@ pub enum Message { ToggleHide, Surface(cosmic::surface::Action), Hi, + Hi2, + Hi3, } /// The [`App`] stores application-specific state. @@ -176,6 +184,12 @@ impl cosmic::Application for App { Message::Hi => { dbg!("hi"); } + Message::Hi2 => { + dbg!("hi 2"); + } + Message::Hi3 => { + dbg!("hi 3"); + } } Task::none() } @@ -221,119 +235,80 @@ impl cosmic::Application for App { } fn header_start(&self) -> Vec> { - use cosmic::widget::menu::Tree; - #[cfg(not(feature = "wayland"))] - { - vec![cosmic::widget::menu::bar(vec![ - Tree::with_children( - menu::root("hiiiiiiiiiiiiiiiiiii 1"), - menu::items( - &self.keybinds, - vec![menu::Item::Button("hi", None, Action::Hi)], - ), + vec![cosmic::widget::responsive_menu_bar().into_element( + self.core(), + &self.keybinds, + MENU_ID.clone(), + Message::Surface, + vec![ + ( + "hi 1".into(), + vec![ + menu::Item::Button("hi 12", None, Action::Hi), + menu::Item::Button("hi 13", None, Action::Hi2), + ], ), - Tree::with_children( - menu::root("hiiiiiiiiiiiiiiiiii 2"), - menu::items( - &self.keybinds, - vec![menu::Item::Button("hi 2", None, Action::Hi)], - ), + ( + "hi 2".into(), + vec![ + menu::Item::Button("hi 21", None, Action::Hi), + menu::Item::Button("hi 22", None, Action::Hi2), + menu::Item::Folder( + "nest 3 2 >".into(), + vec![ + menu::Item::Button("21", None, Action::Hi), + menu::Item::Button("242", None, Action::Hi2), + menu::Item::Button("2443", None, Action::Hi3), + menu::Item::Folder( + "nest 4 2 >".into(), + vec![ + menu::Item::Button("243", None, Action::Hi2), + menu::Item::Button("2444", None, Action::Hi), + ], + ), + ], + ), + ], ), - Tree::with_children( - menu::root("hiiiiiiiiiiiiiiiiiiiii 3"), - menu::items( - &self.keybinds, - vec![ - menu::Item::Button("hi 3", None, Action::Hi), - menu::Item::Button("hi 3 #2", None, Action::Hi), - ], - ), + ( + "hi 3".into(), + vec![ + menu::Item::Button("hi 31", None, Action::Hi), + menu::Item::Button("hi 332", None, Action::Hi2), + menu::Item::Button("hi 3333", None, Action::Hi3), + menu::Item::Button("hi 33334", None, Action::Hi3), + menu::Item::Button("hi 333335", None, Action::Hi3), + menu::Item::Button("hi 3333336", None, Action::Hi3), + ], ), - Tree::with_children( - menu::root("hi 3"), - menu::items( - &self.keybinds, - vec![ - menu::Item::Button("hi 3", None, Action::Hi), - menu::Item::Button("hi 3 #2", None, Action::Hi), - menu::Item::Button("hi 3 #3", None, Action::Hi), - ], - ), + ( + "hiiiiiiiiiiiiiiiiiii 4".into(), + vec![ + menu::Item::Button("hi 4", None, Action::Hi), + menu::Item::Button("hi 44", None, Action::Hi2), + menu::Item::Button("hi 444", None, Action::Hi3), + menu::Item::Folder( + "nest 4 >".into(), + vec![ + menu::Item::Button("hi 41", None, Action::Hi), + menu::Item::Button("hi 442", None, Action::Hi2), + menu::Item::Folder( + "nest 3 4 >".into(), + vec![ + menu::Item::Button("hi 443", None, Action::Hi2), + menu::Item::Button("hi 4444", None, Action::Hi), + menu::Item::Button("hi 44444", None, Action::Hi3), + menu::Item::Button("hi 444445", None, Action::Hi3), + menu::Item::Button("hi 4444446", None, Action::Hi3), + menu::Item::Button("hi 44444447", None, Action::Hi3), + ], + ), + ], + ), + ], ), - Tree::with_children( - menu::root("hi 4"), - menu::items( - &self.keybinds, - vec![ - menu::Item::Folder( - "hi 41 extra root", - vec![menu::Item::Button("hi 3", None, Action::Hi)], - ), - menu::Item::Button("hi 42", None, Action::Hi), - menu::Item::Button("hi 43", None, Action::Hi), - menu::Item::Button("hi 44", None, Action::Hi), - menu::Item::Button("hi 45", None, Action::Hi), - menu::Item::Button("hi 46", None, Action::Hi), - ], - ), - ), - ]) - .into()] - } - #[cfg(feature = "wayland")] - { - vec![cosmic::widget::responsive_menu_bar().into_element( - self.core(), - &self.keybinds, - MENU_ID.clone(), - Message::Surface, - vec![ - ( - "hiiiiiiiiiiiiiiiiiii 1", - vec![menu::Item::Button("hi 1", None, Action::Hi)], - ), - ( - "hiiiiiiiiiiiiiiiiiii 2".into(), - vec![ - menu::Item::Button("hi 2", None, Action::Hi), - menu::Item::Button("hi 22", None, Action::Hi), - ], - ), - ( - "hiiiiiiiiiiiiiiiiiii 3".into(), - vec![ - menu::Item::Button("hi 3", None, Action::Hi), - menu::Item::Button("hi 33", None, Action::Hi), - menu::Item::Button("hi 333", None, Action::Hi), - ], - ), - ( - "hiiiiiiiiiiiiiiiiiii 4".into(), - vec![ - menu::Item::Button("hi 4", None, Action::Hi), - menu::Item::Button("hi 44", None, Action::Hi), - menu::Item::Button("hi 444", None, Action::Hi), - menu::Item::Folder( - "nest 4".into(), - vec![ - menu::Item::Button("hi 4", None, Action::Hi), - menu::Item::Button("hi 44", None, Action::Hi), - menu::Item::Button("hi 444", None, Action::Hi), - menu::Item::Folder( - "nest 2 4".into(), - vec![ - menu::Item::Button("hi 4", None, Action::Hi), - menu::Item::Button("hi 44", None, Action::Hi), - menu::Item::Button("hi 444", None, Action::Hi), - ], - ), - ], - ), - ], - ), - ], - )] - } + ], + )] } } diff --git a/examples/calendar/src/main.rs b/examples/calendar/src/main.rs index c73c4da..47549a7 100644 --- a/examples/calendar/src/main.rs +++ b/examples/calendar/src/main.rs @@ -92,6 +92,7 @@ impl cosmic::Application for App { |date| Message::DateSelected(date), || Message::PrevMonth, || Message::NextMonth, + chrono::Weekday::Sun, ); content = content.push(calendar); diff --git a/examples/context-menu/Cargo.toml b/examples/context-menu/Cargo.toml index 5b9ad02..9a24a1c 100644 --- a/examples/context-menu/Cargo.toml +++ b/examples/context-menu/Cargo.toml @@ -11,4 +11,12 @@ tracing-log = "0.2.0" [dependencies.libcosmic] path = "../../" default-features = false -features = ["debug", "winit", "tokio", "xdg-portal", "multi-window"] +features = [ + "debug", + "winit", + "tokio", + "xdg-portal", + "multi-window", + "surface-message", + "wayland", +] diff --git a/examples/context-menu/src/main.rs b/examples/context-menu/src/main.rs index 840cf86..4a30784 100644 --- a/examples/context-menu/src/main.rs +++ b/examples/context-menu/src/main.rs @@ -93,7 +93,7 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element { let widget = cosmic::widget::context_menu( - cosmic::widget::button::text(&self.button_label).on_press(Message::Clicked), + cosmic::widget::button::text(self.button_label.to_string()).on_press(Message::Clicked), self.context_menu(), ); diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs index 5b65732..7037a62 100644 --- a/examples/menu/src/main.rs +++ b/examples/menu/src/main.rs @@ -15,6 +15,7 @@ use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::key_bind::Modifier; use cosmic::widget::menu::{self, ItemHeight, ItemWidth}; +use cosmic::widget::RcElementWrapper; use cosmic::{executor, Element}; /// Runs application with these settings @@ -155,7 +156,7 @@ impl cosmic::Application for App { pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap) -> Element<'a, Message> { menu::bar(vec![menu::Tree::with_children( - menu::root("File"), + RcElementWrapper::new(Element::from(menu::root("File"))), menu::items( key_binds, vec![ diff --git a/examples/table-view/src/main.rs b/examples/table-view/src/main.rs index 8b2d4f6..6bd773b 100644 --- a/examples/table-view/src/main.rs +++ b/examples/table-view/src/main.rs @@ -209,11 +209,11 @@ impl cosmic::Application for App { if size.width < 600.0 { widget::compact_table(&self.table_model) .on_item_left_click(Message::ItemSelect) - .item_context(|item| { + .item_context(move |item| { Some(widget::menu::items( &HashMap::new(), vec![widget::menu::Item::Button( - format!("Action on {}", item.name), + format!("Action on {}", item.name.to_string()), None, Action::None, )], diff --git a/iced b/iced index 70d104a..717bc5d 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 70d104a28a87f06eb46d76268b6fa18c407ee2c2 +Subproject commit 717bc5dbfbc8f78e367e08e76a9572ee0ebc1f32 diff --git a/src/surface/action.rs b/src/surface/action.rs index e27815e..fdf2680 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -85,7 +85,7 @@ pub fn simple_subsurface( /// Used to create a popup message from within a widget. #[cfg(all(feature = "wayland", feature = "winit"))] #[must_use] -pub fn simple_popup( +pub fn simple_popup( settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index 7f99a1a..ed0e657 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -42,7 +42,7 @@ pub enum MenuBarStyle { #[default] Default, /// A [`Theme`] that uses a `Custom` palette. - Custom(Arc>), + Custom(Arc + Send + Sync>), } impl From Appearance> for MenuBarStyle { diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 5a1da45..aa8f0c3 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -460,7 +460,6 @@ impl<'a, Message: 'a + Clone> Widget if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) { headerbar_alpha = Some(0.8); } - theme.hovered(state.is_focused, self.selected, &self.style) } } else { diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 8712202..6769dff 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -4,26 +4,26 @@ //! A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. use crate::widget::menu::{ - self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, + self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, menu_roots_diff, }; use derive_setters::Setters; use iced::touch::Finger; -use iced::{Event, Vector}; +use iced::{Event, Vector, window}; use iced_core::widget::{Tree, Widget, tree}; use iced_core::{Length, Point, Size, event, mouse, touch}; use std::collections::HashSet; /// 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>(), + crate::Element::from(crate::widget::row::<'static, Message>()), menus, )] }), @@ -43,10 +43,12 @@ pub struct ContextMenu<'a, Message> { #[setters(skip)] content: crate::Element<'a, Message>, #[setters(skip)] - context_menu: Option>>, + context_menu: Option>>, } -impl Widget for ContextMenu<'_, Message> { +impl Widget + for ContextMenu<'_, Message> +{ fn tag(&self) -> tree::Tag { tree::Tag::of::() } @@ -56,6 +58,7 @@ impl Widget for ContextM tree::State::new(LocalState { context_cursor: Point::default(), fingers_pressed: Default::default(), + menu_bar_state: Default::default(), }) } @@ -67,7 +70,6 @@ impl Widget for ContextM // Assign the context menu's elements as this widget's children. if let Some(ref context_menu) = self.context_menu { let mut tree = Tree::empty(); - tree.state = tree::State::new(MenuBarState::default()); tree.children = context_menu .iter() .map(|root| { @@ -75,7 +77,7 @@ impl Widget for ContextM let flat = root .flattern() .iter() - .map(|mt| Tree::new(mt.item.as_widget())) + .map(|mt| Tree::new(mt.item.clone())) .collect(); tree.children = flat; tree @@ -90,6 +92,10 @@ impl Widget for ContextM fn diff(&mut self, tree: &mut Tree) { tree.children[0].diff(self.content.as_widget_mut()); + let state = tree.state.downcast_mut::(); + state.menu_bar_state.inner.with_data_mut(|inner| { + menu_roots_diff(self.context_menu.as_mut().unwrap(), &mut inner.tree); + }); // if let Some(ref mut context_menus) = self.context_menu { // for (menu, tree) in context_menus @@ -183,10 +189,12 @@ impl Widget for ContextM && (right_button_released(&event) || (touch_lifted(&event) && fingers_pressed == 2)) { state.context_cursor = cursor.position().unwrap_or_default(); + let state = tree.state.downcast_mut::(); - let menu_state = tree.children[1].state.downcast_mut::(); - menu_state.open = true; - menu_state.view_cursor = cursor; + state.menu_bar_state.inner.with_data_mut(|state| { + state.open = true; + state.view_cursor = cursor; + }); return event::Status::Captured; } @@ -213,22 +221,19 @@ impl Widget for ContextM ) -> Option> { let state = tree.state.downcast_ref::(); - let Some(context_menu) = self.context_menu.as_mut() else { - return None; - }; + let context_menu = self.context_menu.as_mut()?; - if !tree.children[1].state.downcast_ref::().open { + if !state.menu_bar_state.inner.with_data(|state| state.open) { return None; } let mut bounds = layout.bounds(); bounds.x = state.context_cursor.x; bounds.y = state.context_cursor.y; - Some( crate::widget::menu::Menu { - tree: &mut tree.children[1], - menu_roots: context_menu, + tree: state.menu_bar_state.clone(), + menu_roots: std::borrow::Cow::Owned(context_menu.clone()), bounds_expand: 16, menu_overlays_parent: true, close_condition: CloseCondition { @@ -243,8 +248,12 @@ impl Widget for ContextM cross_offset: 0, root_bounds_list: vec![bounds], path_highlight: Some(PathHighlight::MenuActive), - style: &crate::theme::menu_bar::MenuBarStyle::Default, + style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default), position: Point::new(translation.x, translation.y), + is_overlay: true, + window_id: window::Id::NONE, + depth: 0, + on_surface_action: None, } .overlay(), ) @@ -263,8 +272,10 @@ impl Widget for ContextM } } -impl<'a, Message: Clone + 'a> From> for crate::Element<'a, Message> { - fn from(widget: ContextMenu<'a, Message>) -> Self { +impl<'a, Message: Clone + 'static> From> + for crate::Element<'static, Message> +{ + fn from(widget: ContextMenu<'static, Message>) -> Self { Self::new(widget) } } @@ -283,4 +294,5 @@ fn touch_lifted(event: &Event) -> bool { pub struct LocalState { context_cursor: Point, fingers_pressed: HashSet, + menu_bar_state: MenuBarState, } diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 76ee7b0..d196215 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -536,15 +536,7 @@ pub fn update< let on_close = surface::action::destroy_popup(id); let on_surface_action_clone = on_surface_action.clone(); let translation = layout.virtual_offset(); - let get_popup_action = surface::action::simple_popup::< - AppMessage, - Box< - dyn Fn() -> Element<'static, crate::Action> - + Send - + Sync - + 'static, - >, - >( + let get_popup_action = surface::action::simple_popup::( move || { SctkPopupSettings { parent, diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs index c093e80..5eaf3d9 100644 --- a/src/widget/menu/flex.rs +++ b/src/widget/menu/flex.rs @@ -1,12 +1,14 @@ // From iced_aw, license MIT -use iced_core::widget::Tree; +use iced_core::{Widget, widget::Tree}; use iced_widget::core::{ Alignment, Element, Padding, Point, Size, layout::{Limits, Node}, renderer, }; +use crate::widget::RcElementWrapper; + /// The main axis of a flex layout. #[derive(Debug)] pub enum Axis { @@ -217,3 +219,170 @@ where Node::with_children(size.expand(padding), nodes) } + +/// Computes the flex layout with the given axis and limits, applying spacing, +/// padding and alignment to the items as needed. +/// +/// It returns a new layout [`Node`]. +pub fn resolve_wrapper<'a, Message>( + axis: &Axis, + renderer: &crate::Renderer, + limits: &Limits, + padding: Padding, + spacing: f32, + align_items: Alignment, + items: &[&RcElementWrapper], + tree: &mut [&mut Tree], +) -> Node { + let limits = limits.shrink(padding); + let total_spacing = spacing * items.len().saturating_sub(1) as f32; + let max_cross = axis.cross(limits.max()); + + let mut fill_sum = 0; + let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITY)); + let mut available = axis.main(limits.max()) - total_spacing; + + let mut nodes: Vec = Vec::with_capacity(items.len()); + nodes.resize(items.len(), Node::default()); + + if align_items == Alignment::Center { + let mut fill_cross = axis.cross(limits.min()); + + for (child, tree) in items.iter().zip(tree.iter_mut()) { + let c_size = child.size(); + let cross_fill_factor = match axis { + Axis::Horizontal => c_size.height, + Axis::Vertical => c_size.width, + } + .fill_factor(); + + if cross_fill_factor == 0 { + let (max_width, max_height) = axis.pack(available, max_cross); + + let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); + + let layout = child.layout(tree, renderer, &child_limits); + let size = layout.size(); + + fill_cross = fill_cross.max(axis.cross(size)); + } + } + + cross = fill_cross; + } + + for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { + let c_size = child.size(); + let fill_factor = match axis { + Axis::Horizontal => c_size.width, + Axis::Vertical => c_size.height, + } + .fill_factor(); + + if fill_factor == 0 { + let (min_width, min_height) = if align_items == Alignment::Center { + axis.pack(0.0, cross) + } else { + axis.pack(0.0, 0.0) + }; + + let (max_width, max_height) = if align_items == Alignment::Center { + axis.pack(available, cross) + } else { + axis.pack(available, max_cross) + }; + + let child_limits = Limits::new( + Size::new(min_width, min_height), + Size::new(max_width, max_height), + ); + + let layout = child.layout(tree, renderer, &child_limits); + let size = layout.size(); + + available -= axis.main(size); + + if align_items != Alignment::Center { + cross = cross.max(axis.cross(size)); + } + + nodes[i] = layout; + } else { + fill_sum += fill_factor; + } + } + + let remaining = available.max(0.0); + + for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { + let c_size = child.size(); + let fill_factor = match axis { + Axis::Horizontal => c_size.width, + Axis::Vertical => c_size.height, + } + .fill_factor(); + + if fill_factor != 0 { + let max_main = remaining * f32::from(fill_factor) / f32::from(fill_sum); + let min_main = if max_main.is_infinite() { + 0.0 + } else { + max_main + }; + + let (min_width, min_height) = if align_items == Alignment::Center { + axis.pack(min_main, cross) + } else { + axis.pack(min_main, axis.cross(limits.min())) + }; + + let (max_width, max_height) = if align_items == Alignment::Center { + axis.pack(max_main, cross) + } else { + axis.pack(max_main, max_cross) + }; + + let child_limits = Limits::new( + Size::new(min_width, min_height), + Size::new(max_width, max_height), + ); + + let layout = child.layout(tree, renderer, &child_limits); + + if align_items != Alignment::Center { + cross = cross.max(axis.cross(layout.size())); + } + + nodes[i] = layout; + } + } + + let pad = axis.pack(padding.left, padding.top); + let mut main = pad.0; + + for (i, node) in nodes.iter_mut().enumerate() { + if i > 0 { + main += spacing; + } + + let (x, y) = axis.pack(main, pad.1); + + let node_ = node.clone().move_to(Point::new(x, y)); + + let node_ = match axis { + Axis::Horizontal => node_.align(Alignment::Start, align_items, Size::new(0.0, cross)), + Axis::Vertical => node_.align(align_items, Alignment::Start, Size::new(cross, 0.0)), + }; + + let size = node_.bounds().size(); + + *node = node_; + + main += axis.main(size); + } + + let (width, height) = axis.pack(main - pad.0, cross); + let size = limits.resolve(width, height, Size::new(width, height)); + + Node::with_children(size.expand(padding), nodes) +} diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 2584e76..eddc4f3 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -1,73 +1,98 @@ // From iced_aw, license MIT //! A widget that handles menu trees +use std::{collections::HashMap, sync::Arc}; + use super::{ menu_inner::{ CloseCondition, Direction, ItemHeight, ItemWidth, Menu, MenuState, PathHighlight, }, menu_tree::MenuTree, }; -use crate::style::menu_bar::StyleSheet; +use crate::{ + Renderer, + style::menu_bar::StyleSheet, + widget::{ + RcWrapper, + dropdown::menu::{self, State}, + menu::menu_inner::init_root_menu, + }, +}; -use iced::{Point, Vector}; +use iced::{Point, Shadow, Vector, window}; use iced_core::Border; use iced_widget::core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, event, layout::{Limits, Node}, mouse::{self, Cursor}, - overlay, renderer, touch, + overlay, + renderer::{self, Renderer as IcedRenderer}, + touch, widget::{Tree, tree}, }; /// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing. -pub fn menu_bar( - menu_roots: Vec>, -) -> MenuBar { +pub fn menu_bar(menu_roots: Vec>) -> MenuBar +where + Message: Clone + 'static, +{ MenuBar::new(menu_roots) } +#[derive(Clone, Default)] pub(crate) struct MenuBarState { + pub(crate) inner: RcWrapper, +} + +pub(crate) struct MenuBarStateInner { + pub(crate) tree: Tree, + pub(crate) popup_id: HashMap, pub(crate) pressed: bool, + pub(crate) bar_pressed: bool, pub(crate) view_cursor: Cursor, pub(crate) open: bool, - pub(crate) active_root: Option, + pub(crate) active_root: Vec, pub(crate) horizontal_direction: Direction, pub(crate) vertical_direction: Direction, + /// List of all menu states pub(crate) menu_states: Vec, } -impl MenuBarState { - pub(super) fn get_trimmed_indices(&self) -> impl Iterator + '_ { +impl MenuBarStateInner { + /// get the list of indices hovered for the menu + pub(super) fn get_trimmed_indices(&self, index: usize) -> impl Iterator + '_ { self.menu_states .iter() + .skip(index) .take_while(|ms| ms.index.is_some()) .map(|ms| ms.index.expect("No indices were found in the menu state.")) } pub(super) fn reset(&mut self) { self.open = false; - self.active_root = None; + self.active_root = Vec::new(); self.menu_states.clear(); } } -impl Default for MenuBarState { +impl Default for MenuBarStateInner { fn default() -> Self { Self { + tree: Tree::empty(), pressed: false, view_cursor: Cursor::Available([-0.5, -0.5].into()), open: false, - active_root: None, + active_root: Vec::new(), horizontal_direction: Direction::Positive, vertical_direction: Direction::Positive, menu_states: Vec::new(), + popup_id: HashMap::new(), + bar_pressed: false, } } } -pub(crate) fn menu_roots_children( - menu_roots: &Vec>, -) -> Vec +pub(crate) fn menu_roots_children(menu_roots: &Vec>) -> Vec where - Renderer: renderer::Renderer, + Message: Clone + 'static, { /* menu bar @@ -85,7 +110,7 @@ where let flat = root .flattern() .iter() - .map(|mt| Tree::new(mt.item.as_widget())) + .map(|mt| Tree::new(mt.item.clone())) .collect(); tree.children = flat; tree @@ -94,11 +119,9 @@ where } #[allow(invalid_reference_casting)] -pub(crate) fn menu_roots_diff( - menu_roots: &mut Vec>, - tree: &mut Tree, -) where - Renderer: renderer::Renderer, +pub(crate) fn menu_roots_diff(menu_roots: &mut Vec>, tree: &mut Tree) +where + Message: Clone + 'static, { if tree.children.len() > menu_roots.len() { tree.children.truncate(menu_roots.len()); @@ -112,7 +135,7 @@ pub(crate) fn menu_roots_diff( .flattern() .iter() .map(|mt| { - let widget = mt.item.as_widget(); + let widget = &mt.item; let widget_ptr = widget as *const dyn Widget; let widget_ptr_mut = widget_ptr as *mut dyn Widget; @@ -130,7 +153,7 @@ pub(crate) fn menu_roots_diff( let flat = root .flattern() .iter() - .map(|mt| Tree::new(mt.item.as_widget())) + .map(|mt| Tree::new(mt.item.clone())) .collect(); tree.children = flat; tree @@ -139,12 +162,18 @@ pub(crate) fn menu_roots_diff( } } +pub fn get_mut_or_default(vec: &mut Vec, index: usize) -> &mut T { + if index < vec.len() { + &mut vec[index] + } else { + vec.resize_with(index + 1, T::default); + &mut vec[index] + } +} + /// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing. #[allow(missing_debug_implementations)] -pub struct MenuBar<'a, Message, Renderer = crate::Renderer> -where - Renderer: renderer::Renderer, -{ +pub struct MenuBar { width: Length, height: Length, spacing: f32, @@ -156,17 +185,22 @@ where item_width: ItemWidth, item_height: ItemHeight, path_highlight: Option, - menu_roots: Vec>, + menu_roots: Vec>, style: ::Style, + window_id: window::Id, + #[cfg(all(feature = "wayland", feature = "winit"))] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + pub(crate) on_surface_action: + Option Message + Send + Sync + 'static>>, } -impl<'a, Message, Renderer> MenuBar<'a, Message, Renderer> +impl MenuBar where - Renderer: renderer::Renderer, + Message: Clone + 'static, { /// Creates a new [`MenuBar`] with the given menu roots #[must_use] - pub fn new(menu_roots: Vec>) -> Self { + pub fn new(menu_roots: Vec>) -> Self { let mut menu_roots = menu_roots; menu_roots.iter_mut().for_each(MenuTree::set_index); @@ -188,6 +222,10 @@ where path_highlight: Some(PathHighlight::MenuActive), menu_roots, style: ::Style::default(), + window_id: window::Id::NONE, + #[cfg(all(feature = "wayland", feature = "winit"))] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), + on_surface_action: None, } } @@ -278,17 +316,196 @@ where self.width = width; self } + + #[cfg(all(feature = "wayland", feature = "winit"))] + pub fn with_positioner( + mut self, + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + ) -> Self { + self.positioner = positioner; + self + } + + #[must_use] + pub fn window_id(mut self, id: window::Id) -> Self { + self.window_id = id; + self + } + + #[must_use] + pub fn window_id_maybe(mut self, id: Option) -> Self { + if let Some(id) = id { + self.window_id = id; + } + self + } + + #[must_use] + pub fn on_surface_action( + mut self, + handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, + ) -> Self { + self.on_surface_action = Some(Arc::new(handler)); + self + } + + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[allow(clippy::too_many_lines)] + fn create_popup( + &mut self, + layout: Layout<'_>, + view_cursor: Cursor, + renderer: &Renderer, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + my_state: &mut MenuBarState, + ) { + if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { + use crate::surface::action::destroy_popup; + use iced_runtime::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, + }; + + let surface_action = self.on_surface_action.as_ref().unwrap(); + let old_active_root = my_state + .inner + .with_data(|state| state.active_root.get(0).copied()); + + // if position is not on menu bar button skip. + let hovered_root = layout + .children() + .position(|lo| view_cursor.is_over(lo.bounds())); + + if old_active_root + .zip(hovered_root) + .is_some_and(|r| r.0 == r.1) + { + return; + } + let (id, root_list) = my_state.inner.with_data_mut(|state| { + if let Some(id) = state.popup_id.get(&self.window_id).copied() { + // close existing popups + state.menu_states.clear(); + state.active_root.clear(); + shell.publish(surface_action(destroy_popup(id))); + state.view_cursor = view_cursor; + (id, layout.children().map(|lo| lo.bounds()).collect()) + } else { + ( + window::Id::unique(), + layout.children().map(|lo| lo.bounds()).collect(), + ) + } + }); + + let mut popup_menu: Menu<'static, _> = Menu { + tree: my_state.clone(), + menu_roots: std::borrow::Cow::Owned(self.menu_roots.clone()), + bounds_expand: self.bounds_expand, + menu_overlays_parent: false, + close_condition: self.close_condition, + item_width: self.item_width, + item_height: self.item_height, + bar_bounds: layout.bounds(), + main_offset: self.main_offset, + cross_offset: self.cross_offset, + root_bounds_list: root_list, + path_highlight: self.path_highlight, + style: std::borrow::Cow::Owned(self.style.clone()), + position: Point::new(0., 0.), + is_overlay: false, + window_id: id, + depth: 0, + on_surface_action: self.on_surface_action.clone(), + }; + + init_root_menu( + &mut popup_menu, + renderer, + shell, + view_cursor.position().unwrap(), + viewport.size(), + Vector::new(0., 0.), + layout.bounds(), + self.main_offset as f32, + ); + let (anchor_rect, gravity) = my_state.inner.with_data_mut(|state| { + state.popup_id.insert(self.window_id, id); + (state + .menu_states + .iter() + .find(|s| s.index.is_none()) + .map(|s| s.menu_bounds.parent_bounds) + .map_or_else( + || { + let bounds = layout.bounds(); + Rectangle { + x: bounds.x as i32, + y: bounds.y as i32, + width: bounds.width as i32, + height: bounds.height as i32, + } + }, + |r| Rectangle { + x: r.x as i32, + y: r.y as i32, + width: r.width as i32, + height: r.height as i32, + }, + ), match (state.horizontal_direction, state.vertical_direction) { + (Direction::Positive, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, + (Direction::Positive, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight, + (Direction::Negative, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft, + (Direction::Negative, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft, + }) + }); + + let menu_node = popup_menu.layout(renderer, Limits::NONE.min_width(1.).min_height(1.)); + let popup_size = menu_node.size(); + let positioner = SctkPositioner { + size: Some(( + popup_size.width.ceil() as u32 + 2, + popup_size.height.ceil() as u32 + 2, + )), + anchor_rect, + anchor: + cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft, + gravity, + reactive: true, + ..Default::default() + }; + let parent = self.window_id; + shell.publish((surface_action)(crate::surface::action::simple_popup( + move || SctkPopupSettings { + parent, + id, + positioner: positioner.clone(), + parent_size: None, + grab: true, + close_with_children: false, + input_zone: None, + }, + Some(move || { + Element::from(crate::widget::container(popup_menu.clone()).center(Length::Fill)) + .map(crate::action::app) + }), + ))); + } + } } -impl Widget for MenuBar<'_, Message, Renderer> +impl Widget for MenuBar where - Renderer: renderer::Renderer, + Message: Clone + 'static, { fn size(&self) -> iced_core::Size { iced_core::Size::new(self.width, self.height) } fn diff(&mut self, tree: &mut Tree) { - menu_roots_diff(&mut self.menu_roots, tree); + let state = tree.state.downcast_mut::(); + state + .inner + .with_data_mut(|inner| menu_roots_diff(&mut self.menu_roots, &mut inner.tree)); } fn tag(&self) -> tree::Tag { @@ -318,7 +535,7 @@ where .iter_mut() .map(|t| &mut t.children[0]) .collect::>(); - flex::resolve( + flex::resolve_wrapper( &flex::Axis::Horizontal, renderer, &limits, @@ -330,6 +547,7 @@ where ) } + #[allow(clippy::too_many_lines)] fn on_event( &mut self, tree: &mut Tree, @@ -357,19 +575,70 @@ where viewport, ); - let state = tree.state.downcast_mut::(); + let my_state = tree.state.downcast_mut::(); + + // XXX this should reset the state if there are no other copies of the state, which implies no dropdown menus open. + let reset = self.window_id != window::Id::NONE + && my_state + .inner + .with_data(|d| !d.open && !d.active_root.is_empty()); + + let open = my_state.inner.with_data_mut(|state| { + if reset { + if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { + if let Some(handler) = self.on_surface_action.as_ref() { + shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); + state.reset(); + } + } + } + state.open + }); match event { Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => { - 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 + let create_popup = my_state.inner.with_data_mut(|state| { + let mut create_popup = false; + if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) { + state.view_cursor = view_cursor; + state.open = true; + create_popup = true; + } else if let Some(_id) = state.popup_id.remove(&self.window_id) { + state.menu_states.clear(); + state.active_root.clear(); + state.open = false; + #[cfg(all( + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] + { + let surface_action = self.on_surface_action.as_ref().unwrap(); + + shell.publish(surface_action(crate::surface::action::destroy_popup( + _id, + ))); + } + state.view_cursor = view_cursor; + } + create_popup + }); + + if !create_popup { + return event::Status::Ignored; } + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); + } + Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) + if open && view_cursor.is_over(layout.bounds()) => + { + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); } _ => (), } + root_status } @@ -385,49 +654,51 @@ where ) { let state = tree.state.downcast_ref::(); let cursor_pos = view_cursor.position().unwrap_or_default(); - let position = if state.open && (cursor_pos.x < 0.0 || cursor_pos.y < 0.0) { - state.view_cursor - } else { - view_cursor - }; + state.inner.with_data_mut(|state| { + let position = if state.open && (cursor_pos.x < 0.0 || cursor_pos.y < 0.0) { + state.view_cursor + } else { + view_cursor + }; - // draw path highlight - if self.path_highlight.is_some() { - let styling = theme.appearance(&self.style); - if let Some(active) = state.active_root { - let active_bounds = layout - .children() - .nth(active) - .expect("Active child not found in menu?") - .bounds(); - let path_quad = renderer::Quad { - bounds: active_bounds, - border: Border { - radius: styling.bar_border_radius.into(), - ..Default::default() - }, - shadow: Default::default(), - }; + // draw path highlight + if self.path_highlight.is_some() { + let styling = theme.appearance(&self.style); + if let Some(active) = state.active_root.first() { + let active_bounds = layout + .children() + .nth(*active) + .expect("Active child not found in menu?") + .bounds(); + let path_quad = renderer::Quad { + bounds: active_bounds, + border: Border { + radius: styling.bar_border_radius.into(), + ..Default::default() + }, + shadow: Shadow::default(), + }; - renderer.fill_quad(path_quad, styling.path); + renderer.fill_quad(path_quad, styling.path); + } } - } - self.menu_roots - .iter() - .zip(&tree.children) - .zip(layout.children()) - .for_each(|((root, t), lo)| { - root.item.as_widget().draw( - &t.children[root.index], - renderer, - theme, - style, - lo, - position, - viewport, - ); - }); + self.menu_roots + .iter() + .zip(&tree.children) + .zip(layout.children()) + .for_each(|((root, t), lo)| { + root.item.draw( + &t.children[root.index], + renderer, + theme, + style, + lo, + position, + viewport, + ); + }); + }); } fn overlay<'b>( @@ -437,18 +708,18 @@ where _renderer: &Renderer, translation: Vector, ) -> Option> { - // #[cfg(feature = "wayland")] - // return None; + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + return None; let state = tree.state.downcast_ref::(); - if !state.open { + if state.inner.with_data(|state| !state.open) { return None; } Some( Menu { - tree, - menu_roots: &mut self.menu_roots, + tree: state.clone(), + menu_roots: std::borrow::Cow::Owned(self.menu_roots.clone()), bounds_expand: self.bounds_expand, menu_overlays_parent: false, close_condition: self.close_condition, @@ -459,27 +730,30 @@ where cross_offset: self.cross_offset, root_bounds_list: layout.children().map(|lo| lo.bounds()).collect(), path_highlight: self.path_highlight, - style: &self.style, + style: std::borrow::Cow::Borrowed(&self.style), position: Point::new(translation.x, translation.y), + is_overlay: true, + window_id: window::Id::NONE, + depth: 0, + on_surface_action: self.on_surface_action.clone(), } .overlay(), ) } } -impl<'a, Message, Renderer> From> - for Element<'a, Message, crate::Theme, Renderer> + +impl From> for Element<'_, Message, crate::Theme, Renderer> where - Message: 'a, - Renderer: 'a + renderer::Renderer, + Message: Clone + 'static, { - fn from(value: MenuBar<'a, Message, Renderer>) -> Self { + fn from(value: MenuBar) -> Self { Self::new(value) } } #[allow(unused_results, clippy::too_many_arguments)] -fn process_root_events( - menu_roots: &mut [MenuTree<'_, Message, Renderer>], +fn process_root_events( + menu_roots: &mut [MenuTree], view_cursor: Cursor, tree: &mut Tree, event: &event::Event, @@ -490,7 +764,6 @@ fn process_root_events( viewport: &Rectangle, ) -> event::Status where - Renderer: renderer::Renderer, { menu_roots .iter_mut() @@ -498,7 +771,7 @@ where .zip(layout.children()) .map(|((root, t), lo)| { // assert!(t.tag == tree::Tag::stateless()); - root.item.as_widget_mut().on_event( + root.item.on_event( &mut t.children[root.index], event.clone(), lo, diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index f8c0471..8ebca09 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1,10 +1,13 @@ // From iced_aw, license MIT //! Menu tree overlay +use std::{borrow::Cow, sync::Arc}; + use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; use crate::style::menu_bar::StyleSheet; -use iced_core::{Border, Shadow}; +use iced::window; +use iced_core::{Border, Renderer as IcedRenderer, Shadow, Widget}; use iced_widget::core::{ Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, event, layout::{Limits, Node}, @@ -227,20 +230,21 @@ pub(super) struct MenuSlice { pub(super) upper_bound_rel: f32, } +#[derive(Debug, Clone)] /// Menu bounds in overlay space -struct MenuBounds { +pub struct MenuBounds { child_positions: Vec, child_sizes: Vec, children_bounds: Rectangle, - parent_bounds: Rectangle, + pub parent_bounds: Rectangle, check_bounds: Rectangle, offset_bounds: Rectangle, } impl MenuBounds { #[allow(clippy::too_many_arguments)] - fn new( - menu_tree: &MenuTree<'_, Message, Renderer>, - renderer: &Renderer, + fn new( + menu_tree: &MenuTree, + renderer: &crate::Renderer, item_width: ItemWidth, item_height: ItemHeight, viewport_size: Size, @@ -249,10 +253,8 @@ impl MenuBounds { bounds_expand: u16, parent_bounds: Rectangle, tree: &mut [Tree], - ) -> Self - where - Renderer: renderer::Renderer, - { + is_overlay: bool, + ) -> Self { let (children_size, child_positions, child_sizes) = get_children_layout(menu_tree, renderer, item_width, item_height, tree); @@ -262,7 +264,11 @@ impl MenuBounds { // overlay space children position let (children_position, offset_position) = { let (cp, op) = aod.resolve(view_parent_bounds, children_size, viewport_size); - (cp - overlay_offset, op - overlay_offset) + if is_overlay { + (cp - overlay_offset, op - overlay_offset) + } else { + (Point::ORIGIN, op - overlay_offset) + } }; // calc offset bounds @@ -288,23 +294,22 @@ impl MenuBounds { } } +#[derive(Clone)] pub(crate) struct MenuState { + /// The index of the active menu item pub(super) index: Option, scroll_offset: f32, - menu_bounds: MenuBounds, + pub menu_bounds: MenuBounds, } impl MenuState { - pub(super) fn layout( + pub(super) fn layout( &self, overlay_offset: Vector, slice: MenuSlice, - renderer: &Renderer, - menu_tree: &MenuTree<'_, Message, Renderer>, + renderer: &crate::Renderer, + menu_tree: &[MenuTree], tree: &mut [Tree], - ) -> Node - where - Renderer: renderer::Renderer, - { + ) -> Node { let MenuSlice { start_index, end_index, @@ -312,18 +317,14 @@ impl MenuState { upper_bound_rel, } = slice; - assert_eq!( - menu_tree.children.len(), - self.menu_bounds.child_positions.len() - ); + debug_assert_eq!(menu_tree.len(), self.menu_bounds.child_positions.len()); // viewport space children bounds let children_bounds = self.menu_bounds.children_bounds + overlay_offset; - let child_nodes = self.menu_bounds.child_positions[start_index..=end_index] .iter() .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter()) - .zip(menu_tree.children[start_index..=end_index].iter()) + .zip(menu_tree[start_index..=end_index].iter()) .map(|((cp, size), mt)| { let mut position = *cp; let mut size = *size; @@ -336,10 +337,9 @@ impl MenuState { size.height = upper_bound_rel - position; } - let limits = Limits::new(Size::ZERO, size); + let limits = Limits::new(size, size); mt.item - .as_widget() .layout(&mut tree[mt.index], renderer, &limits) .move_to(Point::new(0.0, position + self.scroll_offset)) }) @@ -348,30 +348,28 @@ impl MenuState { Node::with_children(children_bounds.size(), child_nodes).move_to(children_bounds.position()) } - fn layout_single( + fn layout_single( &self, overlay_offset: Vector, index: usize, - renderer: &Renderer, - menu_tree: &MenuTree<'_, Message, Renderer>, + renderer: &crate::Renderer, + menu_tree: &MenuTree, tree: &mut Tree, - ) -> Node - where - Renderer: renderer::Renderer, - { + ) -> Node { // viewport space children bounds let children_bounds = self.menu_bounds.children_bounds + overlay_offset; let position = self.menu_bounds.child_positions[index]; let limits = Limits::new(Size::ZERO, self.menu_bounds.child_sizes[index]); let parent_offset = children_bounds.position() - Point::ORIGIN; - let node = menu_tree.item.as_widget().layout(tree, renderer, &limits); + let node = menu_tree.item.layout(tree, renderer, &limits); node.clone().move_to(Point::new( parent_offset.x, parent_offset.y + position + self.scroll_offset, )) } + /// returns a slice of the menu items that are inside the viewport pub(super) fn slice( &self, viewport_size: Size, @@ -426,12 +424,11 @@ 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>, +#[derive(Clone)] +pub(crate) struct Menu<'b, Message: std::clone::Clone> { + pub(crate) tree: MenuBarState, + // Flattened menu tree + 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, @@ -443,72 +440,113 @@ 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, + pub(crate) is_overlay: bool, + /// window id for this popup + pub(crate) window_id: window::Id, + pub(crate) depth: usize, + pub(crate) on_surface_action: + Option Message + Send + Sync + 'static>>, } -impl<'b, Message, Renderer> Menu<'_, 'b, Message, Renderer> -where - Renderer: renderer::Renderer, -{ - pub(crate) fn overlay(self) -> overlay::Element<'b, Message, crate::Theme, Renderer> { +impl<'b, Message: Clone + 'static> Menu<'b, Message> { + pub(crate) fn overlay(self) -> overlay::Element<'b, Message, crate::Theme, crate::Renderer> { overlay::Element::new(Box::new(self)) } -} -impl overlay::Overlay - for Menu<'_, '_, Message, Renderer> -where - Renderer: renderer::Renderer, -{ - fn layout(&mut self, renderer: &Renderer, bounds: Size) -> Node { - // layout children - let position = self.position; - let state = self.tree.state.downcast_mut::(); - let overlay_offset = Point::ORIGIN - position; - let tree_children = &mut self.tree.children; - let children = state - .active_root - .map(|active_root| { - let root = &self.menu_roots[active_root]; - let active_tree = &mut tree_children[active_root]; - state.menu_states.iter().enumerate().fold( - (root, Vec::new()), - |(menu_root, mut nodes), (_i, ms)| { - let slice = ms.slice(bounds, overlay_offset, self.item_height); - let _start_index = slice.start_index; - let _end_index = slice.end_index; - let children_node = ms.layout( - overlay_offset, - slice, - renderer, - menu_root, - &mut active_tree.children, - ); - nodes.push(children_node); - // only the last menu can have a None active index - ( - ms.index - .map_or(menu_root, |active| &menu_root.children[active]), - nodes, - ) - }, - ) - }) - .map(|(_, l)| l) - .unwrap_or_default(); - // overlay space viewport rectangle - Node::with_children(bounds, children).translate(Point::ORIGIN - position) + pub(crate) fn layout(&self, renderer: &crate::Renderer, limits: Limits) -> Node { + // layout children; + let position = self.position; + let mut intrinsic_size = Size::ZERO; + + let empty = Vec::new(); + self.tree.inner.with_data_mut(|data| { + if data.active_root.len() < self.depth + 1 || data.menu_states.len() < self.depth + 1 { + return Node::new(limits.min()); + } + + let overlay_offset = Point::ORIGIN - position; + let tree_children: &mut Vec = &mut data.tree.children; + + let children = (if self.is_overlay { 0 } else { self.depth }..=if self.is_overlay { + data.active_root.len() - 1 + } else { + self.depth + }) + .map(|active_root| { + if self.menu_roots.is_empty() { + return (&empty, vec![]); + } + let (active_tree, roots) = + data.active_root[..=active_root].iter().skip(1).fold( + ( + &mut tree_children[data.active_root[0]].children, + &self.menu_roots[data.active_root[0]].children, + ), + |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), + ); + + data.menu_states[if self.is_overlay { 0 } else { self.depth } + ..=if self.is_overlay { + data.active_root.len() - 1 + } else { + self.depth + }] + .iter() + .enumerate() + .filter(|ms| self.is_overlay || ms.0 < 1) + .fold( + (roots, Vec::new()), + |(menu_root, mut nodes), (_i, ms)| { + let slice = + ms.slice(limits.max(), overlay_offset, self.item_height); + let _start_index = slice.start_index; + let _end_index = slice.end_index; + let children_node = ms.layout( + overlay_offset, + slice, + renderer, + menu_root, + active_tree, + ); + let node_size = children_node.size(); + intrinsic_size.height += node_size.height; + intrinsic_size.width = intrinsic_size.width.max(node_size.width); + + nodes.push(children_node); + // if popup just use len 1? + // only the last menu can have a None active index + ( + ms.index + .map_or(menu_root, |active| &menu_root[active].children), + nodes, + ) + }, + ) + }) + .map(|(_, l)| l) + .next() + .unwrap_or_default(); + + // overlay space viewport rectangle + Node::with_children( + limits.resolve(Length::Shrink, Length::Shrink, intrinsic_size), + children, + ) + .translate(Point::ORIGIN - position) + }) } + #[allow(clippy::too_many_lines)] fn on_event( &mut self, event: event::Event, layout: Layout<'_>, view_cursor: Cursor, - renderer: &Renderer, + renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) -> (Option<(usize, MenuState)>, event::Status) { use event::{ Event::{Mouse, Touch}, Status::{Captured, Ignored}, @@ -519,18 +557,25 @@ where }; use touch::Event::{FingerLifted, FingerMoved, FingerPressed}; - if !self.tree.state.downcast_ref::().open { - return Ignored; - }; + if !self + .tree + .inner + .with_data(|data| data.open || data.active_root.len() <= self.depth) + { + return (None, Ignored); + } let viewport = layout.bounds(); + let viewport_size = viewport.size(); let overlay_offset = Point::ORIGIN - viewport.position(); let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset; - + let menu_roots = match &mut self.menu_roots { + Cow::Borrowed(_) => panic!(), + Cow::Owned(o) => o.as_mut_slice(), + }; let menu_status = process_menu_events( - self.tree, - self.menu_roots, + self, event.clone(), view_cursor, renderer, @@ -550,23 +595,28 @@ where self.main_offset as f32, ); - match event { + let ret = match event { Mouse(WheelScrolled { delta }) => { process_scroll_events(self, delta, overlay_cursor, viewport_size, overlay_offset) .merge(menu_status) } Mouse(ButtonPressed(Left)) | Touch(FingerPressed { .. }) => { - let state = self.tree.state.downcast_mut::(); - state.pressed = true; - state.view_cursor = view_cursor; + self.tree.inner.with_data_mut(|data| { + data.pressed = true; + data.view_cursor = view_cursor; + }); Captured } Mouse(CursorMoved { position }) | Touch(FingerMoved { position, .. }) => { let view_cursor = Cursor::Available(position); let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset; - process_overlay_events( + if !self.is_overlay && !view_cursor.is_over(viewport) { + return (None, menu_status); + } + + let (new_root, status) = process_overlay_events( self, renderer, viewport_size, @@ -574,169 +624,452 @@ where view_cursor, overlay_cursor, self.cross_offset as f32, - ) - .merge(menu_status) + shell, + ); + + return (new_root, status.merge(menu_status)); } Mouse(ButtonReleased(_)) | Touch(FingerLifted { .. }) => { - let state = self.tree.state.downcast_mut::(); - state.pressed = false; + self.tree.inner.with_data_mut(|state| { + state.pressed = false; - // process close condition - if state - .view_cursor - .position() - .unwrap_or_default() - .distance(view_cursor.position().unwrap_or_default()) - < 2.0 - { - let is_inside = state - .menu_states - .iter() - .any(|ms| ms.menu_bounds.check_bounds.contains(overlay_cursor)); - - if self.close_condition.click_inside - && is_inside - && matches!( - event, - Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. }) - ) + // process close condition + if state + .view_cursor + .position() + .unwrap_or_default() + .distance(view_cursor.position().unwrap_or_default()) + < 2.0 { - state.reset(); - return Captured; + let is_inside = state.menu_states[..=if self.is_overlay { + state.active_root.len().saturating_sub(1) + } else { + self.depth + }] + .iter() + .any(|ms| ms.menu_bounds.check_bounds.contains(overlay_cursor)); + let mut needs_reset = false; + needs_reset |= self.close_condition.click_inside + && is_inside + && matches!( + event, + Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. }) + ); + + needs_reset |= self.close_condition.click_outside && !is_inside; + + if needs_reset { + #[cfg(all( + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] + if let Some(handler) = self.on_surface_action.as_ref() { + let mut root = self.window_id; + let mut depth = self.depth; + while let Some(parent) = + state.popup_id.iter().find(|(_, v)| **v == root) + { + // parent of root popup is the window, so we stop. + if depth == 0 { + break; + } + root = *parent.0; + depth = depth.saturating_sub(1); + } + shell + .publish((handler)(crate::surface::Action::DestroyPopup(root))); + } + + state.reset(); + return Captured; + } } - if self.close_condition.click_outside && !is_inside { + // close all menus when clicking inside the menu bar + if self.bar_bounds.contains(overlay_cursor) { state.reset(); - return Captured; + Captured + } else { + menu_status } - } - - // close all menus when clicking inside the menu bar - if self.bar_bounds.contains(overlay_cursor) { - state.reset(); - Captured - } else { - menu_status - } + }) } _ => menu_status, - } + }; + (None, ret) } - #[allow(unused_results)] + #[allow(unused_results, clippy::too_many_lines)] fn draw( &self, - renderer: &mut Renderer, + renderer: &mut crate::Renderer, theme: &crate::Theme, style: &renderer::Style, layout: Layout<'_>, view_cursor: Cursor, ) { - let state = self.tree.state.downcast_ref::(); - let Some(active_root) = state.active_root else { - return; - }; + self.tree.inner.with_data(|state| { + if !state.open || state.active_root.len() <= self.depth { + return; + } + let active_root = &state.active_root[..=if self.is_overlay { 0 } else { self.depth }]; + let viewport = layout.bounds(); + let viewport_size = viewport.size(); + let overlay_offset = Point::ORIGIN - viewport.position(); - let viewport = layout.bounds(); - let viewport_size = viewport.size(); - let overlay_offset = Point::ORIGIN - viewport.position(); - let render_bounds = Rectangle::new(Point::ORIGIN, viewport.size()); + let render_bounds = if self.is_overlay { + Rectangle::new(Point::ORIGIN, viewport.size()) + } else { + Rectangle::new(Point::ORIGIN, Size::INFINITY) + }; - let styling = theme.appearance(self.style); + let styling = theme.appearance(&self.style); + let roots = active_root.iter().skip(1).fold( + &self.menu_roots[active_root[0]].children, + |mt, next_active_root| (&mt[*next_active_root].children), + ); + let indices = state.get_trimmed_indices(self.depth).collect::>(); + state.menu_states[if self.is_overlay { 0 } else { self.depth }..=if self.is_overlay { + state.menu_states.len() - 1 + } else { + self.depth + }] + .iter() + .zip(layout.children()) + .enumerate() + .filter(|ms: &(usize, (&MenuState, Layout<'_>))| self.is_overlay || ms.0 < 1) + .fold( + roots, + |menu_roots: &Vec>, (i, (ms, children_layout))| { + let draw_path = self.path_highlight.as_ref().is_some_and(|ph| match ph { + PathHighlight::Full => true, + PathHighlight::OmitActive => { + !indices.is_empty() && i < indices.len() - 1 + } + PathHighlight::MenuActive => self.depth == state.active_root.len() - 1, + }); - let tree = &self.tree.children[active_root].children; - let root = &self.menu_roots[active_root]; - - let indices = state.get_trimmed_indices().collect::>(); - - state - .menu_states - .iter() - .zip(layout.children()) - .enumerate() - .fold(root, |menu_root, (i, (ms, children_layout))| { - let draw_path = self.path_highlight.as_ref().map_or(false, |ph| match ph { - PathHighlight::Full => true, - PathHighlight::OmitActive => !indices.is_empty() && i < indices.len() - 1, - PathHighlight::MenuActive => i < state.menu_states.len() - 1, - }); - - // react only to the last menu - let view_cursor = if i == state.menu_states.len() - 1 { - view_cursor - } else { - Cursor::Available([-1.0; 2].into()) - }; - - let draw_menu = |r: &mut Renderer| { - // calc slice - let slice = ms.slice(viewport_size, overlay_offset, self.item_height); - let start_index = slice.start_index; - let end_index = slice.end_index; - - let children_bounds = children_layout.bounds(); - - // draw menu background - // let bounds = pad_rectangle(children_bounds, styling.background_expand.into()); - // println!("cursor: {:?}", view_cursor); - // println!("bg_bounds: {:?}", bounds); - // println!("color: {:?}\n", styling.background); - let menu_quad = renderer::Quad { - bounds: pad_rectangle(children_bounds, styling.background_expand.into()), - border: Border { - radius: styling.menu_border_radius.into(), - width: styling.border_width, - color: styling.border_color, - }, - shadow: Shadow::default(), - }; - let menu_color = styling.background; - r.fill_quad(menu_quad, menu_color); - - // draw path hightlight - if let (true, Some(active)) = (draw_path, ms.index) { - let active_bounds = children_layout - .children() - .nth(active.saturating_sub(start_index)) - .expect("No active children were found in menu?") - .bounds(); - let path_quad = renderer::Quad { - bounds: active_bounds, - border: Border { - radius: styling.menu_border_radius.into(), - ..Default::default() - }, - shadow: Shadow::default(), + // react only to the last menu + let view_cursor = if self.depth == state.active_root.len() - 1 + || i == state.menu_states.len() - 1 + { + view_cursor + } else { + Cursor::Available([-1.0; 2].into()) }; - r.fill_quad(path_quad, styling.path); - } + let draw_menu = |r: &mut crate::Renderer| { + // calc slice + let slice = ms.slice(viewport_size, overlay_offset, self.item_height); + let start_index = slice.start_index; + let end_index = slice.end_index; - // draw item - menu_root.children[start_index..=end_index] - .iter() - .zip(children_layout.children()) - .for_each(|(mt, clo)| { - mt.item.as_widget().draw( - &tree[mt.index], - r, - theme, - style, - clo, - view_cursor, - &children_layout.bounds(), - ); - }); + let children_bounds = children_layout.bounds(); + + // draw menu background + // let bounds = pad_rectangle(children_bounds, styling.background_expand.into()); + // println!("cursor: {:?}", view_cursor); + // println!("bg_bounds: {:?}", bounds); + // println!("color: {:?}\n", styling.background); + let menu_quad = renderer::Quad { + bounds: pad_rectangle( + children_bounds, + styling.background_expand.into(), + ), + border: Border { + radius: styling.menu_border_radius.into(), + width: styling.border_width, + color: styling.border_color, + }, + shadow: Shadow::default(), + }; + let menu_color = styling.background; + r.fill_quad(menu_quad, menu_color); + // draw path hightlight + if let (true, Some(active)) = (draw_path, ms.index) { + if let Some(active_layout) = children_layout + .children() + .nth(active.saturating_sub(start_index)) + { + let path_quad = renderer::Quad { + bounds: active_layout.bounds(), + border: Border { + radius: styling.menu_border_radius.into(), + ..Default::default() + }, + shadow: Shadow::default(), + }; + + r.fill_quad(path_quad, styling.path); + } + } + if start_index < menu_roots.len() { + // draw item + menu_roots[start_index..=end_index] + .iter() + .zip(children_layout.children()) + .for_each(|(mt, clo)| { + mt.item.draw( + &state.tree.children[active_root[0]].children[mt.index], + r, + theme, + style, + clo, + view_cursor, + &children_layout.bounds(), + ); + }); + } + }; + + renderer.with_layer(render_bounds, draw_menu); + + // only the last menu can have a None active index + ms.index + .map_or(menu_roots, |active| &menu_roots[active].children) + }, + ); + }); + } +} +impl overlay::Overlay + for Menu<'_, Message> +{ + fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> iced_core::layout::Node { + Menu::layout( + self, + renderer, + Limits::NONE + .min_width(bounds.width) + .max_width(bounds.width) + .min_height(bounds.height) + .max_height(bounds.height), + ) + } + + fn on_event( + &mut self, + event: iced::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) + .1 + } + + fn draw( + &self, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + self.draw(renderer, theme, style, layout, cursor); + } +} + +impl Widget + for Menu<'_, Message> +{ + fn size(&self) -> Size { + Size { + width: Length::Shrink, + height: Length::Shrink, + } + } + + fn layout( + &self, + _tree: &mut Tree, + renderer: &crate::Renderer, + limits: &iced_core::layout::Limits, + ) -> iced_core::layout::Node { + Menu::layout(self, renderer, *limits) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + Menu::draw(self, renderer, theme, style, layout, cursor); + } + + #[allow(clippy::too_many_lines)] + fn on_event( + &mut self, + tree: &mut Tree, + event: iced::Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &crate::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let (new_root, status) = self.on_event(event, layout, cursor, renderer, clipboard, shell); + + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + if let Some((new_root, new_ms)) = new_root { + use iced_runtime::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, + }; + let overlay_offset = Point::ORIGIN - viewport.position(); + + let overlay_cursor = cursor.position().unwrap_or_default() - overlay_offset; + + let Some((mut menu, popup_id)) = self.tree.inner.with_data_mut(|state| { + let popup_id = *state + .popup_id + .entry(self.window_id) + .or_insert_with(window::Id::unique); + let active_roots = state + .active_root + .get(self.depth) + .cloned() + .unwrap_or_default(); + + let root_bounds_list = layout + .children() + .next() + .unwrap() + .children() + .map(|lo| lo.bounds()) + .collect(); + + let mut popup_menu = Menu { + tree: self.tree.clone(), + menu_roots: Cow::Owned(Cow::into_owned(self.menu_roots.clone())), + bounds_expand: self.bounds_expand, + menu_overlays_parent: false, + close_condition: self.close_condition, + item_width: self.item_width, + item_height: self.item_height, + bar_bounds: layout.bounds(), + main_offset: self.main_offset, + cross_offset: self.cross_offset, + root_bounds_list, + path_highlight: self.path_highlight, + style: Cow::Owned(Cow::into_owned(self.style.clone())), + position: Point::new(0., 0.), + is_overlay: false, + window_id: popup_id, + depth: self.depth + 1, + on_surface_action: self.on_surface_action.clone(), }; - renderer.with_layer(render_bounds, draw_menu); + state.active_root.push(new_root); - // only the last menu can have a None active index - ms.index - .map_or(menu_root, |active| &menu_root.children[active]) + Some((popup_menu, popup_id)) + }) else { + return status; + }; + // XXX we push a new active root manually instead + init_root_popup_menu( + &mut menu, + renderer, + shell, + cursor.position().unwrap(), + layout.bounds().size(), + Vector::new(0., 0.), + layout.bounds(), + self.main_offset as f32, + ); + let (anchor_rect, gravity) = self.tree.inner.with_data_mut(|state| { + (state + .menu_states + .get(self.depth + 1) + .map(|s| s.menu_bounds.parent_bounds) + .map_or_else( + || { + let bounds = layout.bounds(); + Rectangle { + x: bounds.x as i32, + y: bounds.y as i32, + width: bounds.width as i32, + height: bounds.height as i32, + } + }, + |r| Rectangle { + x: r.x as i32, + y: r.y as i32, + width: r.width as i32, + height: r.height as i32, + }, + ), match (state.horizontal_direction, state.vertical_direction) { + (Direction::Positive, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, + (Direction::Positive, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight, + (Direction::Negative, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft, + (Direction::Negative, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft, + }) }); + + let menu_node = Widget::layout( + &menu, + &mut Tree::empty(), + renderer, + &Limits::NONE.min_width(1.).min_height(1.), + ); + + let popup_size = menu_node.size(); + let positioner = SctkPositioner { + size: Some(( + popup_size.width.ceil() as u32 + 2, + popup_size.height.ceil() as u32 + 2, + )), + anchor_rect, + anchor: + cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::TopRight, + gravity, + reactive: true, + ..Default::default() + }; + let parent = self.window_id; + shell.publish((self.on_surface_action.as_ref().unwrap())( + crate::surface::action::simple_popup( + move || SctkPopupSettings { + parent, + id: popup_id, + positioner: positioner.clone(), + parent_size: None, + grab: true, + close_with_children: false, + input_zone: None, + }, + Some(move || { + crate::Element::from( + crate::widget::container(menu.clone()).center(Length::Fill), + ) + .map(crate::action::app) + }), + ), + )); + + return status; + } + status + } +} + +impl<'a, Message> From> + for iced::Element<'a, Message, crate::Theme, crate::Renderer> +where + Message: std::clone::Clone + 'static, +{ + fn from(value: Menu<'a, Message>) -> Self { + Self::new(value) } } @@ -749,9 +1082,93 @@ fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle { } } -pub(super) fn init_root_menu( - menu: &mut Menu<'_, '_, Message, Renderer>, - renderer: &Renderer, +#[allow(clippy::too_many_arguments)] +pub(super) fn init_root_menu( + menu: &mut Menu<'_, Message>, + renderer: &crate::Renderer, + shell: &mut Shell<'_, Message>, + overlay_cursor: Point, + viewport_size: Size, + overlay_offset: Vector, + bar_bounds: Rectangle, + main_offset: f32, +) { + menu.tree.inner.with_data_mut(|state| { + if !(state.menu_states.get(menu.depth).is_none() + && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) + || menu.depth > 0 + || !state.open + { + return; + } + + let mut set = false; + for (i, (&root_bounds, mt)) in menu + .root_bounds_list + .iter() + .zip(menu.menu_roots.iter()) + .enumerate() + { + if mt.children.is_empty() { + continue; + } + + if root_bounds.contains(overlay_cursor) { + let view_center = viewport_size.width * 0.5; + let rb_center = root_bounds.center_x(); + + state.horizontal_direction = if rb_center > view_center { + Direction::Negative + } else { + Direction::Positive + }; + + let aod = Aod { + horizontal: true, + vertical: true, + horizontal_overlap: true, + vertical_overlap: false, + horizontal_direction: state.horizontal_direction, + vertical_direction: state.vertical_direction, + horizontal_offset: 0.0, + vertical_offset: main_offset, + }; + let menu_bounds = MenuBounds::new( + mt, + renderer, + menu.item_width, + menu.item_height, + viewport_size, + overlay_offset, + &aod, + menu.bounds_expand, + root_bounds, + &mut state.tree.children[0].children, + menu.is_overlay, + ); + set = true; + state.active_root.push(i); + let ms = MenuState { + index: None, + scroll_offset: 0.0, + menu_bounds, + }; + state.menu_states.push(ms); + + // Hack to ensure menu opens properly + shell.invalidate_layout(); + + break; + } + } + debug_assert!(set, "Root not set"); + }); +} + +#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] +pub(super) fn init_root_popup_menu( + menu: &mut Menu<'_, Message>, + renderer: &crate::Renderer, shell: &mut Shell<'_, Message>, overlay_cursor: Point, viewport_size: Size, @@ -759,143 +1176,153 @@ pub(super) fn init_root_menu( bar_bounds: Rectangle, main_offset: f32, ) where - Renderer: renderer::Renderer, + Message: std::clone::Clone, { - let state = menu.tree.state.downcast_mut::(); - if !(state.menu_states.is_empty() && bar_bounds.contains(overlay_cursor)) { - return; - } - - for (i, (&root_bounds, mt)) in menu - .root_bounds_list - .iter() - .zip(menu.menu_roots.iter()) - .enumerate() - { - if mt.children.is_empty() { - continue; + menu.tree.inner.with_data_mut(|state| { + if !(state.menu_states.get(menu.depth).is_none() + && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) + { + return; } - if root_bounds.contains(overlay_cursor) { - let view_center = viewport_size.width * 0.5; - let rb_center = root_bounds.center_x(); + let active_roots = &state.active_root[..=menu.depth]; - state.horizontal_direction = if rb_center > view_center { - Direction::Negative - } else { - Direction::Positive - }; - - let aod = Aod { - horizontal: true, - vertical: true, - horizontal_overlap: true, - vertical_overlap: false, - horizontal_direction: state.horizontal_direction, - vertical_direction: state.vertical_direction, - horizontal_offset: 0.0, - vertical_offset: main_offset, - }; - - let menu_bounds = MenuBounds::new( - mt, - renderer, - menu.item_width, - menu.item_height, - viewport_size, - overlay_offset, - &aod, - menu.bounds_expand, - root_bounds, - &mut menu.tree.children[i].children, - ); - - state.active_root = Some(i); - state.menu_states.push(MenuState { - index: None, - scroll_offset: 0.0, - menu_bounds, + let mut set = false; + let mt = active_roots + .iter() + .skip(1) + .fold(&menu.menu_roots[active_roots[0]], |mt, next_active_root| { + &mt.children[*next_active_root] }); + let i = active_roots.last().unwrap(); + let root_bounds = menu.root_bounds_list[*i]; - // Hack to ensure menu opens properly - shell.invalidate_layout(); + assert!(!mt.children.is_empty(), "skipping menu with no children"); + let aod = Aod { + horizontal: true, + vertical: true, + horizontal_overlap: true, + vertical_overlap: false, + horizontal_direction: state.horizontal_direction, + vertical_direction: state.vertical_direction, + horizontal_offset: 0.0, + vertical_offset: main_offset, + }; + let menu_bounds = MenuBounds::new( + mt, + renderer, + menu.item_width, + menu.item_height, + viewport_size, + overlay_offset, + &aod, + menu.bounds_expand, + root_bounds, + // TODO how to select the tree for the popup + &mut state.tree.children[0].children, + menu.is_overlay, + ); - break; - } - } + let view_center = viewport_size.width * 0.5; + let rb_center = root_bounds.center_x(); + + state.horizontal_direction = if rb_center > view_center { + Direction::Negative + } else { + Direction::Positive + }; + set = true; + + let ms = MenuState { + index: None, + scroll_offset: 0.0, + menu_bounds, + }; + state.menu_states.push(ms); + + // Hack to ensure menu opens properly + shell.invalidate_layout(); + // non tree buttons arent active? + debug_assert!(set, "Root popup menu state was not set."); + }); } #[allow(clippy::too_many_arguments)] -fn process_menu_events<'b, Message, Renderer>( - tree: &'b mut Tree, - menu_roots: &'b mut [MenuTree<'_, Message, Renderer>], +fn process_menu_events( + menu: &mut Menu, event: event::Event, view_cursor: Cursor, - renderer: &Renderer, + renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, overlay_offset: Vector, -) -> event::Status -where - Renderer: renderer::Renderer, -{ +) -> event::Status { use event::Status; - let state = tree.state.downcast_mut::(); - let Some(active_root) = state.active_root else { - return Status::Ignored; + let my_state = &mut menu.tree; + let menu_roots = match &mut menu.menu_roots { + Cow::Borrowed(_) => panic!(), + Cow::Owned(o) => o.as_mut_slice(), }; + my_state.inner.with_data_mut(|state| { + if state.active_root.len() <= menu.depth { + return event::Status::Ignored; + } - let indices = state.get_trimmed_indices().collect::>(); + let Some(hover) = state.menu_states.last_mut() else { + return Status::Ignored; + }; - if indices.is_empty() { - return Status::Ignored; - } + let Some(hover_index) = hover.index else { + return Status::Ignored; + }; - // get active item - let mt = indices - .iter() - .fold(&mut menu_roots[active_root], |mt, &i| &mut mt.children[i]); + let mt = state.active_root.iter().skip(1).fold( + // then use menu states for each open menu + &mut menu_roots[state.active_root[0]], + |mt, next_active_root| &mut mt.children[*next_active_root], + ); - // widget tree - let tree = &mut tree.children[active_root].children[mt.index]; + let mt = &mut mt.children[hover_index]; + let tree = &mut state.tree.children[state.active_root[0]].children[mt.index]; - // get layout - let last_ms = &state.menu_states[indices.len() - 1]; - let child_node = last_ms.layout_single( - overlay_offset, - last_ms.index.expect("missing index within menu state."), - renderer, - mt, - tree, - ); - let child_layout = Layout::new(&child_node); + // get layout + let child_node = hover.layout_single( + overlay_offset, + hover.index.expect("missing index within menu state."), + renderer, + mt, + tree, + ); + let child_layout = Layout::new(&child_node); - // process only the last widget - mt.item.as_widget_mut().on_event( - tree, - event, - child_layout, - view_cursor, - renderer, - clipboard, - shell, - &Rectangle::default(), - ) + // process only the last widget + mt.item.on_event( + tree, + event, + child_layout, + view_cursor, + renderer, + clipboard, + shell, + &Rectangle::default(), + ) + }) } -#[allow(unused_results)] -fn process_overlay_events( - menu: &mut Menu<'_, '_, Message, Renderer>, - renderer: &Renderer, +#[allow(unused_results, clippy::too_many_lines, clippy::too_many_arguments)] +fn process_overlay_events( + menu: &mut Menu, + renderer: &crate::Renderer, viewport_size: Size, overlay_offset: Vector, view_cursor: Cursor, overlay_cursor: Point, cross_offset: f32, -) -> event::Status + _shell: &mut Shell<'_, Message>, +) -> (Option<(usize, MenuState)>, event::Status) where - Renderer: renderer::Renderer, + Message: std::clone::Clone, { use event::Status::{Captured, Ignored}; /* @@ -907,263 +1334,295 @@ where if active item is a menu: add menu // viewport space */ + let mut new_menu_root = None; - let state = menu.tree.state.downcast_mut::(); + menu.tree.inner.with_data_mut(|state| { - let Some(active_root) = state.active_root else { - if !menu.bar_bounds.contains(overlay_cursor) { - state.reset(); - } - return Ignored; - }; + /* When overlay is running, cursor_position in any widget method will go negative + but I still want Widget::draw() to react to cursor movement */ + state.view_cursor = view_cursor; - if state.pressed { - return Ignored; - } + // * remove invalid menus - /* When overlay is running, cursor_position in any widget method will go negative - but I still want Widget::draw() to react to cursor movement */ - state.view_cursor = view_cursor; - - // * remove invalid menus - let mut prev_bounds = std::iter::once(menu.bar_bounds) - .chain( - state.menu_states[..state.menu_states.len().saturating_sub(1)] - .iter() - .map(|ms| ms.menu_bounds.children_bounds), - ) - .collect::>(); - - if menu.close_condition.leave { - for i in (0..state.menu_states.len()).rev() { - let mb = &state.menu_states[i].menu_bounds; - - if mb.parent_bounds.contains(overlay_cursor) - || mb.children_bounds.contains(overlay_cursor) - || mb.offset_bounds.contains(overlay_cursor) - || (mb.check_bounds.contains(overlay_cursor) - && prev_bounds.iter().all(|pvb| !pvb.contains(overlay_cursor))) - { - break; - } - prev_bounds.pop(); - state.menu_states.pop(); - } - } else { - for i in (0..state.menu_states.len()).rev() { - let mb = &state.menu_states[i].menu_bounds; - - if mb.parent_bounds.contains(overlay_cursor) - || mb.children_bounds.contains(overlay_cursor) - || prev_bounds.iter().all(|pvb| !pvb.contains(overlay_cursor)) - { - break; - } - prev_bounds.pop(); - state.menu_states.pop(); - } - } - - // get indices - let indices = state - .menu_states - .iter() - .map(|ms| ms.index) - .collect::>(); - - // * update active item - let Some(last_menu_state) = state.menu_states.last_mut() else { - // no menus left - state.active_root = None; - - // keep state.open when the cursor is still inside the menu bar - // this allows the overlay to keep drawing when the cursor is - // moving aroung the menu bar - if !menu.bar_bounds.contains(overlay_cursor) { - state.open = false; - } - return Captured; - }; - - let last_menu_bounds = &last_menu_state.menu_bounds; - let last_parent_bounds = last_menu_bounds.parent_bounds; - let last_children_bounds = last_menu_bounds.children_bounds; - - if (!menu.menu_overlays_parent && last_parent_bounds.contains(overlay_cursor)) - // cursor is in the parent part - || !last_children_bounds.contains(overlay_cursor) - // cursor is outside - { - last_menu_state.index = None; - return Captured; - } - // cursor is in the children part - - // calc new index - let height_diff = (overlay_cursor.y - (last_children_bounds.y + last_menu_state.scroll_offset)) - .clamp(0.0, last_children_bounds.height - 0.001); - - let active_menu_root = &menu.menu_roots[active_root]; - - let active_menu = indices[0..indices.len().saturating_sub(1)] - .iter() - .fold(active_menu_root, |mt, i| { - &mt.children[i.expect("missing active child index in menu")] - }); - - let new_index = match menu.item_height { - ItemHeight::Uniform(u) => (height_diff / f32::from(u)).floor() as usize, - ItemHeight::Static(_) | ItemHeight::Dynamic(_) => { - let max_index = active_menu.children.len() - 1; - search_bound( - 0, - 0, - max_index, - height_diff, - &last_menu_bounds.child_positions, - &last_menu_bounds.child_sizes, + let mut prev_bounds = std::iter::once(menu.bar_bounds) + .chain( + if menu.is_overlay { + state.menu_states[..state.menu_states.len().saturating_sub(1)].iter() + } else { + state.menu_states[..menu.depth].iter() + } + .map(|s| s.menu_bounds.children_bounds), ) + .collect::>(); + + if menu.is_overlay && menu.close_condition.leave { + for i in (0..state.menu_states.len()).rev() { + let mb = &state.menu_states[i].menu_bounds; + + if mb.parent_bounds.contains(overlay_cursor) + || menu.is_overlay && mb.children_bounds.contains(overlay_cursor) + || mb.offset_bounds.contains(overlay_cursor) + || (mb.check_bounds.contains(overlay_cursor) + && prev_bounds.iter().all(|pvb| !pvb.contains(overlay_cursor))) + { + break; + } + prev_bounds.pop(); + state.active_root.pop(); + state.menu_states.pop(); + } + } else if menu.is_overlay { + for i in (0..state.menu_states.len()).rev() { + let mb = &state.menu_states[i].menu_bounds; + + if mb.parent_bounds.contains(overlay_cursor) + || mb.children_bounds.contains(overlay_cursor) + || prev_bounds.iter().all(|pvb| !pvb.contains(overlay_cursor)) + { + break; + } + prev_bounds.pop(); + state.active_root.pop(); + state.menu_states.pop(); + } } - }; - // set new index - last_menu_state.index = Some(new_index); + // * update active item + let menu_states_len = state.menu_states.len(); - // get new active item - let item = &active_menu.children[new_index]; + let Some(last_menu_state) = state.menu_states.get_mut(if menu.is_overlay { + menu_states_len.saturating_sub(1) + } else { + menu.depth + }) else { + if menu.is_overlay { + // no menus left + // TODO do we want to avoid this for popups? + // state.active_root.remove(menu.depth); - // * add new menu if the new item is a menu - if !item.children.is_empty() { - let item_position = Point::new( - 0.0, - last_menu_bounds.child_positions[new_index] + last_menu_state.scroll_offset, - ); - let item_size = last_menu_bounds.child_sizes[new_index]; + // keep state.open when the cursor is still inside the menu bar + // this allows the overlay to keep drawing when the cursor is + // moving aroung the menu bar + if !menu.bar_bounds.contains(overlay_cursor) { + state.open = false; + } + } - // overlay space item bounds - let item_bounds = Rectangle::new(item_position, item_size) - + (last_menu_bounds.children_bounds.position() - Point::ORIGIN); - - let aod = Aod { - horizontal: true, - vertical: true, - horizontal_overlap: false, - vertical_overlap: true, - horizontal_direction: state.horizontal_direction, - vertical_direction: state.vertical_direction, - horizontal_offset: cross_offset, - vertical_offset: 0.0, + return (new_menu_root, Captured); }; - state.menu_states.push(MenuState { - index: None, - scroll_offset: 0.0, - menu_bounds: MenuBounds::new( - item, - renderer, - menu.item_width, - menu.item_height, - viewport_size, - overlay_offset, - &aod, - menu.bounds_expand, - item_bounds, - &mut menu.tree.children[active_root].children, - ), - }); - } + let last_menu_bounds = &last_menu_state.menu_bounds; + let last_parent_bounds = last_menu_bounds.parent_bounds; + let last_children_bounds = last_menu_bounds.children_bounds; - Captured + if (menu.is_overlay && !menu.menu_overlays_parent && last_parent_bounds.contains(overlay_cursor)) + // cursor is in the parent part + || menu.is_overlay && !last_children_bounds.contains(overlay_cursor) + // cursor is outside + { + + last_menu_state.index = None; + return (new_menu_root, Captured); + } + + // calc new index + let height_diff = (overlay_cursor.y + - (last_children_bounds.y + last_menu_state.scroll_offset)) + .clamp(0.0, last_children_bounds.height - 0.001); + + let active_root = if menu.is_overlay { + &state.active_root + } else { + &state.active_root[..=menu.depth] + }; + + if state.pressed { + return (new_menu_root, Ignored); + } + let roots = active_root.iter().skip(1).fold( + &menu.menu_roots[active_root[0]].children, + |mt, next_active_root| &mt[*next_active_root].children, + ); + let tree = &mut state.tree.children[active_root[0]].children; + + let active_menu: &Vec> = roots; + let new_index = match menu.item_height { + ItemHeight::Uniform(u) => (height_diff / f32::from(u)).floor() as usize, + ItemHeight::Static(_) | ItemHeight::Dynamic(_) => { + let max_index = active_menu.len() - 1; + search_bound( + 0, + 0, + max_index, + height_diff, + &last_menu_bounds.child_positions, + &last_menu_bounds.child_sizes, + ) + } + }; + + let remove = last_menu_state + .index + .as_ref() + .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); + + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + { + if remove { + if let Some(id) = state.popup_id.remove(&menu.window_id) { + state.active_root.truncate(menu.depth + 1); + _shell.publish((menu.on_surface_action.as_ref().unwrap())({ + crate::surface::action::destroy_popup(id) + })); + } + } + } + let item = &active_menu[new_index]; + // set new index + let old_index = last_menu_state.index.replace(new_index); + + // get new active item + // * add new menu if the new item is a menu + if !item.children.is_empty() && old_index.is_none_or(|i| i != new_index) { + let item_position = Point::new( + 0.0, + last_menu_bounds.child_positions[new_index] + last_menu_state.scroll_offset, + ); + let item_size = last_menu_bounds.child_sizes[new_index]; + + // overlay space item bounds + let item_bounds = Rectangle::new(item_position, item_size) + + (last_menu_bounds.children_bounds.position() - Point::ORIGIN); + + let aod = Aod { + horizontal: true, + vertical: true, + horizontal_overlap: false, + vertical_overlap: true, + horizontal_direction: state.horizontal_direction, + vertical_direction: state.vertical_direction, + horizontal_offset: cross_offset, + vertical_offset: 0.0, + }; + let ms = MenuState { + index: None, + scroll_offset: 0.0, + menu_bounds: MenuBounds::new( + item, + renderer, + menu.item_width, + menu.item_height, + viewport_size, + overlay_offset, + &aod, + menu.bounds_expand, + item_bounds, + tree, + menu.is_overlay, + ), + }; + + new_menu_root = Some((new_index, ms.clone())); + if menu.is_overlay { + state.active_root.push(new_index); + } else { + state.menu_states.truncate(menu.depth + 1); + } + state.menu_states.push(ms); + } else if !menu.is_overlay && remove { + state.menu_states.truncate(menu.depth + 1); + } + + (new_menu_root, 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, + Message: Clone, { use event::Status::{Captured, Ignored}; use mouse::ScrollDelta; - let state = menu.tree.state.downcast_mut::(); + menu.tree.inner.with_data_mut(|state| { + let delta_y = match delta { + ScrollDelta::Lines { y, .. } => y * 60.0, + ScrollDelta::Pixels { y, .. } => y, + }; - let delta_y = match delta { - ScrollDelta::Lines { y, .. } => y * 60.0, - ScrollDelta::Pixels { y, .. } => y, - }; + let calc_offset_bounds = |menu_state: &MenuState, viewport_size: Size| -> (f32, f32) { + // viewport space children bounds + let children_bounds = menu_state.menu_bounds.children_bounds + overlay_offset; - let calc_offset_bounds = |menu_state: &MenuState, viewport_size: Size| -> (f32, f32) { - // viewport space children bounds - let children_bounds = menu_state.menu_bounds.children_bounds + overlay_offset; + let max_offset = (0.0 - children_bounds.y).max(0.0); + let min_offset = + (viewport_size.height - (children_bounds.y + children_bounds.height)).min(0.0); + (max_offset, min_offset) + }; - let max_offset = (0.0 - children_bounds.y).max(0.0); - let min_offset = - (viewport_size.height - (children_bounds.y + children_bounds.height)).min(0.0); - (max_offset, min_offset) - }; + // update + if state.menu_states.is_empty() { + return Ignored; + } else if state.menu_states.len() == 1 { + let last_ms = &mut state.menu_states[0]; - // update - if state.menu_states.is_empty() { - return Ignored; - } else if state.menu_states.len() == 1 { - let last_ms = &mut state.menu_states[0]; - - if last_ms.index.is_none() { - return Captured; - } - - let (max_offset, min_offset) = calc_offset_bounds(last_ms, viewport_size); - last_ms.scroll_offset = (last_ms.scroll_offset + delta_y).clamp(min_offset, max_offset); - } else { - // >= 2 - let max_index = state.menu_states.len() - 1; - let last_two = &mut state.menu_states[max_index - 1..=max_index]; - - if last_two[1].index.is_some() { - // scroll the last one - let (max_offset, min_offset) = calc_offset_bounds(&last_two[1], viewport_size); - last_two[1].scroll_offset = - (last_two[1].scroll_offset + delta_y).clamp(min_offset, max_offset); - } else { - if !last_two[0] - .menu_bounds - .children_bounds - .contains(overlay_cursor) - { + if last_ms.index.is_none() { return Captured; } - // scroll the second last one - let (max_offset, min_offset) = calc_offset_bounds(&last_two[0], viewport_size); - let scroll_offset = (last_two[0].scroll_offset + delta_y).clamp(min_offset, max_offset); - let clamped_delta_y = scroll_offset - last_two[0].scroll_offset; - last_two[0].scroll_offset = scroll_offset; + let (max_offset, min_offset) = calc_offset_bounds(last_ms, viewport_size); + last_ms.scroll_offset = (last_ms.scroll_offset + delta_y).clamp(min_offset, max_offset); + } else { + // >= 2 + let max_index = state.menu_states.len() - 1; + let last_two = &mut state.menu_states[max_index - 1..=max_index]; - // update the bounds of the last one - last_two[1].menu_bounds.parent_bounds.y += clamped_delta_y; - last_two[1].menu_bounds.children_bounds.y += clamped_delta_y; - last_two[1].menu_bounds.check_bounds.y += clamped_delta_y; + if last_two[1].index.is_some() { + // scroll the last one + let (max_offset, min_offset) = calc_offset_bounds(&last_two[1], viewport_size); + last_two[1].scroll_offset = + (last_two[1].scroll_offset + delta_y).clamp(min_offset, max_offset); + } else { + if !last_two[0] + .menu_bounds + .children_bounds + .contains(overlay_cursor) + { + return Captured; + } + + // scroll the second last one + let (max_offset, min_offset) = calc_offset_bounds(&last_two[0], viewport_size); + let scroll_offset = + (last_two[0].scroll_offset + delta_y).clamp(min_offset, max_offset); + let clamped_delta_y = scroll_offset - last_two[0].scroll_offset; + last_two[0].scroll_offset = scroll_offset; + + // update the bounds of the last one + last_two[1].menu_bounds.parent_bounds.y += clamped_delta_y; + last_two[1].menu_bounds.children_bounds.y += clamped_delta_y; + last_two[1].menu_bounds.check_bounds.y += clamped_delta_y; + } } - } - Captured + Captured + }) } #[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)), @@ -1183,37 +1642,39 @@ where .children .iter() .map(|mt| { - let w = mt.item.as_widget(); - match w.size().height { - Length::Fixed(f) => Size::new(width, f), - Length::Shrink => { - let l_height = w - .layout( - &mut tree[mt.index], - renderer, - &Limits::new(Size::ZERO, Size::new(width, f32::MAX)), - ) - .size() - .height; + mt.item + .element + .with_data(|w| match w.as_widget().size().height { + Length::Fixed(f) => Size::new(width, f), + Length::Shrink => { + let l_height = w + .as_widget() + .layout( + &mut tree[mt.index], + renderer, + &Limits::new(Size::ZERO, Size::new(width, f32::MAX)), + ) + .size() + .height; - let height = if (f32::MAX - l_height) < 0.001 { - f32::from(d) - } else { - l_height - }; + let height = if (f32::MAX - l_height) < 0.001 { + f32::from(d) + } else { + l_height + }; - Size::new(width, height) - } - _ => mt.height.map_or_else( - || Size::new(width, f32::from(d)), - |h| Size::new(width, f32::from(h)), - ), - } + Size::new(width, height) + } + _ => mt.height.map_or_else( + || Size::new(width, f32::from(d)), + |h| Size::new(width, f32::from(h)), + ), + }) }) .collect(), }; - let max_index = menu_tree.children.len() - 1; + let max_index = menu_tree.children.len().saturating_sub(1); let child_positions: Vec = std::iter::once(0.0) .chain(child_sizes[0..max_index].iter().scan(0.0, |acc, x| { *acc += x.height; diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 1f3fd4a..d02b2b2 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -11,7 +11,7 @@ use iced_widget::core::{Element, renderer}; use crate::iced_core::{Alignment, Length}; use crate::widget::menu::action::MenuAction; use crate::widget::menu::key_bind::KeyBind; -use crate::widget::{Button, icon}; +use crate::widget::{Button, RcElementWrapper, icon}; use crate::{theme, widget}; /// Nested menu is essentially a tree of items, a menu is a collection of items @@ -23,27 +23,25 @@ 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, /// The item of the menu tree - pub(crate) item: Element<'a, Message, crate::Theme, Renderer>, + pub(crate) item: RcElementWrapper, /// 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 +53,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 +90,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,18 +107,18 @@ where } /// Flatten the menu tree - pub(crate) fn flattern(&'a self) -> Vec<&Self> { + pub(crate) fn flattern(&self) -> Vec<&Self> { /// Inner flattening function - fn rec<'a, Message, Renderer>( - mt: &'a MenuTree<'a, Message, Renderer>, - flat: &mut Vec<&MenuTree<'a, Message, Renderer>>, + fn rec<'a, Message: Clone + 'static>( + mt: &'a MenuTree, + flat: &mut Vec<&'a MenuTree>, ) { mt.children.iter().for_each(|c| { flat.push(c); }); mt.children.iter().for_each(|c| { - rec(c, flat); + rec(&c, flat); }); } @@ -132,13 +130,9 @@ where } } -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(RcElementWrapper::new(value)) } } @@ -160,6 +154,7 @@ where .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. @@ -215,20 +210,15 @@ where /// /// # Returns /// - A vector of `MenuTree`. +#[must_use] pub fn menu_items< - 'a, A: MenuAction, L: Into> + 'static, - Message, - Renderer: renderer::Renderer + 'a, + Message: 'static + std::clone::Clone, >( key_binds: &HashMap, children: Vec>, -) -> Vec> -where - Element<'a, Message, crate::Theme, Renderer>: From>, - Message: 'a + Clone, -{ +) -> Vec> { fn find_key(action: &A, key_binds: &HashMap) -> String { for (key_bind, key_action) in key_binds { if action == key_action { @@ -249,9 +239,10 @@ where match item { MenuItem::Button(label, icon, action) => { + let l: Cow<'static, str> = label.into(); let key = find_key(&action, key_binds); let mut items = vec![ - widget::text(label).into(), + widget::text(l.clone()).into(), widget::horizontal_space().into(), widget::text(key).into(), ]; @@ -261,15 +252,18 @@ where items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); } + // dbg!("button with action...", action.message()); 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 l: Cow<'static, str> = label.into(); + let key = find_key(&action, key_binds); let mut items = vec![ - widget::text(label).into(), + widget::text(l.clone()).into(), widget::horizontal_space().into(), widget::text(key).into(), ]; @@ -281,7 +275,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); @@ -311,36 +305,42 @@ 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 - }, - ), + let l: Cow<'static, str> = label.into(); + + trees.push(MenuTree::::with_children( + RcElementWrapper::new(crate::Element::from( + menu_button::<'static, _>(vec![ + widget::text(l.clone()).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/nav_bar.rs b/src/widget/nav_bar.rs index fef3cbe..6923472 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -69,7 +69,7 @@ impl<'a, Message: Clone + 'static> NavBar<'a, Message> { } #[inline] - 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_menu_bar.rs b/src/widget/responsive_menu_bar.rs index 65c5d3e..3d9557d 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -9,6 +9,7 @@ use crate::{ use super::menu::{self, ItemHeight, ItemWidth}; +#[must_use] pub fn responsive_menu_bar() -> ResponsiveMenuBar { ResponsiveMenuBar::default() } @@ -33,18 +34,21 @@ impl Default for ResponsiveMenuBar { impl ResponsiveMenuBar { /// Set the item width + #[must_use] pub fn item_width(mut self, item_width: ItemWidth) -> Self { self.item_width = item_width; self } /// Set the item height + #[must_use] pub fn item_height(mut self, item_height: ItemHeight) -> Self { self.item_height = item_height; self } /// Set the spacing + #[must_use] pub fn spacing(mut self, spacing: f32) -> Self { self.spacing = spacing; self @@ -56,14 +60,14 @@ impl ResponsiveMenuBar { pub fn into_element< 'a, Message: Clone + 'static, - A: menu::Action, + A: menu::Action + Clone, S: Into> + 'static, >( self, core: &Core, key_binds: &HashMap, id: crate::widget::Id, - action_message: impl Fn(crate::surface::Action) -> Message + 'static, + action_message: impl Fn(crate::surface::Action) -> Message + Send + Sync + Clone + 'static, trees: Vec<(S, Vec>)>, ) -> Element<'a, Message> { use crate::widget::id_container; @@ -80,17 +84,21 @@ impl ResponsiveMenuBar { menu::bar( trees .into_iter() - .map(|mt| { + .map(|mt: (S, Vec>)| { menu::Tree::<_>::with_children( - menu::root(mt.0), - menu::items(key_binds, mt.1.into()), + crate::widget::RcElementWrapper::new(Element::from( + menu::root(mt.0), + )), + menu::items(key_binds, mt.1), ) }) .collect(), ) .item_width(self.item_width) .item_height(self.item_height) - .spacing(self.spacing), + .spacing(self.spacing) + .on_surface_action(action_message.clone()) + .window_id_maybe(core.main_window_id()), crate::widget::Id::new(format!("menu_bar_expanded_{id}")), ), id, @@ -123,7 +131,9 @@ impl ResponsiveMenuBar { )]) .item_height(self.item_height) .item_width(self.collapsed_item_width) - .spacing(self.spacing), + .spacing(self.spacing) + .on_surface_action(action_message.clone()) + .window_id_maybe(core.main_window_id()), crate::widget::Id::new(format!("menu_bar_collapsed_{id}")), ), id, diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 3cb64e2..313b686 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -17,7 +17,7 @@ use iced::clipboard::mime::AllowedMimeTypes; use iced::touch::Finger; use iced::{ Alignment, Background, Color, Event, Length, Padding, Rectangle, Size, Task, Vector, alignment, - event, keyboard, mouse, touch, + event, keyboard, mouse, touch, window, }; use iced_core::mouse::ScrollDelta; use iced_core::text::{LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; @@ -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>>, @@ -198,13 +198,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, )] }); @@ -577,6 +577,7 @@ where fn state(&self) -> tree::State { #[allow(clippy::default_trait_access)] tree::State::new(LocalState { + menu_state: Default::default(), paragraphs: SecondaryMap::new(), text_hashes: SecondaryMap::new(), buttons_visible: Default::default(), @@ -955,8 +956,10 @@ where let menu_state = tree.children[0].state.downcast_mut::(); - menu_state.open = true; - menu_state.view_cursor = cursor_position; + menu_state.inner.with_data_mut(|data| { + data.open = true; + data.view_cursor = cursor_position; + }); shell.publish(on_context(key)); return event::Status::Captured; @@ -1346,7 +1349,11 @@ where let center_y = bounds.center_y(); let menu_open = !tree.children.is_empty() - && tree.children[0].state.downcast_ref::().open; + && tree.children[0] + .state + .downcast_ref::() + .inner + .with_data(|data| data.open); let key_is_active = self.model.is_active(key); let key_is_hovered = self.button_is_hovered(state, key); @@ -1556,6 +1563,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; @@ -1575,7 +1583,12 @@ where return None; }; - if !tree.children[0].state.downcast_ref::().open { + if !tree.children[0] + .state + .downcast_ref::() + .inner + .with_data(|data| data.open) + { return None; } @@ -1584,8 +1597,8 @@ where Some( crate::widget::menu::Menu { - tree: &mut tree.children[0], - 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 { @@ -1600,8 +1613,12 @@ where cross_offset: 0, root_bounds_list: vec![bounds], path_highlight: Some(PathHighlight::MenuActive), - style: &crate::theme::menu_bar::MenuBarStyle::Default, + style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default), position: Point::new(translation.x, translation.y), + is_overlay: true, + window_id: window::Id::NONE, + depth: 0, + on_surface_action: None, } .overlay(), ) @@ -1653,6 +1670,8 @@ where /// State that is maintained by each individual widget. pub struct LocalState { + /// Menu state + pub(crate) menu_state: MenuBarState, /// Defines how many buttons to show at a time. pub(super) buttons_visible: usize, /// Button visibility offset, when collapsed. diff --git a/src/widget/table/mod.rs b/src/widget/table/mod.rs index 7063dc8..c546383 100644 --- a/src/widget/table/mod.rs +++ b/src/widget/table/mod.rs @@ -20,9 +20,9 @@ pub type MultiSelectTableView<'a, Item, Category, Message> = TableView<'a, MultiSelect, Item, Category, Message>; pub type MultiSelectModel = Model; -pub fn table<'a, SelectionMode, Item, Category, Message>( - model: &'a Model, -) -> TableView<'a, SelectionMode, Item, Category, Message> +pub fn table( + model: &Model, +) -> TableView<'_, SelectionMode, Item, Category, Message> where Message: Clone, SelectionMode: Default, @@ -33,9 +33,9 @@ where TableView::new(model) } -pub fn compact_table<'a, SelectionMode, Item, Category, Message>( - model: &'a Model, -) -> CompactTableView<'a, SelectionMode, Item, Category, Message> +pub fn compact_table( + model: &Model, +) -> CompactTableView<'_, SelectionMode, Item, Category, Message> where Message: Clone, SelectionMode: Default, diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs index 43a32de..47864f6 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -44,7 +44,7 @@ where #[setters(skip)] pub(super) on_item_mb_right: Option Message + 'static>>, #[setters(skip)] - pub(super) item_context_builder: Box Option>>>, + pub(super) item_context_builder: Box Option>>>, } impl<'a, SelectionMode, Item, Category, Message> @@ -97,7 +97,7 @@ where ] }) .flatten() - .collect::>>(); + .collect::>>(); elements.pop(); elements .apply(widget::row::with_children) @@ -247,7 +247,7 @@ where pub fn item_context(mut self, context_menu_builder: F) -> Self where - F: Fn(&Item) -> Option>> + 'static, + F: Fn(&Item) -> Option>> + 'static, Message: 'static, { self.item_context_builder = Box::new(context_menu_builder); diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index 01d0ea5..eb9ba7a 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -51,7 +51,7 @@ where #[setters(skip)] pub(super) on_item_mb_right: Option Message + 'static>>, #[setters(skip)] - pub(super) item_context_builder: Box Option>>>, + pub(super) item_context_builder: Box Option>>>, // Item DND // === Category Interaction === @@ -64,8 +64,7 @@ where #[setters(skip)] pub(super) on_category_mb_right: Option Message + 'static>>, #[setters(skip)] - pub(super) category_context_builder: - Box Option>>>, + pub(super) category_context_builder: Box Option>>>, } impl<'a, SelectionMode, Item, Category, Message> @@ -83,7 +82,7 @@ where .model .categories .iter() - .cloned() + .copied() .map(|category| { let cat_context_tree = (val.category_context_builder)(category); @@ -167,7 +166,7 @@ where .align_y(Alignment::Center) .apply(Element::from) }) - .collect::>>() + .collect::>>() .apply(widget::row::with_children) .apply(container) .padding(val.item_padding) @@ -328,7 +327,7 @@ where pub fn item_context(mut self, context_menu_builder: F) -> Self where - F: Fn(&Item) -> Option>> + 'static, + F: Fn(&Item) -> Option>> + 'static, Message: 'static, { self.item_context_builder = Box::new(context_menu_builder); @@ -367,7 +366,7 @@ where pub fn category_context(mut self, context_menu_builder: F) -> Self where - F: Fn(Category) -> Option>> + 'static, + F: Fn(Category) -> Option>> + 'static, Message: 'static, { self.category_context_builder = Box::new(context_menu_builder); diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs index 0579c4b..92f26fd 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Borrow, cell::RefCell, rc::Rc, thread::{self, ThreadId}, @@ -14,6 +15,12 @@ pub struct RcWrapper { pub(crate) thread_id: ThreadId, } +impl Default for RcWrapper { + fn default() -> Self { + Self::new(T::default()) + } +} + impl Clone for RcWrapper { fn clone(&self) -> Self { Self { @@ -75,6 +82,12 @@ impl RcElementWrapper { } } +impl Borrow> for RcElementWrapper { + fn borrow(&self) -> &(dyn Widget + 'static) { + self + } +} + impl Widget for RcElementWrapper { fn size(&self) -> Size { self.element.with_data(|e| e.as_widget().size())