feat(panels): open documents with top-level layers collapsed

A fully expanded tree stops answering the one question the LayerPanel
exists for. A six-slide deck opens as ~90 rows, so the boards themselves
scroll off the panel and the user cannot see what the document contains
without scrolling past every leaf of slide one.

Collapse only top-level containers on load: leaves have nothing to hide
and would render a dead disclosure arrow. `collapsed_layers` is view-only
state — not serialized, not in the undo snapshot, no history push — so
this changes what the panel shows on open and nothing about the document.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
This commit is contained in:
Fini 2026-08-02 21:21:23 +08:00
parent ad2f0e4a6a
commit 17a8fdcfdf
2 changed files with 62 additions and 0 deletions

View file

@ -130,9 +130,35 @@ fn finish_loaded_state(mut state: EditorState, meta: Option<EditorMeta>) -> 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<op_editor_core::NodeId> = 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;

View file

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