Changelog
Every version of gui_toolkit, newest first. The format is Keep a Changelog, the versions follow semantic versioning.
0.96.02026-11-07release
Added
ui.press_hand: the press held on one box goes on on another, with what it keeps (ui.press_data), for a gesture whose item is built in another place while it lasts: a tab dragged out of a list into a grid of pinned ones, whose boxes have other parents, stays one drag.
0.95.02026-11-07release
Added
- Contrast:
ui.luminanceandui.contrast(WCAG's ratio),ui.to_oklch/ui.from_oklchfor linear colours, andui.readable(fg, ground, ratio): the colour moved along its OKLCH lightness, its hue and chroma kept, only as far as it must to read on the ground (lighter on a dark one, darker on a light one). - The system's light / dark, on Windows too, and when it changes:
platform.dark_mode_preferred()reads Windows' app mode (AppsUseLightTheme) as it read the portal and GTK on Linux;platform.color_scheme_serial()moves when the choice may have changed (WM_SETTINGCHANGE "ImmersiveColorSet", the portal's SettingChanged): an app that follows the system compares one integer a frame.
0.94.02026-11-07release
Changed
- Selected text reads on its selection: the selected part of a selectable box's text (labels, paragraphs, rich paragraphs) is painted again over the selection in the theme's new
"selection text"colour (by default the text's). A faint text, a dimmed run, was lost on the selection's colour.
0.93.02026-11-07release
Changed
- The wheel scrolls as the system says: a notch moves
platform.wheel_lines()lines (Windows' setting, lines per notch, characters across; three on Linux), eachui.WHEEL_LINE_PX(100/3 logical px, a browser's line) at the window's scale, in whole pixels; where the system says a page, a page less a line. Every view scrolls byui.wheel_step(box, axis), text fields too. Before, a notch was 4 em of the box's font whatever the setting.
Fixed
- A tab is drawn as space:
font.TAB_SPACES(4) spaces wide, the same wherever it is (measured as shown), with a space's glyph. Fonts have no glyph for it: it was drawn as a missing one (a box) in code and in tools' output.
0.92.02026-11-07release
Added
host.host_adopt, hooksstillandafter_frame,Host.times: a window and a renderer an application made in its own order of start become a host; a window whose picture would not change is not built; what a program does between two frames goes after the picture; the last picture's draw and present times.
Removed
ui.Reorderandui.reorder_frameare gone: the drag of a reordered list is kept on the press of the item's box (ui.press_data) and ends with it (the button went up, the box went, the user left the window). The procedures take the item's box key in place of a^Reorderand an id:reorder_begin(item, moved, threshold, press, slot),reorder_dragging(item),reorder_item_pos(item, slot, pointer, lo, hi, rate)(its animation keyed by the item's box too). Lists whose items share ids drag apart, the box's key being its list's.ui.Dock.heldis gone:ui.dock_held(&d)asks the press whether the left button holds one of the dock's controls (a tab or a floating panel's title, a handle, a floating panel's corner). It keeps nothing, so it ends with the press (the release, the box held going, the user leaving the window) and says the same at any time of the frame. Writeui.dock_held(&app.dock)whereapp.dock.heldwas read.
0.91.02026-11-07release
Added
ui.lazy_pin,ui.lazy_pinned: the view stays on an item, a given distance into it, placed after each layout whatever grows around it; what the last layout found of it says when it has settled.- ui.press_data: what a gesture keeps lives on its press: the zero T when the press began, the same while it lasts, gone when it ends by whatever way (release, its box gone, the user leaving the window). A press of the same box again, even in one batch of input, starts from zero.
- ui.window_clipboard_get, ui.window_clipboard_set: Build_Params' clipboard procedures for the system's clipboard of the build's window, so a program no longer needs a global to reach its window.
- platform.events_waiting: whether a window has something for its next poll_events, without waiting. A loop of several windows asks each before it sleeps, because the poll of one window of the thread takes the others' messages.
- host: a window as an object, above ui and render. host_open, host_frame (pace, input, resize, builds, picture, present), host_close, and hosts_wait / hosts_due / hosts_frame for several windows on one renderer. Hooks cover what a rich application's frame does of its own: params, input, builds (several in a frame: host_build), hold (no picture this frame), after_build, before_input, dragging, rest_frame. It keeps the orders that matter: input is read with the renderer given back, a step of a resize is that window's alone, buffers keep the monitor's size, and only the first window is paced. examples/hello and examples/todo use it; the way by hand is unchanged.
- render.surface_scope: surface_use until the end of the calling scope, so a frame on a second window cannot forget to go back.
- ui.view_limit, ui.view_reveal, ui.view_reveal_offset, ui.view_jump, ui.view_shift: the verbs of a scroll box's view, for code that wrote view_off, view_target and view_anim by hand. view_reveal says whether the target moved, so a caller decides on scrollbar_reveal itself; view_shift keeps a glide under way and leaves a target at the top there.
Changed
- A lazy column looks each item up once per build, in one map: an item's height is noted after the layout that placed it.
- Splitters, dialog grips, floating panel corners, range sliders: what their drag starts from is kept on the press (ui.press_data), not in the State's one drag payload. ui.drag_store and ui.drag_load stay, for gestures that read an earlier press (panes).
- render.Window_Target: what is one window's is now a struct of its own, included by using in Renderer and Surface, and Wgpu_Window on the WebGPU side; surface_use exchanges them whole, so a new window field needs no list. Field access is unchanged, except that Surface.gen and Surface.space_epoch are now Surface.swap_gen and Surface.swap_space, and Surface.wgpu_backing is a ^Wgpu_Window.
- A row taller than its view keeps its top: ui.list, menus, reorder_offset, the focus and the accessibility reveal now follow the rule ui.vlist had, instead of showing the row's bottom.
- A box's name and text: copied as they are at each build, without going through the formatter.
Fixed
- WebGPU second windows: a window's frame format now goes with it through surface_use, as its offscreen target does on Direct3D 11.
- Scrolling: dragging a scroll bar's thumb or stepping a long menu ends a glide under way, which could pull the view back for a frame; a ui.vlist whose rows moved during a glide no longer starts the glide over.
0.90.02026-11-06release
Added
render.surface_initunder_children: a composition surface below the window's child windows. A web view moved into a window with a composition surface stayed hidden behind it.
0.89.02026-11-06release
Added
ui.tween: a value that reaches its target in a set time, gently at both ends, for what the eye follows from end to end.draw.glyph_run_stroke: the outlines of a glyph run as a line of a width.font.glyph_ink_extent,font.run_ink_spans: where the ink of a glyph or a run crosses a band, from the outlines, kept per glyph and band: the gaps of a skipping underline.
0.88.02026-11-06release
Fixed
- Text in a dialog or a popover can be selected: their panel took every press, so a plain
.Selectablelabel in it selected nothing, showed no text cursor, and a label made.Clickableto get the press counted as a button. The panel is now.Press_Stop(newBox_Flag: clickable only so that what is under it gets no press): a press on it is a press on nothing, selectable text in it takes its own, and a selection stays on the surface it started on, whatever is built behind it.
0.87.02026-11-06release
Added
push_clip_polygontakes several contours: a point pastrender.POLYGON_BREAKends one, with no edge to the next: a clip with holes.draw.Path: lines, quadratic and cubic Bézier curves, arcs and rounded rectangles, flattened to a tolerance, filled (path_fill, nonzero or even-odd, holes included) or stroked (path_stroke, round ends and joins, even in translucent colors), withpath_containsfor hit tests,path_bounds,bezier_cubic_at,bezier_quad_atanddraw.bezier.draw.line,draw.polyline: a line of any angle, with round ends and antialiased edges.
Changed
render.release_targetstakes the textures to leave: a picture still on screen stays, for the program to carry over to the new generation.
0.86.02026-11-06release
Added
platform.drag_silence_limit: for tests, how long a drag may stay silent before it is given up.Signal.pointer_clicked: the click was the button's, so the pointer is where it says.clickedstays true for Enter, Space and a screen reader too.platform.cancel_close: takes back a Close the app answered by staying open:is_closedno longer stays true for good.- Event.lost: a Mouse_Release made because the system took the pointer. It is no click, and a middle button lost soon after its press no longer latches autoscroll.
- Event.Frame_Press: a press on the window's frame or caption (Windows). What the window was doing ends with it: a latched autoscroll waits for the next click anywhere, and a click there came to nobody.
font.atlas_end_frame: a draw context between frames holds no page (draw.end_framecalls it): a context that stopped drawing no longer keeps the atlas from reusing pages.devtools.stop(srv, s): for a program that goes on after the server, theui.Stateit served: the clients' edits, the highlight and the accessibility tree asked of it end too.Signal.pointer_pressed: the press was the button's, so the pointer is where it says and it is held.pressedstays true for Enter, Space and a screen reader too.ui.sliderand the colour pickers read it: Enter on them no longer set the value from wherever the pointer was.
Changed
ui.Reorderholds the press that drags (held) in place ofactive: a release and a press read in one frame end the drag, as does the box held going away. Readreorder_dragging.- Escape in a dialog goes to the dialog on top at the end of the build, and closes it at the next one: read in the frame a new dialog opens, it closed the one under it.
openturns false one build later than it did. ui.Autoscrollsays where a session is in one field,phase(Autoscroll_Phase:Off,Holding,Latched), in place ofactive,heldandlatched; every end zeroes it, with the box it scrolls (box).State.auto_boxandState.auto_pressedare gone.draw.open_plane_frame:ok = falsefor a plane being recorded, which draws nothing (Open_Plane.modereplaceshit).- A capture of a frame WebGPU could not get (a hidden window's surface) says
Failed: it stayedPending. - A box that goes lets go of what held it: a press, the focus, a click on the way to a double one. A box of a vlist or a lazy column has only left the view: the focus waits for it there.
devtools: what a client started is its own. A screencast, a recording and the tree ofDOMhave one client: another'sPage.startScreencast, itsToolkit.*Recording,DOM.undo/redo/requestChildNodesand the like are refused, and its stop and acks change nothing. A recording ends with its client, typed text included; a highlight goes with its client; what a client held down, buttons and keys, goes up when it goes.instance.Owner_State: anOwnersays where it is (None,Claimed,Serving,Closing) inOwner.state, which replacesOwner.closing. A secondowner_startanswers false and opens nothing, a file descriptor 0 is one like any other, and a window that could not be destroyed no longer points at its Owner.configlayers:Entry.lockedis gone,is_lockedreads the layer, and an observer sees a policy value locked..Defaultno longer goes over a layer,resetunder a policy changes nothing,load_filewants the tag of the layer it reads (POLICY_MAGICfor.Policy),registeranswers how many waiting values it refused, a user's value that its definition refuses stays in the profile, anddirtycounts a change under a higher layer and no longer a file just read.restoresurvives an observer that registers.- A dialog gives the focus back: what had the focus when
ui.modalopened has it again when it closes, unless something else took it meanwhile.
Removed
render.Pacer.latency:Present_State.max_latencysays what the swap chain has, a new one included.
Fixed
- A press on a row of an open menu leaves the keyboard focus where it was: choosing an item of a field's context menu with the mouse took the focus from the field.
testkit/session:advancein a build ends the test (it hung to the test's limit),draw_contexton an attached session says so,Session.damageis the next frame's only, and the guard against two tests at once is atomic.ui.a11y_treeis empty when no tree was made (it answered with the last one), and an active descendant not made in this build is not named.- Title bar regions are forgotten with no caption or window too: buttons no build makes any more stayed in
chrome_regions. - A box of the last build looked up during a build has no text, name, accessibility or custom data: they pointed into memory the build had freed.
ui.theme_destroyleaves an empty theme: a palette applied after it appended to freed memory, a second destroy freed it twice.ui.color_picker: the text left by the user is written as the picker writes it: a spelling of theirs (#F00) or what is no colour stayed.Color_Pick.shownis gone.- Find in page: a sweep's wait is a timed wake, not a frame at each refresh, and a sweep during a glide reports the place in the content (the scroll had moved after the layout). Its clock is the build's.
- Toasts ask for frames only while they fade, and a wake between;
ui.set_toast_arealasts one build, whethertoasts_buildran or not. - Docks: two requests in one frame are both done, and the stack's menu of a closed panel closes.
vlist:vlist_scroll_toon a list with no box yet is kept for its first build, the wheel ends the wait for a row with the reveal, and a row of no height is not waited for over 600 builds.listandvlist: rows gone from under the active row saychanged; no row is nothing to reveal (revealstayed armed); a row selected before the first build comes into view with no other frame asked for.ui.text_edit_caret_rectsaysfalseonce the editor is built no more.text_edit: the column of Up and Down ends with every other move and edit (undo, redo, select all, cut, paste, Ctrl+Backspace, a double click kept it).- The caret of a
line_editis solid after an arrow key, as atext_edit's is: it could stay in the off half of its blink. - Undo in
line_editandtext_edit: a move of the caret ends a step (type, arrow, type was one), and a field's steps go by the build's clock, as a text area's do. - Menus: a press of a side button outside closes them; a Right or Enter on a submenu that turned disabled opens nothing later; a menu opened from the keyboard selects its first item once, so a pointer on a disabled row leaves nothing selected; a menu opened from inside a menu's block opens when the block ends, its rows not mixed with the old one's.
ui.stepper: held with the pointer off its button it waits, as a scroll bar's arrow does.- A press with no pointer (Enter, Space, a screen reader) no longer takes the place of a drag going on (
splitter,pane_split, the edges ofmodal_sized), sets arange_sliderfrom the pointer, or moves the caret of aline_editor atext_editto it. ui.pane_split: a strip pulled out and pushed back under the snap stays folded when let go, and a screen reader's value on a folded pane saystoggled. What a press was on is one state (Pane_Press).ui.modal_sized: a press on an edge that does not move chooses no size (the size a small window clamped it to stayed for good), and a double click on an edge stays at the preferred size while the button is held.platformon Windows: a surrogate pairs only with the unit right after it, a pointer whose end the system no longer describes still ends, a resize made of inner frames has its burst.- Clipboard on Linux: a selection another program cleared is not read back, X11 asks the server who owns what it took, a late answer is not the next read's, a paste leaves no property, an INCR reader that left is given up,
XdndSelectionis let go, a large drag goes by INCR, a reader that closes its pipe no longer ends the process on Wayland. Windows: a real window'sclipboard_changedreads the system's clipboard with an offscreen window open. platformon Linux: a seat that loses its pointer or keyboard and gets it back works again, a scale change of the output or of Xft.dpi reaches the windows, the display is read again after a change of the output.platformon Linux: a window has one state (Show_State), read back from the server: nothing shown is not visible, a state asked before the map reaches the window manager.set_placementrestores a maximized window on every backend and applies at once on a shown Windows window, an offscreenset_fullscreen(false)leaves another state alone,toggle_maximizeleaves full screen on Windows.- Accessibility on Linux: the bridge is read by the window's loop, it keeps the whole tree an update changes (
platform.a11y_tree_merge),a11y_stateisOffuntil a reader runs (org.a11y.Status), a bus that refuses is asked once. - File dialogs on Linux: a bus without a portal falls back to zenity and kdialog, the portal's answer cannot be missed, its
filteris the user's choice, a lost bus isFailedand notCancelled.idle_inhibitends with its window. - Tray on Linux: each icon has its own object, a failed create leaves nothing exported, a panel that restarts gets the icons again (
Recreated), a menu that changes is told (LayoutUpdated), a notification has a default action, soNotify_Clickcan come, and one that closed is not replaced by the next. Windows: an icon a restarting shell refused is added at its next change. - Drag and drop on Linux: a drag says where it is (
Drag_Phase). X11: only the button that began it ends it, Escape gives it up. Both: the limit is one of silence, a target that took a drop has five seconds, the target hears what the source allows and a Move is told, a source that dies ends its drag, a target disabled under a drag saysDrop_Leave, no event of kindNone. A drag script is used up howeverdrag_startends.drop_injectof anything butDrop_Enterneeds a drag over the window. platformon Linux:set_cursorto the cursor the window had takes a custom cursor off the screen, releasing the shown custom cursor shows the window's own, Wayland sends a cursor only for the window under the pointer,set_cursor_custom'sscaleis used.platform: a release comes once and only of a press made here, on every backend; a pointer taken with a button held makes its release on Wayland and when an X11 window is unmapped; X11 says noMouse_Leaveunder a press, as a capture on Windows; a key held when the focus goes makes itsKey_Release.Activatecomes once for a change, and a window is neitherfocusednoractiveuntil the system says so.platform.poll_events: the batch stays whole while the app reads it. What runs from an event may pump the system (a paste on X11,drag_start), andw.eventsmay then move.platform:wait_eventsees all that ends it. On Linux the session bus and the accessibility bus (a tray click answered at once), injected events; on Wayland and Windows a repaint asked for; on Windows the events not handed out; afterinput_pending, what it kept.platformon Linux: a closed window's last notices go to nobody (the X11 window that polled read as minimized), a window closed under a drag or an input method leaves nothing that points at it, a compositor without xdg-shell fails everyopen_window, and a server that goes away closes its windows (Display_Link) instead of leaving a loop that never hears of it.platformon X11: typed text follows Shift, AltGr, Caps Lock and the layout (a key is read with the modifiers and the group of its event), a change of keymap is followed, a held key saysrepeat. On X11 and Wayland a key keeps its name whatever it types: Shift+1 isN1, Shift+Tab isTab, the keypad without Num Lock isHome,Left...- Focus keys: an arrow and a Tab read in one frame both move the focus, and
ui.focus_stepsays its direction (ui.tab_direction): a rich paragraph entered backward that way starts at its last link. Signal.modsfollowsKey_ReleaseandMouse_Release: a modifier let go during a drag is no longer held.ui.rich_paragraph: Enter and Space choose the link that has the focus wherever the pointer is (they took the link under it, or nothing over plain text), and a screen reader's focus on a link of a paragraph without the focus stays.- The build lifecycle:
ui.building()stays true to the last line ofend_build;end_buildends the build that is open, whateveruseselected since, and asserts that there is one;ui.destroyin the middle of a build leaves none open on the thread.end_buildalso asserts the list and menu scopes. - Selected text is one state (
Sel_Phase): no drag goes on with its selection gone (the text cursor and the frames stayed),select_boxends a drag, and a press on a button between two presses on a text breaks their double click. ui.nextandui.a11ybefore what builds nothing (a closedmodal,modal_sized,ctx_menuormenu_nested, a skippedlazy_item) no longer land on the next box.- An open menu no longer gets the keys something else took first (Ctrl+zoom, autoscroll's Escape).
- Autoscroll: a move of the window ends a session, as its documentation said. A move read with the release counts against a tap. What ends a session no longer ends the frame's input: a middle press read with it starts the next one, and the Escape it took does not reach an open menu.
- The window the user leaves (
Event.Activate,Build_Params.inactive): an open menu closes, a held button ends with no click, the modifiers are forgotten, a selection stops following the pointer, autoscroll ends. A dock drag cut that way drops nothing. - A box that goes takes what held it: at the end of a build a press, the focus, the first click of a double click and the link of a rich paragraph let go of a box the build did not make. A key that comes back later is another box; a focus given ahead of its box stands.
splitterandpane_splitsayfinishedwhen their box is back. ui.set_post_wake: the app's wake is called with aniluser too, and anilwake goes back to the window's. Of two windows of a State, posts wake the one that stays when the other is destroyed. A post on a State that is gone is refused.ui.post_configureruns every call that waits, not a build's budget of them (the rest were lost), and a closed queue stays closed.draw.offscreen_end: closes the layers left open in the target (it answered 0 and left the target open).font: the emoji font comes back on the next emoji when whoever shared its id unloaded it.font: a word being shaped while the word cache is cleared is shaped again, and no thread keeps the width of a word cleared under it.font: a glyph the COLR interpreter was drawing when the settings changed is drawn again with them, and its outlines follow a new provider or tolerance.font:glyph_outlineandslug_glyphsanswer on a state never started (they read a nil backend).Text_Backend.face_close, which nothing called, is gone.draw.destroy: in the middle of a recording it no longer leaks the lists of the frame.draw: a layer or plane that replays a picture lacking a pending color glyph is not kept: the glyph comes when it is ready.- Frame statistics on WebGPU:
frame_end(vsync = false)on a Fifo surface counts the vblank it waits for, a reconfigured surface starts its run over, andvblank_gridsays unknown once the compositor stops answering (Direct3D 11 too).gpu_msstarts over with a new device. draw: a mask layer with no target of its own draws nothing (its drawing landed in the content);Open_Layersays what it is and what a mask draws in withLayer_KindandMask_Draw.draw: a layer or a plane owns what is pushed in it: pops stop at where it started and at the root (they panicked there), what is left pushed ends with it, and layers and planes left open inside one another end in the order they were opened. A recording cannot close a plane of the frame.draw: a secondend_framedoes nothing (it drew the frame twice).render.canvas_read_readyandcanvas_read_finishon a read begun before a device loss: ready at once, and the finish says it failed. They used the old device's objects on the new context.- 2D canvas layers nest. A filter pass inside an open layer leaves it open, what is drawn into a layer that could not be made or was freed goes nowhere instead of onto the canvas, and a layer replaced by a filter pass gives its texture back to the pool.
canvas_set_opaqueon a source texture no longer outlives it. - A texture a recovery could not make again:
Texture_Kind.Failed, it draws nothing,texture_sizesays{0, 0}anddestroy_texturefrees its slot. A frame crashed on one (Direct3D 11), and its slot was lost. render.set_fixed_surfacesurvives a device loss: the chain made again had the window's size, and a window larger than it was clipped. A translucent window that fell back to opaque during a recovery tries DirectComposition again at the next one.render.readback_pollsaysFailedfor a read-back whose frame did not run, which stayedPendingfor good, and for an id of another renderer. A renderer destroyed takes its read-backs with it.font: a system fallback that goes character by character (fontconfig) no longer cuts a sequence: joiners, selectors and what follows a ZWJ stay with their base.font: the Slug store follows what its glyphs depend on:color_glyphsandcolor_enginereach the glyphs of color faces, a new provider is asked for what had no curves, no room is not kept, and keys hold the slot of a face, so what a face leaves goes with it at any generation.- Partial redraw: a frame that stopped halfway (Direct3D 11), that had no surface texture (WebGPU) or whose region could not be cleared leaves nothing the next region starts from: the next frame is whole. Their rectangles were lost, or composed onto a blank target.
- A window that cannot be remade (Direct3D 11, WebGPU) counts as a device lost: the next
frame_begintries again, where it drew into nothing and the next clear crashed. A swap chain thatResizeBuffersfinds gone is a loss too, and a size asked while there is no swap chain is kept. font: a page reused uploads whole to a context that had it: the old bitmaps no longer stay in its texture between the new ones.render.surface_destroyof the Surface in use gives the renderer back its own window first: it tore that window down.- Second windows on WebGPU: a window set aside is made again after a recovery and follows
set_blend_space, as on Direct3D 11. Its capture, its clear and its first frame are its own, andsurface_destroyfrees its frame statistics. font: a color glyph drawn on the color thread whose COLR layers cannot be read shows its outline, as on the drawing thread (it was kept as a bitmap at page 0, out of range when no page exists).- A device lost on WebGPU: it is noticed (wgpu-native reports every loss with the reason
Destroyed, which was taken for the renderer's own release), offscreen too. A recovery makes the new device first: with none to be had the old one stays and frames draw nothing, where they ended the process on a nil device. Calls that end the process on a device gone (QueueSubmit,DevicePoll,SurfaceConfigure,SurfaceGetCurrentTexture) wait for the recovery. font: the id of a font just unloaded, or of a cascade, no longer leaves blank glyphs for the next face in its slot (Font.deadandState.generationare gone: an unloaded slot isFont_Kind.None).- AT-SPI on Linux: the bridge's connection is dispatched once a frame, so what a client asks of the exported object is answered and a lost bus is seen.
native/a11y: an update that is refused or finds no assistive technology leaves the deferred focus as it was, and one that would give a subtree a second graft node is refused instead of ending the adapter.arena.Pool: a slot whose generations are spent is retired instead of wrapping, so a handle never comes back to life, even after 2^32 reuses.devtoolsinput and recordings: twoToolkit.clickof one batch are two clicks; a drag cut byToolkit.stopRecordinggets itsup, a second button no longer breaks a recorded drag, and a right drag replays as one (downandupoftools/cdp.mjstake--button right).CSS.setStyleTextsdoes all of its edits or none, a;in a string or a comment no longer ends a declaration, and a frame of a screencast that ended no longer touches the one that took its place.devtoolsconnections: a connection that sends no request is closed after 5 seconds, an upgrade needsUpgrade: websocketand version 13, and a frame RFC 6455 does not allow ends the connection instead of being glued to the message under way.instanceon Linux:acquireandsendnever block past their timeout, with a large message or a full backlog, and an owner that wakes after a sender gave up no longer handles its message as well.config.save_usergoes throughfsx.write_atomic, flushed: a save that fails never removes the profile, and leaves no temporary file.fsx:write_atomicwrites through a temporary file of its own,<path>.<pid>.<thread>.tmp, so two writers can no longer tear a file;replacewithdurableanswers false when the directory could not be synced;read_allno longer returns a replaced file cut to the old size.dbus: connecting cannot hang the window's thread.connect, the SASL answer andHelloeach have a limit of 5 seconds, and a signal no longer fails the authentication.dbus: a lost bus closes its socket.conn_fdanswers -1 from then on, so the window loop no longer spins on a socket at EOF; nothing more is read after a stream that cannot be parsed, a failed send runs thecall_asynchandlers that wait at the nextdispatch, andplatformfalls back as with no bus. A handler of an expired call that calls no longer panics, serial 0 is never used, andconn_closefrom a handler asserts.Call_Status.Unsupported, never produced, is gone.- Read-backs on WebGPU: a buffer is released outside the table's lock. Releasing one still being mapped may call its callback, which takes that lock.
flate: an inflater that failed stays failed (inflate_finishsaidTruncated, and a later push went on from where it stopped), and nothing is written afterdeflate_finish(it wrote past the end of the stream).ui.find: a sweep left waiting when its page went away keptneeds_frametrue for good, and the loop never slept.ui.line_edit,ui.text_edit: the word a double click took collapsed to the pointer at the first frame made with the button down. The drag now goes on by words, as selectable text does.draw: an offscreen target left open is dropped at the end of the frame (it was composited, left its transform on the stack and cut the filters of the layers around it), and a plane left open in a recording ends with the recording (it took the frame's stacks down atend_frame).- Dock drags that lost what they held: closing a floating panel while it was dragged ended in a bounds check (
docking.odin), a panel closed while its tab was held came back at the drop, a drag whose button went up while the dock was not built dropped when the dock came back, and Enter on a held tab restarted the drag. - Dialogs on dialogs: with two open, the one underneath took the focus away at every frame, so a field of the top one could not keep the keyboard, and Tab went through both. Each dialog now knows where it stands among those of the last build: the top one has the keyboard, the others wait.
- D-Bus:
r_skipnever returned from a message cut short inside a struct or a dict entry, or from an array of a type that takes no bytes; it ran on the window's thread. - DevTools:
Page.startScreencaston a window served with noui.Stateis refused. It woke the window for good, and an ack dereferenced the missing state.
0.85.02026-11-06release
Added
ui.toast_action: a toast that a click acts on, and dismisses, for one that says what can be done.
Fixed
- The corners of a floating panel: its panels, drawn after its frame and square, covered the frame's round corners, whose border stopped short of them. They are clipped to the frame now, and its border goes over them.
0.84.02026-11-05release
Added
- Text per colour stripe:
font.Settings.subpixel_text(.Rgb,.Bgr) gives text a coverage per stripe of the screen's pixels, sharper at few pixels per letter, on Direct3D 11 with the atlas (DirectWrite) and on WebGPU with Slug (three samples a third of a pixel apart). Blended per channel in the frame's own target; in a layer, or turned, the same text is grey.font.system_subpixel_textsays what the system does,render.text_subpixel_supportedwhat the renderer can. - The site says what it is when it is shared: a card, Open Graph and Twitter tags on every page, an Atom feed of the versions, a sitemap and a manifest (
tools/site_meta.mjs, at deployment).
Fixed
- Read-backs from several threads: the process's table of read-backs was allocated with the allocator of whoever wrote in it first, and freed by whoever emptied it, through an allocator that could be gone. It is on the process's heap.
0.83.02026-11-05release
Added
- The side buttons of the mouse:
Mouse_Button.Backand.Forwardon Windows, X11 and Wayland,ui.mouse_pressed(.Back), andbackandforwardover the DevTools protocol.
0.82.02026-11-05release
Changed
- Middle-click autoscroll goes five times faster by default: up to 15000 px/s, reached 300 px from the anchor, the speed doubling every 60 px (
DEFAULT_AUTOSCROLL): the curve Wren settled on. It was 2800 px/s at 420 px. - A
target_hzabove the screen's rate is made: 120 on a 60 Hz screen is up to 120 frames a second while something moves (none at rest), each on its slot, none waiting for the screen, shown at once (Cadence_Plan.above): it stayed at the screen's rate. The screen shows the newest: not smoother, but each refresh has fresher input.
0.81.02026-11-05release
Added
ui.modal_sized: a dialog of a size of its own, centered and always within the window: it opens at what it prefers, the person resizes it by its edges and corners (the opposite side follows, it stays centered), a double click goes back; the size is the application's to keep.ui.modalstays in the window too: narrower if it must, higher when what is in it would pass the bottom.
0.80.02026-11-05release
Added
- The output of frames is the application's to choose:
Pacer.choices(a ceiling, a rate; read at the next frame,render.pace_frameplanned with none), andCadence.uncapped: no slot, no wait for the screen, every frame shown at once, for a diagnostic or a benchmark. A window at rest stays at rest.
Changed
platform.wait_slot:render.pace_frameno longer sleeps through a frame's slot: input is taken as it arrives (time-stamped, kept for the nextpoll_events), a key or a button ends the wait, a pointer that moves and the wheel do not.wait_handlehasuntil_press. An offscreen window does not wait.
0.79.02026-11-05release
Added
ui.find_sweep,ui.find_all,ui.find_pending,ui.find_refresh: a find in page over a lazy column reads everything once for a needle (when it rests 80 ms, and again if what is built no longer answers), and stays lazy between:lazy_begin(all = find_sweep()). In opencode native, a frame while finding 40 -> 1.7 ms on a conversation of 150 answers.
Changed
- Text cut with an ellipsis: where it is cut is kept (by text, font, size and width): it was measured rune by rune at every frame, through the shaper. A frame at rest in opencode native 1.65 -> 1.10 ms.
Fixed
render.pace_framewaits for the frame's slot, after the swap chain and only what is left of it:Frame_Decision.wait_nswas worked out and not made, so nothing capped the frames of a screen that does not hold the app back.platform.precise_sleep: a wait under the system's tick.
0.78.02026-11-04release
Added
Dock_Options.strip_last: boxes after a stack's fold and menu buttons, the strip's last control (a window's close button stays at the far right;strip_endcomes before them).ui.field_num_step: the number at the caret of a focused field stepped as browsers' style editors step it: Up / Down 1, Page Up / Page Down 10, Shift 10, Alt 0.1, Ctrl 100, and the wheel over the field with the same modifiers; its unit and the text around it stay (margin: 10px 5px,rgba(10, 20, 30, 0.5),calc(100% - 12px)), a number inside a word or a hex colour is none, the number stepped is selected and the step is an edit of the field's history.ui.num_step_textandui.num_step_deltado it on a string and a key press.ui.color_picker: a colour picker for aui.Color_Pickthe app owns (color_pick_set,color_pick_rgba,color_pick_text,color_pick_destroy;color_picker_width): a saturation / value square, a hue bar and an opacity bar beside the colour it was opened with (a click takes it back) and the new one, a switch of the text's format (hex, rgb, hsl, oklch) and the colour as text, applied as it is typed. Every part is a Tab stop, moved by the arrows (Shift: a tenth; the hue by degrees), and screen readers see sliders, a radio group and a field. No eyedropper: the platform layer reads no pixel of the screen.ui.color_parse/ui.color_text: hex,rgb()/rgba(),hsl()/hsla()andoklch()read and written, in the style they were written in (Color_Style: the format, the legacy comma syntax or the short hex);srgb_to_hsv,hsv_to_srgb,srgb_to_hsl,hsl_to_srgb,srgb_to_oklch,oklch_to_srgb.ui.alpha_picker,ui.checker_draw;hue_pickertakeshorizontal;sat_val_picker,hue_pickerandalpha_pickertake the keyboard and tell screen readers their values.
0.77.02026-11-04release
Fixed
- A folded pane's strip in the theme, a fold button on the stacks that fold, tooltips the size of their text: the strip of a folded side (
pane_split, a dock's folded split) painted nothing at rest and the palette's raw hover colour (opaque white on a dark theme) under the pointer; it now has the theme'spane_strip background(by default the bars' surface) and the theme'shover_fillover it, and its chevron is larger (PANE_CHEVRON_EM1.4, alsopane_toggle's). A dock's stacks on a side that folds have a chevron button at the end of their strip, as large as the menu's button (pane_toggle, which now takes the sizenextgives it; a Tab stop, Enter folds); a strip's tabs are in the room its end's buttons leave, so tabs too many for a narrow stack are cut and the fold and menu buttons stay in view (they were pushed out of the strip). A tooltip's content inherited the app's style, so under a style that fills it was as large as the window; a size relative to the window is now the content's own in it,ui.tooltip_text(text)is as wide as its text up toTOOLTIP_MAX_WIDTH_EMand wraps past it, andtooltipgoes above the pointer when it does not fit below (TOOLTIP_ABOVE_EM); its first frame measures it unseen and asks for the next one, which places it (it was placed by the size of 10 x 3 em it was taken to have, and stayed past the window's edge when nothing else made a frame). Testsdocking a_folded_strip_takes_the_theme_colours,the_fold_button_folds_and_the_strip_brings_it_back,the_end_buttons_stay_in_view_past_too_many_tabs,a_tooltip_over_a_tall_strip_fits_its_text;ui_robust tooltip_places(above the pointer at the bottom).
0.76.02026-11-04release
Added
ui.menu_at(p)andui.wheel_consumed(): the wheel over an open menu is the menu's:menu_atsays whether a point is on a level of the open menu (a submenu, a nested menu, a drop-down's list) as the last build laid it out, so an app that routes the wheel to its own content before it builds the interface (a browser's page) gives it nothing there;wheel_consumedsays whether this build's wheel went to the interface (a box that scrolls under the pointer, or a menu's level, one that fits included). A menu taller than the window scrolls under the wheel, one that fits takes it and does not move, down and across; the scroller under either gets nothing (testkeyboard menu_takes_the_wheel_over_it).
0.75.02026-11-04release
Changed
- Slug glyph data built in half the time:
slug_buildsorts the curves of an axis once (oneu64key each) and puts each curve in the bands it spans, found from its bounds against the bands' own; it sorted the curves of every band and tested every curve against every band of each of the ten band counts it tries. The same data (testslug bands_are_their_definition); 400 glyphs of a face, best of 5: Segoe UI 5.5 -> 2.8 ms, Times New Roman 8.3 -> 3.9 ms, Nirmala UI 10.0 -> 4.6 ms.
0.74.02026-11-04release
Changed
font.color_prepare, textures zeroed on the GPU:font.color_preparemakes what color glyphs are drawn with (Direct2D on a WARP device, about 20 ms) for an application to call on a thread of its own while it starts; the first color glyph a frame looked up made it on the drawing thread. Textures are made render targets too and zeroed by a clear on the GPU (ClearRenderTargetView) where they were zeroed by uploads of zeros: a 2048 x 2048 glyph atlas page 6 ms (R8) and 3.5 ms (RGBA) of the CPU before, its first frame's.
0.73.02026-11-03release
Fixed
- The ASCII case helpers are package
ascii(ascii/ascii.odin:ascii.lower,upper,lower_byte/upper_byte,equal_fold,has_prefix_fold,contains_fold), no longerbase'sascii_*: a package namedbase, whichfont,devtools,platformandconfignow import, clashed with an app's own package of that name (Odin wants one name per package in a program).
0.72.02026-11-03release
Fixed
- ASCII case for names and protocols:
strings.to_lower,to_upperandequal_foldof Odin dev-2026-11 fold by Unicode (the Kelvin sign U+212A iskto them, the long s U+017Fs), where names and protocols want ASCII. New inbase(base/text.odin):ascii_lower,ascii_upper,ascii_lower_byte/ascii_upper_byte,ascii_equal_fold,ascii_has_prefix_fold,ascii_contains_fold. Switched to them: the DevTools server's header names and Host (Ho\u017Ft:is no Host,localho\u017Ftno localhost), CSS colour keywords,nodeName, the accessibility roles and actions; font family names as cache keys (Windows, Linux, macOS) and fontconfig's / Core Text's answer checked against the family asked for (DejaVu San\u017Fno longer loads DejaVu Sans); preference booleans; the GTK theme name, file extensions for MIME types on Linux; the changelog tool's type and slugs,tk's checksums and emoji version line,run_tests' groups, the examples' option names. Unicode stays for user text (find in page, fuzzy matching, type-ahead, the settings' and DevTools' searches, the test session's text lookups), display text and Windows paths. Linttests/unicode_case(a step ofrun_tests): a call ofstrings.to_lower/to_upper/equal_foldorunicode.to_lower/to_upperin the toolkit's sources fails unlesstools/unicode_case_allowed.txtlists its line with why; an entry that matches no line fails too.
0.71.02026-11-03release
Added
- Docking (
ui/dock_model.odin,ui/docking.odin,docs/docking.md): the app's panels in stacks of tabs the user rearranges. The layout is the app'sui.Dock, flat data: a tree of nodes in one#soaarray (leaves: a stack of panels and the one shown; splits: an axis, a ratio, the second side folded), floating roots at a place of their own, the panels (the app'su32ids) in tab order;dock_add,dock_move(into a stack at a tab, at a side of one, at an edge of the space, floating),dock_move_node,dock_redock,dock_close,dock_activate,dock_set_split,dock_layout,dock_target_at; one line of text (dock_format/dock_parse: unknown names dropped with the stacks they leave empty, the panels it does not name placed by the app, any other text refused with the dock left as it was).dock_begin/dock_panel/dock_endbuild it: a tab dragged out shows the drop targets (five squares over the stack under the pointer, four at the edges of the space, the strips between their tabs) and a translucent preview of where it lands; let go elsewhere it floats (an overlay of the window with a frame and a shadow, moved by its strip with the drop targets, resized by its corner, docked back by a double click); Escape cancels; the handles arepane_split's (minimum sizes, fold); close buttons, a middle click; the stack's menu reaches every drop target from the keyboard; the arrows along a strip, Ctrl+Shift+arrows move a tab, Ctrl+Tab goes through a stack; roles tab list, tab (controls its panel), tab panel, separator. Theme entriesdock_previewanddock_marker. Exampleexamples/docking; teststests/docking(the model and the text; the drags, the keyboard and the menu through the test session).
0.70.02026-11-03release
Changed
ui.build_events: a copy of the last build's input, kept by the state until the next build (it was the caller's slice, valid until its next poll).- Built with Odin dev-2026-11 (pinned by SHA-256 in the CI,
docs/getting-started.md):core:image's pixels are a[dynamic]u8, nothing keeps a reference to a stack past its procedure (the compiler's new escape analysis: the tests' globals are cleared,ui.usekeeps the caller's state on purpose),core:compress/deflatechecksflate's streams in the tests anddocs/flate.mdcompares both (flatestays for streams anddeflate_runs). wgpu-native stays 29.0.1.1.
0.69.02026-11-03release
Added
- Menus taller than the window scroll: a level (menu, submenu, nested menu, dropdown list) whose content is taller than the window is as tall as the window, placed again at each frame (a window made shorter while it is open), and scrolls: the wheel, its scroll bar (a press on it is not a press outside; the rows leave it room), the keyboard's selection brought into view (Up / Down / Home / End / Page Up / Page Down, type-ahead to a hidden row, a dropdown's current value from
menu_select). A menu that fits is placed as before (flipped to the other side of its anchor, else shifted to stay whole).ui.menu_item_rect(depth, idx): where an item was laid out. Testsmenu_taller_than_the_window_scrolls,menu_near_the_bottom_flips_or_shifts
Changed
- Menus are dense:
MENU_ITEM_EMis 1.9 (was 2.2): rows of 29 px for 15 px text, as Windows 11's and Chrome's menus (times the density: 22 px atDENSITY_COMPACT, 34 px atDENSITY_MAX); the padding above the first item and below the last is 0.25 em (was 0.3), a separator's gaps 0.2 em (were 0.25); the icon cell is as tall as the row. Testmenu_rows_follow_the_density
0.68.02026-11-03release
Added
ui.pane_split,ui.pane_toggle,ui.Pane: panes the user resizes from a few em (PANE_MIN_EM) to all the room but a few em for the other side, folds (a drag under half the minimum, a double click, Enter, a chevron button) to a strip that brings them back (a click, Enter, a pull), and places at the side or below (pane_axis,pane_place_menu); the app owns aPaneper split (sizes per orientation, folded, place) and saves it throughPane_Options.on_save;pane_format/pane_parsemake it a line of text;menu_move_tolists "Move to" items for a tab that may go to another strip. Keyboard (arrows, Home, End, Enter) and accessible (a separator with its value,expanded).examples/panesuses them. Teststests/panes/pane_split_test.odin
0.67.02026-11-03release
Added
ui.set_scrollbar_arrows: classic arrow buttons at both ends of the scroll bars of scroll boxes, off by default (the bars stay as they were): each as long as the bar is broad, taken from the track; a press scrolls a step (3 em, eased like the wheel), held it repeats after 250 ms every 50 ms while the pointer stays on it.ui.scrollbar_arrow_rectsgives where they are;draw.trianglefills a triangle in one-pixel rows (the arrows' glyph). Test arrow_buttons_step_and_repeat
0.66.02026-11-03release
Added
ui.menu_nested_open/ui.menu_nested_open_rect/ui.menu_nested: a menu opened from inside an open one, at a point or under a rect (a row's context menu in a drop-down panel), over it, the menu staying open behind it. It closes like a submenu: a press outside it closes only it, Escape closes it, choosing an item closes the menu.
0.65.02026-11-03release
Removed
- Public procedures nobody called (neither the toolkit, nor Wren, nor opencode_gui_toolkit):
draw.damage_padded,layer_cache_forget(an idle result goes after its frames anyway),picture_slot,picture_version;dbus.arg_variant_u32,read_variant_string/_bool/_u32,w_i16,w_i64;font.native_coverage(and the text backend'sgt_face_coverage),native_table;platform.a11y_prop_ids;render.canvas_pool_bytes,canvas_texture_changed,canvas_texture_size,damage_rects,heaps_reset,video_gpu_bytes;ui.autoscroll_is_enabled,post_queue_capacity.
0.64.02026-11-03release
Added
tests/globals: every package-level variable of the toolkit's packages (those ofwhenblocks and the procedures'@(static)ones too) is intools/globals_allowed.txtwith its class (const,process,thread,test) and why; the test fails on one not listed, one listed that is gone, and a class the code contradicts (@(thread_local)isthread). It reads the sources next to it, not from the working directory;-define:GLOBALS_WRITE=truerewrites the list keeping the classes known.docs/getting-started.md, "Where state lives", says the rule;CONTRIBUTING.md: a new global needs a class.tests/canvas: the asynchronous read-back (canvas_read_begin/_ready/_finish) gives the bytes drawn, the same ascanvas_read; one cancelled gives nothing and keeps nothing (its staging texture and query released, a finish after it writes nothing); a block out of the canvas is refused at the start; a block read across the canvas's corner is transparent beyond it.render.present_call: what a Present is asked for (the sync interval, the tearing flag, the mode the frame is presented in) from the mode asked, the tearing support, the interval andframe_end'svsync; both backends take it from there (wgpu'sImmediateorFifo).render/present_test.odin: tearing only where allowed and never with a wait, the interval clamped to DXGI's limit,vsync = falseat once.platform.file_dialog_call(Windows): what the Common Item Dialog is told for aFile_Dialog(options set and cleared, the type list and the line selected, file name, default extension, the GUID ofremember_id, the folder and whether it is forced), computed apart from the dialog;DIALOG_OPEN,DIALOG_SAVE,DIALOG_FOLDERname the kinds ofdialog_test_hook.tests/dialogschecks an open, a save and a folder dialog's call without opening one.font.glyph_images,font.slug_glyphs: the bitmaps / Slug data of a whole run of glyphs, the atlas lock taken once, shared, for those already made: threads that draw at once look up side by side (8 threads: 17 ns per glyph instead of 120; 1 thread: 7 instead of 11).draw.glyph_runuses them. Every lock of the toolkit, what it guards and what was measured: docs/locks.md;tools/text_bench -threadstimes the locked paths.
Changed
ui.set_window_button_namestranslates the caption buttons of the current State only (each window'sui.Statehas its own names, English by default): call it after eachui.init. The interfaces' clock starts once for the process from any thread (atomic).- platform: a thread's state is the thread's, the process's is locked or atomic. On Linux each thread that opens windows has its own connection to the display server (Wayland or X11: its events, its seat's focus and serials, its selections and drags, the cursor libraries made on it) and its own session bus (the a11y bus address, the colour-scheme subscription, the idle inhibitions); the loose Wayland and X11 state is in the connection's struct, the XCB functions bound on first use are in the library's table, and the Wayland protocol tables are built once with the library. On Windows OLE is initialized once per thread (
dragdrop_thread_init,drop_target_enableanddrag_startno longer callOleInitializeeach time), the system cursors and pointer settings are the thread's, and the window class is registered once (sync.Once). The virtual screen of the offscreen windows is locked; the build number, the tray's message and ids, the ABI check of the accessibility library are atomic. - A public
uiproc on a thread with no State (noinit,useorbegin_buildthere) stops at once withui.NO_STATE_MESSAGE, "ui: no begin_build on this thread", instead of crashing further on a nil State: one branch where the thread's State is read. - GPU profile: both backends fold a step's timestamp into the profile with one procedure (
profile_add: summed by kind, the slowest kept longest first, a clock that went back counts 0).render/gpu_profile_test.odinchecks it on made-up timestamps;tests/gpufxchecksset_gpu_timing: off nothing is measured, on each submit is timed andgpu_resultnames one of them with a time above 0. tools/tk: the PowerShell scripts of tools/ (build_text, build_a11y, build_naga, fetch_emoji, fetch_wgpu and the Linux fetch_wgpu.sh, gen_emoji_data, make_icon, make_logo, ppm_diff, emoji_board) are one Odin program, a command each with the same flags in Odin's spelling (odin run tools/tk -- fetch_wgpu "-dest:build",fetch_emoji -unicode,build_text -force,ppm_diff a.ppm b.ppm -tolerance:2...), the same outputs, pins and stamps;tools/shadersbuilds naga in process. The static libraries of native/ are built once more after the change (their stamp now covers tools/tk/native.odin). Only tools/release.ps1 stays a script.- Word cache: a shard's lock is shared by the threads that find their word in its current generation (a page's threads shaping the same words: 190 ns per word with 8 threads instead of 440).
Removed
- Wayland: the primary-selection listeners: the toolkit bound
zwp_primary_selection_device_manager_v1, made a device and tracked its offers, but never read one nor offered a selection (no source was ever made): the protocol tables, the listeners and their state are gone. On X11 a copy still ownsPRIMARY.
Fixed
- render, flate: no global raced on. A second window's swap chain is sized by an argument of
create_swapchaininstead of a global flag two renderers on two threads shared; the sRGB tables of the capture are made once (sync.Once) instead of behind a plain flag; the environment switches (TOOLKIT_RENDER_TRACE,TOOLKIT_DAMAGE_VERIFY) are read once atomically;qpc_to_tickasks the system's counter frequency instead of keeping it.flate.deflate_runs' code tables are made at start-up (@(init), as the fixed codes) instead of lazily. odin run tools/run_tests -- -backend:wgpu -group:ci1from PowerShell selected no step: PowerShell 7 hands a-name:valuetyped after--to the program as-name:andvalue(odin passes on what it gets; from cmd or bash the flag arrives whole).tools/cmdlinejoins them again, forrun_tests,changelogandtk; the docs sayodin runagain where they said to build the runner first.- IME (Wayland): the preedit's caret and the text the input method replaces wait for the protocol's
donelike the rest (the caret moved at once, the Backspace presses went out before thedone). The composition isplatform.Ime_Compose(ime_compose_preedit/_commit/_delete/_done/_reset, pure), the caret's rectangle in the compositor's unitsime_cursor_rect_logical(at least one unit high);tests/imefeeds them the protocol's events. - Tray menus (Windows): the default item of a menu is shown in bold (
AppendMenuWignoredMF_DEFAULT: it is set withSetMenuDefaultItem).platform.tray_menu_buildmakes the native menu of a model without showing it;tests/trayreads it back withGetMenuItemInfoW(separators, check and radio marks, the default item, a disabled one, a submenu, each command's id).
0.63.12026-11-02release
Changed
- The interface state a build works on is the thread's:
ui's current state (whatbegin_buildselects and the public procs use) is thread-local, so several apps of one process build and draw their interfaces at once, each on a thread of its own; before, two threads building at once broke each other's parent stack.
0.63.02026-11-02release
Added
render.preload_device: the graphics device made early, on a thread of its own, while the program does the rest of its start; the nextrender.init/init_window/init_offscreenthat asks for that kind of device (hardware or WARP) takes it, waiting for the thread if it has not finished, and makes only the swap chain or the offscreen target. The first device of a process loads the graphics driver, the longest step of a renderer's start. Its state is explicit (render.device_preload_state:Not_Started,Creating,Ready,Failed); a device of the other kind, or one that failed, is released and init makes its own as before;render.release_preloaded_devicelets an unused one go. On wgpu the instance, adapter and device are made without a surface: init makes the window's surface on that instance and makes its own device when the adapter cannot present to it.TOOLKIT_RENDER_TRACEsays how long init waited for it.
0.62.02026-11-02release
Added
devtools.start_local,devtools.Local_Client: the protocol with no socket, for a program or a test that drives itself;Toolkit.statesays how many pictures are being encoded.ui.set_clock: a state's time goes by a clock its owner moves: every delay and wake of the interface follows it,wake_in_msandwake_dueincluded.Build_Params.time_msstays for one build.testkit/session: an@(test)procedure makes an application with no window of the system on the offscreen renderer, sends input throughplatform.inject_event, runs frames, reads widgets and real pixels, with barriers on a state (wait_until,wait_idle,wait_box) and a clock of its own (advance);attachdrives an application that makes its own frames.
Changed
tools/changelogis an Odin program, tested bytests/changelog, in place oftools/changelog.ps1andtools/changelog_test.ps1;-Migrate(for an "## Unreleased" this repository does not have) is gone.- The tests are Odin:
odin run tools/run_testsruns every package on Windows and Linux (on Windows on a desktop of its own, and nothing takes the focus of whoever is at the machine), in place oftools/test_windowless.ps1,tools/test_backends.ps1and the test part oftools/linux_ci.sh; ten packages that no CI ran now do (window,cursors,tray,dialogs,clock,size_move,i18n,planes,video,textures).tests/e2e/run.mjsistests/examples/whatsnew_test.odin, in the test's process. Guide:docs/testing.md. tests/examples:examples/fidgetandexamples/todoare tested in the test's process with no window and no input of the machine (tools/keyboard_test.ps1,tools/fidget_test.ps1,tools/ui_test.ps1are gone, so is fidget's--state_title); the examples haveopenandclose, and a settings folder of their own for a test. Esc in fidget's command palette closes it, as its foot says, instead of only leaving its field.- Offscreen windows have a machine of their own:
platform.virtual_screen_setgives the screenmonitor_sizeanddisplay_infoanswer for them (no more{0, 0}),platform.set_offscreen_activetheir activation, and their clipboard is the process's store on Windows as on Linux: a headless program no longer reads or writes the clipboard of the person at the machine.
Fixed
- The caret of
line_editandtext_editwakes the loop at its flips only: the wake came at every half phase (265 ms), which drew each picture of the caret twice. - The plain text clipboard of an offscreen window on Linux:
get_clipboardgave nothing andset_clipboarddid nothing, whileclipboard_setkept to the process's store: a headless program could not copy and paste its own text. - One clock for the interface: a scroll bar's reveal (
scrollbar_reveal) and the merging of typing into one undo step in a line edit read the system's clock whateverBuild_Params.time_mssaid; a replay or a test with its own clock got other results.
0.61.02026-11-01release
Added
- Color glyphs off Windows: the COLR interpreter gets its tables from the native backend, and CBDT bitmaps (Noto Color Emoji on most Linux systems) are drawn from their PNG images (
font/cbdt.odin).font.glyph_is_colorandfont.color_glyph_rgbawork there. - Fractional scaling on Wayland:
wp_fractional_scale_v1withwp_viewporter, the buffer in physical pixels; a scale of 1.5 was drawn at 2 and scaled down by the compositor. - Cursors on Linux: on X11 from the theme (libxcb-cursor) or the server's cursor font, custom images through RENDER; on Wayland custom images through wl_shm and the theme (libwayland-cursor) where
wp_cursor_shape_v1is missing, and the cursor is sent again on each pointer enter.platform.cursor_on_screensays what shows. tools/doc_check.mjs, run by the CI: the files, flags, defines and package names the documentation cites must exist.tools/release.ps1retakes the About picture itself. The documentation no longer says Linux is missing where it exists, and says what it lacks (color glyphs, X11 cursors, Wayland fractional scaling).
0.60.22026-11-01release
Added
tools/theme_shots.mjs: a picture of a page ofexamples/whatsnewin every palette, with no window;--palette=<name>on that example.
Fixed
- Command line arguments keep every character on Windows:
os.argscame through the C runtime's ANSI code page ("Łódź" became "L\xf3dz", "Привет" "??????"), in paths and passwords too. Any program that importsplatformnow gets them from the system, in UTF-8.
0.60.12026-11-01release
Changed
- The letters of a match are drawn dark, in the theme's new
find_text, on strongerfind_matchandfind_currentbackgrounds: amber code and light text did not read on them (contrast 1.8 on the current match). At least 4.5 in every palette.
Fixed
- Find and selection show over inline code: a rich paragraph painted its runs' backgrounds over them, leaving a pixel at each end of a match. They now lie between the backgrounds and the text.
0.60.02026-11-01release
Added
Rich_Context.line_heightandrich_paragraph(line_height = ...): every line of a paragraph the same height, on a baseline from its base font, whatever other faces it holds (CSSline-height). Inline code no longer has to push a line apart.
0.59.02026-11-01release
Added
draw.image_coveranddraw.cover_src: an image that fills a box cropped rather than distorted (CSSobject-fit: cover).examples/effectsuses it: its photo was stretched into every thumbnail.ui.set_scrollbar_autohide: scroll bars show only while the pointer is over their box or one is held, and fade otherwise.ui.scrollbar_revealshows one for a moment when something else scrolls it (the keyboard);ui.scrollbar_opacitysays how much it shows. Off by default.
Changed
tools/screenshots.mjsreplacestools/screenshots.ps1: the pictures are the frames the examples render, over the DevTools protocol, with an empty settings folder; it no longer moves the mouse, raises windows or moves the user's settings aside. It also refreshessite/img.fidget,todo,transformsandeffectsanswer toTOOLKIT_DEVTOOLS.
0.58.02026-10-31release
Added
ui.scrollbar_rects: the track and the thumb a scroll box shows, for tests and tools.platform.window_on_destroy: a call when a window is destroyed, for what keeps a pointer to it and may outlive it.devtools: Ctrl+Z and Ctrl+Y in Chrome's DevTools:DOM.undoandDOM.redogo over the edits of styles, texts, removed boxes andaria-label, from mark to mark.devtools:aria-labelcan be edited from Chrome's DevTools (DOM.setAttributeValue,DOM.setAttributesAsText,DOM.removeAttribute): it changes what a reader gets. The other attributes are refused with a message that says why.
Changed
Text_Area.sb_dragandText_Edit_Draw.bar_hotare aui.Text_Scrollbar, not a number.Field_SnapshotandText_Snapshotare one type,ui.Edit_Snapshot.- Double click: widgets and selected text share one time, 450 ms (widgets counted 400 ms, selected text 450 ms).
Removed
ui.Picker_Draw.ring: never read.
Fixed
- A command to the DevTools server could wait until something else woke the window: its wake could be taken by the frame already starting, and the window slept with the command in its inbox. The ticker now wakes it for as long as a command waits.
examples/whatsnew: its title says the version it runs on, fromui.VERSION, and that its pages are the changes of 0.55 (it said "What's new in gui_toolkit 0.55" in every version).- Examples: a live resize no longer stretches the last frame in any example (composition swap chain,
render.set_fixed_surface,render.pace_frame;--benchofeffectsandtransformskeeps the ordinary swap chain its published figures are of).examples/hello, which a new program copies, has the three lines anddocs/getting-started.mdexplains them;tests/resize_probechecks a program from outside. font.colr_dump: every transform is listed with its matrix, read by the same decoder as the interpreter.font.colr_nests_glyph: a paint whose fields run past the COLR table now counts as unreadable (the interpreter already refused it), instead of being left to Direct2D.render.capture_poll_rawandrender.capture_raw_finish: the two halves ofcapture_poll, so that a read-back only costs the copy of its rows on the thread of the frames; the devtools take the picture on the encoder thread. The table of an fp16 window is no longer built for an 8-bit one.- WebGPU: a backdrop or a blend mode in a frame of a region reads the frame being composed, not the surface texture, which was still empty.
- WebGPU: frames of an offscreen renderer wait for the device: with no present to pace them they piled up on a slow device, and a read-back waited behind them for up to minutes.
render.damage.verifycompares with the frame really drawn whole, all its steps run again on the side, instead of a recomposition that sampled pooled textures already reused.-define:TOOLKIT_DAMAGE_CHECK=trueruns that proof on a program that marks nothing.draw.destroyleaves the context empty:draw.initon the same struct appended to the lists it had freed, and frames drew garbage.- Partial redraw: a frame with a backdrop or a blend mode into the frame is drawn whole: outside the region it would have read the last frame's finished pixels, and a kept backdrop kept them.
- Partial redraw: a batch that reaches two rectangles of a region no longer draws what lies between them, where translucent text and shapes piled up on the last frame; rectangles fused over the budget no longer overlap (drawn twice).
rich_paragraph: aversiongiven again for shorter runs, or two contents with one hash, lays the runs out again instead of indexing past them.ui.destroyafterplatform.destroy_windowno longer writes to the freed window, and a post no longer wakes it: the State forgets a window when it is destroyed..Hot_Withinsees as far up the tree ashot_within: 64 ancestors (it stopped at 48).- Toolkit drawing at high DPI: the tab underline, the rings of check boxes, radios and color pickers, the slider's ring and the shadows of sliders and switches,
hline, what takes a press around a scrollbar and its least width follow the scale. At 100 % nothing changes. - Linux, Wayland: a clipboard or drag source and a selection offer given up are destroyed on the compositor too (only the client side was freed).
- Windows: a drag of the app over another of its windows is taken as over the window it came from (the thread in
drag_startcannot answer before the drop). wait_event(offscreen window, Linux): events already handed out by the last poll no longer end the wait at once.- Linux:
open_url,open_pathandreveal_in_folderleave no zombie process. - Linux, Wayland: only a window made a drop target with
drop_target_enabletakes a drop, as its answer indrop_set_effectsays;drag_startshows its picture; the press that began a drag gets its release when it ends. - Linux, Wayland: closing a window no longer ends the connection with a protocol error (a wrong request destroyed its decoration).
- Linux, X11:
get_placement,outer_rectandclient_originread the window's position (they said 0,0); a placement saved and restored under a frame does not move the window. - Linux, X11: a waiting poll no longer sleeps over events already read from the connection.
- Linux, X11:
clipboard_changedsees any program taking the clipboard, through XFIXES. - Linux, X11:
display_infogives the rate of the monitor under the window, read from RandR, and follows it when the window moves or the mode changes; 60 Hz only without RandR. - Linux, X11:
drag_startshows its picture (Drag_Image), carries the formats of the app's own (Drag_Data.custom) and followsXdndProxy. - Linux, X11: with several windows, each event goes to the window it names, not to the window that polls; drops report the pointer's position (it was always the corner of the screen).
- Linux, X11: copy and paste with other programs work: requests and answers were read at the wrong offsets of their events, and the transfer of large data never advanced.
devtools:frameMs(Toolkit.metrics,Toolkit.frames, timelines) is taken before a capture is read back, and a capture's conversion to RGBA moved to the encoding thread: a frame that reads a picture back no longer reads as late, with the next one early, although no refresh was missed.devtools: the screencast takes one more capture after a change, so what the program does in answer to the frame just shown is not lost as its last frame.dbus: a bus that goes away with something of ours unread (ECONNRESET) is a lost connection: pending handlers run, as on an orderly end.dbus.call: a call in a handler while another call waits: each gets its reply, whatever their order, where the outer one timed out; what an outer call had read no longer waits for the inner call's timeout.- Recorder: with
injected, an action sent byToolkit.axActionis recorded, as apressline that does it again. - Recorder: the horizontal wheel is recorded, and
wheel <x> <y> <notches> [<across>]plays it (tools/cdp.mjs). devtools: what Chrome's frontend asks as it goes is answered (CSS.takeComputedStyleUpdates,Storage.getStorageKey,DOM.getNodesForSubtreeByStyle,DOM.getAttributes, its check for form-associated elements) instead of filling its log with errors.devtools:DOM.getDocumentandDOM.requestChildNodesrespect the depth asked. Children the client has are no longer sent again (Chrome's frontend made new nodes of them, and what it was unfolding stayed empty), and a node not unfolded says when its number of children changes (DOM.childNodeCountUpdated).font: the native backend no longer takes quadratic time on a very long word without a space.flate.deflate_push: a stream past 2 GiB no longer stops finding matches.flate.inflate_push: a big block that arrives in many chunks is no longer decoded again from its start with each one (quadratic time): its output comes as its input arrives. Inflating and deflating no longer fill the temporary allocator.font.color_glyph_rgba: emptying the outline cache of the COLR interpreter freed heap memory with the allocator of the caller: a caller with its own allocator got bad frees.font: when Direct2D could not make its device for color glyphs, the locks held at that moment were zeroed under their owner: the failure now only gives back the Direct2D objects.font: a hostile COLR font can no longer hang the drawing thread with an outline of many crossing edges: the scan conversion of a glyph counts against its work allowance, and a glyph over it is refused.font: a hostile COLR font can no longer exhaust memory or hang through color lines: the stops of a gradient count against the work allowance of a glyph (a graph that reads too many is refused and shows its outline), and finding a stop no longer walks along them.- A device lost on one renderer no longer fails the read-backs of another (
draw.offscreen_end,render.readback_poll), and the table of read-backs is safe when renderers live on several threads. - A buffer refused or not mapped during a frame no longer writes through a null pointer (Direct3D 11): the frame draws what it can and
frame_endreports the lost device. render.kept_targets_bytescounts asrender.targets_bytesdoes: a kept snapshot is 4 bytes a texel, and mip chains count.render.capture_cancelon WebGPU no longer lets the map it cancelled write on the next capture, and a capture being mapped survivessurface_use.draw.destroygives back the context's glyph atlas texture: a context made and destroyed on a renderer that stays left it behind.- A layer in a blend mode composited right after a filter pass replaces what it read, as any other (Direct3D 11): with nothing drawn in between it blended over translucent pixels and changed their alpha.
render.set_gpu_profileno longer erases the frame's clear color on a window (Direct3D 11): what no draw covered turned black while profiling.- 2D canvas: a clip that lies off the canvas, or one made while a layer is open, no longer sends what is drawn next to the wrong target (into the clip mask, or to the canvas instead of the layer).
- Partial redraw (
draw.damage_begin,render.set_partial_redraw) works as documented on both backends: on Direct3D 11 it never took effect on a window and wiped what lay outside the region offscreen; on WebGPU a window crashed on it, the region was not cleared first, and a frame without a region left the next one showing stale pixels.- A frame with a region now composes into a kept target, copied whole to the swap chain (the swap chain cannot present rectangles). A region of nothing presents nothing. Frames without
damage_beginare drawn as before. damage_scrollredraws where the content was and is: no backend moves pixels by a block copy.render.damage_verify_differencescounts the framesrender.damage.verifyfound different, and the check now runs on Direct3D 11 and draws Slug text and application shaders on WebGPU.
- A frame with a region now composes into a kept target, copied whole to the swap chain (the swap chain cannot present rectangles). A region of nothing presents nothing. Frames without
- Wrapped text: the lines kept between builds no longer live in the allocator of the context at the first wrap (a program that changes
context.allocatoraround a build freed them from another one). ui.fuzzy_scoreno longer panics on a text with invalid UTF-8 that matches the needle, and its ranges stay in the text.rich_paragraph:rich_set_facesapplies to paragraphs already laid out, and the layout cache no longer depends on the padding bytes of a run's style.ui.postthrough a window: afterui.destroyevery window the State was built for returns false, not only the last one (the others used a queue that was gone).line_edit,text_edit: a value set by a screen reader can be undone, and a selection it asks for in the same build lands on the new text.line_edit,text_edit: Backspace at the start or Delete at the end of the text is no edit: it no longer empties the redo history nor adds an undo step.- Autoscroll: the distances and the speed of
Autoscroll_Settingsare logical pixels as documented: at 200 % the dead zone was half as large and smaller than the marker. - Autoscroll: a middle press on a box that scrolls itself (
text_edit) no longer starts a session that scrolls nothing and swallows the press; the scroll region around it scrolls, if there is one. - Autoscroll: the click that ends a latched session does nothing else (it moved the focus, jumped a scrollbar and reached the selection).
ui.modal: Escape goes first to what is inside the dialog (a focused field getscancelled), then to the dialog on top, not to the outermost one. The dialog closes at the end of its block.ui.list_selectbefore a list's first build, or for a row added in this frame, is no longer lost: the row is checked against the rows actually built.ui.selected_textduring a build no longer returns "" when the selected boxes are not built yet (a Copy button above the text): it answers with the text the last build showed.- Linux:
clipboard_changedtells a change once (it said true at every call while another program owned the clipboard),clipboard_formatssays what the owner offers,set_clipboardoffers its text under the text targets, and file lists are percent-encoded URIs. - Linux:
wait_handlewith no limit called inside a frame callback waits for the handle (it returned at once). drag_image_fit: the colours stay right when a large picture is brought down to a few pixels (the sums overflowed).- Windows:
get_placementreturns screen coordinates: with a taskbar on the left or top a saved window no longer moves at each restore. - Linux, X11:
is_minimized,is_maximizedandis_fullscreenfollow the window manager (a restored window stayed minimized). - Linux, X11: a read of a selection returns the whole data or nothing, never a truncated part, and a slow owner's transfer goes on as long as chunks come.
- Linux, X11: a drag no longer makes the app take itself for the clipboard's owner, and losing PRIMARY is no longer taken for losing the clipboard: paste reads what another program copied.
- Linux, X11:
drag_starttalks XDND to the window under the pointer (it reached no window), reports the effect the target took, and no longer leaves the press stuck; drops report client positions and followdrop_set_effect. - Linux, Wayland: a click before any motion lands where the pointer is on a scaled screen (the position on entering the window was not scaled).
- Linux, Wayland:
drag_startreturns no effect for a drag that was cancelled (it returned the effect of the one before or the action last hovered). - Linux, Wayland: reading another client's selection gives up when the peer stays silent instead of freezing the interface, and dropping the app's own drag on its own window no longer hangs it.
- Linux, Wayland: a held key repeats, at the rate and delay of the compositor (
Event.repeat). - Linux:
wakewakes a real window's loop on Wayland (it did nothing) and no longer writes on X11 the property a paste reads, which could corrupt the pasted data. - Linux, Wayland: a loop that polls without waiting (an animation) now receives input and answers the compositor:
poll_events(w, false)reads the socket;wait_handleno longer spins while input waits. tk_instance(Linux): a runtime directory that is not this user's alone (/tmp/tk_instance-<uid>made first by someone else) is not used: the start carries on by itself.dbus.call_async: the handler runs when the bus goes away whatever its deadline, as documented; a message that cannot be one ends the connection instead of stalling it while its buffer grows.dbus: a full socket is waited for instead of being taken for a lost connection, so a burst or a large message no longer ends it.dbus.call: a reply followed by another message in the same read (Hello and NameAcquired) is no longer wiped before the caller reads it, which could makeconnect_busfail now and then. A handler that calls no longer meets its own message again.tk_config.restore: bringing back a value over a saved user one marks the preferences dirty: the next start no longer gets back what the restore took away.devtools: a connection ending at once can no longer be reaped before its thread is known, andacceptfailing for good no longer takes a core.devtools:infandnanare no longer taken as a CSS length, opacity or colour channel of an edited style.devtools: twoPage.captureScreenshotat once each get their ownclip: the second used to get the picture of the first.devtools: what cannot be edited (DOM.setAttributesAsText,DOM.setOuterHTML, otherDOM.set*andCSS.set*,Page.setDocumentContent, cookies, DOM storage) answers an error instead of a false success; what clients send on attaching is still accepted.devtools: an edit lasts as long as its client also when it never asked for the document, and a second client taking the tree undoes the edits of the first instead of inheriting them for good.devtools: a text replaced from Chrome's DevTools, or edits taken back when it leaves, no longer leave the boxes of the last build pointing at freed memory (ui.Box_Edit's text is copied into each frame).devtools:Page.stopScreencastwhile a frame was being read back no longer leaves the renderer unable to capture:Page.captureScreenshotanswered "the renderer cannot capture now" for good and a later screencast sent nothing.
0.57.12026-10-31release
Fixed
- The screencast could miss its last frame: input sent while a capture was coming back, or the last step of an animation, counted as nothing changed. Tests: the end-to-end run waits for the frames to stop instead of fixed delays, and what still depends on a machine's timing (a first frame, the last one) is said as FLAKY without failing it; frames that never stop still fail it.
- Closing Chrome's DevTools could leave a window on freed memory: the connection was freed while the DOM side still held it, so the edits could stay and the program could hang. They now go with the client, every time.
0.57.02026-10-31release
Added
- Chrome's DevTools on a window: the Elements panel shows the box tree and follows it, Styles, Computed and Layout a box's rect and colours in sRGB, Styles and the text editable live for as long as DevTools is attached, the Accessibility tab what a reader gets, hovering a node lights its box.
tools/cdp.mjs inspectprints the address; thedevtools://devtoolsOrigin is taken on the WebSocket only.
0.56.22026-10-31release
Fixed
examples/whatsnewno longer stretches while it is resized: a composition swap chain with a fixed surface, as opencode native, andinner = truefor the frames of a live resize. The other examples keep the ordinary swap chain, which stretches.- A lazy column's height changed at every frame when an item showed nothing: its height of 0 counted as not known yet, so it stood for the mean of the others as a space and for nothing when built. A scroll box following its bottom had its scroll bar jump, without end.
0.56.12026-10-30release
Added
flate.deflate_runs: compression at a high rate for what repeats in known ways, such as the rows of a picture: runs and matches a row above, in one block of the fixed Huffman codes.
Fixed
- A screenshot of a real window took two seconds and froze it: the wake asked for its next frame was consumed by the frame already starting, so the window slept until its next timer (about 1.2 s); reading a translucent window's fp16 target back ran
powon every channel (now a table); the PNG was encoded on the UI thread (nowflate.deflate_runs, made for rate, on a thread of its own). A timeline keeps its pictures in memory and compresses, compares and writes them when it ends. An offscreen window keeps its wakes, so the end-to-end run never saw the first one.
0.56.02026-10-30release
Added
- Timelines:
tools/cdp.mjs record --timeline <dir>keeps a run whole (each action with when it happened, a picture after it, every frame's time), as a page that stands alone, atimeline.jsonand a Chrome trace;play <timeline.json> --timeline <dir>runs it again,--realtimewith its pauses, and fails if a picture differs.tools/timeline.mjsfor scripts.Toolkit.framesandToolkit.pollRecordingfeed it. Page.startScreencast: the window's frames as events, one at a time, limited in rate, none at rest;tools/cdp.mjs cast <dir>.
0.55.02026-10-30release
Added
tests/e2e, in the CI:examples/whatsnew --headlessdriven over the protocol: no layout warning, nothing a reader cannot name and the reference image on every page, the scenarios that broke before, exit code 0.Toolkit.waitForandToolkit.waitIdle: a script says what it waits for (a box there or gone, nothing moving) instead of sleeping a guess;wait-forandidleintools/cdp.mjs.- A recorder:
Toolkit.startRecording/stopRecording(tools/cdp.mjs record,play) write what a person does as a script, clicks by what they hit rather than where. Toolkit.metrics: build, layout and frame times (p50, p95, max) of the last 240 frames, boxes, lazy items built, the accessibility tree's cost, and the latency input to screen in aRENDER_PROBEbuild.ui.build_metricsreads the last build's from the program.- Accessibility actions and audit over devtools:
Toolkit.axActionasks an action of a node by role and name as a screen reader does,Toolkit.axAudit(ui.a11y_unnamed) lists the controls a reader cannot name, which are not even nodes of the tree. devtools: a window driven over the Chrome DevTools Protocol:devtools.start_from_env(win)(TOOLKIT_DEVTOOLS=<port>, 127.0.0.1 only) anddevtools.serviceeach frame. Input.dispatchMouseEvent / dispatchKeyEvent / insertText go into the window's queue (no mouse or keyboard of the machine, no focus needed), Page.captureScreenshot returns the presented frame as PNG, Accessibility.getFullAXTree and queryAXTree the tree a screen reader gets, and the Toolkit domain reads the box tree (getTree, find, click by name or text). A request from a browser's page (anOriginheader), for anotherHostor without the target's random id is refused. Puppeteer and Playwright attach (docs/devtools.md says what works).tools/cdp.mjsdrives it from the command line; docs/devtools.md.ui.box_textandui.box_currentread a box from outside a build.examples/whatsnew: a page per change of this version, each shown live (frames presented at once, items a lazy column built, build times, the layout warning, the emboss, next_merge, find in page, box_at).ui.box_at(ids..): the box of the last build at a path of ids from the root, for a test, a tool or an overlay working from outside a build.ui.next_merge(m): adds to the pendingnextinstead of replacing it, so a helper styles its box while its caller's width or position stay.ui.merge_modis public and merges every field (flags added).- A lazy column,
ui.lazy_begin/ui.lazy_item: inside a scroll box only the items near the view are built, the others stand as one space of the height they had when last built (5000 wrapped paragraphs: 3 ms a frame instead of 100, a first frame of 12 ms instead of 1.4 s). An item never built stands as the mean height; one built at another width keeps its old height until it comes near the view; the view stays on the item it showed when heights change (after the layout, in the same frame); it also builds around where the view is heading (a wheel's burst), and asks for one more frame when the view still shows spaces.lazy_begin(all = true)builds every item (find in page). Likerow(), the item is the block of theif: a column whose items all close empty (if !lazy_item(...) { continue }) is warned of. rich_paragraph(..., version = v): with a version the layout cache finds the paragraph by its box and version instead of hashing all of its text at every frame; the caller changes the version when the text changes.- Layout warnings: a percentage inside a box sized by its children (it resolves against a distant ancestor, often the window) is said once per box on stderr when it grows past half the window. On in debug builds;
ui.set_layout_warnings(s, on), andui.layout_warnings()returns the messages for a tool or a test. platform.input_pending(w),platform.event_is_input(kind): whether input waits in the window's queue, read without taking it (the events stay for the nextpoll_events).render.Pacer,render.pace_frame,render.paced_present: the cadence model executed for a window, as applications wrote it by hand: the screen's plan adopted when the display changes, the swap chain's slot waited for, its latency set. It does not show input after a rest at once: measured, that one frame left the swap chain a frame deeper for the rest of the session.examples/hellouses it; docs/pacing.md.
Changed
- docs: font-backend.md shows how a program with a Rust crate of its own builds the text backend into it (one Rust standard library per program) and why the prebuilt wgpu-native cannot join it on Windows; text.md documents find in page and its limits.
- Tests that drive the global UI state refuse to run on several threads: keyboard, status, states, ui_robust and a11y fail to compile without
-define:ODIN_TEST_THREADS=1, with a message that says why (they failed with bad frees or never ended).tools/test_backends.ps1passes the define to a11y too. - A frame less between input and screen (Direct3D 11): the renderer takes one of the swap chain latency object's counts for good, so a frame starts when the last one is on screen instead of queueing behind it. Measured in opencode native (RENDER_PROBE, a fast hand over a list, six runs each): 49 ms from input to screen before, 34 after, still 60 frames a second. The price: a frame that takes longer than a refresh waits for the next one.
Fixed
- Find in page no longer reads freed memory when its scope is not built any more (a page left while the needle stays): such a scope finds nothing. It crashed the next layout.
- WebGPU: no white window while the first frame compiles: on Direct3D 12 with FXC the first frame's pipelines take seconds; the renderer presents the theme's background first, and says on stderr to put
dxcompiler.dllanddxil.dllnext to the program when that first frame took over a second. - The wheel moves the hover: rolling the wheel without moving the mouse makes the pointer's position valid again, so the highlight follows the row that came under it instead of staying on one scrolled away.
- Embossed text by contrast (Gradient finish): the groove or the light edge comes from the text against what is under it (the box's own background or the nearest opaque one up), not from the text alone: grey text on a dark button no longer takes a light edge under it.
0.54.02026-10-30release
Added
Build_Params.smooth_scroll(Follow,On,Off): whether scrolling glides, as the interface's animations say (the default) or always / never. An application can keep the wheel's glide when the system turns animations off (Windows' "Animation effects"), where every notch used to jump.
0.53.02026-10-30release
Added
- Find in page (
ui.find,ui.find_matches,ui.find_count,ui.find_set_current): the occurrences of a needle in the text the interface shows (labels, wrapped paragraphs, rich text, a match across the runs of a paragraph), found after each layout in reading order with their rectangles whether on screen or scrolled away, painted behind the text (theme entries "find_match background", "find_current background"); case ignored. The application owns the field and the navigation.
0.52.02026-10-30release
Added
tools/slugsmall: DirectWrite's bitmaps against Slug with each combination of its small-text settings, stacked in one PNG per scene and scale with a 3x enlargement (odin run tools/slugsmall -define:FONT_BACKEND=native).- Slug: four coverage samples per pixel (
font.Settings.slug_supersample_ppem) for glyphs at that size or less, on a rotated grid, each over its own half-pixel box. Off by default. - Slug: stem darkening (
font.Settings.slug_darken): every edge of small glyphs moves outwards by that many pixels at 12 px per em and less, fading out by 36, on the side the glyph's winding says (Slug_Glyph.orient). Off by default. - Slug: hinted outlines for small text (
font.Settings.slug_hint_ppem): screen-aligned glyphs at that size or less use an outline hinted for their size by the native backend's light auto-hinter (skrifa: vertical only, the baseline and x-height on whole pixels), cached per quarter pixel;gt_glyph_outline_hinted,Text_Backend.outline_hinted. Off by default.
Fixed
- Segmented controls: the labels are no longer engraved (
button segment sheen0): lit below on the pill and shadowed above on the track, the selected and the other labels looked a pixel apart.
0.51.02026-10-29release
Added
- A site on GitHub Pages (sachahjkl.github.io/gui_toolkit):
site/, published by.github/workflows/pages.yml; the README tells about Linux. - A Linux job in the CI (
tools/linux_ci.sh, also for a local check in WSL):odin check -vetof every package for Linux, the examples built with the native text backend and Vulkan, the tests that need no window.examples/smokeprinted the window's HWND, a field Windows only has. tools/fetch_wgpu.sh: wgpu-native's Linux static library, pinned by SHA-256, into Odin'svendor/wgpu/lib(Odin's Linux release does not ship it).ui.fuzzy_score: subsequence matching for pickers and palettes ("cs5" finds "claude-sonnet-5"), the tightest window, a score that ranks runs and word starts first, and the matched runs to highlight.ui.mouse_pressed: whether a press happened in this frame, wherever it landed (a popover closed by a press outside it).ui.line_edit(..., secret = true): a password field. Bullets are shown in place of the characters (the caret, the selection and the clicks follow them character by character), a double click selects all of it, Ctrl+C / Ctrl+X do nothing, and screen readers are told it is a password.
Changed
- Middle-click autoscroll (
ui.autoscroll_axis_speed): the speed grows geometrically with the distance from the anchor,V * (2^(x/D) - 1) / (2^(F/D) - 1)(xpast the dead zone,D= the newAutoscroll_Settings.doubling,F=full_dist,V=max_speed): it doubles everyDpx and reachesmax_speedatfull_dist.exponent(a power curve) is gone.
Fixed
-vetfailed on the Linux code: dbus casts, unused loop variables in the X11 backend, imports used on Windows only.-define:FONT_NATIVE_LINK=falselinkedkernel32.libas its stand-in on every system: libc off Windows.- Linux: the text renderer defaulted to the atlas, which nothing fills off Windows (no bitmap rasterizer): Slug there (
FONT_TEXT_RENDERER). - Linux: a translucent window asked for a half-float surface that Vulkan on X11 / Wayland does not offer (BGRA8 only): the surface's formats are checked, 8 bits a channel otherwise.
- Linux: the WebGPU backend defaulted to Direct3D 12 on every system: Vulkan off Windows (
TOOLKIT_WGPU_BACKEND). Found by building opencode native for Linux, like the fixes below. ui.text_edit(wrap = true)could show a horizontal scrollbar: a wrapped text's width is the one of the last layout, and when the box became half a pixel narrower the bar appeared, though wrapped text never scrolls sideways.ui.row/ui.columnkeep a cross alignment given bynext:next({cross = .Center}); if row() { ... }centred nothing, the scope's default (.Start) replaced it. An explicitcrossargument other than.Startstill wins.ui.checkbox,ui.radio,ui.togglegiven no width are as wide as their mark (or switch) and their label: the row took the text size of its own text, which is empty, so the clickable part was a sliver at the left of the mark (a click on the label did nothing). A width given bynextis kept, the label filling it as before.
0.50.02026-10-28release
Added
font.glyph_outlineand a neutralfont.color_renderable:glyph_outline(state, face, gid, tolerance, out)hands a 2D canvas (or any glyph-path consumer) a glyph outline through the active backend (DirectWrite or the native Fontations backend) rather than a DirectWrite-only entry point, andcolor_renderablenow answers on every system (off Windows the built-in COLR / CPAL interpreter draws colour faces). Both let an application draw glyph paths and build colour-font cascades the same way on every platform.platform.raise_window(window)andplatform.is_window_visible(window): bring an already-shown window to the front (Win32SetForegroundWindow; X11_NET_ACTIVE_WINDOW; a no-op on Wayland, where the compositor owns focus), and whether a window is on screen (mapped and not minimized). An application no longer calls Win32 for the "switch to this window" and "is this window visible" it needs around multi-window and capture flows.- Linux accessibility, IME and smaller desktop services (
platform): a minimal AT-SPI 2 accessibility bridge over D-Bus (a11y_send/a11y_state/a11y_take_requestexpose the toolkit's neutral tree — roles, names, states, bounds, Invoke / Focus — to screen readers when one connects); input-method composition through Waylandzwp_text_input_v3(ime_set_enabled,ime_set_cursor_rect,ime_preedit; committed text arrives as.Textevents, and X11 composes dead keys through libxkbcommon); keeping the screen awake (idle_inhibit/idle_uninhibitthroughorg.freedesktop.ScreenSaver); the recent-files list (add_recent_fileto the freedesktoprecently-used.xbel); and Wayland cursor shapes drawn by the compositor throughwp_cursor_shape_v1. - Linux file dialogs, open-URL and live theme through the XDG portals (
platform):open_file_dialog/save_file_dialog/pick_folder_dialognow useorg.freedesktop.portal.FileChooserfirst (filters, multiple selection, a suggested name; it works under a sandbox), falling back to zenity / kdialog;open_url/open_pathuseorg.freedesktop.portal.OpenURI;dark_mode_preferredreads the appearance portal'scolor-schemeand the newwatch_color_scheme(cb)reports a live light / dark switch (theSettingChangedsignal). - Linux tray icon and notifications (
platform):tray_create/tray_notifyand the rest of the tray API now work on Linux through the StatusNotifierItem specification over D-Bus, with its menu as acom.canonical.dbusmenutree and notifications throughorg.freedesktop.Notifications(clicks, chosen menu items and notification results come back asTray_Events). Same public API as the Windows backend;tray_createreturns nil when there is no status-notifier host, as Windows does when the shell refuses an icon. - Linux cross-process clipboard and drag-and-drop (
platform): the clipboard owner is now complete on both backends — a full Waylandwl_data_sourceoffering text / HTML / PNG /text/uri-list(and reading a foreign owner overwl_data_offer.receive), thezwp_primary_selection_v1primary selection, and an X11SelectionRequestserver withTARGETS/TIMESTAMP/MULTIPLEand INCR for large images;clipboard_get_image/_get_html/_get_filesread whatever another process owns. Drag and drop works both ways with the same API as Windows (drop_target_enable,drop_data,drag_start,drop_inject): Waylandwl_data_deviceand X11 XDND version 5. - D-Bus client (
dbuspackage): a small, pure-Odin D-Bus client for the toolkit's Linux desktop services, with no libdbus link (a binary starts on a machine without it and reports the bus unreachable). It connects to the session or system bus (connect_bus/connect_bus_address), authenticates with SASL EXTERNAL, does the Hello handshake, marshals and unmarshals the whole type system (basic types, strings, object paths, signatures, arrays, structs, dicts, variants, with the spec's alignment and byte order), calls methods synchronously (call) or asynchronously (call_async), subscribes to signals (subscribe), exports objects (export_object,request_name) and folds its socket into a program's ownpoll(2)loop (conn_fd,dispatch).dbus/*.odin, docs/dbus.md. - Test building blocks (
testkit.Ports,testkit.process_state): a new package with the parts of a test harness that are pure Odin and not tied to any one application.Portsgives each test run its own block of test-server ports (an application sets a base-port environment variable, a default base and a span): the tests write the default ports andurl/portmove every address to the run's block so several worktrees test side by side, interning the results.process_statereads the process's resident and private memory, its open handle count and (on Windows) its GDI and USER object counts for endurance and leak tests, andmemory_split(Windows) shows where the private bytes are — the heaps' allocated against committed, and the private committed pages outside the heaps split read-write vs executable.testkit/ports.odin,testkit/process_<os>.odin,tests/testkit, docs/testkit.md. - Single-instance mechanism (
instance.acquire): a new package so an application can keep one running copy per named resource. The first process to start with a name owns it; a second start hands an opaque message to the first and comes forward instead of opening a second copy on the same state. The interface is platform-neutral — a name (key(prefix, path)), an optional identity token the owner checks so only a copy it trusts hands over, an opaque[]bytemessage the package never reads, and a handler the owner runs while it pumps messages — with no window handle, executable semantics or OS type crossing it. Under it: a named mutex and a message-only window (WM_COPYDATA) on Windows, aflocklock file and a Unix socket in$XDG_RUNTIME_DIRon POSIX (instance_windows.odin,instance_linux.odin,instance_other.odin). An owner that is starting or closing is waited for; a silent one does not hold the second start.acquire,send,owner_start/owner_closing/owner_stop,pump_messages,raise_window,is_elevated.tests/instance, docs/instance.md. - Lifetime and ownership primitives (
arena.Scope,arena.Pool): a new pure-Odin package with the two primitives an application builds its object graph on. AScopeowns a growing virtual-memory arena plus a reverse-order cleanup list and may have child scopes, so closing a scope closes its children, runs its cleanups and frees the arena in one go — an application is a tree of scopes and "close the view" frees everything without any subsystem keeping a list of what it owns (scope_make,scope_allocator,on_close,forget_cleanup,rebind_cleanup,run_cleanup,scope_close). A generationalPoolgives fixed-address slots addressed by aHandle(adistinct u64, generation + index) that can never dangle: removing an object bumps the slot's generation so an old handle fails to resolve instead of reaching whatever reused the slot (pool_insert,pool_get,pool_valid,pool_remove,pool_handle_at,pool_next,pool_destroy).arena/scope.odin,arena/pool.odin, in-package tests, docs/arena.md. - Typed preference registry (
config.Prefs): a new pure-Odin package — an about:config for any application. Every tunable is a named, typed, documented preference with a default, optional bounds or a choices list, a unit, and a layered source (default < user file < command line < policy, where a policy value also locks it). The registry validates every change, resolves a name to aRefonce so hot-path reads never hash a string, notifies observers by name prefix (config.watch, apply without a restart), persists only the user's changes atomically (save_user/load_file, values read before their definition applied later and surviving a round trip), snapshots and restores the whole set (snapshot/restore), and derives readable titles, a first-sentence summary and a word search for a settings UI (title_of,summary_of,matches).config/config.odin,config/snapshot.odin,config/titles.odin, in-package tests, docs/config.md. platform.monitor_size(window): the pixel size of the monitor a window is on, neutral across systems (Win32MonitorFromWindow+GetMonitorInfoW, the Waylandwl_outputmode, or the X11 root screen). An application that wants a fixed surface the size of the screen (render.set_fixed_surface) no longer needs its own Win32 call.- Linux desktop services (
platform): file dialogs (open_file_dialog/save_file_dialog/pick_folder_dialog) through zenity or kdialog;open_url/open_path/reveal_in_folderthrough xdg-open;known_folder_pathfrom the XDG user directories; light / dark (dark_mode_preferred) and reduce-motion (os_animations_enabled) from the GTK settings; the rich clipboard (clipboard_set/clipboard_get_*) with text carried over the X11 CLIPBOARD selection and the Waylandwl_data_device, and HTML / image / files kept by the owner.render/wgpu_surface_linux.odinbuilds the wgpu surface fromSurface_Handles(Wayland / XCB) for the renderer's Linux path. - wgpu is the renderer's Linux backend: on a non-Windows target
renderbuilds the WebGPU backend only (TOOLKIT_RENDERis ignored there andUSE_WGPUis forced on). The Direct3D 11 backend (render_d3d.odin,frame.odin, the canvas, slug, video, app-shader and damage files) is now built only for the Direct3D backend (when !USE_WGPU, Windows); the shared types (Renderer,Texture_Data, the frame description) and the backend-agnostic procedures build on every system. The wgpu surface comes fromplatform.Surface_Handles(Waylandwl_display+wl_surface, or XCB connection + window); present pacing on Linux is the FIFO vblank (there is no DWM timing or waitable-timer latency object). wgpu-native must be built for Linux (libwgpu_native.a). - Linux window backend (
platform):platform.open_windowand the whole window / input API now run on Linux, over Wayland (xdg-shell,wl_seatwith an xkbcommon keymap,wl_outputscale for DPI, zxdg-decoration or a client-side title bar) and X11 (XCB, xkbcommon,Xft.dpi,_NET_WM_STATE, Motif hints), chosen at open fromWAYLAND_DISPLAY/DISPLAY. Every window library is loaded withdlopen(libwayland-client, libxcb, libxkbcommon), so a binary starts on a system missing one and names it; nothing is hard-linked. The sameWindow,Event,Key,Cursor,Placementtypes as the Win32 backend — an application written againstplatformdoes not change. - Text catalogue (
i18n.tr): a new pure-Odin package so an application can be translated without going through its code again.tris a procedure group with three forms —tr(text),tr(context, text)(a translator's context for two meanings of one English word) andtr(count, one, other)(count-dependent forms, the choice left to the language). The English literal is the key; today each call returns its argument (same bytes, no lookup, no allocation), so a call in an interface drawn every frame is free, and a language can later swap in one flat pointer-keyed table without any other change.i18n/i18n.odin,tests/i18n, docs/i18n.md. - Partial redraw (damage regions) (
render.set_partial_redraw,draw.damage): the renderer can now repaint and present only the part of the window that changed since the last frame (a caret, a hover, a progress bar, a video, a local animation) instead of the whole window. The application opens a region withdamage_beginand marks changed boxes withdamage_add/damage(anddamage_scrollfor a scrolled view); the region is the bounded union of the boxes (near ones merged). The renderer clips every frame-target batch to the region, clears only the region, and presents it: Direct3D 11 withPresent1dirty rectangles and a buffer-age redraw for the flip swap chain's un-kept back buffers; WebGPU composes into a persistent target and copies it to the surface (wgpu-native has no present-with-damage yet, a documented limit). A frame that marks nothing draws and presents nothing.render.damage.verify(set_damage_verify, orTOOLKIT_DAMAGE_VERIFY=1) renders every frame in full too and reports the first pixel that differs. Off by default (the whole window every frame, as before).render/damage.odin,render/damage_d3d.odin,render/damage_wgpu.odin,render/damage_verify.odin,draw/damage.odin; docs/draw.md, docs/webgpu.md. font.color_glyph_rgba,font.glyph_is_color,font.Color_Glyph: rasterize one color glyph (COLR / CPAL, sbix, CBDT, SVG) to a straight-alpha RGBA bitmap at an exact em size, with the offset from the pen to the bitmap, outside the glyph atlas and the draw layer, for a caller that composites it itself (a 2D canvas drawing emoji infillText). It reuses the COLR interpreter for the glyphs it owns and Direct2D otherwise;fgcolors foreground / text-color layers. The signature is platform-neutral; off Windows the functions compile and report no color glyph until a portable rasterizer lands. See docs/text.md.font.cascadelanguage: a cascade now carries the content's BCP 47 language (cascade(s, entries, features, no_system_fallback, language)), and the shaper passes it to the backend as the shaping locale (DirectWrite'sGetGlyphs/GetGlyphPlacementslocale name, HarfRust's buffer language). Faces then apply their language-specific forms (locl): Serbian and Macedonian Cyrillic, Han unification of CJK ideographs, Romanian ș/ț, and so on. Empty keeps the previous default (no locale), and two cascades differing only in language are distinctFont_Ids, so the word cache tells them apart. See docs/text.md.- Frame capture on WebGPU (
render.capture_request/render.capture_poll): reading a frame back without the screen now works with the WebGPU backend, not only Direct3D 11. Before the present,frame_endcopies the frame target (surface or offscreen, in whole or in part) into a mappable buffer;capture_pollmaps it without waiting (Pendinguntil the GPU is done) and returns straight RGBA8 with sRGB values and alpha forced to 255, the same picture the Direct3D path gives. It costs nothing until a capture is asked for, and works with the window covered, off the screen, hidden or minimized (render/capture.odin). render.surface_*on WebGPU: a second window drawn by the same renderer now works with the WebGPU backend, not only Direct3D 11.surface_init/surface_use/surface_destroycreate a per-window WebGPU surface (with its own present and latency state, and the DirectComposition visual for a translucent window) and swap it with the renderer's window state, mirroring the Direct3D path. The device, queue and pipelines stay shared (wgpu_surface.odin).
Changed
platformAPI made system-neutral:platform.wake_handlenow returns an opaquerawptr(paired with the newplatform.wake_signal) instead of a Win32HANDLE, and the newplatform.native_surface_handleshands a renderer the window's native handles through a neutralSurface_Handles(Windows HWND, Waylandwl_display+wl_surface, or X11 connection + window) sorendercan make a surface on any system without a Win32 type in the signature.render.inittakes a neutral surface, not anHWND:render.init(&r, surface: platform.Surface_Handles, size, transparent, space)opens the renderer on the native handles the platform package fills (platform.native_surface_handles(window)), the HWND on Windows and the Wayland or XCB handles on Linux;render.init_window(&r, window, size, ...)is the convenience form.surface_init/surface_create(second windows) take the same neutral handles. No Direct3D, DXGI or Win32 type appears in the renderer's public signatures.
0.49.02026-10-27release
Added
ui.text_editsyntax colouring (ui.Text_Highlight,ui.Text_Span): ahighlightparameter draws coloured runs the caller classifies per visible paragraph (spans), underlines paragraphs a caller's validator rejects (error_line), and reports the paragraph under the pointer (Edit_Result.hover_line) for a per-line tooltip. The editor knows no language; the cost is only the visible lines'.- Middle-click autoscroll (
ui.set_autoscroll_enabled,ui.Autoscroll): a press of the wheel on a scroll region drops an anchor and the pointer's distance from it scrolls, the farther the faster, along a tunable acceleration curve (dead zone, power shape, speed cap), as Chrome and Firefox do it on Windows. Hold mode (keep the button down) and toggle mode (a quick tap latches it until the next click) are both live; a direction cursor and an anchor marker show while it runs. Off by default;scroll_y,listandvlistget it once an app opts in. The curve and state machine are a pure, frame-rate-independent core (ui.autoscroll_axis_speed,ui.autoscroll_velocity) an application can reuse for its own view; see docs/autoscroll.md. ui.reorder_visible: the half-open range of items a scrolling reorderable list shows (scroll, view and pitch, with a margin on either side), so a long list builds only the rows in view and a frame's cost stays flat whatever the list's length.fsx: a platform-neutral package of file helpers with the guarantees a persistent store relies on, on every system (fsx_windows.odin,fsx_unix.odin):write_atomic(write through a.tmprenamed over the file, a reader sees old or new),read_all/write_parts/replace/read_at/stat/delete_file,make_dirs/list_files,path_kind(links never followed),remove_tree(one entry at a time, not through the Windows shell) andtemp_dir. No Windows or POSIX type crosses the boundary. See docs/fsx.md.
0.48.12026-10-27release
Fixed
ui.NAMED_PALETTES: the text of the One Dark and Solarized palettes is lighter (One Dark's bright white#c8ccd4, a step between Solarized'sbase1andbase2): their schemes' own foregrounds stood too close to the lit tab of a tab bar in Wren.
0.48.02026-10-27release
Added
ui.NAMED_PALETTES: eleven more palettes from well-known editor schemes, each with its own accent: Gruvbox and Gruvbox Light, Naysayer, Solarized and Solarized Light, Monokai, Tokyo Night, Catppuccin (Mocha) and Catppuccin Latte, One Dark and Everforest.palette_namedfinds them by name; a test checks that every named palette keeps its text legible on its background and panels and itsdarkflag in line with its background.
0.47.22026-10-27release
Changed
- The native libraries are built without fat LTO again (
tools/build_text.ps1,tools/build_a11y.ps1): with it (gt_text.lib6.4 MB instead of 33 MB) thetexttests crashed on the CI runner at their first test (atlas_stress, a signal) while they pass on a development machine; the cause is not found, and the size is not worth a crashing test run. Wren builds these crates into its own library and is not affected.
Fixed
- The text tests no longer fail now and then on a CI runner (
text_test.atlas_stress, "Signal caught: Unknown", sometimes a runner that never ended): DirectWrite throws C++ exceptions and catches them itself when an entry of its process-wide cache does not fit, and the Windows test runner ofcore:testingtakes any exception on a test's thread for a crash and kills that thread. The tests oftests/text,tests/color_glyphsandtests/text_backendsnow run their body on a thread of their own (tests/unwatched), where a caught C++ exception is ignored and any other exception still fails the test. Thetextpackage runs on several threads again intools/test_windowless.ps1. Outside the test runner nothing changes: DirectWrite catches these exceptions itself. - A test package that does not end names itself ( ools/test_windowless.ps1): each package is killed after 300 s, with its log (the state changes of its tests) in the CI's, instead of the CI cutting the whole step off after twelve minutes with nothing to read.
- The CI's cold build of the Rust libraries has room to end (.github/workflows/ci.yml): with fat LTO it is slower than the step's 12 minutes, so the cache was never saved and every run was cold; the build is a step of its own with a 30 minute limit, and the cache fills at the first success.
0.47.12026-10-27release
Fixed
- The native text backend and the accessibility adapter are built with fat LTO (
native/text,native/a11y,tools/build_text.ps1,tools/build_a11y.ps1): their crate types listedrlibnext tostaticlib, and rustc skips LTO whenever anrlibis among them: every dependency stayed a separate unoptimized object (gt_text.lib33.2 MB, now 6.4 MB;gt_a11y.lib16.9 MB, now 4.8 MB). The crates are now plain libraries (an application's Rust library still depends on them) and the scripts ask for the static library alone (cargo rustc --crate-type staticlib); they fail if an object of another crate is left in the archive.
0.47.02026-10-27release
Fixed
- A drag inside the window is shown at the top rate of a variable refresh screen (
platform.frame_decision,Cadence.variable,platform.cadence_adopt): on a variable refresh screen (G-Sync, FreeSync, Windows' dynamic refresh rate, which reports 60 Hz at rest), aDragframe (a splitter, a panel's handle, a tab) is presented at once with two frames queued, still one frame per slot of the top rate, instead of waiting for a refresh at the rate the screen rests at: a splitter dragged at 1 kHz went from 62 to 183 frames a second on a 180 Hz screen reported at 60 Hz. Fixed refresh screens and every other state are unchanged.cadence_adopt(&c, plan)takes the plan's slots and screen (instead ofc.slots = plan.slots).
0.46.02026-10-27release
Added
platform.Event.injected: an event made byinject_event(a test, a remote control, a macro) says so; an application that reads the pointer's position from the system (a window dragged across the screen) uses the event's own position for these, as the real pointer is elsewhere.Build_Params.inactive,ui.inactive: a window that is not the one the user is in is built withinactive = true: its focused line and text edits keep the focus but draw no caret and ask for no wake to blink it (an application with several windows no longer blinks a caret in each).
0.45.12026-10-27release
Added
- Text and bounds in the UI Automation test client (
uia.document_text,uia.text_selection,uia.select_text,uia.bounds;uia_probe text,uia_probe select-text --start A --end B,uia_probe bounds): what a text pattern holds, its selection or caret, a selection made as a screen reader does, and where an element is, for end-to-end tests.
0.45.02026-10-27release
Added
- Accessibility, node by node and several windows:
platform.a11y_diff_patchsends a tree's changes from only the nodes that changed (a document that knows its mutations no longer makes its whole tree again); the children a node lost are forgotten with their subtrees, so a node put back goes whole.ui.a11y_cost(s)gives the nodes and the milliseconds of the last tree. Aui.Statebuilt for another window than its last one sends that window a whole tree.tests/a11yreads two windows apart through UI Automation and checks that a window a reader has read stays quiet at rest (no wake, no update). docs/accessibility.md lists the known limits (Linux and macOS, the toolkit's tree made again in each build, what grafted documents such as Wren's pages lack: iframes, caret and selection, text bounds, a second window). - docs/getting-started.md and
examples/hello: a new project with the toolkit as a submodule (prerequisites with the pinned Odin, MSVC, the optional Rust libraries; a window in a few lines built with-collection:gt=...; the render and text backends, accessibility, tests, versions) and the pitfalls met so far (the output folder,odin testdeleting its exe, arender.Surfacein a local, nested builds, keys,expect_asserton Windows).hello --frames=Nruns without showing its window. Checked on a clean clone;tools/window_shot.ps1accepts an absolute-Out. flate: DEFLATE, zlib and gzip as streams (Inflater/Deflater: input in chunks of any size, each push gives the output it allows;inflate/deflatefor whole buffers), pure Odin for any platform. Checksums and sizes checked, a bound on the output (limit,Too_Large). The deflater writes each block in the cheapest of stored, fixed and dynamic Huffman codes (about 17 % smaller than fixed codes on text). Ported from Wren'ssrc/flate(same API); docs/flate.md,tests/flate.- Offscreen readback on the WebGPU backend:
draw.offscreen_endgives arender.Readback_Idandrender.readback_pollthe picture (straight RGBA8, sRGB values, alpha kept) as under Direct3D 11: the frame copies the target into a buffer, the poll maps it without waiting (Pendinguntil the GPU is done).tests/layerschecks the pixels on both backends. ui.Build_Params.time_ms: the build's clock (caret blink, double and triple clicks, hover delays, timers, toasts),now_ms()when 0. A test or a replay passes its own to be deterministic; double clicks and the text selection's clicks now read the build's clock instead of the wall clock.
Fixed
- CI: two tests that failed now and then are deterministic.
tests/ui_robust(nested_build_is_refused) checked an assertion throughtesting.expect_assert_message, whose exception handler on Windows races the system's end of the process: the whole test program ended without a message. The assertion is now caught on a thread of the test's own.tests/keyboard(caret_rect_is_where_the_caret_is_drawn) depended on the caret's blink phase, i.e. on how long the program had run (a slow WebGPU start blinked it off); it and the double-click tests (selection,panes) now passtime_ms.tools/test_windowless.ps1andtools/test_backends.ps1print the exit code of a failed package (an access violation, a trap nothing caught).
0.44.02026-10-27release
Added
platform.frame_decision: the cadence model (platform/pacing.odin, [docs/pacing.md](docs/pacing.md)): a plan from the screen and the application's choices (cadence_plan: the top useful rate of a variable refresh screen,Frame_Slotsstrict or loose), a state per frame (Rest,Continuous,Drag,Input_Answer,Resize_Step,Resize_Burst) and one pure decision (Frame_Decision: the wait for its slot, whether it waits for the swap chain, whether it is presented at once, the swap chain's latency), pluscadence_inner_frame,cadence_echoand the frame clock (frame_clock_advance,vblank_at_or_before). Test:tests/pacing(states, transitions, and simulated runs on fixed and variable refresh screens, tearing, a 1 kHz mouse, a resize, a panel dragged, AltSnap);tests/frame_slotsandtests/pacingjoin the windowless tests of the CI.
0.43.02026-10-26release
Added
platform.Frame_Gate: the frames of a thread as one state machine (platform/frame_gate.odin, [docs/frame_gate.md](docs/frame_gate.md)):Idle -> Polling -> Work -> Building -> Drawing -> Presenting, Full and Light frames,frame_gate_begin/frame_gate_stage/frame_gate_end; a frame asked for where none may begin is deferred, never nested, and a move the machine does not have is counted (a panic withstrict). The window procedure defers aWM_PAINTwhere no frame may begin;drag_scriptpumps the thread's messages between its steps as the system's drag loop does. Test:tests/frame_gate.
0.42.32026-10-26release
Fixed
- CI: green again and about 6 minutes instead of 30 to 37. The CHANGELOG step failed on every push (
tools/changelog_test.ps1ended with the exit code of a command it made fail on purpose; it now exits 0); a test package of a single test counted as failed (text_backendsprinted "The test was successful", not "All tests were successful"; both are accepted now). The jobs run in parallel: build (changelog, examples, shaders), the windowless tests (tools/test_windowless.ps1, the step's script moved there) and the WebGPU tests in four groups (tools/test_backends.ps1 -Group ci1..ci4, checked to hold every package once;-Group sampleon pull requests) with DXC, a third of the time of FXC on WARP (FXC still runs two packages,-Define TOOLKIT_WGPU_DX12_COMPILER=fxc); each job and slow step has a timeout; the Rust libraries, naga's translator, the emoji font and DXC are cached; Odin is set up by.github/actions/setup-odin, its archive hashed with .NET rather thanGet-FileHash. Every push to master keeps its own run (only pull requests cancel the previous one). - WebGPU:
render.video_convertconverts pictures in memory (I420, NV12; P010 and 10-bit I420 when the adapter hasTextureFormat16bitNorm) withvideo.wgsl, crop, rotation, matrix and range included, as on Direct3D 11: it returnedfalsefor every picture (render/wgpu_video.odin). A decoder's Direct3D 11 texture (gpu_texture) is still refused on WebGPU (wgpu-native cannot import it);tests/videochecks that refusal there instead of calling a Direct3D 11 device the backend does not have (it crashed). - WebGPU: Slug text when the device refuses its shaders draws nothing instead of passing a missing shader module to the pipeline:
r.slug.failedis honoured by both backends (it was Direct3D 11 only), a refusedslug.wgslsets it, a recreated device asks again.
0.42.22026-10-26release
Fixed
tools/fetch_emoji.ps1,build_naga.ps1andfetch_wgpu.ps1hash their files the .NET way, not withGet-FileHash: the build step of a CI run (cmd, after the MSVC environment was set) failed with "Get-FileHash is not recognized" as soon as the emoji font had to be fetched again.
0.42.12026-10-26release
Added
render.surface_init(r, &s, hwnd, size, ...) -> okmakes aSurfacein the one the program points to.surface_create(which returns it by value) is kept and calls it. ASurfaceis several kilobytes (Present_State's ring of 256 frames): held in a local and copied into a record,-o:speed -debugsplit it into 12 612 values with a debug record each, and LLVM spent more than ten minutes on that module (Wren's development build and its CI did, until it calledsurface_init). The doc ofsurface_createsays so.
0.42.02026-10-26release
Added
- Accessibility: screen readers read the toolkit's windows (
ui/a11y.odin,platform/a11y.odin,platform/a11y_diff.odin,platform/a11y_accesskit.odin(Windows),platform/a11y_stub.odin,native/a11y,docs/accessibility.md). Each build turns the boxes into an accessibility tree (boxes with a role or text; layout boxes left out; ids are the boxes' keys; bounds, focus, states, values, actions) and AccessKit 0.25 exposes it through UI Automation (WM_GETOBJECT). Built only once an assistive technology asked for the window; only the nodes that changed are sent (platform.A11y_Diff). Every widget gives its role, name and states (buttons, check boxes, radios, switches, sliders, steppers, progress, segmented, chips, dropdowns, tabs, fields and text editors with text runs and caret, lists and virtual lists, menus and menu bars, tooltips, toasts as live regions, modals, splitters, accordions, cards, form rows, title bar and caption buttons, status bar, labels, paragraphs, rich paragraphs with their links). The app names its own boxes withui.a11y(info)(next box) orui.a11y_set(box, info);ui.a11y_wanted(),ui.a11y_active,ui.a11y_tree,ui.a11y_find,ui.a11y_stats. Requests of assistive technologies (invoke, toggle, select, expand / collapse, focus, set value, increment / decrement, text selection, scrolling, context menu) arrive as the same signals as input; the others inui.a11y_requests();ui.a11y_request(key, action),ui.a11y_numeric_request,ui.a11y_inject(tests). Trees of the app's own (documents) are grafted under a node (A11y_Info.graft) and sent withplatform.a11y_send.ui.set_window_button_names(translated caption buttons). Platform:a11y_state,a11y_send,a11y_take_request,a11y_available, the update writer (a11y_node,a11y_children,a11y_str...);Event_Kind.Accessibility. Compiled in with-define:A11Y_ACCESSKIT=true(tools\build_a11y.ps1buildsbuild\a11y\gt_a11y.lib;A11Y_NATIVE_LINK=falsewhen the program's own Rust library carries it). Every update is checked against a mirror of the tree before AccessKit sees it (a broken update is refused, not a crash); text runs and text selections are made safe for AccessKit's text code.ui.a11y_box_text(the text a box shows to a reader).ui.a11y_focus_on(key): the readers' focus on a graft node while the app's view has the keyboard (a focus on a graft whose tree is not there yet waits for it in the adapter). tools/uia: a small UI Automation client (COMIUIAutomation): elements, control types, names, states, values, invoke / toggle / select / expand / set value / focus.tools/uia_probe: the same as a command, for end-to-end tests of applications (a hidden window works).- Tests:
tests/a11y(the tree, incremental updates, requests; with-define:A11Y_ACCESSKIT=true, a hidden window read and driven through UI Automation). CI runs both.
0.41.12026-10-26release
Added
platform.Chrome_Regions.overlays: what the app shows over everything (the ui fills it with the open menus' panels).hit_test_chromeanswersClientthere before anything else, the resize bands included.
Fixed
- An open menu that covers the integrated title bar's resize band takes the pointer there (
uichrome_collect): a menu taller than the window starts at its top, and over the band the system showed the resize cursor (diagonal at the top right corner) and resized the window on a press. - The open menus are above whatever the app builds at the root after them (
menus_raise, inend_buildbefore layout): a menu is built where its button is, and a notice, panel or dialog built later at the root was drawn over it and took its pointer. Test:tests/chromeopen_menu_over_resize_bands_and_later_boxes. font.glyphgives the ink box where the ink is with the native text backend (native/textgt_glyph_box): read-fonts 0.46 counts a glyph's extents height downward and never negative, and the bottom of the box was put a glyph's height above its top, so the box was twice as high and a glyph centred by it (Wren's tab icons, a chip's icon) was drawn a glyph lower than meant. Test:tests/text_backendsnative_glyph_box_matches_dwrite (-define:FONT_BACKEND=native).
0.41.02026-10-26release
Added
ui.post: calls posted from any thread (ui/post.odin,ui/post_queue.odin,docs/post.md):post(state or window, fn, data, word)runsfn(data, word)on the interface's thread at the start of the nextbegin_build(ordrain_posts), and wakes the window once per drain.ui.Post_Queueis a bounded lock-free ring (Vyukov's MPMC queue, no allocation per message); a full ring blocks (default, nothing lost), drops the oldest (on_drop), grows into a spill list or fails (Post_Overflow); each producer's order holds in every mode.post_configure(capacity, overflow, calls per build,on_drop),drain_posts(bounded;needs_framewhile calls wait),post_close,post_stats,set_post_wake. Example:examples/stream(a thread posting simulated server-sent events). Test:tests/post.- Notification area icon (
platform/tray.odin, Windowstray_win32.odinonShell_NotifyIconWversion 4, stub elsewhere,docs/tray.md):tray_create,tray_set_icon(PNG) /tray_set_icon_rgba,tray_set_tooltip,tray_set_menu(items, separators, check and radio marks, disabled and default items, submenus; opened by a right click or the menu key),tray_show_menu,tray_notify(title, body, icon, silent, quiet time),tray_poll(clicks, double clicks, right and middle clicks, menu items, notification shown / clicked / timed out / hidden, hover,Recreatedafter the shell restarted: the icons are added again onTaskbarCreated),tray_destroy;destroy_windowremoves the window's icons.tray_test_hookandtray_injectfor tests. Test:tests/tray. - Rich clipboard (
platform/clipboard.odin, Windowsclipboard_win32.odin, stub elsewhere,docs/clipboard.md):clipboard_set(text, HTML with its source URL, image, files in one write),clipboard_get_image(PNG, CF_DIBV5, CF_DIB, CF_BITMAP),clipboard_get_html(fragment and source URL),clipboard_get_files(CF_HDROP),clipboard_formats,clipboard_format_names,clipboard_sequence,clipboard_watch/clipboard_changed; images are written as PNG + CF_DIBV5 (alpha) + CF_DIB (over white). Pure conversions:dib_to_rgba(every DIB header and depth),rgba_to_dib,hdrop_encode/hdrop_decode,html_format_source_url;png_encode/png_decode. Test:tests/clipboard. ui.splitter(ui/split.odin,docs/panes.md): a handle that resizes one pane (logical pixels, or any unit throughunit) within its limits and the room the other pane keeps, the pane before or after the handle, double click / Enter back to the default, arrow keys and Home / End with the keyboard focus (optional), the resize cursor, a rule drawn in the accent colour while hot or held;finishedsays when to persist; nothing drawn or animated at rest.ui.accordion: a header with a turning chevron that folds the content built in its block, with an animated height (no frame at rest), Enter / Space; the content is not built once folded.ui/reorder.odin: a list reordered by dragging along X or Y, from Wren's tab strip:reorder_item_size,reorder_offset,reorder_slot,list_move,reorder_edge_scroll,reorder_moved, and the drag's stateReorder(reorder_frame,reorder_begin,reorder_dragging,reorder_item_pos: items slide to their slots, the dragged one follows the pointer).- Example
examples/panes; teststests/panes.
0.40.12026-10-26release
Added
ui.building(): true betweenbegin_buildandend_build(on this thread, whatever the state). An app whose window procedure makes frames (on_frame, inside a loop of the system) asks it before building one.
Fixed
- A build begun inside another one is refused with a clear assertion (
ui: begin_build inside another build) instead of breaking the one in progress:begin_buildemptied its parent stack, freed its frame arena and selected another state under it, and the outer build failed later at an unrelated place (Wren's crash of 2026-10-26: a bounds check inui.spacer, after aTrackPopupMenuopened during the build painted the window fromWM_PAINT). Test:tests/ui_robustnested_build_test.
0.40.02026-10-26release
Added
ui.rich_paragraph(ui/rich_text.odin): runs with their face, size, bold / italic, inline code, colour, background, underline, strike and links in one paragraph; lines break at the backend's opportunities on the whole text, "\n" breaks, long words are cut between clusters,max_linesends with "…",text_alignaligns; links answer hover, click and the keyboard (Tab through them, Enter); layouts are cached by content (thousands of runs laid out once). Geometry:rich_layout,rich_text,rich_index_at,rich_caret,rich_range_rects,rich_link_at;rich_set_faces.Box.measure_y(a .Text height that follows the width). Theme entrieslink,code_bg. Tests:tests/keyboardrich_text_test. Example:examples/markdown.- Selecting text outside fields (
ui/selection.odin):.Selectablelabels, paragraphs and rich paragraphs share one selection across boxes (drag, Shift+click, double / triple click, Shift+arrows, Ctrl+C, Ctrl+A); text cursor, the theme's selection colour;selected_text,has_selection,selection_serial,selection_changed,clear_selection,select_box. Test:tests/keyboardselection_test. font.styled_face: the family's own bold / italic face of a system font, else simulated.
Fixed
- Bold runs of a system font were the regular face with simulated bold (0.02 em: hardly bolder at 24 px): they take the family's bold face now.
0.39.02026-10-26release
Added
- Simulated bold and oblique in the native backend (
font/simulate.odin): as DirectWrite draws them (bold: +upem/50 per inked glyph and emboldened outlines; oblique: 0.33985 shear).Font.simson every platform;with_simulationsoff Windows. Test:tests/text_backendssimulations_test (widths to 0.01 px, Slug outline boxes). - System fonts on Linux (fontconfig) and macOS (CoreText) (
font/font_system_linux.odin,font/font_system_darwin.odin):load_system,has_system_family, the per-character fallback andsystem_families; collection faces by PostScript name (gt_face_postscript_name,gt_file_face_count). Checked withodin check, not run on those systems yet. FONT_NATIVE_LINK=false: a program that buildsnative/textinto its own Rust library links nogt_text.lib(two Rust static libraries collide).native/text/examples/shape_dump: HarfRust's shaping of a text, likehb-shape.tests/keyboardtext_edit_draw_test:text_edit_caret_rectagainst the drawn caret.
Changed
- The Hebrew difference between the backends is explained (
docs/font-backend.md): DirectWrite drops the XPlacement of right-to-left kerning pairs; the native backend applies it, as HarfBuzz does.
0.38.32026-10-26release
Added
- File dialogs (
platform/dialogs.odin, Windowsdialogs_win32.odin, stub elsewhere):open_file_dialog,save_file_dialogandpick_folder_dialogwith a title, filters (File_Filter), an "All files" line, the starting folder (start_dir, or aKnown_Folderdefault), a remembered folder perremember_id, the suggested name and default extension, several files, the overwrite question;Dialog_Resultgives the paths and the type line chosen. Pure helpers:filter_patterns,filter_matches,filters_from_accept(a web page's accept list);dialog_test_hookstands for the real dialogs in tests. Doc:docs/dialogs.md. Test:tests/dialogs. - Shell actions:
open_url,open_path,reveal_in_folder(ShellExecuteExW; the file manager with the file selected),known_folder_path.
0.38.22026-10-26release
Added
ui.text_editwraps and grows (wrap,auto_height,min_lines,max_lines): long lines break at the box's width at the font backend's line break opportunities (a word wider than the box between grapheme clusters); Up / Down / Home / End, clicks and drags follow the visual lines, the caret at the end of a wrapped line stays there (Text_Area.upstream); the box is as tall as its lines within the limits, then scrolls.ui.text_visual_lines,ui.visual_line_of,ui.Visual_Line(ui/text_lines.odin). Tests:tests/keyboard(text_edit_wrap_test.odin). Example: the composer ofexamples/fidget.- Caret rectangle:
Edit_Result.caret(box coordinates, this build) andui.text_edit_caret_rect(a)(the caret and the selection's other end, window coordinates, as last drawn) for completion popovers and input method windows.
0.38.02026-10-26release
Added
- Native text backend (
-define:FONT_BACKEND=native,font/backend_native.odin,native/text,tools/build_text.ps1): Fontations (read-fonts' font model), HarfRust and ICU4X (line breaks with dictionaries, bidi through unicode-bidi) in a static Rust library behind a C ABI, built and pinned like Wren's image decoders. Shapes 2.6 times faster than DirectWrite on the corpus (2.0 µs a word against 5.2, without the word cache). Faces are still opened by the platform;Settings.backendpicks either backend for one State.font.native_table,font.native_coverage,font.native_outline_provider. Seedocs/font-backend.md. tools/text_benchandtests/text_backends: the two backends compared on the corpus (equivalence report and timings; logic checks in the tests).
Changed
- The
fontpackage compiles on Linux and macOS (odin check font -target:linux_amd64/darwin_arm64): DirectWrite and Direct2D are behindfont_windows.odin,raster_windows.odinand the color files;font_other.odinloads faces from files and memory through the native backend (no system fonts yet: TODO fontconfig / CoreText).load_memorytakesfont.Simulations({.Bold, .Oblique}) instead of DirectWrite's flags.
Fixed
- Kerning under letter-spacing and font-feature-settings (dwrite backend): an explicit feature list replaced DirectWrite's default features (kern, liga, clig, calt, rclt); they are now kept unless the list sets them.
tests/retainedpasses withFONT_TEXT_RENDERER=slugtoo (its atlas page assertions follow the renderer).
0.37.02026-10-25release
Added
- Text backends (
font/backend.odin,docs/font-backend.md): thefontpackage shapes, analyses (scripts, bidi, line breaks) and reads faces (coverage, metrics, glyph boxes, Slug outlines) throughText_Backend, a table of procedures, and finds the system's fonts (fallback,font.system_families) throughSystem_Fonts, the platform's. DirectWrite is thedwritebackend (font/backend_dwrite.odin, the code moved, not changed: the corpus dump oftools/text_dumpis byte-identical before and after); other platforms get an "unsupported" system stub (font/system_other.odin, TODO fontconfig / CoreText).Settings.backend(init only),font.backend,font.backend_available,font.face_metrics. tools/text_corpus,tools/text_dump: a multilingual corpus (Latin with ligatures and kerning, Cyrillic, Greek, Arabic, Hebrew, Devanagari, Thai, Lao, Khmer, Myanmar, Chinese, Japanese, Korean, emoji sequences, a variable font, fallback) and a dump of everything the package says about it (glyphs, clusters, widths, metrics, bidi levels, line breaks), to diff two builds.
Changed
Font.features(cascade features) are[]Featureinstead of DirectWrite's structure; the system fallback and the script analysis of ASCII text go through the backend.
0.36.22026-10-25release
Added
platform.Frame_Slots/frame_slot_wait(platform/frame_slots.odin, pure): the app's own cap on the cadence of its frames, a grid of slots one period apart that no frame starts before. The swap chain's latency object paces an app only where the system holds frames back until a refresh; on a variable or dynamic refresh display (G-Sync, FreeSync, Windows 11's dynamic refresh rate) it is released at once, and an app woken by a mouse at 1 kHz makes and presents a frame per move (270 to 915 a second measured on a 180 Hz G-Sync monitor).strictmakes the grid the period itself; a loose one (0.9 of it) is a net a screen that paces the app never reaches, and tightens by itself after 8 delayed frames in a row. A frame that starts an answer to input (immediate) is not delayed. Test:tests/frame_slots(a loop woken every millisecond on a screen that paces nothing: at most one frame per period).Display_Info.max_hz: the highest rate of the monitor's modes at the current size and orientation. With a dynamic refresh rate the mode and the display path say 60 Hz all the time while the screen is at 180 Hz when something moves;platform.refresh_is_variable(hz, max_hz)tells when to aim abovehz.platform.display_live_hz(w): the refresh rate the compositor says it runs at right now (DwmGetCompositionTimingInfo,rateRefresh): with a dynamic refresh rate the one reading that moves with the screen. DWM answers for the primary monitor only and only to a NULL window handle (a handle givesE_INVALIDARG), so a window on another monitor has no answer (okfalse). Its refresh count is no use (it counts the calls of the process).
0.36.12026-10-25release
Added
Box_Flag.Scrollbar_Left(ui/scrollbar.odin,ui/events.odin): the overlay scrollbar of a.View_Scroll_Ybox sits at its left edge instead of its right one (drawn, hit and dragged there; its grab margin widens toward the content). A panel docked on the left whose right edge joins the content (a tab sidebar whose current tab flows into the page) keeps its scrollbar on the outside. Test:tests/ui_robustscrollbar_on_the_left.
0.36.02026-10-25release
Added
- Window placement, for apps with several windows or a saved layout (
platform/placement.odin,placement_win32.odin):platform.get_placement(w)/set_placement(w, p)(Placement{rect, maximized}: the outer rectangle in screen pixels where the window is restored to, and whether it is maximized;set_placementmoves and sizes the window without showing it, on the monitor nearest to the rectangle and inside its work area, andshow_windowthen shows it maximized if asked), the purefit_rect_to_work(rect, work),outer_rect(w),client_origin(w)(where the client area starts on the screen: turns a pointer position of a window, captured outside it, into a screen position) andmove_window(w, x, y).tests/placement. A browser that moves tabs between windows used all of them.
Fixed
set_placementasks twice when the window changed monitor: a window that crosses to a monitor of another DPI is resized by the system for it (640 x 480 became 896 x 672); asked again, now that it is on that monitor, the rectangle is the one wanted. Windows now open on the monitor the user is on (0.35.3), so this is the common case.ui.initno longer restarts the toolkit's clock (ui/state.odin): the origin ofui.now_ms()was reset by everyinit, so opening a second window (oneui.Stateeach) made the time of the first one jump back. It is set by the firstinitonly.
0.35.32026-10-25release
Fixed
- A menu row that the pointer left for a disabled row stayed selected (
ui/popups.odin): a disabled row gets no signal, so the selection followed the pointer over enabled rows only and kept the last one while the pointer crossed disabled ones (the burger menu, the dropdown lists). A pointer that moves onto a disabled row now leaves nothing selected. Test:tests/keyboardmenu_pointer_over_a_disabled_row_selects_nothing. - A new window opened on the primary monitor (
platform/platform_win32.odin):CW_USEDEFAULTputs it there whatever program launched it, so a window started from a program on another monitor had that monitor's refresh rate and DPI wrong until it was dragged. It is now made on the monitor of the foreground window, else the one under the pointer. ui.VERSIONsaid 0.35.1 in the 0.35.2 release; it follows the changelog again.
0.35.22026-10-25release
Added
- Offscreen drawing and readback (
draw/offscreen.odin,render/readback.odin,docs/draw.md):draw.offscreen_begin/offscreen_enddraw into a transparent target of their own, never shown and not cut by the window;render.readback_pollgives its pixels after the frame (straight RGBA8, sRGB, alpha kept; anOp_Kind.Readcopies the target where it was recorded).render.straight_srgb. Test:tests/layersoffscreen_draw_and_read_back (both blend spaces, WARP). ui.wake_in_ms(the timed wake the last build asked for, measured from now) andui.wake_due.wake_mscounts from the build: a loop that used it as its wait timeout and as "a frame is wanted" made a frame at every early wake of any message. Measured with Wren'stools\altdrag_sim.ps1(AltSnap sendsWM_SIZINGbefore each step): the extra frame of each step was made for a caret blink not yet due (Ui_Timer882 of 1544 frames of the run); withwake_due/wake_in_ms, 494 presents for 500 steps.
0.35.12026-10-25release
Added
- Drag images:
Drag_Imageis now inplatform/dragdrop.odin(independent of the system), withdrag_image_valid,drag_image_fade(the translucent ghost of a dragged element) anddrag_image_fit(scaled down to a size, its hot spot following).drag_image_heldsays what the system's drag image helper holds afterdrag_start(Windows: read back from the data object'sDragImageBits). - The app's own drag back over its window (
drop_sync_set,Drop_Sync): whiledrag_startblocks, the window asks the app at once for eachDrop_*instead of queueing them, so the app can handle the drop beforedrag_startreturns (a browser firesdropbeforedragend). - Scripted drags (
drag_script,Drag_Step,drag_script_calls): the nextdrag_startfollows given points through the window's own drop target, with the same data object and drag image, never the real mouse (tests, remote control). dragdrop_thread_init: OLE in a single-threaded apartment on the interface thread, before any library puts it in the multithreaded one (which madedrop_target_enableanddrag_startfail).- Pointing devices (
platform/input_devices.odin):input_devices(mouse, pen, touch points; Windows:GetSystemMetrics),touch_max_points, and the rules of the primary pointer / every pointer (primary_pointer,any_pointer) as the CSS media featurespointer,any-pointer,hover,any-hoversee them. - Touch gestures (
platform/touch_gesture.odin, pure):Touch_Gestureturns the fingers' positions and times into tap, pan, pinch (scale about the fingers' middle), long press and fling (release speed);touch_flinggives the glide's distance and time under exponential friction. Event.pointer.kindof an emulatedMouse_*event says which device the system emulates (touch or pen).render.capture_cancel: forgets a capture asked for and not read.tests/dragdrop: drag images, the scripted drag back over the window withdrop_sync_set(enter, over, drop answered at once, the image held by the system), gestures, device rules.
0.35.02026-10-25release
Added
- Drag and drop between applications (
platform/dragdrop.odin,dragdrop_win32.odin,dragdrop_stub.odin,docs/dragdrop.md): the API is independent of the system; Windows implements it with OLE, other systems answer "not supported". A window becomes a drop target withdrop_target_enable; a drag over it queuesDrop_Enter/Drop_Over/Drop_Leave/Drop(position, modifiers,Event.drop_allowed) with the data indrop_data(Drag_Data: files, text, HTML fragment, URL and its title, PNG image, the app's own MIME types); the app answers withdrop_set_effect(the system is given the last answer).drag_startruns a drag of the system with aDrag_Dataand an optionalDrag_Image, and returns the effect the target chose.drop_effect_for(the Ctrl / Shift / Alt convention),drag_data_types,drag_threshold,drop_inject(tests, remote control),drop_self_test(the round trip through the window's own drop target, no mouse). Pure helpers:drag_custom_encode/decode,uri_list_first,html_format_wrap/html_format_fragment(CF_HTML). - Fingers and pens (
platform/pointer.odin,pointer_win32.odin):Pointer_Down/Pointer_Move/Pointer_Up/Pointer_Cancelevents withEvent.pointer(Pointer_Info: identifier, touch or pen, primary, pressure, tilt, twist, barrel, eraser, contact size) fromWM_POINTER. The system's mouse emulation still comes, markedEvent.emulated.pointer_buttons,pointer_pressure,Pointer_State. Mouse_*events carryEvent.buttons(Mouse_Buttons: the buttons held after the event), so a move during a drag says which button is held.tests/dragdrop: the pure helpers, the pointer details, the round trip of every format through the system (hidden window), injected drops.
0.34.02026-10-25release
Fixed
- Raising then lowering
set_max_frame_latencyno longer leaves the swap chain with two frames in flight for good (render/present.odin,render/render.odin, D3D11): the swap chain's frame latency object is a semaphore that the system raises whenMaximumFrameLatencygoes up and does not take back when it goes down. After a 1 -> 2 -> 1 (an app that allows a second frame for a resize step or an input frame, as Wren does) the object held one count more than a slot per round trip: every wait onframe_waitablereturned at once,Presentblocked for a whole refresh instead, and the frame on screen was one more frame behind the input, until the program restarted. The surplus is now remembered when the latency is lowered (Present_State.latency_debt) and consumed, without waiting, byframe_waitable/wait_for_frameas soon as the object has the counts to give. Measured on a 60 Hz window (a tab dragged at 1 kHz of pointer moves): wait for the slot 0.1 ms andPresent16 ms before, 16 ms and 0.6 ms after, and the time from input to the refresh that showed it, as the frame statistics give it, from 118 ms to 56 ms.
Added
render/latency_object_test.odin: the surplus of the latency object against a semaphore that stands for it (lowering, raising, taking without waiting, counts that come later).
0.33.02026-10-25release
Fixed
- A window moved by another program's drag tool is no longer taken for a window in the system's move / size loop (
platform/size_move.odin,platform_win32.odin): AltSnap (Alt + right button drag) and similar tools resize a window withSetWindowPoscalls from their own process and POST itWM_ENTERSIZEMOVE/WM_EXITSIZEMOVE. The window took them for a loop of the system: it painted fromWM_PAINTinside its ownpoll_events(a frame within the app's frame, two presents per turn) every 16 ms, while the tool waited at every step for its thread. Now the two messages open and close the modal mode only when they arrive inside the window procedure'sDefWindowProccall, where the system's real loop runs (size_move_step, pure); from outside they are ignored and the size change arrives asWM_SIZE, drawn by the app's own loop like any other.WM_ENTERMENULOOP/WM_EXITMENULOOPare unchanged. A move that ended withWM_EXITSIZEMOVEfrom outside still refreshes the display information (another monitor's refresh rate).
Added
platform.pump_stats(window)(Pump_Stats): how many frames were made from the window procedure (paint_frames, a loop of the system), how many loops of the system were entered (modal_loops), how manyWM_ENTERSIZEMOVE/WM_EXITSIZEMOVEfrom outside were ignored (ignored_size_moves). For tests and diagnostics of the message loop.tests/size_move: the decisionsize_move_step(messages inside / outsideDefWindowProc, nesting, an exit with no loop open);tests/windowchecks that posted and sentWM_ENTERSIZEMOVEfrom outside open no modal mode (the frames-in-a-loop test now uses the menu loop, which a test can enter without the mouse).
0.32.12026-10-25release
Changed
- The selected row of a menu is a filled, rounded row with no outline (
ui/theme_default.odin:menu_item selected backgroundaccent at 0.30,menu_item selected bordertransparent): it was the generic "selected" look, an opaque accent ring around a row with a 22 % fill, which read as a focus box more than as a highlight (the fill was too faint to see beside the ring). Lists, fields and every other selected item keep their look.
0.32.02026-10-25release
Added
- Input -> screen latency, stage by stage (
render/latency.odin, off by default):render.set_latency_probe(r, true)records, for each frame that reached the screen and was built from input, the time waiting for the frame (wait: event received -> frame began), building and drawing (work),Present(present) and scan-out (scan: Present returned -> the refresh that showed it), thenlatency_summary(r)gives p50 / p95 / max / mean of each, the total in refreshes of the monitor, the share of frames built from input and how many presents the swap chain's statistics count as not yet shown.latency_reset,latency_summarize(pure),latency_sample_of. The application tells when the frame read its input withrender.set_frame_read.Displayed_Framegainedread,drawnandcall(whenframe_endandPresentwere called). Both backends feed it; nothing is recorded or allocated while the probe is off (render/latency_test.odin: arithmetic and ring, no clocks).
0.31.02026-10-25release
Fixed
- Menus: the row under the pointer is drawn selected in the frame that sees the pointer on it, and the row it left is released in that same frame. The selection was decided while a row was built, so rows built before the hovered one (and the hovered row itself) showed the previous selection until the next frame: a pointer crossing a menu left two rows lit for a frame, and the new one a frame late (a refresh or two more once the frame had to wait for the screen).
Menu_Item_Infogainedkey(the row's box): a level reads the hovered row from the previous frame's items when it begins (tests/states:menu_hover_selects_in_the_same_frame).
0.30.12026-10-25release
Changed
- Timing checks in the tests (durations, CPU and GPU times, frames in a time window) are opt-in:
base.perf_skip(packagebase,base/perf.odin) runs them only withGUI_PERF=1ortrue, and never whenCIis defined. The results are still checked in CI (tests/colr,tests/gpufx).
0.30.02026-10-25release
Fixes from the review of font, ui and platform (0.26.0). 0.29.0 is the version of another branch (render and draw).
Added
- Grapheme clusters (
font/grapheme.odin):font.grapheme_next,grapheme_prev,is_grapheme_boundary,grapheme_count(UAX #29: ZWJ and modifier sequences, flags, tags, keycaps, Hangul, Indic conjuncts, CR LF), a boundary after or before any byte index. font.selection_spans/x_spans_of_range: the stretches of a line that a range of characters occupies on screen (several in bidirectional text).font.raster_size_limit(s): the largest size glyphs are rasterized at (half an atlas page).Colr_Params.surface_limitandwork_limit,COLR_MAX_COMPOSITES,COLR_SURFACE_LIMIT,COLR_WORK_*: the bounds of the COLR interpreter's memory and work.gsub_ligateis bounded byGSUB_BUDGET.ui.wake_within(s, ms): asks for a wake withinms, keeping the soonest of all the requests.ui.TIMER_KEEP_BUILDS.tests/window(windows and messages; needs a desktop, not in CI),tests/ui_robust(in CI),tests/colr/hostile_test.odin(COLR and GSUB tables built by hand),tests/text/grapheme_test.odin.
Changed
Font_Idcarries a generation (slot index in the low 20 bits): the slot of an unloaded font is reused by the next one, so loading and unloading no longer runs out of ids at 65 536; the id of an unloaded font reaches nothing, even once its slot is taken. Ids of the first font in a slot are the same numbers as before. Code that storesFont_Idas an opaque number is not affected; code that computes with it (index into a table by id) must mask with1 << 20 - 1.- Caret, arrows, Backspace, Delete, clicks (
hit_index), the cut of a toast and the line breaks ofwrap_lineswork on grapheme clusters, not on code points. Word movement keeps its code point steps. platform.poll_events/wait_handledrop only the events they returned before (they used to clear the whole list, losing what a poll of another window had queued for its window).platform: the mouse stays captured until the last button is up; a lost capture makes the missingMouse_Release. A repaint of a window that is not itself polled, arriving while another window of the thread polls, no longer runson_frameinside that poll: it setspaint_requested(take_paint_request). A move and a menu now get frames (a 16 ms timer,WM_ENTERSIZEMOVE..WM_EXITSIZEMOVE) as the documentation said.platform.open_windowreturns nil when the system cannot make the window;inject_eventof aClosesetsis_closed.- COLR: a glyph over a bound of memory or work is refused (drawn as its outline);
PaintCompositenests at most 8 levels; the interpreter draws without the atlas lock.colr_nests_glyphanswers "nested" for a graph it cannot read to its end (so that it goes to the interpreter, not to Direct2D),colr_findkeeps a base glyph record whose offset is outside the table as an unreadable graph,colr_openaccepts any offsets in a v0 header with no records and refuses a table whose base glyph list does not fit; a face with such a table is not drawn in color by Direct2D. - Atlas: a bitmap bigger than a page makes no page and evicts none;
draw.glyph_runcaps the rasterization size and stretches the bitmap (text over 1024 px per em was invisible). The Direct2D canvas over 1M pixels is given back after use.Settings.page_sizeis documented as read byinitonly. ui.menu_itemcloses the menu when its block ends, not in the middle (the items after the chosen one were built as buttons for a frame).ui.apply_palettekeeps the entries an application added to the theme first (Theme.palette_at);theme_addcopies its tags.ui.focus_stepkeeps to the open modal;focused_nameis empty for a box not built yet in this build.ui.fuzzy_findreturns positions in the text it was given (a dotted capital I made them one byte off).- Timers, scrollbar owners and a virtual list's measured heights of rows that are gone no longer grow without end.
Fixed
ui.dropdownwith an empty list or a selection outside it,ui.tabswith fewer icons than labels, andui.segmentedwith no label panicked or drew an infinite rectangle.platform.config_dirwith%APPDATA%over 259 characters panicked; the clipboard is read up to the size of its block, freed when it cannot be given to the system, andGlobalLockis checked.- The wake-ups of the caret blink and of a tooltip delay no longer replace each other.
0.29.02026-10-25release
Fixes found by the review of render and draw (branch fix/render-draw). 0.30.0 is the font / ui / platform review branch. Each fix has a test that failed before it (tests/lifecycle is new).
Changed
docs/webgpu.mdlists the functions the WebGPU backend does not have;docs/draw.mdno longer says 3D is unsupported (planes are);docs/shaders.md:app_shaderandapp_shader_ready.- Not fixed (reported):
bspstill splits the whole context when one pair crosses, with no limit on the number of pieces, andPlane_Item.geomis the texel-aligned rectangle (faces that touch look crossing; Wren works around it); a backdrop of a cut plane is captured once per piece; the hash of an app shader does not coverAPP_BINDINGS(changing it makes every precompiled blob stale: to be done with the applications' blobs);Surfacerelease on a thread, the DirectCompositionPanellifetime, canvas source textures never zeroed (to check).
Fixed
- Plane pieces with more than 6 cuts (
draw/bsp.odin): a piece bounded by more thanMAX_PLANE_CUTScuts had the extra ones thrown away, so it was drawn over its siblings' region at its own rank and covered planes that had to be in front. It is now divided by a chord between two of its corners into convex halves (the chord is one more cut for each), as often as needed: no cut is lost, no limit. Hidden (skip) items no longer take part in the crossing tests. draw.app_shaderkeepsAppIn.pos,sizeandpxin local units under a uniform scale plus translation (DPI, zoom) asdocs/shaders.mdsays (they were pixels: a scale was baked into the rectangle); any scale now goes through a transform entry. It returns false for a shader that is notrender.app_shader_ready.- Device loss with several windows (Direct3D 11): a
Surfaceset aside bysurface_usekept its swap chain, views and capture copy from the old device (undefined behaviour, then a loop of recoveries); andset_blend_spacegave the new view format to the current window only. The window state carries the device generation and the blend space it was made in, andsurface_useremakes a stale one (render.Renderer.swap_gen,swap_space,space_epoch;Surface.gen,space_epoch).d3d_recoverflushes the old context after releasing its objects (the old HWND swap chain used to keep the window, andCreateSwapChainForHwndcrashed). An offscreen renderer notices a device loss atframe_end. - Captures belong to their window (
Capture_Stateis exchanged with theSurface: the nextframe_endof that window takes it, not whichever window ends a frame first); the staging copy of an old device is released at recovery (a pending copy becomesFailed) instead of being used on the new device. wgpu_destroyread the texture table after freeing it.wgpu_recoverwithout a device (adapter gone) returns false at once instead of creating textures on a null device and dropping the textures and shaders for good. A refused app pipeline on wgpu (the module passed, the pipeline did not) gives the shader up instead of invalidating every frame.- App shaders that cannot be remade after a device loss keep their handle (and name) until
destroy_app_shader; the handle used to be given to the next shader.render.app_shader_ready(r, s)says whether one can be drawn. - Kept layers and planes (
cache_id) followset_text_rendering,set_corner_smoothingand the font atlas settings (they are in the key now), and a result drawn while a color glyph was still pending (Settings.color_async) is not kept (the glyph never came in). A masked layer that lost a target is not kept as the masked result. push_planeinside a recorded picture records nothing (it recorded the content flat, unprojected);current_clipis not empty while recording.- Slug text, when the device refuses the Slug shaders, is not drawn (it was one solid rounded rect per glyph);
d3d_update_texture_r8checks the handle, the block and the pixels like the other uploads.
0.28.02026-10-25release
0.27.0 is reserved for the fixes of the review branch.
Added
- Boxes with leaning sides (
ui.Box.slant,platform.Slant): a clickable box can say that its left and right edges lean (how far each is moved inwards at its top and at its bottom). Only that trapezoid is hit (ui.box_contains; the topmost box under the pointer still wins), and over an integrated title bar it is a *slanted hole* (platform.Chrome_Regions.slanted,platform.Slanted_Region,platform.slanted_contains;hit_test_chromeanswers.Clientinside it), so that the corners a trapezoid tab does not cover keep dragging the window. For tab strips drawn with sloped sides. Tests intests/chrome.
0.26.02026-10-24release
Merge of three branches: the GPU 2D canvas engine, faster image uploads, the window border color. 0.20.0 and 0.21.0 (reserved below for the canvas engine and the uploads) were never released: their content is here.
Added
- A 2D canvas engine on the GPU (
render/canvas.odin,canvas_types.odin,canvas.wgsl, moduleCanvasoftools/shaders, [docs/canvas.md](docs/canvas.md)), Direct3D 11:render.canvas_texture_create(r, w, h, target, mips)makes a canvas (a render target of premultiplied sRGB-encoded RGBA8, kept) or a source of pixels (canvas_upload); the owner records its drawing in arender.Canvas_List(instances with a geometry, a paint and an operator; commands for clip masks, layers, snapshots,putImageData) andcanvas_submit(r, target, &list)replays it in runs of one draw call per operator / source / clip / scissor. Rectangles, rounded rectangles and ellipses have analytic coverage (exact box filter when axis-aligned), polygons 16 N-rook samples on boundary pixels only with the fill rule per sample (the pieces of a stroke never count twice); solid, linear / radial / conic gradients and images (four filters, wrap modes) as paints; the 11 Porter-Duff operators as blend states and the 15 blend modes from a copy of the backdrop; clip masks, layers with color-matrix and gaussian-blur passes (large blurs on reduced images).canvas_read(waits),canvas_read_begin/canvas_read_ready/canvas_read_finish/canvas_read_cancel(polled),canvas_read_mask,canvas_clear,canvas_set_opaque,canvas_supported(false on WebGPU),canvas_stats,canvas_pool_bytes.tests/canvas(12 pixel tests), intools\test_backends.ps1. platform.set_border_color(w, rgb): the color of the 1-pixel border Windows 11 draws around the window (DWMWA_BORDER_COLOR), ornilfor the system's. With an integrated title bar the system's border is half transparent along the top edge: a line of whatever lies behind the window showed above the app's tab strip.tests/textures(both backends): an unwritten texture is transparent, an opaque upload with a wider stride shows exactly, a translucent one is premultiplied in both spaces, a partial update zeroes the rest, mips follow an opaque upload;render/premultiply_test.odinchecks the word-at-a-time premultiplication against(c * a + 127) / 255for every value. Both intools\test_backends.ps1.
Changed
- Faster image textures:
create_texture_rgbano longer zeroes a new texture with oneUpdateSubresourceper row (551 driver calls, 1 to 4 ms, for a 735 x 551 photo). The texture is marked unfilled; an update that covers it whole clears the mark, and one still unfilled when the frame is submitted (or hit by a partial update first) is zeroed with a few band-sized calls from one zero buffer (Renderer.zeros). Observable behavior is the same: a new texture is transparent. Data textures (create_texture_data) and video targets keep their own paths. - Faster uploads:
update_texture_rgbachecks whether the block is opaque (a vectorized AND of the words) and uploads opaque pixels, which is every JPEG and most PNG / WebP / AVIF, straight from the caller's buffer with its own pitch: no conversion pass and no scratch copy. Translucent blocks in the sRGB blend space are premultiplied a word at a time without branches or tables; the linear space keeps its table version. Both backends.
Fixed
- Windows 11, integrated title bar: a line of what lay behind the window showed above the tab strip, because the system's border is half transparent there. An app now paints it opaque with
platform.set_border_color(nothing changes until it does).
0.25.02026-10-24release
Added
- Application shaders (
render/app_shader.odin,app.wgsl,wgpu_app.odin,draw/app_shader.odin, [docs/shaders.md](docs/shaders.md)):render.create_app_shader(r, {name, wgsl, dxbc, hash})registers a fragment function the application wrote in WGSL (fn app_shade(a: AppIn) -> vec4<f32>, linear light, straight alpha; it readsapp_param(k)andapp_tex0..3), anddraw.app_shader(dc, shader, rect, params, textures, blend, radii, tint)fills a rounded rectangle with it, in order with everything else, under the transform stack, clips, alpha stack, layers and planes (the vertex stage, corner shape and clip coverage are the renderer's). Up to 64 vec4 parameters and 4 textures per draw; blend modesNormal,Add,Screen,Multiply. Refuses (returns false, draws nothing) for shader 0, while a picture is recorded, and over the limits. Both backends: WebGPU compiles the modulerect.wgsl+app.wgsl+ the application's WGSL; Direct3D 11 embeds the pixel shader precompiled byodin run tools/shaders -- app <name> <file.wgsl> <dir>(naga + D3DCompile,<name>.dxbcand<name>.sha256; the hash of the module is checked when the shader is created, a stale blob is refused with a message). - Data textures:
render.create_texture_data(r, w, h, format = .RGBA8 | .R8, mips = true)andupdate_texture_datamake textures of raw 8-bit values (a normal map, a height map, a mask): no sRGB decoding, no premultiplication, in either blend space; mip chain byGenerateMips/mip.wgsl; recreated (empty) after a device loss like images. render.renderer_id(r): a number no other renderer of the process has had (a renderer destroyed and made again at the same address is another one), for caches of GPU objects keyed by renderer (withdevice_generation).tests/appshaders(both backends), added totools\test_backends.ps1and CI.
0.24.0 is reserved for another branch; 0.25.0 follows 0.23.0 on purpose.
0.23.02026-10-24release
Changed
render.surface_create(r, hwnd, size, transparent = false, fixed_size = {}): a second window can have a composition swap chain (transparent, with the window opened withplatform.open_window(..., transparent = true)) and a fixed surface (fixed_size, asset_fixed_surfacedoes for the renderer's own window): resized, it shows its buffers 1:1 clipped by the client area, never stretched. The default stays an opaque HWND swap chain, which stretches the last frame while the window is resized.surface_destroyalso releases a translucent surface's DirectComposition objects (and goes throughset_teardown_wait).
0.22.12026-10-24release
Added
render.set_teardown_wait(r, ms):destroywaits at most that long for the swap chain (and a translucent window's DirectComposition objects) to be released, which happens on a thread of its own; past it the release goes on by itself. Releasing a swap chain waits for the system's compositor to let go of the window's surface, and on a machine whose cores are all busy that took seconds in a program that was closing. 0 (the default):destroywaits as long as it takes, as before.
0.22.02026-10-23release
Added
- Synthetic input:
platform.inject_event(w, event)queues an event that the nextpoll_eventsreturns after the system's own, through the same path (hover, focus, shortcuts, text input behave as for a real device). For tests, remote control and macros. - Reading a frame back without the screen (
render/capture.odin):render.capture_request(r, rect)marks the next frame,frame_endcopies that frame's render target (or a part of it) into a staging texture just before presenting, andrender.capture_poll(r)returns RGBA8 pixels once the GPU is done (it never waits:Pendinguntil then). The picture is what the renderer drew: it works with the window covered, off the screen or hidden, and does not depend on the monitor's scale. Nothing is paid until a capture is asked for. Direct3D 11 (swap chain and offscreen); the wgpu backend answers false.
0.20.0 and 0.21.0 were reserved (feat/canvas-gpu and feat/gpu-accel); they were merged into 0.26.0 instead, and those two numbers stay unused.
0.19.02026-10-23release
Added
ui.menu_item(..., keep_open = true): choosing the item does not close the menu, for the switches of a settings menu (the check mark changes, the menu stays for the next one).ui.set_menu_item_height(em): the height of the items of the menus built from then on, in em (defaultMENU_ITEM_EM, 2.2; 0 puts it back). A dense tool panel sets it while it builds its menus.ui.menu_heading(text): a small weak title above a group of items of a menu (not an item: the keyboard skips it).- Fixed:
platform.open_windowkept the window class name in the caller's allocator, so a window opened later (once that memory was reused) failed with "cannot find window class"; the name is a literal now. ui.use(state): selects the state the public procedures operate on, for programs that draw several windows with a state each.render.surface_create / surface_use / surface_destroy: a second window drawn by the same renderer (same device, textures, fonts), as a surface swapped in for the frames drawn to it (Direct3D 11).ui.tooltip_beside():tooltipplaced beside the pointer (its right, or its left when there is no room), centered on it, for the rows of a list or a table, where a tooltip below the pointer hides the rows under it.
0.18.02026-10-23release
Added
- Video pictures as textures (
render/video.odin,video.wgsl,shaders/video.hlsl):render.create_video_target(r, w, h)makes an RGBA texture for a decoded picture andrender.video_convert(r, target, picture)fills it. AVideo_Pictureis what a decoder hands over: NV12 and P010 surfaces of the device itself (the hardware decoders of Media Foundation / DXVA write into textures of the renderer's device, shared with no copy when the surface allows it), or I420 and 10-bit I420 planes in memory. It carries the crop rectangle, the rotation (0, 90, 180, 270), the matrix (BT.709, BT.601, BT.2020), the range, and the transfer function (SDR, PQ, HLG, tone-mapped to the display's white withhdr_peak_nitsandsdr_white_nits). The result is an ordinary texture to draw withdrawas any image. - D3D11 creates its device with
VIDEO_SUPPORT(dropped again when the software rasterizer refuses it: there is no hardware decoding there anyway); the wgpu backend has the same entry points (wgpu_video.odin,video.wgsl). tests/video: the conversion of each format against reference values (colour matrices, ranges, crops, rotations, 10 bits), on D3D11 and wgpu;tools\test_backends.ps1runs it.render.set_max_frame_latency(r, n): the most frames the swap chain may queue (IDXGISwapChain2::SetMaximumFrameLatency); 1 by default, more while a window is resized so a present does not wait behind the previous frame. The wgpu backend has no equivalent (returns false).render.set_fixed_surface(r, max_size): one surface size while the window resizes, shown 1:1 and clipped by the client area (composition swap chains only): a window being resized cuts the last frame instead of stretching it, and the newly shown strip stays the background until the next frame paints it. The blend-space change keeps the whole surface presented (d3d_resizeskips the source region then).
0.17.12026-10-24release
Fixed
- Direct2D crash on nested PaintGlyph: Direct2D of Windows 11 24H2 as first released (
d2d1.dll10.0.26100.1, the image of GitHub'swindows-2025runners; a patched 24H2 such as 10.0.26100.8875 draws them) dies with a null read insided2d1.dll, an access violation nothing can catch, when it draws a COLR v1 glyph whose paint graph has a PaintGlyph inside the paint of another PaintGlyph (a clip inside a clip, directly or through transforms, layers, compositions and PaintColrGlyph). WithColor_Engine.Systemsuch glyphs are now drawn by the COLR interpreter (font.colr_nests_glyphfinds them in the font's own tables, once per glyph and face); every other glyph still goes to Direct2D.font.colr_avoids_system(state, face, gid)tells which. Found by the CI of Wren:tests/colrinterpreter_agrees_with_direct2d died on U+F1400 .. U+F140F of the test font, 16 glyphs, and the process with it. tests/colr: the comparison with Direct2D leaves out the glyphs Direct2D is no longer given;nested_glyphs_stay_off_direct2dchecks, without depending on the machine'sd2d1.dll, that exactly those 16 glyphs are kept off it and are drawn in color by the interpreter.
0.17.02026-10-22release
Added
- Planes that cut through each other (
draw/bsp.odin):draw.draw_planes(dc, items)draws the planes of one 3D rendering context in the order they hide each other, and when planes cut through each other (or hide each other in a cycle) splits them along each other's planes by a BSP (as Chromium'sBspTree/DrawPolygon), the pieces ordered back to front for a viewer in front. A piece is the plane's layer drawn again underPlane_Draw.cuts(Plane_Cut: half-spaces of the space after the perspective divide, at mostMAX_PLANE_CUTS, tested per pixel on the interpolated plane position inrect.wgsl: no seams between pieces).Plane_Item.parts: several layers on one plane (a plane and what rides on it) are cut and ordered together. - Effects on the composite of a plane:
Plane_Draw.blend(mix-blend-mode of the projected quad),backdrop+shape(backdrop-filter filtered in screen space, shown through the alpha of a layer projected like the plane). - Sharp planes:
Plane_Fx.mipsgives a plane's layer a mip chain (render.acquire_target_mips,render.target_mips_supported,Op_Kind.Mipson D3D11 and wgpu); the projected quad's filter is anisotropic along the long axis of the footprint at the mip level of the short axis (proj_texel).FLAG_PROJECTinstances take a frame rect (the antialiased edge, for a quad that is one piece of a larger plane) and the valid texel range of the layer (colors[2],colors[3]).draw.open_plane_frame(map a pointer back through a plane exactly). tests/planes: planes crossing in an X (both orders of drawing give the same picture, with and without perspective), a star of three, blend on a flat and a turned plane, backdrop-filter through a silhouette, mip levels against aliasing; D3D11 and wgpu.- A COLR / CPAL interpreter (
font/colr.odin,colr_raster.odin,colr_glyph.odin):Settings.color_engine = .Builtindraws COLR v0 and v1 color glyphs (gradients, transforms, the 28 composite modes, clip boxes, text-color paints) on the CPU from the face's own tables, the same on every platform, into the Color atlas pages (so also async, per text color, in pictures). Default.System(Direct2D), unchanged.colr_samples,colr_cache_kb,colr_counts,colr_of,colr_dump;Colr/colr_open/colr_renderfor the portable core. A graph it refuses (cycle, bad offset) shows the glyph's outline. - Built-in emoji:
font.set_emoji_source(s, path | data): an emoji font loaded on first need, used for emoji characters before the system fallback (font/emoji.odin,emoji_data.odingenerated from Unicode 18emoji-data.txtbytools\gen_emoji_data.ps1). Tag sequences are formed from the face's GSUBccmpligatures (colr_gsub.odin). tools\fetch_emoji.ps1(Noto Color Emoji COLR v1, pinned by commit and SHA-256, OT-SVG table dropped: 4.9 MiB),font/data/NotoColorEmoji-OFL.txt,tools/colrcompare(interpreter against Direct2D),tools/colrboard+tools\emoji_board.ps1(against Chrome / Edge),tools/emojiperf,tests/colr(alsotests/color_glyphs -define:COLR_ENGINE=builtin).
Changed
rect.wgsl: two more flat varyings (the cuts of a projected quad); HLSL regenerated.ui.VERSIONand the version constants agree again (they said 0.16.0 and 0.15.0).
0.16.02026-10-22release
Added
- Planes: layers drawn through a projective 4x4 transform (CSS Transforms 2:
perspective(),rotateX/Y,translateZ,preserve-3d).draw.push_plane/pop_plane/draw_plane(draw/plane.odin): the content of a rectangle is drawn offscreen in its own frame at a chosen raster scale, then composited as a quad through a column-major 4x4 matrix (draw.Mat4, the order of CSSmatrix3d()), with perspective-correct interpolation, antialiased edges, anisotropic filtering along the long axis of the pixel's footprint (Plane_Draw.max_taps), the clips and alpha stack of the moment, andbackface-visibility(Plane_Draw.cull_back,plane_faces_away).pop_planehands the layer back so several planes can be sorted beforedraw_plane(preserve-3d). A plane whose content did not change is kept between frames (Plane_Fx.cache_id/cache_key, the layer cache oflayer_cache.odin):push_planereturns false and only the matrix changes. - Shader:
FLAG_PROJECTinrect.wgsl(aKIND_IMAGEinstance whose homography is in two entries of the transform table): the vertex shader writes homogeneous clip positions, so the rasterizer clips against w > 0 and interpolates with perspective. No new pipeline: D3D11 (naga's HLSL, regenerated) and wgpu share it.tests/planes(flat plane equals the plain rect, perspective correctness, backface, a plane crossing the eye, kept planes, nested planes), on both backends.
0.15.02026-10-22release
Added
- A WebGPU render backend (experimental), behind the same interface as the Direct3D 11 one:
-define:TOOLKIT_RENDER=wgpu(defaultd3d11). wgpu-native over Direct3D 12 (or Vulkan with-define:TOOLKIT_WGPU_BACKEND=vulkan); the shaders ported to WGSL; translucent windows through a DirectComposition visual; GPU time per step. The pixel tests pass on both backends (tools\test_backends.ps1). Seedocs/webgpu.md. tools\fetch_wgpu.ps1(wgpu-native and DXC pinned by SHA-256),tools\render_probe,tools\ppm_diff.ps1,tools\window_shot.ps1.- Shaders written once, in WGSL:
render/rect.wgsl,filter.wgsl,slug.wgsl(the Slug text shaders, ported),mip.wgsl. The HLSL of the Direct3D 11 backend is generated from them by naga (tools/wgsl2hlsl, naga pinned exactly, Cargo.lock pinned by SHA-256 intools\build_naga.ps1), compiled to DXBC and embedded as before;odin run tools/shaderswrites both;tests/shaderschecks the SHA-256 of the sources in the header of each HLSL and the blobs; CI runstools/shaders -- -check.render/rect.hlsl,filter.hlsl,slug.hlslare gone (generated copies live inrender/shaders/). D3D11 pixel tests unchanged, GPU time at a held clock and CPU per frame unchanged, first present +0 to +10 ms.n-render.backend_name(),render.adapter_description();TOOLKIT_RENDER_TRACE=1` prints the time of the first present.
0.14.02026-10-22release
Added
- Slug text:
font.Settings.text_renderer = .Slug(default.Atlas, unchanged) draws glyphs on the GPU from their quadratic Bézier outlines (Eric Lengyel's Slug algorithm, MIT, patent in the public domain; shaders pinned infont/slug_ref, credit in NOTICE.md): sharp at any scale, rotation, skew or page zoom, exact subpixel positions, no bitmaps, nothing to re-rasterize when the scale changes (a picture of Slug text is never stale for its scale). Color glyphs (emoji) stay on the atlas path.font: outlines come from a provider (font.Outline_Provider,slug_set_provider; DirectWrite'sGetGlyphRunOutlineon Windows, cubics turned into quadratics withinSettings.slug_tolerance),slug_buildmakes the curve texels (RGBA16F) and band texels (RG16UI),slug_glyphcaches them per (face, glyph) in pages of the two textures (Settings.slug_pages, least recently used page reused, pictures that drew from it go stale),slug_sync_page/slug_stats.render:slug.hlsl(compiled afterrect.hlsl, which it reuses for transforms, clips and the text correction),KIND_SLUGinstances in the ordinary instance list drawn by their own pipeline (Frame_Data.slug_runs,Replay.slug_runs: a batch is drawn in pieces, in order),create_slug_textures,update_slug_page.draw:glyph_runuses it when the setting says so; pictures remember their Slug pages.tools/textcompare: atlas | Slug | difference images and measurements;tests/slug.
0.13.12026-10-21release
Added
- GPU profile steps
Upload,Acquire,PresentandIdle(only whileset_gpu_profileis on): where a frame's GPU time goes, the swap chain's wait and the present included.
0.13.02026-10-21release
Changed
- Interaction states show at once: hover, press, focus, selection and disabled no longer fade in or out (
hot_t,active_t,focus_t,disabled_tgo from 0 to 1 in the frame that sees the change), and box colours jump to their targets (a selected row, the hovered menu item). A hover change asks for no further frame. A change of palette still cross-fades every colour forui.PALETTE_FADE_MS(160 ms). - Hover and press fills are visible: the theme's new state entries
hover_fill/press_fillare fractions of the way to the palette's tint (white on dark palettes, black on light ones) in sRGB values, drawn over the surface actually under the box (ui.box_under) with the alpha that gives that step in either blend space (draw.alpha_like_srgb). Per role: buttons 7 % / 13 %, ghost and icon buttons 8 % / 14 %, accent and danger buttons 10 % / 18 %, list rows 5 % / 9 %, tabs 6 % / 10 %, segments 7 % / 12 %, anything else with.Draw_Hot_Effects7 % / 12 %; menu items show their selection under the pointer (8 % more while pressed). The old tiny overlay (1.4 % / 2 % in linear light) is gone. The tint entryhoveritself is unchanged. - Fields: the border of a
line_editunder the pointer moves 22 % toward the tint (input hover_border). - No press "squish" by default;
Theme.press_squish(e.g. 0.04) brings it back, at once and without animation.
Added
Theme.transition_ms: > 0 brings back short cross-fades of the interaction states (default 0);Theme.serial, bumped bytheme_add,theme_setandapply_palettewhen a colour changes.- Selectable
list_rows have.Draw_Hot_Effects(taglist_row, built under it only);segmenteditems light up under the pointer (tagsegment);tabsitems have the tagtab. "<tags> hot text": the text colour of a clickable box under the pointer (falls back to the normal one).remove_button hot textis the palette's red,remove_button hover_fill/press_filla light red step: push the tagremove_buttonaround anicon_buttonfor a delete cross.Box_Flag.Hot_Within: a box stays hot (and lit) while the pointer is on a clickable box inside it; selectablelist_rows have it (a row and its delete button).ui.hot_within(key): the same question while building (last frame's tree), e.g. to show a row's buttons only while the pointer is on it.list_rowkeeps a cursor given withnext({cursor = .Hand})(rows that open something).ui.state_fill(box, name, under),ui.box_under(box),ui.srgb_step(a, b, t): the theme's state steps for custom draw procs.draw.alpha_like_srgb: the inverse ofalpha_like_linear(the alpha that looks in a blend space as a given alpha blended in sRGB values).tests/states: instant states, transitions on demand, palette cross-fade, the size of the steps in both blend spaces.
0.12.02026-10-21release
Added
- Every CSS cursor:
platform.Cursorhas all the values of CSS Basic User Interface 4 (none,context-menu,help,progress,wait,cell,crosshair,vertical-text,alias,copy,no-drop,grab,grabbing,all-scroll,col-resize,row-resize, the eight one-way resizes,nesw-resize,nwse-resize,zoom-in,zoom-out) next to the seven it had, which keep their names (Arrow= default,Hand= pointer,Resize_H= ew-resize,Resize_V= ns-resize).cursor_from_css/cursor_css_name(CURSOR_CSS_NAMES) convert from / to the keyword (-webkit-graband the other legacy names too). - On Windows a cursor is the system's (
IDC_*: the user's scheme and size) when Windows has it (cursor_system_id,cursor_source); the twelve it lacks (grab,grabbing,zoom-in,zoom-out,cell,copy,alias,context-menu,no-drop,vertical-text,col-resize,row-resize) are drawn by the toolkit (platform/cursor_shapes.odin: shapes on a 32-unit grid, signed distances sampled 4 x 4 per pixel,cursor_draw) at the size of the system's cursors on the window's monitor (SM_CXCURSORat its dpi,cursor_size_px), in the colours of the user's scheme (white or black pointer, read from the system's arrow:cursor_scheme), with the hotspot where Windows puts it (an arrow's tip, a hand's palm, a magnifier's lens); made once per size and scheme, again when the dpi or the cursor settings change (WM_SETTINGCHANGESPI_SETCURSORS), destroyed with the window.Nonehides the pointer over the client area. - Custom cursors from an image:
custom_cursor_create(w, rgba, size, hotspot, density, key, max_css)(hotspot clamped into the image; larger thanCUSTOM_CURSOR_MAX_CSS= 128 CSS pixels, Chromium's limit:.Too_Large, the caller shows its fallback), shared bykeywith counted references,custom_cursor_release,set_cursor_custom(w, cc, fallback, scale)(the image resized to its CSS size times the scale,cursor_resample: premultiplied, a tent filter as wide as the reduction; at most four sizes kept per image),custom_cursor_valid,custom_cursor_stats,custom_cursor_info. destroy_window: destroys the window and what it holds (its cursors, events, title bar regions).cursor_info,cursor_handle_of,shown_cursor,cursor_render(a cursor as the system draws it, for previews and tests).examples/cursors: every cursor in a grid (light and dark tiles, its CSS name, system or drawn), hover a cell to get it; the last cell has a custom cursor.--preview Ndraws the previews at N pixels.tests/cursors: CSS names, system or drawn for each value, the drawn shapes at several sizes and both schemes (colours, bounds, hotspots), resampling, and on a hidden window every cursor made at the system's size, custom cursors' references, sizes, hotspots, limits and release.
Changed
- The cursor is shown at once when
set_cursor/set_cursor_customchanges it while the pointer is over the client area or captured by the window (a drag: noWM_SETCURSORcomes during a capture; a page scrolled or changed under a pointer that does not move), andWM_SETCURSORover the client area sets the one resolved last (no frame needed while the pointer moves over the same thing). The system keeps its own over the frame, the resize bands and the caption of an integrated title bar. Native_Window.cursorsis no longer an array of handles (the cursor state of cursors_win32.odin).
0.11.12026-10-20release
Fixed
ui.initmakes the state current: getters (scale,font_size,focus_key...) called before the firstbegin_buildread a nil state and crashed (Wren crashed at start-up when the pointer was over its window).
0.11.02026-10-20release
Changed
- Shaders compiled ahead of time:
render/rect.hlslandrender/filter.hlslare embedded as DXBC (render/shaders/*.dxbc,render.SHADERS) instead of being compiled byD3DCompilewhen the renderer starts (280 ms to 1.5 s of every launch,rect.hlsl's pixel shader dominating).odin run tools/shaderswrites the blobs after an edit of the HLSL;tests/shadersfails while one is stale.-define:RENDER_COMPILE_SHADERS=truecompiles at start-up as before.render.shader_compile(id). - Resizing without
ResizeBuffersat every step:render.resizekeeps the swap chain's buffers larger than the window (rounded up toset_resize_slackpixels, defaultDEFAULT_RESIZE_SLACK= 256) and only changes the presented region (IDXGISwapChain2::SetSourceSize, microseconds) while the window fits in them; they are reallocated once per slack crossed, and made smaller when they hold more than twice the pixels shown.ResizeBufferscost 1.6 to 3.5 ms a step.render.buffer_size. The window's swap chain is created withDXGI_SCALING_STRETCH(withNONE, DWM showed a smaller source region magnified).
0.10.12026-10-20release
Added
font.Metrics.line_gap: the face's line gap, unrounded (a layout rounds ascent, descent and gap one by one, as Blink does).
0.10.02026-10-19release
Added
- Kept filter results (
draw/layer_cache.odin):Layer_Effects.cache_id,cache_key,cache_anchorkeep a layer's filtered texture from frame to frame (stored once the same key came two frames in a row); while the caller's key, the filters run by passes, the transform's linear part, the anchor's fraction and the bounds relative to the anchor hold,push_effectsreturns false (the content need not be drawn; what is drawn is discarded) andpop_layercomposites the kept result, moved by whole-pixel moves of the anchor. Opacity, blend mode and trailing color matrices stay live. A layer up tomax_layer_pxis drawn whole when it is to be kept (scrolling it into view keeps it good).Layer_Effects.cache_under(blend modes into the opaque frame) keeps the blended pixels: no layer, copy or blend while what lies under is the same.backdrop_capture/backdroptakecache_id,cache_key(what lies under),cache_anchor.set_layer_cache(dc, budget, max_layer_px, idle_frames),layer_cache_stats,layer_cache_forget,layer_cache_clear. - Masked layers are kept too:
push_masktakes the samecache_*fields and returns false likepush_effectswhen the kept result stands (the mask is in the kept result: neither the content nor the mask layers are drawn, andmask_layerdoes nothing). The caller'scache_keymust cover the mask layers; the toolkit salts it so that a masked and an unmasked layer never share a result. Trailing color matrices stay live only when they commute with the mask (no alpha column in the color rows, an alpha row that only scales alpha by [0, 1], as every CSS filter function); others run as passes before the mask, as CSS orders them. render.keep_target/unkeep_target(a pooled texture held across frames),targets_generation,kept_targets_bytes,texture_bpp,acquire_target_format,Target_Format.- Per-step GPU profile (
render/gpu_profile.odin):set_gpu_profile(r, on, detail),gpu_profile(r): time, count, pixels and estimated traffic of each kind of step (Gpu_Step: draw runs into the frame or a layer, clears, copies, color / blur / reduction / shadow / mask passes), render targets bound, the slowest steps;detailtimes every draw call (batch, instances, kinds, flags, scissor). render.set_filter_precision(r, .Unorm8 | .Half): filter results in sRGB-encoded 8 bits or fp16.render.FLAG_SCALED,render.FLAG_MATRIXfor layer composites and backdrops;Pass.factor.examples/effects:--nocache,--half;--benchprints the GPU time by kind of step; the thumbnails and the stress cards keep their results.tests/gpufx: reduced blurs against the exact gaussian, kept layers / blended pixels / backdrops against drawn ones (hits, whole-pixel moves, invalidation), stale texels never read, the profile, masked layers kept (against drawn ones, a new mask key redraws, a blur under a mask).- Integrated title bar (
platform,ui/window_chrome.odin): the app's own boxes are the window's title bar, as in Edge and Chrome (tabs in the title bar).platform.open_window(..., integrated_title = true)orplatform.set_integrated_title(w, on)at any time: the client area covers the title bar (WM_NCCALCSIZE); the system's frame stays on the left, right and bottom (invisible resize borders, the shadow, Windows 11's rounded corners and border). Maximized, the client area stops at the monitor's edge and leaves 2 px along an auto-hidden taskbar so that it still comes up.- The app reports its title bar every frame (
platform.set_chrome_regions(w, &Chrome_Regions): caption areas, the interactive holes in them, the caption buttons);WM_NCHITTESTanswers from it (platform.hit_test_chrome, pure and unit-tested):HTCAPTIONwhere nothing is interactive (drag, Aero Snap, double-click to maximize, right click for the system menu),HTMINBUTTON/HTMAXBUTTON(Snap Layouts on Windows 11) /HTCLOSE,HTTOP*along the top edge (a band of the frame's thickness, corners at both ends), the system's own answer on the other borders. - Over the caption and its buttons, mouse moves come as
Mouse_Moveevents (hover follows); a press on a caption button is captured and comes asMouse_Press/Mouse_Release(the system never draws its own buttons over the app's).Mouse_Leaveonly when the pointer leaves the window, not when it goes from the client area to the title bar. Event_Kind.ActivateandWindow.active: the window became active or inactive (dimmed caption glyphs).platform.minimize,toggle_maximize,request_close,is_maximized,is_minimized,show_system_menu(w, pos),set_fullscreen(w, on)/is_fullscreen(the window covers its monitor without frame; back to its placement),needs_top_border(Windows 10 draws no top edge: the toolkit draws it),resize_border,is_windows_11,app_icon_rgba(size)(the exe's icon as RGBA, to draw it in the title bar). Alt+Space opens the system menu.ui.Build_Params.window: after each build the toolkit sends the window its title bar: every box with the new.Window_Captionflag is a caption, every clickable box over it a hole (popups included),window_buttonsare the caption buttons.ui.title_bar(id)(a row with.Window_Caption),ui.window_buttons()(minimize, maximize / restore, close: 46 px wide, as tall as the row, glyphs from Segoe Fluent Icons or Segoe MDL2 Assets, else drawn; the system's fills: hover / pressed tints, close in red#c42b1c, dimmed glyphs while the window is inactive; clicks act on the window),ui.chrome_regions(s).- fidget:
--integratedmakes its menu bar the title bar.
- Virtualized list
ui.vlist(id, &Virtual_List, ids, labels, selectable)(ui/vlist.odin): only the rows in view (and a margin) are built, whatever their number. Rows of any height: measured when laid out, kept per row identity and per width / scale / font size, the average for rows never seen; the view stays on the row it shows when heights above it change. The keyboard oflistover every row (Up / Down, Page Up / Down, Home / End, type-ahead onlabels, Enter, Escape), the active row scrolled into view at once.vlist_destroy,vlist_invalidate,vlist_scroll_to,vlist_row_extent. - Wrapped text cache: a paragraph's lines are kept per (text, font, size, width) between builds (
wrap_lines_cached,wrap_cache_len); layout and drawing no longer wrap every paragraph twice per frame. - Smooth scrolling along a curve for the toolkit's scroll regions (
ui/scroll_anim.odin): impulse (default, as Edge) or ease-out, a duration per move, sampled at the frame's time (Build_Params.frame_ms: a display clock, e.g. the vblank the frame is shown at); a new target while moving keeps the speed.ui.set_scroll_settings(s, {curve, impulse, duration_ms});.Exponentialis the former easing. Apps that scroll their own document the same way feel alike. tests/chrome: hit test (zones, edges, maximized), title bar regions and caption button clicks from a ui tree, virtualized list (rows built, measured heights, keyboard over unbuilt rows, view anchoring), wrap cache, scroll curve.
Changed
list: the keyboard handling and type-ahead are shared withvlist(List_State.virtual); no change forlist.- Wrapped text is drawn at the width layout wrapped it at (
Box.size), not the rounded rect's. - Color matrices ending a filter chain are applied by the composite (one pass and one target fewer; a layer with only color filters runs no pass).
- Large blurs: the reduction by 2, 4 or 8 is one pass; the result stays reduced and is sampled back by the next pass or the composite (no upsampling pass); the variance of the bilinear resampling is taken off the gaussian's.
- Filter passes no longer clear their target: they write a transparent ring of one texel past their region and read no further (the mask pass included). Filter chains end in sRGB-encoded values (no conversion pass back to linear).
push_effectsandpush_maskreturn a bool (see above); existing calls that ignore it are unchanged.Backdropholds the chain's trailing matrices and whether its texture is kept.- A masked layer whose filters end with a large blur applies its mask at scale 1 over the reduced result (sampled back bilinearly, as the composite would).
tests/chromeandtests/keyboardtake a lock around each test (they drive the global UI state): they no longer need-define:ODIN_TEST_THREADS=1, without whichtests/chromeused to hang the test runner.
0.9.32026-10-19release
Added
- Direct2D draws color glyphs (
font/color.odin, bindings infont/d2d.odin): COLR v1 paint trees (linear, radial and sweep gradients with their extend modes, transforms, PaintComposite modes, clip boxes, PaintColrGlyph), COLR v0 layers, sbix / CBDT / PNG bitmaps and OT-SVG (plain or gzip) are drawn on a private WARP device into a bitmap read back into the atlas' Color pages. Windows 11 22H2 and later:ID2D1DeviceContext7::DrawGlyphRunWithColorSupport; Windows 10 1607 and later: the runs ofTranslateColorGlyphRundrawn by the call of their format (no paint trees: v0 layers or the outline); without Direct2D: COLR v0 layers composited on the CPU as before. Whether a glyph is a color glyph comes fromIDWriteFactory8::TranslateColorGlyphRun(paint trees) orIDWriteFactory4's. - Text-color layers (palette entry 0xFFFF, COLR v1 foreground paints, SVG
currentColor) take the text color: a color glyph is drawn in two colors once to find whether it depends on it; such glyphs are kept once per text color.font.glyph_imagegains anfgparameter (sRGB, default black);draw.glyph_runpasses the run's color.Glyph_Image.uses_fg. font.color_renderable(s, id): false for a face whose only color format the system cannot draw (COLR v1 before Windows 11 22H2): a cascade should take those characters elsewhere rather than show outlines.- Faces with only COLR v1 (
DWRITE_GLYPH_IMAGE_FORMATS_COLR_PAINT_TREE) count as color faces. Whether a COLR v1 glyph uses the text color comes from its paint attributes (IDWriteFontFace7::CreatePaintReader), of a COLR v0 glyph from its layers; only SVG glyphs are drawn twice to find out. - Off the drawing thread (
Settings.color_async, off by default): a complex color glyph costs milliseconds (a Segoe UI Emoji COLR v1 emoji: about 4 ms on WARP). The first lookup queues it and returns a pending image (Glyph_Image.pending); one thread draws the queue, puts the bitmaps in the atlas and callsfont.set_color_ready_callback's callback (ask for a frame there).font.color_ready_generation,font.color_pending,font.color_wait. A picture recorded while one of its glyphs was pending is stale once the ready generation moves (Picture.glyphs_pending,pending_ready; checked before the empty-picture cull).unloaddrops a face's queued requests and waits for the one in flight;destroystops the thread. tests/color_glyphs: Google's COLR v1 test glyphs (gradient ends and middle, sweep quadrants, radial, composite, rotate / skew / transform, foreground paints in two text colors), Twemoji sbix / OT-SVG / OT-SVGZ smileys, the ChromaCheck CBDT glyph, color glyphs off, and the same glyphs drawn through the draw layer (WARP).draw.push_mask(dc, bounds, fx),draw.mask_layer(dc, mode, op),draw.pop_mask(dc)(draw/mask.odin): content drawn into a layer, multiplied by the coverage of mask layers drawn after it, each by its alpha or its luminance (Mask_Mode), composited with the layers below it byMask_Op(add, subtract, intersect, exclude: CSS mask-composite); then filters, opacity and blend mode aspush_effects. Mask layers draw in sRGB. Without a render target (or in a picture) the content is drawn unmasked and the mask layers are cut away. CSS mask-image and background-clip: text are drawn with it.render.Pass_Mode.Mask(render/filter.hlsl),PASS_MASK_LUMINANCE,PASS_MASK_DEST,PASS_MASK_APPLY,MASK_OP_SHIFT,render.Mask_Op.pop_layercloses a mask too.tests/masks(both blend spaces): alpha and luminance coverage, the four operators, a gradient mask under opacity and a clip, no mask layer, text as a mask (background-clip: text), the fallback inside a picture.
0.9.22026-10-19release
Added
- Frame pacing at the monitor's rate (
platform/display_win32.odin,render/present.odin):platform.display_info(w): the refresh rate of the monitor the window is on, exact (Display_Info.hz59.94, 143.998, 180: the rational rate of the display path fromQueryDisplayConfig, else the current mode, else DWM's composition rate, else 60), itssource, the monitor and aserialthat changes with any of them. Cached; read again afterWM_DISPLAYCHANGE,WM_DPICHANGED, a move or size loop, or when the window is on another monitor.display_invalidate(w).platform.wait_handle(w, handle, timeout_ms): waits for a handle (the swap chain's latency object, a timer) while taking the window's keyboard and mouse messages as they arrive; they are kept for the nextpoll_events.Event.stamp: when the window procedure received the event (input latency).platform.take_paint_request(w): aWM_PAINTthat came during the app's ownpoll_events(see Changed).render.set_present_mode(r, .Vsync | .Tearing)(tearing: for variable refresh displays, the app paces its frames; the swap chain is created withALLOW_TEARINGwhentearing_supported(r)),present_mode,set_sync_interval(r, n)(every n-th refresh),set_expected_refreshes(r, x)(app-timed frames),frame_waitable(r).- Frame statistics after every present:
take_displayed(r)returns each frame once it reached the screen (Displayed_Frame: present number, the input it was built from (set_frame_input, ormark_input), when Present returned, the vblank it was shown at, refreshes since the previous one and refreshes it came late for in a run of continuous frames (set_frame_continuous)).present_totals,measured_refresh_hz(vblank interval measured from the statistics),vblank_grid(the last vblank and the period: the grid frames land on),qpc_to_tick. Renderer.swap_buffers(set beforeinit): back buffers of the swap chain (default 2).-define:D3D_DEBUG=falseturns the Direct3D debug layer off in a-debugbuild (profiling with true GPU times).
render.wait_for_frame(r, timeout_ms)takes a timeout and says whether the swap chain was ready.
Changed
WM_PAINTduring the app's ownpoll_eventsno longer runs a nested frame (it presented twice in one turn of the loop and waited twice for the swap chain, breaking the cadence): the frame being built repaints, andtake_paint_requestsays one was asked for. Inside a loop of the system (live resize, move, menus:WM_ENTERSIZEMOVE/WM_ENTERMENULOOP)on_frameis still called fromWM_PAINT.RENDER_PROBEprints frames presented / shown / missed, input-to-screen latency and the measured rate from the new statistics.stepper: holding a button wakes the loop at the next repeat instead of drawing a frame at every refresh.
0.9.12026-10-19release
Changed
- Merge of 0.9.0 (DirectWrite text, retained glyph runs) with 0.8.1 (status bar, frame graph) and 0.8.2 (named palettes).
0.9.02026-10-19release
Text on DirectWrite (shaping, fallback, bidi, color glyphs, atlas pages) merged with retained drawing (0.7.0).
Added
- Glyph runs record into pictures:
draw.glyph_run(andtext/text_exon top of it) records shaped glyphs (positions from the shaper, subpixel positions, color glyphs) like direct drawing; text is rasterized for the picture'sraster_scaletimes the slot's scale (withinTEXT_RASTER_TOLERANCEthe bitmap is stretched), baked only at the picture's own scale. Batches switch between gray and color pages inside a picture as in a frame. - Staleness per atlas page: a picture remembers the pages its glyphs are on and their epoch (
font.Page_Ref,Picture.pages); it is.Stalewhen one of them was reused for other glyphs, or when the rendering settings changed (font.atlas_settings_generation), not on every eviction. A replayed picture marks its pages as used by the current frame (font.atlas_pages_hold): they are not reused under it.Batch_Bounds.gray/.color: the pages a picture batch samples, bound to the replaying context's textures (a picture can be replayed by another context). - Device loss: the atlas pages upload again whole from their CPU copy (same texture handles); pictures with text stay valid and replay without recording (before: the atlas was reset and every picture with text went stale).
font.Glyph_Image.epoch,font.Page_Ref,font.atlas_pages_hold,font.atlas_settings_generation.tests/retained: the scene has color emoji and shaped Arabic (system fallback) next to Latin text, rotated glyphs with an emoji; a page reuse with tiny pages makes only the picture on it stale while a picture drawn every frame keeps its page and replays exactly; new rendering settings andclear_atlasmake a picture stale; after a device loss a picture with text replays identically without recording.- Cascades:
font.cascade(entries, features, no_system_fallback): per-character fallback through the entries (ranges= CSSunicode-range), then the system fallback (IDWriteFontFallback::MapCharacters); clusters stay in one face (marks, selectors, ZWJ emoji sequences). OpenTypefeatureson every face. font.with_simulations(synthetic bold / oblique),font.axes,font.with_variations(variable fonts),font.unload,font.valid,font.primary_face,font.set_settings,font.clear_atlas,font.clear_word_cache.- Shaping:
font.shape->Shaped{glyphs (visual order), clusters (logical order), width}(Glyph_Pos,Cluster),Shape_Options{direction, one_run, letter_spacing, word_spacing, justify},font.measure,font.caret_x,font.hit_index,font.shaped_destroy. Words shaped once and cached (sharded, bounded, per-thread front cache): measuring is thread-safe without a global lock. - Analyses:
font.bidi_levels,bidi_runs,runs_of_levels,visual_order,reorder(UAX #9);font.line_breaks(UAX #14, dictionaries for Thai...);font.maybe_rtl,font.first_strong_rtl. - Glyph bitmaps:
font.glyph_image(grayscale,Settings.subpixel_stepshorizontal positions, rendering mode and grid fitting fromSettings), color glyphs (COLR / CPAL layers composited to RGBA, drawn as images: emoji), atlas pages of both kinds with LRU reuse that never clears a page a current frame uses (atlas_begin_frame,atlas_remove_user,atlas_page_count,atlas_page_info,atlas_evictions). draw.glyph_run(dc, size, pos, color, glyphs)(the painter's entry point),draw.text_ex(..., opts).font.Settings/font.DEFAULT_SETTINGS(font.init(s, settings)).tests/text: faces, memory faces, fallback (U+2190 in a Latin font), ligatures and kerning, Arabic joining, Devanagari, bidi, line breaks, color emoji pixels, atlas stress with a tiny atlas, measuring from 8 threads, and a WARP pixel test of drawn text.
Changed
font.set_settingswith new rendering settings (subpixel steps, rendering mode, grid fit, color glyphs) forgets the cached bitmaps (they were made with the old settings; with other subpixel steps their keys meant other positions) and bumps the settings generation.- Text under a GPU transform at its layout size (rotation, skew) is placed at subpixel positions in local units, as text at a uniform scale: a picture recorded at its own scale and replayed rotated draws what direct drawing draws.
Picture.atlas_generationandfont.reset_atlasare gone (font.clear_atlasclears the pages: pictures on them go stale).fontis rebuilt on DirectWrite (stb_truetype is gone): faces from the system collection (font.load_system,font.has_system_family), files (load_from_file) and memory (load_memory,load_from_memory,load_embedded: a custom font file loader, keys never reused). SameFont_Id,metrics,text_width,x_from_index,index_from_x,glyph(now outline metrics) andkern(a pair shaped together minus apart) as before, so existing callers get fallback and shaping unchanged. See [docs/text.md](docs/text.md).font.Metricsgainsx_height,cap_height,underline_offset/_thickness,strike_offset/_thickness.draw.textshapes (fallback, ligatures, kerning, bidi in the string's direction) and draws throughdraw.glyph_run; it moved todraw/text.odinwithTEXT_RASTER_TOLERANCE.draw.Context.glyphsholds the textures of the atlas pages;draw.init/begin_frame/end_frame/destroycall into it (text.odin).font.Stateno longer haspending/pending_pixels/glyphs/fonts: the atlas is in pages uploaded per draw context (font.atlas_sync_page).
Fixed
font.shapeof one right-to-left run of several words (one_run, a layout fragment) left its clusters in visual word order: the caret of the logical end and hit testing landed inside the text. Clusters are sorted back to logical order (O(n log n)).- A full atlas no longer resets in the middle of a frame (glyphs drawn earlier in the frame showed the glyphs rasterized over them: "20 ☒ 7 minutes").
0.8.22026-10-19release
Added
- The palettes of fidget are built in:
PALETTE_GRAPHITE,PALETTE_NORD,PALETTE_DRACULA,PALETTE_AMBER,PALETTE_AERO,PALETTE_BOOTSTRAP, theNAMED_PALETTEStable (with Midnight and Daylight) andpalette_named(name). fidget uses the table.
0.8.12026-10-19release
Added
- Status bars and frame statistics (
ui/status.odin):status_bar(id)(a bar along the bottom of a window, theme tags "bar", text a little smaller),status_fill(id)(the flexible part of the bar: takes the room left, clips its items),status_item(id, tone, mono, clickable, width),status_separator(). Frame_History: a ring ofHistory_Samples (up toHISTORY_SERIES= 8 stacked values, a time, an app tag):history_init/history_destroy/history_resize(keeps the newest) /history_clear/history_push/history_at/history_last/history_find_tag(complete a sample later: GPU times arrive a few frames late) /sample_total;history_stats(h, since_ms, budget)(n, mean, min, max, p50, p95, p99, over budget, per-series mean and max, span) andhistory_rate(h, now_ms, window_ms)(samples per second).frame_graph(id, &history, Frame_Graph_Opts): one stacked bar per sample, newest at the right; per-series colors,series(draw more series than the history counts, e.g. a GPU time over the CPU phases),bars, a dashedbudgetline,scale/max_scale(the top fits most bars; a rare spike is cut and marked instead of flattening the rest),warn/badcap marks,highlight. The result is the box's signal plus the history index of the bar under the pointer.graph_scalegives the top it uses. Nothing asks for frames.stat_row(id, label, value, width, mono, swatch_color, tone),stat_heading,swatch: rows of detail panels and graph legends.popover(id, anchor, width, side, align_right): a floating panel above or below a widget (the other side when it does not fit, clamped to the window), open as long as the app builds it;popover_rect(id)for input the app routes itself.render.set_gpu_timing(r, on): the GPU time of eachframe_submitmeasured at run time, without-define:GPU_TIMING(timestamp queries in a ring, read back later: the CPU never waits);gpu_timing_enabled,gpu_next_frameandgpu_result(the time and the index of the submit it measured, to attribute it to its frame).render.texture_memory(r): GPU memory of the renderer's textures by kind (glyph atlases, images with their mips, layer targets).examples/fidget: its status bar uses them (build and draw time per frame, stacked, with a tooltip per frame).
0.8.02026-10-19release
Added
- Menus rebuilt (
popups.odin): any depth of submenus (menu_submenu, opens on click, afterMENU_HOVER_MSof hover, or with Right / Enter / Space; to the right of its item, or the left when there is no room),menu_separator, disabled items (menu_item(..., enabled = false)or adisabledscope: muted, skipped by the keyboard), automatic width (ctx_menu(key)with width 0: as wide as the widest item), placement that stays on screen (flips to the other side of the point or anchor rect, then clamps),ctx_menu_open_rect(below a button, above when there is no room),ctx_menu_open(..., from_keyboard = true)(first enabled item selected),menu_select,menu_depth,menu_rect. An open menu owns the keyboard (key presses and typed text, in order): Up / Down / Home / End over enabled items (wrapping), Right / Left through submenus, Enter / Space choose, Escape closes one level, a letter selects the next item starting with it. Levels the app stops building close. - Keyboard lists (
list.odin):list(id, &List_State)+list_row(&st, i, label, selectable): one Tab stop with an active row; Up / Down / Page Up / Page Down / Home / End (skipping non-selectable rows such as group headers), Enter or double click (activated), Escape (cancelled), type-ahead (LIST_TYPEAHEAD_MS), the active row kept in view; rows drawn with "selected" / "weak_selected" tags.list_select,list_state_destroy. - Wrapped text:
Box_Flag.Text_Wrap(a.Textheight is that of all the lines at the width the X pass gave),paragraph(id),wrap_lines. - Line edit history and commands: Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z (typing and deleting merge into steps,
FIELD_UNDO_STEPS), Ctrl+Insert / Shift+Insert / Shift+Delete;field_undo,field_redo,field_can_undo,field_can_redo,field_cut,field_copy,field_paste,field_delete,field_insert,field_select_all,field_has_selection,field_selection(for context menus). tab_stops(on)scopes (widgets left out of the Tab order),tab_direction(s)andfocus_step(s, from, dir)(an app handing the focus to its own content and back),tabs(..., right_clicked, middle_clicked).ui.destroy(s)frees the state.platform.Key.Menu(the application key),Num_Add,Num_Subtract(Ctrl+keypad plus / minus zoom), keypad digits asN0..N9.tests/keyboard: menus (keyboard, submenus, Escape by level, placement, press outside), lists (moves, headers, type-ahead, kept in view, Enter / Escape), line edit history and clipboard, wrapped text, without a window.- fidget: View menu with submenus.
Changed
ctx_menu(widthnow optional) pads its panel;State.ctx_menu_key,menu_sel,menu_width... are replaced byState.menus(the stack of open levels). Dropdowns open below their button (above without room) and from the keyboard (Down, Alt+Down, F4) with the current value selected.
0.7.02026-10-19release
Added
- Retained drawing:
draw.Picture,picture_begin/picture_end(record what is drawn between them instead of drawing it),replay(dc, pic, m)(Replay_Result:Drawn,Culled,Stale),picture_destroy,picture_release,picture_bounds,picture_stale,picture_size,recording. Transforms pushed while recording are slots (current_slot,push_slot,picture_set_slot,picture_slot): their matrices change without recording again (one GPU range rewritten). Clips pushed while recording are mapped at replay (an exact scissor under a uniform scale + translation, aspush_clip). A replay draws under the current transform, alpha stack and clips, in the order of the frame (immediate drawing and replays mix). Pictures that would show text at another raster size, or whose glyphs were evicted from the atlas, reportStale. - Persistent GPU heaps for pictures (
render.Heap_Kind,heap_alloc,heap_free,heap_write,heap_bytes,heaps_reset,Slot_Data,Replay,Batch.replay,Frame_Data.replays): first-fit ranges, growth by GPU copy.draw.set_retained_budget/retained_bytes: pictures not drawn in the current frame are evicted (least recently drawn first) past the budget and uploaded again from their recording when drawn. - Device loss:
render.recoverrecreates the device, the swap chain or offscreen target, the pipeline and every texture under the same handle (contents cleared:device_generationchanges);frame_beginanddraw.begin_framerecover on their own (Presentfailures andGetDeviceRemovedReasonare watched);device_lost,simulate_device_loss(tests). The glyph atlas is reset, the heaps emptied (heap_generation). - Gradients evaluated per pixel:
draw.gradient(dc, rect, Gradient, radii, tint);Gradient_Kind(linear, radial, conic), repeating, stop positions, transition hints,Gradient_Space(sRGB, linear, Oklab, Oklch, Lab, Lch, HSL, HWB),Hue_Method, tiles (size, repeat per axis, spacing: CSS background-size / repeat / space), dithering.render.KIND_GRADIENT, gradient tables (Frame_Data.grads,GRAD_*). - Box shadows:
draw.box_shadow(outer, optionally cut out of a rounded rect) anddraw.inset_shadow: the exact gaussian blur of a rounded rect with per-corner radii (erf along x, the corner arcs integrated over their angle),render.KIND_SHADOW,SHADOW_INSET,SHADOW_HOLE. Rect_Inst.aux,Clip_Data.xform,CLIP_SCISSOR,Batch.shape.- Tests:
tests/retained(replay identical to direct drawing, slots, z order, partial recording, culling, budget, device loss, in both blend spaces),tests/gradients,tests/shadows(against a numeric convolution). examples/transforms --retained(with--stress=N).
Changed
Clip_Datais 96 bytes;render.initandinit_offscreenare split (create_swapchain,create_offscreen_target) so thatrecovercan rebuild them.push_effects/push_layerwhile recording fall back to the alpha (a picture holds no offscreen layer);backdrop_capturereturns nothing.
0.6.02026-10-19release
Added
- Blend spaces:
render.Blend_Space(Linear,Srgb): where colors mix (blending, antialiased edges, interpolation, image filtering and mips, layer composites).Srgbcomposites like web browsers (CSS Color 4 / Compositing 1:rgba(0, 0, 0, .5)over white is 128).render.init(..., space),render.init_offscreen(..., space),render.set_blend_space(at run time: frame view, translucent swap chain format and color space, pooled targets),render.blend_space. Per layer:draw.Layer_Effects.space(draw.Layer_Space),draw.blend_space(dc);render.Layer.space,render.Image_Ref.space. draw.set_interpolation(dc, .Target | .Srgb | .Linear)(draw.Interpolation): where corner colors interpolate (render.FLAG_LERP_SRGB,FLAG_LERP_LINEAR), e.g. CSS gradients in sRGB whatever the frame's space.- RGBA textures stored in the renderer's space when created (
render.texture_space;R8G8B8A8_UNORMpremultiplied on sRGB values inSrgb), converted by the shader when drawn into a target of the other space (render.FLAG_TEX_SRGB). - Text correction:
render.Text_Rendering(curve:None,Srgb,Power;gamma,contrast,coverage_exponent),render.set_text_rendering,render.text_rendering,render.text_rendering_default,TEXT_RENDERING_LINEAR(the historical look),TEXT_RENDERING_SRGB(Chromium on Windows: Skia's sRGB curve, contrast 1.0). Skia'sSkMaskGammacomputed per glyph in the vertex shader; same text weight in both spaces. draw.alpha_like_linear,ui.tuned: translucent colors tuned for linear light keep their look in an sRGB frame;ui.apply_palette(t, p, space); the UI interpolates its gradients in linear light in both spaces.render.linear_to_srgb,render.srgb_to_linear.tests/blend_space(WARP pixel tests of both spaces);tests/layersandtests/effectsrun in both spaces.- Examples:
effects --srgb,transforms --srgb, fidget's Settings > Blending (and--blending=srgb|linear).
Changed
- Corner colors are interpolated premultiplied (a transparent corner no longer darkens a gradient).
- The glyph atlas holds raw coverage (
font.GLYPH_GAMMAis gone): the 0.8 exponent is nowText_Rendering.coverage_exponent, applied by the shader (same look inLinear). - The constant buffer of
rect.hlslis 48 bytes (target space, text parameters). - A translucent window's swap chain color space is set explicitly (
SetColorSpace1).
0.5.02026-10-19release
Added
- Filters on layers:
draw.push_effects(dc, bounds, Layer_Effects{alpha, filters, blend})(thenpop_layer).Filter/Filter_Kind/Color_Matrix; constructorsfilter_blur,filter_drop_shadow,filter_brightness,filter_contrast,filter_grayscale,filter_sepia,filter_saturate,filter_hue_rotate,filter_invert,filter_opacity,filter_matrix;filter_is_identity,filters_outset. Chains run as GPU passes (render/filter.hlsl): color matrices fused (up to 8, clamped between), separable gaussian blur with bilinear tap pairs and power-of-two reductions aboverender.set_blur_sigma_limit, drop shadows. Colors are filtered in sRGB as browsers do. - Blend modes:
Blend_Mode(the 16 modes of CSS Compositing 1, plusPlus_Darker/Plus_Lighter) onLayer_Effects.blend; the composite reads a copy of what lies under it (render.FLAG_BLEND,Batch.backdrop). - Backdrops:
draw.backdrop(dc, r, radii, filters),backdrop_capture/backdrop_draw(CSS backdrop-filter, frosted glass),render.FLAG_SCREEN. - Clip shapes:
draw.push_clip_ellipse,draw.push_clip_polygon(nonzero / even-odd), antialiased. - Renderer:
render.Frame_Data,render.Op(clear, copy, filter pass),render.Pass,render.Image_Ref,render.acquire_snapshot,render.release_target,render.blur_taps. tests/effects: pixel tests of every filter and blend mode against the specifications' formulas, blur energy / symmetry / profile (full resolution and reduced), drop shadows, ellipse and polygon clip coverage, backdrops, target reuse.examples/effects(build.bat effects): demo,--stress=N,--bench=N. Screenshot indocs/screenshots.
Changed
- Frames run their steps in recording order (layers are no longer drawn before the frame):
frame_submit(r, Frame_Data)replaces the positional arguments;Batchgainsbackdropandop;Layer.doneis gone. - Inside a layer the clip stack restarts at the layer's rect; the clip shapes around it cut its composite.
- Pooled targets are handed back as soon as their last reader is recorded: a frame with many layers uses a few textures.
Clip_Datais 80 bytes (kind,first,count,flags);CLIP_CIRCLEmoved fromparenttoflags.
0.4.22026-10-18release
Added
draw.set_circular_corners(dc, on) -> previous: rounded rects, images and rounded clips drawn while it is on use circular arcs whateverrender.set_corner_smoothingsays (the squircle suits the UI; web content and other specifications need circles). Per primitive:render.FLAG_CIRCLE,render.CLIP_CIRCLE(or-ed intoClip_Data.parent). Reset bybegin_frame. Pixel test intests/layers.
0.4.12026-10-18release
Added
- Layers (group opacity):
draw.push_layer(dc, bounds, alpha)/draw.pop_layerdraw into an offscreen render target composited once withalpha, so overlapping primitives of the group do not show through each other (CSSopacity). Transforms, rounded / rotated clips, images, text and nested layers work inside. Falls back topush_alphawhen the layer would be empty or too large.draw.layer_depth. - Renderer support:
render.Layer,Batch.target,frame_submit(..., layers), a pool of fp16 render targets (render.acquire_target,release_targets,targets_bytes),render.FLAG_LAYER(texel-exact composite). - Offscreen renderer:
render.init_offscreen(r, size, warp)(no window; WARP software rasterizer available) andrender.read_pixels.render.destroyreleases every GPU object. tests/layers: pixel tests (group opacity vs. per-primitive alpha, opaque layer = no layer, nested layers with a rotated rounded clip, target pooling, fallback).examples/transforms: the orbiting planet fades as one group (push_layer).
Changed
- The shader constant buffer is 32 bytes (adds
target_origin).initis split into device creation and pipeline setup (shared withinit_offscreen);frame_endandresizedo nothing for an offscreen renderer.
0.4.02026-10-18release
Added
- 2D affine transforms in
draw:Affine(CSSmatrix(a, b, c, d, e, f)convention),push_transform/pop_transform,current_transform, and the helpersaffine_mul,affine_translate,affine_scale,affine_rotate,affine_skew,affine_around,affine_apply,affine_apply_vector,affine_inverse,affine_bounds,affine_determinant,affine_scale_factor,affine_is_identity,affine_is_scale_translate,affine_from_xform. Rects (rounded, borders, shadows), gradients, text and images follow the transform; rotated and skewed shapes are evaluated in local space in the pixel shader (exact corners, 1 px antialiasing). Scaled text is re-rasterized at its on-screen size. - Rounded / transformed clips:
draw.push_clip_rounded(dc, r, radii)(per-pixel, antialiased, nests up to 8 deep);push_clipunder rotation or skew clips the exact rect.draw.current_clip. - Images:
render.create_texture_rgba,update_texture_rgba(sRGB, straight alpha in; stored premultiplied in linear light),destroy_texture,texture_size,max_texture_size; mip chains regenerated after updates.draw.image,draw.image_corners(rounded corners, sub-rectangles, tint) anddraw.image_tiled(seamless repeat). Images and glyphs share batches. examples/transforms(build.bat transforms): animated demo,--stress=N/--bench=Nload test.-define:GPU_TIMING=true+render.gpu_ms: GPU time of the frame (timestamp queries).- Unit tests for the affine helpers:
odin test tests/affine. - [docs/draw.md](docs/draw.md): transforms, clips, images, costs and limits.
Changed
render.Rect_Instis 144 bytes (addsxform,clip,flags);use_textakesKIND_SHAPE/KIND_GLYPH/KIND_IMAGE.render.Batchgainsimage;render.frame_submittakes the optional transform and clip tables.draw.Context.clipsholdsdraw.Clip(scissor + shape) andxformsholdsdraw.Transform(anAffine).push_scale/pop_xformkeep their behaviour; the existing examples render pixel-identical to 0.3.2.- Soft shapes (
soft > 1) drawn underpush_scalescale their blur with the rest of the shape.
0.3.22026-10-15release
Fixed
- Wine (and any system without DirectComposition):
render.init(transparent = true)fell over onCreateSwapChainForComposition; it now falls back to an opaque window (render.is_transparenttells which). - fidget: removing one of the initial tags crashed (it deleted a string literal).
- fidget: a clicked or dragged sticky note went to the back instead of the front.
0.3.12026-10-15release
Changed
- Self-contained exes: the icon font is compiled in (
ui.ICON_FONT_DATA,font.load_embedded); nothing has to sit next to the exe any more. - Text fonts: Segoe UI and Consolas are read from the Windows font folder (
platform.system_font_path); when they are missing (Wine, trimmed installs) the exe falls back to Roboto and Inconsolata compiled into it (font.load_system_or_embedded,font.FALLBACK_UI_DATA,font.FALLBACK_MONO_DATA). fidget:--embedded_fontsforces the fallbacks.
0.3.02026-10-15release
Added
- Arrow-key focus navigation: once a widget that does not use arrows has the focus, the arrows move it to the nearest Tab stop in that direction (trapped inside modals, scrolled into view). Sliders, text fields and open menus keep their arrows.
- Scrollbars: every
View_Scroll_Ybox gets an overlay scrollbar (drag the thumb, click the track to jump);text_edithas vertical and horizontal bars. - Wrapping rows:
ui.flow(gap)and theWrapbox flag place children left to right and start a new line when the row is too narrow. Chips and the fidget button / tag / swatch rows use it. - Corner rounding:
ui.set_rounding(0 = square, 1 = default, up to 2) scales box radii and widget chrome;ui.radfor custom draws. Radio buttons stay round, and so do boxes with the newFixed_Radiusflag. Rounded rects can use squircle corners (render.set_corner_smoothing), which removes the visible kink where an arc meets a straight edge. At rounding 0 the Windows 11 window corners go square (platform.set_square_corners). - Density:
ui.set_density/ui.dense(Comfortable orDENSITY_COMPACT): row heights, vertical gaps and paddings shrink, the text keeps its size. - Disabled and muted scopes:
if ui.disabled(cond) { ... }disables everything built inside (no clicks, hover or focus) with a neutral look and a fade;button_icon(disabled = ...)uses it.if ui.muted() { ... }dims secondary content that stays interactive. - Finishes:
Palette.finishpaints raised surfacesFlat,Gradient(Bootstrap 2: vertical gradient, top highlight, embossed text, focus glow) orGlass(Windows 7 Aero: glossy top half, bottom glow, white rim).ui.paint_rectgives custom draws the same finish. Palettes can also set a bar colour. - Translucent windows:
render.init(transparent = true)presents through DirectComposition with a linear fp16 (scRGB) swap chain, so translucent pixels compose correctly;platform.set_backdropshows the Windows 11 acrylic backdrop behind them. - Selection outline: selected rows, menu items and palette entries get a crisp accent border over a light fill.
- fidget: Aero and Bootstrap themes (8 themes; Graphite is now the default), Settings for rounding, corner shape and density, an "Enabled" switch that disables a card, a grouped data table (collapsible department groups with totals, two-line headers with column totals, right-aligned numbers, column separators, alternating rows),
--bench=N(CPU time per frame),--compact,--size=WxH,--long_text. tools/keyboard_test.ps1: 33 checks (arrow navigation, menus closing on a click outside).
Changed
- A box with
Draw_Borderkeeps its border out of the layout (like CSS border-box): its children sit inside it, so a border never hides a child's own outline. - Disabled boxes no longer get a dimming overlay: they switch to neutral colours and fade as a whole.
Fixed
- Context menus and dropdowns now close on a click anywhere outside, not only on clickable boxes.
- Rects ignored the alpha stack (
draw.push_alpha): only text faded. - The page and the menu bar inherited the default radius, leaving dark pixels in the window corners.
- The sort arrow and resize lines of the table header were not aligned with the column separators.
0.2.02026-10-15release
Added
- Multi-line editor
ui.text_editwithui.Text_Area: selection (keyboard and mouse, double-click word), Up / Down keep the column, Home goes to the first non-blank character, Ctrl+Home / End, Page Up / Down, word moves and deletes, Enter keeps the indentation, clipboard (CRLF and tabs normalized), undo / redo (Ctrl+Z, Ctrl+Y or Ctrl+Shift+Z; edits grouped per second), wheel scrolling, optional line numbers, optional Tab-indents mode, Ctrl+Enter = committed, Escape leaves the field. - Keyboard focus navigation: Tab / Shift+Tab move through the Tab stops in build order (wrapping), the focused widget is scrolled into view and gets a focus ring; Enter / Space activate buttons, checkboxes, radios, switches, segmented items, chips, tabs and dropdowns; sliders take arrows, Page Up / Down, Home / End. Inside a modal, Tab cycles through the dialog only, and the page loses the focus when the dialog opens.
- Context menus and dropdowns from the keyboard: Up / Down select, Enter / Space choose.
ui.focused_name()(id of the focused box), new box flagsText_InputandScroll_Self.- fidget: a multi-line editor card, more shortcuts in the cheat sheet,
--state_titlefor scripted tests. tools/keyboard_test.ps1: 28 checks with real keystrokes and modifiers (Ctrl+A/C/V, Tab, Space, Ctrl+Z/Y, Ctrl +/-/0, Ctrl+K, Ctrl+, and a dropdown driven from the keyboard), only sent while fidget is in front.- New logo: a white switch and slider on a cobalt rounded square (
tools/make_logo.ps1), also the icon oftodo.exe.
Fixed
icon_buttonregistered its box under a key it then replaced: the button could not hold the keyboard focus.- A focused widget no longer steals Enter / Space from an open context menu.
0.1.02026-10-15release
First release.
Added
- platform (Win32): per-monitor DPI aware window sized from the monitor DPI, events (keys, text with UTF-16 surrogates, mouse, wheel with modifiers), clipboard, cursors, dark title bar, OS "show animations" setting,
wait_event/poll_events(re-entrancy safe during live resize), app icon from resource id 1. - render (Direct3D 11): one instanced-quad pipeline for SDF rounded rects, borders, soft shadows and glyphs; sRGB target (linear blending); waitable swap chain with one frame in flight for low input latency (~47 ms input-to-scan-out at 60 Hz, measured);
RENDER_PROBElatency / frame-gap probe. - font: TrueType via
stb_truetype, glyph cache, shelf-packed atlas, text measurement and caret mapping. - draw: rects (per-corner colors and radii), text, clip / alpha / scale stacks, sRGB / hex / HSV helpers.
- ui: box tree with keyed persistence (
##/###id rules), style / tag / parent scopes using@(deferred_none), five-pass layout (pixels, text, percent, children-sum, strictness, floating, cross-axis alignment, scrolling), signals (hover, press, click, double / right / middle click, drag, wheel), animations that follow the OS setting, tag-based themes with cross-fades and two built-in palettes, built-in zoom (Ctrl+wheel, Ctrl +/-/0). - Widgets: label, button (+icon), icon button, checkbox, radio, switch, slider, range slider, stepper, segmented control, chips, rating, progress (determinate / indeterminate), dropdown, tabs, key caps, cards, form rows, single-line text field (selection, word moves, clipboard, mouse), scroll region, context menu, menu bar, tooltips, toasts (8 positions), modal dialogs, color pickers, fuzzy matching with highlighted labels, custom-draw boxes.
- fidget: widget gallery (Controls, Inputs, Data with a 20,000-row virtualized table, Playground), command palette, menu bar, Settings and About dialogs, six themes, persisted settings.
- todo: a todo app with persistence, filters, search, context menu, light / dark theme.
- smoke: renderer, font and draw layers without the UI.
build.bat, embedded exe icons, scripted interaction tests (tools/fidget_test.ps1,tools/ui_test.ps1), screenshot generator (tools/screenshots.ps1), icon generator (tools/make_icon.ps1).