name: gum-visual-events description: Gum runtime cursor-event dispatch — how Click/Push/RollOver/etc. are raised and routed. Triggers: InteractiveGue, DoUiActivityRecursively, RoutedEventArgs, HandledActions, ClickPreview, RollOverBubbling, ClickBubbling, HasEvents, ExposeChildrenEvents, adding/bubbling a visual event.
Gum Visual Event Dispatch
Internals of how cursor events are raised on visuals. For the user-facing event list and "what fires when" tables, see visual-events.md — don't duplicate it here.
Where it lives
| File | Role |
|---|---|
GumRuntime/InteractiveGue.cs |
All of it: event declarations, RoutedEventArgs/InputEventArgs, the HandledActions class, and DoUiActivityRecursively (the dispatch engine). |
Only types deriving from InteractiveGue raise events, and only when HasEvents == true. Forms controls layer their own events on top of these visual events (out of scope here).
The one method that matters: DoUiActivityRecursively
A single recursive walk over the visual tree drives every cursor event. It descends from the root, recurses into the deepest child under the cursor first, then unwinds. Understanding the descend-then-unwind shape is the whole game.
Three dispatch disciplines coexist (this is the non-obvious part)
The event names half-hide that three different routing models live side by side:
| Discipline | Order | Suppression | Events |
|---|---|---|---|
| Tunneling | parent → child (on the way down) | child skipped if a parent set Handled |
ClickPreview, PushPreview |
| Bubbling | deepest child → parent (on the way up) | parent skipped if a descendant set Handled |
RollOverBubbling, MouseWheelScroll, ClickBubbling |
| Single-target | fires on exactly one element | n/a — no routing | Click, Push, DoubleClick, RightClick, and most others |
These mirror WPF's Preview (tunneling) / bubbling routed-event split — same idea, same naming convention.
Gotchas
- Bubbling is NOT done by walking parent pointers. It's a single shared
HandledActionsinstance threaded through the recursion, combined withRoutedEventArgs.Handled. A handler setsargs.Handled = true; the dispatcher copies that into aHandledActionsflag (e.g.HandledRollOver), and ancestors check the flag before raising. ThatHandledActions+Handledpair is the routing machinery — reuse it to add routed events. handledByChildis the opposite of bubbling. Once a child "handles" (returns true), the parent's entire click/push block is skipped (if (!handledByChild)). That short-circuit is whyClickis single-target: a parent container does not getClickwhen a child button is clicked. Bubbling events live in a separate block that runs regardless ofhandledByChild, keyed off their ownHandledActionsflag — so they and the single-target events are independent channels.- To make an event bubble without breaking existing behavior, add a parallel
*Bubblingevent rather than changing the single-target one. Precedent:RollOverBubblingalongsideRollOver, andClickBubblingalongsideClick. Give it its ownHandledActionsflag so itsHandledonly suppresses itself on ancestors and never touches single-targetClick/Push. - A click has a push origin; rollover doesn't. Single-target
Clickonly fires wherecursor.WindowPushed == asInteractive— you must push and release on the same element. So a bubbling click can't copy theRollOverBubblingblock (which fires on anything merely under the cursor).ClickBubblingsolves this with aHandledActions.DidClickOccurflag set only when a real click resolves on its target; the bubbling pass then fires on that element and the ancestors it unwinds through. - Participation is gated. Beyond
HasEvents, an element's involvement depends onExposeChildrenEventsandIsEnabledRecursively. InputEventArgs.InputDevicecarries the cursor, not the underlying hardware device — single-target input events (Click,Push, etc.) pass theICursorthrough here.- FRB does not share
InteractiveGue. It's absent fromGumCoreShared.projitems; FlatRedBall has its own copy. Adding an event here does NOT reach FRB. If a shared Forms control (those ARE in FRB viaFlatRedBall.Forms.Shared.projitems) starts subscribing to a new visual event, FRB'sInteractiveGuemust gain the same member or FRB breaks withCS1061.