Skip to content
mike-ward edited this page May 17, 2026 · 1 revision

Events

Go-gui's event system routes input events through the layout tree to the correct widget. The routing rules are simple and deterministic — no hidden event bus, no observer subscriptions, no lifecycle hooks.


The Event struct

Every event callback receives a *gui.Event. The fields relevant to a given callback depend on the event type; unused fields are zero.

type Event struct {
    // Mouse
    MouseX      float32     // cursor X relative to the receiving shape
    MouseY      float32     // cursor Y relative to the receiving shape
    MouseDX     float32     // horizontal movement delta
    MouseDY     float32     // vertical movement delta
    MouseButton MouseButton // MouseLeft, MouseRight, MouseMiddle

    // Scroll
    ScrollX float32
    ScrollY float32

    // Keyboard
    KeyCode   KeyCode
    Modifiers Modifier    // Shift, Ctrl, Alt, Super
    CharCode  uint32      // Unicode code point (EventChar only)

    // Touch
    NumTouches int
    Touches    [8]TouchPoint

    // Gesture
    GestureType  GestureType
    GesturePhase GesturePhase // Began, Changed, Ended, Cancelled

    // Misc
    Type         EventType
    FilePath     string   // EventFileDropped
    FrameCount   uint64

    // Propagation
    IsHandled bool
}

Event types

Constant When it fires
EventKeyDown Key pressed (repeats while held)
EventKeyUp Key released
EventChar Printable character typed (after OS key mapping)
EventMouseDown Mouse button pressed
EventMouseUp Mouse button released
EventMouseMove Cursor moved
EventMouseScroll Mouse wheel or trackpad scroll
EventMouseEnter Cursor entered a shape's bounds
EventMouseLeave Cursor left a shape's bounds
EventTouchesBegan Touch contact started
EventTouchesMoved Touch contact moved
EventTouchesEnded Touch contact lifted
EventGesture Recognised gesture (tap, pan, pinch, etc.)
EventResized Window resized
EventFocused Window gained focus
EventUnfocused Window lost focus
EventFileDropped File dragged onto the window
EventClipboardPasted Clipboard paste

Routing rules

The routing logic is built into the framework and runs automatically each frame.

Mouse events route to the topmost (last-drawn) shape whose bounds contain the cursor. The tree is traversed in reverse child order so later children occlude earlier ones:

Mouse click at (x, y)
  → walk children in reverse
  → first child containing (x, y)
  → deliver OnClick / OnMouseDown
  → if e.IsHandled = true, stop

Keyboard events (EventKeyDown, EventChar) route to the currently focused element. Focus is tracked by IDFocus — see Focus and Scrolling.

Scroll events route to the innermost IDScroll container whose bounds contain the cursor. If that container has no scroll handler of its own, the event propagates up to the next IDScroll ancestor.


Stopping propagation

Set e.IsHandled = true in any callback to prevent the event from being delivered to further handlers. This is the idiomatic go-gui pattern:

OnClick: func(_ *gui.Layout, e *gui.Event, w *gui.Window) {
    doSomething(w)
    e.IsHandled = true  // stop propagation
},

If you don't set IsHandled, the event continues up the tree. This is intentional for cases like a scrollable list inside a scrollable panel — the inner list handles vertical scroll, but horizontal scroll can bubble to the outer container.


EventHandlers callbacks

Event callbacks are attached to shapes via fields on the widget config struct. The full set available on EventHandlers:

Callback Trigger
OnClick Mouse button released inside shape
OnMouseDown Mouse button pressed inside shape
OnMouseUp Mouse button released (anywhere, if locked)
OnMouseMove Cursor moved
OnMouseEnter Cursor entered shape
OnMouseLeave Cursor left shape
OnMouseScroll Scroll wheel over shape
OnKeyDown Key pressed while shape is focused
OnKeyUp Key released while shape is focused
OnChar Printable character while shape is focused
OnFocus Shape gained keyboard focus
OnBlur Shape lost keyboard focus

All callbacks share the same signature:

func(layout *gui.Layout, e *gui.Event, w *gui.Window)

The layout argument is the node that received the event; e carries the event data; w is the window — the same w available in the view function.


Keyboard modifiers

e.Modifiers is a bitmask. Test individual modifiers:

OnKeyDown: func(_ *gui.Layout, e *gui.Event, w *gui.Window) {
    if e.KeyCode == gui.KeyS && e.Modifiers.Has(gui.ModCtrl) {
        save(w)
        e.IsHandled = true
    }
},

Modifier constants: ModShift, ModCtrl, ModAlt, ModSuper (⌘ on macOS).


Window-level event handling

For events not tied to a specific shape — file drops, window focus changes, quit requests — attach a handler to the window directly:

w.OnEvent = func(e *gui.Event, w *gui.Window) {
    switch e.Type {
    case gui.EventFileDropped:
        loadFile(w, e.FilePath)
    case gui.EventQuitRequested:
        if !app.Saved {
            w.Dialog(gui.DialogCfg{
                Title: "Unsaved changes",
                Body:  "Save before quitting?",
                // ...
            })
        }
    }
}

Coordinate system

Mouse coordinates in callbacks are relative to the receiving shape's top-left corner after the framework applies any rotation (via RotatedBox). If you need window-absolute coordinates, read layout.Shape.X and layout.Shape.Y from the *gui.Layout argument and add the relative offset.

Clone this wiki locally