Added
-
Window.HTMLrenders a small, safe subset of HTML (#1158). Use it to
preview HTML that something else shows, such as a promo snippet an admin
writes for a mobile app. It takes the sameMarkdownCfgasWindow.Markdown
and draws through the same renderer, so styles, selection, links and the image
policy work the same. Supported:p,div,br,hr,h1–h6,
b/strong,i/em,u,s/del,mark,sup,sub,code,pre,
blockquote,ul/ol/li,a hrefandimg src alt width height. Other
tags are dropped and their text kept.script,styleand similar elements
are dropped with their content. Links and image sources pass the same safety
checks as Markdown, and remote images stay blocked unlessAllowRemoteImages
is set. The text goes straight into the document, so*,_,#and|
stay literal; converting HTML to Markdown text in the app changed their
meaning. There is no CSS and no script: this is not a browser. The parser is
markdown.ParseHTML. Seeexamples/html_preview/and the HTML page in the
showcase. -
Tablegets an optional row header column and a select-all corner, like the
WinFormsDataGridView(#1156). SetTableCfg.RowHeaderto put a narrow
selector cell in front of every row. A click on it selects that row. Under
MultiSelect, Ctrl+click (Cmd+click on macOS) toggles the row and Shift+click
selects a range. On the header row the cell is the corner: a click there
selects every data row. The row that was clicked or moved to last shows a ▶
marker. The cell is not part ofData, so column widths, alignments, and your
sort and copy code do not see it. Selection still goes throughSelectedand
OnSelect, and it keys on row index, so it stays stable while virtualization
scrolls rows out of view.RowHeaderWidthsets the width (default 24). A
focusable table with a row header also takes Shift+Space (select only the
current row) and Escape (clear the selection; with nothing selected, Escape
travels on). Any focusableMultiSelecttable now takes Ctrl+A (Cmd+A on
macOS) to select every data row. The showcase table demo has a "Row header"
toggle. -
A right press on an
Inputopens a Cut, Copy, Paste and Select All menu
(#1151). Before, a right press in a text field did nothing. Copy and Cut act
on the current selection, which the press keeps. Cut and Paste are off in a
ReadOnlyfield, Copy and Cut are off in a password field, and Paste is off
when the clipboard is empty. A paste from the menu goes through the field's
mask andPreTextChange, the same as Ctrl+V. The field keeps its focus look
while the menu is open, and it does not fireOnBluror a blur
OnTextCommit. Escape closes the menu and gives focus back to the field. The
menu also works in anInputinside a dialog. To use your own menu, set
InputCfg.ContextMenuDisabledand wrap the field in aContextMenu. A
FocusDisabledfield gets no menu. -
examples/solar_systemgets pause, speed and mute controls, a close button,
nav-dot names and a phone-width layout. Space pauses the orbits, Up and Down
double and halve their speed (0.25x to 16x), and M mutes the ambience track.
Buttons for mute and pause, and a speed readout, sit beside the zoom buttons.
The camera keeps running while the orbits are paused. The fact sheet has a ×
button, because a touch screen has no Escape key. Hovering a nav dot names its
body with the canvas tooltip. Below 792 px wide, the fact sheet shrinks to the
window and puts its six cards in two rows of three. Before, it ran off the
edge of a narrow window. The embedded track is now FLAC (3.05 MB, was a 4.08
MB WAV), lossless so the loop stays seamless, and low-passed at 3.5 kHz to cut
the 8-bit source's hiss by 13 dB. -
KrokiMermaidFetcher(baseURL)renders Mermaid diagrams through a
self-hosted Kroki server (#1147). Set
MarkdownCfg.MermaidFetcher: gui.KrokiMermaidFetcher("http://localhost:8000").
Before, the Kroki address was fixed to the publichttps://kroki.io. When
that service was slow or down, diagrams did not render. To use another server,
an app had to write its own fetcher and copy the size limits and error
handling. The new fetcher keeps all of them and uses the shared diagram HTTP
client. A self-hosted server also keeps diagram source on your own network.
The default is unchanged. -
cmd/shaderportports a Shadertoy shader to a*gui.Shader(#573). Run
shaderport -name myShader -o my_shader.go my.fragon a shader with
mainImage(out vec4, in vec2). The tool writes a Go file whose*gui.Shader
has both bodies: GLSL for OpenGL, WebGL2 and Android, and MSL for macOS and
iOS. Before, a copied shader was GLSL only and drew nothing on Metal. The tool
is a token rewriter, so the output keeps your comments and macros and reads as
a diff to review. It handles helper functions, structs,constglobals,
macros,outparameters, and the GLSL names that MSL spells differently. It
translatesmodwith GLSL's floor rule, because MSLfmodtruncates. It sets
Animatewhen the shader readsiTime,iTimeDeltaoriFrame. A construct
it cannot translate (iMouse,iChannel*,texture(), a mutable global)
stops it with the line and column, and it writes no file.
examples/custom_shaderadds a ported tile, anddocs/shader-porting.md
lists what the tool supports and rejects. -
Shader.GLSLDeclsandShader.MetalDeclshold program-scope declarations
(#573). A body runs inside the fragment function, so it cannot define a
helper function or a struct. The two new fields go before that function, on
every backend. A GLSL helper can read the built-in inputs, which are globals
there. In MSL they are locals of the fragment function, so a helper takes them
as parameters.gui.BuildGLSLFragmentDeclsandgui.BuildGLSLESFragmentDecls
wrap a body together with its decls.ShaderHashnow covers all four strings. -
gui.ShaderHashCachememoizesShaderHashper*Shader(#573). Each
backend callsShaderHashon every custom-shader draw of every frame, and it
reads every byte of the source. For a ported shader of 32 KB that took about
30 µs per draw. Every GPU backend now keeps one cache per window: a hit takes
about 4 ns and allocates nothing. The cache compares each string's data
pointer and length, so a field set to new text gets a new hash.
Changed
-
BREAKING: an
Inputinside aContextMenumust now set
ContextMenuDisabledto keep the app's menu (#1151). A right press on an
Inputnow opens the built-in Cut, Copy, Paste and Select All menu and
consumes the press, so aContextMenuwrapped around the field no longer gets
it. To keep your own menu, setInputCfg.ContextMenuDisabled: trueon the
field. -
A menu item now runs when the mouse button comes up, not when it goes
down. This applies to every menu:ContextMenu,OverflowPanel,Menubar
dropdowns, submenus and the Input menu. The item runs only when the press and
the release are both on it, so a press dragged from another item, or the
release of the right press that opened the menu, runs nothing. A press inside
an open menu no longer closes it: a press on a disabled item, a separator or
the menu padding leaves the menu open, as in a native menu. An item with a
submenu still opens the submenu on the press. A right click on an item no
longer runs it. A test that sends only a mouse down to a menu item must now
send the release too;Window.TestClickalready sends both. -
Touch: a long press that no handler consumed lets the finger go when it
drags, so a finger that rests before it drags scrolls (#1117). A touch held
still for 500 ms became a long press, and from then on its moves were ignored:
the page did not scroll until the finger lifted. A reader who placed a finger
carefully, for example aiming at the thin scrollbar and missing it by a few
pixels, saw the page freeze. Now a move past the pan distance cancels the long
press (GesturePhaseCancelled, notEnded), releases the press it sent where
the finger now is, and scrolls. A long press still keeps the finger when its
press took the mouse lock (a reorderable row, a text selection, a scrollbar
thumb, which the finger then drags) or when anOnGesturehandler consumed
itsBegan. An app whoseOnGestureacts onGestureLongPresswithout
ctx.Consume()changes behaviour: a finger that drags after the long press
now gets aCancelledphase and pans. Callctx.Consume()in that handler to
keep the finger, as the showcase gesture pad now does.
Fixed
-
The open list of a Select or Combobox is at least as wide as the field
(#1155). Before, a Select list was only as wide as its longest option, so a
FillFitfield in a wide column opened a narrow list. A Combobox list took
the field's width, but its rows kept their own width, so the hover color
stopped short of the edge, and an option wider than the field was cut off. Now
both lists are as wide as the field, or wider when an option needs more space.
The rows fill the list.MaxWidthis still the upper limit. -
A macOS app built with
CGO_ENABLED=0fails to link instead of panicking at
launch (#1159). The macOS backend needs cgo. Before, a darwin build with cgo
off linked, and the app panicked at start with "no native backend available".
Now the link fails and the error names the cause:
relocation target .../gui/backend.macOS_requires_cgo__set_CGO_ENABLED_1 not defined.
SetCGO_ENABLED=1to build for macOS.go vetand lint with cgo off still
pass, and builds for other platforms do not change. -
Touch: a vertical pan over a horizontally scrolling child scrolls the page
(#1118). Before, a page barely moved while the finger was over a child that
scrolls only sideways, such as a wide Markdown table: the table took each
move's sideways drift, and the vertical part never reached the page. A pan now
locks to one axis when it starts more than twice as far along it as across, as
on iOS and Android, and scrolls only on that axis until the lift. A diagonal
start scrolls both axes. Each axis goes to the innermost scrollable that moves
on it, so a diagonal pan over the table scrolls the table sideways and the
page vertically, and the lift flings each container on its own axis. The lock
applies to scrolling only: anOnGesturehandler still reads the finger's raw
deltas, less any axis a scrollable inside it took. Note for apps: an
ancestor'sOnGesturenow runs on a pan move that an inner scrollable took
only one axis of, with that axis zeroed. Before, the inner scroll stopped the
walk. A handler that must stay quiet while an inner scrollable moves (a
swipe-to-dismiss) checks for a zero delta on its axis. -
An open menu no longer passes hover to the widgets under it. Before, the
pointer over a disabled item, a separator or the edge of aContextMenu,
OverflowPanel, submenu or Input menu also hovered what was below the menu,
so a button under it showed its hover color. The menu panel now takes the
hover. Enter, Space or a click on a menu item now keeps a focus that either
action set, the item'sMenuItemCfg.Actionor the menu'sMenubarCfg.Action.
Before, Enter and Space always cleared focus after the action, and a click
kept only a focus set byMenubarCfg.Action. While a modal dialog is open,
the layers above it (the Input menu, the inspector) now take hover too;
before, only the dialog's own shapes did. Found while testing #1151. -
A widget shows its pressed color only for a press that began on it.
Button,Input,InputNumeric,Radio,Switch,Toggle,ExpandPanel
andBreadcrumbread the held left button to draw their pressed color, so a
press that began on another shape and dragged over them made them look
pressed. A menu item picked over a button left that button pressed until the
release. They now use the shape the press went down on, the same rule
Window.IsPresseduses.GoldenCfg.MousePressednow records its press on the
shape underHoverX,HoverY. Found while testing #1151. -
An open
Comboboxdropdown now closes when focus leaves the field
(#1150). Before, a press on anInputor another focusable widget moved
focus there but left the dropdown open over the form. The dropdown now also
closes on a press in empty space, which clears focus. A press on the dropdown
scrollbar keeps it open, and a press on a row still picks that row. -
audio.LoadMusicBytes: embedded MP3, OGG and FLAC tracks play.Playand
FadeInseek the track to its start, but in-memory MP3, OGG and FLAC data
reached the decoder through a reader that could not seek. FLAC and OGG
returnedseek music: ... not enabledand stayed silent. MP3 panicked with a
divide by zero inside go-mp3. Only WAV worked. All four formats now get a
seekable reader.LoadMusicfrom a file path was not affected. -
examples/solar_system: motion stays smooth on long runs, keeps pace when
frames drop, and-screenshotplays no audio. The simulation clock was
float32. After about a day, one float32 step was larger than a tick: Earth
froze on some ticks and jumped on others. The clock is nowfloat64, and
every orbit, spin and twinkle angle is reduced to a fraction of a turn before
float32 math. Each tick also advanced a fixed 16 ms, so a slow frame slowed
the orrery. A tick now measures the real time since the last one, capped at
100 ms so a laptop wake does not jump the planets. And-screenshotstarted
the ambience track, becausesoft.RenderToPNGrunsOnInit. The track now
starts inmain, on the interactive path only. -
SVG: geometry from CSS (
r,cx,cy, ...) renders, so the showcase
var() + calc()sample is no longer blank (#1145). A circle whose radius
came only from a stylesheet (.v { r: calc(var(--r) - 4); }) parsed to zero
paths: the cascade dropped the unknown property and the shape parsers read
only therattribute. The SVG2 geometry properties now ride the normal
cascade (presentation attribute < author rule < inline style, with
var()/calc()resolved), and the circle/rect/ellipse/line parsers use the
cascade winner. A bogus rule value is ignored instead of clobbering the
attribute, a negativerstill disables rendering, and geometry never
inherits from the parent. -
Touch: a slow drag no longer jumps the content where it starts (#1117). A
pan is recognized after 10 px of travel, and its first move caught up those 10
px in one step, so the content sat under the finger. A fast swipe hid it; a
slow drag, such as one that rested first, showed a 10 px jump. The pan now
starts where the finger crossed the threshold and drops the distance, as
Android does andDragScrollcontainers already did: the content stays 10 px
behind the finger. The pan'sGestureDX/GestureDYdrop it too, so an
OnGesturehandler that sums them lags the finger by the same 10 px, and a
short, slow flick reads a little slower inVelocityX/VelocityYand is
less often aGestureSwipe. The fling after a lift reads the finger's real
positions and is unchanged. -
Android: temp files land in the app's own directory, so icons and other
temp-file assets load (#1140). Go'sos.TempDir()fell back to
/data/local/tmp, which an app cannot write: a Go shared library reads its
environment once when it loads and never sees theTMPDIRAndroid sets later
through libc (from Android 13), and before Android 13 Android sets none. The
icon font failed to load, so every icon showed as a box, and anything else
written withos.CreateTempfailed too: app fonts added as bytes, the diagram
cache, PDF printing, the showcase's images.TMPDIRis now the app's cache
directory on every Android version: the backend takes it from libc where
Android set it, and below Android 13 uses the directory the host passes from
Context.getCacheDir()toSetCacheDir. Android trims that directory when
storage runs low, so the remote image cache persists across launches, and all
processes of one app share it. A host calls
android.SetCacheDir(cacheDir.absolutePath)beforeStart, as
examples/android_demonow does; without it, temp files still fail below
Android 13. Android fonts loaded from bytes use stable cache files rather than
leaking new temporary files on every launch. The remote image cache now
resolves its directory on first use instead of at package init, so it follows
the sameTMPDIR. -
Web: a NaN, infinite or out-of-range event field reads as 0, and a fake
touch list cannot hang the page (#1131). A script can dispatch an event
whoseoffsetXordeltaYis NaN, ±Inf or too large for afloat32. The web
backend passed the value on toEvent.MouseXorScrollY, and from there
into hit tests and scroll offsets. It now reads such a value as 0, as it
already did for a missing field. Integer fields (keyCode,deltaMode, a
touchidentifier) do the same. A touch list that claims a hugelengthmade
the backend scan it without end; it now looks at no more than 64 entries. -
Touch: a long press, then a drag, reorders a list on a scrolling page
(#1134). Since #1116, a touch on a reorderableListBoxorTreerow on a
page that scrolls vertically waited to see a tap or a scroll, and a vertical
drag counted as a scroll: the row could not be reordered by touch. A long
press armed the drag, but its moves were dropped. Now the moves after a long
press go to the drag, so holding a row for half a second and then dragging
reorders it, as on iOS and Android. A quick swipe still scrolls the page. A
touch drag that the platform cancels, or that a second finger takes over, is
now cancelled through the lock'sCancelhook instead of released, so it no
longer drops a reordered row, a docked panel or any other drag where the
finger happened to be. The moves after a long press drive any drag this
touch's press started, so a row held and then dragged on a list that does not
scroll reorders too, and a held text selection grows as the finger moves. A
drag already held when the finger landed is another drag's: it gets none of
the moves, and an abort of this touch does not cancel it. -
Android: an app returning from the background is drawn again, not left black
(#1136).GLSurfaceViewdestroys its GL context while the app is in the
background, and the next frame ran in a new context with the old programs,
buffers and textures, so only the background color appeared. The new
android.SurfaceCreatedtells the backend about the new context. It then
rebuilds its GL resources without deleting the dead ones: images and custom
shaders are uploaded again on first use, and the glyph atlas is sent again
from its CPU copy before the first frame draws, so text does not flash blank.
The window, its view and app state, and the loaded fonts stay as they were. If
the rebuild fails, the backend logs it once and tries again each frame; the
app no longer has to crash. A host must callSurfaceCreatedfrom
GLSurfaceView.Renderer.onSurfaceCreated, asexamples/android_demonow
does. A host built from that demo should also set
preserveEGLContextOnPause = trueon itsGLSurfaceView: Android then
usually keeps the context, and a resume rebuilds nothing. -
Android: rotating the device lays the window out at the new size (#1138).
A host callsStartfrom everyonSurfaceChanged, and on a running backend
Startonly resized the GL viewport. The window was never told, so after a
rotation, a split-screen change or a fold it kept laying out at the size it
started with: in landscape the content stayed portrait-wide with the rest of
the screen empty.Startnow sends the window the new size, asResize
already did. A host needs no change.
What's Changed
Other changes
- Retune dark ramp, add graphite theme by @mike-ward in #1133
- Drag a touch-reordered row after a long press; cancel aborted touch drags (#1134) by @ollex in #1135
- Rebuild Android GL resources after the EGL context is lost (#1136) by @ollex in #1137
- Send the Android window its new size when the surface changes (#1138) by @ollex in #1139
- Give Android a writable TMPDIR so temp files and icons load (#1140) by @ollex in #1141
- Web backend: reject non-finite DOM event fields, bound touch-list scan (#1131) by @mike-ward in #1142
- Add cmd/shaderport: port Shadertoy GLSL to a *gui.Shader (#573) by @mike-ward in #1143
- Let a resting finger scroll once it drags; drop the pan threshold (#1117) by @ollex in #1144
- fix(svg): cascade CSS geometry (r, cx, cy, ...) onto shapes by @mike-ward in #1145
- ci(showcase): copy whole website tree on deploy by @mike-ward in #1146
- feat(markdown): add KrokiMermaidFetcher for self-hosted Kroki (#1147) by @mike-ward in #1149
- solar_system: smooth long runs, controls, cleaner audio; fix LoadMusicBytes seek by @mike-ward in #1152
- fix(combobox): close dropdown when focus leaves the field (#1150) by @mike-ward in #1153
- feat(input): built-in Cut/Copy/Paste/Select All context menu (#1151) by @mike-ward in #1154
- fix(touch): lock pan axis and route each axis to its own scrollable (#1118) by @ollex in #1157
- fix(backend): fail the link for a darwin build with cgo off (#1159) by @mike-ward in #1161
- docs(spec): record #1160 purego macOS spike results by @mike-ward in #1162
- test(theme): stop the real tween in TestGoldenThemeFade (#1163) by @mike-ward in #1164
- docs: add issue template with away notice by @mike-ward in #1165
- docs: move issue template into ISSUE_TEMPLATE directory by @mike-ward in #1166
- fix(select,combobox): open list at least as wide as the field (#1155) by @mike-ward in #1167
- feat(table): optional row header column with select-all corner (#1156) by @mike-ward in #1173
- feat(markdown): render a safe HTML subset with Window.HTML (#1158) by @mike-ward in #1175
- chore(claude): run the Claude Code hooks through a Go tool by @ollex in #1174
- changelog: add v0.93.0 by @mike-ward in #1176
- fix(claudehook): pin a/ b/ diff prefixes in the review diff by @mike-ward in #1177
Full Changelog: v0.92.0...v0.93.0