feat(editor): open with a single empty starter frame

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`.
This commit is contained in:
Kayshen-X 2026-05-22 11:40:08 +08:00
parent 12747384c6
commit d1093b7dc4
4 changed files with 46 additions and 9 deletions

View file

@ -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();

View file

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

View file

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

View file

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