fix(editor): new/opened documents open with nothing selected
A fresh starter canvas and a just-opened .op no longer ship the root frame pre-selected, which both reads cleaner and stops a stray selection from mis-routing a whole-screen design request into modify.
This commit is contained in:
parent
92977e08dd
commit
2090b5a8eb
|
|
@ -66,8 +66,7 @@ impl EditorState {
|
|||
|
||||
/// Build the document a fresh launch opens with — a single empty
|
||||
/// starter Frame `n10` matching the TypeScript app's blank
|
||||
/// document geometry, selected so the user can immediately resize
|
||||
/// / move it or drop nodes inside. No demo decoration.
|
||||
/// document geometry. No default selection and no demo decoration.
|
||||
pub fn starter() -> Self {
|
||||
let src = r##"{
|
||||
"version": "0.8.0",
|
||||
|
|
@ -81,9 +80,7 @@ impl EditorState {
|
|||
let doc = jian_ops_schema::load_str(src)
|
||||
.expect("EditorState::starter() fixture parses")
|
||||
.value;
|
||||
let mut state = Self::from_document(doc);
|
||||
state.set_single_selection(NodeId::new("n10"));
|
||||
state
|
||||
Self::from_document(doc)
|
||||
}
|
||||
|
||||
/// Spawn a fresh leaf node for the active shape / frame / text /
|
||||
|
|
@ -625,14 +622,14 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn starter_is_a_single_empty_frame_with_it_selected() {
|
||||
fn starter_is_a_single_empty_frame_with_nothing_selected() {
|
||||
let s = EditorState::starter();
|
||||
// Exactly one top-level node — the starter Frame. The
|
||||
// `max_node_id() == 10` check proves no demo children:
|
||||
// the n11..n14 sample tree would lift it to 14.
|
||||
assert_eq!(s.doc.children.len(), 1);
|
||||
assert_eq!(s.max_node_id(), 10);
|
||||
assert_eq!(s.selection.anchor, NodeId::new("n10"));
|
||||
assert!(s.selection.is_empty());
|
||||
let frame = match &s.doc.children[0] {
|
||||
jian_ops_schema::node::PenNode::Frame(frame) => frame,
|
||||
other => panic!("starter should be a frame, got {:?}", other),
|
||||
|
|
|
|||
|
|
@ -177,7 +177,8 @@ fn first_layer_lock_top_left(panel: &LayerPanel, rect: Rect) -> Point2D {
|
|||
|
||||
#[test]
|
||||
fn selected_visible_unlocked_layer_does_not_expose_trailing_actions_without_hover() {
|
||||
let state = EditorState::starter();
|
||||
let mut state = EditorState::starter();
|
||||
state.set_single_selection(NodeId::new("n10"));
|
||||
let panel = LayerPanel::from_editor(&state);
|
||||
assert!(panel.items[0].selected);
|
||||
assert!(!panel.items[0].hovered);
|
||||
|
|
|
|||
|
|
@ -236,11 +236,11 @@ pub fn launch_if_pending(
|
|||
fn should_launch_direct_modify(state: &EditorState, user_text: &str) -> bool {
|
||||
// A pristine "from-scratch" canvas holds only the blank starter frame:
|
||||
// there is nothing real to modify, so ANY design request on it is a NEW
|
||||
// design, never a modify — even when the starter frame is selected and the
|
||||
// prompt is a bare noun phrase the new-screen gates don't recognize
|
||||
// (measured: "Luxury webapp for managing barbershop clients" on a fresh
|
||||
// canvas fell into run_modify_turn → glm flat-nodes → empty `"`). This is
|
||||
// the desktop parity of web_chat_standard's `page_children_empty => New`.
|
||||
// design, never a modify — even when the prompt is a bare noun phrase the
|
||||
// new-screen gates don't recognize (measured: "Luxury webapp for managing
|
||||
// barbershop clients" on a fresh canvas fell into run_modify_turn → glm
|
||||
// flat-nodes → empty `"`). This is the desktop parity of
|
||||
// web_chat_standard's `page_children_empty => New`.
|
||||
if active_page_is_blank_starter_frame(state) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,10 +51,10 @@ fn state_with_selected_header() -> EditorState {
|
|||
fn blank_starter_canvas_never_launches_modify() {
|
||||
let state = EditorState::starter();
|
||||
|
||||
assert!(!state.selection.is_empty(), "starter frame is selected");
|
||||
assert!(state.selection.is_empty(), "fresh starter has no selection");
|
||||
assert!(
|
||||
!should_launch_direct_modify(&state, "Luxury webapp for managing barbershop clients"),
|
||||
"fresh blank starter canvas has no real target to modify"
|
||||
"fresh blank starter canvas has no selected target or real content to modify"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,14 +90,14 @@ fn selected_count_chip_clear_click_clears_canvas_selection() {
|
|||
#[test]
|
||||
fn fresh_app_fits_blank_frame_like_ts_canvas_init() {
|
||||
let app = DesktopApp::new(None);
|
||||
assert!(app.host.editor_state().selection.is_empty());
|
||||
let v = app.host.editor_state().viewport;
|
||||
|
||||
// Golden fit values track `property_panel_width` (the right rail is
|
||||
// shown on the fresh app, so the canvas region = 1440 − panel). At
|
||||
// the TS-matching `w-64` (256 px) panel the blank frame fits at 0.68.
|
||||
assert!((v.zoom - 0.68).abs() < 1e-3, "zoom {}", v.zoom);
|
||||
// With no fresh selection, the right rail is hidden and the
|
||||
// blank frame fits in the wider canvas region.
|
||||
assert!((v.zoom - 0.8933333).abs() < 1e-3, "zoom {}", v.zoom);
|
||||
assert!((v.pan_x - 64.0).abs() < 1e-2, "pan_x {}", v.pan_x);
|
||||
assert!((v.pan_y - 158.0).abs() < 1e-2, "pan_y {}", v.pan_y);
|
||||
assert!((v.pan_y - 72.66669).abs() < 1e-2, "pan_y {}", v.pan_y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -108,9 +108,9 @@ fn fresh_app_refits_blank_frame_to_actual_window_size_once() {
|
|||
|
||||
assert!(app.fit_initial_blank_frame_to_actual_viewport());
|
||||
let v = app.host.editor_state().viewport;
|
||||
assert!((v.zoom - 0.31333333).abs() < 1e-3, "zoom {}", v.zoom);
|
||||
assert!((v.zoom - 0.52666664).abs() < 1e-3, "zoom {}", v.zoom);
|
||||
assert!((v.pan_x - 64.0).abs() < 1e-2, "pan_x {}", v.pan_x);
|
||||
assert!((v.pan_y - 204.66666).abs() < 1e-2, "pan_y {}", v.pan_y);
|
||||
assert!((v.pan_y - 119.33334).abs() < 1e-2, "pan_y {}", v.pan_y);
|
||||
|
||||
app.viewport_width = 1200.0;
|
||||
app.viewport_height = 800.0;
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ fn load_into_host(host: &mut WidgetHostNative, path: &std::path::Path) -> Result
|
|||
let mut state = load_editor_state(path, locale)?;
|
||||
preserve_app_preferences(host.editor_state(), &mut state);
|
||||
set_file_name_display(&mut state, Some(path));
|
||||
state.clear_selection();
|
||||
let bb = active_page_bbox(&state);
|
||||
eprintln!(
|
||||
"[open] {} top-level nodes; content bbox {:?}",
|
||||
|
|
@ -440,10 +441,7 @@ mod tests {
|
|||
assert_eq!(outcome, ActionOutcome::Saved);
|
||||
assert!(current_path.is_none());
|
||||
assert_eq!(host.editor_state().doc.children.len(), 1);
|
||||
assert_eq!(
|
||||
host.editor_state().selection.anchor,
|
||||
op_editor_core::NodeId::new("n10")
|
||||
);
|
||||
assert!(host.editor_state().selection.is_empty());
|
||||
let frame = match &host.editor_state().doc.children[0] {
|
||||
jian_ops_schema::node::PenNode::Frame(frame) => frame,
|
||||
other => panic!(
|
||||
|
|
@ -462,9 +460,9 @@ mod tests {
|
|||
Some(jian_ops_schema::sizing::SizingBehavior::Number(800.0))
|
||||
));
|
||||
let v = host.editor_state().viewport;
|
||||
assert!((v.zoom - 0.68).abs() < 1e-3, "zoom {}", v.zoom);
|
||||
assert!((v.zoom - 0.8933333).abs() < 1e-3, "zoom {}", v.zoom);
|
||||
assert!((v.pan_x - 64.0).abs() < 1e-2, "pan_x {}", v.pan_x);
|
||||
assert!((v.pan_y - 158.0).abs() < 1e-2, "pan_y {}", v.pan_y);
|
||||
assert!((v.pan_y - 72.66669).abs() < 1e-2, "pan_y {}", v.pan_y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -555,4 +553,25 @@ mod tests {
|
|||
let _ = std::fs::remove_file(&path);
|
||||
let _ = std::fs::remove_file(sidecar_path(&path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_document_leaves_nothing_selected() {
|
||||
let mut host = WidgetHostNative::new();
|
||||
host.editor_state_mut()
|
||||
.set_single_selection(op_editor_core::NodeId::new("n10"));
|
||||
|
||||
let mut state_to_open = EditorState::starter();
|
||||
state_to_open.set_single_selection(op_editor_core::NodeId::new("n10"));
|
||||
let path = temp_op_path("open-clears-selection");
|
||||
save_to_path(&state_to_open, &path).expect("save succeeds");
|
||||
let mut current_path = None;
|
||||
|
||||
assert!(open_path(&mut host, path.clone(), &mut current_path, None));
|
||||
|
||||
assert!(host.editor_state().selection.is_empty());
|
||||
assert_eq!(host.editor_state().doc.children.len(), 1);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let _ = std::fs::remove_file(sidecar_path(&path));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -414,11 +414,11 @@ fn incremental_drag_then_doc_restored_to_cached_value_rebuilds_the_scene() {
|
|||
|
||||
#[test]
|
||||
fn host_carries_editor_state_as_source_of_truth() {
|
||||
// A fresh host opens with the demo sample seeded onto
|
||||
// A fresh host opens with the blank starter document seeded onto
|
||||
// `EditorState` — the host's single source of truth.
|
||||
let host = WidgetHostNative::new();
|
||||
assert!(!host.editor_state().doc.children.is_empty());
|
||||
assert!(!host.editor_state().selection.is_empty());
|
||||
assert!(host.editor_state().selection.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -93,8 +93,10 @@ fn ime_commit_without_any_focused_input_is_a_no_op() {
|
|||
#[test]
|
||||
fn paste_text_routes_to_focused_rename() {
|
||||
let mut host = WidgetHost::new();
|
||||
// The starter document selects the blank starter frame; begin an
|
||||
// inline rename on it like a layer-row double-click would.
|
||||
// Begin an inline rename on the blank starter frame like a
|
||||
// layer-row double-click would.
|
||||
host.editor_state
|
||||
.set_single_selection(op_editor_core::NodeId::new("n10"));
|
||||
let id = host.editor_state.selection.anchor.clone();
|
||||
assert!(host.editor_state.start_rename_layer(id));
|
||||
// Select-all so the paste replaces the seeded name deterministically.
|
||||
|
|
|
|||
Loading…
Reference in a new issue