github go-gui-org/go-gui v0.87.0

latest releases: v0.94.0, v0.93.0, v0.92.0...
one month ago

Added

  • ergonomics-audit -mode pointeramend flags AmendLayout hooks that read
    the pointer without pointerAmend (#978).
    Idle moves skip the rebuild when
    the last arranged frame proves nothing reacts to them (#973). A hook that
    tests the pointer position against its bounds must mark its container
    pointerAmend, or a move across its bounds is skipped and the hook never sees
    it. The mode scans gui/ for hooks that read mousePosX, mousePosY,
    pointerX or pointerY, directly or through a same-package wrapper, and
    fails when the install sets no pointerAmend and the read carries no
    // ergonomics-audit:pointeramend marker with a reason. rtfAmendTooltip
    carries the marker: it reads only while tooltip text is set, and set tooltip
    state forces a rebuild on every move. make check runs it.

  • ContainerCfg.Hover paints a hover fill and cursor with no OnHover
    (#977).
    Set
    Hover: gui.HoverStyle{Color: c, Cursor: gui.CursorPointingHand} in place of
    an OnHover that only sets the shape's color or the cursor. gui paints the
    style while the pointer is over the shape, so no app code runs and a pointer
    move inside the shape needs no layout rebuild. The container also allocates no
    hover closure per frame. With an OnHover set as well, the style paints first
    and OnHover runs after it; that shape still rebuilds on every move inside
    it, because OnHover can read the pointer position. Data grid rows and
    sortable header cells now use it. The grid also dropped its own OnMouseMove:
    it reads which header column is hovered with IsHovered while it builds. A
    pointer moving over a data grid no longer rebuilds the frame unless the
    hovered row or column changes. One side effect: a non-sortable header cell has
    no hover look, so it no longer takes the hover. An OnHover on a container
    around the grid now fires while the pointer is over such a cell, as it already
    did over the grid's other parts that have no hover.

  • GOGUI_DEVICE_SCALE and GOGUI_EMULATE_CLIPBOARD for testing (#971).
    GOGUI_DEVICE_SCALE=2 makes the backend use that device scale in place of the
    monitor's: a number from 0.25 to 8. The window keeps its logical size and
    renders with that many pixels per point, so 2x layout and text bugs show on a
    1x monitor. On macOS and the web the system resamples the result into the same
    window area. On X11, Wayland and Windows the window gets the matching number
    of physical pixels. A Wayland compositor with no viewport takes only whole
    scales, so there the value is rounded to a whole number. A value that is not a
    number in that range is ignored. iOS and Android are not covered.
    GOGUI_EMULATE_CLIPBOARD=1 keeps the clipboard and the PRIMARY selection in
    each window: SetClipboard, GetClipboard, SetPrimary and GetPrimary use
    them, and the backend's clipboard is never called, so tests and manual runs do
    not change the system clipboard. GOGUI_CLIPBOARD_TEXT sets the text the
    emulated clipboard holds at the start. Both variables are read at startup and
    add no exported API.

  • GOGUI_DEBUG_REBUILDS=1 turns on DebugRebuilds (#975). The variable is
    read at startup, so an existing app logs why its frames rebuild with no code
    change. Before, DebugRebuilds needed a gui.DebugCategories call in main,
    because GOGUI_DEBUG=1 leaves it out of DebugAll. The two variables
    combine: GOGUI_DEBUG=1 GOGUI_DEBUG_REBUILDS=1 turns on both.

  • Screen readers work on Windows (#944). The OpenGL backend now has a UI
    Automation provider, so Narrator and NVDA read a go-gui window instead of an
    empty one. Each accessible node becomes a UIA element with its control type,
    name, description, state and screen bounds. Focus changes, value changes and
    check, selection and expand state changes are raised as UIA events. Buttons,
    links and menu items take Invoke. Checkboxes and switches take Toggle. Radio
    buttons, tab items, list items and tree items take SelectionItem, in the
    Selection of their list, tree, tab list or radio group. Sliders take
    RangeValue; SetValue moves one step toward the requested value. Text fields,
    combo boxes and date fields take Value, read-only, because text arrives as
    keys. Combo boxes, disclosures and tree items take ExpandCollapse. A button
    whose label is a child text node takes that text as its name. A11yAnnounce
    and live regions raise a UIA notification (Windows 10 1709 and later). The
    provider is pure Go with no cgo. Two assembly thunks, for amd64 and arm64,
    cover the two methods that take doubles. Other Windows architectures still
    have no accessibility. Adapted from mygo's MIT provider; the notice is in
    THIRD_PARTY_NOTICES.

  • DebugRebuilds logs why a frame rebuilds (#970). A new debug category
    prints the cause of each rebuild, such as gui: rebuild layout: input or
    gui: rebuild render: svg. A line prints only when the causes or the kind of
    pass change, so a steady stream prints once. The view phase allocates on every
    full rebuild; this shows which request causes it. It is not in DebugAll.
    Turn it on with gui.DebugCategories(gui.DebugAll | gui.DebugRebuilds). The
    cause is a kind, not a call site: every InvalidateLayout caller reports
    invalidate. Recording the cause costs no allocation while the category is
    off.

Fixed

  • A sideways touchpad swipe or tilt wheel scrolls horizontally on Windows and
    X11 (#990).
    Win32 delivers both as WM_MOUSEHWHEEL, and X11 delivers tilt
    as buttons 6/7. Each arrives as a discrete scroll with ScrollX and no
    modifier. The scroll container eased only ScrollY for such an event, so a
    wide container ignored the gesture and moved only with the scrollbar. Each
    axis the event names now eases, as far as the container's ScrollMode allows,
    the same rule #585 set for precise scrolls. Windows also had the sign of the
    sideways delta reversed, so a swipe to the right scrolled left; it now matches
    X11 and the browser. Shift+wheel also scrolls sideways there now: Win32, X11
    and Wayland report it as ScrollY with Shift held, while the container read
    only ScrollX, so Shift+wheel did nothing. With Shift held, a discrete-wheel
    ScrollY delta now scrolls horizontally when ScrollX is 0. In a container
    whose ScrollMode is ScrollVerticalOnly, a discrete Shift+wheel now scrolls
    vertically on every platform. A precise (trackpad) ScrollY keeps its axis,
    so Shift plus a vertical swipe does not scroll sideways. macOS and browsers
    already move the wheel's delta to ScrollX, so they are unchanged.

  • GOGUI_DEVICE_SCALE: Win32 size limits and large X11 windows (#984). On
    Windows, MinWidth/MaxWidth and the other size limits were scaled by the
    monitor DPI, not by the override, so the resize bounds did not match the
    logical limits. They now use the same scale as the content. On X11, a large
    Width at a high scale (9000 at scale 8) wrapped in the 16-bit window size to
    a small or zero window. The physical size is now clamped to 1–32767.

  • A RotatedBox child stays hoverable along its full length (#976). Inside
    a 90° or 270° box, the clipping bound was intersected in the screen frame
    while the child lives in the unrotated frame, so only the center square where
    the frames overlap took hover and clicks. The bound is now carried into the
    unrotated frame before the intersection.

  • OnMouseLeave no longer misses a leave after frames that arrange nothing
    (#973).
    The hover record counted frames. A frame that ran no arrange pass,
    such as a caret blink, made a real hover look stale, so moving out of the
    shape after a few such frames fired no leave. The record now counts arrange
    passes, so only a pass that ran can age it.

  • Text shortcuts use Cmd on macOS and Ctrl elsewhere; macOS gets the Cocoa
    Emacs keys (#969).
    Text widgets used to accept Ctrl or Cmd for select all,
    copy, cut, paste and undo on every platform. On macOS, Ctrl+A selected all
    text, but every native text field moves the caret to the start of the line. On
    Linux and Windows, the Super key also triggered shortcuts. The modifier now
    comes from the platform. On macOS and iOS, Cmd triggers shortcuts,
    Option+Arrow moves by word, and Cmd+Arrow moves to the line or document edge.
    Input, Text and RichText selection also take the Cocoa Emacs keys: Ctrl+A/E
    (paragraph start/end), Ctrl+F/B/N/P (caret moves), and in Input Ctrl+D/H
    (delete), Ctrl+K (cut to the paragraph end into a kill buffer, not the
    clipboard) and Ctrl+Y (paste the kill buffer). On every other platform, only
    Ctrl triggers shortcuts, and Ctrl or Alt with an arrow moves by word.
    Markdown, Dialog, FileBrowser and DataGrid follow the same rule. The new
    KeyBindingMode (KeyBindingCommand, KeyBindingControl) and
    ShortcutModifier() expose the mode. The web backend reads the browser's OS,
    so a Mac browser gets Cmd. Set GOGUI_KEY_BINDING_MODE=command or control
    to try the other platform's keys. Tests that send ModCtrl as the shortcut on
    a macOS machine must pin the mode with
    gui.SetKeyBindingMode(gui.KeyBindingControl) in TestMain.

  • An accessibility action redraws the window (#944). A screen reader's
    press, increment or decrement ran the widget's handler from the command queue,
    which requested no layout refresh. The app state changed, but the window
    showed the old view until some later input arrived. The action now requests a
    refresh, as a mouse click does. This affects every platform: a VoiceOver or
    Orca press now updates the window at once.

  • A screen reader Increment on a slider raises its value (#964). Increment
    synthesized KeyUp and Decrement KeyDown, but the slider read Up as
    decrease, so a raise moved the value down: UIA RangeValue.SetValue(max) on a
    slider at 50 moved it to 49, and VoiceOver and AT-SPI share the path. The
    slider now follows the WAI-ARIA pattern, like InputNumeric: Up and Right
    increase, Down and Left decrease.

Changed

  • A plain Text allocates 25% fewer bytes. Each Text view carried 176
    bytes of rich-text fields it never used, which put it in the 768-byte size
    class. Those fields now sit behind a pointer that only rich text sets, so a
    plain Text view is 576 bytes. Text-heavy frames allocate 7–10% fewer bytes:
    a 200-button frame drops from 274 KB to 249 KB, a tree frame from 722 KB to
    647 KB. Allocation counts do not change, and rich text and markdown allocate
    the same as before.

  • A virtualized DataGrid frame no longer allocates per row of data. The
    scroll body sized its views slice for every row, though it builds only the
    visible ones, and the grid built a fresh list of every row index each frame.
    On a 100,000-row grid showing 30 rows that was 2.3 MB of garbage per frame.
    The views slice now follows the visible window, and the row index list is cut
    from one list the window keeps. A frame now allocates the same 439 KB at
    100,000 rows as at 2,000. The cache check that hashes every row index now
    mixes each index in one step, not eight, which cuts frame time at 100,000 rows
    by 39% (1.57 ms to 0.96 ms). Rendering, selection and keyboard navigation do
    not change.

  • DataGrid rows allocate 42% less. Each cell built its own Content slice
    and its own ID string, and each row built the editor focus ID twice and a
    click closure that copied a dozen grid values. Now a row shares one Content
    backing array and one ID string across its cells, and the grid builds the
    click state and the editor focus ID once per frame. On 8 columns a visible row
    costs 22 allocations per frame instead of 38. A frame of a grid showing 30
    rows drops from 1,441 to 884 allocations and runs 4% faster. Cell IDs,
    selection, editing and rendering do not change.

  • ListBox, Tree, Combobox and CommandPalette rows allocate less.
    Each row built closures and a slice: an OnHover that set the hover fill and
    cursor, an OnClick that held the row's data, and a Content slice. Rows now
    set ContainerCfg.Hover (#977), and gui paints the same fill and cursor.
    Every row now uses one shared click handler, which finds its row through the
    shape. ListBox and Tree rows take their Content slice from the frame
    arena. A Tree row also stopped copying itself into a map and into its click
    closure. Allocations per row per frame:

    Row Before After
    ListBox 6 3
    ListBox reorder 8 4
    Tree 11.25 5.25
    Tree reorder 14.75 7.38

    A 200-node Tree builds a frame in half the allocations and 13–20% less time.
    Combobox and CommandPalette rows drop from 5 to 3, paid when the query or
    scroll changes; those widgets cache their rows between frames. A virtualized
    Tree also sized its row list by every expanded row, about 16 bytes per row
    of data each frame; it now follows the visible window. Hover, click, drag and
    focus behavior do not change.

  • go-glyph v1.26.3. Two fewer heap allocations per text layout and per glyph
    load. golang.org/x/image, x/sys, x/text and x/tools move to the
    versions go-glyph builds with.

  • Selected Radio draws a center dot; unselected Radio is an empty well.
    A selected radio was a solid accent disc. It looked like a status light, not a
    radio. It now has a dot in the center, 40% of the diameter, in
    ColorTextOnSelect, so it looks like the macOS and Fluent radios. An
    unselected radio now uses the ColorPanel fill, the same as Checkbox, and
    not ColorActive. Before, it looked like a filled grey dot. A borderless
    theme (WithBorders(false)) keeps ColorActive, because a panel-colored well
    with no border cannot be seen on a panel. When you set your own
    RadioCfg.ColorSelect, the dot is black or white, whichever contrasts with
    that color. A NaN, infinite or negative RadioCfg.Size now uses the theme
    size. The size and layout do not change. This also changes RadioButtonGroup.

  • BREAKING: a pointer move that changes nothing no longer rebuilds the frame
    (#973).
    Before, every input event asked for a full layout rebuild, so a
    pointer crossing an idle window ran the view phase, and its allocations, on
    every frame. Now a mouse move, or a one-finger touch drag under the pan
    threshold, asks for no rebuild when the last arranged frame proves nothing
    reacts to it: the IsHovered target is the same, no OnMouseMove shape is
    under the pointer, no drag holds the mouse, no OnHover or OnMouseLeave
    shape is entered or left, and no tooltip is pending. Moving inside a Button, a
    list, listbox, tree, select or menu row skips the rebuild too; their hover
    look depends only on being hovered. An app OnHover may read the pointer
    position, so moving inside a shape that has one still rebuilds, and so does
    every move while a Window.OnEvent handler is set. The pointer position is
    still recorded, so the next rebuild hit-tests where the pointer is now. What
    this changes for an app: a view that reads changing state without asking for a
    refresh (a clock, data from another goroutine) no longer catches up when the
    mouse moves. Call InvalidateLayout when the state changes, or run an
    animation. DebugRebuilds reports the moves it skipped, as
    gui: skipped N idle moves, in front of the next pass.

  • A minimized X11 window draws no animation frames (#954). The X11 backend
    now reports occlusion: iconify unmaps the window, and its UnmapNotify stops
    animation frames the way a minimize already does on macOS and Windows (#943).
    MapNotify on restore draws one full frame at the animations' current phase.
    Only minimize counts; a window covered by others still draws, because
    compositing window managers report every window as visible.

  • A hidden Wayland window draws no animation frames (#953). The Wayland
    backend reads the suspended state of xdg-shell v6, which a compositor sets
    on a window it shows nowhere: minimized, on another workspace, or behind a
    locked screen. A suspended window stops waking the main thread every 16 ms
    while an animation runs; the configure that drops suspended draws one full
    frame at the animations' current phase. Both the plain xdg_toplevel path and
    the libdecor path (libdecor 0.2 or later) report it. A compositor before v6
    never sends the state, so its windows draw as before.

  • A covered Wayland window draws no animation frames (#959). Compositors
    stop answering frame callbacks for a surface they do not paint. A callback
    still outstanding after one second now marks the window occluded, so a window
    that others cover completely stops waking the main thread every 16 ms and
    stops the once-a-second fallback render it used to get. The callback's done,
    which the compositor sends when it paints the window again, draws one full
    frame at the animations' current phase. This also covers compositors without
    xdg-shell v6 suspended, such as muffin 6.6. It applies with
    GOGUI_WAYLAND=1; X11 has no reliable signal for a covered window.

  • A button's per-frame colors no longer allocate (#962). Every Button, and
    every widget built on it (tabs, segmented controls, date pickers, dialogs,
    toasts), put its state colors on the heap once per frame, although they come
    from a frame pool. A nil-window fallback took their address, and that moved
    them to the heap on every call. Now generating a built button allocates
    nothing.

  • A scrollable container's scrollbars cost 2 allocations per frame, not 14.
    Every scrollable container builds two stock scrollbars, one per axis, on every
    frame, even when nothing overflows. Each bar was a Row or Column with a thumb
    child and four closures, and each closure held its own copy of the scrollbar
    settings: 7 allocations per bar. A stock bar is now one view that builds both
    shapes itself, and its handlers are shared functions that read the settings
    from the shape. ListBox, VirtualList, Combobox, CommandPalette and
    every other scrolling widget save 12 allocations per frame. What the bars draw
    and how they respond does not change. A bar with a Thumb or Track hook is
    built as before.

  • BREAKING: Up and Right raise a slider; Down and Left lower it (#964). Up
    used to decrease the value and Down used to increase it. Up now increases,
    matching InputNumeric and the WAI-ARIA slider pattern. Horizontal sliders
    need no code change: only the key directions swap. On a vertical slider, Down
    now lowers the value although dragging down still raises it, because the
    vertical track runs from the minimum at the top.

What's Changed

Bug fixes

  • ergonomics-audit: normalize window paths by @ollex in #965
  • fix(gui): sideways touchpad, tilt wheel and Shft+wheel scroll on Windows and X11 by @ollex in #990

Performance

  • test(soft): stabilize keypress allocation gate by @ollex in #966

Other changes

  • gl/x11: report occlusion so minimized windows stop animation frames (#954) by @mike-ward in #958
  • gl/wayland: report suspended windows as occluded (#953) by @mike-ward in #960
  • gl/wayland: treat a stalled frame callback as occlusion (#959) by @mike-ward in #961
  • perf(gui): stop per-frame heap alloc of button colors by @mike-ward in #962
  • gl/windows: UI Automation provider for screen readers (#944) by @mike-ward in #963
  • docs(showcase): correct Table doc to the real API by @mike-ward in #968
  • fix(gui): split Ctrl and Cmd in text widgets, add Cocoa Emacs keys (#969) by @mike-ward in #972
  • feat(gui): DebugRebuilds logs why a frame rebuilds (#970) by @mike-ward in #974
  • perf(gui): idle pointer moves skip the layout rebuild (#973) by @mike-ward in #979
  • feat(gui): GOGUI_DEBUG_REBUILDS env var turns on DebugRebuilds (#975) by @mike-ward in #980
  • Up and Right raise the slider; a11y Increment raises (#964) by @mike-ward in #981
  • fix(gui): RotatedBox child hoverable along full length (#976) by @mike-ward in #982
  • feat(gui): GOGUI_DEVICE_SCALE and GOGUI_EMULATE_CLIPBOARD for testing (#971) by @mike-ward in #983
  • perf(gui): ContainerCfg.Hover paints hover fill and cursor (#977) by @mike-ward in #985
  • audit(ergonomics): flag AmendLayout hooks that read the pointer without pointerAmend (#978) by @mike-ward in #986
  • fix(gl): GOGUI_DEVICE_SCALE Win32 size limits and X11 window size wrap by @mike-ward in #987
  • build: bump x/tools to v0.50.0, pin gosec for go1.27.2 by @mike-ward in #988
  • feat(gui): Radio draws a center dot when selected by @mike-ward in #989
  • docs(claude): move durable rules from agent memory into CLAUDE.md by @mike-ward in #991
  • build(deps): bump go-glyph to v1.26.3 by @mike-ward in #993
  • perf(gui): cut per-frame allocs in scrollbars, Text a11y and showcase catalog by @mike-ward in #992
  • perf(gui): cut per-row allocs in ListBox, Tree, Combobox and CommandPalette rows by @mike-ward in #994
  • perf(datagrid): cut per-row allocs and make frame cost independent of row count by @mike-ward in #995
  • perf(gui): move rich-text fields out of plain Text shapes by @mike-ward in #996
  • changelog: add v0.87.0 by @mike-ward in #997

Full Changelog: v0.86.0...v0.87.0

Don't miss a new go-gui release

NewReleases is sending notifications on new releases.