From 4fcd09d69088b2658ebb754b10a135759dd6f5f5 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 24 Apr 2025 18:24:10 -0400 Subject: [PATCH 01/12] wip --- src/widget/context_menu.rs | 51 +- src/widget/menu/flex.rs | 171 +++- src/widget/menu/menu_bar.rs | 219 +++-- src/widget/menu/menu_inner.rs | 1124 ++++++++++++------------- src/widget/menu/menu_tree.rs | 105 ++- src/widget/nav_bar.rs | 2 +- src/widget/segmented_button/widget.rs | 37 +- src/widget/table/widget/compact.rs | 15 +- src/widget/table/widget/standard.rs | 25 +- src/widget/wrapper.rs | 13 + 10 files changed, 999 insertions(+), 763 deletions(-) diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 87122029..79b216a0 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -13,17 +13,19 @@ use iced_core::widget::{Tree, Widget, tree}; use iced_core::{Length, Point, Size, event, mouse, touch}; use std::collections::HashSet; +use super::dropdown::menu::State; + /// 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 +45,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 +60,7 @@ impl Widget for ContextM tree::State::new(LocalState { context_cursor: Point::default(), fingers_pressed: Default::default(), + menu_state: Default::default(), }) } @@ -75,7 +80,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 @@ -183,10 +188,12 @@ impl Widget for ContextM && (right_button_released(&event) || (touch_lifted(&event) && fingers_pressed == 2)) { state.context_cursor = cursor.position().unwrap_or_default(); - let menu_state = tree.children[1].state.downcast_mut::(); - menu_state.open = true; - menu_state.view_cursor = cursor; + + menu_state.inner.with_data_mut(|state| { + state.open = true; + state.view_cursor = cursor; + }); return event::Status::Captured; } @@ -212,23 +219,28 @@ impl Widget for ContextM translation: Vector, ) -> Option> { let state = tree.state.downcast_ref::(); + let menu_state = state.menu_state.clone(); let Some(context_menu) = self.context_menu.as_mut() else { return None; }; - if !tree.children[1].state.downcast_ref::().open { + if !tree.children[1] + .state + .downcast_ref::() + .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: menu_state, + menu_roots: std::borrow::Cow::Borrowed(context_menu), bounds_expand: 16, menu_overlays_parent: true, close_condition: CloseCondition { @@ -243,7 +255,7 @@ 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), } .overlay(), @@ -263,8 +275,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 +297,5 @@ fn touch_lifted(event: &Event) -> bool { pub struct LocalState { context_cursor: Point, fingers_pressed: HashSet, + menu_state: MenuBarState, } diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs index c093e802..5eaf3d94 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 2584e762..ba4562f5 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -7,26 +7,42 @@ use super::{ }, menu_tree::MenuTree, }; -use crate::style::menu_bar::StyleSheet; +use crate::{ + Renderer, + style::menu_bar::StyleSheet, + widget::{ + RcWrapper, + dropdown::menu::{self, State}, + }, +}; -use iced::{Point, Vector}; +use iced::{Point, 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) pressed: bool, pub(crate) view_cursor: Cursor, pub(crate) open: bool, @@ -35,7 +51,7 @@ pub(crate) struct MenuBarState { pub(crate) vertical_direction: Direction, pub(crate) menu_states: Vec, } -impl MenuBarState { +impl MenuBarStateInner { pub(super) fn get_trimmed_indices(&self) -> impl Iterator + '_ { self.menu_states .iter() @@ -49,9 +65,10 @@ impl MenuBarState { 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, @@ -63,11 +80,9 @@ impl Default for MenuBarState { } } -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 +100,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 +109,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 +125,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 +143,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 @@ -141,10 +154,7 @@ pub(crate) fn menu_roots_diff( /// 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 +166,20 @@ where item_width: ItemWidth, item_height: ItemHeight, path_highlight: Option, - menu_roots: Vec>, + menu_roots: Vec>, style: ::Style, + window_id: window::Id, + #[cfg(feature = "wayland")] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, } -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 +201,9 @@ where path_highlight: Some(PathHighlight::MenuActive), menu_roots, style: ::Style::default(), + window_id: window::Id::NONE, + #[cfg(feature = "wayland")] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), } } @@ -278,10 +294,31 @@ where self.width = width; self } + + #[cfg(feature = "wayland")] + pub fn with_positioner( + mut self, + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + ) -> Self { + self.positioner = positioner; + self + } + + pub fn window_id(mut self, id: window::Id) -> Self { + self.window_id = id; + self + } + + pub fn window_id_maybe(mut self, id: Option) -> Self { + if let Some(id) = id { + self.window_id = id; + } + self + } } -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) @@ -318,7 +355,7 @@ where .iter_mut() .map(|t| &mut t.children[0]) .collect::>(); - flex::resolve( + flex::resolve_wrapper( &flex::Axis::Horizontal, renderer, &limits, @@ -361,12 +398,14 @@ where 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 - } + state.inner.with_data_mut(|state| { + 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 + } + }); } _ => (), } @@ -385,49 +424,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 { + 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(), + }; - 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>( @@ -441,14 +482,14 @@ where // return None; let state = tree.state.downcast_ref::(); - if !state.open { + if state.inner.with_data_mut(|state| !state.open) { return None; - } + }; Some( Menu { - tree, - menu_roots: &mut self.menu_roots, + tree: state.clone(), + menu_roots: std::borrow::Cow::Borrowed(&mut self.menu_roots), bounds_expand: self.bounds_expand, menu_overlays_parent: false, close_condition: self.close_condition, @@ -459,27 +500,26 @@ 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), } .overlay(), ) } } -impl<'a, Message, Renderer> From> - for Element<'a, Message, crate::Theme, Renderer> + +impl<'a, Message> From> for Element<'a, 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 +530,6 @@ fn process_root_events( viewport: &Rectangle, ) -> event::Status where - Renderer: renderer::Renderer, { menu_roots .iter_mut() @@ -498,7 +537,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 f8c0471a..21820a97 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1,10 +1,12 @@ // From iced_aw, license MIT //! Menu tree overlay +use std::borrow::Cow; + use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; use crate::style::menu_bar::StyleSheet; -use iced_core::{Border, Shadow}; +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}, @@ -238,9 +240,9 @@ struct MenuBounds { } 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 +251,7 @@ impl MenuBounds { bounds_expand: u16, parent_bounds: Rectangle, tree: &mut [Tree], - ) -> Self - where - Renderer: renderer::Renderer, - { + ) -> Self { let (children_size, child_positions, child_sizes) = get_children_layout(menu_tree, renderer, item_width, item_height, tree); @@ -294,17 +293,14 @@ pub(crate) struct MenuState { 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, @@ -339,7 +335,6 @@ impl MenuState { let limits = Limits::new(Size::ZERO, size); mt.item - .as_widget() .layout(&mut tree[mt.index], renderer, &limits) .move_to(Point::new(0.0, position + self.scroll_offset)) }) @@ -348,24 +343,21 @@ 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, @@ -426,12 +418,9 @@ impl MenuState { } } -pub(crate) struct Menu<'a, 'b, Message, Renderer> -where - Renderer: renderer::Renderer, -{ - pub(crate) tree: &'b mut Tree, - pub(crate) menu_roots: &'b mut Vec>, +pub(crate) struct Menu<'b, Message: std::clone::Clone> { + pub(crate) tree: MenuBarState, + 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,61 +432,57 @@ where pub(crate) cross_offset: i32, pub(crate) root_bounds_list: Vec, pub(crate) path_highlight: Option, - pub(crate) style: &'b ::Style, + pub(crate) style: Cow<'b, ::Style>, pub(crate) position: Point, } -impl<'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, +impl overlay::Overlay + for Menu<'_, Message> { - fn layout(&mut self, renderer: &Renderer, bounds: Size) -> Node { + fn layout(&mut self, renderer: &crate::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(); + self.tree.inner.with_data_mut(|data| { + let overlay_offset = Point::ORIGIN - position; + let tree_children = &mut data.tree.children; + let children = data + .active_root + .map(|active_root| { + let root = &self.menu_roots[active_root]; + let active_tree = &mut tree_children[active_root]; + data.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) + // overlay space viewport rectangle + Node::with_children(bounds, children).translate(Point::ORIGIN - position) + }) } fn on_event( @@ -505,7 +490,7 @@ where event: event::Event, layout: Layout<'_>, view_cursor: Cursor, - renderer: &Renderer, + renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { @@ -519,7 +504,7 @@ where }; use touch::Event::{FingerLifted, FingerMoved, FingerPressed}; - if !self.tree.state.downcast_ref::().open { + if !self.tree.inner.with_data(|data| data.open) { return Ignored; }; @@ -527,17 +512,22 @@ where 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_status = process_menu_events( - self.tree, - self.menu_roots, - event.clone(), - view_cursor, - renderer, - clipboard, - shell, - overlay_offset, - ); + let menu_roots = match &mut self.menu_roots { + Cow::Borrowed(_) => panic!(), + Cow::Owned(o) => o.as_mut_slice(), + }; + let menu_status = self.tree.inner.with_data_mut(|data| { + process_menu_events( + &mut data.tree, + menu_roots, + event.clone(), + view_cursor, + renderer, + clipboard, + shell, + overlay_offset, + ) + }); init_root_menu( self, @@ -557,9 +547,10 @@ where } 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 } @@ -579,46 +570,47 @@ where } 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 + .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 { .. }) + ) + { + state.reset(); + return Captured; + } + + if self.close_condition.click_outside && !is_inside { + 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, @@ -628,115 +620,118 @@ where #[allow(unused_results)] 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(|data| { + let Some(active_root) = data.active_root else { + return; + }; - 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 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 styling = theme.appearance(self.style); + let styling = theme.appearance(&self.style); - let tree = &self.tree.children[active_root].children; - let root = &self.menu_roots[active_root]; + let tree = &data.tree.children[active_root].children; + let root = &self.menu_roots[active_root]; - let indices = state.get_trimmed_indices().collect::>(); + let indices = data.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, - }); + data.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 < data.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(), + // react only to the last menu + let view_cursor = if i == data.menu_states.len() - 1 { + view_cursor + } else { + Cursor::Available([-1.0; 2].into()) }; - 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, + 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; + + 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(), - ..Default::default() + width: styling.border_width, + color: styling.border_color, }, shadow: Shadow::default(), }; + let menu_color = styling.background; + r.fill_quad(menu_quad, menu_color); - r.fill_quad(path_quad, styling.path); - } + // 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(), + }; - // 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(), - ); - }); - }; + r.fill_quad(path_quad, styling.path); + } - renderer.with_layer(render_bounds, draw_menu); + // draw item + menu_root.children[start_index..=end_index] + .iter() + .zip(children_layout.children()) + .for_each(|(mt, clo)| { + mt.item.draw( + &data.tree.children[mt.index], + r, + theme, + style, + clo, + view_cursor, + &children_layout.bounds(), + ); + }); + }; - // only the last menu can have a None active index - ms.index - .map_or(menu_root, |active| &menu_root.children[active]) - }); + renderer.with_layer(render_bounds, draw_menu); + + // only the last menu can have a None active index + ms.index + .map_or(menu_root, |active| &menu_root.children[active]) + }); + }) } } @@ -749,145 +744,143 @@ fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle { } } -pub(super) fn init_root_menu( - menu: &mut Menu<'_, '_, Message, Renderer>, - renderer: &Renderer, +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, -) where - Renderer: renderer::Renderer, -{ - 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.is_empty() && 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(); + for (i, (&root_bounds, mt)) in menu + .root_bounds_list + .iter() + .zip(menu.menu_roots.iter()) + .enumerate() + { + if mt.children.is_empty() { + continue; + } - state.horizontal_direction = if rb_center > view_center { - Direction::Negative - } else { - Direction::Positive - }; + if root_bounds.contains(overlay_cursor) { + let view_center = viewport_size.width * 0.5; + let rb_center = root_bounds.center_x(); - 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, - }; + state.horizontal_direction = if rb_center > view_center { + Direction::Negative + } else { + Direction::Positive + }; - 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, - ); + 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, + }; - state.active_root = Some(i); - state.menu_states.push(MenuState { - index: None, - scroll_offset: 0.0, - menu_bounds, - }); + 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, + ); - // Hack to ensure menu opens properly - shell.invalidate_layout(); + state.active_root = Some(i); + state.menu_states.push(MenuState { + index: None, + scroll_offset: 0.0, + menu_bounds, + }); - break; + // Hack to ensure menu opens properly + shell.invalidate_layout(); + + break; + } } - } + }); } #[allow(clippy::too_many_arguments)] -fn process_menu_events<'b, Message, Renderer>( +fn process_menu_events<'b, Message>( tree: &'b mut Tree, - menu_roots: &'b mut [MenuTree<'_, Message, Renderer>], + menu_roots: &'b mut [MenuTree], 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; - }; + state.inner.with_data(|state| { + let Some(active_root) = state.active_root else { + return Status::Ignored; + }; - let indices = state.get_trimmed_indices().collect::>(); + let indices = state.get_trimmed_indices().collect::>(); - if indices.is_empty() { - return Status::Ignored; - } + if indices.is_empty() { + return Status::Ignored; + } - // get active item - let mt = indices - .iter() - .fold(&mut menu_roots[active_root], |mt, &i| &mut mt.children[i]); + // get active item + let mt = indices + .iter() + .fold(&mut menu_roots[active_root], |mt, &i| &mut mt.children[i]); - // widget tree - let tree = &mut tree.children[active_root].children[mt.index]; + // widget tree + let tree = &mut tree.children[active_root].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 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); - // 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, +fn process_overlay_events( + menu: &mut Menu, + renderer: &crate::Renderer, viewport_size: Size, overlay_offset: Vector, view_cursor: Cursor, @@ -895,7 +888,7 @@ fn process_overlay_events( cross_offset: f32, ) -> event::Status where - Renderer: renderer::Renderer, + Message: std::clone::Clone, { use event::Status::{Captured, Ignored}; /* @@ -907,263 +900,260 @@ where if active item is a menu: add menu // viewport space */ - - let state = menu.tree.state.downcast_mut::(); - - let Some(active_root) = state.active_root else { - if !menu.bar_bounds.contains(overlay_cursor) { - state.reset(); - } - return Ignored; - }; - - if state.pressed { - 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; - - // * 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; + menu.tree.inner.with_data_mut(|state| { + let Some(active_root) = state.active_root else { + if !menu.bar_bounds.contains(overlay_cursor) { + state.reset(); } - 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, - ) - } - }; - - // set new index - last_menu_state.index = Some(new_index); - - // get new active item - let item = &active_menu.children[new_index]; - - // * 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]; - - // 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 Ignored; }; - 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, - ), - }); - } + if state.pressed { + return Ignored; + } - Captured + /* 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, + ) + } + }; + + // set new index + last_menu_state.index = Some(new_index); + + // get new active item + let item = &active_menu.children[new_index]; + + // * 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]; + + // 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, + }; + + 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 state.tree.children[active_root].children, + ), + }); + } + 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,32 +1173,34 @@ 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(), }; diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 1f3fd4ab..71f93231 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. @@ -216,19 +211,13 @@ where /// # Returns /// - A vector of `MenuTree`. 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 { @@ -263,7 +252,7 @@ where let menu_button = menu_button(items).on_press(action.message()); - trees.push(MenuTree::::new(menu_button)); + trees.push(MenuTree::::from(Element::from(menu_button))); } MenuItem::ButtonDisabled(label, icon, action) => { let key = find_key(&action, key_binds); @@ -281,7 +270,7 @@ where let menu_button = menu_button(items); - trees.push(MenuTree::::new(menu_button)); + trees.push(MenuTree::::from(Element::from(menu_button))); } MenuItem::CheckBox(label, icon, value, action) => { let key = find_key(&action, key_binds); @@ -311,36 +300,40 @@ where items.insert(2, widget::icon::icon(icon).size(14).into()); } - trees.push(MenuTree::new(menu_button(items).on_press(action.message()))); + trees.push(MenuTree::from(Element::from( + menu_button(items).on_press(action.message()), + ))); } MenuItem::Folder(label, children) => { - trees.push(MenuTree::::with_children( - menu_button(vec![ - widget::text(label).into(), - widget::horizontal_space().into(), - widget::icon::from_name("pan-end-symbolic") - .size(16) - .icon() - .into(), - ]) - .class( - // Menu folders have no on_press so they take on the disabled style by default - if children.is_empty() { - // This will make the folder use the disabled style if it has no children - theme::Button::MenuItem - } else { - // This will make the folder use the enabled style if it has children - theme::Button::MenuFolder - }, - ), + trees.push(MenuTree::::with_children( + RcElementWrapper::new(crate::Element::from( + menu_button::<'static, _>(vec![ + widget::text(label).into(), + widget::horizontal_space().into(), + widget::icon::from_name("pan-end-symbolic") + .size(16) + .icon() + .into(), + ]) + .class( + // Menu folders have no on_press so they take on the disabled style by default + if children.is_empty() { + // This will make the folder use the disabled style if it has no children + theme::Button::MenuItem + } else { + // This will make the folder use the enabled style if it has children + theme::Button::MenuFolder + }, + ), + )), menu_items(key_binds, children), )); } MenuItem::Divider => { if i != size - 1 { - trees.push(MenuTree::::new( + trees.push(MenuTree::::from(Element::from( widget::divider::horizontal::light(), - )); + ))); } } } diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index fef3cbe4..6923472a 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/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 3cb64e2d..c681262d 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -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,7 +1613,7 @@ 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), } .overlay(), @@ -1653,6 +1666,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/widget/compact.rs b/src/widget/table/widget/compact.rs index 43a32de2..2d060ca2 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -44,11 +44,12 @@ 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> - From> for Element<'a, Message> +impl + From> + for Element<'static, Message> where Category: ItemCategory, Item: ItemInterface, @@ -56,7 +57,7 @@ where SelectionMode: Default, Message: Clone + 'static, { - fn from(val: CompactTableView<'a, SelectionMode, Item, Category, Message>) -> Self { + fn from(val: CompactTableView<'static, SelectionMode, Item, Category, Message>) -> Self { let cosmic_theme::Spacing { space_xxxs, .. } = theme::spacing(); val.model .iter() @@ -97,7 +98,7 @@ where ] }) .flatten() - .collect::>>(); + .collect::>>(); elements.pop(); elements .apply(widget::row::with_children) @@ -171,7 +172,7 @@ where ) .apply(Element::from) }) - .collect::>>() + .collect::>>() .apply(widget::column::with_children) .spacing(val.item_spacing) .padding(val.element_padding) @@ -247,7 +248,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 01d0ea56..112d673f 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,12 +64,11 @@ 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> - From> for Element<'a, Message> +impl + From> for Element<'static, Message> where Category: ItemCategory, Item: ItemInterface, @@ -77,13 +76,13 @@ where SelectionMode: Default, Message: Clone + 'static, { - fn from(val: TableView<'a, SelectionMode, Item, Category, Message>) -> Self { + fn from(val: TableView<'static, SelectionMode, Item, Category, Message>) -> Self { // Header row let header_row = val .model .categories .iter() - .cloned() + .copied() .map(|category| { let cat_context_tree = (val.category_context_builder)(category); @@ -126,7 +125,7 @@ where .apply(|mouse_area| widget::context_menu(mouse_area, cat_context_tree)) .apply(Element::from) }) - .collect::>>() + .collect::>>() .apply(widget::row::with_children) .apply(Element::from); // Build the items @@ -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) @@ -235,12 +234,12 @@ where ] }) .flatten() - .collect::>>() + .collect::>>() }; vec![vec![header_row], items_full] .into_iter() .flatten() - .collect::>>() + .collect::>>() .apply(widget::column::with_children) .width(val.width) .height(val.height) @@ -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 0579c4b2..92f26fd4 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()) From 9e9449d3020c417b035b7488f7b799de55e94952 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 25 Apr 2025 00:28:08 -0400 Subject: [PATCH 02/12] wip --- src/widget/context_menu.rs | 5 +- src/widget/menu/menu_bar.rs | 90 +++- src/widget/menu/menu_inner.rs | 716 ++++++++++++++++++++------ src/widget/responsive_menu_bar.rs | 24 +- src/widget/segmented_button/widget.rs | 5 +- 5 files changed, 649 insertions(+), 191 deletions(-) diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 79b216a0..46dc1b88 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -8,7 +8,7 @@ use crate::widget::menu::{ }; 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; @@ -257,6 +257,9 @@ impl Widget path_highlight: Some(PathHighlight::MenuActive), 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, } .overlay(), ) diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index ba4562f5..12808ee3 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -1,6 +1,8 @@ // From iced_aw, license MIT //! A widget that handles menu trees +use std::collections::HashMap; + use super::{ menu_inner::{ CloseCondition, Direction, ItemHeight, ItemWidth, Menu, MenuState, PathHighlight, @@ -43,25 +45,30 @@ pub(crate) struct MenuBarState { 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: HashMap>, pub(crate) horizontal_direction: Direction, pub(crate) vertical_direction: Direction, - pub(crate) menu_states: Vec, + pub(crate) menu_states: HashMap>, } impl MenuBarStateInner { - pub(super) fn get_trimmed_indices(&self) -> impl Iterator + '_ { + pub(super) fn get_trimmed_indices(&self, id: &window::Id) -> impl Iterator + '_ { self.menu_states - .iter() + .get(id) + .into_iter() + .map(|v| v.iter()) + .flatten() .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 = HashMap::new(); self.menu_states.clear(); } } @@ -72,10 +79,12 @@ impl Default for MenuBarStateInner { pressed: false, view_cursor: Cursor::Available([-0.5, -0.5].into()), open: false, - active_root: None, + active_root: HashMap::new(), horizontal_direction: Direction::Positive, vertical_direction: Direction::Positive, - menu_states: Vec::new(), + menu_states: HashMap::new(), + popup_id: HashMap::new(), + bar_pressed: false, } } } @@ -171,6 +180,7 @@ pub struct MenuBar { window_id: window::Id, #[cfg(feature = "wayland")] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + pub(crate) on_surface_action: Option Message>>, } impl MenuBar @@ -182,7 +192,13 @@ where pub fn new(menu_roots: Vec>) -> Self { let mut menu_roots = menu_roots; menu_roots.iter_mut().for_each(MenuTree::set_index); - + // println!("======================================================"); + // for menu_root in &menu_roots { + // dbg!(menu_root.index); + // for inner_root in &menu_root.children { + // dbg!(inner_root.index); + // } + // } Self { width: Length::Shrink, height: Length::Shrink, @@ -204,6 +220,7 @@ where window_id: window::Id::NONE, #[cfg(feature = "wayland")] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), + on_surface_action: None, } } @@ -315,6 +332,14 @@ where } self } + + pub fn on_surface_action( + mut self, + handler: impl Fn(crate::surface::Action) -> Message + 'static, + ) -> Self { + self.on_surface_action = Some(Box::new(handler)); + self + } } impl Widget for MenuBar where @@ -325,7 +350,10 @@ where } 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 { @@ -394,21 +422,38 @@ where viewport, ); - let state = tree.state.downcast_mut::(); + let my_state = tree.state.downcast_mut::(); - match event { - Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => { - state.inner.with_data_mut(|state| { + // 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); + + my_state.inner.with_data_mut(|state| { + if reset { + if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { + dbg!("reset destroy"); + // TODO emit message + if let Some(handler) = self.on_surface_action.as_ref() { + shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); + state.reset(); + } + } + } + + let tree = &mut state.tree; + + 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 } - }); + } + _ => (), } - _ => (), - } + }); + root_status } @@ -434,10 +479,14 @@ where // draw path highlight if self.path_highlight.is_some() { let styling = theme.appearance(&self.style); - if let Some(active) = state.active_root { + if let Some(active) = state + .active_root + .get(&self.window_id) + .and_then(|active| active.get(0)) + { let active_bounds = layout .children() - .nth(active) + .nth(*active) .expect("Active child not found in menu?") .bounds(); let path_quad = renderer::Quad { @@ -489,7 +538,7 @@ where Some( Menu { tree: state.clone(), - menu_roots: std::borrow::Cow::Borrowed(&mut self.menu_roots), + menu_roots: std::borrow::Cow::Owned(self.menu_roots.clone()), bounds_expand: self.bounds_expand, menu_overlays_parent: false, close_condition: self.close_condition, @@ -502,6 +551,9 @@ where path_highlight: self.path_highlight, style: std::borrow::Cow::Borrowed(&self.style), position: Point::new(translation.x, translation.y), + is_overlay: false, + window_id: window::Id::NONE, + depth: 0, } .overlay(), ) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 21820a97..dc6595f2 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -6,6 +6,7 @@ use std::borrow::Cow; use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; use crate::style::menu_bar::StyleSheet; +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, @@ -229,6 +230,7 @@ pub(super) struct MenuSlice { pub(super) upper_bound_rel: f32, } +#[derive(Clone)] /// Menu bounds in overlay space struct MenuBounds { child_positions: Vec, @@ -251,6 +253,7 @@ impl MenuBounds { bounds_expand: u16, parent_bounds: Rectangle, tree: &mut [Tree], + is_overlay: bool, ) -> Self { let (children_size, child_positions, child_sizes) = get_children_layout(menu_tree, renderer, item_width, item_height, tree); @@ -261,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 @@ -287,6 +294,7 @@ impl MenuBounds { } } +#[derive(Clone)] pub(crate) struct MenuState { pub(super) index: Option, scroll_offset: f32, @@ -298,7 +306,7 @@ impl MenuState { overlay_offset: Vector, slice: MenuSlice, renderer: &crate::Renderer, - menu_tree: &MenuTree, + menu_tree: &[MenuTree], tree: &mut [Tree], ) -> Node { let MenuSlice { @@ -308,21 +316,33 @@ impl MenuState { upper_bound_rel, } = slice; - assert_eq!( - menu_tree.children.len(), - self.menu_bounds.child_positions.len() - ); + // assert_eq!( + // menu_tree.children.len(), + // self.menu_bounds.child_positions.len() + // ); + // dbg!( + // overlay_offset, + // menu_tree.index, + // menu_tree.width, + // menu_tree.height + // ); + // for mt in &menu_tree.children[start_index..=end_index] { + // dbg!(mt.index); + // } + dbg!((start_index, end_index)); // viewport space children bounds let children_bounds = self.menu_bounds.children_bounds + overlay_offset; - + dbg!(children_bounds); 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()) - .map(|((cp, size), mt)| { + .zip(menu_tree[start_index..=end_index].iter()) + .enumerate() + .map(|(i, ((cp, size), mt))| { let mut position = *cp; let mut size = *size; + dbg!(position, size); if position < lower_bound_rel && (position + size.height) > lower_bound_rel { size.height = position + size.height - lower_bound_rel; @@ -331,8 +351,13 @@ impl MenuState { { size.height = upper_bound_rel - position; } + // dbg!(size); - let limits = Limits::new(Size::ZERO, size); + let limits = Limits::new(size, size); + dbg!(i, mt.index, tree.len()); + for child in &mt.children { + dbg!(child.index); + } mt.item .layout(&mut tree[mt.index], renderer, &limits) @@ -340,6 +365,7 @@ impl MenuState { }) .collect::>(); + // dbg!(children_bounds.size()); Node::with_children(children_bounds.size(), child_nodes).move_to(children_bounds.position()) } @@ -434,54 +460,94 @@ pub(crate) struct Menu<'b, Message: std::clone::Clone> { pub(crate) path_highlight: Option, 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, } 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> -{ - fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> Node { + fn layout(&mut self, renderer: &crate::Renderer, limits: Limits) -> Node { // layout children let position = self.position; + let mut intrinsic_size = Size::ZERO; + self.tree.inner.with_data_mut(|data| { let overlay_offset = Point::ORIGIN - position; let tree_children = &mut data.tree.children; let children = data .active_root + .get(&self.window_id) + .cloned() .map(|active_root| { - let root = &self.menu_roots[active_root]; - let active_tree = &mut tree_children[active_root]; - data.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 (active_tree, roots) = if self.depth > 0 { + active_root.iter().skip(1).take(self.depth).fold( + ( + &mut tree_children[active_root[0]].children, + &self.menu_roots[active_root[0]].children, + ), + |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), + ) + } else { + (tree_children, self.menu_roots.as_ref()) + }; + + dbg!(roots.len()); + dbg!(&active_root); + dbg!(active_tree.len()); + // dbg!( + // self.window_id, + // limits.max(), + // my_ref_mut.menu_states[&self.window_id].len() + // ); + // dbg!(my_ref_mut.active_root.get(&self.window_id)); + // for (i, active_tree) in active_tree.iter().enumerate() { + // dbg!(i); + // for (i2, active_tree) in active_tree.children.iter().enumerate() { + // dbg!(i2); + // for (i3, active_tree) in active_tree.children.iter().enumerate() { + // dbg!(i3); + // } + // } + // } + data.menu_states[&self.window_id] + .iter() + .enumerate() + .filter(|ms| self.is_overlay || ms.0 < active_root.len()) + .fold((roots, Vec::new()), |(menu_root, mut nodes), (_i, ms)| { + dbg!(ms.index); + 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, - &mut active_tree.children, - ); + dbg!(&self.window_id, menu_root.len()); + let children_node = + ms.layout(overlay_offset, slice, renderer, menu_root, active_tree); + let node_size = children_node.size(); + // dbg!(node_size.height); + 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 + // dbg!(ms.index); ( ms.index - .map_or(menu_root, |active| &menu_root.children[active]), + .map_or(menu_root, |active| &menu_root[active].children), nodes, ) - }, - ) + }) }) .map(|(_, l)| l) .unwrap_or_default(); - + // dbg!(intrinsic_size); // overlay space viewport rectangle - Node::with_children(bounds, children).translate(Point::ORIGIN - position) + Node::with_children( + limits.resolve(Length::Shrink, Length::Shrink, intrinsic_size), + children, + ) + .translate(Point::ORIGIN - position) }) } @@ -493,7 +559,7 @@ impl overlay::Overlay 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}, @@ -505,7 +571,7 @@ impl overlay::Overlay use touch::Event::{FingerLifted, FingerMoved, FingerPressed}; if !self.tree.inner.with_data(|data| data.open) { - return Ignored; + return (None, Ignored); }; let viewport = layout.bounds(); @@ -516,18 +582,15 @@ impl overlay::Overlay Cow::Borrowed(_) => panic!(), Cow::Owned(o) => o.as_mut_slice(), }; - let menu_status = self.tree.inner.with_data_mut(|data| { - process_menu_events( - &mut data.tree, - menu_roots, - event.clone(), - view_cursor, - renderer, - clipboard, - shell, - overlay_offset, - ) - }); + let menu_status = process_menu_events( + self, + event.clone(), + view_cursor, + renderer, + clipboard, + shell, + overlay_offset, + ); init_root_menu( self, @@ -540,7 +603,7 @@ impl overlay::Overlay 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) @@ -557,7 +620,12 @@ impl overlay::Overlay 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); + } + // dbg!(view_cursor, viewport, self.window_id); + + let (new_root, status) = process_overlay_events( self, renderer, viewport_size, @@ -565,8 +633,9 @@ impl overlay::Overlay view_cursor, overlay_cursor, self.cross_offset as f32, - ) - .merge(menu_status) + self.is_overlay, + ); + return (new_root, status.merge(menu_status)); } Mouse(ButtonReleased(_)) | Touch(FingerLifted { .. }) => { @@ -581,8 +650,7 @@ impl overlay::Overlay .distance(view_cursor.position().unwrap_or_default()) < 2.0 { - let is_inside = state - .menu_states + let is_inside = state.menu_states[&self.window_id] .iter() .any(|ms| ms.menu_bounds.check_bounds.contains(overlay_cursor)); @@ -614,7 +682,8 @@ impl overlay::Overlay } _ => menu_status, - } + }; + (None, ret) } #[allow(unused_results)] @@ -626,36 +695,63 @@ impl overlay::Overlay layout: Layout<'_>, view_cursor: Cursor, ) { - self.tree.inner.with_data(|data| { - let Some(active_root) = data.active_root else { + self.tree.inner.with_data(|state| { + let Some(active_root) = state.active_root.get(&self.window_id) else { + // dbg!(self.window_id); return; }; - 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) + }; + // dbg!(self.window_id, &active_root); let styling = theme.appearance(&self.style); + let (active_tree, roots) = if self.depth > 0 { + active_root.iter().skip(1).take(self.depth).fold( + ( + &state.tree.children[active_root[0]].children, + &self.menu_roots[active_root[0]].children, + ), + |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), + ) + } else { + (&state.tree.children, self.menu_roots.as_ref()) + }; + // let tree = &state.tree.children[active_root].children; + // let root = &self.menu_roots[active_root]; - let tree = &data.tree.children[active_root].children; - let root = &self.menu_roots[active_root]; - - let indices = data.get_trimmed_indices().collect::>(); - - data.menu_states + let indices = state + .get_trimmed_indices(&self.window_id) + .collect::>(); + // dbg!( + // self.window_id, + // state.menu_states[&self.window_id].len(), + // layout.children().count(), + // root.children.len(), + // layout.bounds() + // ); + state.menu_states[&self.window_id] .iter() .zip(layout.children()) .enumerate() - .fold(root, |menu_root, (i, (ms, children_layout))| { + .filter(|ms: &(usize, (&MenuState, Layout<'_>))| { + self.is_overlay || ms.0 < active_root.len() + }) + .fold(roots, |menu_roots, (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 < data.menu_states.len() - 1, + PathHighlight::MenuActive => i < state.menu_states.len() - 1, }); // react only to the last menu - let view_cursor = if i == data.menu_states.len() - 1 { + let view_cursor = if i == state.menu_states.len() - 1 { view_cursor } else { Cursor::Available([-1.0; 2].into()) @@ -691,49 +787,89 @@ impl overlay::Overlay // draw path hightlight if let (true, Some(active)) = (draw_path, ms.index) { - let active_bounds = children_layout + if let Some(active_layout) = 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(), - }; + { + 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); + 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( + &active_tree[mt.index], + r, + theme, + style, + clo, + view_cursor, + &children_layout.bounds(), + ); + }); } - - // draw item - menu_root.children[start_index..=end_index] - .iter() - .zip(children_layout.children()) - .for_each(|(mt, clo)| { - mt.item.draw( - &data.tree.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_root, |active| &menu_root.children[active]) + .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); + } +} fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle { Rectangle { @@ -755,10 +891,60 @@ pub(super) fn init_root_menu( main_offset: f32, ) { menu.tree.inner.with_data_mut(|state| { - if !(state.menu_states.is_empty() && bar_bounds.contains(overlay_cursor)) { + if !(state + .menu_states + .get(&menu.window_id) + .is_none_or(|s| s.is_empty()) + && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) + { return; } + println!( + "init root menu {} {:?}", + menu.window_id, menu.root_bounds_list + ); + dbg!(menu.menu_roots.len()); + // dbg!(state + // .menu_states + // .get(&menu.window_id) + // .is_none_or(|s| s.is_empty())); + // dbg!(menu + // .menu_roots + // .as_slice() + // .iter() + // .map(|r| r.index) + // .collect::>()); + + // dbg!(state + // .active_root + // .get(&menu.window_id) + // .into_iter() + // .flatten() + // .count() + // .saturating_sub(1)); + // let root = state + // .active_root + // .get(&menu.window_id) + // .iter() + // .map(|l| l.into_iter()) + // .flatten() + // .take( + // state + // .active_root + // .get(&menu.window_id) + // .into_iter() + // .flatten() + // .count() + // .saturating_sub(1), + // ) + // .fold(menu.menu_roots.as_slice(), |m, i| { + // dbg!(m.len()); + // m[*i].children.as_slice() + // }); + // dbg!(root.len()); + + let mut set = false; for (i, (&root_bounds, mt)) in menu .root_bounds_list .iter() @@ -766,10 +952,140 @@ pub(super) fn init_root_menu( .enumerate() { if mt.children.is_empty() { + // dbg!("skipping menu with no children"); continue; } - + // dbg!(i, root_bounds.contains(overlay_cursor)); if root_bounds.contains(overlay_cursor) { + dbg!(root_bounds); + // dbg!(i, root_bounds, mt.width, mt.height); + 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; + // dbg!(i); + // dbg!("inserting root menu state"); + // dbg!(&menu_bounds.children_bounds); + // dbg!((&menu_bounds.child_positions)); + state.active_root.insert(menu.window_id, vec![i]); + // do we need to insert the rest now too? + let ms = MenuState { + index: None, + scroll_offset: 0.0, + menu_bounds, + }; + let v = state.menu_states.entry(menu.window_id).or_default(); + v.push(ms); + + // Hack to ensure menu opens properly + shell.invalidate_layout(); + + break; + } + } + if !set { + dbg!(overlay_cursor); + } + }); +} + +pub(super) fn init_root_popup_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, +) where + Message: std::clone::Clone, +{ + menu.tree.inner.with_data_mut(|state| { + if !(state + .menu_states + .get(&menu.window_id) + .is_none_or(|s| s.is_empty()) + && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) + { + return; + } + + println!( + "init root menu {} {:?}", + menu.window_id, menu.root_bounds_list + ); + // dbg!(state + // .menu_states + // .get(&menu.window_id) + // .is_none_or(|s| s.is_empty())); + // dbg!(menu + // .menu_roots + // .as_slice() + // .iter() + // .map(|r| r.index) + // .collect::>()); + + // dbg!(state + // .active_root + // .get(&menu.window_id) + // .into_iter() + // .flatten() + // .count()); + let active_roots = state + .active_root + .get(&menu.window_id) + .cloned() + .unwrap_or_default(); + dbg!(&active_roots); + dbg!(menu.menu_roots.len()); + let root = active_roots + .iter() + .take(active_roots.iter().count().saturating_sub(1)) + .fold(menu.menu_roots.as_slice(), |m, i| { + // dbg!(m.len()); + m[*i].children.as_slice() + }); + // dbg!(root.len()); + + let mut set = false; + for (i, (&root_bounds, mt)) in menu.root_bounds_list.iter().zip(root.iter()).enumerate() { + if mt.children.is_empty() { + // dbg!("skipping menu with no children"); + continue; + } + // dbg!(i, root_bounds.contains(overlay_cursor)); + if root_bounds.contains(overlay_cursor) { + // dbg!(i, root_bounds, mt.width, mt.height); let view_center = viewport_size.width * 0.5; let rb_center = root_bounds.center_x(); @@ -790,6 +1106,11 @@ pub(super) fn init_root_menu( vertical_offset: main_offset, }; + // dbg!( + // state.tree.children.len(), + // state.tree.children[0].children.len(), + // ); + let menu_bounds = MenuBounds::new( mt, renderer, @@ -800,15 +1121,19 @@ pub(super) fn init_root_menu( &aod, menu.bounds_expand, root_bounds, + // TODO how to select the tree for the popup &mut state.tree.children[0].children, + menu.is_overlay, ); + set = true; - state.active_root = Some(i); - state.menu_states.push(MenuState { + let ms = MenuState { index: None, scroll_offset: 0.0, menu_bounds, - }); + }; + let v = state.menu_states.entry(menu.window_id).or_default(); + v.push(ms); // Hack to ensure menu opens properly shell.invalidate_layout(); @@ -816,13 +1141,15 @@ pub(super) fn init_root_menu( break; } } - }); + if !set { + dbg!(overlay_cursor); + } + }) } #[allow(clippy::too_many_arguments)] -fn process_menu_events<'b, Message>( - tree: &'b mut Tree, - menu_roots: &'b mut [MenuTree], +fn process_menu_events<'b, Message: std::clone::Clone>( + menu: &'b mut Menu, event: event::Event, view_cursor: Cursor, renderer: &crate::Renderer, @@ -832,28 +1159,45 @@ fn process_menu_events<'b, Message>( ) -> event::Status { use event::Status; - let state = tree.state.downcast_mut::(); - state.inner.with_data(|state| { - let Some(active_root) = state.active_root else { + let window_id = menu.window_id; + let is_overlay = menu.is_overlay; + 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| { + let Some(active_root) = state.active_root.get(&window_id).cloned() else { return Status::Ignored; }; - let indices = state.get_trimmed_indices().collect::>(); + let indices = state.get_trimmed_indices(&window_id); + + let indices = if is_overlay { + indices.collect::>() + } else { + indices.take(1).collect::>() + }; if indices.is_empty() { return Status::Ignored; } // get active item - let mt = indices - .iter() - .fold(&mut menu_roots[active_root], |mt, &i| &mut mt.children[i]); + // let mt = indices.iter().fold(root | mt, &i | &mut mt.children[i]); + let (tree, mt) = active_root.iter().take(menu.depth).skip(1).fold( + ( + &mut state.tree.children[active_root[0]].children, + &mut menu_roots[active_root[0]], + ), + |(tree, mt), next_active_root| (tree, &mut mt.children[*next_active_root]), + ); // widget tree - let tree = &mut tree.children[active_root].children[mt.index]; + let tree = &mut tree[mt.index]; // get layout - let last_ms = &state.menu_states[indices.len() - 1]; + let last_ms = &state.menu_states[&window_id][indices.len() - 1]; let child_node = last_ms.layout_single( overlay_offset, last_ms.index.expect("missing index within menu state."), @@ -886,7 +1230,8 @@ fn process_overlay_events( view_cursor: Cursor, overlay_cursor: Point, cross_offset: f32, -) -> event::Status + is_overlay: bool, +) -> (Option<(usize, MenuState)>, event::Status) where Message: std::clone::Clone, { @@ -900,16 +1245,18 @@ where if active item is a menu: add menu // viewport space */ + let mut new_menu_root = None; + menu.tree.inner.with_data_mut(|state| { - let Some(active_root) = state.active_root else { - if !menu.bar_bounds.contains(overlay_cursor) { + let Some(active_root) = state.active_root.get(&menu.window_id).clone() else { + if is_overlay && !menu.bar_bounds.contains(overlay_cursor) { state.reset(); } - return Ignored; + return (new_menu_root, Ignored); }; if state.pressed { - return Ignored; + return (new_menu_root, Ignored); } /* When overlay is running, cursor_position in any widget method will go negative @@ -919,18 +1266,20 @@ where // * remove invalid menus let mut prev_bounds = std::iter::once(menu.bar_bounds) .chain( - state.menu_states[..state.menu_states.len().saturating_sub(1)] + state.menu_states[&menu.window_id] + [..state.menu_states.len().saturating_sub(1).min(1)] .iter() .map(|ms| ms.menu_bounds.children_bounds), ) .collect::>(); + let menu_states = state.menu_states.get_mut(&menu.window_id).unwrap(); if menu.close_condition.leave { - for i in (0..state.menu_states.len()).rev() { - let mb = &state.menu_states[i].menu_bounds; + for i in (0..menu_states.len()).rev() { + let mb = &menu_states[i].menu_bounds; if mb.parent_bounds.contains(overlay_cursor) - || mb.children_bounds.contains(overlay_cursor) + || 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))) @@ -938,11 +1287,11 @@ where break; } prev_bounds.pop(); - state.menu_states.pop(); + menu_states.pop(); } } else { - for i in (0..state.menu_states.len()).rev() { - let mb = &state.menu_states[i].menu_bounds; + for i in (0..menu_states.len()).rev() { + let mb = &menu_states[i].menu_bounds; if mb.parent_bounds.contains(overlay_cursor) || mb.children_bounds.contains(overlay_cursor) @@ -950,22 +1299,21 @@ where { break; } + // dbg!(menu_states.len()); prev_bounds.pop(); - state.menu_states.pop(); + menu_states.pop(); } } // get indices - let indices = state - .menu_states - .iter() - .map(|ms| ms.index) - .collect::>(); + let indices = menu_states.iter().map(|ms| ms.index).collect::>(); + let should_add = is_overlay || menu_states.len() < 2; // * update active item - let Some(last_menu_state) = state.menu_states.last_mut() else { + let Some(last_menu_state) = menu_states.last_mut() else { // no menus left - state.active_root = None; + // TODO do we want to avoid this for popups? + state.active_root.remove(&menu.window_id); // keep state.open when the cursor is still inside the menu bar // this allows the overlay to keep drawing when the cursor is @@ -973,20 +1321,20 @@ where if !menu.bar_bounds.contains(overlay_cursor) { state.open = false; } - return Captured; + return (new_menu_root, 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) + if (is_overlay && !menu.menu_overlays_parent && last_parent_bounds.contains(overlay_cursor)) + // cursor is in the parent part + || is_overlay && !last_children_bounds.contains(overlay_cursor) // cursor is outside { last_menu_state.index = None; - return Captured; + return (new_menu_root, Captured); } // cursor is in the children part @@ -994,19 +1342,41 @@ where 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")] - }); - + // dbg!(height_diff); + // let (tree, active_menu_root) = active_root.iter().skip(1).fold( + // ( + // &mut state.tree.children[active_root[0]].children, + // &menu.menu_roots[active_root[0]], + // ), + // |(tree, mr), next_active_root| (tree, &mr.children[*next_active_root]), + // ); + let (active_tree, roots) = if menu.depth > 0 { + active_root.iter().skip(1).take(menu.depth).fold( + ( + &mut state.tree.children[active_root[0]].children, + &menu.menu_roots[active_root[0]].children, + ), + |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), + ) + } else { + (&mut state.tree.children, menu.menu_roots.as_ref()) + }; + let active_menu = if is_overlay { + indices[0..indices.len().saturating_sub(1)] + .iter() + .fold(roots, |mt, i| { + &mt[i.expect("missing active child index in menu")].children + }) + } else { + // popup does one layer deep + roots + }; + // dbg!(active_menu.children.len()); + // dbg!(menu.window_id); 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; + let max_index = active_menu.len() - 1; search_bound( 0, 0, @@ -1018,14 +1388,26 @@ where } }; + let item = &active_menu[new_index]; + // dbg!(new_index); + // dbg!(item.index); + // if the index changes, get a new root + // if !last_menu_state.index.is_some_and(|old| old == new_index) && !item.children.is_empty() { + // dbg!(&indices); + // dbg!(new_index); + // dbg!(item.children.len()); + // dbg!(menu.window_id); + // new_menu_root = Some(new_index); + // } + // set new index - last_menu_state.index = Some(new_index); + let old_index = last_menu_state.index.replace(new_index); + // dbg!(should_add); // get new active item - let item = &active_menu.children[new_index]; - // * add new menu if the new item is a menu if !item.children.is_empty() { + // dbg!("its a menu"); let item_position = Point::new( 0.0, last_menu_bounds.child_positions[new_index] + last_menu_state.scroll_offset, @@ -1046,8 +1428,7 @@ where horizontal_offset: cross_offset, vertical_offset: 0.0, }; - - state.menu_states.push(MenuState { + let ms = MenuState { index: None, scroll_offset: 0.0, menu_bounds: MenuBounds::new( @@ -1060,11 +1441,21 @@ where &aod, menu.bounds_expand, item_bounds, - &mut state.tree.children[active_root].children, + active_tree, + menu.is_overlay, ), - }); + }; + if !old_index.is_some_and(|old| old == new_index) { + // dbg!("adding new menu"); + new_menu_root = Some((new_index, ms.clone())); + } + if should_add { + let v = state.menu_states.entry(menu.window_id).or_default(); + v.push(ms); + } } - Captured + + (new_menu_root, Captured) }) } @@ -1096,12 +1487,13 @@ where (viewport_size.height - (children_bounds.y + children_bounds.height)).min(0.0); (max_offset, min_offset) }; + let menu_states = state.menu_states.get_mut(&menu.window_id).unwrap(); // update - if state.menu_states.is_empty() { + if menu_states.is_empty() { return Ignored; - } else if state.menu_states.len() == 1 { - let last_ms = &mut state.menu_states[0]; + } else if menu_states.len() == 1 { + let last_ms = &mut menu_states[0]; if last_ms.index.is_none() { return Captured; @@ -1111,8 +1503,8 @@ where 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]; + let max_index = menu_states.len() - 1; + let last_two = &mut menu_states[max_index - 1..=max_index]; if last_two[1].index.is_some() { // scroll the last one diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index 65c5d3eb..b08e86ea 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use apply::Apply; +use iced::window; use crate::{ Core, Element, @@ -80,17 +81,23 @@ impl ResponsiveMenuBar { menu::bar( trees .into_iter() - .map(|mt| { - menu::Tree::<_>::with_children( - menu::root(mt.0), - menu::items(key_binds, mt.1.into()), - ) - }) + .map( + |mt: ( + std::borrow::Cow<'_, str>, + Vec>>, + )| { + menu::Tree::<_>::with_children( + menu::root(mt.0), + menu::items(key_binds, mt.1.into()), + ) + }, + ) .collect(), ) .item_width(self.item_width) .item_height(self.item_height) - .spacing(self.spacing), + .spacing(self.spacing) + .window_id_maybe(core.main_window_id()), crate::widget::Id::new(format!("menu_bar_expanded_{id}")), ), id, @@ -123,7 +130,8 @@ impl ResponsiveMenuBar { )]) .item_height(self.item_height) .item_width(self.collapsed_item_width) - .spacing(self.spacing), + .spacing(self.spacing) + .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 c681262d..ba57719d 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}; @@ -1615,6 +1615,9 @@ where path_highlight: Some(PathHighlight::MenuActive), 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, } .overlay(), ) From 2cfef8814ec37e66f7b3de3ae2a139b5e129d544 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 29 Apr 2025 13:16:02 -0400 Subject: [PATCH 03/12] wip --- src/widget/menu/menu_bar.rs | 38 +++--- src/widget/menu/menu_inner.rs | 224 +++++++++++++++++++--------------- 2 files changed, 147 insertions(+), 115 deletions(-) diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 12808ee3..2cd44ec3 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -50,15 +50,15 @@ pub(crate) struct MenuBarStateInner { pub(crate) bar_pressed: bool, pub(crate) view_cursor: Cursor, pub(crate) open: bool, - pub(crate) active_root: HashMap>, + pub(crate) active_root: Vec>, pub(crate) horizontal_direction: Direction, pub(crate) vertical_direction: Direction, - pub(crate) menu_states: HashMap>, + pub(crate) menu_states: Vec>, } impl MenuBarStateInner { - pub(super) fn get_trimmed_indices(&self, id: &window::Id) -> impl Iterator + '_ { + pub(super) fn get_trimmed_indices(&self, index: usize) -> impl Iterator + '_ { self.menu_states - .get(id) + .get(index) .into_iter() .map(|v| v.iter()) .flatten() @@ -68,7 +68,7 @@ impl MenuBarStateInner { pub(super) fn reset(&mut self) { self.open = false; - self.active_root = HashMap::new(); + self.active_root = Vec::new(); self.menu_states.clear(); } } @@ -79,10 +79,10 @@ impl Default for MenuBarStateInner { pressed: false, view_cursor: Cursor::Available([-0.5, -0.5].into()), open: false, - active_root: HashMap::new(), + active_root: Vec::new(), horizontal_direction: Direction::Positive, vertical_direction: Direction::Positive, - menu_states: HashMap::new(), + menu_states: Vec::new(), popup_id: HashMap::new(), bar_pressed: false, } @@ -161,6 +161,15 @@ where } } +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 { @@ -479,11 +488,7 @@ where // draw path highlight if self.path_highlight.is_some() { let styling = theme.appearance(&self.style); - if let Some(active) = state - .active_root - .get(&self.window_id) - .and_then(|active| active.get(0)) - { + if let Some(active) = get_mut_or_default(&mut state.active_root, 0).get(0) { let active_bounds = layout .children() .nth(*active) @@ -527,11 +532,14 @@ where _renderer: &Renderer, translation: Vector, ) -> Option> { - // #[cfg(feature = "wayland")] - // return None; + //#[cfg(feature = "wayland")] + //return None; let state = tree.state.downcast_ref::(); - if state.inner.with_data_mut(|state| !state.open) { + if state + .inner + .with_data_mut(|state| !state.open || state.active_root.is_empty()) + { return None; }; diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index dc6595f2..905d2eed 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -329,7 +329,7 @@ impl MenuState { // for mt in &menu_tree.children[start_index..=end_index] { // dbg!(mt.index); // } - dbg!((start_index, end_index)); + dbg!((start_index, end_index, menu_tree.len())); // viewport space children bounds let children_bounds = self.menu_bounds.children_bounds + overlay_offset; @@ -360,7 +360,7 @@ impl MenuState { } mt.item - .layout(&mut tree[mt.index], renderer, &limits) + .layout(&mut tree[i], renderer, &limits) .move_to(Point::new(0.0, position + self.scroll_offset)) }) .collect::>(); @@ -469,21 +469,28 @@ 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)) } + fn layout(&mut self, renderer: &crate::Renderer, limits: Limits) -> Node { - // layout children + // layout children; let position = self.position; let mut intrinsic_size = Size::ZERO; + dbg!(self.depth); + let empty = Vec::new(); self.tree.inner.with_data_mut(|data| { let overlay_offset = Point::ORIGIN - position; let tree_children = &mut data.tree.children; + dbg!(&data.active_root,); let children = data .active_root - .get(&self.window_id) + .get(self.depth) .cloned() .map(|active_root| { + if active_root.is_empty() || self.menu_roots.is_empty() { + return (&empty, vec![]); + } let (active_tree, roots) = if self.depth > 0 { - active_root.iter().skip(1).take(self.depth).fold( + active_root.iter().take(self.depth).fold( ( &mut tree_children[active_root[0]].children, &self.menu_roots[active_root[0]].children, @@ -491,7 +498,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), ) } else { - (tree_children, self.menu_roots.as_ref()) + (tree_children, &self.menu_roots[active_root[0]].children) }; dbg!(roots.len()); @@ -512,32 +519,41 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { // } // } // } - data.menu_states[&self.window_id] - .iter() - .enumerate() - .filter(|ms| self.is_overlay || ms.0 < active_root.len()) - .fold((roots, Vec::new()), |(menu_root, mut nodes), (_i, ms)| { - dbg!(ms.index); - let slice = ms.slice(limits.max(), overlay_offset, self.item_height); - let _start_index = slice.start_index; - let _end_index = slice.end_index; - dbg!(&self.window_id, menu_root.len()); - let children_node = - ms.layout(overlay_offset, slice, renderer, menu_root, active_tree); - let node_size = children_node.size(); - // dbg!(node_size.height); - 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 - // dbg!(ms.index); - ( - ms.index - .map_or(menu_root, |active| &menu_root[active].children), - nodes, - ) - }) + if let Some(ms) = data.menu_states.get(self.depth + 1) { + ms.iter() + .enumerate() + .filter(|ms| self.is_overlay || ms.0 < active_root.len()) + .fold((roots, Vec::new()), |(menu_root, mut nodes), (_i, ms)| { + dbg!(ms.index); + let slice = + ms.slice(limits.max(), overlay_offset, self.item_height); + let _start_index = slice.start_index; + let _end_index = slice.end_index; + dbg!(&self.depth, &self.window_id, menu_root.len()); + let children_node = ms.layout( + overlay_offset, + slice, + renderer, + menu_root, + active_tree, + ); + let node_size = children_node.size(); + // dbg!(node_size.height); + 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 + // dbg!(ms.index); + ( + ms.index + .map_or(menu_root, |active| &menu_root[active].children), + nodes, + ) + }) + } else { + (&empty, vec![]) + } }) .map(|(_, l)| l) .unwrap_or_default(); @@ -592,6 +608,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { overlay_offset, ); + dbg!("init_root_menu"); init_root_menu( self, renderer, @@ -650,7 +667,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .distance(view_cursor.position().unwrap_or_default()) < 2.0 { - let is_inside = state.menu_states[&self.window_id] + let is_inside = state.menu_states[self.depth + 1] .iter() .any(|ms| ms.menu_bounds.check_bounds.contains(overlay_cursor)); @@ -696,7 +713,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { view_cursor: Cursor, ) { self.tree.inner.with_data(|state| { - let Some(active_root) = state.active_root.get(&self.window_id) else { + let Some(active_root) = state.active_root.get(self.depth) else { // dbg!(self.window_id); return; }; @@ -713,7 +730,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { let styling = theme.appearance(&self.style); let (active_tree, roots) = if self.depth > 0 { - active_root.iter().skip(1).take(self.depth).fold( + active_root.iter().take(self.depth).fold( ( &state.tree.children[active_root[0]].children, &self.menu_roots[active_root[0]].children, @@ -726,9 +743,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { // let tree = &state.tree.children[active_root].children; // let root = &self.menu_roots[active_root]; - let indices = state - .get_trimmed_indices(&self.window_id) - .collect::>(); + let indices = state.get_trimmed_indices(self.depth).collect::>(); // dbg!( // self.window_id, // state.menu_states[&self.window_id].len(), @@ -736,7 +751,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { // root.children.len(), // layout.bounds() // ); - state.menu_states[&self.window_id] + state.menu_states[self.depth + 1] .iter() .zip(layout.children()) .enumerate() @@ -893,7 +908,7 @@ pub(super) fn init_root_menu( menu.tree.inner.with_data_mut(|state| { if !(state .menu_states - .get(&menu.window_id) + .get(menu.depth + 1) .is_none_or(|s| s.is_empty()) && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) { @@ -901,48 +916,53 @@ pub(super) fn init_root_menu( } println!( - "init root menu {} {:?}", - menu.window_id, menu.root_bounds_list + "init root menu {} {} {:?}", + menu.depth, menu.window_id, &menu.root_bounds_list ); dbg!(menu.menu_roots.len()); - // dbg!(state - // .menu_states - // .get(&menu.window_id) - // .is_none_or(|s| s.is_empty())); - // dbg!(menu - // .menu_roots - // .as_slice() - // .iter() - // .map(|r| r.index) - // .collect::>()); + dbg!( + state + .menu_states + .get(menu.depth + 1) + .is_none_or(|s| s.is_empty()) + ); + dbg!( + menu.menu_roots + .as_slice() + .iter() + .map(|r| r.index) + .collect::>() + ); - // dbg!(state - // .active_root - // .get(&menu.window_id) - // .into_iter() - // .flatten() - // .count() - // .saturating_sub(1)); - // let root = state - // .active_root - // .get(&menu.window_id) - // .iter() - // .map(|l| l.into_iter()) - // .flatten() - // .take( - // state - // .active_root - // .get(&menu.window_id) - // .into_iter() - // .flatten() - // .count() - // .saturating_sub(1), - // ) - // .fold(menu.menu_roots.as_slice(), |m, i| { - // dbg!(m.len()); - // m[*i].children.as_slice() - // }); - // dbg!(root.len()); + dbg!( + state + .active_root + .get(menu.depth) + .into_iter() + .flatten() + .count() + .saturating_sub(1) + ); + let root = state + .active_root + .get(menu.depth) + .iter() + .map(|l| l.into_iter()) + .flatten() + .take( + state + .active_root + .get(menu.depth) + .into_iter() + .flatten() + .count() + .saturating_sub(1), + ) + .fold(menu.menu_roots.as_slice(), |m, i| { + dbg!(m.len()); + m[*i].children.as_slice() + }); + dbg!(root.len()); let mut set = false; for (i, (&root_bounds, mt)) in menu @@ -955,7 +975,8 @@ pub(super) fn init_root_menu( // dbg!("skipping menu with no children"); continue; } - // dbg!(i, root_bounds.contains(overlay_cursor)); + dbg!(mt.children.len()); + dbg!(i, root_bounds.contains(overlay_cursor)); if root_bounds.contains(overlay_cursor) { dbg!(root_bounds); // dbg!(i, root_bounds, mt.width, mt.height); @@ -992,18 +1013,20 @@ pub(super) fn init_root_menu( menu.is_overlay, ); set = true; - // dbg!(i); - // dbg!("inserting root menu state"); - // dbg!(&menu_bounds.children_bounds); - // dbg!((&menu_bounds.child_positions)); - state.active_root.insert(menu.window_id, vec![i]); + dbg!(i); + + dbg!("inserting root menu state"); + dbg!(&menu_bounds.children_bounds); + dbg!((&menu_bounds.child_positions)); + state.active_root.insert(menu.depth, vec![i]); // do we need to insert the rest now too? let ms = MenuState { index: None, scroll_offset: 0.0, menu_bounds, }; - let v = state.menu_states.entry(menu.window_id).or_default(); + dbg!("pushing to menu states..."); + let v = super::menu_bar::get_mut_or_default(&mut state.menu_states, menu.depth + 1); v.push(ms); // Hack to ensure menu opens properly @@ -1014,6 +1037,7 @@ pub(super) fn init_root_menu( } if !set { dbg!(overlay_cursor); + panic!("huh"); } }); } @@ -1033,7 +1057,7 @@ pub(super) fn init_root_popup_menu( menu.tree.inner.with_data_mut(|state| { if !(state .menu_states - .get(&menu.window_id) + .get(menu.depth + 1) .is_none_or(|s| s.is_empty()) && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) { @@ -1063,7 +1087,7 @@ pub(super) fn init_root_popup_menu( // .count()); let active_roots = state .active_root - .get(&menu.window_id) + .get(menu.depth) .cloned() .unwrap_or_default(); dbg!(&active_roots); @@ -1132,7 +1156,7 @@ pub(super) fn init_root_popup_menu( scroll_offset: 0.0, menu_bounds, }; - let v = state.menu_states.entry(menu.window_id).or_default(); + let v = super::menu_bar::get_mut_or_default(&mut state.menu_states, menu.depth + 1); v.push(ms); // Hack to ensure menu opens properly @@ -1167,11 +1191,11 @@ fn process_menu_events<'b, Message: std::clone::Clone>( Cow::Owned(o) => o.as_mut_slice(), }; my_state.inner.with_data_mut(|state| { - let Some(active_root) = state.active_root.get(&window_id).cloned() else { + let Some(active_root) = state.active_root.get(menu.depth).cloned() else { return Status::Ignored; }; - let indices = state.get_trimmed_indices(&window_id); + let indices = state.get_trimmed_indices(menu.depth); let indices = if is_overlay { indices.collect::>() @@ -1185,7 +1209,7 @@ fn process_menu_events<'b, Message: std::clone::Clone>( // get active item // let mt = indices.iter().fold(root | mt, &i | &mut mt.children[i]); - let (tree, mt) = active_root.iter().take(menu.depth).skip(1).fold( + let (tree, mt) = active_root.iter().take(menu.depth).fold( ( &mut state.tree.children[active_root[0]].children, &mut menu_roots[active_root[0]], @@ -1197,7 +1221,7 @@ fn process_menu_events<'b, Message: std::clone::Clone>( let tree = &mut tree[mt.index]; // get layout - let last_ms = &state.menu_states[&window_id][indices.len() - 1]; + let last_ms = &state.menu_states[menu.depth + 1][indices.len() - 1]; let child_node = last_ms.layout_single( overlay_offset, last_ms.index.expect("missing index within menu state."), @@ -1248,7 +1272,7 @@ where let mut new_menu_root = None; menu.tree.inner.with_data_mut(|state| { - let Some(active_root) = state.active_root.get(&menu.window_id).clone() else { + let Some(active_root) = state.active_root.get(menu.depth).clone() else { if is_overlay && !menu.bar_bounds.contains(overlay_cursor) { state.reset(); } @@ -1266,13 +1290,13 @@ where // * remove invalid menus let mut prev_bounds = std::iter::once(menu.bar_bounds) .chain( - state.menu_states[&menu.window_id] + state.menu_states[menu.depth + 1] [..state.menu_states.len().saturating_sub(1).min(1)] .iter() .map(|ms| ms.menu_bounds.children_bounds), ) .collect::>(); - let menu_states = state.menu_states.get_mut(&menu.window_id).unwrap(); + let menu_states = state.menu_states.get_mut(menu.depth + 1).unwrap(); if menu.close_condition.leave { for i in (0..menu_states.len()).rev() { @@ -1313,7 +1337,7 @@ where let Some(last_menu_state) = menu_states.last_mut() else { // no menus left // TODO do we want to avoid this for popups? - state.active_root.remove(&menu.window_id); + state.active_root.remove(menu.depth); // keep state.open when the cursor is still inside the menu bar // this allows the overlay to keep drawing when the cursor is @@ -1351,7 +1375,7 @@ where // |(tree, mr), next_active_root| (tree, &mr.children[*next_active_root]), // ); let (active_tree, roots) = if menu.depth > 0 { - active_root.iter().skip(1).take(menu.depth).fold( + active_root.iter().take(menu.depth).fold( ( &mut state.tree.children[active_root[0]].children, &menu.menu_roots[active_root[0]].children, @@ -1450,7 +1474,7 @@ where new_menu_root = Some((new_index, ms.clone())); } if should_add { - let v = state.menu_states.entry(menu.window_id).or_default(); + let v = super::menu_bar::get_mut_or_default(&mut state.menu_states, menu.depth); v.push(ms); } } @@ -1487,7 +1511,7 @@ where (viewport_size.height - (children_bounds.y + children_bounds.height)).min(0.0); (max_offset, min_offset) }; - let menu_states = state.menu_states.get_mut(&menu.window_id).unwrap(); + let menu_states = state.menu_states.get_mut(menu.depth + 1).unwrap(); // update if menu_states.is_empty() { From 8642a922700d6fd228e773f1f1ab553d8dfdddd8 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 12 May 2025 10:16:19 -0400 Subject: [PATCH 04/12] wip --- examples/application/src/main.rs | 119 +++++++++++++++--------------- src/widget/menu/menu_bar.rs | 3 +- src/widget/menu/menu_inner.rs | 122 +++++++++++++++---------------- 3 files changed, 124 insertions(+), 120 deletions(-) diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 92c2c242..d7501a61 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -7,11 +7,11 @@ use std::collections::HashMap; use std::sync::LazyLock; use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::Length; use cosmic::iced::alignment::{Horizontal, Vertical}; use cosmic::iced::widget::column; -use cosmic::iced::Length; use cosmic::iced_core::Size; -use cosmic::widget::icon::{from_name, Handle}; +use cosmic::widget::icon::{Handle, from_name}; use cosmic::widget::menu::KeyBind; use cosmic::widget::{button, text}; use cosmic::widget::{ @@ -20,7 +20,7 @@ use cosmic::widget::{ menu::{self, action::MenuAction}, nav_bar, responsive, }; -use cosmic::{executor, iced, ApplicationExt, Element}; +use cosmic::{ApplicationExt, Element, executor, iced}; static MENU_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("menu_id")); @@ -224,61 +224,63 @@ impl cosmic::Application for App { 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::menu::bar(vec![ + Tree::with_children( + menu::root("hiiiiiiiiiiiiiiiiiii 1"), + menu::items( + &self.keybinds, + vec![menu::Item::Button("hi", None, Action::Hi)], + ), ), - ), - Tree::with_children( - menu::root("hiiiiiiiiiiiiiiiiii 2"), - menu::items( - &self.keybinds, - vec![menu::Item::Button("hi 2", None, Action::Hi)], + Tree::with_children( + menu::root("hiiiiiiiiiiiiiiiiii 2"), + menu::items( + &self.keybinds, + vec![menu::Item::Button("hi 2", 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), - ], + 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), + ], + ), ), - ), - 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), - ], + 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), + ], + ), ), - ), - 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), - ], + 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()] + ]) + .into(), + ] } #[cfg(feature = "wayland")] { @@ -289,18 +291,21 @@ impl cosmic::Application for App { Message::Surface, vec![ ( - "hiiiiiiiiiiiiiiiiiii 1", - vec![menu::Item::Button("hi 1", None, Action::Hi)], + "hi 1".into(), + vec![ + menu::Item::Button("hi 1".into(), None, Action::Hi), + menu::Item::Button("hi 1 2".into(), None, Action::Hi), + ], ), ( - "hiiiiiiiiiiiiiiiiiii 2".into(), + "hi 2".into(), vec![ menu::Item::Button("hi 2", None, Action::Hi), menu::Item::Button("hi 22", None, Action::Hi), ], ), ( - "hiiiiiiiiiiiiiiiiiii 3".into(), + "hi 3".into(), vec![ menu::Item::Button("hi 3", None, Action::Hi), menu::Item::Button("hi 33", None, Action::Hi), diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 2cd44ec3..dd998e81 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -53,6 +53,7 @@ pub(crate) struct MenuBarStateInner { 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 MenuBarStateInner { @@ -559,7 +560,7 @@ where path_highlight: self.path_highlight, style: std::borrow::Cow::Borrowed(&self.style), position: Point::new(translation.x, translation.y), - is_overlay: false, + is_overlay: true, window_id: window::Id::NONE, depth: 0, } diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 905d2eed..e9c2d233 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -296,6 +296,7 @@ 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, @@ -320,12 +321,7 @@ impl MenuState { // menu_tree.children.len(), // self.menu_bounds.child_positions.len() // ); - // dbg!( - // overlay_offset, - // menu_tree.index, - // menu_tree.width, - // menu_tree.height - // ); + // for mt in &menu_tree.children[start_index..=end_index] { // dbg!(mt.index); // } @@ -360,13 +356,16 @@ impl MenuState { } mt.item - .layout(&mut tree[i], renderer, &limits) + .layout(&mut tree[mt.index], renderer, &limits) .move_to(Point::new(0.0, position + self.scroll_offset)) }) .collect::>(); // dbg!(children_bounds.size()); - Node::with_children(children_bounds.size(), child_nodes).move_to(children_bounds.position()) + let node = Node::with_children(children_bounds.size(), child_nodes) + .move_to(children_bounds.position()); + // dbg!(&node); + node } fn layout_single( @@ -390,6 +389,7 @@ impl MenuState { )) } + /// returns a slice of the menu items that are inside the viewport pub(super) fn slice( &self, viewport_size: Size, @@ -446,6 +446,7 @@ impl MenuState { 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 @@ -489,17 +490,13 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { if active_root.is_empty() || self.menu_roots.is_empty() { return (&empty, vec![]); } - let (active_tree, roots) = if self.depth > 0 { - active_root.iter().take(self.depth).fold( - ( - &mut tree_children[active_root[0]].children, - &self.menu_roots[active_root[0]].children, - ), - |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), - ) - } else { - (tree_children, &self.menu_roots[active_root[0]].children) - }; + let (active_tree, roots) = active_root.iter().take(self.depth).fold( + ( + &mut tree_children[active_root[0]].children, + &self.menu_roots[active_root[0]].children, + ), + |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), + ); dbg!(roots.len()); dbg!(&active_root); @@ -519,7 +516,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { // } // } // } - if let Some(ms) = data.menu_states.get(self.depth + 1) { + if let Some(ms) = data.menu_states.get(self.depth) { ms.iter() .enumerate() .filter(|ms| self.is_overlay || ms.0 < active_root.len()) @@ -667,7 +664,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .distance(view_cursor.position().unwrap_or_default()) < 2.0 { - let is_inside = state.menu_states[self.depth + 1] + let is_inside = state.menu_states[self.depth] .iter() .any(|ms| ms.menu_bounds.check_bounds.contains(overlay_cursor)); @@ -717,6 +714,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { // dbg!(self.window_id); return; }; + dbg!(active_root); let viewport = layout.bounds(); let viewport_size = viewport.size(); let overlay_offset = Point::ORIGIN - viewport.position(); @@ -729,29 +727,33 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { // dbg!(self.window_id, &active_root); let styling = theme.appearance(&self.style); - let (active_tree, roots) = if self.depth > 0 { - active_root.iter().take(self.depth).fold( - ( - &state.tree.children[active_root[0]].children, - &self.menu_roots[active_root[0]].children, - ), - |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), - ) - } else { - (&state.tree.children, self.menu_roots.as_ref()) - }; + let (active_tree, roots) = active_root.iter().take(self.depth).fold( + ( + &state.tree.children[active_root[0]].children, + &self.menu_roots[active_root[0]].children, + ), + |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), + ); + + dbg!( + state.tree.children.len(), + state.tree.children[active_root[0]].children.len() + ); // let tree = &state.tree.children[active_root].children; // let root = &self.menu_roots[active_root]; + dbg!(&active_root.len()); let indices = state.get_trimmed_indices(self.depth).collect::>(); - // dbg!( - // self.window_id, - // state.menu_states[&self.window_id].len(), - // layout.children().count(), - // root.children.len(), - // layout.bounds() - // ); - state.menu_states[self.depth + 1] + dbg!(self.depth); + + dbg!( + self.window_id, + state.menu_states.len(), + state.menu_states[self.depth].len(), + layout.children().count(), + layout.bounds() + ); + state.menu_states[self.depth] .iter() .zip(layout.children()) .enumerate() @@ -819,6 +821,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } } if start_index < menu_roots.len() { + dbg!(start_index, end_index, menu_roots.len()); // draw item menu_roots[start_index..=end_index] .iter() @@ -908,7 +911,7 @@ pub(super) fn init_root_menu( menu.tree.inner.with_data_mut(|state| { if !(state .menu_states - .get(menu.depth + 1) + .get(menu.depth) .is_none_or(|s| s.is_empty()) && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) { @@ -923,7 +926,7 @@ pub(super) fn init_root_menu( dbg!( state .menu_states - .get(menu.depth + 1) + .get(menu.depth) .is_none_or(|s| s.is_empty()) ); dbg!( @@ -1026,7 +1029,7 @@ pub(super) fn init_root_menu( menu_bounds, }; dbg!("pushing to menu states..."); - let v = super::menu_bar::get_mut_or_default(&mut state.menu_states, menu.depth + 1); + let v = super::menu_bar::get_mut_or_default(&mut state.menu_states, menu.depth); v.push(ms); // Hack to ensure menu opens properly @@ -1057,7 +1060,7 @@ pub(super) fn init_root_popup_menu( menu.tree.inner.with_data_mut(|state| { if !(state .menu_states - .get(menu.depth + 1) + .get(menu.depth) .is_none_or(|s| s.is_empty()) && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) { @@ -1156,7 +1159,7 @@ pub(super) fn init_root_popup_menu( scroll_offset: 0.0, menu_bounds, }; - let v = super::menu_bar::get_mut_or_default(&mut state.menu_states, menu.depth + 1); + let v = super::menu_bar::get_mut_or_default(&mut state.menu_states, menu.depth); v.push(ms); // Hack to ensure menu opens properly @@ -1221,7 +1224,7 @@ fn process_menu_events<'b, Message: std::clone::Clone>( let tree = &mut tree[mt.index]; // get layout - let last_ms = &state.menu_states[menu.depth + 1][indices.len() - 1]; + let last_ms = &state.menu_states[menu.depth][indices.len() - 1]; let child_node = last_ms.layout_single( overlay_offset, last_ms.index.expect("missing index within menu state."), @@ -1245,7 +1248,7 @@ fn process_menu_events<'b, Message: std::clone::Clone>( }) } -#[allow(unused_results)] +#[allow(unused_results, clippy::too_many_lines, clippy::too_many_arguments)] fn process_overlay_events( menu: &mut Menu, renderer: &crate::Renderer, @@ -1290,13 +1293,12 @@ where // * remove invalid menus let mut prev_bounds = std::iter::once(menu.bar_bounds) .chain( - state.menu_states[menu.depth + 1] - [..state.menu_states.len().saturating_sub(1).min(1)] + state.menu_states[menu.depth][..state.menu_states.len().saturating_sub(1).min(1)] .iter() .map(|ms| ms.menu_bounds.children_bounds), ) .collect::>(); - let menu_states = state.menu_states.get_mut(menu.depth + 1).unwrap(); + let menu_states = state.menu_states.get_mut(menu.depth).unwrap(); if menu.close_condition.leave { for i in (0..menu_states.len()).rev() { @@ -1374,17 +1376,13 @@ where // ), // |(tree, mr), next_active_root| (tree, &mr.children[*next_active_root]), // ); - let (active_tree, roots) = if menu.depth > 0 { - active_root.iter().take(menu.depth).fold( - ( - &mut state.tree.children[active_root[0]].children, - &menu.menu_roots[active_root[0]].children, - ), - |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), - ) - } else { - (&mut state.tree.children, menu.menu_roots.as_ref()) - }; + let (active_tree, roots) = active_root.iter().take(menu.depth).fold( + ( + &mut state.tree.children[active_root[0]].children, + &menu.menu_roots[active_root[0]].children, + ), + |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), + ); let active_menu = if is_overlay { indices[0..indices.len().saturating_sub(1)] .iter() @@ -1511,7 +1509,7 @@ where (viewport_size.height - (children_bounds.y + children_bounds.height)).min(0.0); (max_offset, min_offset) }; - let menu_states = state.menu_states.get_mut(menu.depth + 1).unwrap(); + let menu_states = state.menu_states.get_mut(menu.depth).unwrap(); // update if menu_states.is_empty() { From a372ce800f4f29f4d3630c150f980dbb78bfcec3 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 28 May 2025 17:01:24 -0400 Subject: [PATCH 05/12] wip: popup --- examples/application/src/main.rs | 175 ++++++++------------- src/surface/action.rs | 2 +- src/theme/style/menu_bar.rs | 2 +- src/widget/dropdown/widget.rs | 10 +- src/widget/menu/menu_bar.rs | 172 +++++++++++++++++++-- src/widget/menu/menu_inner.rs | 246 +++++++++++++++++++++++++++--- src/widget/menu/menu_tree.rs | 19 ++- src/widget/responsive_menu_bar.rs | 2 + 8 files changed, 469 insertions(+), 159 deletions(-) diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index d7501a61..3908cabd 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() } @@ -222,123 +236,60 @@ impl cosmic::Application for App { fn header_start(&self) -> Vec> { use cosmic::widget::menu::Tree; - #[cfg(not(feature = "wayland"))] - { + + vec![cosmic::widget::responsive_menu_bar( + self.core(), + &self.keybinds, + MENU_ID.clone(), + Message::Surface, 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)], - ), - ), - Tree::with_children( - menu::root("hiiiiiiiiiiiiiiiiii 2"), - menu::items( - &self.keybinds, - vec![menu::Item::Button("hi 2", 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), - ], - ), - ), - 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), - ], - ), - ), - Tree::with_children( - menu::root("hi 4"), - menu::items( - &self.keybinds, + ( + "hi 1".into(), + vec![ + menu::Item::Button("hi 12".into(), None, Action::Hi), + menu::Item::Button("hi 13".into(), None, Action::Hi2), + ], + ), + ( + "hi 2".into(), + vec![ + menu::Item::Button("hi 21".into(), None, Action::Hi), + menu::Item::Button("hi 22".into(), None, Action::Hi2), + ], + ), + ( + "hi 3".into(), + vec![ + menu::Item::Button("hi 33".into(), None, Action::Hi), + menu::Item::Button("hi 333".into(), None, Action::Hi2), + menu::Item::Button("hi 3333".into(), None, Action::Hi3), + ], + ), + ( + "hiiiiiiiiiiiiiiiiiii 4".into(), + vec![ + menu::Item::Button("hi 4".into(), None, Action::Hi), + menu::Item::Button("hi 44".into(), None, Action::Hi2), + menu::Item::Button("hi 444".into(), None, Action::Hi3), + menu::Item::Folder( + "nest 4".into(), vec![ + menu::Item::Button("hi 41".into(), None, Action::Hi), + menu::Item::Button("hi 442".into(), None, Action::Hi2), + menu::Item::Button("hi 4443".into(), None, Action::Hi3), menu::Item::Folder( - "hi 41 extra root", - vec![menu::Item::Button("hi 3", None, Action::Hi)], + "nest 2 4".into(), + vec![ + menu::Item::Button("hi 443".into(), None, Action::Hi2), + menu::Item::Button("hi 4444".into(), 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![ - ( - "hi 1".into(), - vec![ - menu::Item::Button("hi 1".into(), None, Action::Hi), - menu::Item::Button("hi 1 2".into(), None, Action::Hi), - ], - ), - ( - "hi 2".into(), - vec![ - menu::Item::Button("hi 2", None, Action::Hi), - menu::Item::Button("hi 22", None, Action::Hi), - ], - ), - ( - "hi 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/src/surface/action.rs b/src/surface/action.rs index e27815eb..fdf2680e 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 7f99a1a5..ed0e657a 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/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 76ee7b05..d196215d 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/menu_bar.rs b/src/widget/menu/menu_bar.rs index dd998e81..5cdbf2d2 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -15,6 +15,7 @@ use crate::{ widget::{ RcWrapper, dropdown::menu::{self, State}, + menu::menu_inner::init_root_menu, }, }; @@ -57,6 +58,7 @@ pub(crate) struct MenuBarStateInner { pub(crate) menu_states: Vec>, } 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 .get(index) @@ -188,7 +190,7 @@ pub struct MenuBar { menu_roots: Vec>, style: ::Style, window_id: window::Id, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", feature = "winit"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, pub(crate) on_surface_action: Option Message>>, } @@ -228,7 +230,7 @@ where menu_roots, style: ::Style::default(), window_id: window::Id::NONE, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", feature = "winit"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, } @@ -322,7 +324,8 @@ where self } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", feature = "winit"))] + pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -405,6 +408,7 @@ where ) } + #[allow(clippy::too_many_lines)] fn on_event( &mut self, tree: &mut Tree, @@ -441,28 +445,170 @@ where if reset { if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { dbg!("reset destroy"); - // TODO emit message if let Some(handler) = self.on_surface_action.as_ref() { shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); state.reset(); } } } + }); - let tree = &mut state.tree; + // let tree: &mut _ = &mut state.tree; - match event { - Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => { + match event { + Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => { + my_state.inner.with_data_mut(|state| { 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 } + }); + #[cfg(all(feature = "wayland", feature = "winit"))] + + dbg!( + self.window_id != window::Id::NONE, + self.on_surface_action.is_some() + ); + // TODO emit Message to open menu + 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) + .filter(|r| r.len() == 1) + .map(|r| r[0]) + }); + + // if position is not on menu bar button skip. + let position = view_cursor.position(); + let hovered_root = layout + .children() + .position(|lo| view_cursor.is_over(lo.bounds())); + dbg!(old_active_root); + // TODO why exit here? + // if hovered_root + // .zip(old_active_root.as_ref()) + // .is_none_or(|r| r.0 != *r.1) + // { + // panic!(); + // } + + 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, + }; + + 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 = my_state.inner.with_data(|state| { + state.menu_states[0] + .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, + }, + ) + }); + + 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:cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, + 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: true, + input_zone: None, + }, + Some(move || { + Element::from( + crate::widget::container(popup_menu.clone()).center(Length::Fill), + ) + .map(crate::action::app) + }), + ))); } - _ => (), } - }); + // Window(Focused) => { + // if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { + // dbg!("window focused"); + // shell.publish(Message::from(SurfaceMessage::DestroyPopup(popup_id))); + // } + // state.reset(); + // } + _ => (), + } root_status } @@ -533,8 +679,8 @@ where _renderer: &Renderer, translation: Vector, ) -> Option> { - //#[cfg(feature = "wayland")] - //return None; + #[cfg(all(feature = "wayland", feature = "winit"))] + return None; let state = tree.state.downcast_ref::(); if state diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index e9c2d233..5ff73053 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -232,11 +232,11 @@ pub(super) struct MenuSlice { #[derive(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, } @@ -299,7 +299,7 @@ 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( @@ -444,6 +444,7 @@ impl MenuState { } } +#[derive(Clone)] pub(crate) struct Menu<'b, Message: std::clone::Clone> { pub(crate) tree: MenuBarState, // Flattened menu tree @@ -471,7 +472,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { overlay::Element::new(Box::new(self)) } - fn layout(&mut self, renderer: &crate::Renderer, limits: Limits) -> Node { + pub(crate) fn layout(&self, renderer: &crate::Renderer, limits: Limits) -> Node { // layout children; let position = self.position; let mut intrinsic_size = Size::ZERO; @@ -889,6 +890,207 @@ impl overlay::Overlay Widget + for Menu<'a, 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 { + // dbg!(self.window_id, limits); + 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); + } + + 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(feature = "wayland")] + // if let Some((new_root, new_ms)) = new_root { + // use iced_runtime::platform_specific::wayland::popup::{ + // SctkPopupSettings, SctkPositioner, + // }; + // let mut guard = self.tree.inner.lock().unwrap(); + // let popup_id = *guard + // .popup_id + // .entry(self.window_id) + // .or_insert_with(window::Id::unique); + // let active_roots = &guard.active_root[&self.window_id]; + // // dbg!(active_roots); + // let root_bounds_list = active_roots + // .into_iter() + // .fold(layout, |l, active_root| { + // // dbg!(active_root); + // l.children().nth(*active_root).unwrap() + // }) + // .children() + // .map(|c| c.bounds()) + // .collect(); + // drop(guard); + + // // dbg!(&root_bounds_list); + // // dbg!(self.menu_roots.clone().len()); + // // dbg!(self + // // .menu_roots + // // .clone() + // // .iter() + // // .map(|r| r.index) + // // .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, + // }; + // let mut guard = self.tree.inner.lock().unwrap(); + // // dbg!(guard.menu_states.keys()); + // let Some(parent_root) = guard.active_root.get(&self.window_id) else { + // // TODO log warning + // return status; + // }; + // let mut roots = parent_root.clone(); + // roots.push(new_root); + // // dbg!(&roots); + // guard.active_root.insert(popup_id, roots); + // _ = guard.menu_states.remove(&popup_id); + // drop(guard); + // init_root_popup_menu( + // &mut popup_menu, + // renderer, + // shell, + // cursor.position().unwrap(), + // layout.bounds().size(), + // Vector::new(0., 0.), + // layout.bounds(), + // self.main_offset as f32, + // ); + // let mut guard = self.tree.inner.lock().unwrap(); + + // guard + // .menu_states + // .get_mut(&self.window_id) + // .unwrap() + // .push(new_ms); + + // let anchor_rect = guard.menu_states[&self.window_id] + // .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, + // }, + // ); + // // dbg!(&anchor_rect); + + // drop(guard); + // let menu_node = Widget::layout( + // &popup_menu, + // &mut Tree::empty(), + // renderer, + // &Limits::NONE.min_width(1.).min_height(1.), + // ); + // // dbg!(menu_node.size()); + + // // dbg!(&menu_node); + + // 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:cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, + // reactive: true, + // ..Default::default() + // }; + // let parent = self.window_id; + // // dbg!(&positioner); + // shell.publish(crate::app::message::simple_popup( + // move || SctkPopupSettings { + // parent, + // id: popup_id, + // positioner: positioner.clone(), + // parent_size: None, + // grab: true, + // close_with_children: true, + // }, + // Some(move || { + // crate::Element::from( + // crate::widget::container(popup_menu.clone()).center(Length::Fill), + // ) + // .map(crate::app::Message::App) + // }), + // )); + // } + 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) + } +} + fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle { Rectangle { x: rect.x - padding.left, @@ -1194,34 +1396,48 @@ fn process_menu_events<'b, Message: std::clone::Clone>( Cow::Owned(o) => o.as_mut_slice(), }; my_state.inner.with_data_mut(|state| { + dbg!(&state.active_root); let Some(active_root) = state.active_root.get(menu.depth).cloned() else { return Status::Ignored; }; + dbg!("got the active root"); let indices = state.get_trimmed_indices(menu.depth); - let indices = if is_overlay { - indices.collect::>() - } else { - indices.take(1).collect::>() - }; + let indices = indices.collect::>(); + // if is_overlay { + // indices.collect::>() + // } else { + // indices.take(1).collect::>() + // }; if indices.is_empty() { return Status::Ignored; } + dbg!(&active_root, &indices); // get active item // let mt = indices.iter().fold(root | mt, &i | &mut mt.children[i]); - let (tree, mt) = active_root.iter().take(menu.depth).fold( + let (tree, mt) = indices.iter().take(indices.len()).fold( ( &mut state.tree.children[active_root[0]].children, &mut menu_roots[active_root[0]], ), |(tree, mt), next_active_root| (tree, &mut mt.children[*next_active_root]), ); + dbg!(mt.children.len(), mt.index,); + // let Some(i) = state.menu_states[menu.depth].iter().position(|ms| { + // ms.menu_bounds + // .check_bounds + // .contains(view_cursor.position().unwrap_or(Point { x: -1., y: -1. })) + // }) else { + // return Status::Ignored; + // }; + // let mt = &mut mt.children[indices[indices.len() - 1]]; // widget tree let tree = &mut tree[mt.index]; + dbg!(tree.children.len()); // get layout let last_ms = &state.menu_states[menu.depth][indices.len() - 1]; @@ -1234,7 +1450,9 @@ fn process_menu_events<'b, Message: std::clone::Clone>( ); let child_layout = Layout::new(&child_node); + dbg!("item on event handler..."); // process only the last widget + dbg!(&event); mt.item.on_event( tree, event, @@ -1369,13 +1587,7 @@ where - (last_children_bounds.y + last_menu_state.scroll_offset)) .clamp(0.0, last_children_bounds.height - 0.001); // dbg!(height_diff); - // let (tree, active_menu_root) = active_root.iter().skip(1).fold( - // ( - // &mut state.tree.children[active_root[0]].children, - // &menu.menu_roots[active_root[0]], - // ), - // |(tree, mr), next_active_root| (tree, &mr.children[*next_active_root]), - // ); + let (active_tree, roots) = active_root.iter().take(menu.depth).fold( ( &mut state.tree.children[active_root[0]].children, diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 71f93231..82ed6db8 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -213,7 +213,7 @@ where pub fn menu_items< A: MenuAction, L: Into> + 'static, - Message: 'static + std::clone::Clone, + Message: 'static + std::clone::Clone + std::fmt::Debug, >( key_binds: &HashMap, children: Vec>, @@ -238,9 +238,10 @@ pub fn menu_items< 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(), ]; @@ -250,15 +251,18 @@ pub fn menu_items< items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); } - let menu_button = menu_button(items).on_press(action.message()); + // dbg!("button with action...", action.message()); + let menu_button = menu_button(items).on_press(action.message()).description(l); 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(), ]; @@ -268,7 +272,7 @@ pub fn menu_items< items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); } - let menu_button = menu_button(items); + let menu_button = menu_button(items).description((l.clone())); trees.push(MenuTree::::from(Element::from(menu_button))); } @@ -305,16 +309,19 @@ pub fn menu_items< ))); } MenuItem::Folder(label, children) => { + let l: Cow<'static, str> = label.into(); + trees.push(MenuTree::::with_children( RcElementWrapper::new(crate::Element::from( menu_button::<'static, _>(vec![ - widget::text(label).into(), + widget::text(l.clone()).into(), widget::horizontal_space().into(), widget::icon::from_name("pan-end-symbolic") .size(16) .icon() .into(), ]) + .description(l.clone()) .class( // Menu folders have no on_press so they take on the disabled style by default if children.is_empty() { diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index b08e86ea..e5416151 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -97,6 +97,7 @@ impl ResponsiveMenuBar { .item_width(self.item_width) .item_height(self.item_height) .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}")), ), @@ -131,6 +132,7 @@ impl ResponsiveMenuBar { .item_height(self.item_height) .item_width(self.collapsed_item_width) .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}")), ), From 89700a2ed5e93b2fc1fc95927846921521ef4e93 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 29 May 2025 10:32:13 -0400 Subject: [PATCH 06/12] wip --- examples/application/src/main.rs | 32 +++++++++++++++---------------- src/widget/responsive_menu_bar.rs | 23 ++++++++++------------ 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 3908cabd..176be39f 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -237,7 +237,7 @@ impl cosmic::Application for App { fn header_start(&self) -> Vec> { use cosmic::widget::menu::Tree; - vec![cosmic::widget::responsive_menu_bar( + vec![cosmic::widget::responsive_menu_bar().into_element( self.core(), &self.keybinds, MENU_ID.clone(), @@ -246,42 +246,42 @@ impl cosmic::Application for App { ( "hi 1".into(), vec![ - menu::Item::Button("hi 12".into(), None, Action::Hi), - menu::Item::Button("hi 13".into(), None, Action::Hi2), + menu::Item::Button("hi 12", None, Action::Hi), + menu::Item::Button("hi 13", None, Action::Hi2), ], ), ( "hi 2".into(), vec![ - menu::Item::Button("hi 21".into(), None, Action::Hi), - menu::Item::Button("hi 22".into(), None, Action::Hi2), + menu::Item::Button("hi 21", None, Action::Hi), + menu::Item::Button("hi 22", None, Action::Hi2), ], ), ( "hi 3".into(), vec![ - menu::Item::Button("hi 33".into(), None, Action::Hi), - menu::Item::Button("hi 333".into(), None, Action::Hi2), - menu::Item::Button("hi 3333".into(), None, Action::Hi3), + menu::Item::Button("hi 33", None, Action::Hi), + menu::Item::Button("hi 333", None, Action::Hi2), + menu::Item::Button("hi 3333", None, Action::Hi3), ], ), ( "hiiiiiiiiiiiiiiiiiii 4".into(), vec![ - menu::Item::Button("hi 4".into(), None, Action::Hi), - menu::Item::Button("hi 44".into(), None, Action::Hi2), - menu::Item::Button("hi 444".into(), None, Action::Hi3), + 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".into(), None, Action::Hi), - menu::Item::Button("hi 442".into(), None, Action::Hi2), - menu::Item::Button("hi 4443".into(), None, Action::Hi3), + menu::Item::Button("hi 41", None, Action::Hi), + menu::Item::Button("hi 442", None, Action::Hi2), + menu::Item::Button("hi 4443", None, Action::Hi3), menu::Item::Folder( "nest 2 4".into(), vec![ - menu::Item::Button("hi 443".into(), None, Action::Hi2), - menu::Item::Button("hi 4444".into(), None, Action::Hi), + menu::Item::Button("hi 443", None, Action::Hi2), + menu::Item::Button("hi 4444", None, Action::Hi), ], ), ], diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index e5416151..307395a4 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -56,15 +56,15 @@ impl ResponsiveMenuBar { /// Will panic if the menu bar collapses without tracking the size pub fn into_element< 'a, - Message: Clone + 'static, - A: menu::Action, + Message: std::fmt::Debug + Clone + 'static, + 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 + Clone + 'static, trees: Vec<(S, Vec>)>, ) -> Element<'a, Message> { use crate::widget::id_container; @@ -81,17 +81,14 @@ impl ResponsiveMenuBar { menu::bar( trees .into_iter() - .map( - |mt: ( - std::borrow::Cow<'_, str>, - Vec>>, - )| { - menu::Tree::<_>::with_children( + .map(|mt: (S, Vec>)| { + menu::Tree::<_>::with_children( + crate::widget::RcElementWrapper::new(Element::from( menu::root(mt.0), - menu::items(key_binds, mt.1.into()), - ) - }, - ) + )), + menu::items(key_binds, mt.1.into()), + ) + }) .collect(), ) .item_width(self.item_width) From 4c03483e254d7be5c1819f614260f7c759dbd929 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 2 Jun 2025 13:25:05 -0400 Subject: [PATCH 07/12] wip --- src/widget/context_menu.rs | 1 + src/widget/menu/menu_bar.rs | 69 ++++-- src/widget/menu/menu_inner.rs | 291 ++++++++++++++------------ src/widget/responsive_menu_bar.rs | 2 +- src/widget/segmented_button/widget.rs | 1 + 5 files changed, 210 insertions(+), 154 deletions(-) diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 46dc1b88..dc46da01 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -260,6 +260,7 @@ impl Widget is_overlay: true, window_id: window::Id::NONE, depth: 0, + on_surface_action: None, } .overlay(), ) diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 5cdbf2d2..21e0331e 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -1,7 +1,7 @@ // From iced_aw, license MIT //! A widget that handles menu trees -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use super::{ menu_inner::{ @@ -12,6 +12,7 @@ use super::{ use crate::{ Renderer, style::menu_bar::StyleSheet, + surface::action::destroy_popup, widget::{ RcWrapper, dropdown::menu::{self, State}, @@ -70,6 +71,7 @@ impl MenuBarStateInner { } pub(super) fn reset(&mut self) { + dbg!("reset"); self.open = false; self.active_root = Vec::new(); self.menu_states.clear(); @@ -192,7 +194,8 @@ pub struct MenuBar { 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>>, + pub(crate) on_surface_action: + Option Message + Send + Sync + 'static>>, } impl MenuBar @@ -348,9 +351,9 @@ where pub fn on_surface_action( mut self, - handler: impl Fn(crate::surface::Action) -> Message + 'static, + handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, ) -> Self { - self.on_surface_action = Some(Box::new(handler)); + self.on_surface_action = Some(Arc::new(handler)); self } } @@ -420,9 +423,10 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> event::Status { - use event::Event::{Mouse, Touch}; + use event::Event::{Mouse, Touch, Window}; use mouse::{Button::Left, Event::ButtonReleased}; use touch::Event::{FingerLifted, FingerLost}; + use window::Event::Focused; let root_status = process_root_events( &mut self.menu_roots, @@ -439,12 +443,16 @@ where 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); + let reset = self.window_id != window::Id::NONE + && my_state + .inner + .with_data(|d| !d.open && !d.active_root.is_empty()); my_state.inner.with_data_mut(|state| { if reset { if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { dbg!("reset destroy"); + if let Some(handler) = self.on_surface_action.as_ref() { shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); state.reset(); @@ -457,13 +465,29 @@ where match event { Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => { - my_state.inner.with_data_mut(|state| { + 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; + dbg!(view_cursor.is_over(layout.bounds())); state.open = true; + create_popup = true; + } else if let Some(id) = state.popup_id.remove(&self.window_id) { + dbg!("destroy popup..."); + state.menu_states.clear(); + state.active_root.clear(); + let surface_action = self.on_surface_action.as_ref().unwrap(); + state.open = false; + shell.publish(surface_action(destroy_popup(id))); + state.view_cursor = view_cursor; } + create_popup }); - #[cfg(all(feature = "wayland", feature = "winit"))] + dbg!(create_popup); + if !create_popup { + return root_status; + } + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] dbg!( self.window_id != window::Id::NONE, @@ -533,6 +557,7 @@ where is_overlay: false, window_id: id, depth: 0, + on_surface_action: self.on_surface_action.clone(), }; init_root_menu( @@ -545,7 +570,8 @@ where layout.bounds(), self.main_offset as f32, ); - let anchor_rect = my_state.inner.with_data(|state| { + let anchor_rect = my_state.inner.with_data_mut(|state| { + state.popup_id.insert(self.window_id, id); state.menu_states[0] .iter() .find(|s| s.index.is_none()) @@ -601,11 +627,15 @@ where } } // Window(Focused) => { - // if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { - // dbg!("window focused"); - // shell.publish(Message::from(SurfaceMessage::DestroyPopup(popup_id))); - // } - // state.reset(); + // my_state.inner.with_data_mut(|state| { + // if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { + // dbg!("window focused"); + // if let Some(handler) = self.on_surface_action.as_ref() { + // shell.publish((handler)(destroy_popup(popup_id))); + // state.reset(); + // } + // } + // }); // } _ => (), } @@ -635,7 +665,11 @@ where // draw path highlight if self.path_highlight.is_some() { let styling = theme.appearance(&self.style); - if let Some(active) = get_mut_or_default(&mut state.active_root, 0).get(0) { + if let Some(active) = state + .active_root + .get(0) + .and_then(|active_root| active_root.get(0)) + { let active_bounds = layout .children() .nth(*active) @@ -679,13 +713,13 @@ where _renderer: &Renderer, translation: Vector, ) -> Option> { - #[cfg(all(feature = "wayland", feature = "winit"))] + // #[cfg(all(feature = "wayland", feature = "winit"))] return None; let state = tree.state.downcast_ref::(); if state .inner - .with_data_mut(|state| !state.open || state.active_root.is_empty()) + .with_data(|state| !state.open || state.active_root.is_empty()) { return None; }; @@ -709,6 +743,7 @@ where is_overlay: true, window_id: window::Id::NONE, depth: 0, + on_surface_action: self.on_surface_action.clone(), } .overlay(), ) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 5ff73053..d8103436 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1,7 +1,7 @@ // From iced_aw, license MIT //! Menu tree overlay -use std::borrow::Cow; +use std::{borrow::Cow, sync::Arc}; use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; use crate::style::menu_bar::StyleSheet; @@ -466,6 +466,8 @@ pub(crate) struct Menu<'b, Message: std::clone::Clone> { /// 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: Clone + 'static> Menu<'b, Message> { pub(crate) fn overlay(self) -> overlay::Element<'b, Message, crate::Theme, crate::Renderer> { @@ -473,6 +475,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } pub(crate) fn layout(&self, renderer: &crate::Renderer, limits: Limits) -> Node { + dbg!("layout"); // layout children; let position = self.position; let mut intrinsic_size = Size::ZERO; @@ -480,6 +483,8 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { dbg!(self.depth); let empty = Vec::new(); self.tree.inner.with_data_mut(|data| { + dbg!(data.active_root.len()); + let overlay_offset = Point::ORIGIN - position; let tree_children = &mut data.tree.children; dbg!(&data.active_root,); @@ -557,11 +562,12 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .unwrap_or_default(); // dbg!(intrinsic_size); // overlay space viewport rectangle - Node::with_children( + let node = Node::with_children( limits.resolve(Length::Shrink, Length::Shrink, intrinsic_size), children, ) - .translate(Point::ORIGIN - position) + .translate(Point::ORIGIN - position); + node }) } @@ -574,6 +580,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> (Option<(usize, MenuState)>, event::Status) { + // dbg!("on event"); use event::{ Event::{Mouse, Touch}, Status::{Captured, Ignored}, @@ -606,7 +613,6 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { overlay_offset, ); - dbg!("init_root_menu"); init_root_menu( self, renderer, @@ -650,6 +656,8 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { self.cross_offset as f32, self.is_overlay, ); + dbg!(new_root.as_ref().map(|r| r.0)); + return (new_root, status.merge(menu_status)); } @@ -668,19 +676,24 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { let is_inside = state.menu_states[self.depth] .iter() .any(|ms| ms.menu_bounds.check_bounds.contains(overlay_cursor)); - - if self.close_condition.click_inside + let mut needs_reset = false; + needs_reset |= self.close_condition.click_inside && is_inside && matches!( event, Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. }) - ) - { - state.reset(); - return Captured; - } + ); + + needs_reset |= self.close_condition.click_outside && !is_inside; + + if needs_reset { + dbg!("reset"); + if let Some(handler) = self.on_surface_action.as_ref() { + shell.publish((handler)(crate::surface::Action::DestroyPopup( + self.window_id, + ))); + } - if self.close_condition.click_outside && !is_inside { state.reset(); return Captured; } @@ -711,11 +724,12 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { view_cursor: Cursor, ) { self.tree.inner.with_data(|state| { + if !state.open { + return; + } let Some(active_root) = state.active_root.get(self.depth) else { - // dbg!(self.window_id); return; }; - dbg!(active_root); let viewport = layout.bounds(); let viewport_size = viewport.size(); let overlay_offset = Point::ORIGIN - viewport.position(); @@ -725,7 +739,6 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } else { Rectangle::new(Point::ORIGIN, Size::INFINITY) }; - // dbg!(self.window_id, &active_root); let styling = theme.appearance(&self.style); let (active_tree, roots) = active_root.iter().take(self.depth).fold( @@ -736,24 +749,8 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), ); - dbg!( - state.tree.children.len(), - state.tree.children[active_root[0]].children.len() - ); - // let tree = &state.tree.children[active_root].children; - // let root = &self.menu_roots[active_root]; - dbg!(&active_root.len()); - let indices = state.get_trimmed_indices(self.depth).collect::>(); - dbg!(self.depth); - dbg!( - self.window_id, - state.menu_states.len(), - state.menu_states[self.depth].len(), - layout.children().count(), - layout.bounds() - ); state.menu_states[self.depth] .iter() .zip(layout.children()) @@ -822,7 +819,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } } if start_index < menu_roots.len() { - dbg!(start_index, end_index, menu_roots.len()); + // dbg!(start_index, end_index, menu_roots.len()); // draw item menu_roots[start_index..=end_index] .iter() @@ -906,7 +903,7 @@ impl<'a, Message: std::clone::Clone + 'static> Widget iced_core::layout::Node { - // dbg!(self.window_id, limits); + // dbg!("layout", self.window_id, limits); Menu::layout(self, renderer, *limits) } @@ -935,70 +932,84 @@ impl<'a, Message: std::clone::Clone + 'static> Widget event::Status { let (new_root, status) = self.on_event(event, layout, cursor, renderer, clipboard, shell); - // #[cfg(feature = "wayland")] + dbg!(new_root.as_ref().map(|r| r.0)); + // #[cfg(all(feature = "wayland", feature = "surface-message"))] // if let Some((new_root, new_ms)) = new_root { // use iced_runtime::platform_specific::wayland::popup::{ // SctkPopupSettings, SctkPositioner, // }; - // let mut guard = self.tree.inner.lock().unwrap(); - // let popup_id = *guard - // .popup_id - // .entry(self.window_id) - // .or_insert_with(window::Id::unique); - // let active_roots = &guard.active_root[&self.window_id]; - // // dbg!(active_roots); - // let root_bounds_list = active_roots - // .into_iter() - // .fold(layout, |l, active_root| { - // // dbg!(active_root); - // l.children().nth(*active_root).unwrap() - // }) - // .children() - // .map(|c| c.bounds()) - // .collect(); - // drop(guard); + // dbg!(new_root); + // 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(); // dbg!(active_roots); + // // let root_bounds_list = active_roots + // // .into_iter() + // // .fold(layout, |l, active_root| { + // // // dbg!(active_root); + // // l.children().nth(active_root).unwrap() + // // }) + // // .children() + // // .map(|c| c.bounds()) + // // .collect(); + // let root_bounds_list = layout.children().map(|lo| lo.bounds()).collect(); + // // drop(state); - // // dbg!(&root_bounds_list); - // // dbg!(self.menu_roots.clone().len()); - // // dbg!(self - // // .menu_roots - // // .clone() - // // .iter() - // // .map(|r| r.index) - // // .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, - // }; - // let mut guard = self.tree.inner.lock().unwrap(); - // // dbg!(guard.menu_states.keys()); - // let Some(parent_root) = guard.active_root.get(&self.window_id) else { - // // TODO log warning + // // dbg!(&root_bounds_list); + // // dbg!(self.menu_roots.clone().len()); + // // dbg!(self + // // .menu_roots + // // .clone() + // // .iter() + // // .map(|r| r.index) + // // .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(), + // }; + // // let mut state = self.tree.inner.lock().unwrap(); + // // dbg!(state.menu_states.keys()); + // // let Some(parent_root) = state.active_root.get(&self.window_id) else { + // // // TODO log warning + // // return status; + // // }; + // let Some(parent_root) = state.active_root.get(self.depth).cloned() else { + // return None; + // }; + // let mut roots = parent_root.clone(); + // roots.push(new_root); + // // dbg!(&roots); + // state.active_root.push(roots); + // // _ = state.menu_states.remove(&popup_id); + // // drop(state); + // Some((popup_menu, popup_id)) + // }) else { // return status; // }; - // let mut roots = parent_root.clone(); - // roots.push(new_root); - // // dbg!(&roots); - // guard.active_root.insert(popup_id, roots); - // _ = guard.menu_states.remove(&popup_id); - // drop(guard); // init_root_popup_menu( - // &mut popup_menu, + // &mut menu, // renderer, // shell, // cursor.position().unwrap(), @@ -1007,40 +1018,38 @@ impl<'a, Message: std::clone::Clone + 'static> Widget Widget Widget( ) where Message: std::clone::Clone, { + dbg!("init"); menu.tree.inner.with_data_mut(|state| { if !(state .menu_states @@ -1396,11 +1409,9 @@ fn process_menu_events<'b, Message: std::clone::Clone>( Cow::Owned(o) => o.as_mut_slice(), }; my_state.inner.with_data_mut(|state| { - dbg!(&state.active_root); let Some(active_root) = state.active_root.get(menu.depth).cloned() else { return Status::Ignored; }; - dbg!("got the active root"); let indices = state.get_trimmed_indices(menu.depth); @@ -1414,7 +1425,6 @@ fn process_menu_events<'b, Message: std::clone::Clone>( if indices.is_empty() { return Status::Ignored; } - dbg!(&active_root, &indices); // get active item // let mt = indices.iter().fold(root | mt, &i | &mut mt.children[i]); @@ -1425,7 +1435,6 @@ fn process_menu_events<'b, Message: std::clone::Clone>( ), |(tree, mt), next_active_root| (tree, &mut mt.children[*next_active_root]), ); - dbg!(mt.children.len(), mt.index,); // let Some(i) = state.menu_states[menu.depth].iter().position(|ms| { // ms.menu_bounds // .check_bounds @@ -1437,7 +1446,6 @@ fn process_menu_events<'b, Message: std::clone::Clone>( // widget tree let tree = &mut tree[mt.index]; - dbg!(tree.children.len()); // get layout let last_ms = &state.menu_states[menu.depth][indices.len() - 1]; @@ -1450,9 +1458,7 @@ fn process_menu_events<'b, Message: std::clone::Clone>( ); let child_layout = Layout::new(&child_node); - dbg!("item on event handler..."); // process only the last widget - dbg!(&event); mt.item.on_event( tree, event, @@ -1495,12 +1501,14 @@ where menu.tree.inner.with_data_mut(|state| { let Some(active_root) = state.active_root.get(menu.depth).clone() else { if is_overlay && !menu.bar_bounds.contains(overlay_cursor) { + dbg!("overlay"); state.reset(); } return (new_menu_root, Ignored); }; if state.pressed { + dbg!("pressed"); return (new_menu_root, Ignored); } @@ -1543,7 +1551,6 @@ where { break; } - // dbg!(menu_states.len()); prev_bounds.pop(); menu_states.pop(); } @@ -1562,6 +1569,7 @@ where // 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 + dbg!("oops menu bar shouldn't disable popup"); if !menu.bar_bounds.contains(overlay_cursor) { state.open = false; } @@ -1621,6 +1629,15 @@ where ) } }; + // if last_menu_state + // .index + // .as_ref() + // .is_some_and(|old_index| *old_index == new_index) + // { + // dbg!("skipping duplicate", last_menu_state.index); + // return (None, Captured); + // } + dbg!(&last_menu_state.index, new_index); let item = &active_menu[new_index]; // dbg!(new_index); @@ -1707,6 +1724,8 @@ where use mouse::ScrollDelta; menu.tree.inner.with_data_mut(|state| { + dbg!(state.active_root.len()); + let delta_y = match delta { ScrollDelta::Lines { y, .. } => y * 60.0, ScrollDelta::Pixels { y, .. } => y, diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index 307395a4..af9d7f76 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -64,7 +64,7 @@ impl ResponsiveMenuBar { core: &Core, key_binds: &HashMap, id: crate::widget::Id, - action_message: impl Fn(crate::surface::Action) -> Message + Clone + '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; diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index ba57719d..313b686d 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1618,6 +1618,7 @@ where is_overlay: true, window_id: window::Id::NONE, depth: 0, + on_surface_action: None, } .overlay(), ) From c5c327482be4b5907b97ba8cbd54dc25aaa6f9c5 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 4 Jun 2025 00:29:24 -0400 Subject: [PATCH 08/12] wip: hover display working for nested popups --- examples/application/src/main.rs | 22 +- src/widget/button/widget.rs | 1 - src/widget/menu/menu_bar.rs | 20 +- src/widget/menu/menu_inner.rs | 684 ++++++++++++++++--------------- 4 files changed, 379 insertions(+), 348 deletions(-) diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 176be39f..b37e263f 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -7,11 +7,11 @@ use std::collections::HashMap; use std::sync::LazyLock; use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::Length; use cosmic::iced::alignment::{Horizontal, Vertical}; use cosmic::iced::widget::column; +use cosmic::iced::Length; use cosmic::iced_core::Size; -use cosmic::widget::icon::{Handle, from_name}; +use cosmic::widget::icon::{from_name, Handle}; use cosmic::widget::menu::KeyBind; use cosmic::widget::{button, text}; use cosmic::widget::{ @@ -20,7 +20,7 @@ use cosmic::widget::{ menu::{self, action::MenuAction}, nav_bar, responsive, }; -use cosmic::{ApplicationExt, Element, executor, iced}; +use cosmic::{executor, iced, ApplicationExt, Element}; static MENU_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("menu_id")); @@ -177,6 +177,7 @@ impl cosmic::Application for App { self.hidden = !self.hidden; } Message::Surface(a) => { + dbg!("got action..."); return cosmic::task::message(cosmic::Action::Cosmic( cosmic::app::Action::Surface(a), )); @@ -255,6 +256,21 @@ impl cosmic::Application for App { 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("31", None, Action::Hi), + menu::Item::Button("342", None, Action::Hi2), + menu::Item::Button("3443", None, Action::Hi3), + menu::Item::Folder( + "nest 4 2".into(), + vec![ + menu::Item::Button("343", None, Action::Hi2), + menu::Item::Button("3444", None, Action::Hi), + ], + ), + ], + ), ], ), ( diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 5a1da458..aa8f0c32 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/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 21e0331e..2d87f8fc 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -461,8 +461,6 @@ where } }); - // let tree: &mut _ = &mut state.tree; - match event { Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => { let create_popup = my_state.inner.with_data_mut(|state| { @@ -483,16 +481,11 @@ where } create_popup }); - dbg!(create_popup); + if !create_popup { return root_status; } #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] - - dbg!( - self.window_id != window::Id::NONE, - self.on_surface_action.is_some() - ); // TODO emit Message to open menu if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { use crate::surface::action::destroy_popup; @@ -514,14 +507,6 @@ where let hovered_root = layout .children() .position(|lo| view_cursor.is_over(lo.bounds())); - dbg!(old_active_root); - // TODO why exit here? - // if hovered_root - // .zip(old_active_root.as_ref()) - // .is_none_or(|r| r.0 != *r.1) - // { - // panic!(); - // } let (id, root_list) = my_state.inner.with_data_mut(|state| { if let Some(id) = state.popup_id.get(&self.window_id).copied() { @@ -629,7 +614,6 @@ where // Window(Focused) => { // my_state.inner.with_data_mut(|state| { // if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { - // dbg!("window focused"); // if let Some(handler) = self.on_surface_action.as_ref() { // shell.publish((handler)(destroy_popup(popup_id))); // state.reset(); @@ -713,7 +697,7 @@ where _renderer: &Renderer, translation: Vector, ) -> Option> { - // #[cfg(all(feature = "wayland", feature = "winit"))] + #[cfg(all(feature = "wayland", feature = "winit"))] return None; let state = tree.state.downcast_ref::(); diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index d8103436..5d78feab 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -325,11 +325,9 @@ impl MenuState { // for mt in &menu_tree.children[start_index..=end_index] { // dbg!(mt.index); // } - dbg!((start_index, end_index, menu_tree.len())); // viewport space children bounds let children_bounds = self.menu_bounds.children_bounds + overlay_offset; - dbg!(children_bounds); let child_nodes = self.menu_bounds.child_positions[start_index..=end_index] .iter() .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter()) @@ -338,7 +336,7 @@ impl MenuState { .map(|(i, ((cp, size), mt))| { let mut position = *cp; let mut size = *size; - dbg!(position, size); + // dbg!(position, size); if position < lower_bound_rel && (position + size.height) > lower_bound_rel { size.height = position + size.height - lower_bound_rel; @@ -350,10 +348,10 @@ impl MenuState { // dbg!(size); let limits = Limits::new(size, size); - dbg!(i, mt.index, tree.len()); - for child in &mt.children { - dbg!(child.index); - } + // dbg!(i, mt.index, tree.len()); + // for child in &mt.children { + // dbg!(child.index); + // } mt.item .layout(&mut tree[mt.index], renderer, &limits) @@ -475,19 +473,14 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } pub(crate) fn layout(&self, renderer: &crate::Renderer, limits: Limits) -> Node { - dbg!("layout"); // layout children; let position = self.position; let mut intrinsic_size = Size::ZERO; - dbg!(self.depth); let empty = Vec::new(); self.tree.inner.with_data_mut(|data| { - dbg!(data.active_root.len()); - let overlay_offset = Point::ORIGIN - position; let tree_children = &mut data.tree.children; - dbg!(&data.active_root,); let children = data .active_root .get(self.depth) @@ -496,43 +489,26 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { if active_root.is_empty() || self.menu_roots.is_empty() { return (&empty, vec![]); } - let (active_tree, roots) = active_root.iter().take(self.depth).fold( - ( - &mut tree_children[active_root[0]].children, - &self.menu_roots[active_root[0]].children, - ), - |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), - ); + let (active_tree, roots) = active_root + .iter() + .skip(if self.is_overlay { 0 } else { 1 }) + .fold( + ( + &mut tree_children[active_root[0]].children, + &self.menu_roots[active_root[0]].children, + ), + |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), + ); - dbg!(roots.len()); - dbg!(&active_root); - dbg!(active_tree.len()); - // dbg!( - // self.window_id, - // limits.max(), - // my_ref_mut.menu_states[&self.window_id].len() - // ); - // dbg!(my_ref_mut.active_root.get(&self.window_id)); - // for (i, active_tree) in active_tree.iter().enumerate() { - // dbg!(i); - // for (i2, active_tree) in active_tree.children.iter().enumerate() { - // dbg!(i2); - // for (i3, active_tree) in active_tree.children.iter().enumerate() { - // dbg!(i3); - // } - // } - // } if let Some(ms) = data.menu_states.get(self.depth) { ms.iter() .enumerate() .filter(|ms| self.is_overlay || ms.0 < active_root.len()) .fold((roots, Vec::new()), |(menu_root, mut nodes), (_i, ms)| { - dbg!(ms.index); let slice = ms.slice(limits.max(), overlay_offset, self.item_height); let _start_index = slice.start_index; let _end_index = slice.end_index; - dbg!(&self.depth, &self.window_id, menu_root.len()); let children_node = ms.layout( overlay_offset, slice, @@ -541,13 +517,11 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { active_tree, ); let node_size = children_node.size(); - // dbg!(node_size.height); 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 - // dbg!(ms.index); ( ms.index .map_or(menu_root, |active| &menu_root[active].children), @@ -580,7 +554,6 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> (Option<(usize, MenuState)>, event::Status) { - // dbg!("on event"); use event::{ Event::{Mouse, Touch}, Status::{Captured, Ignored}, @@ -639,9 +612,11 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } Mouse(CursorMoved { position }) | Touch(FingerMoved { position, .. }) => { + dbg!("moved", self.window_id); let view_cursor = Cursor::Available(position); let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset; if !(self.is_overlay || view_cursor.is_over(viewport)) { + dbg!("exit early", view_cursor, viewport); return (None, menu_status); } // dbg!(view_cursor, viewport, self.window_id); @@ -655,8 +630,9 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { overlay_cursor, self.cross_offset as f32, self.is_overlay, + shell, ); - dbg!(new_root.as_ref().map(|r| r.0)); + dbg!(new_root.as_ref().map(|new_root| new_root.0)); return (new_root, status.merge(menu_status)); } @@ -730,6 +706,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { let Some(active_root) = state.active_root.get(self.depth) else { return; }; + dbg!(self.depth, &active_root); let viewport = layout.bounds(); let viewport_size = viewport.size(); let overlay_offset = Point::ORIGIN - viewport.position(); @@ -741,16 +718,18 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { }; let styling = theme.appearance(&self.style); - let (active_tree, roots) = active_root.iter().take(self.depth).fold( - ( - &state.tree.children[active_root[0]].children, - &self.menu_roots[active_root[0]].children, - ), - |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), - ); + let (active_tree, roots) = active_root + .iter() + .skip(if self.is_overlay { 0 } else { 1 }) + .fold( + ( + &state.tree.children[active_root[0]].children, + &self.menu_roots[active_root[0]].children, + ), + |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), + ); let indices = state.get_trimmed_indices(self.depth).collect::>(); - state.menu_states[self.depth] .iter() .zip(layout.children()) @@ -762,15 +741,16 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { 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, + PathHighlight::MenuActive => self.depth == state.active_root.len() - 1, }); // react only to the last menu - let view_cursor = if i == state.menu_states.len() - 1 { + if self.depth == state.active_root.len() - 1 { view_cursor } else { Cursor::Available([-1.0; 2].into()) }; + dbg!(view_cursor); let draw_menu = |r: &mut crate::Renderer| { // calc slice @@ -799,7 +779,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { }; let menu_color = styling.background; r.fill_quad(menu_quad, menu_color); - + dbg!(ms.index, children_layout.children().count(), start_index); // draw path hightlight if let (true, Some(active)) = (draw_path, ms.index) { if let Some(active_layout) = children_layout @@ -825,6 +805,8 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .iter() .zip(children_layout.children()) .for_each(|(mt, clo)| { + dbg!(self.depth, view_cursor, clo.bounds()); + mt.item.draw( &active_tree[mt.index], r, @@ -920,175 +902,198 @@ impl<'a, Message: std::clone::Clone + 'static> Widget, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, + viewport: &Rectangle, ) -> event::Status { + let prev_hover = self.is_overlay.then(|| { + self.tree.inner.with_data(|d| { + let menu_states = d.menu_states.get(self.depth).unwrap(); + menu_states.get(0).and_then(|ms| ms.index) + }) + }); let (new_root, status) = self.on_event(event, layout, cursor, renderer, clipboard, shell); - dbg!(new_root.as_ref().map(|r| r.0)); - // #[cfg(all(feature = "wayland", feature = "surface-message"))] - // if let Some((new_root, new_ms)) = new_root { - // use iced_runtime::platform_specific::wayland::popup::{ - // SctkPopupSettings, SctkPositioner, - // }; - // dbg!(new_root); - // 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(); // dbg!(active_roots); - // // let root_bounds_list = active_roots - // // .into_iter() - // // .fold(layout, |l, active_root| { - // // // dbg!(active_root); - // // l.children().nth(active_root).unwrap() - // // }) - // // .children() - // // .map(|c| c.bounds()) - // // .collect(); - // let root_bounds_list = layout.children().map(|lo| lo.bounds()).collect(); - // // drop(state); + // dbg!(new_root.as_ref().map(|r| r.0)); + #[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(); + dbg!(overlay_offset); - // // dbg!(&root_bounds_list); - // // dbg!(self.menu_roots.clone().len()); - // // dbg!(self - // // .menu_roots - // // .clone() - // // .iter() - // // .map(|r| r.index) - // // .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(), - // }; - // // let mut state = self.tree.inner.lock().unwrap(); - // // dbg!(state.menu_states.keys()); - // // let Some(parent_root) = state.active_root.get(&self.window_id) else { - // // // TODO log warning - // // return status; - // // }; - // let Some(parent_root) = state.active_root.get(self.depth).cloned() else { - // return None; - // }; - // let mut roots = parent_root.clone(); - // roots.push(new_root); - // // dbg!(&roots); - // state.active_root.push(roots); - // // _ = state.menu_states.remove(&popup_id); - // // drop(state); - // Some((popup_menu, popup_id)) - // }) else { - // return status; - // }; - // 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 = self.tree.inner.with_data_mut(|state| { - // // let mut state = self.tree.inner.lock().unwrap(); - // state.menu_states.get_mut(self.depth).unwrap().push(new_ms); + let overlay_cursor = cursor.position().unwrap_or_default() - overlay_offset; - // state.menu_states[self.depth] - // .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, - // }, - // ) - // }); + 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(); // dbg!(active_roots); + // let root_bounds_list = active_roots + // .into_iter() + // .fold(layout, |l, active_root| { + // dbg!(active_root, l.children().count()); + // l.children().nth(active_root).unwrap() + // }) + // .children() + // .map(|c| c.bounds()) + // .collect(); - // // dbg!(&anchor_rect); + let root_bounds_list = layout + .children() + .next() + .unwrap() + .children() + .map(|lo| lo.bounds()) + .collect(); + // drop(state); - // // drop(state); - // let menu_node = Widget::layout( - // &menu, - // &mut Tree::empty(), - // renderer, - // &Limits::NONE.min_width(1.).min_height(1.), - // ); - // // dbg!(menu_node.size()); + // dbg!(&root_bounds_list); + // dbg!(self.menu_roots.clone().len()); + // dbg!(self + // .menu_roots + // .clone() + // .iter() + // .map(|r| r.index) + // .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(), + }; + // let mut state = self.tree.inner.lock().unwrap(); + // dbg!(state.menu_states.keys()); + // let Some(parent_root) = state.active_root.get(&self.window_id) else { + // // TODO log warning + // return status; + // }; + let Some(parent_root) = state.active_root.get(self.depth).cloned() else { + dbg!("NO ROOT?"); + return None; + }; + let mut roots = parent_root.clone(); + roots.push(new_root); + // dbg!(&roots); + state.active_root.push(roots); + // _ = state.menu_states.remove(&popup_id); + // drop(state); + 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 = self.tree.inner.with_data_mut(|state| { + // let mut state = self.tree.inner.lock().unwrap(); + state.menu_states.get_mut(self.depth).unwrap().push(new_ms); - // // dbg!(&menu_node); + state.menu_states[self.depth] + .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, + }, + ) + }); - // 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:cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, - // reactive: true, - // ..Default::default() - // }; - // let parent = self.window_id; - // // dbg!(&positioner); - // (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: true, - // input_zone: todo!(), - // }, - // Some(move || { - // crate::Element::from( - // crate::widget::container(menu.clone()).center(Length::Fill), - // ) - // .map(crate::action::app) - // }), - // )); + // dbg!(&anchor_rect); - // return status; - // } + // drop(state); + let menu_node = Widget::layout( + &menu, + &mut Tree::empty(), + renderer, + &Limits::NONE.min_width(1.).min_height(1.), + ); + // dbg!(menu_node.size()); + + // dbg!(&menu_node); + + 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:cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, + reactive: true, + ..Default::default() + }; + let parent = self.window_id; + dbg!(&positioner); + + 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 } } @@ -1129,12 +1134,15 @@ pub(super) fn init_root_menu( .is_none_or(|s| s.is_empty()) && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) { + // dbg!("exiting from root menu init early...", menu.depth); return; } - println!( - "init root menu {} {} {:?}", - menu.depth, menu.window_id, &menu.root_bounds_list + dbg!( + menu.depth, + menu.window_id, + &menu.root_bounds_list, + state.menu_states.get(menu.depth).is_none() ); dbg!(menu.menu_roots.len()); dbg!( @@ -1160,26 +1168,27 @@ pub(super) fn init_root_menu( .count() .saturating_sub(1) ); - let root = state - .active_root - .get(menu.depth) - .iter() - .map(|l| l.into_iter()) - .flatten() - .take( - state - .active_root - .get(menu.depth) - .into_iter() - .flatten() - .count() - .saturating_sub(1), - ) - .fold(menu.menu_roots.as_slice(), |m, i| { - dbg!(m.len()); - m[*i].children.as_slice() - }); - dbg!(root.len()); + // let root = state + // .active_root + // .get(menu.depth) + // .iter() + // .map(|l| l.into_iter()) + // .flatten() + // .take( + // state + // .active_root + // .get(menu.depth) + // .into_iter() + // .flatten() + // .count() + // .saturating_sub(1), + // ) + // .fold(menu.menu_roots.as_slice(), |m, i| { + // dbg!(m.len()); + // m[*i].children.as_slice() + // }); + // dbg!(root.len()); + dbg!(menu.depth); let mut set = false; for (i, (&root_bounds, mt)) in menu @@ -1235,6 +1244,7 @@ pub(super) fn init_root_menu( dbg!("inserting root menu state"); dbg!(&menu_bounds.children_bounds); dbg!((&menu_bounds.child_positions)); + dbg!(&state.active_root); state.active_root.insert(menu.depth, vec![i]); // do we need to insert the rest now too? let ms = MenuState { @@ -1279,6 +1289,7 @@ pub(super) fn init_root_popup_menu( .is_none_or(|s| s.is_empty()) && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) { + dbg!("exiting init early", menu.depth); return; } @@ -1309,82 +1320,79 @@ pub(super) fn init_root_popup_menu( .cloned() .unwrap_or_default(); dbg!(&active_roots); - dbg!(menu.menu_roots.len()); - let root = active_roots - .iter() - .take(active_roots.iter().count().saturating_sub(1)) - .fold(menu.menu_roots.as_slice(), |m, i| { - // dbg!(m.len()); - m[*i].children.as_slice() - }); - // dbg!(root.len()); + dbg!(menu.menu_roots.len(), menu.root_bounds_list.len()); let mut set = false; - for (i, (&root_bounds, mt)) in menu.root_bounds_list.iter().zip(root.iter()).enumerate() { - if mt.children.is_empty() { - // dbg!("skipping menu with no children"); - continue; - } - // dbg!(i, root_bounds.contains(overlay_cursor)); - if root_bounds.contains(overlay_cursor) { - // dbg!(i, root_bounds, mt.width, mt.height); - let view_center = viewport_size.width * 0.5; - let rb_center = root_bounds.center_x(); + let mt = active_roots + .iter() + .skip(if menu.is_overlay { 0 } else { 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]; - 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, - }; - - // dbg!( - // state.tree.children.len(), - // state.tree.children[0].children.len(), - // ); - - 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, - ); - set = true; - - let ms = MenuState { - index: None, - scroll_offset: 0.0, - menu_bounds, - }; - let v = super::menu_bar::get_mut_or_default(&mut state.menu_states, menu.depth); - v.push(ms); - - // Hack to ensure menu opens properly - shell.invalidate_layout(); - - break; - } + if mt.children.is_empty() { + panic!("skipping menu with no children"); } + dbg!(root_bounds, overlay_cursor); + 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, + ); + dbg!(mt.children.len(), root_bounds, overlay_cursor, i,); + dbg!(i, root_bounds, mt.width, mt.height); + 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 + }; + + // dbg!( + // state.tree.children.len(), + // state.tree.children[0].children.len(), + // ); + + set = true; + + let ms = MenuState { + index: None, + scroll_offset: 0.0, + menu_bounds, + }; + let v = super::menu_bar::get_mut_or_default(&mut state.menu_states, menu.depth); + v.push(ms); + dbg!(v.len(), menu.depth); + dbg!(i); + + // Hack to ensure menu opens properly + shell.invalidate_layout(); + if !set { - dbg!(overlay_cursor); + panic!("oops"); } }) } @@ -1482,6 +1490,7 @@ fn process_overlay_events( overlay_cursor: Point, cross_offset: f32, is_overlay: bool, + shell: &mut Shell<'_, Message>, ) -> (Option<(usize, MenuState)>, event::Status) where Message: std::clone::Clone, @@ -1500,15 +1509,14 @@ where menu.tree.inner.with_data_mut(|state| { let Some(active_root) = state.active_root.get(menu.depth).clone() else { + panic!(); if is_overlay && !menu.bar_bounds.contains(overlay_cursor) { - dbg!("overlay"); state.reset(); } return (new_menu_root, Ignored); }; if state.pressed { - dbg!("pressed"); return (new_menu_root, Ignored); } @@ -1559,20 +1567,26 @@ where // get indices let indices = menu_states.iter().map(|ms| ms.index).collect::>(); let should_add = is_overlay || menu_states.len() < 2; + dbg!(&indices); + let menu_state_len = menu_states.len(); + // dbg!(menu_states.iter().map(|m| m.index).collect::>()); // * update active item - let Some(last_menu_state) = menu_states.last_mut() else { - // no menus left - // TODO do we want to avoid this for popups? - state.active_root.remove(menu.depth); + let Some(last_menu_state) = menu_states.get_mut(0) else { + if menu.is_overlay { + // no menus left + // TODO do we want to avoid this for popups? + state.active_root.remove(menu.depth); - // 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 - dbg!("oops menu bar shouldn't disable popup"); - if !menu.bar_bounds.contains(overlay_cursor) { - state.open = false; + // 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; + } } + + dbg!("no last menu state", menu.depth, menu_states.len()); return (new_menu_root, Captured); }; @@ -1586,6 +1600,7 @@ where // cursor is outside { last_menu_state.index = None; + dbg!("cursor is outside..."); return (new_menu_root, Captured); } // cursor is in the children part @@ -1596,19 +1611,23 @@ where .clamp(0.0, last_children_bounds.height - 0.001); // dbg!(height_diff); - let (active_tree, roots) = active_root.iter().take(menu.depth).fold( - ( - &mut state.tree.children[active_root[0]].children, - &menu.menu_roots[active_root[0]].children, - ), - |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), - ); + let (active_tree, roots) = active_root + .iter() + .skip(if menu.is_overlay { 0 } else { 1 }) + .fold( + ( + &mut state.tree.children[active_root[0]].children, + &menu.menu_roots[active_root[0]].children, + ), + |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), + ); let active_menu = if is_overlay { - indices[0..indices.len().saturating_sub(1)] - .iter() - .fold(roots, |mt, i| { + indices[0..indices.len().saturating_sub(1)].iter().fold( + roots, + |mt: &Vec>, i| { &mt[i.expect("missing active child index in menu")].children - }) + }, + ) } else { // popup does one layer deep roots @@ -1637,7 +1656,20 @@ where // dbg!("skipping duplicate", last_menu_state.index); // return (None, Captured); // } - dbg!(&last_menu_state.index, new_index); + if last_menu_state + .index + .as_ref() + .is_some_and(|i| *i != new_index) + { + shell.publish((menu.on_surface_action.as_ref().unwrap())( + crate::surface::action::destroy_popup( + *state + .popup_id + .entry(menu.window_id) + .or_insert_with(window::Id::unique), + ), + )); + } let item = &active_menu[new_index]; // dbg!(new_index); @@ -1654,7 +1686,7 @@ where // set new index let old_index = last_menu_state.index.replace(new_index); // dbg!(should_add); - + dbg!(old_index, new_index); // get new active item // * add new menu if the new item is a menu if !item.children.is_empty() { @@ -1740,7 +1772,7 @@ where (viewport_size.height - (children_bounds.y + children_bounds.height)).min(0.0); (max_offset, min_offset) }; - let menu_states = state.menu_states.get_mut(menu.depth).unwrap(); + let menu_states = state.menu_states.get_mut(0).unwrap(); // update if menu_states.is_empty() { From ff41ff0532655a692dac780c46d4e680aafdf8ec Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 4 Jun 2025 10:28:05 -0400 Subject: [PATCH 09/12] avoid closing with children --- src/widget/menu/menu_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 2d87f8fc..c19b3ad4 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -599,7 +599,7 @@ where positioner: positioner.clone(), parent_size: None, grab: true, - close_with_children: true, + close_with_children: false, input_zone: None, }, Some(move || { From 51c391a6560724fad84e39a87f303861e9a2f8cd Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 5 Jun 2025 12:10:07 -0400 Subject: [PATCH 10/12] wip: on hover switch and fix for nesting --- examples/application/Cargo.toml | 3 +- src/widget/menu/menu_bar.rs | 301 +++++++++++++++------------- src/widget/menu/menu_inner.rs | 341 ++++++++++++++++++-------------- 3 files changed, 364 insertions(+), 281 deletions(-) diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 3c5ce8e2..18cf3b61 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [features] -default = ["wayland"] +default = [] wayland = ["libcosmic/wayland"] [dependencies] @@ -25,4 +25,5 @@ features = [ "wgpu", "single-instance", "multi-window", + "surface-message", ] diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index c19b3ad4..6c4bcea3 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -12,7 +12,6 @@ use super::{ use crate::{ Renderer, style::menu_bar::StyleSheet, - surface::action::destroy_popup, widget::{ RcWrapper, dropdown::menu::{self, State}, @@ -64,8 +63,7 @@ impl MenuBarStateInner { self.menu_states .get(index) .into_iter() - .map(|v| v.iter()) - .flatten() + .flat_map(|v| v.iter()) .take_while(|ms| ms.index.is_some()) .map(|ms| ms.index.expect("No indices were found in the menu state.")) } @@ -356,6 +354,145 @@ where self.on_surface_action = Some(Arc::new(handler)); self } + + fn create_popup( + &mut self, + layout: Layout<'_>, + view_cursor: Cursor, + renderer: &Renderer, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + my_state: &mut MenuBarState, + ) -> event::Status { + let mut status = event::Status::Ignored; + + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + // TODO emit Message to open menu + 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) + .filter(|r| r.len() == 1) + .map(|r| r[0]) + }); + + // if position is not on menu bar button skip. + let position = view_cursor.position(); + let hovered_root = layout + .children() + .position(|lo| view_cursor.is_over(lo.bounds())); + + if old_active_root == hovered_root { + return status; + } + 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 = my_state.inner.with_data_mut(|state| { + state.popup_id.insert(self.window_id, id); + state.menu_states[0] + .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, + }, + ) + }); + + 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:cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, + 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) + }), + ))); + } + status + } } impl Widget for MenuBar where @@ -448,7 +585,7 @@ where .inner .with_data(|d| !d.open && !d.active_root.is_empty()); - my_state.inner.with_data_mut(|state| { + let open = my_state.inner.with_data_mut(|state| { if reset { if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { dbg!("reset destroy"); @@ -459,6 +596,7 @@ where } } } + state.open }); match event { @@ -467,149 +605,48 @@ where let mut create_popup = false; if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) { state.view_cursor = view_cursor; - dbg!(view_cursor.is_over(layout.bounds())); state.open = true; create_popup = true; } else if let Some(id) = state.popup_id.remove(&self.window_id) { - dbg!("destroy popup..."); state.menu_states.clear(); state.active_root.clear(); let surface_action = self.on_surface_action.as_ref().unwrap(); state.open = false; - shell.publish(surface_action(destroy_popup(id))); + #[cfg(all( + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] + shell.publish(surface_action(crate::surface::action::destroy_popup(id))); state.view_cursor = view_cursor; } create_popup }); if !create_popup { - return root_status; + return event::Status::Ignored; } - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] - // TODO emit Message to open menu - 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) - .filter(|r| r.len() == 1) - .map(|r| r[0]) - }); - - // if position is not on menu bar button skip. - let position = view_cursor.position(); - let hovered_root = layout - .children() - .position(|lo| view_cursor.is_over(lo.bounds())); - - 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 = my_state.inner.with_data_mut(|state| { - state.popup_id.insert(self.window_id, id); - state.menu_states[0] - .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, - }, - ) - }); - - 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:cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, - 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) - }), - ))); - } + return root_status.merge(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()) => + { + return root_status.merge(self.create_popup( + layout, + view_cursor, + renderer, + shell, + viewport, + my_state, + )); } // Window(Focused) => { // my_state.inner.with_data_mut(|state| { diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 5d78feab..ec5300c7 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -503,7 +503,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { if let Some(ms) = data.menu_states.get(self.depth) { ms.iter() .enumerate() - .filter(|ms| self.is_overlay || ms.0 < active_root.len()) + .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); @@ -564,7 +564,11 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { }; use touch::Event::{FingerLifted, FingerMoved, FingerPressed}; - if !self.tree.inner.with_data(|data| data.open) { + if !self + .tree + .inner + .with_data(|data| data.open || data.active_root.len() <= self.depth) + { return (None, Ignored); }; @@ -612,11 +616,9 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } Mouse(CursorMoved { position }) | Touch(FingerMoved { position, .. }) => { - dbg!("moved", self.window_id); let view_cursor = Cursor::Available(position); let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset; if !(self.is_overlay || view_cursor.is_over(viewport)) { - dbg!("exit early", view_cursor, viewport); return (None, menu_status); } // dbg!(view_cursor, viewport, self.window_id); @@ -665,9 +667,21 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { if needs_reset { dbg!("reset"); if let Some(handler) = self.on_surface_action.as_ref() { - shell.publish((handler)(crate::surface::Action::DestroyPopup( - self.window_id, - ))); + 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.clone(); + depth = depth.saturating_sub(1); + } + dbg!(root); + shell + .publish((handler)(crate::surface::Action::DestroyPopup(root))); } state.reset(); @@ -700,13 +714,12 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { view_cursor: Cursor, ) { self.tree.inner.with_data(|state| { - if !state.open { + if !state.open || state.active_root.len() <= self.depth { return; } let Some(active_root) = state.active_root.get(self.depth) else { return; }; - dbg!(self.depth, &active_root); let viewport = layout.bounds(); let viewport_size = viewport.size(); let overlay_offset = Point::ORIGIN - viewport.position(); @@ -728,104 +741,103 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { ), |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), ); - let indices = state.get_trimmed_indices(self.depth).collect::>(); + state.menu_states[self.depth] .iter() .zip(layout.children()) .enumerate() - .filter(|ms: &(usize, (&MenuState, Layout<'_>))| { - self.is_overlay || ms.0 < active_root.len() - }) - .fold(roots, |menu_roots, (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 => self.depth == state.active_root.len() - 1, - }); - - // react only to the last menu - if self.depth == state.active_root.len() - 1 { - view_cursor - } else { - Cursor::Available([-1.0; 2].into()) - }; - dbg!(view_cursor); - - 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; - - 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); - dbg!(ms.index, children_layout.children().count(), start_index); - // 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); + .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().map_or(false, |ph| match ph { + PathHighlight::Full => true, + PathHighlight::OmitActive => { + !indices.is_empty() && i < indices.len() - 1 } - } - if start_index < menu_roots.len() { - // dbg!(start_index, end_index, menu_roots.len()); - // draw item - menu_roots[start_index..=end_index] - .iter() - .zip(children_layout.children()) - .for_each(|(mt, clo)| { - dbg!(self.depth, view_cursor, clo.bounds()); + PathHighlight::MenuActive => self.depth == state.active_root.len() - 1, + }); - mt.item.draw( - &active_tree[mt.index], - r, - theme, - style, - clo, - view_cursor, - &children_layout.bounds(), - ); - }); - } - }; + // react only to the last menu + if self.depth == state.active_root.len() - 1 { + view_cursor + } else { + Cursor::Available([-1.0; 2].into()) + }; - renderer.with_layer(render_bounds, draw_menu); + 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; - // only the last menu can have a None active index - ms.index - .map_or(menu_roots, |active| &menu_roots[active].children) - }); + 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() { + // dbg!(start_index, end_index, menu_roots.len()); + // draw item + menu_roots[start_index..=end_index] + .iter() + .zip(children_layout.children()) + .for_each(|(mt, clo)| { + mt.item.draw( + &active_tree[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) + }, + ); }) } } @@ -914,13 +926,8 @@ impl<'a, Message: std::clone::Clone + 'static> Widget, viewport: &Rectangle, ) -> event::Status { - let prev_hover = self.is_overlay.then(|| { - self.tree.inner.with_data(|d| { - let menu_states = d.menu_states.get(self.depth).unwrap(); - menu_states.get(0).and_then(|ms| ms.index) - }) - }); let (new_root, status) = self.on_event(event, layout, cursor, renderer, clipboard, shell); + // dbg!(new_root.as_ref().map(|r| r.0)); #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] if let Some((new_root, new_ms)) = new_root { @@ -928,7 +935,6 @@ impl<'a, Message: std::clone::Clone + 'static> Widget Widget Widget Widget Widget Widget Widget Widget( .get(menu.depth) .is_none_or(|s| s.is_empty()) && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) + || menu.depth > 0 + || !state.open { // dbg!("exiting from root menu init early...", menu.depth); return; @@ -1436,13 +1465,26 @@ fn process_menu_events<'b, Message: std::clone::Clone>( // get active item // let mt = indices.iter().fold(root | mt, &i | &mut mt.children[i]); - let (tree, mt) = indices.iter().take(indices.len()).fold( - ( - &mut state.tree.children[active_root[0]].children, - &mut menu_roots[active_root[0]], - ), - |(tree, mt), next_active_root| (tree, &mut mt.children[*next_active_root]), - ); + // let (tree, mt) = indices.iter().take(indices.len()).fold( + // ( + // &mut state.tree.children[active_root[0]].children, + // &mut menu_roots[active_root[0]], + // ), + // |(tree, mt), next_active_root| { + // dbg!(mt.children.len(), next_active_root); + // (tree, &mut mt.children[*next_active_root]) + // }, + // ); + let (tree, mt) = active_root + .iter() + .skip(if menu.is_overlay { 0 } else { 1 }) + .fold( + ( + &mut state.tree.children[active_root[0]].children, + &mut menu_roots[active_root[0]], + ), + |(tree, mt), next_active_root| (tree, &mut mt.children[*next_active_root]), + ); // let Some(i) = state.menu_states[menu.depth].iter().position(|ms| { // ms.menu_bounds // .check_bounds @@ -1509,7 +1551,6 @@ where menu.tree.inner.with_data_mut(|state| { let Some(active_root) = state.active_root.get(menu.depth).clone() else { - panic!(); if is_overlay && !menu.bar_bounds.contains(overlay_cursor) { state.reset(); } @@ -1525,11 +1566,18 @@ where state.view_cursor = view_cursor; // * remove invalid menus + dbg!( + &state + .menu_states + .iter() + .map(|s| s.iter().map(|s| s.index).collect::>()) + .collect::>() + ); let mut prev_bounds = std::iter::once(menu.bar_bounds) .chain( - state.menu_states[menu.depth][..state.menu_states.len().saturating_sub(1).min(1)] + (state.menu_states[..menu.depth]) .iter() - .map(|ms| ms.menu_bounds.children_bounds), + .map(|s| s[0].menu_bounds.children_bounds), ) .collect::>(); let menu_states = state.menu_states.get_mut(menu.depth).unwrap(); @@ -1569,7 +1617,6 @@ where let should_add = is_overlay || menu_states.len() < 2; dbg!(&indices); - let menu_state_len = menu_states.len(); // dbg!(menu_states.iter().map(|m| m.index).collect::>()); // * update active item let Some(last_menu_state) = menu_states.get_mut(0) else { @@ -1621,6 +1668,9 @@ where ), |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), ); + if is_overlay { + panic!(); + } let active_menu = if is_overlay { indices[0..indices.len().saturating_sub(1)].iter().fold( roots, @@ -1648,27 +1698,19 @@ where ) } }; - // if last_menu_state - // .index - // .as_ref() - // .is_some_and(|old_index| *old_index == new_index) - // { - // dbg!("skipping duplicate", last_menu_state.index); - // return (None, Captured); - // } - if last_menu_state + let remove = last_menu_state .index .as_ref() - .is_some_and(|i| *i != new_index) - { - shell.publish((menu.on_surface_action.as_ref().unwrap())( - crate::surface::action::destroy_popup( - *state - .popup_id - .entry(menu.window_id) - .or_insert_with(window::Id::unique), - ), - )); + .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.pop(); + shell.publish((menu.on_surface_action.as_ref().unwrap())({ + crate::surface::action::destroy_popup(id) + })) + }; } let item = &active_menu[new_index]; @@ -1737,6 +1779,9 @@ where v.push(ms); } } + if remove { + state.menu_states.pop(); + } (new_menu_root, Captured) }) From 3fd6c4f6bced58ddc8822a9b3c16d7b22763d515 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 9 Jun 2025 10:38:33 -0400 Subject: [PATCH 11/12] popup refactor --- examples/application/Cargo.toml | 2 +- examples/application/src/main.rs | 26 +- src/widget/menu/menu_bar.rs | 111 +++---- src/widget/menu/menu_inner.rs | 519 +++++++----------------------- src/widget/menu/menu_tree.rs | 5 +- src/widget/responsive_menu_bar.rs | 2 +- 6 files changed, 180 insertions(+), 485 deletions(-) diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 18cf3b61..e5ae2f30 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [features] -default = [] +default = ["wayland"] wayland = ["libcosmic/wayland"] [dependencies] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index b37e263f..c1aba67f 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -257,16 +257,16 @@ impl cosmic::Application for App { menu::Item::Button("hi 21", None, Action::Hi), menu::Item::Button("hi 22", None, Action::Hi2), menu::Item::Folder( - "nest 3 2".into(), + "nest 3 2 >".into(), vec![ - menu::Item::Button("31", None, Action::Hi), - menu::Item::Button("342", None, Action::Hi2), - menu::Item::Button("3443", None, Action::Hi3), + 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(), + "nest 4 2 >".into(), vec![ - menu::Item::Button("343", None, Action::Hi2), - menu::Item::Button("3444", None, Action::Hi), + menu::Item::Button("243", None, Action::Hi2), + menu::Item::Button("2444", None, Action::Hi), ], ), ], @@ -276,9 +276,12 @@ impl cosmic::Application for App { ( "hi 3".into(), vec![ - menu::Item::Button("hi 33", None, Action::Hi), - menu::Item::Button("hi 333", None, Action::Hi2), + 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), ], ), ( @@ -288,13 +291,12 @@ impl cosmic::Application for App { menu::Item::Button("hi 44", None, Action::Hi2), menu::Item::Button("hi 444", None, Action::Hi3), menu::Item::Folder( - "nest 4".into(), + "nest 4 >".into(), vec![ menu::Item::Button("hi 41", None, Action::Hi), menu::Item::Button("hi 442", None, Action::Hi2), - menu::Item::Button("hi 4443", None, Action::Hi3), menu::Item::Folder( - "nest 2 4".into(), + "nest 3 4 >".into(), vec![ menu::Item::Button("hi 443", None, Action::Hi2), menu::Item::Button("hi 4444", None, Action::Hi), diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 6c4bcea3..8658f29a 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -51,25 +51,23 @@ pub(crate) struct MenuBarStateInner { pub(crate) bar_pressed: bool, pub(crate) view_cursor: Cursor, pub(crate) open: bool, - pub(crate) active_root: Vec>, + 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>, + pub(crate) menu_states: Vec, } 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 - .get(index) - .into_iter() - .flat_map(|v| v.iter()) + .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) { - dbg!("reset"); self.open = false; self.active_root = Vec::new(); self.menu_states.clear(); @@ -205,13 +203,7 @@ where pub fn new(menu_roots: Vec>) -> Self { let mut menu_roots = menu_roots; menu_roots.iter_mut().for_each(MenuTree::set_index); - // println!("======================================================"); - // for menu_root in &menu_roots { - // dbg!(menu_root.index); - // for inner_root in &menu_root.children { - // dbg!(inner_root.index); - // } - // } + Self { width: Length::Shrink, height: Length::Shrink, @@ -326,7 +318,6 @@ where } #[cfg(all(feature = "wayland", feature = "winit"))] - pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -335,11 +326,13 @@ where 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; @@ -347,6 +340,7 @@ where self } + #[must_use] pub fn on_surface_action( mut self, handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, @@ -355,6 +349,8 @@ where self } + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[allow(clippy::too_many_lines)] fn create_popup( &mut self, layout: Layout<'_>, @@ -363,11 +359,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, my_state: &mut MenuBarState, - ) -> event::Status { - let mut status = event::Status::Ignored; - - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] - // TODO emit Message to open menu + ) { 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::{ @@ -375,22 +367,20 @@ where }; 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) - .filter(|r| r.len() == 1) - .map(|r| r[0]) - }); + 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 position = view_cursor.position(); let hovered_root = layout .children() .position(|lo| view_cursor.is_over(lo.bounds())); - if old_active_root == hovered_root { - return status; + 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() { @@ -441,7 +431,8 @@ where ); let anchor_rect = my_state.inner.with_data_mut(|state| { state.popup_id.insert(self.window_id, id); - state.menu_states[0] + state + .menu_states .iter() .find(|s| s.index.is_none()) .map(|s| s.menu_bounds.parent_bounds) @@ -491,7 +482,6 @@ where }), ))); } - status } } impl Widget for MenuBar @@ -588,8 +578,6 @@ where let open = my_state.inner.with_data_mut(|state| { if reset { if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { - dbg!("reset destroy"); - if let Some(handler) = self.on_surface_action.as_ref() { shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); state.reset(); @@ -607,17 +595,22 @@ where state.view_cursor = view_cursor; state.open = true; create_popup = true; - } else if let Some(id) = state.popup_id.remove(&self.window_id) { + } else if let Some(_id) = state.popup_id.remove(&self.window_id) { state.menu_states.clear(); state.active_root.clear(); - let surface_action = self.on_surface_action.as_ref().unwrap(); state.open = false; #[cfg(all( feature = "wayland", feature = "winit", feature = "surface-message" ))] - shell.publish(surface_action(crate::surface::action::destroy_popup(id))); + { + 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 @@ -626,38 +619,15 @@ where if !create_popup { return event::Status::Ignored; } - - return root_status.merge(self.create_popup( - layout, - view_cursor, - renderer, - shell, - viewport, - my_state, - )); + #[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()) => { - return root_status.merge(self.create_popup( - layout, - view_cursor, - renderer, - shell, - viewport, - my_state, - )); + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); } - // Window(Focused) => { - // my_state.inner.with_data_mut(|state| { - // 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)(destroy_popup(popup_id))); - // state.reset(); - // } - // } - // }); - // } _ => (), } @@ -686,11 +656,7 @@ where // draw path highlight if self.path_highlight.is_some() { let styling = theme.appearance(&self.style); - if let Some(active) = state - .active_root - .get(0) - .and_then(|active_root| active_root.get(0)) - { + if let Some(active) = state.active_root.get(0) { let active_bounds = layout .children() .nth(*active) @@ -734,16 +700,13 @@ where _renderer: &Renderer, translation: Vector, ) -> Option> { - #[cfg(all(feature = "wayland", feature = "winit"))] + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] return None; let state = tree.state.downcast_ref::(); - if state - .inner - .with_data(|state| !state.open || state.active_root.is_empty()) - { + if state.inner.with_data(|state| !state.open) { return None; - }; + } Some( Menu { diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index ec5300c7..3b783af0 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -230,7 +230,7 @@ pub(super) struct MenuSlice { pub(super) upper_bound_rel: f32, } -#[derive(Clone)] +#[derive(Debug, Clone)] /// Menu bounds in overlay space pub struct MenuBounds { child_positions: Vec, @@ -322,9 +322,6 @@ impl MenuState { // self.menu_bounds.child_positions.len() // ); - // for mt in &menu_tree.children[start_index..=end_index] { - // dbg!(mt.index); - // } // viewport space children bounds let children_bounds = self.menu_bounds.children_bounds + overlay_offset; @@ -336,7 +333,6 @@ impl MenuState { .map(|(i, ((cp, size), mt))| { let mut position = *cp; let mut size = *size; - // dbg!(position, size); if position < lower_bound_rel && (position + size.height) > lower_bound_rel { size.height = position + size.height - lower_bound_rel; @@ -345,13 +341,9 @@ impl MenuState { { size.height = upper_bound_rel - position; } - // dbg!(size); let limits = Limits::new(size, size); - // dbg!(i, mt.index, tree.len()); - // for child in &mt.children { - // dbg!(child.index); - // } + mt.item .layout(&mut tree[mt.index], renderer, &limits) @@ -359,11 +351,8 @@ impl MenuState { }) .collect::>(); - // dbg!(children_bounds.size()); - let node = Node::with_children(children_bounds.size(), child_nodes) - .move_to(children_bounds.position()); - // dbg!(&node); - node + Node::with_children(children_bounds.size(), child_nodes) + .move_to(children_bounds.position()) } fn layout_single( @@ -479,29 +468,30 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { 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(Size::ZERO); + } let overlay_offset = Point::ORIGIN - position; let tree_children = &mut data.tree.children; - let children = data - .active_root - .get(self.depth) - .cloned() + let children = (if self.is_overlay {0} else {self.depth}..=self.depth) .map(|active_root| { - if active_root.is_empty() || self.menu_roots.is_empty() { + if self.menu_roots.is_empty() { return (&empty, vec![]); } - let (active_tree, roots) = active_root + let (active_tree, roots) = data.active_root[..=active_root] .iter() - .skip(if self.is_overlay { 0 } else { 1 }) + .skip(1) .fold( ( - &mut tree_children[active_root[0]].children, - &self.menu_roots[active_root[0]].children, + &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), + |(tree, mt), next_active_root| { + (tree, &mt[*next_active_root].children) + }, ); - if let Some(ms) = data.menu_states.get(self.depth) { - ms.iter() + data.menu_states[if self.is_overlay {0} else {self.depth}..=self.depth].iter() .enumerate() .filter(|ms| self.is_overlay || ms.0 < 1) .fold((roots, Vec::new()), |(menu_root, mut nodes), (_i, ms)| { @@ -528,23 +518,19 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { nodes, ) }) - } else { - (&empty, vec![]) - } - }) - .map(|(_, l)| l) - .unwrap_or_default(); - // dbg!(intrinsic_size); + + }).map(|(_, l)| l).next().unwrap_or_default(); + // overlay space viewport rectangle - let node = Node::with_children( + Node::with_children( limits.resolve(Length::Shrink, Length::Shrink, intrinsic_size), children, ) - .translate(Point::ORIGIN - position); - node + .translate(Point::ORIGIN - position) }) } + #[allow(clippy::too_many_lines)] fn on_event( &mut self, event: event::Event, @@ -618,10 +604,9 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { Mouse(CursorMoved { position }) | Touch(FingerMoved { position, .. }) => { let view_cursor = Cursor::Available(position); let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset; - if !(self.is_overlay || view_cursor.is_over(viewport)) { + if !self.is_overlay && !view_cursor.is_over(viewport) { return (None, menu_status); } - // dbg!(view_cursor, viewport, self.window_id); let (new_root, status) = process_overlay_events( self, @@ -631,10 +616,8 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { view_cursor, overlay_cursor, self.cross_offset as f32, - self.is_overlay, shell, ); - dbg!(new_root.as_ref().map(|new_root| new_root.0)); return (new_root, status.merge(menu_status)); } @@ -651,7 +634,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .distance(view_cursor.position().unwrap_or_default()) < 2.0 { - let is_inside = state.menu_states[self.depth] + let is_inside = state.menu_states[..=self.depth] .iter() .any(|ms| ms.menu_bounds.check_bounds.contains(overlay_cursor)); let mut needs_reset = false; @@ -665,7 +648,6 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { needs_reset |= self.close_condition.click_outside && !is_inside; if needs_reset { - dbg!("reset"); if let Some(handler) = self.on_surface_action.as_ref() { let mut root = self.window_id; let mut depth = self.depth; @@ -679,7 +661,6 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { root = parent.0.clone(); depth = depth.saturating_sub(1); } - dbg!(root); shell .publish((handler)(crate::surface::Action::DestroyPopup(root))); } @@ -704,7 +685,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { (None, ret) } - #[allow(unused_results)] + #[allow(unused_results, clippy::too_many_lines)] fn draw( &self, renderer: &mut crate::Renderer, @@ -717,9 +698,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { if !state.open || state.active_root.len() <= self.depth { return; } - let Some(active_root) = state.active_root.get(self.depth) else { - return; - }; + let active_root = &state.active_root[..=self.depth]; let viewport = layout.bounds(); let viewport_size = viewport.size(); let overlay_offset = Point::ORIGIN - viewport.position(); @@ -733,7 +712,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { let styling = theme.appearance(&self.style); let (active_tree, roots) = active_root .iter() - .skip(if self.is_overlay { 0 } else { 1 }) + .skip(1) .fold( ( &state.tree.children[active_root[0]].children, @@ -742,8 +721,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), ); let indices = state.get_trimmed_indices(self.depth).collect::>(); - - state.menu_states[self.depth] + state.menu_states[if self.is_overlay {0} else {self.depth}..=self.depth] .iter() .zip(layout.children()) .enumerate() @@ -812,7 +790,6 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } } if start_index < menu_roots.len() { - // dbg!(start_index, end_index, menu_roots.len()); // draw item menu_roots[start_index..=end_index] .iter() @@ -838,7 +815,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .map_or(menu_roots, |active| &menu_roots[active].children) }, ); - }) + }); } } impl overlay::Overlay @@ -897,7 +874,6 @@ impl<'a, Message: std::clone::Clone + 'static> Widget iced_core::layout::Node { - // dbg!("layout", self.window_id, limits); Menu::layout(self, renderer, *limits) } @@ -928,7 +904,6 @@ impl<'a, Message: std::clone::Clone + 'static> Widget event::Status { let (new_root, status) = self.on_event(event, layout, cursor, renderer, clipboard, shell); - // dbg!(new_root.as_ref().map(|r| r.0)); #[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::{ @@ -948,11 +923,9 @@ impl<'a, Message: std::clone::Clone + 'static> Widget Widget>()); let mut popup_menu = Menu { tree: self.tree.clone(), menu_roots: Cow::Owned(Cow::into_owned(self.menu_roots.clone())), @@ -997,24 +962,7 @@ impl<'a, Message: std::clone::Clone + 'static> Widget Widget Widget Widget( if !(state .menu_states .get(menu.depth) - .is_none_or(|s| s.is_empty()) + .is_none() && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) || menu.depth > 0 || !state.open { - // dbg!("exiting from root menu init early...", menu.depth); return; } - dbg!( - menu.depth, - menu.window_id, - &menu.root_bounds_list, - state.menu_states.get(menu.depth).is_none() - ); - dbg!(menu.menu_roots.len()); - dbg!( - state - .menu_states - .get(menu.depth) - .is_none_or(|s| s.is_empty()) - ); - dbg!( - menu.menu_roots - .as_slice() - .iter() - .map(|r| r.index) - .collect::>() - ); - - dbg!( - state - .active_root - .get(menu.depth) - .into_iter() - .flatten() - .count() - .saturating_sub(1) - ); - // let root = state - // .active_root - // .get(menu.depth) - // .iter() - // .map(|l| l.into_iter()) - // .flatten() - // .take( - // state - // .active_root - // .get(menu.depth) - // .into_iter() - // .flatten() - // .count() - // .saturating_sub(1), - // ) - // .fold(menu.menu_roots.as_slice(), |m, i| { - // dbg!(m.len()); - // m[*i].children.as_slice() - // }); - // dbg!(root.len()); - dbg!(menu.depth); - let mut set = false; for (i, (&root_bounds, mt)) in menu .root_bounds_list @@ -1227,14 +1093,10 @@ pub(super) fn init_root_menu( .enumerate() { if mt.children.is_empty() { - // dbg!("skipping menu with no children"); continue; } - dbg!(mt.children.len()); - dbg!(i, root_bounds.contains(overlay_cursor)); + if root_bounds.contains(overlay_cursor) { - dbg!(root_bounds); - // dbg!(i, root_bounds, mt.width, mt.height); let view_center = viewport_size.width * 0.5; let rb_center = root_bounds.center_x(); @@ -1268,22 +1130,14 @@ pub(super) fn init_root_menu( menu.is_overlay, ); set = true; - dbg!(i); - dbg!("inserting root menu state"); - dbg!(&menu_bounds.children_bounds); - dbg!((&menu_bounds.child_positions)); - dbg!(&state.active_root); - state.active_root.insert(menu.depth, vec![i]); - // do we need to insert the rest now too? + state.active_root.insert(menu.depth, i); let ms = MenuState { index: None, scroll_offset: 0.0, menu_bounds, }; - dbg!("pushing to menu states..."); - let v = super::menu_bar::get_mut_or_default(&mut state.menu_states, menu.depth); - v.push(ms); + state.menu_states.push(ms); // Hack to ensure menu opens properly shell.invalidate_layout(); @@ -1292,12 +1146,12 @@ pub(super) fn init_root_menu( } } if !set { - dbg!(overlay_cursor); panic!("huh"); } }); } +#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] pub(super) fn init_root_popup_menu( menu: &mut Menu<'_, Message>, renderer: &crate::Renderer, @@ -1310,61 +1164,29 @@ pub(super) fn init_root_popup_menu( ) where Message: std::clone::Clone, { - dbg!("init"); menu.tree.inner.with_data_mut(|state| { if !(state .menu_states .get(menu.depth) - .is_none_or(|s| s.is_empty()) + .is_none() && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) { - dbg!("exiting init early", menu.depth); return; } - println!( - "init root menu {} {:?}", - menu.window_id, menu.root_bounds_list - ); - // dbg!(state - // .menu_states - // .get(&menu.window_id) - // .is_none_or(|s| s.is_empty())); - // dbg!(menu - // .menu_roots - // .as_slice() - // .iter() - // .map(|r| r.index) - // .collect::>()); - - // dbg!(state - // .active_root - // .get(&menu.window_id) - // .into_iter() - // .flatten() - // .count()); - let active_roots = state - .active_root - .get(menu.depth) - .cloned() - .unwrap_or_default(); - dbg!(&active_roots); - dbg!(menu.menu_roots.len(), menu.root_bounds_list.len()); + let active_roots = &state + .active_root[..=menu.depth]; let mut set = false; - let mt = active_roots - .iter() - .skip(if menu.is_overlay { 0 } else { 1 }) + 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]; - if mt.children.is_empty() { - panic!("skipping menu with no children"); - } - dbg!(root_bounds, overlay_cursor); + assert!(!mt.children.is_empty(), "skipping menu with no children"); let aod = Aod { horizontal: true, vertical: true, @@ -1389,8 +1211,7 @@ pub(super) fn init_root_popup_menu( &mut state.tree.children[0].children, menu.is_overlay, ); - dbg!(mt.children.len(), root_bounds, overlay_cursor, i,); - dbg!(i, root_bounds, mt.width, mt.height); + let view_center = viewport_size.width * 0.5; let rb_center = root_bounds.center_x(); @@ -1399,12 +1220,6 @@ pub(super) fn init_root_popup_menu( } else { Direction::Positive }; - - // dbg!( - // state.tree.children.len(), - // state.tree.children[0].children.len(), - // ); - set = true; let ms = MenuState { @@ -1412,23 +1227,18 @@ pub(super) fn init_root_popup_menu( scroll_offset: 0.0, menu_bounds, }; - let v = super::menu_bar::get_mut_or_default(&mut state.menu_states, menu.depth); - v.push(ms); - dbg!(v.len(), menu.depth); - dbg!(i); + state.menu_states.push(ms); // Hack to ensure menu opens properly shell.invalidate_layout(); - - if !set { - panic!("oops"); - } - }) + // non tree buttons arent active? + assert!(set, "oops"); + }); } #[allow(clippy::too_many_arguments)] -fn process_menu_events<'b, Message: std::clone::Clone>( - menu: &'b mut Menu, +fn process_menu_events( + menu: &mut Menu, event: event::Event, view_cursor: Cursor, renderer: &crate::Renderer, @@ -1438,70 +1248,44 @@ fn process_menu_events<'b, Message: std::clone::Clone>( ) -> event::Status { use event::Status; - let window_id = menu.window_id; - let is_overlay = menu.is_overlay; 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| { - let Some(active_root) = state.active_root.get(menu.depth).cloned() else { + if state.active_root.len() <= menu.depth { + return event::Status::Ignored; + } + + let Some(hover) = state.menu_states.last_mut() else { return Status::Ignored; }; - let indices = state.get_trimmed_indices(menu.depth); - - let indices = indices.collect::>(); - // if is_overlay { - // indices.collect::>() - // } else { - // indices.take(1).collect::>() - // }; - - if indices.is_empty() { + let Some(hover_index) = hover.index else { return Status::Ignored; - } + }; - // get active item - // let mt = indices.iter().fold(root | mt, &i | &mut mt.children[i]); - // let (tree, mt) = indices.iter().take(indices.len()).fold( - // ( - // &mut state.tree.children[active_root[0]].children, - // &mut menu_roots[active_root[0]], - // ), - // |(tree, mt), next_active_root| { - // dbg!(mt.children.len(), next_active_root); - // (tree, &mut mt.children[*next_active_root]) - // }, - // ); - let (tree, mt) = active_root + let (tree, mt) = state.active_root .iter() - .skip(if menu.is_overlay { 0 } else { 1 }) + .skip(1) .fold( + // then use menu states for each open menu ( - &mut state.tree.children[active_root[0]].children, - &mut menu_roots[active_root[0]], + &mut state.tree.children[state.active_root[0]].children, + &mut menu_roots[state.active_root[0]], ), |(tree, mt), next_active_root| (tree, &mut mt.children[*next_active_root]), ); - // let Some(i) = state.menu_states[menu.depth].iter().position(|ms| { - // ms.menu_bounds - // .check_bounds - // .contains(view_cursor.position().unwrap_or(Point { x: -1., y: -1. })) - // }) else { - // return Status::Ignored; - // }; - // let mt = &mut mt.children[indices[indices.len() - 1]]; + - // widget tree + let mt = &mut mt.children[hover_index]; let tree = &mut tree[mt.index]; // get layout - let last_ms = &state.menu_states[menu.depth][indices.len() - 1]; - let child_node = last_ms.layout_single( + let child_node = hover.layout_single( overlay_offset, - last_ms.index.expect("missing index within menu state."), + hover.index.expect("missing index within menu state."), renderer, mt, tree, @@ -1531,8 +1315,7 @@ fn process_overlay_events( view_cursor: Cursor, overlay_cursor: Point, cross_offset: f32, - is_overlay: bool, - shell: &mut Shell<'_, Message>, + _shell: &mut Shell<'_, Message>, ) -> (Option<(usize, MenuState)>, event::Status) where Message: std::clone::Clone, @@ -1550,12 +1333,7 @@ where let mut new_menu_root = None; menu.tree.inner.with_data_mut(|state| { - let Some(active_root) = state.active_root.get(menu.depth).clone() else { - if is_overlay && !menu.bar_bounds.contains(overlay_cursor) { - state.reset(); - } - return (new_menu_root, Ignored); - }; + let active_root = &state.active_root[..=menu.depth]; if state.pressed { return (new_menu_root, Ignored); @@ -1566,28 +1344,24 @@ where state.view_cursor = view_cursor; // * remove invalid menus - dbg!( - &state - .menu_states - .iter() - .map(|s| s.iter().map(|s| s.index).collect::>()) - .collect::>() - ); + let mut prev_bounds = std::iter::once(menu.bar_bounds) .chain( - (state.menu_states[..menu.depth]) - .iter() - .map(|s| s[0].menu_bounds.children_bounds), + 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::>(); - let menu_states = state.menu_states.get_mut(menu.depth).unwrap(); - - if menu.close_condition.leave { - for i in (0..menu_states.len()).rev() { - let mb = &menu_states[i].menu_bounds; + + 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) - || is_overlay && mb.children_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))) @@ -1595,31 +1369,18 @@ where break; } prev_bounds.pop(); - menu_states.pop(); - } - } else { - for i in (0..menu_states.len()).rev() { - let mb = &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(); - menu_states.pop(); + state.menu_states.pop(); } } - // get indices - let indices = menu_states.iter().map(|ms| ms.index).collect::>(); - let should_add = is_overlay || menu_states.len() < 2; - dbg!(&indices); - - // dbg!(menu_states.iter().map(|m| m.index).collect::>()); // * update active item - let Some(last_menu_state) = menu_states.get_mut(0) else { + let menu_states_len = state.menu_states.len(); + let Some(last_menu_state) = state.menu_states.get_mut(if menu.is_overlay { + menu_states_len - 1 + } else { + menu.depth + }) else { if menu.is_overlay { // no menus left // TODO do we want to avoid this for popups? @@ -1633,7 +1394,6 @@ where } } - dbg!("no last menu state", menu.depth, menu_states.len()); return (new_menu_root, Captured); }; @@ -1641,26 +1401,27 @@ where let last_parent_bounds = last_menu_bounds.parent_bounds; let last_children_bounds = last_menu_bounds.children_bounds; - if (is_overlay && !menu.menu_overlays_parent && last_parent_bounds.contains(overlay_cursor)) + if (menu.is_overlay && !menu.menu_overlays_parent && last_parent_bounds.contains(overlay_cursor)) // cursor is in the parent part - || is_overlay && !last_children_bounds.contains(overlay_cursor) + || menu.is_overlay && !last_children_bounds.contains(overlay_cursor) // cursor is outside { last_menu_state.index = None; - dbg!("cursor is outside..."); return (new_menu_root, Captured); } // cursor is in the children part + // TODO set active root here even when not a tree. + // ensure that the + // 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); - // dbg!(height_diff); let (active_tree, roots) = active_root .iter() - .skip(if menu.is_overlay { 0 } else { 1 }) + .skip(1) .fold( ( &mut state.tree.children[active_root[0]].children, @@ -1668,22 +1429,8 @@ where ), |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), ); - if is_overlay { - panic!(); - } - let active_menu = if is_overlay { - indices[0..indices.len().saturating_sub(1)].iter().fold( - roots, - |mt: &Vec>, i| { - &mt[i.expect("missing active child index in menu")].children - }, - ) - } else { - // popup does one layer deep - roots - }; - // dbg!(active_menu.children.len()); - // dbg!(menu.window_id); + + let active_menu = roots; let new_index = match menu.item_height { ItemHeight::Uniform(u) => (height_diff / f32::from(u)).floor() as usize, ItemHeight::Static(_) | ItemHeight::Dynamic(_) => { @@ -1698,41 +1445,32 @@ where ) } }; - let remove = last_menu_state - .index - .as_ref() - .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); + + + let remove = !menu.is_overlay + && 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.pop(); - shell.publish((menu.on_surface_action.as_ref().unwrap())({ - crate::surface::action::destroy_popup(id) - })) - }; + { + 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]; - // dbg!(new_index); - // dbg!(item.index); - // if the index changes, get a new root - // if !last_menu_state.index.is_some_and(|old| old == new_index) && !item.children.is_empty() { - // dbg!(&indices); - // dbg!(new_index); - // dbg!(item.children.len()); - // dbg!(menu.window_id); - // new_menu_root = Some(new_index); - // } // set new index let old_index = last_menu_state.index.replace(new_index); - // dbg!(should_add); - dbg!(old_index, new_index); // get new active item // * add new menu if the new item is a menu - if !item.children.is_empty() { - // dbg!("its 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, @@ -1770,19 +1508,14 @@ where menu.is_overlay, ), }; - if !old_index.is_some_and(|old| old == new_index) { - // dbg!("adding new menu"); - new_menu_root = Some((new_index, ms.clone())); - } - if should_add { - let v = super::menu_bar::get_mut_or_default(&mut state.menu_states, menu.depth); - v.push(ms); - } + + new_menu_root = Some((new_index, ms.clone())); + state.menu_states.truncate(menu.depth + 1); + state.menu_states.push(ms); + } else if remove { + state.menu_states.truncate(menu.depth + 1); } - if remove { - state.menu_states.pop(); - } - + (new_menu_root, Captured) }) } @@ -1801,7 +1534,6 @@ where use mouse::ScrollDelta; menu.tree.inner.with_data_mut(|state| { - dbg!(state.active_root.len()); let delta_y = match delta { ScrollDelta::Lines { y, .. } => y * 60.0, @@ -1817,13 +1549,12 @@ where (viewport_size.height - (children_bounds.y + children_bounds.height)).min(0.0); (max_offset, min_offset) }; - let menu_states = state.menu_states.get_mut(0).unwrap(); // update - if menu_states.is_empty() { + if state.menu_states.is_empty() { return Ignored; - } else if menu_states.len() == 1 { - let last_ms = &mut menu_states[0]; + } else if state.menu_states.len() == 1 { + let last_ms = &mut state.menu_states[0]; if last_ms.index.is_none() { return Captured; @@ -1833,8 +1564,8 @@ where last_ms.scroll_offset = (last_ms.scroll_offset + delta_y).clamp(min_offset, max_offset); } else { // >= 2 - let max_index = menu_states.len() - 1; - let last_two = &mut menu_states[max_index - 1..=max_index]; + 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 @@ -1927,7 +1658,7 @@ fn get_children_layout( .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 82ed6db8..3a875fa1 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -252,7 +252,7 @@ pub fn menu_items< } // dbg!("button with action...", action.message()); - let menu_button = menu_button(items).on_press(action.message()).description(l); + let menu_button = menu_button(items).on_press(action.message()); trees.push(MenuTree::::from(Element::from(menu_button))); } @@ -272,7 +272,7 @@ pub fn menu_items< items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); } - let menu_button = menu_button(items).description((l.clone())); + let menu_button = menu_button(items); trees.push(MenuTree::::from(Element::from(menu_button))); } @@ -321,7 +321,6 @@ pub fn menu_items< .icon() .into(), ]) - .description(l.clone()) .class( // Menu folders have no on_press so they take on the disabled style by default if children.is_empty() { diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index af9d7f76..41b4e00a 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -86,7 +86,7 @@ impl ResponsiveMenuBar { crate::widget::RcElementWrapper::new(Element::from( menu::root(mt.0), )), - menu::items(key_binds, mt.1.into()), + menu::items(key_binds, mt.1), ) }) .collect(), From 3d84fb2a8992289036687293b7528d4c9c6813df Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 10 Jun 2025 11:05:29 -0400 Subject: [PATCH 12/12] cleanup --- examples/application/src/main.rs | 7 +- examples/calendar/src/main.rs | 1 + examples/context-menu/Cargo.toml | 10 +- examples/context-menu/src/main.rs | 2 +- examples/menu/src/main.rs | 3 +- examples/table-view/src/main.rs | 4 +- src/widget/context_menu.rs | 33 ++-- src/widget/menu/menu_bar.rs | 42 +++-- src/widget/menu/menu_inner.rs | 235 +++++++++++++++------------- src/widget/menu/menu_tree.rs | 3 +- src/widget/responsive_menu_bar.rs | 7 +- src/widget/table/mod.rs | 12 +- src/widget/table/widget/compact.rs | 9 +- src/widget/table/widget/standard.rs | 12 +- 14 files changed, 203 insertions(+), 177 deletions(-) diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index c1aba67f..c70a9d30 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -177,7 +177,6 @@ impl cosmic::Application for App { self.hidden = !self.hidden; } Message::Surface(a) => { - dbg!("got action..."); return cosmic::task::message(cosmic::Action::Cosmic( cosmic::app::Action::Surface(a), )); @@ -236,8 +235,6 @@ impl cosmic::Application for App { } fn header_start(&self) -> Vec> { - use cosmic::widget::menu::Tree; - vec![cosmic::widget::responsive_menu_bar().into_element( self.core(), &self.keybinds, @@ -300,6 +297,10 @@ impl cosmic::Application for App { 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), ], ), ], diff --git a/examples/calendar/src/main.rs b/examples/calendar/src/main.rs index c73c4da7..47549a70 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 5b9ad020..9a24a1c8 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 840cf865..4a307840 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 5b65732e..7037a62c 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 8b2d4f62..6bd773bc 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/src/widget/context_menu.rs b/src/widget/context_menu.rs index dc46da01..6769dff2 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -4,7 +4,7 @@ //! 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; @@ -13,8 +13,6 @@ use iced_core::widget::{Tree, Widget, tree}; use iced_core::{Length, Point, Size, event, mouse, touch}; use std::collections::HashSet; -use super::dropdown::menu::State; - /// 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( content: impl Into> + 'static, @@ -60,7 +58,7 @@ impl Widget tree::State::new(LocalState { context_cursor: Point::default(), fingers_pressed: Default::default(), - menu_state: Default::default(), + menu_bar_state: Default::default(), }) } @@ -72,7 +70,6 @@ impl Widget // 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| { @@ -95,6 +92,10 @@ impl Widget 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 @@ -188,9 +189,9 @@ impl Widget && (right_button_released(&event) || (touch_lifted(&event) && fingers_pressed == 2)) { state.context_cursor = cursor.position().unwrap_or_default(); - let menu_state = tree.children[1].state.downcast_mut::(); + let state = tree.state.downcast_mut::(); - menu_state.inner.with_data_mut(|state| { + state.menu_bar_state.inner.with_data_mut(|state| { state.open = true; state.view_cursor = cursor; }); @@ -219,18 +220,10 @@ impl Widget translation: Vector, ) -> Option> { let state = tree.state.downcast_ref::(); - let menu_state = state.menu_state.clone(); - let Some(context_menu) = self.context_menu.as_mut() else { - return None; - }; + let context_menu = self.context_menu.as_mut()?; - if !tree.children[1] - .state - .downcast_ref::() - .inner - .with_data(|state| state.open) - { + if !state.menu_bar_state.inner.with_data(|state| state.open) { return None; } @@ -239,8 +232,8 @@ impl Widget bounds.y = state.context_cursor.y; Some( crate::widget::menu::Menu { - tree: menu_state, - menu_roots: std::borrow::Cow::Borrowed(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 { @@ -301,5 +294,5 @@ fn touch_lifted(event: &Event) -> bool { pub struct LocalState { context_cursor: Point, fingers_pressed: HashSet, - menu_state: MenuBarState, + menu_bar_state: MenuBarState, } diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 8658f29a..eddc4f3a 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -19,7 +19,7 @@ use crate::{ }, }; -use iced::{Point, Vector, window}; +use iced::{Point, Shadow, Vector, window}; use iced_core::Border; use iced_widget::core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, event, @@ -429,9 +429,9 @@ where layout.bounds(), self.main_offset as f32, ); - let anchor_rect = my_state.inner.with_data_mut(|state| { + let (anchor_rect, gravity) = my_state.inner.with_data_mut(|state| { state.popup_id.insert(self.window_id, id); - state + (state .menu_states .iter() .find(|s| s.index.is_none()) @@ -452,19 +452,28 @@ where 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:cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, - reactive: true, - ..Default::default() - }; + 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 { @@ -496,7 +505,7 @@ where let state = tree.state.downcast_mut::(); state .inner - .with_data_mut(|inner| menu_roots_diff(&mut self.menu_roots, &mut inner.tree)) + .with_data_mut(|inner| menu_roots_diff(&mut self.menu_roots, &mut inner.tree)); } fn tag(&self) -> tree::Tag { @@ -550,10 +559,9 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> event::Status { - use event::Event::{Mouse, Touch, Window}; + use event::Event::{Mouse, Touch}; use mouse::{Button::Left, Event::ButtonReleased}; use touch::Event::{FingerLifted, FingerLost}; - use window::Event::Focused; let root_status = process_root_events( &mut self.menu_roots, @@ -656,7 +664,7 @@ where // draw path highlight if self.path_highlight.is_some() { let styling = theme.appearance(&self.style); - if let Some(active) = state.active_root.get(0) { + if let Some(active) = state.active_root.first() { let active_bounds = layout .children() .nth(*active) @@ -668,7 +676,7 @@ where radius: styling.bar_border_radius.into(), ..Default::default() }, - shadow: Default::default(), + shadow: Shadow::default(), }; renderer.fill_quad(path_quad, styling.path); @@ -734,7 +742,7 @@ where } } -impl<'a, Message> From> for Element<'a, Message, crate::Theme, Renderer> +impl From> for Element<'_, Message, crate::Theme, Renderer> where Message: Clone + 'static, { diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 3b783af0..f410905e 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -322,15 +322,13 @@ impl MenuState { // 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[start_index..=end_index].iter()) - .enumerate() - .map(|(i, ((cp, size), mt))| { + .map(|((cp, size), mt)| { let mut position = *cp; let mut size = *size; @@ -343,7 +341,6 @@ impl MenuState { } let limits = Limits::new(size, size); - mt.item .layout(&mut tree[mt.index], renderer, &limits) @@ -351,8 +348,7 @@ impl MenuState { }) .collect::>(); - Node::with_children(children_bounds.size(), child_nodes) - .move_to(children_bounds.position()) + Node::with_children(children_bounds.size(), child_nodes).move_to(children_bounds.position()) } fn layout_single( @@ -468,12 +464,18 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { 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(Size::ZERO); + 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 data.tree.children; - let children = (if self.is_overlay {0} else {self.depth}..=self.depth) + 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![]); @@ -491,7 +493,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { }, ); - data.menu_states[if self.is_overlay {0} else {self.depth}..=self.depth].iter() + 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)| { @@ -509,6 +511,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { 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 @@ -556,9 +559,10 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .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; @@ -634,7 +638,11 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .distance(view_cursor.position().unwrap_or_default()) < 2.0 { - let is_inside = state.menu_states[..=self.depth] + 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; @@ -648,6 +656,11 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { 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; @@ -658,7 +671,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { if depth == 0 { break; } - root = parent.0.clone(); + root = *parent.0; depth = depth.saturating_sub(1); } shell @@ -698,7 +711,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { if !state.open || state.active_root.len() <= self.depth { return; } - let active_root = &state.active_root[..=self.depth]; + 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(); @@ -710,18 +723,16 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { }; let styling = theme.appearance(&self.style); - let (active_tree, roots) = active_root - .iter() - .skip(1) - .fold( - ( - &state.tree.children[active_root[0]].children, - &self.menu_roots[active_root[0]].children, - ), - |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), - ); + 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}..=self.depth] + 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() @@ -729,7 +740,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .fold( roots, |menu_roots: &Vec>, (i, (ms, children_layout))| { - let draw_path = self.path_highlight.as_ref().map_or(false, |ph| match ph { + 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 @@ -738,7 +749,9 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { }); // react only to the last menu - if self.depth == state.active_root.len() - 1 { + 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()) @@ -796,7 +809,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .zip(children_layout.children()) .for_each(|(mt, clo)| { mt.item.draw( - &active_tree[mt.index], + &state.tree.children[active_root[0]].children[mt.index], r, theme, style, @@ -858,8 +871,8 @@ impl overlay::Overlay Widget - for Menu<'a, Message> +impl Widget + for Menu<'_, Message> { fn size(&self) -> Size { Size { @@ -923,14 +936,7 @@ impl<'a, Message: std::clone::Clone + 'static> Widget Widget Widget Widget 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( @@ -1013,7 +1025,7 @@ impl<'a, Message: std::clone::Clone + 'static> Widget Rectangle { } } +#[allow(clippy::too_many_arguments)] pub(super) fn init_root_menu( menu: &mut Menu<'_, Message>, renderer: &crate::Renderer, @@ -1074,10 +1087,8 @@ pub(super) fn init_root_menu( main_offset: f32, ) { menu.tree.inner.with_data_mut(|state| { - if !(state - .menu_states - .get(menu.depth) - .is_none() + + if !(state.menu_states.get(menu.depth).is_none() && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) || menu.depth > 0 || !state.open @@ -1130,8 +1141,7 @@ pub(super) fn init_root_menu( menu.is_overlay, ); set = true; - - state.active_root.insert(menu.depth, i); + state.active_root.push(i); let ms = MenuState { index: None, scroll_offset: 0.0, @@ -1145,9 +1155,7 @@ pub(super) fn init_root_menu( break; } } - if !set { - panic!("huh"); - } + debug_assert!(set, "Root not set"); }); } @@ -1165,20 +1173,17 @@ pub(super) fn init_root_popup_menu( Message: std::clone::Clone, { menu.tree.inner.with_data_mut(|state| { - if !(state - .menu_states - .get(menu.depth) - .is_none() + if !(state.menu_states.get(menu.depth).is_none() && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) { return; } - let active_roots = &state - .active_root[..=menu.depth]; + let active_roots = &state.active_root[..=menu.depth]; let mut set = false; - let mt = active_roots.iter() + let mt = active_roots + .iter() .skip(1) .fold(&menu.menu_roots[active_roots[0]], |mt, next_active_root| { &mt.children[*next_active_root] @@ -1232,7 +1237,7 @@ pub(super) fn init_root_popup_menu( // Hack to ensure menu opens properly shell.invalidate_layout(); // non tree buttons arent active? - assert!(set, "oops"); + debug_assert!(set, "Root popup menu state was not set."); }); } @@ -1264,23 +1269,16 @@ fn process_menu_events( let Some(hover_index) = hover.index else { return Status::Ignored; - }; + }; - let (tree, mt) = state.active_root - .iter() - .skip(1) - .fold( - // then use menu states for each open menu - ( - &mut state.tree.children[state.active_root[0]].children, - &mut menu_roots[state.active_root[0]], - ), - |(tree, mt), next_active_root| (tree, &mut mt.children[*next_active_root]), - ); - + 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], + ); let mt = &mut mt.children[hover_index]; - let tree = &mut tree[mt.index]; + let tree = &mut state.tree.children[state.active_root[0]].children[mt.index]; // get layout let child_node = hover.layout_single( @@ -1333,18 +1331,14 @@ where let mut new_menu_root = None; menu.tree.inner.with_data_mut(|state| { - let active_root = &state.active_root[..=menu.depth]; - - if state.pressed { - return (new_menu_root, 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; // * remove invalid menus - + let mut prev_bounds = std::iter::once(menu.bar_bounds) .chain( if menu.is_overlay { @@ -1355,7 +1349,7 @@ where .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; @@ -1369,22 +1363,37 @@ where 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(); } } // * update active item let menu_states_len = state.menu_states.len(); + let Some(last_menu_state) = state.menu_states.get_mut(if menu.is_overlay { - menu_states_len - 1 + 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); + // state.active_root.remove(menu.depth); // keep state.open when the cursor is still inside the menu bar // this allows the overlay to keep drawing when the cursor is @@ -1406,31 +1415,32 @@ where || menu.is_overlay && !last_children_bounds.contains(overlay_cursor) // cursor is outside { + last_menu_state.index = None; return (new_menu_root, Captured); } - // cursor is in the children part - - // TODO set active root here even when not a tree. - // ensure that the // 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_tree, roots) = active_root - .iter() - .skip(1) - .fold( - ( - &mut state.tree.children[active_root[0]].children, - &menu.menu_roots[active_root[0]].children, - ), - |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), - ); + let active_root = if menu.is_overlay { + &state.active_root + } else { + &state.active_root[..=menu.depth] + }; - let active_menu = roots; + 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(_) => { @@ -1446,12 +1456,10 @@ where } }; - - let remove = !menu.is_overlay - && last_menu_state - .index - .as_ref() - .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); + 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"))] { @@ -1465,9 +1473,9 @@ where } } 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) { @@ -1504,18 +1512,22 @@ where &aod, menu.bounds_expand, item_bounds, - active_tree, + tree, menu.is_overlay, ), }; - + new_menu_root = Some((new_index, ms.clone())); - state.menu_states.truncate(menu.depth + 1); + 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 remove { + } else if !menu.is_overlay && remove { state.menu_states.truncate(menu.depth + 1); } - + (new_menu_root, Captured) }) } @@ -1534,7 +1546,6 @@ where use mouse::ScrollDelta; menu.tree.inner.with_data_mut(|state| { - let delta_y = match delta { ScrollDelta::Lines { y, .. } => y * 60.0, ScrollDelta::Pixels { y, .. } => y, diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 3a875fa1..d02b2b27 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -210,10 +210,11 @@ where /// /// # Returns /// - A vector of `MenuTree`. +#[must_use] pub fn menu_items< A: MenuAction, L: Into> + 'static, - Message: 'static + std::clone::Clone + std::fmt::Debug, + Message: 'static + std::clone::Clone, >( key_binds: &HashMap, children: Vec>, diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index 41b4e00a..3d9557d0 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use apply::Apply; -use iced::window; use crate::{ Core, Element, @@ -10,6 +9,7 @@ use crate::{ use super::menu::{self, ItemHeight, ItemWidth}; +#[must_use] pub fn responsive_menu_bar() -> ResponsiveMenuBar { ResponsiveMenuBar::default() } @@ -34,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,7 +59,7 @@ impl ResponsiveMenuBar { /// Will panic if the menu bar collapses without tracking the size pub fn into_element< 'a, - Message: std::fmt::Debug + Clone + 'static, + Message: Clone + 'static, A: menu::Action + Clone, S: Into> + 'static, >( diff --git a/src/widget/table/mod.rs b/src/widget/table/mod.rs index 7063dc8e..c546383c 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 2d060ca2..47864f6d 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -47,9 +47,8 @@ where pub(super) item_context_builder: Box Option>>>, } -impl - From> - for Element<'static, Message> +impl<'a, SelectionMode, Item, Category, Message> + From> for Element<'a, Message> where Category: ItemCategory, Item: ItemInterface, @@ -57,7 +56,7 @@ where SelectionMode: Default, Message: Clone + 'static, { - fn from(val: CompactTableView<'static, SelectionMode, Item, Category, Message>) -> Self { + fn from(val: CompactTableView<'a, SelectionMode, Item, Category, Message>) -> Self { let cosmic_theme::Spacing { space_xxxs, .. } = theme::spacing(); val.model .iter() @@ -172,7 +171,7 @@ where ) .apply(Element::from) }) - .collect::>>() + .collect::>>() .apply(widget::column::with_children) .spacing(val.item_spacing) .padding(val.element_padding) diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index 112d673f..eb9ba7a4 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -67,8 +67,8 @@ where pub(super) category_context_builder: Box Option>>>, } -impl - From> for Element<'static, Message> +impl<'a, SelectionMode, Item, Category, Message> + From> for Element<'a, Message> where Category: ItemCategory, Item: ItemInterface, @@ -76,7 +76,7 @@ where SelectionMode: Default, Message: Clone + 'static, { - fn from(val: TableView<'static, SelectionMode, Item, Category, Message>) -> Self { + fn from(val: TableView<'a, SelectionMode, Item, Category, Message>) -> Self { // Header row let header_row = val .model @@ -125,7 +125,7 @@ where .apply(|mouse_area| widget::context_menu(mouse_area, cat_context_tree)) .apply(Element::from) }) - .collect::>>() + .collect::>>() .apply(widget::row::with_children) .apply(Element::from); // Build the items @@ -234,12 +234,12 @@ where ] }) .flatten() - .collect::>>() + .collect::>>() }; vec![vec![header_row], items_full] .into_iter() .flatten() - .collect::>>() + .collect::>>() .apply(widget::column::with_children) .width(val.width) .height(val.height)