From d1093b7dc4028d150ea47d63b495dc722a2bf8cf Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Fri, 22 May 2026 11:40:08 +0800 Subject: [PATCH] feat(editor): open with a single empty starter frame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fresh launch seeded the demo `sample()` document — a Frame with a 'Hello OpenPencil' title and a 'Click me' button group. New `EditorState::starter()` returns just one empty Frame (selected), and both hosts open with it; `sample()` stays as the widget-test fixture. input_tests retarget their `n11` selections to the starter frame's `n10`. --- crates/op-editor-core/src/host_support.rs | 39 +++++++++++++++++-- crates/op-host-native/src/widget_host.rs | 4 +- .../src/widget_host/input_tests.rs | 8 ++-- crates/op-host-web/src/widget_host.rs | 4 +- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/crates/op-editor-core/src/host_support.rs b/crates/op-editor-core/src/host_support.rs index 5e312bba2..47e3cc562 100644 --- a/crates/op-editor-core/src/host_support.rs +++ b/crates/op-editor-core/src/host_support.rs @@ -21,9 +21,8 @@ impl EditorState { /// containing a Text `n11` and a Group `n12` (`n13` blue rect + /// `n14` text). Selection anchors on `n11`. /// - /// Mirrors the historical shell-core `Document::sample()` fixture - /// so the native host opens with identical content after the - /// migration onto `EditorState`. + /// This is the widget-test fixture. The native host opens with + /// [`EditorState::starter`] (a single empty Frame) instead. pub fn sample() -> Self { let src = r##"{ "version": "0.8.0", @@ -55,6 +54,29 @@ impl EditorState { state } + /// Build the document a fresh launch opens with — a single empty + /// starter Frame `n10` (white fill, 1-px black stroke), selected + /// so the user can immediately resize / move it or drop nodes + /// inside. No demo decoration. + pub fn starter() -> Self { + let src = r##"{ + "version": "0.8.0", + "children": [ + {"type":"frame","id":"n10","name":"Frame", + "x":40,"y":40,"width":360,"height":240, + "fill":[{"type":"solid","color":"#FFFFFF"}], + "stroke":{"thickness":1,"fill":[{"type":"solid","color":"#000000"}]}, + "children":[]} + ] + }"##; + 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 + } + /// Spawn a fresh leaf node for the active shape / frame / text /// tool at `(doc_x, doc_y)`, sized `init_w × init_h`. Returns the /// new node's id on success; `None` for `Select` / `Hand` (no @@ -200,6 +222,17 @@ mod tests { assert_eq!(s.max_node_id(), 14); } + #[test] + fn starter_is_a_single_empty_frame_with_it_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")); + } + #[test] fn create_node_for_tool_spawns_a_rect_and_returns_its_id() { let mut s = EditorState::new(); diff --git a/crates/op-host-native/src/widget_host.rs b/crates/op-host-native/src/widget_host.rs index 3a7153536..b70734a5a 100644 --- a/crates/op-host-native/src/widget_host.rs +++ b/crates/op-host-native/src/widget_host.rs @@ -373,7 +373,9 @@ pub(in crate::widget_host) struct ComponentBrowserDragState { impl WidgetHostNative { pub fn new() -> Self { - let editor_state = op_editor_core::EditorState::sample(); + // A fresh launch opens with a single empty starter Frame — + // see `EditorState::starter`. + let editor_state = op_editor_core::EditorState::starter(); // Seed the render scene once up front; subsequent frames // re-derive only when `editor_state_dirty` is set. let layout_scene = op_pen_loader::editor_state_to_layout_scene(&editor_state); diff --git a/crates/op-host-native/src/widget_host/input_tests.rs b/crates/op-host-native/src/widget_host/input_tests.rs index 9286f1850..7a873e4de 100644 --- a/crates/op-host-native/src/widget_host/input_tests.rs +++ b/crates/op-host-native/src/widget_host/input_tests.rs @@ -50,7 +50,7 @@ fn escape_closes_one_overlay_per_press_in_priority_order() { host.editor_state_mut().editor_ui.fill_type_picker_open = true; host.editor_state_mut().chat.focused = true; host.editor_state_mut() - .set_single_selection(NodeId::new("n11")); + .set_single_selection(NodeId::new("n10")); // 1. Property focus clears first. assert!(host.apply_escape()); @@ -92,21 +92,21 @@ fn backspace_with_property_draft_does_not_delete_selected() { // char from the draft, not delete the selected node. let mut host = WidgetHostNative::new(); host.editor_state_mut() - .set_single_selection(NodeId::new("n11")); + .set_single_selection(NodeId::new("n10")); host.editor_state_mut().ui.property_focus = Some(PropertyFocus::PositionX); host.editor_state_mut().ui.property_input_draft = "123".to_string(); assert!(host.apply_backspace()); assert_eq!(host.editor_state().ui.property_input_draft, "12"); // Selection must be untouched. - assert_eq!(host.editor_state().selection.anchor, NodeId::new("n11")); + assert_eq!(host.editor_state().selection.anchor, NodeId::new("n10")); } #[test] fn backspace_without_focus_deletes_selected() { let mut host = WidgetHostNative::new(); host.editor_state_mut() - .set_single_selection(NodeId::new("n11")); + .set_single_selection(NodeId::new("n10")); host.editor_state_mut().ui.property_focus = None; host.editor_state_mut().chat.focused = false; diff --git a/crates/op-host-web/src/widget_host.rs b/crates/op-host-web/src/widget_host.rs index 84f19c850..d814355ff 100644 --- a/crates/op-host-web/src/widget_host.rs +++ b/crates/op-host-web/src/widget_host.rs @@ -157,7 +157,9 @@ pub(in crate::widget_host) struct LayerDragState { impl WidgetHost { pub fn new() -> Self { - let editor_state = op_editor_core::EditorState::sample(); + // A fresh launch opens with a single empty starter Frame — + // see `EditorState::starter`. + let editor_state = op_editor_core::EditorState::starter(); // Seed the render scene once up front; subsequent frames // re-derive only when `editor_state_dirty` is set. let layout_scene = op_pen_loader::editor_state_to_layout_scene(&editor_state);