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

  1. Handle the empty fallback: replace the expect with a checked let-else that logs and returns early when no child exists.
  2. Filter the incoming selected list through document.metadata() before calling drag_shallowest_manipulation so find_deepest cannot receive dead ids.
  3. Reproduce with a scripted sequence: create a layer, select it, undo its creation, then drag — confirm the guard path instead of a panic.
  4. 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

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


AI-assisted analysis of GraphiteEditor/Graphite@c507b35645 (2026-08-16). Data as JSON: /api/errors/d33c9598072553fe. Report an issue: GitHub.