{"record":{"id":"d33c9598072553fe","repo":"GraphiteEditor/Graphite","slug":"root-parent-should-have-at-least-one-layer-when-cl","errorCode":null,"errorMessage":"ROOT_PARENT should have at least one layer when clicking","messagePattern":"ROOT_PARENT should have at least one layer when clicking","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"editor/src/messages/tool/tool_messages/select_tool.rs","lineNumber":1932,"sourceCode":"\tfn update_cursor(&self, responses: &mut VecDeque<Message>) {\n\t\tresponses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });\n\t}\n}\n\nfn not_artboard(document: &DocumentMessageHandler) -> impl Fn(&LayerNodeIdentifier) -> bool + '_ {\n\t|&layer| layer != LayerNodeIdentifier::ROOT_PARENT && !document.network_interface.is_artboard(&layer.to_node(), &[])\n}\n\nfn drag_shallowest_manipulation(responses: &mut VecDeque<Message>, selected: Vec<LayerNodeIdentifier>, tool_data: &mut SelectToolData, document: &DocumentMessageHandler, remove: bool, exists: bool) {\n\tif selected.is_empty() {\n\t\treturn;\n\t}\n\n\tlet clicked_layer = document.find_deepest(&selected).unwrap_or_else(|| {\n\t\tLayerNodeIdentifier::ROOT_PARENT\n\t\t\t.children(document.metadata())\n\t\t\t.next()\n\t\t\t.expect(\"ROOT_PARENT should have at least one layer when clicking\")\n\t});\n\n\tlet metadata = document.metadata();\n\n\tlet selected_layers = document.network_interface.selected_nodes().selected_layers(document.metadata()).collect::<Vec<_>>();\n\tlet final_selection: Option<LayerNodeIdentifier> = (!selected_layers.is_empty() && selected_layers != vec![LayerNodeIdentifier::ROOT_PARENT]).then_some(()).and_then(|_| {\n\t\tlet mut relevant_layers = document.network_interface.selected_nodes().selected_layers(document.metadata()).collect::<Vec<_>>();\n\t\tif !relevant_layers.contains(&clicked_layer) {\n\t\t\trelevant_layers.push(clicked_layer);\n\t\t}\n\t\tclicked_layer\n\t\t\t.ancestors(metadata)\n\t\t\t.filter(not_artboard(document))\n\t\t\t.find(|&ancestor| relevant_layers.iter().all(|layer| *layer == ancestor || ancestor.is_ancestor_of(metadata, layer)))\n\t\t\t.and_then(|least_common_ancestor| {\n\t\t\t\tlet common_siblings: Vec<_> = least_common_ancestor.children(metadata).collect();\n\t\t\t\t(clicked_layer == least_common_ancestor)\n\t\t\t\t\t.then_some(least_common_ancestor)","sourceCodeStart":1914,"sourceCodeEnd":1950,"githubUrl":"https://github.com/GraphiteEditor/Graphite/blob/c507b356453361e31638b8bff8f6d46b6da2961e/editor/src/messages/tool/tool_messages/select_tool.rs#L1914-L1950","documentation":"drag_shallowest_manipulation needs a concrete clicked layer to anchor a shallow drag; when find_deepest fails on the selected set it falls back to the first top-level child of ROOT_PARENT. The expect asserts that fallback exists — i.e., the document has at least one layer at the root. It panics when both find_deepest returns None (selection not resolvable in current metadata) and the document is empty or all root children are gone, so the last-resort iterator yields None. In practice this fires only during metadata/selection desync in an otherwise empty document, because the function early-returns when selected is empty.","triggerScenarios":"Starting a drag manipulation with a non-empty selected list whose layers cannot be found by document.find_deepest (stale selection after deletions/undo), in a document where ROOT_PARENT.children(metadata()).next() is None (no layers or artboards at the top level).","commonSituations":"Undo/redo sequences that delete the last layers while the select tool still holds their ids in tool_data; documents where every layer was cut and the selection message arrives before metadata refresh; edge-case automated tests that synthesize selections against empty documents; desync between the selection message stream and document metadata updates.","solutions":["Handle the empty fallback: replace the expect with a checked let-else that logs and returns early when no child exists.","Filter the incoming selected list through document.metadata() before calling drag_shallowest_manipulation so find_deepest cannot receive dead ids.","Reproduce with a scripted sequence: create a layer, select it, undo its creation, then drag — confirm the guard path instead of a panic.","If tool_data can hold stale dragging layers, clear layers_dragging on structural document changes (deletion/undo) to keep the selection coherent."],"exampleFix":"// before\nlet clicked_layer = document.find_deepest(&selected).unwrap_or_else(|| {\n\tLayerNodeIdentifier::ROOT_PARENT\n\t\t.children(document.metadata())\n\t\t.next()\n\t\t.expect(\"ROOT_PARENT should have at least one layer when clicking\")\n});\n\n// after\nlet Some(clicked_layer) = document.find_deepest(&selected).or_else(|| LayerNodeIdentifier::ROOT_PARENT.children(document.metadata()).next()) else {\n\tlog::warn!(\"select tool: no resolvable clicked layer in empty document; aborting shallow drag\");\n\treturn;\n};","handlingStrategy":"validation","validationCode":"let Some(clicked_layer) = document\n\t.find_deepest(&selected)\n\t.or_else(|| LayerNodeIdentifier::ROOT_PARENT.children(document.metadata()).next())\nelse {\n\t// no resolvable layer (empty document / stale selection): skip drag\n\treturn;\n};","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never assume document-level fallbacks (root children) exist: treat 'no target' as a normal early-return.","Prune selection lists against current metadata before drag handlers run.","Cover the empty-document + stale-selection case in select-tool integration tests."],"tags":["rust","graphite-editor","select-tool","empty-collection","invariant-violation","expect-panic","document-metadata"],"backgroundTag":"empty-collection-invariant","analyzedSha":"c507b356453361e31638b8bff8f6d46b6da2961e","analyzedAt":"2026-08-16T21:57:18.596Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}