From 0e52ee472b5beedfabf58b5ef265f20fb56d066b Mon Sep 17 00:00:00 2001 From: junzhuo Date: Sun, 26 Apr 2026 22:30:42 +0800 Subject: [PATCH] Add safe triangle behavior for nested menus --- src/widget/menu/common.rs | 50 +++++++++++++++++++++++++++++ src/widget/menu/menu_tree.rs | 62 ++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/src/widget/menu/common.rs b/src/widget/menu/common.rs index b1ddb5e5..ad354ced 100644 --- a/src/widget/menu/common.rs +++ b/src/widget/menu/common.rs @@ -319,3 +319,53 @@ macro_rules! itl_iter_slice_enum { }; } pub(super) use itl_iter_slice_enum; + +#[derive(Debug, Clone, Copy)] +pub(super) struct SafeTriangle { + pub(super) p1: iced_core::Point, + pub(super) p2: iced_core::Point, + pub(super) p3: iced_core::Point, +} + +impl SafeTriangle { + pub(super) fn new( + p1: iced_core::Point, + child_bounds: Rectangle, + direction: (Direction, Direction), + ) -> Self { + let (child_corner1, child_corner2) = match direction.0 { + Direction::Positive => ( + iced_core::Point::new(child_bounds.x, child_bounds.y), + iced_core::Point::new(child_bounds.x, child_bounds.y + child_bounds.height), + ), + Direction::Negative => ( + iced_core::Point::new(child_bounds.x + child_bounds.width, child_bounds.y), + iced_core::Point::new( + child_bounds.x + child_bounds.width, + child_bounds.y + child_bounds.height, + ), + ), + }; + + Self { + p1, + p2: child_corner1, + p3: child_corner2, + } + } + + pub(super) fn contains(&self, point: iced_core::Point) -> bool { + let sign = |p1: iced_core::Point, p2: iced_core::Point, p3: iced_core::Point| -> f32 { + (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y) + }; + + let d1 = sign(point, self.p1, self.p2); + let d2 = sign(point, self.p2, self.p3); + let d3 = sign(point, self.p3, self.p1); + + let has_neg = (d1 < 0.0) || (d2 < 0.0) || (d3 < 0.0); + let has_pos = (d1 > 0.0) || (d2 > 0.0) || (d3 > 0.0); + + !(has_neg && has_pos) + } +} diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 67aebe4b..d0fb07c6 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -66,6 +66,8 @@ pub(super) struct MenuState { pub(super) scroll_offset: f32, pub(super) active: Index, pub(super) slice: MenuSlice, + pub(super) safe_triangle: Option, + pub(super) last_cursor_on_parent: Option, } impl MenuState { /// item_tree: Tree{item state, [Tree{widget state}, Tree{menu state, [...]}]} @@ -117,6 +119,8 @@ impl Default for MenuState { lower_bound_rel: 0.0, upper_bound_rel: f32::MAX, }, + safe_triangle: None, + last_cursor_on_parent: None, } } } @@ -418,6 +422,46 @@ where Padding::new(global_parameters.safe_bounds_margin), ); + { + let menu_state = tree.state.downcast_mut::(); + let parent_direction = { + let hcenter = viewport.width / 2.0; + let vcenter = viewport.height / 2.0; + let phcenter = parent_bounds.x + parent_bounds.width / 2.0; + let pvcenter = parent_bounds.y + parent_bounds.height / 2.0; + ( + if phcenter < hcenter { + Direction::Positive + } else { + Direction::Negative + }, + if pvcenter < vcenter { + Direction::Positive + } else { + Direction::Negative + }, + ) + }; + + if cursor.is_over(parent_bounds) { + if let Some(pos) = cursor.position() { + menu_state.last_cursor_on_parent = Some(pos); + } + } + + let p1 = menu_state + .last_cursor_on_parent + .unwrap_or_else(|| parent_bounds.center()); + + let triangle = SafeTriangle::new(p1, background_bounds, parent_direction); + + #[cfg(feature = "debug_log")] + debug!(target:"menu::Menu::update", "SafeTriangle created: p1={:?}, p2={:?}, p3={:?}", + triangle.p1, triangle.p2, triangle.p3); + + menu_state.safe_triangle = Some(triangle); + } + enum Op { UpdateItems, OpenEvent, @@ -595,9 +639,27 @@ where assert!(!shell.is_event_captured(), "Returning RecEvent::None"); RecEvent::None } else { + let menu_state = tree.state.downcast_ref::(); + let in_safe_triangle = if let (Some(cursor_pos), Some(triangle)) = + (cursor.position(), menu_state.safe_triangle) + { + let result = triangle.contains(cursor_pos); + #[cfg(feature = "debug_log")] + debug!(target:"menu::Menu::update", "Cursor at {:?}, in_safe_triangle: {}", cursor_pos, result); + result + } else { + #[cfg(feature = "debug_log")] + debug!(target:"menu::Menu::update", "No cursor position or no safe triangle"); + false + }; + let open = { if global_state.pressed { true + } else if in_safe_triangle { + #[cfg(feature = "debug_log")] + debug!(target:"menu::Menu::update", "Keeping menu open due to safe triangle"); + true } else if prev_bounds_list.iter().any(|r| cursor.is_over(*r)) { false } else {