Skip to content

Focus and Scrolling

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

Focus and Scrolling

Go-gui uses integer IDs — not object references — to track focus and scroll state across frames. This is the right design for an immediate-mode system: widget objects don't persist between frames, so there's nothing to hold a reference to. The IDs are the stable handles.


Why IDs?

Each frame the view function runs from scratch and returns a new tree. The framework has no way to identify "the same TextInput as last frame" by pointer. Instead, the widget registers an integer ID, and the framework uses that ID as a key into per-window state maps for focus position, scroll offsets, cursor state, and so on.

The rule is: assign an ID to any widget that needs keyboard focus or scroll tracking. The value determines tab order — lower values are focused first when the user presses Tab.


IDFocus

IDFocus uint32 appears on widget config structs. Set it to a non-zero value to make the widget keyboard-accessible.

gui.TextInput(gui.TextInputCfg{
    ID:      "search",
    IDFocus: 1,          // Tab lands here first
    // ...
})

gui.Button(gui.ButtonCfg{
    ID:      "submit",
    IDFocus: 2,          // Tab moves here second
    // ...
})

Reading and setting focus programmatically:

// Read which widget currently has focus
current := w.IDFocus()

// Move focus to a specific widget
w.SetIDFocus(2)

Focus auto-scrolls into view: if the focused widget is inside an IDScroll container, the framework scrolls it into the visible area automatically.


IDScroll

IDScroll uint32 marks a container as scrollable. The framework stores the scroll offset in a per-window state map keyed by that ID.

gui.OverflowPanel(gui.OverflowPanelCfg{
    ID:       "log-panel",
    IDScroll: 300,        // non-zero; value is arbitrary but must be unique per window
    Sizing:   gui.FillFixed,
    Height:   300,
    Content:  logViews,
})

The scroll value is negative (offset from the top/left), clamped so the content never scrolls beyond its bounds.


Scroll API

All scroll methods are on *gui.Window and take the IDScroll value as their first argument.

Vertical

Method Effect
w.ScrollVerticalBy(id, delta) Scroll by delta pixels
w.ScrollVerticalTo(id, offset) Scroll to offset (negative value, 0 = top)
w.ScrollVerticalToPct(id, pct) Scroll to percentage; 0.0 = top, 1.0 = bottom
w.ScrollVerticalPct(id) Read current vertical position as percentage

Horizontal

Method Effect
w.ScrollHorizontalBy(id, delta) Scroll by delta pixels
w.ScrollHorizontalTo(id, offset) Scroll to offset (negative, 0 = left)
w.ScrollHorizontalToPct(id, pct) Scroll to percentage; 0.0 = left, 1.0 = right
w.ScrollHorizontalPct(id) Read current horizontal position as percentage

Scroll to a widget

w.ScrollToView(id string) finds the layout node with the given ID string, walks its parent chain to the nearest IDScroll container, and adjusts the vertical offset to bring the node into view:

w.ScrollToView("row-42")

Practical example — scroll to a selected item

const scrollID uint32 = 100

gui.OverflowPanel(gui.OverflowPanelCfg{
    ID:       "item-list",
    IDScroll: scrollID,
    Sizing:   gui.FillFixed,
    Height:   400,
    Content:  itemViews,
})

// In a callback that selects item 42:
OnClick: func(_ *gui.Layout, e *gui.Event, w *gui.Window) {
    a := gui.State[App](w)
    a.SelectedIndex = 42
    w.ScrollToView("row-42")   // bring it into view
    e.IsHandled = true
},

Auto-scroll behaviour

The framework scrolls automatically in two situations:

  1. Focus: when w.SetIDFocus(id) targets a widget inside a scroll container, the container scrolls to make it visible.
  2. Text cursor: in a multiline TextInput, the cursor position is tracked and the inner scroll container adjusts each keystroke so the cursor stays visible.

Both use the same parent-chain walk as ScrollToView.

Clone this wiki locally