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 ContextMenuItemViewModels |
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.
1---2name: gum-tool-tree-view3description: Gum Tool Tree View Reference4---56# Gum Tool Tree View Reference78The left-hand panel listing Screens, Components, Standard Elements, Behaviors, and the instances9inside an open element. The model and logic are framework-neutral in `Tool/TreeViewPlugin.Core`10(net10.0, shared by both heads); each head supplies only the panel, through `IElementTreeView`.1112## File map1314| File | Purpose |15|---|---|16| `Tool/TreeViewPlugin.Core/GumTreeNode.cs` (+ `GumTreeNodeCollection.cs`) | The node model both trees bind to; implements `ITreeNodeMutable` |17| `Tool/TreeViewPlugin.Core/ElementTreeViewManager.cs` (+ `.RightClick.cs`) | Builds, refreshes, searches and selects nodes; builds the right-click menu as `ContextMenuItemViewModel`s |18| `Tool/TreeViewPlugin.Core/IElementTreeView.cs` | The panel contract the manager talks to, its factory, and the external-drop event args |19| `Tool/TreeViewPlugin.Core/TreeSelection/TreeSelectionModel.cs` | Selection state and rules (click, range, toggle, keyboard nav, drag start, pruning) both controls feed |20| `Tool/TreeViewPlugin.Core/TreeSelection/` (other files) | Click/range/key decision classes on neutral enums; `TreeDropKind` and `TreeDropLogic` |21| `Tool/TreeViewPlugin.Core/TreeIconCatalog.cs` | Icon index → artwork path + theme color key, shared by both heads' registries |22| `Tool/TreeViewPlugin.Core/MainTreeViewPlugin.cs` | Wires plugin events to `RefreshUi(...)` and error-indicator updates; loaded by both hosts |23| `Tool/TreeViewPlugin.Core/TreeViewStateService.cs`, `CollapseToggleService.cs` | Expansion state: persisted across sessions, and the collapse-button toggle |24| `Gum/Plugins/InternalPlugins/TreeView/WpfElementTreeView.cs` | WPF panel: `GumTreeView`, search box, flat results, collapse buttons, chip palette; WPF drag/cursor glue |25| `Gum/Controls/GumTreeView.cs` (+ `.DragDrop.cs`) | WPF control: hit testing, expander clicks, drag start, drop adornment; delegates selection to `TreeSelectionModel` |26| `Gum/Themes/Frb.TreeView.xaml`, `Gum/Controls/TreeIconRegistry.cs`, `TreeNodeIcon.cs` | WPF row template and icon drawing |27| `Tool/Gum.Avalonia/Plugins/TreeView/` | Avalonia panel (`AvaloniaElementTreeView`), row-list control (`AvaloniaGumTreeView`), palette, icons |28| `Tools/Gum.Presentation/Services/RefreshCoalescer.cs` | Collapses N `RequestRefresh()` calls in one synchronous burst into a single `IDispatcher`-posted refresh |2930The states tree (center-top "States" tab) shares `StateTreePluginBase`, `StateTreeRightClickService`31and `StateTreeKeyboardHandler` in `Tools/Gum.Presentation/Plugins/InternalPlugins/StatePlugin/`;32`MainStatePlugin` (WPF) and `AvaloniaStatePlugin` only build their tree control.3334`ElementTreeViewManager` and its `RightClick` partial speak `ITreeNode`/`ITreeNodeMutable`, delegating35to headless twins in `Tools/Gum.Presentation/Managers/` (`TreeNodeImageLogic`, the `TreeNode*Extensions`36families, `TreeNodeExpansionPaths`). Prefer adding logic there over growing the manager.3738## Selection is on the model, not the container3940`TreeView` enforces a single selected item and clears the previous one on every change, so41`TreeViewItem.IsSelected` is deliberately never set. `TreeSelectionModel` tracks the selection and the42row visuals bind to `GumTreeNode.IsSelected`. The Avalonia tree goes further and is a flat,43virtualized row list rather than a `TreeView`. Consequences:4445- Keyboard navigation is `TreeSelectionModel.HandleKeyDown`, called from each control's key handler.46- A change to selection behavior goes in `TreeSelectionModel` (with a test), never in one head's control.47- `IsExpanded` is ordinary two-way bound state, so expansion survives a rebuild without being48 captured and replayed.4950## Icons5152`TreeIconRegistry` maps an index (the shared `TreeNodeImageIndices` constants, produced by the53headless `TreeNodeImageLogic`) to a pack URI plus a theme color key; `TreeNodeIcon` renders the pair.5455- **Source PNGs must be white-on-transparent**, alpha carrying the shading. Tinting fills a shape with56 the theme brush and uses the artwork as an `OpacityMask`, so a colored source multiplies wrong.57- Adding an icon is a constant in `TreeNodeImageIndices` plus an entry in `TreeIconRegistry` — in any58 position. The numbering is not tied to load order.59- Icons re-tint on theme change via `TreeIconRegistry.NotifyThemeChanged()`; nothing is regenerated.6061## Refresh model6263`RefreshUi()` is diff-based — existing nodes are reused and only differing `ImageIndex`/position/`Tag`/64`Text` are written. Replacing nodes wholesale would drop selection and scroll position.6566`Tag` distinguishes node kinds: folder/container nodes have `Tag == null`; element nodes carry an67`ElementSave`/`BehaviorSave`; instance nodes an `InstanceSave`.6869`MainTreeViewPlugin.HandleElementImported` requests a refresh through a `RefreshCoalescer` rather than70calling `RefreshUi()` directly, so importing N elements in one batch (Forms theme, `.gumx` import)71produces one refresh instead of N.7273## Gotchas7475- **Reordering within one collection must be remove-then-insert.** `GumTreeNodeCollection` throws if a76 node is inserted into the collection it already belongs to, because detaching first would shift the77 index the caller computed. Reparenting *across* collections is a plain add.78- **Drag payloads travel in `TreeDragPayload`, not on the drag data.** Gum's `*Save` types aren't79 `[Serializable]`, so anything put on a WPF data object comes back null; the data carries only a80 marker format (`TreeDragPayload.DataFormat` in WPF, `AvaloniaDragFormats.TreeNodes` in Avalonia).81 Both heads' canvas drop readers and search-result lists use the same static.82- **`ITreeNode.FullPath` is backslash-separated.** `CopyPasteLogic` slices a `"Components\\"` prefix83 off it.84- **Persisted expansion state is forward-slash-joined node `Text` paths** (`TreeNodeExpansionPaths`).85 Changing either the separator or the use of `Text` silently discards every user's saved state.86- **Virtualization is off** (the WPF `TreeView` default). That is what makes87 `GumTreeView.ContainerFor`/`EnsureVisible` reliable — turning it on would break container lookup for88 off-screen nodes.89- **`GumTreeView.EnsureVisible` defers to a `Loaded` dispatcher callback**, since a newly-expanded90 ancestor's child has no container until layout runs.