Gum Visual Event Dispatch
SkillDev toolsGum 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Gum Visual Event Dispatch skill
What this skill tells your AI
The instructions your AI receives, as published by vchelaru/gum in .claude/skills/gum-visual-events/SKILL.md and read by ahel’s review.
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. Cursor.Activity()is edge-triggered — a secondUpdate()call in the same frame collapses the press/release edge, so Push/Click silently never fire (hover still works, it's position-only).GetEventFailureReason(MonoGameGum/Input/CursorExtensions.cs) is a structural/snapshot check and won't catch this — suspect a doubleUpdate()call when it reports no problem but clicks still don't fire.- Multiple roots are checked in reverse order, and the walk stops at the first one that claims
VisualOver.GueInteractiveExtensionMethods.DoUiActivityRecursively(IList<GraphicalUiElement>, ...)iterates the roots list back-to-front and breaks on the first root that setscursor.VisualOver. A root whose top element hasHasEvents = trueand covers a large area (e.g.Dock.Fill) claims the cursor there unconditionally, including empty space — silently blocking every root earlier in the list, not just a same-parent sibling. 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.
Signals
- GitHub stars
- 620
- Forks
- 80
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
gum-visual-events- Source
- github.com/vchelaru/gum