fix(editor): don't drift locked/hidden nodes in the scene on native node drag

The incremental scene patch built its id list with only the flex-flow filter,
but translate_selected also skips non-editable (locked / hidden) nodes. A
selection containing a locked node would translate it in the scene mid-drag,
then snap it back on the release-time reconversion. Apply the same is_editable
filter the document mutator uses (parity with the web host fix). +regression
test.
This commit is contained in:
Kayshen-X 2026-06-19 15:30:47 +08:00
parent 10daa64ffa
commit f4eec05ece
2 changed files with 55 additions and 1 deletions

View file

@ -228,7 +228,15 @@ impl WidgetHostNative {
.selection
.set
.iter()
.filter(|id| !op_editor_core::walkers::is_flow_child_of_flex(children, id))
.filter(|id| {
// Move exactly what `translate_selected` moved in the
// document: editable nodes only (locked / hidden are
// skipped there) and not flex-flow children. Otherwise
// the scene drifts nodes the doc never moved, then snaps
// back on the release-time reconversion.
self.editor_state.is_editable(id)
&& !op_editor_core::walkers::is_flow_child_of_flex(children, id)
})
.map(|id| id.as_str().to_string())
.collect();
let _ = self

View file

@ -318,6 +318,52 @@ fn node_drag_keeps_flex_child_in_layout_flow() {
);
}
#[test]
fn dragging_a_selection_with_a_locked_node_does_not_drift_it_in_the_scene() {
// Parity with the web host: the incremental scene patch must move exactly
// what `translate_selected` moved — editable nodes only. A selected locked
// node must not drift in the scene (it would otherwise jump during the drag
// and snap back on the release-time reconversion).
let mut host = WidgetHostNative::new();
seed(
&mut host,
r#"{"version":"0.8.0","children":[
{"type":"rectangle","id":"free","name":"free","x":100,"y":100,"width":80,"height":60},
{"type":"rectangle","id":"locked","name":"locked","x":600,"y":100,"width":80,"height":60,"locked":true}
]}"#,
);
host.editor_state_mut().selection.set = vec![NodeId::new("free"), NodeId::new("locked")];
host.mark_paint_dirty_for_test();
let _ = host.layout_scene();
assert!(!host.editor_state_dirty);
let free_before = scene_node_xy(&host, "free");
let locked_before = scene_node_xy(&host, "locked");
host.node_drag = Some(NodeDragState {
last_screen_x: 500.0,
last_screen_y: 500.0,
press_screen_x: 500.0,
press_screen_y: 500.0,
moved: true,
total_dx: 0.0,
total_dy: 0.0,
});
assert!(host.apply_cursor_move(540.0, 500.0));
let free_after = scene_node_xy(&host, "free");
let locked_after = scene_node_xy(&host, "locked");
assert!(
(free_after.0 - free_before.0).abs() > 1.0,
"editable node should move in the scene during the drag"
);
assert_eq!(
locked_after, locked_before,
"locked node must not drift in the scene"
);
}
#[test]
fn host_carries_editor_state_as_source_of_truth() {
// A fresh host opens with the demo sample seeded onto