# Gum Tool Tree View

> Gum Tool Tree View Reference

- Skill: `vchelaru/gum-tool-tree-view` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vchelaru/gum-tool-tree-view`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vchelaru/gum-tool-tree-view/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: vchelaru (https://skillmd.com/u/vchelaru)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vchelaru/gum-tool-tree-view

---


# Gum Tool Tree View Reference

The left-hand panel listing Screens, Components, Standard Elements, Behaviors, and the instances
inside an open element. The model and logic are framework-neutral in `Tool/TreeViewPlugin.Core`
(net10.0, shared by both heads); each head supplies only the panel, through `IElementTreeView`.

## File map

| File | Purpose |
|---|---|
| `Tool/TreeViewPlugin.Core/GumTreeNode.cs` (+ `GumTreeNodeCollection.cs`) | The node model both trees bind to; implements `ITreeNodeMutable` |
| `Tool/TreeViewPlugin.Core/ElementTreeViewManager.cs` (+ `.RightClick.cs`) | Builds, refreshes, searches and selects nodes; builds the right-click menu as `ContextMenuItemViewModel`s |
| `Tool/TreeViewPlugin.Core/IElementTreeView.cs` | The panel contract the manager talks to, its factory, and the external-drop event args |
| `Tool/TreeViewPlugin.Core/TreeSelection/TreeSelectionModel.cs` | Selection state and rules (click, range, toggle, keyboard nav, drag start, pruning) both controls feed |
| `Tool/TreeViewPlugin.Core/TreeSelection/` (other files) | Click/range/key decision classes on neutral enums; `TreeDropKind` and `TreeDropLogic` |
| `Tool/TreeViewPlugin.Core/TreeIconCatalog.cs` | Icon index → artwork path + theme color key, shared by both heads' registries |
| `Tool/TreeViewPlugin.Core/MainTreeViewPlugin.cs` | Wires plugin events to `RefreshUi(...)` and error-indicator updates; loaded by both hosts |
| `Tool/TreeViewPlugin.Core/TreeViewStateService.cs`, `CollapseToggleService.cs` | Expansion state: persisted across sessions, and the collapse-button toggle |
| `Gum/Plugins/InternalPlugins/TreeView/WpfElementTreeView.cs` | WPF panel: `GumTreeView`, search box, flat results, collapse buttons, chip palette; WPF drag/cursor glue |
| `Gum/Controls/GumTreeView.cs` (+ `.DragDrop.cs`) | WPF control: hit testing, expander clicks, drag start, drop adornment; delegates selection to `TreeSelectionModel` |
| `Gum/Themes/Frb.TreeView.xaml`, `Gum/Controls/TreeIconRegistry.cs`, `TreeNodeIcon.cs` | WPF row template and icon drawing |
| `Tool/Gum.Avalonia/Plugins/TreeView/` | Avalonia panel (`AvaloniaElementTreeView`), row-list control (`AvaloniaGumTreeView`), palette, icons |
| `Tools/Gum.Presentation/Services/RefreshCoalescer.cs` | Collapses N `RequestRefresh()` calls in one synchronous burst into a single `IDispatcher`-posted refresh |

The states tree (center-top "States" tab) shares `StateTreePluginBase`, `StateTreeRightClickService`
and `StateTreeKeyboardHandler` in `Tools/Gum.Presentation/Plugins/InternalPlugins/StatePlugin/`;
`MainStatePlugin` (WPF) and `AvaloniaStatePlugin` only build their tree control.

`ElementTreeViewManager` and its `RightClick` partial speak `ITreeNode`/`ITreeNodeMutable`, delegating
to headless twins in `Tools/Gum.Presentation/Managers/` (`TreeNodeImageLogic`, the `TreeNode*Extensions`
families, `TreeNodeExpansionPaths`). Prefer adding logic there over growing the manager.

## Selection is on the model, not the container

`TreeView` enforces a single selected item and clears the previous one on every change, so
`TreeViewItem.IsSelected` is deliberately never set. `TreeSelectionModel` tracks the selection and the
row visuals bind to `GumTreeNode.IsSelected`. The Avalonia tree goes further and is a flat,
virtualized row list rather than a `TreeView`. Consequences:

- Keyboard navigation is `TreeSelectionModel.HandleKeyDown`, called from each control's key handler.
- A change to selection behavior goes in `TreeSelectionModel` (with a test), never in one head's control.
- `IsExpanded` is ordinary two-way bound state, so expansion survives a rebuild without being
  captured and replayed.

## Icons

`TreeIconRegistry` maps an index (the shared `TreeNodeImageIndices` constants, produced by the
headless `TreeNodeImageLogic`) to a pack URI plus a theme color key; `TreeNodeIcon` renders the pair.

- **Source PNGs must be white-on-transparent**, alpha carrying the shading. Tinting fills a shape with
  the theme brush and uses the artwork as an `OpacityMask`, so a colored source multiplies wrong.
- Adding an icon is a constant in `TreeNodeImageIndices` plus an entry in `TreeIconRegistry` — in any
  position. The numbering is not tied to load order.
- Icons re-tint on theme change via `TreeIconRegistry.NotifyThemeChanged()`; nothing is regenerated.

## Refresh model

`RefreshUi()` is diff-based — existing nodes are reused and only differing `ImageIndex`/position/`Tag`/
`Text` are written. Replacing nodes wholesale would drop selection and scroll position.

`Tag` distinguishes node kinds: folder/container nodes have `Tag == null`; element nodes carry an
`ElementSave`/`BehaviorSave`; instance nodes an `InstanceSave`.

`MainTreeViewPlugin.HandleElementImported` requests a refresh through a `RefreshCoalescer` rather than
calling `RefreshUi()` directly, so importing N elements in one batch (Forms theme, `.gumx` import)
produces one refresh instead of N.

## Gotchas

- **Reordering within one collection must be remove-then-insert.** `GumTreeNodeCollection` throws if a
  node is inserted into the collection it already belongs to, because detaching first would shift the
  index the caller computed. Reparenting *across* collections is a plain add.
- **Drag payloads travel in `TreeDragPayload`, not on the drag data.** Gum's `*Save` types aren't
  `[Serializable]`, so anything put on a WPF data object comes back null; the data carries only a
  marker format (`TreeDragPayload.DataFormat` in WPF, `AvaloniaDragFormats.TreeNodes` in Avalonia).
  Both heads' canvas drop readers and search-result lists use the same static.
- **`ITreeNode.FullPath` is backslash-separated.** `CopyPasteLogic` slices a `"Components\\"` prefix
  off it.
- **Persisted expansion state is forward-slash-joined node `Text` paths** (`TreeNodeExpansionPaths`).
  Changing either the separator or the use of `Text` silently discards every user's saved state.
- **Virtualization is off** (the WPF `TreeView` default). That is what makes
  `GumTreeView.ContainerFor`/`EnsureVisible` reliable — turning it on would break container lookup for
  off-screen nodes.
- **`GumTreeView.EnsureVisible` defers to a `Loaded` dispatcher callback**, since a newly-expanded
  ancestor's child has no container until layout runs.

