# Tree

> A collapsible file tree with icons and git colors. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API.

Source: https://albakore.github.io/pyzed/library/tree/

:::tip[In plain words]
A list of folders you can open and close, like the files of a project.
:::

A project panel: collapsible folders, an icon per file type and git status colors. `TreeNode.from_paths(paths, status=...)` builds the nodes, puts folders first and propagates the git status to the folders that contain changes. Each node's id is its path; you own the `expanded` set and the `selected` id.

`ui.tree` · Category: [Lists and menus](https://albakore.github.io/pyzed/library/#lists-and-menus) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#tree)

**Code:**

```python
nodes = ui.TreeNode.from_paths(
    ["src/main.rs", "src/app.rs", "src/ui/button.rs", "src/ui/mod.rs", "tests/app.rs", "Cargo.toml"],
    status={"src/app.rs": "modified", "src/ui/button.rs": "created"},
)
pz.container(ui.tree(nodes, expanded={"src", "src/ui"}, selected="src/app.rs",
                     on_toggle=on_toggle, on_select=on_select), width=280)
```

## When to use

- For files and any hierarchy: outlines, categories, settings sections.
- `status` is `"modified"`, `"created"`, `"deleted"`, `"conflict"` or `"ignored"`.

## Usage

```python
import pyzed as pz
from pyzed import ui

class Explorer(pz.App):
    def __init__(self):
        self.nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "Cargo.toml"])
        self.expanded = {"src"}
        self.selected = None

    def toggle(self, folder):
        self.expanded ^= {folder}

    def view(self):
        return ui.tree(self.nodes, expanded=self.expanded, selected=self.selected,
                       on_toggle=self.toggle, on_select=lambda path: setattr(self, "selected", path))
```

## Variants

:::note[About these captures]
Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks.
:::

### Expanded

```python
nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"])
pz.container(ui.tree(nodes, expanded=set()), width=240)
```

```python
nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"])
pz.container(ui.tree(nodes, expanded={"src"}), width=240)
```

```python
nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"])
pz.container(ui.tree(nodes, expanded={"src", "src/ui"}, selected="src/ui/button.rs"), width=240)
```

### Git status

```python
nodes = ui.TreeNode.from_paths(["src/app.rs", "src/new.rs", "src/old.rs", "build.log"],
    status={"src/app.rs": "modified", "src/new.rs": "created", "src/old.rs": "deleted", "build.log": "ignored"})
pz.container(ui.tree(nodes, expanded={"src"}), width=240)
```

```python
nodes = ui.TreeNode.from_paths(["merge.py", "ok.py"], status={"merge.py": "conflict"})
pz.container(ui.tree(nodes, expanded=set()), width=240)
```

### Density

```python
nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"])
pz.container(ui.tree(nodes, expanded={"src", "src/ui"}, item_height=20, indent=10), width=240)
```

```python
nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"])
pz.container(ui.tree(nodes, expanded={"src"}, item_height=30, indent=20), width=240)
```

### States and events

`on_open(id)` runs on a double click of a file and `on_context(id)` on a right click.

```python
pz.container(ui.tree(ui.TreeNode.from_paths(["src/app.py", "src/ui.py", "README.md"]), expanded={"src"},
                     selected={"src/app.py", "README.md"}, focused="src/ui.py", on_open=on_open,
                     on_context=on_context), width=300)
```

## Examples

### A real folder

Build the paths with `pathlib` and pass them to `from_paths`.

```python
from pathlib import Path

root = Path(".")
paths = [p.relative_to(root).as_posix() for p in root.rglob("*")
         if p.is_file() and ".git" not in p.parts]
nodes = ui.TreeNode.from_paths(paths)
```

### Nodes by hand

Folders are nodes with `children`.

**Code:**

```python
nodes = [
    ui.TreeNode("Inbox", children=[ui.TreeNode("Welcome", id="inbox/1", icon="mail")], id="inbox"),
    ui.TreeNode("Archive", children=[], id="archive"),
    ui.TreeNode("Notes.md", id="notes", icon="notebook-pen"),
]
pz.container(ui.tree(nodes, expanded={"inbox"}, selected="inbox/1"), width=260)
```

## Events

Tree has **4 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/).

| Event | Handler gets | When |
| --- | --- | --- |
| `on_toggle` | `(id)` | Called with the id of the clicked folder. |
| `on_select` | `(id)` | Called with the id of the clicked file. |
| `on_open` | `(id)` | Called with the id of a file that is double-clicked. |
| `on_context` | `(id)` | Called with the id of a row that is right-clicked (show a `ui.menu` there). |

**Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`.

### Handling the events

The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/).

**Imperative:**

```python
class Explorer(pz.App):
    def __init__(self):
        self.nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "Cargo.toml"])
        self.expanded = {"src"}
        self.selected = None

    def toggle(self, folder):            # on_toggle: the folder's id (its path)
        self.expanded ^= {folder}

    def view(self):
        return ui.tree(self.nodes, expanded=self.expanded, selected=self.selected,
                       on_toggle=self.toggle,
                       on_select=lambda path: setattr(self, "selected", path),   # click on a file
                       on_open=lambda path: print("open", path),                 # double click
                       on_context=lambda path: print("menu for", path))          # right click
```

**Declarative:**

```python
from dataclasses import dataclass

@dataclass
class Toggle:
    folder: str

@dataclass
class Select:
    path: str

class Explorer(pz.App):
    nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "Cargo.toml"])
    expanded = frozenset({"src"})
    selected = None

    def update(self, message):
        match message:
            case Toggle(folder):
                self.expanded = self.expanded ^ {folder}
            case Select(path):
                self.selected = path

    def view(self):
        return ui.tree(self.nodes, expanded=self.expanded, selected=self.selected,
                       on_toggle=Toggle, on_select=Select)
```

**Functional:**

```python
from dataclasses import dataclass

NODES = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "Cargo.toml"])

@dataclass(frozen=True)
class State:
    expanded: frozenset = frozenset({"src"})
    selected: str = ""

def view(state, set_state):
    return ui.tree(NODES, expanded=state.expanded, selected=state.selected,
                   on_toggle=lambda folder: set_state(expanded=state.expanded ^ {folder}),
                   on_select=lambda path: set_state(selected=path))
```

## Properties and API

### `ui.tree`

```python
tree(
    nodes,
    *,
    expanded,
    selected=None,
    on_toggle=None,
    on_select=None,
    on_open=None,
    on_context=None,
    focused=None,
    indent=14,
    item_height=24,
    theme=None,
)
```

A collapsible file tree, like a code editor's project panel.

| Property | Default | Description |
| --- | --- | --- |
| `nodes` | *required* | List of `TreeNode`. |
| `expanded` | *required* | Set of open folder ids. |
| `selected` | `None` | One id or a collection of ids (multiple selection). |
| `focused` | `None` | Outlines the row the keyboard is on. |
| `indent` | `14` | Left indentation, in pixels. |
| `item_height` | `24` | Height of each row. |
| `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). |

Events: `on_toggle`, `on_select`, `on_open`, `on_context` ([events](#events)).

[Full reference →](https://albakore.github.io/pyzed/reference/ui/#tree)

### `ui.TreeNode`

```python
@dataclass
class TreeNode(name, children=None, id=None, status=None, icon=None, icon_color=None)
```

A file or folder for `ui.tree`. Folders have `children` (a list). `status` is `"modified"`, `"created"`, `"deleted"`, `"conflict"` or `"ignored"`. `TreeNode.from_paths(paths, status=None)` builds the tree from paths like `"src/main.rs"`, with folders first and the git status propagated to folders.

| Property | Default | Description |
| --- | --- | --- |
| `name` | *required* | Display name. |
| `children` | `None` | Children (`None` for a file). |
| `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. |
| `status` | `None` | Git status. |
| `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). |
| `icon_color` | `None` | Icon color: a theme token or a hex color. |

[Full reference →](https://albakore.github.io/pyzed/reference/ui/#treenode)

## Related

- [List item](https://albakore.github.io/pyzed/library/list-item/): A list row with icon, text and an end slot.
- [Panel](https://albakore.github.io/pyzed/library/panel/): A dockable, resizable side panel.
- [Workspace](https://albakore.github.io/pyzed/library/workspace/): The layout of an editor-style window: title bar, docks, center, status bar.
