refactor(editor): migrate path anchor menu to shared menu

This commit is contained in:
Kayshen-X 2026-06-14 00:56:42 +08:00
parent 5f9c6ec63b
commit b64a928bee
3 changed files with 62 additions and 19 deletions

View file

@ -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<u8>,
/// 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.

View file

@ -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<PathAnchorMenuAction> {
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);
}
}

View file

@ -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;
}