Tree View

A hierarchical collection.

Supports drag and drop. See the shared DnD guide for lifecycle, callbacks, and transaction rules.

Try it

Expand folders, collapse branches, and select files in this workspace tree. This preview runs the exact code sample shown on this page.

Open example separately ↗

Basic use

Radix · integration snippet
use gpui::{Context};
use xgpui_luma_look_radix as radix;
use xgpui_luma::controls::tree_view::TreeNode;

struct Example {
    control: xgpui_luma::controls::tree_view::TreeView<()>,
    selected_nodes: std::collections::HashSet<gpui::SharedString>,
}

impl Example {
    fn new(cx: &mut Context<Self>) -> Self {
        let control = radix::TreeView::<()>::new("example")
            .items([
                TreeNode::new("workspace", "Workspace", ()).expanded(true).children([
                    TreeNode::new("src", "src", ()).expanded(true).children([
                        TreeNode::new("main", "main.rs", ()),
                        TreeNode::new("controls", "controls", ()).children([
                            TreeNode::new("button", "button.rs", ()),
                            TreeNode::new("input", "input.rs", ()),
                        ]),
                    ]),
                    TreeNode::new("assets", "assets", ()).children([
                        TreeNode::new("icons", "icons", ()),
                        TreeNode::new("fonts", "fonts", ()),
                    ]),
                    TreeNode::new("cargo", "Cargo.toml", ()),
                ]),
                TreeNode::new("docs", "Documentation", ()).children([
                    TreeNode::new("guide", "Getting started", ()),
                    TreeNode::new("reference", "API reference", ()),
                ]),
            ])
            .on_selection_changed(cx, |app, selected_ids, cx| {
                app.selected_nodes = selected_ids.clone();
                cx.notify();
            })
            .spawn(cx);

        Self { control, selected_nodes: Default::default() }
    }
}

Integration snippet, checked against the current SDK and Radix crates. cx is the initialized owner context. The control retains its callbacks; callback owners are held weakly. Render self.control.clone() in the owner’s view.

SDK behavior & customization

Rows own disclosure, selection, focus, and scrolling. Use Frame for optional outer decoration.

Relevant contracts: TreeViewRenderModel, TreeViewTemplateHandlers, TreeViewTemplateModifier, TreeViewTemplate.

Use the module’s template contract for structural changes. Builder modifiers and item templates vary by control.

Events

Callbacks follow the existing SDK event stream. Registrations append, hold the owner weakly, and live until the control is released. cx.notify() requests an owner redraw. Compound payloads are borrowed; scalar values are passed by value.

Event / payloadCallback
DragDrop(super::TreeViewDragEvent).on_drag_drop(cx, |app, event, cx| { … })
NodeActivated { node_id: SharedString, data: T }.on_node_activated(cx, |app, node_id, data, cx| { … })
SelectionPolicyChanged { policy: super::TreeViewSelectionPolicy }.on_selection_policy_changed(cx, |app, policy, cx| { … })
NodeExpanded { node_id: SharedString, data: T }.on_node_expanded(cx, |app, node_id, data, cx| { … })
NodeCollapsed { node_id: SharedString, data: T }.on_node_collapsed(cx, |app, node_id, data, cx| { … })
SelectionChanged { selected_ids: HashSet<SharedString> }.on_selection_changed(cx, |app, selected_ids, cx| { … })
ActiveNodeChanged { node_id: Option<SharedString> }.on_active_node_changed(cx, |app, node_id, cx| { … })
ScrollChanged { top_index: usize }.on_scroll_changed(cx, |app, top_index, cx| { … })
FocusChanged { focused: bool }.on_focus_changed(cx, |app, focused, cx| { … })
RowHoverChanged { node_id: SharedString, index: usize, hovered: bool }.on_row_hover_changed(cx, |app, node_id, index, hovered, cx| { … })
EnabledChanged { enabled: bool }.on_enabled_changed(cx, |app, enabled, cx| { … })

Use .on_event(cx, |app, event, cx| { … }) for all events. Factory-created controls support the same callbacks through TreeViewEventExt. Existing cx.subscribe observers remain supported.

Drag/drop

See Drag and drop for the lifecycle, proposal/completion distinction, ordering, scopes, and callback ownership shared by these controls.

Enable drag_drop(TreeViewDragDrop::new(scope, on_drop)) on SDK, Shadcn or Radix builders. The handler receives TreeDropProposal<T>: call validate(cx), prepare both snapshots, apply the move and transferred selection/expansion state, then call committed(changed, cx) or rejected(error, cx). can_drop is admission policy for markers, auto-scroll and drop revalidation.

SDK TreeViewBuilder, TreeViewEventExt and both Look builders expose all seven named helpers through TreeViewEvent::DragDrop(TreeViewDragEvent). The domain payloads below replace the shared key/gap fields: node_ids is the normalized moving roots in displayed order, with selected descendants covered by their ancestor; node_id is the first root. Add/reorder/drop retain TreeDropLocation { parent_id, anchor_id, position }, including Into and After. Moves within a tree include reparenting.

Compatibility: Started, Dropped, Rejected, Ended and their callback signatures remain. Exhaustive enum matches must add ItemsRemoved, ItemsAdded, ItemsReordered arms or a wildcard. The historical TreeDragEndReason names map Reordered → MovedWithinTree and Transferred → MovedAcrossTrees.

Callback / phasePayload (between owner and context)
on_drag_started / Startedsource: EntityId, node_id: &SharedString, node_ids: &Vec<SharedString>
on_items_removed / ItemsRemovedsource: EntityId, target: EntityId, node_id: &SharedString, node_ids: &Vec<SharedString>
on_items_added / ItemsAddedsource: EntityId, target: EntityId, node_id: &SharedString, node_ids: &Vec<SharedString>, location: &TreeDropLocation
on_items_reordered / ItemsReorderedtarget: EntityId, node_id: &SharedString, node_ids: &Vec<SharedString>, location: &TreeDropLocation
on_dropped / Droppedsource: EntityId, target: EntityId, node_id: &SharedString, node_ids: &Vec<SharedString>, location: &TreeDropLocation, changed: bool
on_drop_rejected / Rejectedsource: EntityId, target: EntityId, node_id: &SharedString, node_ids: &Vec<SharedString>, error: &SharedString
on_drag_ended / Endedsource: EntityId, node_id: &SharedString, node_ids: &Vec<SharedString>, reason: TreeDragEndReason

Validate proposals and observe completion

Compiled by scripts/check_control_docs.py.

mod drag_drop_example {
    use gpui::{App, Context, SharedString};
    use xgpui_luma::controls::tree_view::{TreeDropProposal, TreeNode, TreeView, TreeViewDragDrop};
    use xgpui_luma_look_radix as radix;

    #[derive(Default)]
    struct Workspace { dirty: bool, error: Option<String> }

    // The host transaction prepares and applies both snapshots and transferred
    // selection/expansion state, returning whether data changed. On error it
    // must leave both trees unchanged. This callback runs synchronously.
    fn build(
        cx: &mut Context<Workspace>,
        apply_move: impl Fn(&TreeDropProposal<()>, &mut App) -> Result<bool, SharedString> + 'static,
    ) -> TreeView<()> {
        let config = TreeViewDragDrop::new(cx.entity_id(), move |proposal, _, cx| {
            if proposal.node_ids().iter().any(|id| id.as_ref() == "root") {
                proposal.rejected("Workspace root cannot move", cx);
                return;
            }
            match proposal.validate(cx).and_then(|()| apply_move(&proposal, cx)) {
                Ok(changed) => proposal.committed(changed, cx),
                Err(error) => proposal.rejected(error, cx),
            }
        }).drag_selected(true);
        radix::TreeView::new("workspace")
            .items([TreeNode::new("root", "Workspace", ())])
            .drag_drop(config)
            .on_dropped(cx, |owner, _, _, _, _, _, changed, cx| {
                owner.dirty |= changed;
                owner.error = None;
                cx.notify();
            })
            .on_drop_rejected(cx, |owner, _, _, _, _, error, cx| {
                owner.error = Some(error.to_string());
                cx.notify();
            })
            .spawn(cx)
    }
}

Committed changes also support shared drop-layout motion, including duration settings, reduced motion and documented virtualization/transfer limits.

Source

SDK module · Radix adapter