Tree

A versatile tree component for displaying hierarchical data with expand/collapse functionality, keyboard navigation, and custom item rendering. Perfect for file explorers, navigation menus, or any nested data structure.

#Import

use gpui_kit::component::tree::{tree, TreeState, TreeItem, TreeEntry};

#Usage

#Basic Tree

// Create tree state
let tree_state = cx.new(|cx| {
    TreeState::new(cx).items(vec![
        TreeItem::new("src", "src")
            .expanded(true)
            .child(TreeItem::new("src/lib.rs", "lib.rs"))
            .child(TreeItem::new("src/main.rs", "main.rs")),
        TreeItem::new("Cargo.toml", "Cargo.toml"),
        TreeItem::new("README.md", "README.md"),
    ])
});

// Render tree
tree(&tree_state, |ix, entry, selected, window, cx| {
    ListItem::new(ix)
        .child(
            h_flex()
                .gap_2()
                .child(entry.item().label.clone())
        )
})

#File Tree with Icons

use gpui_kit::component::{ListItem, IconName, h_flex};

tree(&tree_state, |ix, entry, selected, window, cx| {
    let item = entry.item();
    let icon = if !entry.is_folder() {
        IconName::File
    } else if entry.is_expanded() {
        IconName::FolderOpen
    } else {
        IconName::Folder
    };

    ListItem::new(ix)
        .selected(selected)
        .pl(px(16.) * entry.depth() + px(12.)) // Indent based on depth
        .child(
            h_flex()
                .gap_2()
                .child(icon)
                .child(item.label.clone())
        )
        .on_click(cx.listener(move |_, _, _, _| {
            // Handle item click
        }))
})

#Dynamic Tree Loading

impl MyView {
    fn load_files(&mut self, path: PathBuf, cx: &mut Context<Self>) {
        let tree_state = self.tree_state.clone();
        cx.spawn(async move |cx| {
            let items = build_file_items(&path).await;
            tree_state.update(cx, |state, cx| {
                state.set_items(items, cx);
            })
        }).detach();
    }
}

fn build_file_items(path: &Path) -> Vec<TreeItem> {
    let mut items = Vec::new();
    if let Ok(entries) = std::fs::read_dir(path) {
        for entry in entries.flatten() {
            let path = entry.path();
            let name = path.file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("Unknown")
                .to_string();

            if path.is_dir() {
                let children = build_file_items(&path);
                items.push(TreeItem::new(path.to_string_lossy(), name)
                    .children(children));
            } else {
                items.push(TreeItem::new(path.to_string_lossy(), name));
            }
        }
    }
    items
}

#Tree with Selection Handling

struct MyTreeView {
    tree_state: Entity<TreeState>,
    selected_item: Option<TreeItem>,
}

impl MyTreeView {
    fn handle_selection(&mut self, item: TreeItem, cx: &mut Context<Self>) {
        self.selected_item = Some(item.clone());
        println!("Selected: {} ({})", item.label, item.id);
        cx.notify();
    }
}

// In render method
tree(&self.tree_state, {
    let view = cx.entity();
    move |ix, entry, selected, window, cx| {
        view.update(cx, |this, cx| {
            ListItem::new(ix)
                .selected(selected)
                .child(entry.item().label.clone())
                .on_click(cx.listener({
                    let item = entry.item().clone();
                    move |this, _, _, cx| {
                        this.handle_selection(item.clone(), cx);
                    }
                }))
        })
    }
})

#Disabled Items

TreeItem::new("protected", "Protected Folder")
    .disabled(true)
    .child(TreeItem::new("secret.txt", "secret.txt"))

#Programmatic Tree Control

// Get current selection
if let Some(entry) = tree_state.read(cx).selected_entry() {
    println!("Current selection: {}", entry.item().label);
}

// Set selection programmatically (by selected_index)
tree_state.update(cx, |state, cx| {
    state.set_selected_index(Some(2), cx); // Select third item
});

// Set selection programmatically (by tree item)
tree_state.update(cx, |state, cx| {
    state.set_selected_item(Some(item), cx); // Select third item
});

// Scroll to specific item
tree_state.update(cx, |state, _| {
    state.scroll_to_item(5, gpui_kit::ScrollStrategy::Center);
});

// Clear selection (by selected_index)
tree_state.update(cx, |state, cx| {
    state.set_selected_index(None, cx);
});

// Clear selection (by tree item)
tree_state.update(cx, |state, cx| {
    state.set_selected_item(None, cx);
});

#API Reference

#TreeState

MethodDescription
new(cx)Create a new tree state
items(items)Set initial tree items
set_items(items, cx)Update tree items and notify
selected_index()Get currently selected index
set_selected_index(ix, cx)Set selected index
set_selected_item(item, cx)Set selected by tree item
selected_item(item, cx)Get currently selected tree item
selected_entry()Get currently selected entry
scroll_to_item(ix, strategy)Scroll to specific item

#TreeItem

MethodDescription
new(id, label)Create new tree item with ID and label
child(item)Add single child item
children(items)Add multiple child items
expanded(bool)Set expanded state
disabled(bool)Set disabled state
is_folder()Check if item has children
is_expanded()Check if item is expanded
is_disabled()Check if item is disabled

#TreeEntry

MethodDescription
item()Get the source TreeItem
depth()Get item depth in tree
is_folder()Check if entry has children
is_expanded()Check if entry is expanded
is_disabled()Check if entry is disabled

#tree() Function

ParameterDescription
stateEntity<TreeState> for managing tree
render_itemClosure for rendering each item

#Render Item Closure

Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem
  • usize: Item index in flattened tree
  • &TreeEntry: Tree entry with item and metadata
  • bool: Whether item is currently selected
  • &mut Window: Current window context
  • &mut App: Application context
  • Returns: ListItem for rendering

#Examples

#Lazy Loading Tree

struct LazyTreeView {
    tree_state: Entity<TreeState>,
    loaded_paths: HashSet<String>,
}

impl LazyTreeView {
    fn load_children(&mut self, item_id: &str, cx: &mut Context<Self>) {
        if self.loaded_paths.contains(item_id) {
            return;
        }

        let path = PathBuf::from(item_id);
        if path.is_dir() {
            let tree_state = self.tree_state.clone();
            let item_id = item_id.to_string();

            cx.spawn(async move |cx| {
                let children = load_directory_children(&path).await;
                tree_state.update(cx, |state, cx| {
                    // Update specific item with loaded children
                    state.update_item_children(&item_id, children, cx);
                })
            }).detach();

            self.loaded_paths.insert(item_id.to_string());
        }
    }
}

#Search and Filter

struct SearchableTree {
    tree_state: Entity<TreeState>,
    original_items: Vec<TreeItem>,
    search_query: String,
}

impl SearchableTree {
    fn filter_tree(&mut self, query: &str, cx: &mut Context<Self>) {
        self.search_query = query.to_string();

        let filtered_items = if query.is_empty() {
            self.original_items.clone()
        } else {
            filter_tree_items(&self.original_items, query)
        };

        self.tree_state.update(cx, |state, cx| {
            state.set_items(filtered_items, cx);
        });
    }
}

fn filter_tree_items(items: &[TreeItem], query: &str) -> Vec<TreeItem> {
    items.iter()
        .filter_map(|item| {
            if item.label.to_lowercase().contains(&query.to_lowercase()) {
                Some(item.clone().expanded(true)) // Auto-expand matches
            } else {
                // Check if any children match
                let filtered_children = filter_tree_items(&item.children, query);
                if !filtered_children.is_empty() {
                    Some(item.clone()
                        .children(filtered_children)
                        .expanded(true))
                } else {
                    None
                }
            }
        })
        .collect()
}

#Multi-Select Tree

struct MultiSelectTree {
    tree_state: Entity<TreeState>,
    selected_items: HashSet<String>,
}

impl MultiSelectTree {
    fn toggle_selection(&mut self, item_id: &str, cx: &mut Context<Self>) {
        if self.selected_items.contains(item_id) {
            self.selected_items.remove(item_id);
        } else {
            self.selected_items.insert(item_id.to_string());
        }
        cx.notify();
    }

    fn is_selected(&self, item_id: &str) -> bool {
        self.selected_items.contains(item_id)
    }
}

// In render method
tree(&self.tree_state, {
    let view = cx.entity();
    move |ix, entry, _selected, window, cx| {
        view.update(cx, |this, cx| {
            let item = entry.item();
            let is_multi_selected = this.is_selected(&item.id);

            ListItem::new(ix)
                .selected(is_multi_selected)
                .child(
                    h_flex()
                        .gap_2()
                        .child(checkbox().checked(is_multi_selected))
                        .child(item.label.clone())
                )
                .on_click(cx.listener({
                    let item_id = item.id.clone();
                    move |this, _, _, cx| {
                        this.toggle_selection(&item_id, cx);
                    }
                }))
        })
    }
})

#Keyboard Navigation

The Tree component supports comprehensive keyboard navigation:

KeyAction
↑Select previous item
↓Select next item
←Collapse current folder or move to parent
→Expand current folder
EnterToggle expand/collapse for folders
SpaceCustom action (configurable)
// Custom keyboard handling
tree(&tree_state)
    .key_context("MyTree")
    .on_action(cx.listener(|this, action: &MyCustomAction, _, cx| {
        // Handle custom actions
    }))

CC BY 4.0 · Documentation prose and original illustrations GPUI Kit has rights to license may be copied or adapted with attribution. License. Credit GPUI Kit, link to this source page and the CC BY 4.0 license, and indicate any changes. Code examples and software source remain under Apache-2.0. Existing Apache permissions remain; third-party material retains its own terms.