From 51c391a6560724fad84e39a87f303861e9a2f8cd Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 5 Jun 2025 12:10:07 -0400 Subject: [PATCH] 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) })