gum-tool-selection
$
npx mdskill add vchelaru/Gum/gum-tool-selectionManage complex selection gestures and instance locking in Gum editors.
- Enables agents to handle drag, resize, rotate, and polygon editing tasks.
- Depends on SelectionManager, InputHandlerBase, and InstanceSave.Locked.
- Coordinates gestures through specific handlers like MoveInputHandler.
- Delivers precise selection states to wireframe editors and plugins.
SKILL.md
.github/skills/gum-tool-selectionView on GitHub ↗
---
name: gum-tool-selection
description: Reference guide for Gum's editor selection system. Load this when working on click/drag selection, the rectangle/marquee selector, input handlers (move, resize, rotate, polygon points), the IsActive flag, locked instance behavior, SelectionManager coordination, or the selection event cascade (plugin events, forced default state, tree view sync).
---
# Gum Editor Selection System Reference
## Overview
Selection in the wireframe (XNA) editor is coordinated by `SelectionManager`. It delegates specific interactions to a set of **input handlers**, each responsible for one type of gesture (move, resize, rotate, polygon point editing). A separate **rectangle selector** handles marquee/rubber-band multi-selection. Locking (`InstanceSave.Locked`) cuts across all of these.
## Input Handlers
**Base class:** `Tool/EditorTabPlugin_XNA/Editors/Handlers/InputHandlerBase.cs`
Each handler represents one interaction mode. Concrete handlers:
| Handler | File | Responsibility |
|---------|------|----------------|
| `MoveInputHandler` | `Handlers/MoveInputHandler.cs` | Drag-to-move selected instance(s) |
| `ResizeInputHandler` | `Handlers/ResizeInputHandler.cs` | Resize handle dragging |
| `RotationInputHandler` | `Handlers/RotationInputHandler.cs` | Rotation handle dragging |
| `PolygonPointInputHandler` | `Handlers/PolygonPointInputHandler.cs` | Polygon vertex select/move/add/delete |
### Handler Lifecycle
```
Mouse down → HandlePush(x, y) → returns true to claim gesture; sets IsActive = true
Mouse drag → OnDrag() → only meaningful when IsActive; applies transform
Mouse up → OnRelease() → cleans up; resets IsActive to false
```
`HandlePush` returns `bool`: `true` means this handler claims the gesture and sets `IsActive = true`; `false` passes to the next handler or the rectangle selector.
### `IsActive` Flag
`IsActive = true` signals that a handler owns the current drag gesture. It suppresses the rectangle selector — `SelectionManager` passes `isHandlerActive = true` to `RectangleSelector.HandleDrag`, which returns immediately. Must be set in `HandlePush` when claiming a gesture and reset in `OnRelease`.
The base class `HandlePush` automatically checks `Context.IsSelectionLocked()` and returns `false` if locked. Handlers that **override** `HandlePush` must replicate or explicitly call this check.
## Rectangle Selector (Marquee Selection)
**File:** `Tool/EditorTabPlugin_XNA/RectangleSelector.cs`
The rectangle selector activates on drag when no handler is active and the cursor is not over the element body (or Shift is held for additive selection), after a minimum drag distance is exceeded. `SelectionManager` passes `isHandlerActive` based on whether any handler's `IsActive` is `true`.
`GetElementsInRectangle()` finds visible elements whose bounds intersect the drag rectangle, skipping `ScreenSave` elements and instances where `Locked == true`. On release, it either replaces the selection or toggles additively (Shift held).
## Locking (`InstanceSave.Locked`)
`InstanceSave.Locked` is defined in `GumDataTypes/InstanceSave.cs`. The helper `EditorContext.IsSelectionLocked()` (in `Tool/EditorTabPlugin_XNA/Editors/EditorContext.cs`) returns `true` when the selected instance is locked.
### Where Locking Is Enforced
| Location | File | What It Prevents |
|----------|------|-----------------|
| `InputHandlerBase.HandlePush()` | `InputHandlerBase.cs` | Base lock check; handlers that don't override inherit this |
| `PolygonPointInputHandler.HandlePush()` | `PolygonPointInputHandler.cs` | Overrides base; manually checks lock before allowing vert select/add |
| `PolygonPointInputHandler.TryHandleDelete()` | `PolygonPointInputHandler.cs` | Prevents DEL key from deleting verts |
| `PolygonPointInputHandler.UpdateHover()` | `PolygonPointInputHandler.cs` | Hides the "add point" sprite on polygon edges |
| `ElementCommands.MoveSelectedObjectsBy()` | `Gum/ToolCommands/ElementCommands.cs` | Skips locked instances in multi-selection moves |
| `ResizeInputHandler.ApplySizeChange()` | `ResizeInputHandler.cs` | Skips locked instances during resize |
| `MoveInputHandler.ApplyAxisLockIfNeeded()` | `MoveInputHandler.cs` | Skips locked instances during axis-lock correction |
| `MoveInputHandler.ApplyAxisLockToSelectedState()` | `MoveInputHandler.cs` | Skips locked instances when writing axis-lock to state |
| `MoveInputHandler.SnapSelectedToUnitValues()` | `MoveInputHandler.cs` | Skips locked instances during snap-to-unit |
| `RectangleSelector.GetElementsInRectangle()` | `RectangleSelector.cs` | Excludes locked instances from marquee results |
| `SelectionManager.ReverseLoopToFindIpso()` | `SelectionManager.cs` | Prevents click-selection of locked instances on canvas |
| `ListBoxDisplay` (variable grid) | `WpfDataUi/Controls/ListBoxDisplay.xaml.cs` | Disables Add/Delete/Edit in list variables (e.g. polygon Points) |
### Locked + IsActive Interaction (Critical)
When a locked instance is selected and the cursor is over one of its polygon verts, `PolygonPointInputHandler.HandlePush` must: detect the vert, set `IsActive = true` (to suppress the rectangle selector), but not set `_grabbedIndex` (so `OnDrag` is a no-op), and return `true` to consume the push. Without setting `IsActive`, the rectangle selector activates on drag because the cursor over a vert is typically not "over body".
### Locked Selection Display
`LockedSelectionVisual` draws a dashed bounding rectangle for a locked selected instance, replacing the resize handles that would normally appear. It shows regardless of the instance's `Visible` property. Registered in `StandardWireframeEditor`; not used in `PolygonWireframeEditor`.
### Locked Instances Are Still Tree-Selectable
Locked instances cannot be canvas-clicked or rectangle-selected, but **can always be selected via the tree view** — the only way to select a locked instance to unlock it. Multi-selection of mixed locked/unlocked is supported; transforms apply only to unlocked members.
## `_lastPushWasOnLockedBody`
Tracked in `SelectionManager.ProcessInputForSelection()` — set to `true` when the selected instance is locked and the cursor is over the body. Used in `ProcessRectangleSelection()` to prevent deselection when the user releases the mouse over a locked body without dragging.
## Selection Event Cascade
When the user selects an instance (via tree view or wireframe), `SelectedState` orchestrates a synchronous cascade of plugin events:
```
User selects instance
→ SelectedState.HandleSelectedInstances()
→ PerformAfterSelectInstanceLogic()
→ SelectedStateSave = element.States[0] (forced default state)
→ PluginManager.ReactToStateSaveSelected() ← fires FIRST
→ PluginManager.InstanceSelected() ← fires SECOND
```
**Key behaviors:**
- State selection fires BEFORE instance selection (from inside `PerformAfterSelectInstanceLogic`). State is only force-selected when the current state doesn't belong to the new element (checked via `AllStates.Contains`).
- Both events trigger `RefreshEntireGrid` in `MainVariableGridPlugin`. A `_stateJustRefreshedGrid` flag prevents the double refresh — set by `HandleStateSelected`, checked and consumed by `HandleInstanceSelected`.
- `MainTreeViewPlugin` responds to `InstanceSelected` by syncing the tree view node. It sets `SuppressCallAfterClickSelect` on `ElementTreeViewManager` so the `Select` methods update the visual tree node without re-firing `CallAfterClickSelect`, which would cause a redundant plugin cascade.
**`IsInUiInitiatedSelection` vs `SuppressCallAfterClickSelect`:** `IsInUiInitiatedSelection` is set during `OnSelect` to prevent programmatic `Select` calls from re-entering while the tree view processes a user-initiated selection — but it's cleared before plugin events fire, so it doesn't prevent the `MainTreeViewPlugin` sync path. `SuppressCallAfterClickSelect` handles that case specifically.
### Cascade Key Files
| File | Purpose |
|------|---------|
| `Gum/ToolStates/SelectedState.cs` | `HandleSelectedInstances`, `PerformAfterSelectInstanceLogic`, `HandleStateSaveSelected` |
| `Gum/Plugins/PluginManager.cs` | `InstanceSelected`, `ReactToStateSaveSelected` event dispatch |
| `Gum/Plugins/InternalPlugins/TreeView/MainTreeViewPlugin.cs` | Tree view sync with `SuppressCallAfterClickSelect` |
| `Gum/Plugins/InternalPlugins/TreeView/ElementTreeViewManager.cs` | `Select` methods, `CallAfterClickSelect`, both suppression flags |
| `Gum/Plugins/InternalPlugins/VariableGrid/MainVariableGridPlugin.cs` | `_stateJustRefreshedGrid` double-refresh guard |
## Key Files Summary
| File | Purpose |
|------|---------|
| `Tool/EditorTabPlugin_XNA/SelectionManager.cs` | Main coordinator; manages `IsOverBody`, routes events to handlers, passes `isHandlerActive` to rectangle selector |
| `Tool/EditorTabPlugin_XNA/RectangleSelector.cs` | Marquee selection; activation gated on `isHandlerActive` and `IsOverBody` |
| `Tool/EditorTabPlugin_XNA/Editors/Handlers/InputHandlerBase.cs` | Base class; provides default `HandlePush` with lock check |
| `Tool/EditorTabPlugin_XNA/Editors/Handlers/MoveInputHandler.cs` | Move gesture; also handles axis lock and snap-to-unit for multi-selection |
| `Tool/EditorTabPlugin_XNA/Editors/Handlers/ResizeInputHandler.cs` | Resize handle gestures |
| `Tool/EditorTabPlugin_XNA/Editors/Handlers/PolygonPointInputHandler.cs` | Polygon vertex editing; overrides `HandlePush` (must manage lock manually) |
| `Tool/EditorTabPlugin_XNA/Editors/EditorContext.cs` | Provides `IsSelectionLocked()` helper used throughout handlers |
| `Tool/EditorTabPlugin_XNA/Editors/Visuals/LockedSelectionVisual.cs` | Dashed bounding outline for locked selected instances; display-only, no interaction |
| `GumDataTypes/InstanceSave.cs` | `Locked` property definition |
| `Gum/ToolCommands/ElementCommands.cs` | `MoveSelectedObjectsBy()`; skips locked instances in multi-move |
| `WpfDataUi/Controls/ListBoxDisplay.xaml.cs` | Variable grid list control; respects `IsReadOnly` (driven by `Locked`) |