News
Release notes from the Teksilo changelog.
Teksilo is pre-1.0. Review breaking changes before upgrading. Full Changelog: https://github.com/FernTech-EU/teksilo/compare/v0.15.0…v0.15.1 Chart constructors ( Stop dispatching the key presses winit makes up for keys held as a window gains focus on Windows and X11. Enter that answered a native modal on its press reached the parent window a second time once it took focus back, so a list behind a confirmation opened the row the cursor had moved to. Resolve public unsafe traits by name in Limit named-type heading boosts in documentation search so results relevant to the full query can outrank basic examples that only name the type. Accept the application guide’s source path in corpus validation while retaining checks for relative paths and existing source files. Update the app guide and bundled skill reference for the 0.14 API, including AccessKit re-exports and derived sample signals. Document categorical chart axes, history retention and scrolling limits. Refresh corpus vectors and cover common chart and scrolling queries. Include public trait contracts, associated types and constants, and required and default methods in Dispatch intents emitted by app callbacks, including window close guards, before returning to the event loop. A vetoed close that requests a confirmation dialog no longer waits for another key or pointer event to show that dialog. Restored the generated gesture arbitration table and refreshed the search corpus after documentation edits changed its no-winner markers. Removed an unnecessary borrow flagged by Clippy on Rust 1.99. Restored crates.io sources and checksums in the release lockfile so clean checkouts can resolve dependencies with Full Changelog: https://github.com/FernTech-EU/teksilo/compare/v0.14.3…v0.15.0 Updated the Breaking: cargo-teksilo CLI. Removed Explicit agent selection. Explicit model download. Concise CLI output. Short summaries and actionable errors replace lengthy explanations. Updated command examples, migration documentation, the repository skill, and the embedded skill and agent instructions installed by cargo-teksilo. Expanded onboarding and shortened user guides. Moved engineering records out of the published docs and search corpus. Removed obsolete chapters and corrected documentation claims. Added README synchronization and documentation checks. Full Changelog: https://github.com/FernTech-EU/teksilo/compare/v0.14.2…v0.14.3 Full Changelog: https://github.com/FernTech-EU/teksilo/compare/v0.14.1…v0.14.2 Full Changelog: https://github.com/FernTech-EU/teksilo/compare/v0.14.0…v0.14.1 Tabbing away from a control whose tooltip was showing sent focus back to it. Once focus had brought up a control’s rich or composite tooltip, Tab moved on to the next control and then, about 120 ms later as the tooltip faded out, focus returned to the control just left: a screen reader’s reading of the next control was cut, the old control was read again, and only a second Tab got away. Tabbing out of a tooltip that had turned sticky did the same. Focus now stays where Tab put it. A Orca heard no key typed into a Teksilo window in a Wayland session. Orca 46 reads no keyboard there: it learns of a key only when the application reports it to the AT-SPI registry, as GTK and Qt do, and Teksilo did not. So Orca said nothing when an arrow key moved the caret, echoed no typing, and none of its own commands ran (Orca+T, which says the time): the keys went to the application instead. On Linux, while an assistive technology is attached, each key press and release is now reported to the registry before the application sees it, and a key the screen reader takes for one of its commands no longer reaches the application, nor does its release. A key typed into a secure field (a An empty text measured by text-typeset had no text run. A composite that takes focus itself was announced by its structural node. Where a focusable widget hands its accessibility node to a descendant ( Focus that a rebuild took away came back in the wrong place. When a widget rebuilt itself and the control that had focus went with it, focus went to the first control of the whole rebuilt widget. For a widget that keeps its parts across a rebuild (a toast stack, a tab widget, a docking layout) that was the first part, whichever one the user was in. Focus now comes back in the innermost part that is still there. A screen reader could act on the page behind a modal. While an in-tree modal was up (a Every window was an unnamed “frame” to Orca. The accessibility tree’s root, which AT-SPI presents as the window, carried no name, so Orca 46.1 announced each window as it came up with the bare word “frame”. The root now takes the window’s title, and follows it when the title changes. Windows and macOS read the title from the native window and are unchanged. Orca heard only the first message the framework’s announcer said. Every A control a reader had met went silent on Orca once it left and came back. A tab page shown again, a reopened menu, combo list or date picker calendar, a snackbar, tooltip or Caps Lock warning shown a second time, a page of the widget catalog visited twice, a chart or combo box scrolled back into view, an editor focus came back to: each returned to the platform under the id it had left with, which the AT-SPI adapter had announced defunct as it left, and Orca 46.1 dropped every event from it, focus included, so the reader heard nothing from the second visit on. Anything that leaves the tree a reader sees and comes back now reaches the platform under an id it has never had, and is heard as it was the first time. The ids An announcement made as focus moved was cut before it was heard. A keyboard move of a row or a tab (“Moved to 2 of 3”, “Doc 1 moved to 5 of 6”, “Moved to level 2”) puts focus on the moved item and says where it went, and Orca 46.1 stopped the message to read the item: no move in a The announcement ring recorded text no platform announces. A merged name took the text from under a hidden descendant. A field’s validation message was never read on arriving at the field. A throttled frame-tick subscriber held back every frame requested while it was on screen. A widget on A check box, a switch, a radio button or a slider changed without a screen reader being told. On Space, an arrow key, a click or a screen reader’s own activation, A screen reader heard nothing as the highlight moved through a menu. In every A submenu opened from the keyboard closed itself. Right, Enter or Space on a submenu row, or its mnemonic, opened the submenu as a mouse click would, and it closed about 165 ms later whenever the mouse rested anywhere but on the row or the submenu, so a keyboard user could not reach File > Recent in the example, or a docking layout’s “Move to” sides. It now stays open until Escape, Left, a choice or a click outside, as one an assistive technology opens always did. Text fields told a screen reader nothing of the caret, of an empty field, or of a password. In every field built on A A date field’s calendar wrote a date over the field that the user had not chosen. The calendar of a A calendar’s months and years views could not be used from the keyboard or by a screen reader. There the arrows moved the cursor of a day grid no one could see, in silence, and Enter or Space selected that hidden day. Now the arrows move over the months (the years), each heard with its year (“May 2026”), PageUp and PageDown move a year (a decade), Enter or Space opens the one under the cursor, and Escape goes back to the days; nothing is selected on the way. Activating the title takes the keyboard into the grid, on the month shown. A month or a year offers a screen reader’s click, and they are one Tab stop, the grid’s, where each month was a stop of its own. The years view follows the cursor, or a header arrow, into the next decade. Behaviour change: a month cell is named with its year. The fields of the date and time editors were read without a name, or without their date. The field focus lands on in a A custom overlay trigger could not be reached from the keyboard, or was reached as an unnamed panel. The widget handed to Opening a popover with nothing to focus in it put the reader on a node with no name and no role. Orca 46.1 said nothing, and Tab from there went to the first control of the window. Focus now lands on the popover’s dialog, so the reader hears its name and the text it holds; Tab goes on to the control after the trigger and Shift+Tab back to the trigger, and either closes the popover. A popover’s dialog given no Focus in a A keyboard user could not leave a Arrowing through an open Every change to the toast stack read every toast again and moved keyboard focus. Showing, updating or dismissing any toast announced each toast still on screen again, in no set order: with an Error toast shown after three others, Orca spoke all four titles. A progress toast updated in place announced its unchanged title at every step, twenty times over for A toast expired under keyboard focus. A reader who Tabbed to a toast’s action had the rest of its ten seconds to decide, then the toast went and Orca said “frame”. A toast now stays while keyboard or screen-reader focus is on it or inside it, as it does under a resting pointer. A toast shown while another was up left with it. A second toast was charged for the time before it appeared and expired at the first one’s deadline: shown 5 s after another it lasted 4.8 s instead of 10, and an Error toast shown last lasted 2 s. Each toast now gets its own time. The notification bell and log were rebuilt on every notification. Every toast is archived, and a background job archives each of its steps. Focus on the bell was fired again on a new button, which cut the toast being announced and said “Notifications push button” instead; an open bell popover closed under the reader and marked the new notification read unseen; and the log dialog threw focus back to Mark all read at every step of a job, so Clear all could not be pressed. The bell, its popover and the log’s buttons now stay put and keep focus; only the unread badge and the rows whose notification changed follow the archive. Clear all now says “No notifications”. The chosen segment of a A dialog’s content was hidden from assistive technology. The panel The content of a An open menu listed none of its items to assistive technology. The panel Toasts were never announced and could not be reached. The A A A Opening a A focused A A After a live change of language, a A The calendar spoke English and ISO dates to a user in any language. In French the grid was “Calendar, septembre 2026”, a day was “lundi août 31, 2026”, and the value read “2026-09-24 (selected: 2027-03-12)”. Every string A A calendar said things twice. The grid was a polite live region, and a live node speaks its name as it enters the tree and on every rename, with every descendant inheriting the setting. Opening a date field’s calendar announced the grid’s name, then each of the seven weekday headers, and then focus said the name again; PageUp or PageDown in the grid announced the new month while Orca spoke the rename of its focus as well; and each header button Tab reached was announced as it appeared and again as it took focus. The grid is no longer live, and the calendar’s content is The context menu opened from the keyboard acted somewhere else. Shift+F10 or the Menu key in a Closing a field’s context menu selected the whole field. When focus came back to a single-line field from its own menu (Escape, one of its commands, a click outside it), the field treated it as a keyboard arrival and selected everything: Orca said the field’s text followed by “selected”, and the next key replaced it all, even right after the menu’s Paste. Focus now comes back to the caret and selection the menu found. Arriving by Tab still selects the field. Full Changelog: https://github.com/FernTech-EU/teksilo/compare/v0.13.1…v0.14.0 Full Changelog: https://github.com/FernTech-EU/teksilo/compare/v0.13.0…v0.13.1 Three things it now refuses to do. It never writes Cline is served too, and is the one vendor whose path is read off the disk rather than fixed: The order is deliberately not newest-first. Cline’s source calls No frontmatter in any layout: Cline’s docs say a rule without one is always active, so its absence is what keeps the brief unconditional. Two things it refuses to do, both found by adversarially reviewing the command against itself and reproduced before being fixed. It will not rewrite a shared file it cannot read whole: In user scope it reaches three agents, Claude Code ( It is the dry run of Three words ( Its own command rather than “Reads only” is enforced rather than asserted. It asks cargo for the resolved version through Version binding is the design constraint, not a detail. The tool reads the app’s What a refusal can tell a model to run depends on which side of this release the app sits on, because The automation probe harness, written into a consumer’s project by Three new guides: Agent tooling documents the above; Scroll areas is the reference the docs did not have, scrolling was covered only by an architecture chapter and the kinetic-scroll physics, so an ordinary “how do I make this scrollable?” had nothing to land on; and Docking layout is the consumer guide for Full Changelog: https://github.com/FernTech-EU/teksilo/compare/v0.12.1…v0.13.0 No changelog entry found for version 0.12.1 Full Changelog: https://github.com/FernTech-EU/teksilo/compare/v0.12.0…v0.12.1 Breaking, pre-1.0: Undo itself stays in the data layer; this crate ships no stack and no Full Changelog: https://github.com/FernTech-EU/teksilo/compare/v0.11.0…v0.12.0 The Underneath, the GPU floor drops to what the renderer actually needs. A machine with no Vulkan driver could not open a window at all; it now falls back to OpenGL, which is what an older GPU has. Behaviour change, breaking. Full Changelog: https://github.com/FernTech-EU/teksilo/compare/v0.10.0…v0.11.0 One strand above all: the input model is a pointer model. A touchscreen, a pen and a trackpad reach the widget tree as real pointers, a density knob resizes every target with a 24 dp conformance floor at each density, and a mouse behaves exactly as it always has. Around it: touch and a stylus in the automation bridge, a target-size gate over every shipped preset, a previewer that exports at a chosen density, every SVG icon drawn pixel-exact, and a window that opens on GLES-3.1 class hardware. The rich text editor takes text-document 1.12.2, so a sentence cut at a paragraph’s end is the sentence and nothing more. Breaking, pre-1.0: the pointer-event variants gained fields, two positions were renamed, The framework’s input model was a mouse: one pointer, always hovering, always precise, always present. It is now a pointer model, and a mouse behaves exactly as it always has. The pointer-model behaviour changes. Each is a token or a knob an application can override, and none of them changes what a mouse does. Full Changelog: https://github.com/FernTech-EU/teksilo/compare/v0.9.5…v0.10.0Version 0.15.1 2026-10-04
Added
Widgets
eo) translations for all 313 framework messages, available through teksilo_widgets::framework_locales().NotificationArchiveModel::push_update, which archives an in-place update of a notice still on screen, beside push, which archives a notice raised. UPDATE_HISTORY_LIMIT is how many update records a row keeps.Changed
Widgets
NotificationArchiveModel::push with the dedup_id of an archived row now takes the entry as that notice raised again: the row moves to the top, takes the entry’s time and read state, and records the occurrence even when nothing it says has changed. An app that pushed to the archive itself to update a notice still on screen calls push_update instead, which updates the row where it is.Fixed
Core
Collapse opened there (by an assistive technology, an automation client, or the app itself in a window behind another) held its body at no height while its header already said it was expanded, in a scrolling dialog as anywhere else. Only looping animations are paused now; a tween runs to its end, as it already did for a widget off screen. A loop started while its window had no focus also starts when the window gets it, rather than later by however long the window had been without focus before the loop existed.Render
Widgets
ImageWidget::new and IconWidget::from_raster / from_animated named their texture after the icon’s memory address, so an icon created where a dropped one had lived reused the old texture. They now name it after the icon’s identity, which a clone shares. An IconWidget shown in both Tintable and FullColor mode also shared one texture between the two and drew both from whichever registered first; each mode now has its own. As before, these textures last as long as the window: an icon decoded afresh for every widget gets a new texture each time.Version 0.15.0 2026-10-04
Added
ChartSource and direct ChartWindow support in line, bar and pie charts and their legends, borrowing visible points and tracking window changes.cargo teksilo search --path PREFIX to restrict documentation search before lexical and semantic results are combined.Changed
LineChart::new, BarChart::new, PieChart::new and ChartLegend::new) now accept impl Into<ChartSource<T>> instead of ChartModel<T>. Explicit ChartModel arguments continue to work. Calls that rely on the constructor to infer the argument type may need an explicit type:// Before
LineChart::<String>::new(Default::default());
// After
LineChart::<String>::new(ChartModel::<String>::default());Fixed
cargo teksilo symbol, including cross-crate lookup when the module name differs from the trait name.cargo teksilo symbol output.--locked.Version 0.14.3 2026-10-03
Changed
text-document dependency to 1.12.5.setup, bare probe, and version. Use init, probe install, and --version (tool version) or status (resolved app version). User installation is now agent install <agents...> --user, which installs instructions only.agent install <agents...> and repeated init --agent options create the selected agents’ configuration even when no detection marker exists. Without explicit targets, init uses detection. agent list reports supported targets and installation state.model fetch replaces automatic downloads and the --no-model flag. Initialization never downloads weights; search loads complete local model files or falls back to BM25.--verbose adds diagnostics; --quiet suppresses informational messages without truncating API or document contents. Search, status, and agent listings support --json.Fixed
cargo-teksilo
[package] table.--force; shared-file text outside managed regions is retained.teksilo-widgets.Version 0.14.2 2026-09-28
Changed
text-document 1.12.4.Fixed
Widgets
n你 behind. The range is now measured back from where the candidate ends.Version 0.14.1 2026-09-28
Changed
text-document 1.12.3.Accessibility
accesskit 0.25.1 and accesskit_consumer 0.39.1, and through accesskit_winit 0.34.1 the platform adapters accesskit_windows 0.35.1, accesskit_macos 0.27.1 and, on Linux, accesskit_unix 0.24.0 with accesskit_atspi_common 0.21.0. On Linux a disabled control is no longer reported enabled and sensitive, so Orca announces it as unavailable rather than as a control that should respond, and an image (Role::Image, and Canvas, GraphicsSymbol, SvgRoot) answers AT-SPI’s Image interface. The re-exported teksilo::accesskit stays on 0.25, so nothing changes for code that uses it.Fixed
Widgets
outdent, decrease_blockquote_depth, and Backspace at the start of a quote or a list item) are not limited by depth. A document already deeper than the limits opens and edits as it is. The deepest structure the gestures can now build, a level-16 list item inside 64 quotes, costs a Djot parser at most 80 levels of recursion. indent, increase_blockquote_depth and toggle_blockquote stop at the same place, and a toggle or a Tab over a selection is judged by the deepest quote the selection holds, since the wrap takes all of it one level down. The default context menu greys out its blockquote row where the toggle would do nothing.create_list only reached blocks in the document’s root frame, so inside a quote or a table cell the item left its list and joined none; every later Tab then nested the quote instead. With text-document 1.12.3, Tab and Shift+Tab in a quote, and indent and outdent there or in a table cell, move the item one level as they do in the main text, stop at the same list ceiling, and undo in one step.increase_blockquote_depth quoted a paragraph that was not in a quote. It is documented as the command behind Tab in a quote and as doing nothing outside one, and now does nothing outside one.Automation
example_dialogs.py, which cargo teksilo probe writes into a project, expected focus back where it was before a popover opened. Since 0.14.0 a custom popover trigger can take focus, so a mouse press on it does, and closing the popover rightly hands focus back to the trigger. The probe now expects the trigger, or, where the press gave the trigger no focus, the control that had it before.Workspace
Cargo.lock recorded text-document and text-typeset with no source or checksum. A developer’s local [patch.crates-io] overlay strips both, and 0.13.1 and 0.14.0 were tagged with such a lockfile: cargo metadata --locked failed on a fresh clone, and cargo audit skipped those eleven crates. The lockfile resolves from crates.io again, a new CI job fails one that does not, and tools/relock-crates-io.sh repairs it. The script keeps every crate at the version the lockfile was tested with, and leaves the lockfile untouched with an error naming each crate when crates.io cannot supply that version, rather than taking the newest release.Version 0.14.0 2026-09-26
Added
Accessibility
teksilo_platform::key_report: each key reported to the AT-SPI registry. KeyReportGate reports every key press and release to org.a11y.atspi.DeviceEventController.NotifyListenersSync while an assistive technology is attached, and says which keys the screen reader took, so they are not dispatched. teksilo-app runs every KeyboardInput through it; Linux only, it does nothing on Windows and macOS.tools/reader/: what a screen reader gets from an example, recorded. tools/reader/reader.py runs an example in a private, invisible desktop session (its own D-Bus, AT-SPI bus, KWin and runtime directory, no DISPLAY, no sound) and records the AT-SPI events it emits, the tree a reader walks, and what Orca 46.1 says, speaking to nothing. Acts are real key presses through the private compositor, AT-SPI actions, or the automation bridge. Scenarios state what a reader should get; tabwalk and tree need no scenario. Linux only. See docs/a11y/reader-harness.md.Widget::accessibility_proxy: a composite can publish itself through the field that holds focus. Where a composite keeps its focus, text and value on one inner widget, that widget is the node a screen reader lands on and has to carry the composite’s name, while an application can only reach the composite’s id. Returning the inner widget from the hook makes the tree apply everything attached to the composite (its overrides, a FormLayout’s labelled_by, access_described_by, the tooltip it owns) to that widget’s node, after the widget’s own, and point relations naming the composite at it. The overrides guide has a section on it.accessibility::audit::focusable_nodes_hidden: a control that takes focus inside a hidden subtree. A hidden node hides everything under it from every platform adapter, and only the focused node is let back through, alone, so a reader lands on the control and finds nothing around it. The audit lists every node that offers Action::Focus while hidden, itself or through an ancestor, and is not disabled. The widget previewer’s catalog census now runs it over every widget and documentation snippet. Beside it, audit::nodes_in_filtered_tree lists the nodes accesskit_consumer’s filter keeps, which are the ones a platform adapter walks and can announce.RichTextEditor, CodeEditor, PlainTextEditor and LogView take a .label(..). It names the text itself, which is what a screen reader lands on and announces (“Notes, entry”, “Build output, document”), and stays locale-reactive. Before, nothing could name that text: a name given to the widget went to its outer node, not to the text a reader reads.HandlerSet::access_customize: a widget can finish the accessibility of a node it reaches by id. The twin of WidgetWithHandlers::access_customize, for the nodes a widget writes to through BuildContext::apply_handlers, and chained after any customization the node already carries.BuildContext::first_focus_capable_descendant: the control a wrapper stands for, even while it is disabled. Beside first_focusable_descendant, which finds only what can take focus now, this finds the first widget in a subtree that is declared focusable, so a control disabled as it mounts is still recognised as the control.Internationalization
TeksiloDateTimeFormatter::date_fields(DateFields) picks which fields the date part names: YearMonthDay, the default and what every date style rendered before; YearMonthDayWeekday, CLDR’s full date at DateStyle::Long (“lundi 31 août 2026”, “Monday, August 31, 2026”); or YearMonth, a month with its year (“août 2026”). ICU picks the pattern for the whole combination, so the order and the grammar are the locale’s: a Russian month is genitive after a day and nominative beside its year alone. format_in_locale(value, &lang) renders one value once, as a plain String, in a locale the caller names, for a string computed on demand, such as an accessibility name. calendar_system(CalendarSystem::Gregorian) keeps the date on the Gregorian calendar where CLDR gives the locale another one: by default fa-IR writes 24 September 2026 as the 2nd of Mehr 1405, and th-TH counts its year 2569.Core
EventContext::context_menu_trigger: what asked for a context menu. A context_menu factory is handed a point whatever opened the menu, and only a pointer’s is one the user chose. Inside the factory, ctx.context_menu_trigger() now says ContextMenuTrigger::Pointer (a secondary click, or a hold), Keyboard (the Menu key, Shift+F10, Ctrl+Shift+M on macOS) or Accessibility (an assistive technology’s ShowContextMenu), so a factory can act on the point, moving a caret to it or picking the item under it, only when the user pointed there.Changed
Data views
TreeView over an index SelectionModel keeps the selected row selected through a structural change. An insert, a removal or a collapse above it used to leave the selection at its old position, on whichever row moved there. The selection now moves with its row, as a keyed selection does. Multiple selections are unchanged. Behaviour change.Fixed
Core
Snackbar that times out no longer sends focus back to its trigger after the user has moved on either.PasswordField) is reported without its character: its physical key and modifiers only, which the screen reader’s own commands need, so a program listening to the registry can still tell which keys were pressed there. GTK and Qt report the character too. A press waits at most 200 ms for the registry’s answer. A registry that stops answering costs one such pause, however many keys follow; one that answers every key more than 200 ms late costs up to 200 ms on each key typed after it has caught up. What a reader hears on an X11 session, where Orca reads the keyboard itself, on Windows and on macOS is unchanged.TextRunSource::from_geometry built no line from the geometry text-typeset returns for an empty string, so an empty label or field laid out by it offered no Text interface on AT-SPI and none of the text ranges the other platforms read. It now emits the one empty run TextRunSource::flat always did, so the first text such a node gains, and the edit that empties it, are reported like any other change.Widget::accessibility_proxy), a screen reader now lands on that descendant when the widget takes focus, hears its name and content, and is told of the caret and selection moves AT-SPI reports only for the focused node. A screen reader’s own request to focus that descendant puts the keyboard on the composite, where its caret, input method and focus ring live, and a context menu the composite owns is offered on it.MessageBox, a Dialog, any Centered overlay), its scrim kept the pointer off the page and Tab stayed inside it, but a screen reader’s own requests still reached the controls behind it: an AT-SPI click behind “Save changes?” opened a second box over it, a click behind “Close window?” toggled the document’s checkbox so that the next close quit with no question, and a focus request put the keyboard on a control the box covers. An assistive-technology action aimed behind the modal is now refused and reported unhandled, so the reader’s click does nothing and focus stays in the modal. The modal’s own controls, and a menu or drop-down opened over it, take every request as before. The page stays in the tree, readable, and is as alive to the reader once the modal closes as before it opened. The automation bridge answers such a request (invoke_action, focus_node, set_value, expand, collapse) with its unhandled-action error.announce in every Teksilo application, after the first of a session, was dropped by Orca 46.1, and even a first one was lost whenever Orca handled the event after its node had gone. Each message is now spoken from a node the tree has never had, which stays in the tree for five seconds (teksilo_core::announcer::LINGER) and then leaves for good. A burst keeps at most eight in the tree at once. The tree no longer carries two hidden announcer nodes while nothing is being said, so a TreeUpdate of an idle tree has two nodes fewer.sync_accessibility returns, which the automation bridge and tests read, are unchanged. An integration that feeds an AccessKit adapter itself hands it WidgetTree::deliver_accessibility and routes the adapter’s action requests through WidgetTree::resolve_adapter_action. Windows and macOS keep no defunct state, and nothing changes there that a reader can tell.ListView, TreeView, GridView or tab bar was ever heard whole. A message the announcer is asked for as focus moves, including to a list’s new current row, now reaches the screen reader after the focus change, one frame later, so the reader hears the item and then the message. A field focused as the message is announced leaves the message out of its description while focus stays, as it already did where the reader keeps the announcement.WidgetTree::announcements_since, and the automation bridge’s pull_announcements that reads it, recorded a live node’s value before its label, from every live node the walk emitted, whenever that text changed. Every AccessKit adapter announces a node’s name instead, which is its value only for a Role::Label, and only for a node in the filtered tree. So a hidden live region, one inside a hidden subtree, and a Status or Alert carrying its text as a value were all recorded while every platform stayed silent, and a probe reading the ring passed over the silence. A tree that records announcements now replays every update sync_accessibility returns, including the re-placed copy a scroll hands out, through accesskit_consumer, and the ring records what all three adapters announce: a live node’s name as it enters the filtered tree, and again when it changes while the node stays there, at the politeness the node has or inherits from a live ancestor. A node inside a live region is recorded, as the adapters announce it. A change of politeness alone, which Windows and macOS announce and AT-SPI does not, is not, and neither is a blank name. Several in one update are kept in reading order, so a message the framework’s announcer says comes after a change of the application’s own live region in the same frame. The replay is a second pass over every node of every update, making a full sync of 3,000 nodes 1.4 to 2 times as long, so only a tree that has a reader records: one built with WidgetTree::new(), as every headless tree and every test is, and the tree of a window teksilo-app opens only when install_automation_bridge_in_debug() installed the bridge, which it does in a debug build with the automation feature. The new WidgetTree::set_records_announcements switches it. Behaviour change for anything that read the ring, and for anything that read it from a window without the automation bridge, where it is now empty.announce_unless_widget_speaks kept quiet beside a live region nobody could hear. It took a widget to be speaking when any node in its subtree carried a politeness and a value or a label, so a hidden live region, or a Status holding its text as a value, silenced the framework’s own message, and the user heard nothing at all. It now reads the last update through accesskit_consumer, whether or not the tree records announcements, and counts only a live node the platform adapters walk, with a name to speak, whose politeness is set inside the widget. A button that is live only because it sits in a toast or another live region is not the one speaking, and does not keep the framework quiet about its own menu. The long-press context-menu announcement goes through it.accessibility::announced_text and the label audits took a value for a name. For any role but Role::Label, announced_text fell back to the node’s value when it had no label, and audit::duplicate_label_leaks and labels_without_text_ranges read names the same way, while no platform adapter takes a value as the name of anything but a label. A Status holding its text as a value was reported as announcing it, and a label repeating such a value was reported as repeating a name nobody hears. Both now read a Role::Label’s value and any other node’s label, and nothing else. Behaviour change for a test that found a node by a value through announced_text.access_merge_subtree skipped a hidden descendant’s own name but went on merging its children, so text an application hid with access_hidden(true) on a wrapper was read aloud on the merged node. A hidden descendant and its whole subtree now contribute nothing, as the adapters treat them. The rustdoc of AccessNodeBuilder::set_hidden, clear_hidden and access_hidden said the flag was local to its node; it now says that it hides the subtree and that a wrapper which is only chrome is a Role::GenericContainer instead.access_described_by, and the four stock inputs that wire their ValidationStrip with it (TextInput, PasswordField, DateTimeEdit, DateRangeEdit), promised WCAG 3.3.1: the message read as the field’s description when it gains focus. AccessKit 0.25 passes the relation to no screen reader, on any platform: the consumer never derives a description from it, and no adapter exports it. So coming back to a refused field said its name and value and not why. The tree now writes each described_by target’s text into the node’s description, after its own, which Orca, NVDA and VoiceOver read. Around focus it holds new text back, because Orca speaks a change to the description of the node it holds as focus: a message appearing while the user is in the field is said once, by its live region, is not begun again as the user leaves, and is read with the field on the next visit. A shown tooltip no longer takes away its anchor’s description either. Behaviour change: a node carrying described_by now carries a description. An application that announces a message itself as it sends focus to the field showing it is heard once on each platform, by a different voice: on Windows the arrival leaves the announced text out, since NVDA says the announcement; on Linux and macOS the arrival reads it, since Orca cuts an announcement to read the new focus (VoiceOver is unverified and treated the same way).subscribe_frame_tick_throttled set the pace not only of its own tick but of every request_frame: with a once-a-minute clock painted, the announcer’s follow-up syncs, a caret or a drag auto-scroll each waited up to a minute. An announcement queue drained one step per wait, so its later messages reached the screen reader at the user’s next key press, spoken ahead of whatever that key said. The interval now delays the subscriber’s own tick and nothing else; a requested frame is due at 60 Hz whatever is subscribed.Widgets
Checkbox, Toggle, RadioButton and Slider changed their state and no platform heard of it: the new state reached the accessibility tree only with the next unrelated update, most often the focus move that followed, where Orca’s “checked” or “52” was cut by the new focus. Each change now reaches every platform in the frame that makes it, whether the user, a screen reader or the application made it, and so does a Checkbox in a ListView or TreeView row checked with Space on the row. A Slider also gives the platform its figures as they are written: an f32 0.3 was published as 0.30000001192092896, which Orca spoke digit by digit, and is now 0.3, as are its minimum, maximum and steps. A value the arrow keys reach reads as the step it reached: three steps of 0.01 up from 0.3 are read as 0.33, not as the 0.32999998 the f32 sums come to.MenuList, which is every menu-bar menu, context menu, popover menu and submenu, the arrow keys, Home, End, PageUp, PageDown and type-ahead moved a highlight no platform could see: Orca said “menu.” as a menu opened and nothing after it, and Enter ran a command the reader had never heard. Each move of the highlight is now a focus change to the highlighted item on Linux, Windows and macOS, so a reader hears “Open”, or “Word Wrap, check menu item, checked”, as it goes. A menu is named after what opened it, so opening one says “File menu”, “Recent menu” or “Add menu” (a context menu stays unnamed), and each item carries its place among the menu’s items for a reader that announces it (“3 of 5”).TextInputField (TextInput, SearchField, SpinBox, PasswordField, the date and time editors), an arrow, Home, End, a Shift selection or Ctrl+A reached no platform: Orca 46.1 said nothing, and a reader was told the caret and selection of the last edit. Each move and selection is now published as it happens, and Orca speaks the character or the selection. An empty field now offers the Text interface on AT-SPI, so the first character typed or pasted into it, and the deletion that empties it, are reported. A masked password field now exposes its mask as its text, one echo character per character and never the plaintext, so Orca echoes each keystroke and deletion; a NoEcho field exposes an empty text and reports nothing typed.PrivacySettings with telemetry configured crashed every debug build. Its consent switches are named by their row’s label, which the switch’s own check for a missing name could not see, so the application panicked as its accessibility tree was built, and the widget catalog’s Settings tab exited as it opened. The panel now stays up, and a screen reader reaches each switch by Tab, named by its row (“Anonymous usage metrics”, “Crash reports”, “Feature flags”) and read with its state. Release builds did not crash.DateEdit, a DateTimeEdit or a DateRangeEdit opened on the date the field held when it was built, or wherever the last opening had left its cursor, or on the months view it was closed on, and Enter wrote that day over the field’s value without a word. It now opens on the date the field holds (for a DateRangeEdit, the start; for a DateEdit, the text typed in it counts), in the day view, and the reader hears that day. A DateRangeEdit’s calendar closed with one end of a range picked kept that end, and one Enter in the next opening wrote a range from it to the cursor over the field’s and closed the calendar; each opening now begins a new range.DateEdit or a TimeEdit had no name, so Orca said “entry 05/02/2026”; it now carries the editor’s .label(), or “Date” and “Time”, and an access_label or access_described_by given to the editor reaches it. A DateTimeEdit’s parts and a DateRangeEdit’s halves reached Orca as date editors, read without their text (“End date date editor.”); they are now entries, read with their date or time. Behaviour change: those parts are Role::TextInput, no longer Role::DateInput or Role::TimeInput; the editor around them keeps its date role.Dialog::trigger, Snackbar::trigger, Wizard::trigger or a PopoverCustom carried its button role and name on one node and took focus on another. A popover over a custom trigger was no Tab stop at all, and a screen reader could not focus it either; a dialog’s custom trigger took focus as the unnamed panel it wrapped, which Orca 46.1 read as “panel.”. The node that takes focus is now the trigger’s button: Tab lands on it, the reader hears its name, and Enter, Space and the reader’s own activation open the overlay. A trigger that is already a control, such as a Button, stays the one Tab stop under its own name, carries the popup state, and now opens the overlay when a screen reader activates it, where before nothing happened; the wrapper around it is no longer a second button, which for a Snackbar had been named with the snackbar’s message. Behaviour change: a custom trigger around a widget that takes no focus is now a Tab stop, including the filter glyph of a filterable TableView column.surface_name is now named by its trigger, where before it had no name. A bare() popover with nothing to focus leaves focus on its trigger.RichTextEditor, CodeEditor, PlainTextEditor or LogView reached no screen reader. Tabbing or clicking into one put focus on an unnamed node with no text, which Orca 46.1 announced as “section.” or not at all, and no caret move that followed was ever reported, so every arrow key was silent. A reader now lands on the text itself, an editable text or a document, hears its name and the line at the caret, and hears each caret move. Coming back to a rich text editor or viewer is heard too, where the return used to be silent. An .access_label(..), .access_labelled_by(..), .access_description(..) or tooltip attached to one of these widgets now names or describes the text. The rich-text-editor, rich-text-viewer, code_editor and log_view examples name their surfaces.CodeEditor or a PlainTextEditor. Tab indents in them, and so did Ctrl+Tab, while Ctrl+Shift+Tab dedented, so every key meant to move focus out wrote into the document instead, and a screen reader heard nothing of it. Ctrl+Tab and Ctrl+Shift+Tab now move focus to the next and the previous control and leave the text alone, as they already do out of a terminal and a table; Tab and Shift+Tab still indent and dedent. The editor’s text node now says so in its description (“Tab indents. Ctrl+Tab moves to the next control, Ctrl+Shift+Tab to the previous one.”), in the user’s language, and Orca reads it as focus arrives there. A read-only viewer does not take Tab, so Tab leaves it and it carries no such description.ComboBox list was silent, and changed the value at every step. Focus stayed on the combo box (or on its search field) while the arrows moved its value, and nothing told a screen reader which option they had reached, so Orca heard nothing as a reader moved through a long list, the font list or the list of languages. The keys now move a highlight that the focused node names as its active descendant, so a screen reader on Linux, Windows or macOS hears each option as it is reached, and the option the list opens on. The value changes only on a commit: Enter or Space in the open list, or a click on a row; Escape, Tab and closing the list keep it. on_select therefore fires once, on that commit, and a LanguageSwitcher no longer switches the application’s language (nor a ThemeSwitcher its theme) at each arrow while a reader listens to the choices. Down from an empty combo box reaches the first item, where it skipped to the second. A long list is one list box of named options: each option used to sit inside an unnamed, unselected option of a second list box, and on Linux the list box reported no selected option. A searchable list’s search field is named after the combo box (“Search” when it has no label), and its placeholder is translated. Behaviour change: the arrows, Home / End, the page keys and type-ahead no longer write the bound selected signal or fire on_select; code that read the value after an arrow press presses Enter first.toast-demo’s background job. Each change also replaced the control a keyboard user was on and put focus on the oldest toast, so the Cancel of a progress toast could not be pressed from the keyboard. A toast is now announced when it appears and when its title changes, and nothing else is read again. Focus stays where the reader put it, on a toast or on one of its actions, and an action kept across an update runs the update’s callback. When an update removes the focused action, focus stays in that toast.SegmentedControl was read as “not selected”. Each segment is a radio button, and a screen reader tells a radio button’s state from whether it is checked, which no segment ever was. Orca read the segment the user had just chosen as “not selected radio button”, and by the Windows and macOS adapters’ own code NVDA was given no state for it at all and VoiceOver no checked value. The chosen segment is now checked, as a RadioButton is, so Orca says “selected radio button”. A segment also stopped claiming to be selected in the list sense, which had Orca speak the chosen segment of a control whenever the control came into view or its choice changed, wherever the reader was: five cut “not selected radio button” at the launch of chart-demo, and a Tab onto a field that scrolled a control into view was cut short by that control’s segment. Behaviour change: a test or a probe that asserted a segment’s selected asserts its toggled now.RecipeDialogStyle draws around a ModalContainer’s content called set_hidden() to say it was only chrome, and the consumer every platform adapter reads through treats a hidden node as hiding everything under it. So on Linux, Windows and macOS a screen reader reached the focused control of a dialog and nothing else: not the title or the message as text, not the other fields and buttons, and not even the focused control when walking the dialog, whose children it no longer listed. Orca’s flat review gathers what it reviews from those children (getOnScreenObjects in its script_utilities.py), so it had nothing to review, and a walk of the UIA tree met the same empty dialog, since the Windows adapter lists children through the same filter; what NVDA’s object navigation made of it was not observed. The panel is now a bare Role::GenericContainer, which the adapters drop while keeping its children. Every ModalContainer is affected, on the default style and on the macOS and Fluent presets, which reuse it. A custom DialogStyle should do the same, as DialogStyle::make_panel now says. What that lets through is presented once: a ModalContainer whose content is itself a Role::Dialog or Role::AlertDialog, as a MessageBox’s and a CommandPalette’s is, now publishes a bare Role::GenericContainer instead of a second dialog of the same name, which Orca would speak as focus entered each; and the CommandPalette’s content is Live::Off, so the polite palette is announced by its name alone, and not row by row. Behaviour change: the container of a presented MessageBox is no longer a Role::Dialog named by the title. A test or a probe that found the box’s buttons under that node finds them under the box’s Role::AlertDialog, which holds them on this version and the last.Card, a Panel, a Toolbar, a StatusBar, a snackbar and a RadioTile body was hidden from assistive technology. The frames RecipeCardStyle, RecipePanelStyle, RecipeSnackbarStyle and RecipeRadioTileStyle draw, and a Panel marked a11y_presentational, called set_hidden() the way the dialog panel did. A screen reader found a card, a panel, a toolbar, a status bar or a snackbar empty except for whichever of its controls held focus, and never reached a tile’s body. Each is now a bare Role::GenericContainer, and the snackbar’s frame is also Live::Off, so a snackbar that announces its message says it once. The macOS and Fluent presets build their cards, panels and snackbars from these frames, and Material 3 its cards, so they are fixed with them. The make_body of CardStyle, PanelStyle, SnackbarStyle and RadioTileStyle now says what a custom frame has to do.RecipePopoverStyle draws for its Menu variant, under every MenuList, ComboBox drop-down and search suggestion list, called set_hidden(). Whatever held focus could still be heard, since the filter lets a focused node and an active descendant through, but the menu listed no children, so a screen reader could not review the items around it. The surface is now a bare Role::GenericContainer, and PopoverStyle::make_body says what a custom one has to do.ToastHost every toast is mounted under called set_hidden(), so no toast ever entered the tree a platform adapter reads. The AT-SPI, UIA and macOS adapters all announce a live region as it enters that tree and pass over one that is filtered out, so a toast’s Role::Status or Role::Alert said nothing on any platform, and its text and actions could not be reviewed or used from a screen reader. The host is now a bare Role::GenericContainer, and the toast’s content is Live::Off, so a toast is announced by its title, once; its body is its description, read on reaching it, and its actions and close button stay reachable. ToastStyle::make_body says a custom style has to do the same.TabWidget’s panel, a Stepper’s step and anything inside a Switcher, MaxSize or AspectRatio was hidden from assistive technology. The three layout primitives called set_hidden() to publish no node of their own, and a hidden node takes its whole subtree out of every platform’s tree. TabWidget and Stepper show their pages through a Switcher, so the selected Role::TabPanel and everything in it could not be reached, and neither could a Calendar’s day grid, the title bar’s maximize button or the debug inspector’s panel, while a menu capped by max_visible_items lost its rows to the MaxSize around them. A screen reader met a control inside them only once focus reached it. All three are now a bare Role::GenericContainer, pruned from the tree, and the page a Switcher shows takes its place; its other pages stay out of the tree as before, because they are dormant. Behaviour change: none of the three publishes a node of its own any more, so what one wraps is a child of the node around it, in the update sync_accessibility returns and in an automation snapshot alike. The snapshot, which ignores the hidden flag, held the same pages before, under the hidden node.Snackbar’s button, the notification bell, a TitleBar’s centre content, an unlabelled Splitter pane and every TreeTableView cell were hidden from assistive technology. Each sits inside a shell that called set_hidden() to keep itself out of the tree: the Snackbar around its trigger, NotificationCenterButton around its IconButton, the title bar’s drag region around TitleBar::center, a splitter pane that has no pane_label (or a labelled one while collapsed, sliver and all), and the row inside each TreeTableView row. A screen reader could reach any of them only while it held focus, and a tree table’s cells not at all. Each shell is now a bare Role::GenericContainer. Behaviour change for a test that read the drag region’s hidden flag: it is no longer hidden, and still no stop.Banner was followed by its description, its action and its dismiss button. The banner is a polite live region named by its title, and every node inside it that sets no politeness of its own inherits it, so the AT-SPI, UIA and macOS adapters announced each of them as a message of its own as the banner appeared, in the order of the consumer’s hash set. Orca speaks each announcement with interrupt set, so what was left to hear was whichever came last. The banner’s content is now Live::Off: its title is what it announces, once, and the rest stays reachable. A StatusBar with announce_changes(true) keeps the inheritance, which is what it is for.MessageBox announced its default button, and not its question. The box is an assertive live region so that its question is announced as it appears, and every node inside inherited the setting. The hidden dialog panel let only the focused default button through, and it was announced, assertively, as a message of its own, while the box’s name never was: a confirmation opening on its default button announced “No”, and its question reached a reader only as the name of the dialog presenting the box, around the focus. With the dialog’s content reachable, everything inside the box is now Live::Off: the question, the box’s own name, is what it announces, once, and its text and buttons stay reachable.SpinBox reported a nameless text field. Focus lands on the spin box’s editing field, while its name, value and range sat on a node around it, so a screen reader named the field once or not at all, and could not hear a step move the number. The editing field is now the spin button: Role::SpinButton, the label as its name, the value, range and step, Increment and Decrement, and its own text runs. A focus change says the name and value once, and a step changes the node the reader is on. What an application gives the spin box’s id (access_label, a FormLayout label, access_described_by, a tooltip) reaches that node through the new Widget::accessibility_proxy. The accessible value is now exactly the text a reader can review, so a painted suffix is no longer in it. Behaviour change: a test that read the spin button off the SpinBox’s own id finds a GenericContainer there; the spin button is the field, first_focusable_descendant of that id.SpinBox with a custom value_from_text could not be typed in words. The numeric input filter ran whatever the parser, so a month field that reads "march" as 3 dropped the letters as they were typed, and the commit put the old month back. A custom parser now receives every character and alone decides, at commit, what it can read; the default parser keeps its filter.SpinBox step threw away what had just been typed. Up, Down, the wheel, the step buttons and an assistive Increment stepped from the value last committed, so 35 typed over 10 in a field stepping by 5 became 15 on Up. A step now reads the typed text first, as Enter would, and moves on from it: 40, with one on_value_changed. Text that cannot be read steps nothing and the held value is shown again, as in Qt. Enter and focus loss now commit keystrokes delivered in the same batch as the key that commits them, which they used to miss by a frame. Behaviour change.SpinBox wrote wrong values. Switched to French, every spin box showed 440,00, and then went on reading, stepping and filtering in the language it was built in: focus turned the text back to 440.00, a step said -0.5, a grouped 1 234 567 was read “1,234,567”, which is a decimal to a French reader, and the comma typed into a decimal field was dropped, so 12,5 wrote 125. The field now shows, reads, steps, commits and accepts the number in the language switched to: 12,5 writes 12.5, shown and spoken 12,50.SpinBox at its special_value_text told a screen reader “0”. Focus replaced “Auto” with the minimum as it arrived, so a reader moving to a timeout heard “Timeout 0 spin button” and never what 0 means there, while a step back down to the minimum said “Auto”. The special text now stays while the field has focus, as Qt’s specialValueText does: arriving says “Timeout Auto spin button”, keyboard focus selects it so a number typed replaces it, and leaving the field no longer rewrites its text, which Orca spoke as “Text unselected.”. Behaviour change: a click into the field puts the caret in the special text, not in the number; select it, or step, to replace it.Calendar gives assistive technology is now in the user’s language: the words from the framework bundle, and every date written by ICU for the tree’s locale, in the locale’s order and grammar, and on the Gregorian calendar the grid is laid out in even where the locale prefers another. French says the first of the month as “premier” (“1er” on screen), where ICU’s bare “1” is read “un” by a speech engine, and Italian and Romanian say it “primo” and “întâi”, where the digit is read “uno” and “unu” (Serianni, cited by the Accademia della Crusca; DOOM2): “lunedì primo marzo 2027”, “luni, întâi martie 2027”. The Italian date drawn under a range calendar writes “1º mar 2027”, and the Romanian one keeps the digit, which is how Romanian writes it. No other shipped language is rewritten: Spanish and Portuguese read the digit right as it stands, and the languages that read every day as an ordinal are a question for every day, not the first. The same grid now reads “Calendrier, septembre 2026”, “lundi 31 août 2026” and “jeudi 24 septembre 2026 (sélection : vendredi 12 mars 2027)”, and a range is joined by words (“du … au …”). With the cursor on the one selected day, which is where a date field’s popover opens, the day is said once, marked selected, where the value used to say it twice: “vendredi 12 mars 2027 (sélectionné)”. The title is ICU’s month with its year, so Japanese reads “2027年3月”; the decade title reads “2020 to 2029” instead of joining the years with an em-dash; the line under a range calendar uses the locale’s medium date and now follows a range committed from the keyboard or by the application, not only by a click. DateEdit, DateRangeEdit and DateTimeEdit open this calendar, so their popovers are fixed with it. calendar-name-with-month now takes the month with its year as its one $month, the unused calendar-cell-name is gone, and calendar-value-with-selection, calendar-value-on-selection, calendar-date-range and calendar-decade are new, in all 23 locales. Behaviour change for anything that read the grid’s value as an ISO date.DateEdit, DateRangeEdit and DateTimeEdit gave assistive technology an ISO value. The value on the node standing for the whole field was “2026-05-02”, “2026-05-01/2026-05-10” or “2026-05-02T14:35:07”, which UIA and macOS hand to a screen reader as it stands. It is now the day in full in the tree’s locale, “samedi 2 mai 2026”, the range joined by words, “du vendredi premier mai 2026 au dimanche 10 mai 2026”, and the day with its time, “samedi 2 mai 2026 à 14:35”, with the seconds only when the field shows them. The editable text inside keeps its pattern. Behaviour change for anything that parsed those values.Calendar’s days and header buttons could not be reached, and arrowing through its days said nothing on Linux. The day grid’s body and the header row called set_hidden() so as not to be announced beside the Role::Grid, and hid what they hold with them: the 42 cells, each naming its date in full with its selected and today states, and the arrows and the title button. A screen reader found nothing inside the grid to review, and reached a header button only once it held focus. The day under the keyboard cursor lived only in the grid’s value, which UIA and macOS report as a value change and AT-SPI carries on no interface, so Orca heard nothing while the cursor moved. Both are now a bare Role::GenericContainer, so the days are in the platform’s tree, six rows of seven cells under the grid, and so are the header’s five buttons. While it holds focus in the day view, the grid names the cell under the cursor as its active descendant, and AccessKit reports that cell as the focus on all three platforms: each arrow press, and each change of month from the keyboard, is a focus change to the new day (“samedi 13 mars 2027”). The value still says the cursor and the selection, for a client that reads the grid. A day offers Action::Click and, like a grouped RadioTile, no longer Action::Focus: the dispatcher moved keyboard focus onto a day an assistive technology focused, off the grid, and every arrow press after that moved the cursor in silence. DateEdit, DateRangeEdit and DateTimeEdit open this calendar and are fixed with it. Behaviour change for an automation client that focused a day, which is now reported unhandled.Live::Off, so one placed inside an application’s own live region lends it the grid’s name and no day, weekday or button. A change of month made from a header arrow, where focus stays on the arrow, is announced once through the tree’s announcer, as the title now reads; the Today button announces the day it moved to, in full. Neither announces while the grid itself holds focus. Behaviour change for anything that listened to the grid’s live region.RichTextEditor, a CodeEditor or a single-line field (TextInput, PasswordField, SpinBox, SearchField) moved the caret to the middle of the surface before the menu opened, as a right-click there would have. The reader heard nothing of it, or “Text unselected.” when a selection collapsed, and the menu’s Paste then wrote into the middle of the text while Cut and Copy found no selection. The caret and the selection now stay where they were, and the menu acts there. A right-click still moves the caret to the click.Data views
Ctrl+arrow and Ctrl (⌘ on macOS) + Home / End / Page moved the cursor and left the selection behind, so a screen reader announced one row while actions used another. They now move the selection with the cursor in any of the five data views whose selection holds one entry: a SelectionMode::Single model, including one handed to a table in the default MultiRow mode, or a TableSelectionMode::SingleRow / SingleCell table. Multi-selection views are unchanged. Behaviour change.GridView never said how many tiles were selected, and announced each tile it realized. The count was an English value on a grid marked as a live region. A grid takes its name from its label, so no platform announced the value, while every tile inherited the live setting and was announced as it scrolled into the realized window. The grid is no longer live. A click, a key, an assistive click or a marquee that changes how many tiles are selected now says the new count once, through the tree’s announcer, in the user’s language (“3 éléments sélectionnés”), Space on a tile the grid has not realized included. Moving a single selection says no count, since the tile the reader lands on says it is selected. The grid’s value carries the same words. grid-view-selection-count is new, in all 23 locales. Behaviour change for anything that read the value in English.GridView tile made Enter open another tile. A tile offered a focus of its own, so UIA’s SetFocus, AT-SPI’s grab_focus or VoiceOver’s keyboard focus following its cursor put keyboard focus on that tile while the grid’s cursor stayed where it was. Enter and Space then acted on the cursor’s tile, not the one the reader had just heard, and the next key that changed the selection dropped focus onto the window, where the keys did nothing. A tile no longer offers focus: the request moves nothing, the reader’s focus and the grid’s cursor stay together, and a click on a tile, which a reader’s activation sends, chooses it. Behaviour change for anything that focused a tile through Action::Focus.GridView’s selection was heard as a move of focus. The tile under the cursor came back as a tile the reader had never met, so every platform reported a focus change to it, and Orca stopped reading the selection count it had just been given to read the tile’s name again. A toggle was never heard as a change of the tile’s selected state. The tile under the cursor now stays the same tile through a change of selection, its selected state changes where the reader is, and the count is heard in full.GridView tile opened nothing. Its first click set the selection again, which replaced every tile in view, so the second click reached a tile that had not seen the first. A double click on a tile that is already selected now opens it.Internationalization
I18nManager, tr!, tr_widget! and tr_signal! gave a message in the source language only when it was plain text and { $var }; any other came back as its key. A screen reader said “grid-view-selection-count” where “1 item selected” was meant, and a CommandPalette gave its result count the same way. Such a message is now formatted in the source language, plurals, functions and the messages and terms it refers to included.Terminal
Terminal never announced its new output. Each completed line went to a polite Role::Status live region as its value. A Status is named by its label, and every platform adapter announces a live node’s name, so no platform said the line; only the automation ring, which read the value, recorded it. The line is now the region’s name.Automation
type_text and type_ime on a text editor’s own node blurred the editor. A RichTextEditor, CodeEditor or LogView is found as focused, and by its name, on the node that holds its text. Typing into that node still reached the document, but left the editor without its caret, input method and focus ring. The keyboard now stays on the editor.list_live_regions listed live regions no screen reader can reach. It listed every node that declared a politeness: a hidden one, one inside a hidden subtree, and the framework’s two announcer nodes, which are hidden while they have nothing to say. It now lists only the ones accesskit_consumer’s filter keeps, which are the ones a platform adapter walks and can announce. The set comes from teksilo_core::accessibility::audit::nodes_in_filtered_tree. Behaviour change.Version 0.13.1 2026-09-22
Fixed
cargo teksilo build-vectors now runs on a Windows checkout. Git for Windows checks the tree out with CRLF, and corpus/index.json holds exactly one raw newline, so the round-trip guard saw a one-byte difference and refused to run, reporting that the Rust schema and tools/build_corpus.py had diverged. They had not: the checkout had rewritten the file. The guard now compares what the generator wrote rather than what Git handed the platform. cargo-teksilo’s two corpus tests failed on Windows only for the same reason and pass there now..gitattributes normalises the working tree on every platform, so a Windows clone means the same bytes as a Linux one. This is what the corpus depends on: index.json is compared byte for byte, and every file the corpus quotes is reconstructed line for line by cargo teksilo show. Existing Windows working trees keep their CRLF files until refreshed, git rm --cached -r . && git reset --hard, or a fresh clone.Version 0.13.0 2026-09-20
Added
cargo teksilo, agent tooling for apps that depend on Teksilo. An agent working inside this repository has the guides, the worked examples, the skill and the automation harness. An agent working in someone’s Teksilo app had none of them: docs/ ships in no crate, every example crate is publish = false, and the only probe harness lived in one app’s repository. cargo install cargo-teksilo closes that gap, and every answer is matched to the Teksilo the app actually resolved.cargo teksilo symbol <Name>, the exact public API of a type, read from the resolved sources. Works with a crates.io, git or path dependency and needs no checkout: where one is reachable it runs that checkout’s own extractor, otherwise it stages a throwaway repository shaped like this one around the registry sources. A bare name resolves against every teksilo crate, so cargo teksilo symbol ListModel answers with teksilo-data’s without --crate data; the note on stderr says which crate answered, and names the others when more than one defines that name. Types declared directly in a crate’s lib.rs are included, teksilo-webview’s WebView among them. This is the one subcommand that needs python3 on PATH.cargo teksilo search "<question>", retrieval over the 70 hand-written guides and 56 worked example crates, which reach no consumer today. Hits are ranked and carry no score: neither ranker produces a confidence, and the lexical and hybrid numbers are not in the same unit.cargo teksilo show <path>, the document behind a search hit, in full, offline: cargo teksilo show docs/scroll-area.md, or just the lines the hit cited (--lines 166-172, 1-based and inclusive, as search prints them). Nothing is re-fetched, a chunk already carries its own text and the line range it occupied, so the document is reassembled from the corpus, byte for byte. It closes the last version-binding hole in the tool: the path a hit cites exists in no consumer’s project, and fetching it from blob/main/ or the published book serves main rather than the version the app pinned. search now says so in a footer line, and --list prints every path.cargo teksilo probe, writes the automation probe harness into scripts/teksilo_probe/, so an agent can drive the running app and assert on it. Generated files are checksummed: a local edit is reported rather than silently overwritten, and your own probes live outside the generated tree and are never touched.cargo teksilo setup, the above, plus the Teksilo briefing for every coding agent this project already configures, each in that agent’s own format: the full four-file skill into .claude/skills/teksilo/ where it is native, and a self-contained ~40-line brief into .cursor/rules/teksilo.mdc (MDC frontmatter), .windsurf/rules/teksilo.md (trigger: frontmatter), .github/copilot-instructions.md and AGENTS.md (each a <!-- BEGIN teksilo --> region) where it is not. These tools share no format, so copying the skill directory into .cursor/ would accomplish nothing; the brief never refers to the skill, which on those machines is not installed. It also pre-fetches the search encoder (~129 MB, into the per-user cache, --no-model to skip), so the first search does not stall on a download, and a failed fetch is a warning, because search degrades to BM25 by design.$HOME from project scope, the previous version silently installed into ~/.claude/skills/ when the project had no .claude/ of its own, a machine-wide change from a project-scoped command; --user is now the only path that reaches the home directory. It never prompts where nobody can answer: with no terminal on stdin the question is an error naming the flag that would have skipped it (-y, or --user when there is no Cargo.toml anywhere above the working directory), because CI and agents run this and a hang is worse than a failure. And it never writes before showing the plan, every path, every detected vendor, and the download with its size, then a confirmation. Runs are idempotent: a second run reports unchanged and leaves the shared files byte for byte, including whatever the project wrote outside the markers..clinerules/teksilo.md where that directory exists, a marker region where .clinerules is a plain file, which it may be, and which has nowhere to put a file of our own, and .cline/rules/teksilo.md where that is the only layout present..cline CLINE_CONFIG_DIR and .clinerules DEPRECATED_CONFIG_DIR, so the newer path looks like the obvious target; but the VS Code extension was hardcoded to .clinerules and ignored .cline/rules/ outright (cline/cline#14186), the cross-surface fix reached main only in September 2026 and is in no released build, and Cline’s own docs still say its Rules panel creates new workspace rules in .clinerules/. Preferring the modern name would install, in the most-used Cline surface, a file nothing reads, which this tool holds to be worse than installing nothing, because it reports success. Reachability beats recency, and a test says so, so that a later tidy-up does not quietly invert it.AGENTS.md and copilot-instructions.md belong to the project, and reading them with read_to_string(..).unwrap_or_default() turned “cannot decode” into “the file is empty”, one Latin-1 byte was enough for setup -y to replace a project’s rules with nothing but its own region, silently, reporting success. It now stops and names the file. And its stray-pruning compares filenames case-insensitively: the skill is written through fs::write but audited through read_dir, and on APFS or NTFS those disagree about case, so an exact comparison classified a file it had just written as a leftover and deleted it, leaving a skill with no SKILL.md while reporting updated. Folding can only err towards keeping a stale file, which is recoverable; deleting a live one is not.~/.claude/skills/), Mistral Vibe (~/.vibe/AGENTS.md, or $VIBE_HOME) and opencode (~/.config/opencode/AGENTS.md, or $XDG_CONFIG_HOME), each gated on its directory already existing, so a home directory gains no config directory for a tool that was never run. Both environment overrides are honoured: Vibe relocates its entire state directory through VIBE_HOME, so writing the default path for a user who moved theirs would leave a file nothing reads. What was not found is reported with the path that was checked, not with a label that would send a relocated install looking in the wrong place. The rest have nowhere to go, which is likewise reported rather than silently narrowed: Cursor’s user rules are edited in its settings UI, Copilot’s personal instructions live on github.com, Windsurf’s global rules are one file at ~/.codeium/windsurf/memories/global_rules.md, and a repository-root AGENTS.md is per-repository by definition.cargo teksilo status reports what is actually installed, every agent, in both the project and the user scope, plus the search model, the Python 3 interpreter symbol needs, and where each sits on disk. It reads only, and its exit code never depends on what it finds.setup, not a second opinion: every agent row comes from setup::inspect, the read-only twin of the function that decides whether a write is needed, and the two share their content computation. A test pins the equivalence for every form, inspect reports current exactly when setup would report unchanged, because a status that computed “installed” its own way would eventually disagree with the command it claims to predict, and the disagreement would surface as a user following advice that does nothing.here / not here / n/a) and a reason beside each, because three words cannot carry the difference between Cursor is not used in this project and Cursor is used here and has no brief, and that difference is the whole of what to do next. An install from an older release reads here, since it is being read right now, with “from another release” in the detail, calling it anything else would be false. And n/a always says why: a bare one beside Windsurf would read as “Windsurf has nothing”, when in fact it keeps a global file this tool declines to write.setup --status: it reports on both scopes while setup is scope-selected, so the flag would have to mean something the unflagged command does not. Keeping the read-only thing out of a writing command’s flag space also leaves no --status -y to reason about.--locked, because plain cargo metadata resolves, it creates a missing Cargo.lock and rewrites a stale one, which a command whose headline claim is that it touches nothing must not do. A project without an up-to-date lockfile therefore gets an honest “unknown” rather than a lockfile it never asked for. Paths reaching the report from $VIBE_HOME, $XDG_CONFIG_HOME or the working directory have their control characters escaped, so a newline in one cannot split a row and let its tail pose as another agent’s line.Cargo.lock, and a minor or major mismatch refuses instead of answering, serving 0.12 answers to an app on 0.9 is worse than serving nothing, because SplitView was deleted outright in favour of Splitter between them and the wrong answer reads exactly like the right one. A patch-level difference warns and proceeds.cargo-teksilo is the first crate in this workspace without version parity across the framework’s history: it did not exist before 0.13.0, and no tag before that carries a crates/cargo-teksilo directory. cargo-teksilo-fmt is the contrast, it has been published at every teksilo version since 0.9.0, which is exactly why a --version <app> line can be emitted there unconditionally and cannot be emitted here. So there are three regimes. Below 0.13.0 no install command exists at all, and the refusal says so and says to move the app forward; printing routes that cannot work is worse than printing none, because the model spends its turn on them and then falls back on its own memory of the API anyway, which is the single failure this tool exists to prevent. At 0.13.0 or above both routes print unconditionally, --version <v> --locked from the registry, or cargo install --path <checkout>/crates/cargo-teksilo --locked from the framework tree, because whether a given version reached crates.io is not observable offline, and an app pinning teksilo by path or git resolved a version that was never published at all. An unparseable version gets the checkout routes, which are the ones that do not depend on the registry. Where routes print, the refusal also names the two ways to keep apps on different minors working at once, installing the second with --root <dir> and putting that <dir>/bin first on PATH for that tree, or running the tool straight out of a checkout with cargo run -p cargo-teksilo.teksilo-corpus, the guides and the worked examples, chunked into a retrieval index and published per release so cargo resolves the corpus matching an app’s Teksilo. It is one file: a chunk carries its own text, and its path names the original in this repository (docs/scroll-area.md, examples/simple_button/src/main.rs), so a search result cites something that opens. Data only; it carries no ML dependency. A guide’s closing navigation footer, “See also”, “Reference”, “Code references”, is carried under its own kind and excluded from retrieval rather than from the corpus: show reassembles a document from its chunks, so dropping one truncates the file, while indexing one lets a short list of links outrank the prose it points at.cargo teksilo probe. Python, stdlib only, embedded in the binary rather than published to an index, so it is version-matched by construction, lands in the repository where an agent reading that repository can see it, and needs no pip or virtualenv. It supplies the JSON-RPC client every probe previously hand-rolled, a tool surface generated from TOOL_CATALOG (so it cannot drift from the bridge), and the virtualized-view navigation rules that are otherwise rediscovered one misdiagnosis at a time: an off-screen row has no AT node, a row scrolled back into view is a new widget with a new id, and clicking a row’s reported bounds below the viewport hits empty chrome. Three worked examples ship with it and run against this repository’s own example apps in CI.DockingLayout, which had only a generated catalog page and a design note written for reviewers rather than for users. Those gaps were found by running retrieval tests over the corpus, not by reading the table of contents. The design note is gone; its unstarted work is now Horizontal activity rail (backlog), which the retrieval index leaves out, a proposal written in the instruction voice reads as shipped API to whatever retrieves it, and this one outranked the guide’s own activity-rail section.tools/extract_widget_api.py now covers every crate with a public API (30, up from 4), including the types a crate declares in its own lib.rs. mdBook catalog generation stays scoped to the four cataloged crates, so docs/ is unchanged; the rest are queryable through --crate, --list, --all and by name.cargo-teksilo ships a README, so a crates.io reader learns what the semantic default feature pulls in, that --no-default-features is a fully working tool, where the model cache lives, and that symbol needs python3, none of which was readable outside this repository.Changed
Breaking changes
OverlayDismissCallback is Rc<dyn Fn(DismissReason, &mut EventContext)>, where it was Rc<dyn Fn()>. A closure that wants neither gains two ignored parameters, Rc::new(move |_, _| …). The new #[non_exhaustive] teksilo_core::overlay::DismissReason names the route that closed the overlay: Escape, OutsidePress, PointerLeave, Cascade or Programmatic. Both OverlayRequest::on_dismiss and ModalRequest::on_dismiss carry the new type. The five OverlayManager dismissal methods that do not name a reason, dismiss, dismiss_top, dismiss_all, dismiss_except and dismiss_with_focus_restore, keep the signatures they had and report Programmatic; their dismiss_because, dismiss_top_because, dismiss_all_because, dismiss_except_because and dismiss_with_focus_restore_because twins take the reason instead.OverlayManager no longer runs its on_dismiss. Code reaching through WidgetTree::overlay_manager_mut() to dismiss calls WidgetTree::dismiss_overlay instead. The ordering is unchanged, the callback still runs during dismissal, before focus returns to the trigger.MessageBoxResult::dismissed_by_escape is replaced by dismissal. MessageBoxDismissal is #[non_exhaustive] and names the route, Button, Escape, ClickOutside or Programmatic, where the boolean could only say Escape or not, while its own rustdoc claimed to cover a click outside that no code path could produce. result.dismissed_by_escape becomes result.dismissal == MessageBoxDismissal::Escape; where the flag was read as “the user did not choose a button”, read result.was_dismissed(). button is unchanged, and still carries the escape-button resolution on every route.Fixed
Dialog that has a custom trigger. The trigger published a named Role::Button node that advertised no actions at all and answered none, so assistive technology could see the control and not press it, while a mouse worked. This reached the ordinary Dialog::new(label), not only an explicitly hand-built trigger, and the same shape affected Snackbar and a PopoverWidget over a custom trigger. The node now advertises Action::Click and acts on it.ModalCloseBehavior. Escape did nothing to it on macOS and Windows, including in the default configuration, where Dialog asks for EscapeOrClickOutside and the presentation resolves to a real window. The click-outside half stays unavailable there, deliberately: the OS blocks the parent window while the modal is up, so there is no outside to click, and ClickOutside now means the same as Manual for that presentation. Linux and the BSDs were never affected, they present modals in-tree, where Escape already worked.MessageBox dismissed with Escape or a press outside reports its answer. The dialog closed and the application was told nothing, on_result ran only when a button was pressed, so a “Save changes?” prompt could be waved away and leave the caller with no idea whether to save. This is the in-tree presentation, which is what ModalPresentation::Auto resolves to on Linux and the BSDs, every unix but macOS, and so was the default behaviour there; ModalPresentation::InTree reaches it on any platform. Both routes now resolve the escape button and report it, with MessageBoxResult::dismissal naming which one closed the dialog, and a button that has already answered is not reported over. Distinct from the native-window fix above, which made Escape close such a window; this makes the closing report.InputDialog dismissed without a button press reports the cancellation. None is this dialog’s cancellation payload, so a dismissal that reported nothing was indistinguishable from a dialog still sitting open , against the type’s own contract, which promises the callback runs exactly once when the user accepts or cancels. Escape, a press outside and an application-driven dismissal now all deliver None. The in-tree presentation only: a natively presented InputDialog, macOS and Windows, still reports nothing when Escape closes it, and MessageBox reported correctly on that presentation already.CommandPalette runs its on_dismiss when the palette is dismissed. The hook fired only when a command was actually invoked, so a caller that installed it to release whatever opening the palette reserved was never told about the two commonest ways of closing the palette. CommandPalette presents in-tree on every platform, so every platform was affected. It now runs on every route, and once only.Version 0.12.1 2026-09-18
Version 0.12.0 2026-09-17
teksilo-scene becomes an editor. A selection can be moved, resized and rotated, with a pointer, from the keyboard and through a screen reader, where a heavyweight item could not be dragged with a pointer at all. SceneCard is the container a note lives in, and a card can hand its height to its own words. One picker now answers every “what did the pointer hit?”, over one geometry descriptor that also brings Qt’s four selection modes and a lasso. A pan over fifty thousand lightweight items costs what a pan over an empty scene costs, and the cards nobody can see cost no accessibility node and no Tab stop. Around that: a third paint band and a surface for ink that is still wet, every position the OS batched, a seam a data layer can reverse a scene edit through, and a finger that can pan a SceneView with selection or magnetism switched on.PathItem::new takes one argument, SceneItem loses two methods and gains one, Path::commands is a method, item_change_signal carries an envelope, and several types the crate hands to consumer code are now #[non_exhaustive]. Each one is listed under Breaking changes below with what to do about it.Added
Moving, resizing and rotating a selection
SceneView::transform_controller(TransformConfig): a frame around the selection, eight resize handles, a rotate handle and a body drag. Knobs for aspect lock, centred scaling, rotation snaps and their tolerance, a minimum size, handle and padding sizes, edge auto-pan distance and speed, and a chrome closure for painting the whole thing yourself. on_start / on_change / on_end report the gesture; LivePreview decides whether the content moves under the frame or only the frame does.Scene::apply_transform_delta. One gesture is one step to reverse, Esc cancels with nothing to roll back, and a sample costs a relayout rather than one model write per selected item.t by default) enters the controller, Tab cycles the handles, the arrows move or resize by a step, Esc cancels.SceneView::transform_session_signal() publishes the live gesture, for an app-owned inspector, status bar or size readout.Increment and Decrement move the coordinates that handle actually drives, and each two-dimensional handle also advertises a named action per direction (TransformStep), so a width can be changed without a height. TransformLabels takes tr! like every other label.Alt+arrow nudge and the controller all go through one door.SceneView::transform_enabled_signal() and magnetism_enabled_signal() for a toolbar to bind. Turning either off takes its chrome off the screen and out of the published accessibility tree.SceneCard, and a card that sizes itself to its contentSceneCard, the container a heavyweight item usually wants: a surface, a header that is the grab handle, a selection ring, three CardModes (Idle / Selected / Editing) and one named Role::Group, with no opinion about what is inside it. Dragging the title strip moves the card; dragging the prose inside it selects text; a trailing header slot holds buttons that can be clicked, even with the jitter a real click carries, without starting the drag. Slots take a widget, a boxed widget or an id, and surface(..) replaces the chrome entirely.SizePolicy on a heavyweight entry, Fixed (today’s behaviour and the default), HeightForWidth (the width is authored, the height follows the words) and Intrinsic (the entry shrink-wraps its widget). Set it with Scene::set_size_policy / SceneModel::set_size_policy, or with SceneCard::height_for_width(). Only the cards a pass lays out are measured, so an off-screen card keeps the estimate it was created with and a pan still costs the viewport. Scene::set_measured_size is there for an app measuring something the framework cannot.SceneView answers ScrollIntoView. A caret moving inside an embedded editor pans the camera to follow it, with no app wiring, and an outer ScrollArea is re-targeted to where the card will land. It honours reduced motion, which the public SceneView::ensure_visible cannot.Switcher that swaps the editor in on the next pass.One geometry, one picker
SceneItem::shape() -> ItemShape is how an item describes its geometry: a rectangle, a rounded rectangle, an ellipse or a path, with a fill rule and an optional stroke band in local or screen units. It replaces shape_contains and clone_shape_test, which had to be kept in agreement by hand.ItemSelectionModes, IntersectsItemShape (the default), ContainsItemShape, IntersectsItemBoundingRect and ContainsItemBoundingRect, chosen per view with SceneView::marquee_selection_mode, or per call on Scene::items_in_region and colliding_items_with.SceneRegion is a rectangle or a path, so a lasso is expressible: SceneRegion::lasso(path), ::stroke(path, width), ::from_screen_rect(..). A rotated marquee stops over-selecting.PaintKey, band, then z, then insertion order, is the order the tap, the drag, the hover, the cursor, the tooltip, the hold and Scene::item_at all read. claims_press and hit_testable are public for a consumer running its own pass.PathItem carries a real fill_rule, used by painting and hit-testing alike, and hit_stroke_width for a hairline that should still be clickable.Path: flatten(tolerance) (a polyline per subpath), exact_bounds(tolerance) (a tight box, where bounds() returns the control-point hull and so over-reports every curve), contains_point(p, rule, tolerance), the Subpath type, and arc_to_cubics shared rather than duplicated inside the path atlas.Ink
SceneLayer::Interleaved orders a lightweight item against the heavyweight cards by z, so a dried stroke can sit above note A and below note B, which two bands could not express. The pointer still resolves through the lightweight hit snapshot, the item keeps its own accessibility node, and the Tab ring does not move.WetLayer, a surface for content being authored right now, installed with SceneView::wet_layer(..). It repaints on its own without re-running the band beneath it, and always sits above every card and interleaved item and below the Over band. WetLayer::request_repaint(ctx) from a pointer handler; WetNode names the nodes it owns.EventContext::coalesced(), the positions the OS batched into the packet being dispatched, oldest first, each with its own timestamp and its own pressure and tilt. A drawing surface fans out over it and then over pointer_position(). Opt the producer in with TeksiloAppBuilder::pen_batching(PenBatching::Coalesce), which emits one dispatch per pen drain instead of one per packet and never folds a transition. Not on_drag, the drag recogniser swallows every move inside its slop and reports the press position, so the start of a stroke never arrives there.A seam a data layer can reverse a scene edit through
undo(). What it ships is a record complete enough to invert.ChangeSource (User / Programmatic / Remote), a HistoryMode (Record / RecordPreserveRedo / Ignore) and an ephemeral flag. The framework stamps its own gesture commits User, so an app can tell a finished drag from a programmatic move.SceneModel::transaction / user_edit; nesting joins rather than splits. SceneTransaction::abandon tags a cancelled interaction, and squash (off by default) folds a transaction’s repeated writes to one quantity into its endpoints.Scene::take / Scene::restore, an owning salvage door. take hands back the item box, its magnets (ids included) and its whole slice of the logical accessibility tree; restore puts it back at the same ItemId and the same place in the reading order, which is what selection, magnets, relations and every app side-map are keyed on. Scene::remove is the same call with the salvage routed to the edit sink, and RemovedItem::detach() forgets the recorded parent so a salvage restores at root level, or into a different scene.SceneModel::set_edit_sink receives one owning SceneTransactionRecord per committed transaction, with the scene unborrowed so the sink may read and write it; the sink’s own writes are journaled in turn. transaction_signal fires once per transaction, after the sink.Scene::replace_item swaps a lightweight item’s box while keeping the entry, the id and everything keyed on it. Placement { parent, z, local_pos, transform } is written as one property with set_placement, plus reparent_keeping_scene_pos for a drag into a group that must not move the item, and z_between, which reports when f32 precision has run out at a locus.ItemChange::is_edit() separates the scene’s edits from the derived notifications emitted beside them, so an app counting the changes in an edit gets the number the transaction record has.Scene::a11y_live_of / a11y_landmark_of, and a11y_relations / a11y_live_of / a11y_landmark_of / a11y_categories_of on SceneModel.Saying what a change should become before it is applied
SceneModel, one closure that rewrites a gesture’s proposed geometry before anything is applied, so snap-to-grid, axis lock and page clamping reach the drag ghost instead of correcting it a frame late. Install it with SceneModel::set_geometry_constraint; it is handed a ProposedChange (the scene read-only, the roots the gesture moves, and the gesture’s start and proposed frames in scene coordinates) and returns a ChangeVerdict, Accept, Adjust(frame) or Reject.Alt+arrow nudge, for pointer, keyboard and assistive technology alike. An app driving its own drag calls SceneModel::constrain_move / constrain_frame. Programmatic mutators are never constrained.SceneModel::downgrade() and WeakSceneModel, a non-owning handle. A constraint normally needs no handle at all (ProposedChange::scene is the whole read surface); this is for a policy object that holds one for its other work, and it is what keeps such a closure from leaking the scene that owns it.Core, platform and canvas
Widget::accepts_child_hit(child, point), a parent can veto one child for one point, which is the per-point question hit_transparent (a per-node declaration) cannot ask.EventContext::dispatch_target(), the arena’s own answer to who the press landed on, so a container need not guess it from a rectangle.EventContext::release_cursor(), withdraws a handler’s cursor override so the node-declared one resolves again.PanClaim and also installs on_drag now competes as both: the pan wins at pan_slop, and a hold arms the node’s own drag. LongPressRole::DragHandle declares that the hold inside a subtree belongs to that drag, and applies to a direct pointer only, a mouse enrols no pan member at all, so nothing about it changes.PenPacket::device_time_ms plus a shared back_date rule that places a drained batch on the tree’s timeline, wrapped 32-bit counters and missing clocks included. Windows now drains GetPointerPenInfoHistory, so a digitizer running above the message rate delivers every sample with its own pressure and tilt rather than one per message.AccessNodeBuilder::set_custom_actions advertises Action::CustomAction itself, so a node’s custom actions are reachable rather than decorative.StrokeStyle, dashed, dotted, or a custom cap or join, reaches the screen from every canvas stroke primitive, not only stroke_path.Accessibility
SceneMinimap is operable. Given an on_click it is focusable, publishes a Role::Group node, advertises Click and the four scroll actions, pans with the arrow keys and recentres on Home / Enter / Space. access_readout supplies the phrasing for where the viewport sits, as a MinimapReadout, so it can be localised.Demos
cargo run -p scene-ink, every pen sample through ctx.coalesced(), a WetLayer that repaints alone, and a dried stroke interleaved between two notes.cargo run -p scene-corkboard, beats are SceneCards with real editors: drag the title strip to move one, drag its prose to select text, pan a beat off-screen and keep typing (the camera follows the caret), and “Fit to text” hands every beat’s height to its words. Both panes carry a transform controller, and one “Snap to grid” toggle drives a single geometry constraint shared by both.Changed
Breaking changes
PathItem::new(path) takes one argument and derives its own bounds. Drop the local_bounds you were passing; if you were widening it to make a hairline clickable, use hit_stroke_width instead.ItemFlags::NEGATIVE_Z_BEHIND_PARENT is removed. It was declared and documented but read by nothing, and PaintKey is flat, so it could not have been honoured without making the key hierarchical. Nothing observable changes; delete the reference.PointerSequence::has_deferred_grab() is now has_deferred_grab_for(id). A deferred grab is a statement about the node it arms, not about the press: read sequence-wide it suppressed every descendant’s touch long-press and context menu. Pass the node you are asking about.SceneItem::shape_contains and clone_shape_test are gone, replaced by shape() -> ItemShape. An item that overrode either overrides shape() now; the default returns the item’s local_bounds, which is what an item that overrode neither already got.SceneItem::set_fill / set_stroke return AppearanceWrite<T> instead of bool, carrying the value they overwrote, the only place a journal can get it from. AppearanceWrite::Refused is the default, and is what an item with no such slot should keep returning.SceneItemA11yContext publishes scene coordinates. screen_bounds, local_to_screen, advertised_bounds and bounds_space are replaced by scene_bounds and local_to_scene; the camera is declared once as an AccessKit transform above the whole subtree. An item emitting its own sub-element geometry stops projecting through the view transform and publishes in scene space. A11yBoundsSpace is removed with them.item_change_signal carries a SceneChange, the change plus its transaction envelope, rather than a bare ItemChange. Observers read notification.change.ItemChange is no longer Copy or PartialEq (it carries owned values now) and gains PlacementChanged, ItemReplaced, HandlersChanged, MeasuredSizeChanged and SizePolicyChanged. TransformChanged, PayloadChanged and AppearanceChanged carry both sides of the value they replaced.Path::commands is a method. Read with commands(), append with push, build from a vector with Path::from_commands. The field backs a rolling content stamp that a public Vec could not be kept in step with.PointerSample::coalesced is a Vec<CoalescedSample>, not a tuple, each batched position keeps its own axes, which the tuple dropped.PenPacket::time: EventTime is replaced by device_time_ms: Option<u32>, the device’s own counter rather than a stamp with an unknown epoch, and ToolEvent::Frame carries time_ms. A backend reports what the device said; back_date places the batch on the tree’s timeline.#[non_exhaustive] on the types the crate hands to consumer code, so a new field is not a source break next time: ItemChange, SceneLayer, MagnetVisualState, MagnetRef, MagnetConnection, MagnetSnap, MagnetMarker, MagnetFeedback, SceneItemPaintContext and SceneItemA11yContext. A match gains a _ arm; a struct literal becomes the type’s new constructor, which every one of them now has, with its fields still public.WidgetPlacement gained a dormant field and is now #[non_exhaustive]. A place_children implementation reads and writes the slice it is handed and is unaffected. What breaks is a struct literal, WidgetPlacement { id, origin, size }, which becomes WidgetPlacement::new(id, origin, size). The field arrives pre-set to the child’s current state, so a parent that ignores it changes nothing; it is read only for a parent whose culls_children returns true.SceneCard has one method per slot. header_id, header_boxed, header_trailing_id, header_trailing_boxed, body_id and body_boxed are gone; header, header_trailing and body each take a widget, a Box<dyn Widget> or a WidgetId. Drop the suffix, .body_id(id) becomes .body(id). This follows the slot-twin purge the widget catalog went through in 0.11.0, so a card addresses its slots the way every other container does, and last call wins where before an id set after a widget left both stored.Performance
crates/teksilo-scene/tests/pan_scaling_probe.rs.) A pan over off-screen heavyweight cards is cheaper than it was but is not flat: the cards are parked rather than laid out, and what remains is the framework’s own walk over their arena nodes. (crates/teksilo-scene/tests/heavyweight_retention_probe.rs prints both.)crates/teksilo-scene/tests/heavyweight_retention_probe.rs.)Scene::set_local_pos walks a kept adjacency list instead of rebuilding a scene-wide parent map on every move. (crates/teksilo-scene/tests/mutation_scaling_probe.rs.)Scene::selection_roots is linear rather than quadratic in the number of selected items, and the transform controller resolves it once per hover instead of thirteen times per pointer move. With the whole of a 5 000-item scene selected, one selection_roots call went from 177 ms to a figure that no longer grows with the selection, and a drag sample in a scene with no constraint installed no longer pays for the selection at all. (crates/teksilo-scene/tests/selection_roots_scaling_probe.rs, transform_scaling_probe.rs print the tables.)crates/teksilo-render/tests/wet_stroke_cost.rs.)Scene
item_change_signal observer may read and write the scene. Changes are queued and drained after the mutation’s borrow drops, the teksilo-data mutate-then-notify discipline, so the snap-to-grid pattern the docs describe is implementable at last. An observer that panics mid-flush costs that observer’s delivery and nothing else. A CascadeBudget bounds one drain’s observer-generated deliveries (100 000 by default, flat, a knob on SceneModel::set_cascade_budget), and its diagnostic names the subject that piled up.SceneListAdapter keeps each row’s ItemId. A row whose content changed, and every row an insert or a removal shifted, keeps the id it had, so selection, magnets and accessibility parenting survive a data change instead of being retired with it. Each source change is one transaction.ItemChange::VisibilityChanged is emitted by every door that flips IS_VISIBLE, not only set_flag, and is a derived notification rather than an edit. Hiding a card is one recorded edit and two notifications whichever door it went through; it used to be two edits through one and one through the other.Scene::set_z ignores a write only when the entry already holds that exact value. Its old f32::EPSILON test never fired at 1e6 and swallowed millions of distinct floats near zero, including the midpoints z_between hands out, so a caller was told there was room and then got a silent no-op.apply_transform_delta that also moves the item now emits TransformChanged as well as LocalPosChanged; set_transform ignores a write that changes nothing.ItemChange::HandlersChanged carries the handler sets it replaced. handlers_mut cannot know what the caller does with the &mut it hands out, so it announces without recording.Adjust with a frame that cannot be applied, non-finite, or a negative extent, is refused rather than stored, and a debug build panics naming the frame. A NaN frame used to be kept verbatim, which removed the item from hit-testing, from the marquee and from every spatial query for good, with nothing raised to say so.SceneModel::constrain_move / constrain_frame called inside an open write scope panic naming the rule instead of reporting RefCell already mutably borrowed.Over item occludes a card only if it would act on a press. A decorative halo over an embedded note takes nothing and focus stays in the note; a hover affordance is not a press claim, and neither is accepting a drop.commit_marquee runs through Scene::items_in_region, which answers in scene coordinates, and a pinned item does not live there, it holds a fixed place in the viewport, so the scene rectangle a rubber band describes says nothing about whether the pointer crossed it. Dragging a band across the page therefore leaves a pinned badge alone where it used to select it. Selecting one deliberately still works by click. There is no screen-space region query yet; the deferral is recorded at the query’s own site.SceneView::retention_margin(px) sets how far outside the viewport a heavyweight card stays mounted, the band that keeps a fling from mounting and unmounting cards under the finger. Default 96 screen px; read it back with current_retention_margin.Platform
PlatformWindow::reconfigure_surface answers bool rather than (): false means the display server is gone and the caller should wind down instead of asking for another frame, which would return to the same place.FrameOutcome gains DisplayLost, distinct from Error and NeedsReconfigure because it is terminal: reconfiguring or redrawing after it spins the loop.Fixed
Scene
SceneView with selection or magnetism switched on can be panned with a finger. The view’s own drag was enrolled in a way that was never resolved, so its recogniser latched at the mouse slop and the pan was never evaluated.on_tap, panning a list with a finger used to tap a row on the way past.PointerCancel arm at all, so a cancelled touch or a lost capture left the item wherever the last sample put it.SceneView clears what leaving it should clear. Its PointerLeave arm had never run, so an item stayed hovered for ever, the tooltip stayed armed, and the cursor stayed on Grab with the pointer outside the view.ItemFlags do what their documentation says. An item without IS_VISIBLE is no longer clickable or draggable, and one without IS_ENABLED no longer eats a click.z ties were resolved to the bottom-most item, exactly inverting paint, and the scene’s two pickers were wired to opposite halves of the dispatch pipeline, so a tap beat every card and a drag lost to every card.SceneSelection, reaches the published accessibility tree. Both panes kept describing the previous selection, at its previous position.Increment on a “Resize top” or “Resize bottom” slider reported the action handled, announced an unchanged number and moved nothing.Esc cancels a transform the pointer started, not only one begun from the keyboard.on_end fires with TransformOutcome::Cancelled and the edge auto-pan stops. The pan used to keep tweening for up to thirty seconds with no input, and the hook an app tears its overlay down on never fired.TransformConfig::min_size floors only the axes the dragged handle drives. Dragging the bottom edge of a 2 × 100 hairline used to widen it to 4.The minimap
Rendering and canvas
stroke_rect, stroke_rounded_rect, stroke_circle, stroke_ellipse and draw_line all accepted a StrokeStyle and dropped it, along with line_cap and line_join. A styled stroke now routes to the tier that carries the pattern, and a solid one keeps its cheaper tier.MoveTo at the bitmap origin, drawing a long spoke from there to the shape, visibly painted, and a click inside it hit nothing. The atlas opens each implicit subpath where the flattener opens it.Path::transformed and Path::flatten agree about where a subpath begins. They did not for an arc following a Close, so rotating a two-loop lasso changed what it selected.Input
PointerSample::coalesced reaches a handler. It was populated and then discarded, with no accessor and two constructors writing it out empty, so a tool written the obvious way drew at frame rate on a 200–360 Hz digitizer.Platform
Surface::configure failed inside wgpu’s default error handler, which panics. What the user saw was wgpu error: Validation Error / In Surface::configure / Surface does not support the adapter's queue family, and what that reads like is a GPU mismatch. It is not one: request_adapter filters candidates on the very query that fails there, so the adapter had answered yes to the same question moments earlier. The message cost a real investigation, and a bug report aimed at the renderer for a compositor crash. Every Surface::configure now runs inside an error scope, so the failure is returned rather than thrown. A surface with no formats left for the adapter it was matched against is reported as a lost display server, in those words; anything else keeps wgpu’s own message, so a genuine mistake is not relabelled as a dead compositor.ExitFailure(errno), and run treated every error alike. A lost display connection now exits cleanly with one line naming the cause; NotSupported, Os and RecreationAttempt stay fatal, since those are genuine startup faults where a backtrace is the useful answer.wl_seat::get_touch without the touch capability is a missing_capability error, and the client is the one at fault. Mutter (GNOME 50) does not answer with that error, it serves the request out of a MetaWaylandTouch whose list head is still all-zero, because the wl_list is initialised only when a touch device appears, and writes through it. gnome-shell takes SIGSEGV, and since the compositor is the session, every window on the desktop dies with it, a second or two after the app opened one. KWin is unaffected, which is what made it read as a compositor bug rather than ours. Both objects are now bound from the wl_seat::capabilities event instead, and released when a capability is withdrawn, the press serials start_drag needs arrive on the same terms as before, since a seat that can start a drag is by definition one that announced the capability. (This is the cause of the lost display server the two entries above learned to survive.)Known limitations
accesskit_consumer resolves a hit by walking a node’s children in reverse, and the scene emits its children in band order, so the heavyweight tier wins an explore-by-touch probe whatever band the overlay is in and whatever it claims. The press-claiming rule below fixes the pointer answer, not this one. Closing it means reordering an AccessKit child list, which is also the reading order, an accessibility-tree decision rather than a hit-test one. Documented in docs/teksilo-scene-a11y.md and pinned by what_the_press_claiming_rule_does_and_does_not_reconcile, so it cannot drift silently.Version 0.11.0 2026-09-17
teksu! DSL reaches the code applications are actually made of: a call to your own fn row(..) -> impl Widget is a child, where before only a stock widget was. Alongside it the container API loses its twin methods, so a slot has one name and that name takes a WidgetId or a widget.Added
teksu
VStack { my_row(x) } and VStack { row(1).spacing(4.0) } compile. A lowercase identifier standing alone is still the argument-free property.self.row(x), Self::header(), crate::ui::header(), super::row().#{ expr } carries a widget value as well as a WidgetId, which is how a Rust struct literal is passed at body position: #{ Card { title: t } }.Widgets
on_change on Checkbox, Toggle, RadioButton and Slider, taking the value the activation produced and an EventContext, so flipping one can send an intent, set the theme or open a window, things a bare Signal write cannot do, because an observer receives only &T. They fire for the pointer, for Space, for an assistive-technology Click, and, for a checkbox inside a data-view row, for Space on that row. They do not fire for programmatic writes to the bound signal: there is no event in flight to carry, and the signal remains the source of truth. A tristate checkbox reports a bool too, since activation cycles Checked ↔ Unchecked only. RadioButton reports only a real change, so re-activating the selected button is silent. Slider reports every value a drag produces, but not a write that changes nothing (a drag past the end, a snap onto the grid point already held); it has no commit-on-release callback, so once-per-interaction work still belongs on the signal.StandardListItem::on_checkbox_toggle and the StandardTreeItem forwarder, which make the embedded checkbox’s on_change reachable, the canonical row builds its own checkbox, so it was the one place the callback could not be installed. To read check state, CheckedModel remains the answer: it is the source of truth and it survives row recycling, which a per-row callback does not.child_opt on every container that has child, 38 of them, up from 7. A bare teksu! if now works inside a single-child wrapper, not only inside a stack.for loop needs: tabs, static_tabs, panes, items, actions, lines, rails, docks, radios, full_width_rows, add_children and others.Core
BuildContext::set_keyboard_toggle, and the fallback passed to EventContext::row_space_activate, are now Rc<dyn Fn(&mut EventContext)> rather than Rc<dyn Fn()>. Space on a data view’s focused row therefore runs with a context, which is what lets a row checkbox fire on_change on that path instead of only under the pointer.Box<W> implements Widget for any W: Widget + ?Sized, so a boxed widget goes wherever a widget goes and adds no arena node.HandlerSet::merge_under composes two handler sets, the later declaration winning.Documentation
teksu! reference leads with the real widget catalog: worked examples for ListView, TreeView, TableView, TabWidget, FormLayout, MenuList, Toolbar, Switcher and DockingLayout, and a section listing only what genuinely does not work, with the compiler’s own text.NOTICE file records what the theme presets derive from and on what terms: the WinUI theme-resource values under Microsoft’s MIT licence, the Material 3 baseline tokens from documentation Google publishes under CC-BY-4.0, the macOS preset’s mix of published and measured numbers, and every bundled font with its licence, its copyright line, and whether it is embedded by default.fluent theme preset from Project Fluent, the unrelated localization system behind Teksilo’s translations.roboto-LICENSE.txt carried a summary of Apache-2.0 – four of its nine clauses, paraphrased, with the redistribution conditions collapsed to one line – where section 4(a) requires a copy; it now carries the complete text. noto-LICENSE.txt named one project and one year for two faces; it now carries the copyright line each font’s own name table declares, notofonts/arabic 2022 and notofonts/hebrew 2024.Changed
cargo teksilo-fmt --check over crates and examples.FormLayout::lines takes impl IntoTeksiChild in both columns; the field column used to require impl Widget, so a loop holding ids could not use it.teksu-language-spec-v3.md is the design rationale and names teksu-macro-reference.md normative for behaviour; fourteen divergences from the implementation are corrected, and appendices A.2, A.3, A.4 and A.6 are marked superseded where they describe an API that has since been removed.Removed
StandardTreeItem::on_toggle / on_toggle_rc are renamed on_chevron_toggle / on_chevron_toggle_rc. They are the expand / collapse control, and the name became ambiguous the moment a row gained on_checkbox_toggle.add_child, child_id, child_boxed and every *_id slot twin, 80 methods. Call the slot by its own name instead: .child(id), .content(id), .header(id), .pane(id). Every widget-accepting slot method takes impl IntoTeksiChild, which is implemented for WidgetId and for every Widget. Not affected: shortcut_id, static_tab_with_id, the alternate constructors (from_id, new_id, around_id, custom_id, with_child_id), and Breadcrumb::item_id.WidgetBuilder::dim_when_inactive and dim_when_inactive_default. Wrap instead: DimWhenInactive::new().factor(f).child(w).Checkbox::labels_hidden(bool) is now Checkbox::labelled_externally(), the name and shape Toggle already used for the same thing: the control’s accessible name comes from an ancestor, so it renders no label of its own.FormLayout::line_ids no longer adds one row from two ids; it takes an iterator of (label_id, field_id) pairs and adds a row per pair. The single-row form is gone because line takes impl IntoTeksiChild and accepts a WidgetId directly.Fixed
Platform
incompatible_surface_backends: GL. Confirmed fixed on both Wayland and X11 with the Vulkan driver removed.WGPU_BACKEND, WGPU_POWER_PREF and the rest were silently ignored, leaving no way to move off a backend whose driver is the problem.Debug-printed wgpu struct.Rendering
Core
WidgetWithHandlers wrapped an already-wrapped widget, and only the outer handler set reached the node, so .on_tap(cb).clips_children_on(true) never fired. Handler sets at any depth now arrive.dim_when_inactive retargeted the rest of the chain at the wrapper, so VStack::new().dim_when_inactive(0.7).child(a).child(b) built DimWhenInactive > b and lost the stack and a. The method is gone; teksilo-teksu-guard fails the build if a WidgetBuilder method returns a foreign wrapper again.RichTextEditor keeps one full-viewport affordance host in the TextAffordance band for its selection handles, so two editors on one page, a prose column and the synopsis next to it, read as a submenu over its parent menu: the key tore down an affordance host and returned, and no editor in that window saw an ArrowLeft for as long as both were up. The count now considers OverlayBand::Standard overlays only, the same line OverlayBand::dismissed_by_outside_press already draws for presses. A submenu over a mounted affordance still closes on the back key.teksu
teksilo-fmt re-indented a multi-line expression child by the block’s own indent on every run, walking a multi-line argument list further right each time.Version 0.10.0 2026-09-15
WebViewHandle gained a required method and MemoryShared is non-exhaustive; docs/porting-widgets-to-the-pointer-model.md is the contract for a widget crossing over.Added
Touch, pen and density
zwp_tablet_v2, Windows through a WM_POINTER* window subclass; no winit upgrade required.TargetDensity, Compact, Comfortable and Touch. One knob resizes every interactive target, gap and padding in the application: 24 / 32 / 44 dp targets, 6 / 10 / 16 dp grab handles, spacing ×1.00 / 1.15 / 1.30. Compact is the default and is today’s layout unchanged. Set it with Theme::with_density or WidgetTree::set_input_density; the shipped Material 3, Fluent and macOS presets project their own dimensions onto the ladder rather than inheriting Teksilo’s.Widget::hit_outset widens a target for one pointer kind only, Widget::target_regions lets a self-painting widget say where its parts are, and a miss-only slop pass re-attributes a near miss for a coarse pointer. None of them moves a painted pixel.OverScroller or iOS’s bouncing simulation, chosen per host by default , with rubber-band overscroll available per surface. Nine scrollable surfaces adopt it from one implementation.RichTextEditor, CodeEditor, PlainTextEditor, LogView, TextInput, PasswordField, SearchField, SpinBox and the date/time family.LongPressRole overrides the choice per subtree.touch_action and pan_claim node declarations, the CSS touch-action model: a subtree can forbid panning or pinching over itself, and a scrollable declares which axes it claims. A press freezes the effective value, so nothing a widget does mid-gesture can change what that gesture was allowed to become.InputTokens::touch_enabled off, the platform translator drops touch input and the router installs no touch-only recognizers, a mouse-only fallback with no rebuild of the binary.TEKSILO_TRACE_INPUT=samples|gestures|all prints every sample, every arbitration decision and every cancellation, guarded so that a normal run formats nothing.examples/touch_playground, a live pointer inspector (identity, kind, primary flag, pressure, tilt, twist, contact patch, speed, the frozen touch action, the press and the capture), five arbitration scenarios each naming which contender won the press, a density toggle, an editable kinetic-constants panel, and a touch-text surface.examples/widget_catalog, and --density compact|comfortable|touch on the catalog itself.Automation
inject_pointer, inject_key and type_text take a command modifier beside ctrl, because a chord declared Ctrl+S resolves to ⌘S on macOS.Accessibility
Previewer
--export-docs renders at a chosen density: --density=compact,touch writes docs/widgets/img/<slug>-touch.png beside the canonical image, and a catalog page grows a ## Density section when one exists. Compact stays canonical and keeps every existing filename, so nothing committed moves.teksilo_preview::PreviewPass, the density a preview renders at, and the image-naming rule that follows from it.Documentation
docs/porting-widgets-to-the-pointer-model.md, the contract for moving a widget onto the pointer model, as numbered clauses, each stating the rule, how to comply, and what checks it.Changed
text-document 1.12.2 and text-typeset 1.11.1.Core
ScrollHandlingOptions::pan_devices.DragActivation on the node decides; Auto is the default and resolves per pointer kind.on_hover never fires for a touch contact, and neither do hover_within and the hovered signals. A contact does not hover, that is what a contact is, so an affordance revealed only on hover is unreachable with a finger. InputTokens::reveal promotes those affordances to always-visible at the Touch density, and the tree’s hold route reaches a tooltip regardless.advance_time ticks gesture recognizers. A test that advances the virtual clock now sees a long press ripen, a fling coast and a hold dispatch; the clock is one axis for animation, gestures and kinetics rather than three.WidgetEvent::PointerDown, PointerUp, PointerMove, PointerEnter and PointerLeave now carry pointer: PointerInfo, so a handler can tell a finger from a stylus from a mouse without reaching for the context. PointerEnter and PointerLeave become struct variants; a pattern that named them bare now needs { .. }.PointerMove also carries modifiers: Modifiers. A drag reads Shift and Ctrl from the move rather than from the press, so a modifier pressed mid-drag reaches the widget.PointerInfo::mouse at the epoch: WidgetEvent::pointer_move_with, pointer_enter, pointer_leave, beside the existing pointer_down, pointer_up, pointer_move.WidgetEvent::Scroll::position and PointerCancel::position are renamed window_position. Both stay in window-logical coordinates, the router routes by the first, and the kinetic tracker behind a pan follows the pointer rather than the widget, and they are the only positional fields a handler receives that are not localised to it. The frame is now in the name, so a reader that needs content coordinates is told to convert at every use.GestureEvent::PinchChanged carries deltas, and says so. scale is the factor since the previous sample and rotation the twist since the previous sample, in radians; fold each in rather than assigning it.WindowOps::begin_os_drag and ExternalDndGuard::begin_drag take the device’s PointerKind, and both gain set_drop_accepted. ExternalDragEvent gains a Cancelled variant.TouchSelection::on_long_press takes a pointer: PointerInfo. A hold is recognised by a timer, so the context it is dispatched under had no device to report; the holding contact’s identity is now an argument.WebViewHandle::set_input_passthrough is a new required trait method, for out-of-tree backends. WebViewEvent::EngineFocusChanged is new.MemoryShared is #[non_exhaustive] and gained four fields.InspectorState::toggle() is public. New: teksilo_widgets::drop_target::{band_depth, region_at_floored, region_rect_floored}, styles::recipe_menu_item_style::menu_item_height.Widgets
ScrollArea, the five data views and the three text surfaces, plus Terminal and SceneView, which claim their own axes.SplitButton chevron takes a direct pointer’s press up to the conformance floor, borrowing from the action half; a mouse’s boundary stays where it is painted.TableView header cell’s filter zone is the density’s target size, and the cell reports its three parts through Widget::target_regions.DropTarget’s edge zones, and a docking pane’s five, are floored per axis and capped at a third of the extent. Not pointer-kind-gated: the acting zone and the painted one must be one rectangle.ToolBox header or a RadioTile leaves it untinted rather than resting hovered.WebView in the default Native input mode answers a press itself and revokes the pointer’s interaction, declares no touch default over its rectangle, and opts out of hit widening; Transparent asks the engine to stop taking input. Its subview is mirrored at the intersection with every clipping ancestor, hidden when out of view, and stood down while an interactive overlay covers it. Engine focus moves the toolkit’s focus onto the frame.InspectorState::toggle() or a corner-grip hold, its pressed rows take the density’s target size, and it has a Pointers tab listing every pointer declaration in the application’s tree.--density, switches it live, and its canvas zoom works (pinch and Ctrl-wheel).Terminal
Fixed
Rendering
bounds × scale_factor and sized to ceil of that, so the quad sat at a fractional device position, and at a fractional display scale it was not even the same size as its atlas region. Sampled through the atlas’s linear filter, both errors smear. A 1 px hairline drawn this way peaked at 48 % coverage instead of 100 %, and a 16 dp dashed ring lost its gaps entirely and read as a grey haze. The path pipeline now snaps the quad out to whole device pixels and bakes the bitmap against that same origin, so one texel lands on one pixel, the guarantee QuadVertex::from_glyph_quad_transformed has always given glyphs, which is why text was sharp and icons were not. The rect now travels with the raster as one PathPlacement, so the two can no longer be derived apart. Snapping is skipped under a transform, where the mask is being resampled anyway and rounding would make a translating path step between pixels instead of gliding; a gradient’s geometry is re-based onto the snapped quad, so it lands in the same place either way.Core
max_zoom. A spread to twice the starting span now leaves a SceneView at exactly twice, whatever the sample rate; before, each sample carried the ratio to the start of the gesture and the handler multiplied every one of them in, so a single spread compounded to the product of its intermediate ratios.SceneView by one radian, about 57°, because winit reports degrees and the payload is read as radians. The conversion now happens where the incoming unit is known.GestureEvent::PinchChanged and PinchPhase::Changed now document scale and rotation: both are deltas against the previous sample, and rotation is in radians. Fold each sample in (zoom *= scale, rotation += rotation) rather than assigning it.TouchPinchRecognizer::scale and rotation are renamed cumulative_scale and cumulative_rotation. They report the totals since the gesture started, which is not what a PinchChanged carries; the names now say which of the two a caller is reading.PointerCancel for a touch contact reported the mouse, with no position, so an application branching on the cancelled pointer’s kind behaved wrongly.Switcher, TabWidget, SceneView, DockingLayout, Repeater and PopoverWidget with a handler attached.InputTokens::min_target_conformance, a PinnedDp::ClearsFloor axis excused the very failures the raised floor exists to report. TargetMeasurement/TargetViolation carry the measured floor as conformance_floor, and every consumer reads it instead of re-deriving one.Platform
wgpu::Limits::default(), which demands eight colour attachments. A Raspberry Pi 4’s V3D driver allows four, so device creation was refused and the application panicked before its first window existed. Nothing in the renderer wanted that headroom: every render pass has one colour attachment, it binds no storage buffers, its widest uniform binding is 8 KiB and its widest shader carries ten inter-stage variables. A window now asks for downlevel_defaults with the texture-dimension limits lifted to the adapter’s own, which is the set the offscreen test renderer already opens with, so a frame that renders in a test renders in a window. An adapter sitting below even that floor gets one retry with its own reported limits, which cannot be refused on limit grounds.Widgets
ToolBox header, RadioTile, and the calendar’s month/year cell, the last only ever writing false into its own state.inf tall, its y resolved to NaN, and a rectangle containing NaN contains no point.TableStyle, CalendarStyle and SearchFieldStyle now hand the dimension over, and a custom style keeps the ladder it had unless it says otherwise. Int UI at the default density is unchanged.Terminal
Previewer
Text
--all-features builds. Five fonts-* features named a Noto face that is not in the repository, and because include_bytes! resolves at compile time, enabling one was a hard build error, so those five features, both fonts-all meta-features, and any --all-features build of the workspace had never compiled on any revision. A face is now embedded only if its file is present; enabling a feature without it warns and names the path to drop it at, rather than failing the build.RichTextEditor, CodeEditor, PlainTextEditor, TextInput, PasswordField, SearchField, SpinBox and the date/time family; the assistive-technology route onto the same handle reports it too.PlainTextEditor could neither replace nor suppress the context menu its documentation offered it, and the CodeEditor family gained the right-click menu it never had.--all-features builds. Five fonts-* features named a Noto face that is not in the repository, and because include_bytes! resolves at compile time, enabling one was a hard build error, so those five features, both fonts-all meta-features, and any --all-features build of the workspace had never compiled on any revision. A face is now embedded only if its file is present; enabling a feature without it warns and names the path to drop it at, rather than failing the build.Documentation
ScrollArea in the Thin scroll-bar mode documented a keyboard route it does not have. The bar’s arrow / Home / End / Page handlers sit on a node that cannot take focus, under every mode, the same limit the ScrollBarPolicy::AlwaysOff documentation states from the other side.PointerInfo::primary’s documentation described the wrong one of three similarly-named things. The field is the W3C per-kind flag, a mouse and a first finger are both primary at once on a hybrid machine, and the rule it carried (“exactly one live pointer, a mouse always wins”) belongs to the pointer table’s own election.StandardListItem’s documentation claimed the row reads its recipe’s projected heights. It projects the module constants instead; those two recipe fields have no reader.teksu
teksu! bodies could not place gesture_dead_zone or keyboard_capture before a child; both failed with “no method named child”. A compile-time guard now keeps the DSL’s builder-method list complete.teksu! UI fixtures had never run, a glob naming a directory that does not exist, which trybuild reports as “no tests enabled yet” and then passes, and one of them was asserting a caret column rustc does not emit.Known limitations
DensityPolicy::FollowLastPointer has an ingress and no writer; what a stray tap should cost and when hysteresis commits are unanswered.ScrollableAxes::overscroll carries the value; nothing renders a stretch or a glow.ScrollArea has no keyboard scroll route. Assistive technology can scroll it and the data views bring their own key handling, but a keyboard user facing a scroll region whose content holds no focus has none, a pre-existing WCAG 2.1.1 gap, now written down.cargo check --workspace --all-features does not build on any revision, including before this release: five of the seven optional fallback font faces are named by the build and were never committed.
Release data updated 2026-10-05. https://github.com/ferntech-eu/teksilo/releases