diff --git a/crates/op-host-services/src/doc_io/load.rs b/crates/op-host-services/src/doc_io/load.rs index b1d221edc..c9912c8c5 100644 --- a/crates/op-host-services/src/doc_io/load.rs +++ b/crates/op-host-services/src/doc_io/load.rs @@ -130,9 +130,35 @@ fn finish_loaded_state(mut state: EditorState, meta: Option) -> Edit } else { state.ui.active_page_index = first_page_with_content(&state); } + collapse_top_level_layers(&mut state); state } +/// Start every top-level frame collapsed in the LayerPanel. +/// +/// A fully expanded tree is unreadable the moment a document has depth: a +/// six-slide deck opens as ~90 rows and the boards themselves scroll off the +/// panel, so the one thing the list is for — seeing what the document +/// contains — is the first thing lost. Collapsed roots show the six slides +/// and let the user open the one they want. +/// +/// `collapsed_layers` is view-only state: not serialized, not in the undo +/// snapshot, and toggling it pushes no history. So this changes what the +/// panel shows on open and nothing about the document. +fn collapse_top_level_layers(state: &mut EditorState) { + let ids: Vec = state + .active_children() + .iter() + .filter(|node| { + // Only containers collapse; a leaf has nothing to hide and would + // just render a dead disclosure arrow. + op_editor_core::PenNodeExt::children(*node).is_some_and(|kids| !kids.is_empty()) + }) + .map(|node| op_editor_core::NodeId::new(op_editor_core::PenNodeExt::id_str(node))) + .collect(); + state.editor_ui.collapsed_layers.extend(ids); +} + fn first_page_with_content(state: &EditorState) -> usize { let Some(pages) = state.doc.pages.as_ref() else { return 0; diff --git a/crates/op-host-services/src/doc_io/load_report_tests.rs b/crates/op-host-services/src/doc_io/load_report_tests.rs index 3be4f5085..56c658f48 100644 --- a/crates/op-host-services/src/doc_io/load_report_tests.rs +++ b/crates/op-host-services/src/doc_io/load_report_tests.rs @@ -105,3 +105,39 @@ fn reports_only_adopted_sidecar_and_reliable_editor_meta_inference() { let _ = std::fs::remove_file(inferred); let _ = std::fs::remove_file(ordinary); } + +/// Top-level frames open collapsed in the LayerPanel. +/// +/// A six-slide deck expands to ~90 rows, pushing the boards themselves off +/// the panel — the list stops answering "what is in this document", which is +/// the only question it exists for. +#[test] +fn loading_collapses_top_level_frames_but_not_leaves() { + let source = serde_json::json!({ + "version": "1.0", + "children": [ + { + "type": "frame", "id": "board-1", "name": "01", "width": 1920, "height": 1080, + "children": [{"type": "text", "id": "t1", "content": "hi"}] + }, + { + "type": "frame", "id": "board-2", "name": "02", "width": 1920, "height": 1080, + "children": [{"type": "text", "id": "t2", "content": "hi"}] + }, + // A childless top-level node: collapsing it would only render a + // disclosure arrow that does nothing. + {"type": "rectangle", "id": "loose", "name": "Loose", "width": 10, "height": 10} + ] + }) + .to_string(); + + let state = + super::load_editor_state_from_source(&source, op_editor_core::Locale::EnUs).expect("loads"); + let collapsed = &state.editor_ui.collapsed_layers; + assert!(collapsed.contains(&op_editor_core::NodeId::new("board-1"))); + assert!(collapsed.contains(&op_editor_core::NodeId::new("board-2"))); + assert!( + !collapsed.contains(&op_editor_core::NodeId::new("loose")), + "a leaf has nothing to collapse" + ); +}