GraphiteEditor/Graphite · error
ROOT_PARENT should have at least one layer when clicking
Error message
ROOT_PARENT should have at least one layer when clicking
What it means
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.
Source
Thrown at editor/src/messages/tool/tool_messages/select_tool.rs:1932
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
}
}
fn not_artboard(document: &DocumentMessageHandler) -> impl Fn(&LayerNodeIdentifier) -> bool + '_ {
|&layer| layer != LayerNodeIdentifier::ROOT_PARENT && !document.network_interface.is_artboard(&layer.to_node(), &[])
}
fn drag_shallowest_manipulation(responses: &mut VecDeque<Message>, selected: Vec<LayerNodeIdentifier>, tool_data: &mut SelectToolData, document: &DocumentMessageHandler, remove: bool, exists: bool) {
if selected.is_empty() {
return;
}
let clicked_layer = document.find_deepest(&selected).unwrap_or_else(|| {
LayerNodeIdentifier::ROOT_PARENT
.children(document.metadata())
.next()
.expect("ROOT_PARENT should have at least one layer when clicking")
});
let metadata = document.metadata();
let selected_layers = document.network_interface.selected_nodes().selected_layers(document.metadata()).collect::<Vec<_>>();
let final_selection: Option<LayerNodeIdentifier> = (!selected_layers.is_empty() && selected_layers != vec![LayerNodeIdentifier::ROOT_PARENT]).then_some(()).and_then(|_| {
let mut relevant_layers = document.network_interface.selected_nodes().selected_layers(document.metadata()).collect::<Vec<_>>();
if !relevant_layers.contains(&clicked_layer) {
relevant_layers.push(clicked_layer);
}
clicked_layer
.ancestors(metadata)
.filter(not_artboard(document))
.find(|&ancestor| relevant_layers.iter().all(|layer| *layer == ancestor || ancestor.is_ancestor_of(metadata, layer)))
.and_then(|least_common_ancestor| {
let common_siblings: Vec<_> = least_common_ancestor.children(metadata).collect();
(clicked_layer == least_common_ancestor)
.then_some(least_common_ancestor)View on GitHub (pinned to c507b35645)
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.
Example fix
// before
let clicked_layer = document.find_deepest(&selected).unwrap_or_else(|| {
LayerNodeIdentifier::ROOT_PARENT
.children(document.metadata())
.next()
.expect("ROOT_PARENT should have at least one layer when clicking")
});
// after
let Some(clicked_layer) = document.find_deepest(&selected).or_else(|| LayerNodeIdentifier::ROOT_PARENT.children(document.metadata()).next()) else {
log::warn!("select tool: no resolvable clicked layer in empty document; aborting shallow drag");
return;
}; Defensive patterns
Strategy: validation
Validate before calling
let Some(clicked_layer) = document
.find_deepest(&selected)
.or_else(|| LayerNodeIdentifier::ROOT_PARENT.children(document.metadata()).next())
else {
// no resolvable layer (empty document / stale selection): skip drag
return;
}; Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- ROOT_PARENT should have a layer child when clicking
- Cannot find connected anchor
- Node Id 0 should be a layer
- Star node can't be found
- Brush node does not exist
AI-assisted analysis of GraphiteEditor/Graphite@c507b35645 (2026-08-16).
Data as JSON: /api/errors/d33c9598072553fe.
Report an issue: GitHub.