gum-visual-events

star 547

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.

vchelaru By vchelaru schedule Updated 6/12/2026

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 HandledActions instance threaded through the recursion, combined with RoutedEventArgs.Handled. A handler sets args.Handled = true; the dispatcher copies that into a HandledActions flag (e.g. HandledRollOver), and ancestors check the flag before raising. That HandledActions + Handled pair is the routing machinery — reuse it to add routed events.
  • handledByChild is 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 why Click is single-target: a parent container does not get Click when a child button is clicked. Bubbling events live in a separate block that runs regardless of handledByChild, keyed off their own HandledActions flag — so they and the single-target events are independent channels.
  • To make an event bubble without breaking existing behavior, add a parallel *Bubbling event rather than changing the single-target one. Precedent: RollOverBubbling alongside RollOver, and ClickBubbling alongside Click. Give it its own HandledActions flag so its Handled only suppresses itself on ancestors and never touches single-target Click/Push.
  • A click has a push origin; rollover doesn't. Single-target Click only fires where cursor.WindowPushed == asInteractive — you must push and release on the same element. So a bubbling click can't copy the RollOverBubbling block (which fires on anything merely under the cursor). ClickBubbling solves this with a HandledActions.DidClickOccur flag 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 on ExposeChildrenEvents and IsEnabledRecursively.
  • InputEventArgs.InputDevice carries the cursor, not the underlying hardware device — single-target input events (Click, Push, etc.) pass the ICursor through here.
  • FRB does not share InteractiveGue. It's absent from GumCoreShared.projitems; FlatRedBall has its own copy. Adding an event here does NOT reach FRB. If a shared Forms control (those ARE in FRB via FlatRedBall.Forms.Shared.projitems) starts subscribing to a new visual event, FRB's InteractiveGue must gain the same member or FRB breaks with CS1061.
Install via CLI
npx skills add https://github.com/vchelaru/Gum --skill gum-visual-events
Repository Details
star Stars 547
call_split Forks 77
navigation Branch main
article Path SKILL.md
More from Creator