From b64a928beea400b61bf51322be7d398b9a2ef9c7 Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Sun, 14 Jun 2026 00:56:42 +0800 Subject: [PATCH] refactor(editor): migrate path anchor menu to shared menu --- crates/op-editor-core/src/ui_draft.rs | 4 +- .../src/widgets/path_anchor_context_menu.rs | 47 +++++++++++++++++-- .../src/widget_host/pen_press.rs | 30 +++++++----- 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/crates/op-editor-core/src/ui_draft.rs b/crates/op-editor-core/src/ui_draft.rs index e28c457cd..6ffb2fc18 100644 --- a/crates/op-editor-core/src/ui_draft.rs +++ b/crates/op-editor-core/src/ui_draft.rs @@ -151,8 +151,8 @@ pub struct PathAnchorMenuState { /// Menu anchor in viewport coords (the right-click position). pub x: f32, pub y: f32, - /// Hovered row index for the menu paint; `None` = no row hovered. - pub hovered_row: Option, + /// Shared menu interaction state; `hover = None` means no row hovered. + pub menu: jian_widgets::components::menu::MenuState, } /// Inline rename in progress on a layer or page row. diff --git a/crates/op-editor-ui/src/widgets/path_anchor_context_menu.rs b/crates/op-editor-ui/src/widgets/path_anchor_context_menu.rs index 2a47dad4e..6c99f7df4 100644 --- a/crates/op-editor-ui/src/widgets/path_anchor_context_menu.rs +++ b/crates/op-editor-ui/src/widgets/path_anchor_context_menu.rs @@ -13,6 +13,7 @@ use crate::theme::Theme; use crate::widgets::editor_state_ext::theme_for; use crate::widgets::PaintCx; use crate::{Point2D, Rect, TextLayout}; +pub use jian_widgets::components::menu::MenuHit; use op_editor_core::{EditorState, PathAnchorMenuState}; pub const PATH_ANCHOR_MENU_WIDTH: f32 = 160.0; @@ -76,10 +77,34 @@ impl PathAnchorContextMenu { (idx < ROWS.len()).then_some(idx) } - /// The action under `point`; `None` when outside the menu - /// (caller closes it and lets the press route on — TS parity). + /// The action under `point`; `None` when the hit is not an + /// actionable row. Callers use `hit` to distinguish inside blank + /// menu space from an outside dismiss. pub fn hit_test(&self, point: Point2D) -> Option { - self.hovered_row_at(point).map(|i| ROWS[i].0) + match self.hit(point) { + MenuHit::Row(idx) => Some(ROWS[idx].0), + MenuHit::Inside | MenuHit::Outside => None, + } + } + + pub fn hit(&self, point: Point2D) -> MenuHit { + let rect = self.rect(); + if point.x < rect.origin.x + || point.x > rect.origin.x + rect.size.x + || point.y < rect.origin.y + || point.y > rect.origin.y + rect.size.y + { + return MenuHit::Outside; + } + if point.y < rect.origin.y + PAD_Y || point.y > rect.origin.y + rect.size.y - PAD_Y { + return MenuHit::Inside; + } + let idx = ((point.y - rect.origin.y - PAD_Y) / ROW_HEIGHT) as usize; + if idx < ROWS.len() { + MenuHit::Row(idx) + } else { + MenuHit::Inside + } } pub fn paint(&self, cx: &mut PaintCx<'_>) { @@ -89,7 +114,7 @@ impl PathAnchorContextMenu { .stroke_round_rect(rect, 6.0, self.theme.border, 1.0); let mut y = rect.origin.y + PAD_Y; for (i, (_, label)) in ROWS.iter().enumerate() { - if self.state.hovered_row == Some(i as u8) { + if self.state.menu.hover == Some(i) { let hover_rect = Rect { origin: Point2D::new(rect.origin.x + 4.0, y + 1.0), size: Point2D::new(rect.size.x - 8.0, ROW_HEIGHT - 2.0), @@ -132,7 +157,7 @@ mod tests { anchor_index: 0, x: 100.0, y: 50.0, - hovered_row: None, + menu: Default::default(), }, } } @@ -165,4 +190,16 @@ mod tests { assert_eq!(m.hit_test(Point2D::new(50.0, 60.0)), None); assert_eq!(m.hit_test(Point2D::new(110.0, 300.0)), None); } + + #[test] + fn hit_uses_shared_menu_state_protocol() { + let mut m = menu(); + m.state.menu.hover = Some(2); + assert_eq!(m.state.menu.hover, Some(2)); + + let row_y = 50.0 + PAD_Y + ROW_HEIGHT * 2.5; + assert_eq!(m.hit(Point2D::new(110.0, row_y)), MenuHit::Row(2)); + assert_eq!(m.hit(Point2D::new(110.0, 52.0)), MenuHit::Inside); + assert_eq!(m.hit(Point2D::new(50.0, 52.0)), MenuHit::Outside); + } } diff --git a/crates/op-host-native/src/widget_host/pen_press.rs b/crates/op-host-native/src/widget_host/pen_press.rs index 2cc519192..fc823ba85 100644 --- a/crates/op-host-native/src/widget_host/pen_press.rs +++ b/crates/op-host-native/src/widget_host/pen_press.rs @@ -28,7 +28,7 @@ use super::WidgetHostNative; use op_editor_ui::widgets::path_anchor_context_menu::{ - PathAnchorContextMenu, PathAnchorMenuAction, + MenuHit, PathAnchorContextMenu, PathAnchorMenuAction, }; use op_editor_ui::Point2D; @@ -383,7 +383,7 @@ impl WidgetHostNative { anchor_index, x, y, - hovered_row: None, + menu: Default::default(), }); self.mark_dirty(); return true; @@ -409,10 +409,17 @@ impl WidgetHostNative { return false; }; let menu = PathAnchorContextMenu::for_state(&self.editor_state, state.clone()); - let Some(action) = menu.hit_test(Point2D::new(x, y)) else { - self.editor_state.ui.path_anchor_menu = None; - self.mark_dirty(); - return false; + let action = match menu.hit(Point2D::new(x, y)) { + MenuHit::Row(_) => menu.hit_test(Point2D::new(x, y)), + MenuHit::Inside => return true, + MenuHit::Outside => { + self.editor_state.ui.path_anchor_menu = None; + self.mark_dirty(); + return false; + } + }; + let Some(action) = action else { + return true; }; use jian_ops_schema::node::PenPathPointType as P; let id = state.node_id.clone(); @@ -450,12 +457,11 @@ impl WidgetHostNative { return false; }; let menu = PathAnchorContextMenu::for_state(&self.editor_state, state.clone()); - let new_hover = menu.hovered_row_at(Point2D::new(x, y)).map(|i| i as u8); - if new_hover != state.hovered_row { - self.editor_state.ui.path_anchor_menu = Some(op_editor_core::PathAnchorMenuState { - hovered_row: new_hover, - ..state - }); + let new_hover = menu.hovered_row_at(Point2D::new(x, y)); + if new_hover != state.menu.hover { + let mut next = state; + next.menu.hover = new_hover; + self.editor_state.ui.path_anchor_menu = Some(next); self.mark_dirty(); return true; }