<?xml version="1.0" encoding="UTF-8"?>

<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Teksilo releases</title>
  <id>https://teksilo.rs/atom.xml</id>
  <link rel="self" type="application/atom+xml" href="https://teksilo.rs/atom.xml" />
  <link rel="alternate" href="https://teksilo.rs/news/" />
  <updated>2026-10-05T00:00:00Z</updated>
  <author><name>FernTech</name></author>
  
  
  <entry>
    <title>Teksilo 0.15.1</title>
    <id>https://github.com/FernTech-EU/teksilo/releases/tag/v0.15.1</id>
    <link href="https://github.com/FernTech-EU/teksilo/releases/tag/v0.15.1" />
    <published>2026-10-04T00:00:00Z</published>
    <updated>2026-10-04T00:00:00Z</updated>
    <content type="html">&lt;h4 id=&quot;added&quot;&gt;Added&lt;/h4&gt;
&lt;h5 id=&quot;widgets&quot;&gt;Widgets&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Esperanto (&lt;code&gt;eo&lt;/code&gt;) translations for all 313 framework messages, available
through &lt;code&gt;teksilo_widgets::framework_locales()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NotificationArchiveModel::push_update&lt;/code&gt;, which archives an in-place update
of a notice still on screen, beside &lt;code&gt;push&lt;/code&gt;, which archives a notice raised.
&lt;code&gt;UPDATE_HISTORY_LIMIT&lt;/code&gt; is how many update records a row keeps.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;changed&quot;&gt;Changed&lt;/h4&gt;
&lt;h5 id=&quot;widgets-1&quot;&gt;Widgets&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NotificationArchiveModel::push&lt;/code&gt; with the &lt;code&gt;dedup_id&lt;/code&gt; 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 &lt;code&gt;push_update&lt;/code&gt; instead, which
updates the row where it is.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;fixed&quot;&gt;Fixed&lt;/h4&gt;
&lt;h5 id=&quot;core&quot;&gt;Core&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A drag preview’s resources outlived the drag.&lt;/strong&gt; When a drag ended, the
preview widget was removed from the tree without the teardown every other
removal goes through, so whatever it had registered (animations, animated
indicators, bindings, subscriptions, shortcuts) stayed registered until
the window closed. The preview is now torn down like any other widget.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A widget changed while out of view could come back showing its old
content.&lt;/strong&gt; A widget marked for repaint while it was clipped out of a
scroll area or under a fully transparent ancestor (or itself fully
transparent) lost the mark without repainting, and if it came back into
view without moving it replayed the paint it had before the change. Such
a widget now drops its stale paint and repaints when it is next visible;
a widget that did not change keeps its cached paint as before.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An accordion opened in a window without focus stayed closed.&lt;/strong&gt; The
animation scheduler paused every animation of a window that was unfocused
or occluded, one-shot tweens included, so a &lt;code&gt;Collapse&lt;/code&gt; 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;render&quot;&gt;Render&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Clipping was misplaced at fractional scales and lost around blurs.&lt;/strong&gt; A
clip inside a translated scope (a panned scene, a moved transform
wrapper) landed at the wrong place whenever the display scale was not 1,
because the translation was scaled twice. Content drawn after a blur
inside a clipped region (a scroll area, for instance) was not clipped at
all, since the blur restarted drawing without restoring the clip. Both
now clip where the content is, and a clip opened outside a blur no
longer reaches the blur’s own offscreen drawing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blur used the wrong kernel width.&lt;/strong&gt; Every pass of every blur in a frame
read the kernel offset written for the frame’s last blur pass, because
the per-pass parameters were rewritten into one buffer that the GPU reads
only once the whole frame runs. A blur therefore came out softer or
sharper than its radius asked for, and changed when another blur of a
different size appeared on screen. Each pass now reads its own
parameters.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;widgets-2&quot;&gt;Widgets&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;An image or raster icon could show another image’s pixels.&lt;/strong&gt;
&lt;code&gt;ImageWidget::new&lt;/code&gt; and &lt;code&gt;IconWidget::from_raster&lt;/code&gt; / &lt;code&gt;from_animated&lt;/code&gt; 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 &lt;code&gt;IconWidget&lt;/code&gt; shown in both
&lt;code&gt;Tintable&lt;/code&gt; and &lt;code&gt;FullColor&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The notification log kept the first notice’s actions for a notice
updated in place.&lt;/strong&gt; An archived row merged from a later notice with the
same id took only its title and body, so a progress notice offering Cancel
that became a result offering Open now and See report stayed a Cancel row,
under its first severity, and the log never offered the report. The row
now takes the latest notice’s actions, replay names included, its
severity, priority and audience; an update offering no actions leaves the
row with none, as it leaves the live notice. The row keeps its id, its
place in the log and the time the notice was raised, and its group and
source unless the update names them. Updating a row nobody had read no
longer adds to the unread count, which grew by one per update and stayed
up under a bell scoped to a window or an audience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A notice raised again was filed under the first one.&lt;/strong&gt; A toast raised
with the id of an archived notice that had already left the screen merged
into that row like an update of a live toast: the row stayed where it
was, dated when the first notice was raised, below every notice raised
since. An import run on Friday under the same id as Monday’s showed its
result in the log under Monday. The row now comes back to the top, unread,
dated when the notice came back, with a record of the occurrence even when
it says the same as before.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A progress notice grew its archive row without bound.&lt;/strong&gt; Every in-place
update appended a record to the row, written to the archive file, so a
notice reporting each step of a long import gathered thousands, and an id
reused for every run of an operation gathered them from every run. An
update that repeats the row exactly now records nothing, and a row keeps
only its 20 most recent records.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Full Changelog&lt;/strong&gt;: https://github.com/FernTech-EU/teksilo/compare/v0.15.0…v0.15.1&lt;/p&gt;
</content>
  </entry>
  
  
  
  <entry>
    <title>Teksilo 0.15.0</title>
    <id>https://github.com/FernTech-EU/teksilo/releases/tag/v0.15.0</id>
    <link href="https://github.com/FernTech-EU/teksilo/releases/tag/v0.15.0" />
    <published>2026-10-04T00:00:00Z</published>
    <updated>2026-10-04T00:00:00Z</updated>
    <content type="html">&lt;h4 id=&quot;added&quot;&gt;Added&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ChartSource&lt;/code&gt; and direct &lt;code&gt;ChartWindow&lt;/code&gt; support in line, bar and pie charts
and their legends, borrowing visible points and tracking window changes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cargo teksilo search --path PREFIX&lt;/code&gt; to restrict documentation search before
lexical and semantic results are combined.&lt;/li&gt;
&lt;li&gt;The application guide in the bundled documentation search index.&lt;/li&gt;
&lt;li&gt;A runnable live dashboard example with a worker event source, bounded history
and tests of visible bindings.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;changed&quot;&gt;Changed&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Chart constructors (&lt;code&gt;LineChart::new&lt;/code&gt;, &lt;code&gt;BarChart::new&lt;/code&gt;, &lt;code&gt;PieChart::new&lt;/code&gt; and
&lt;code&gt;ChartLegend::new&lt;/code&gt;) now accept &lt;code&gt;impl Into&amp;lt;ChartSource&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; instead of
&lt;code&gt;ChartModel&amp;lt;T&amp;gt;&lt;/code&gt;. Explicit &lt;code&gt;ChartModel&lt;/code&gt; arguments continue to work. Calls that
rely on the constructor to infer the argument type may need an explicit type:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-3&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-3&quot;&gt; Before&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;LineChart&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;::&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;String&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;Default&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;default&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-3&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-3&quot;&gt; After&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;LineChart&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;::&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;String&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;ChartModel&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;::&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;String&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;default&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;fixed&quot;&gt;Fixed&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Resolve public unsafe traits by name in &lt;code&gt;cargo teksilo symbol&lt;/code&gt;, including
cross-crate lookup when the module name differs from the trait name.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Limit named-type heading boosts in documentation search so results relevant
to the full query can outrank basic examples that only name the type.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Accept the application guide’s source path in corpus validation while retaining
checks for relative paths and existing source files.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Include public trait contracts, associated types and constants, and required
and default methods in &lt;code&gt;cargo teksilo symbol&lt;/code&gt; output.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Restored the generated gesture arbitration table and refreshed the search
corpus after documentation edits changed its no-winner markers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Removed an unnecessary borrow flagged by Clippy on Rust 1.99.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Restored crates.io sources and checksums in the release lockfile so clean
checkouts can resolve dependencies with &lt;code&gt;--locked&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Full Changelog&lt;/strong&gt;: https://github.com/FernTech-EU/teksilo/compare/v0.14.3…v0.15.0&lt;/p&gt;
</content>
  </entry>
  
  
  
  <entry>
    <title>Teksilo 0.14.3</title>
    <id>https://github.com/FernTech-EU/teksilo/releases/tag/v0.14.3</id>
    <link href="https://github.com/FernTech-EU/teksilo/releases/tag/v0.14.3" />
    <published>2026-10-03T00:00:00Z</published>
    <updated>2026-10-03T00:00:00Z</updated>
    <content type="html">&lt;h4 id=&quot;changed&quot;&gt;Changed&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Updated the &lt;code&gt;text-document&lt;/code&gt; dependency to 1.12.5.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Breaking: cargo-teksilo CLI.&lt;/strong&gt; Removed &lt;code&gt;setup&lt;/code&gt;, bare &lt;code&gt;probe&lt;/code&gt;, and &lt;code&gt;version&lt;/code&gt;.
Use &lt;code&gt;init&lt;/code&gt;, &lt;code&gt;probe install&lt;/code&gt;, and &lt;code&gt;--version&lt;/code&gt; (tool version) or &lt;code&gt;status&lt;/code&gt;
(resolved app version). User installation is now
&lt;code&gt;agent install &amp;lt;agents...&amp;gt; --user&lt;/code&gt;, which installs instructions only.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Explicit agent selection.&lt;/strong&gt; &lt;code&gt;agent install &amp;lt;agents...&amp;gt;&lt;/code&gt; and repeated
&lt;code&gt;init --agent&lt;/code&gt; options create the selected agents’ configuration even when
no detection marker exists. Without explicit targets, &lt;code&gt;init&lt;/code&gt; uses detection.
&lt;code&gt;agent list&lt;/code&gt; reports supported targets and installation state.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Explicit model download.&lt;/strong&gt; &lt;code&gt;model fetch&lt;/code&gt; replaces automatic downloads and
the &lt;code&gt;--no-model&lt;/code&gt; flag. Initialization never downloads weights; search loads
complete local model files or falls back to BM25.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Concise CLI output.&lt;/strong&gt; Short summaries and actionable errors replace lengthy
explanations. &lt;code&gt;--verbose&lt;/code&gt; adds diagnostics; &lt;code&gt;--quiet&lt;/code&gt; suppresses informational
messages without truncating API or document contents. Search, status, and
agent listings support &lt;code&gt;--json&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Updated command examples, migration documentation, the repository skill,
and the embedded skill and agent instructions installed by cargo-teksilo.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Expanded onboarding and shortened user guides.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Moved engineering records out of the published docs and search corpus.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Removed obsolete chapters and corrected documentation claims.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Added README synchronization and documentation checks.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;fixed&quot;&gt;Fixed&lt;/h4&gt;
&lt;h5 id=&quot;cargo-teksilo&quot;&gt;cargo-teksilo&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Resolve the selected app’s dependency graph instead of selecting another
workspace member’s Teksilo version. Reject ambiguous framework dependencies.&lt;/li&gt;
&lt;li&gt;Store probe provenance in workspace metadata for virtual workspaces, avoiding
an invalid, incomplete &lt;code&gt;[package]&lt;/code&gt; table.&lt;/li&gt;
&lt;li&gt;Preserve conflicting probe files on first installation, even without a
checksum manifest. Replacing modified generated instructions or harness
files requires &lt;code&gt;--force&lt;/code&gt;; shared-file text outside managed regions is retained.&lt;/li&gt;
&lt;li&gt;Allow symbol lookup in applications depending only on non-widget crates,
including the extractor’s cross-crate lookup without &lt;code&gt;teksilo-widgets&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Refresh the bundled documentation corpus and its semantic embeddings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Full Changelog&lt;/strong&gt;: https://github.com/FernTech-EU/teksilo/compare/v0.14.2…v0.14.3&lt;/p&gt;
</content>
  </entry>
  
  
  
  <entry>
    <title>Teksilo 0.14.2</title>
    <id>https://github.com/FernTech-EU/teksilo/releases/tag/v0.14.2</id>
    <link href="https://github.com/FernTech-EU/teksilo/releases/tag/v0.14.2" />
    <published>2026-09-28T00:00:00Z</published>
    <updated>2026-09-28T00:00:00Z</updated>
    <content type="html">&lt;h4 id=&quot;changed&quot;&gt;Changed&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;text-document&lt;/code&gt; 1.12.4.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;fixed&quot;&gt;Fixed&lt;/h4&gt;
&lt;h5 id=&quot;widgets&quot;&gt;Widgets&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dragging a rich text selection that held a table dropped it a character
early.&lt;/strong&gt; A selection from inside a paragraph to the start of the paragraph
after a table holds the table whole and leaves the two paragraphs apart, so
its removal takes one position fewer than the selection spans. The editor
moved the drop point back by the whole span, and the passage landed one
character before it: inside the word there, or in front of a paragraph’s
full stop, which was left in a paragraph of its own after the table. The
drop point is now held by a cursor of its own, which the document moves
across the removal, and the move is one edit, so one undo takes it back
where it took two.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dragging a selection of table cells moved a copy of the table and left
the emptied grid behind.&lt;/strong&gt; A selection from one cell to another copies as a
table of those cells, but its removal only empties them. Dropped back into
the editor it came from, such a drag now does nothing: no caret shows a
landing place while it is over that editor, and the cells stay selected.
The editor has no modifier that makes a drag a copy. Dropped into another
editor, the cells are copied as before.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shift and an arrow over a selection holding a whole table turned it into
a range of the table’s cells.&lt;/strong&gt; The text before and after the table left
the selection at the first press. Shift with an arrow now moves the
selection’s end as text, so the table stays whole or is given back, also
when the table’s last cell is empty.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shift with Up, Page Up or Home could not give back a table the selection
had taken.&lt;/strong&gt; A selection that runs into a table holds it whole, its moving
end at the table’s far edge. A step back into the table with Shift and Up,
Down, Page Up, Page Down or Home landed on that same edge again, so the
selection could only grow. The step now gives the table back and takes the
end to the table’s other side, as Shift with Left or Right does since
text-document 1.12.4.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ctrl+A in a table cell selected the cell, then the table, and showed
neither.&lt;/strong&gt; The second and third presses make cell selections, which the
editor never handed to the paint, so both looked like the first press, which
selects the cell’s paragraph. Cell selections, from Ctrl+A, from Shift and an
arrow at a cell’s edge, or from a drag from one cell to another, are now
painted cell by cell, and a selection running across tables paints the cells
of every one of them whole instead of drawing each row’s highlight on over
the next column. A table inside a quotation is still painted as before: the
typesetter draws the cells of the main text’s tables only.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shift and an arrow over a rectangle of a large table’s cells took a
moment per press.&lt;/strong&gt; Each press looked every selected cell up among all of the
table’s cells: once Ctrl+A had selected a whole table, a press took about 80
milliseconds over a table of a thousand cells and two seconds over one of
five thousand. The editor now reads the table’s size once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tab, Shift+Tab and Enter did nothing in a table inside a quotation.&lt;/strong&gt; The
editor looked the table up among the main text’s own paragraphs and tables,
where a table in a quotation is not. It now reads into quotations however
deeply they nest, so Tab and Shift+Tab move between the cells, Tab in the
last cell adds a row and enters it, and Enter moves down a column or, on the
last row, to the paragraph that follows the table, in the quotation or
after it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An input method composing over a selection made from left to right left
its first candidate in the text.&lt;/strong&gt; The caret stood at the selection’s end,
and the composition’s range was measured from there although the candidate
replaced the selection at its start. The next candidate and the commit then
took away the wrong characters, or none: composing 你 over a selected word
left &lt;code&gt;n你&lt;/code&gt; behind. The range is now measured back from where the candidate
ends.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Full Changelog&lt;/strong&gt;: https://github.com/FernTech-EU/teksilo/compare/v0.14.1…v0.14.2&lt;/p&gt;
</content>
  </entry>
  
  
  
  <entry>
    <title>Teksilo 0.14.1</title>
    <id>https://github.com/FernTech-EU/teksilo/releases/tag/v0.14.1</id>
    <link href="https://github.com/FernTech-EU/teksilo/releases/tag/v0.14.1" />
    <published>2026-09-28T00:00:00Z</published>
    <updated>2026-09-28T00:00:00Z</updated>
    <content type="html">&lt;h4 id=&quot;changed&quot;&gt;Changed&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;text-document&lt;/code&gt; 1.12.3.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;accessibility&quot;&gt;Accessibility&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AccessKit 0.25.1.&lt;/strong&gt; &lt;code&gt;accesskit&lt;/code&gt; 0.25.1 and &lt;code&gt;accesskit_consumer&lt;/code&gt; 0.39.1,
and through &lt;code&gt;accesskit_winit&lt;/code&gt; 0.34.1 the platform adapters
&lt;code&gt;accesskit_windows&lt;/code&gt; 0.35.1, &lt;code&gt;accesskit_macos&lt;/code&gt; 0.27.1 and, on Linux,
&lt;code&gt;accesskit_unix&lt;/code&gt; 0.24.0 with &lt;code&gt;accesskit_atspi_common&lt;/code&gt; 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 (&lt;code&gt;Role::Image&lt;/code&gt;, and &lt;code&gt;Canvas&lt;/code&gt;, &lt;code&gt;GraphicsSymbol&lt;/code&gt;, &lt;code&gt;SvgRoot&lt;/code&gt;)
answers AT-SPI’s Image interface. The re-exported &lt;code&gt;teksilo::accesskit&lt;/code&gt; stays
on 0.25, so nothing changes for code that uses it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;fixed&quot;&gt;Fixed&lt;/h4&gt;
&lt;h5 id=&quot;widgets&quot;&gt;Widgets&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Holding Tab in a rich text quote or list nested it without end.&lt;/strong&gt; Each
press wrapped the block in one more blockquote, or moved the list item one
level deeper, for as long as the key repeated: about a hundred presses
built a quote that a Djot loader bounding its parser’s recursion refuses to
open again. Tab now stops at 64 blockquote levels and 16 list levels and
then does nothing, rather than typing a tab character over the text. The
ways back out (Shift+Tab, &lt;code&gt;outdent&lt;/code&gt;, &lt;code&gt;decrease_blockquote_depth&lt;/code&gt;, 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.
&lt;code&gt;indent&lt;/code&gt;, &lt;code&gt;increase_blockquote_depth&lt;/code&gt; and &lt;code&gt;toggle_blockquote&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tab or Shift+Tab on a list item inside a quote took the item out of its
list.&lt;/strong&gt; Moving an item to another level rebuilds its list, and up to
text-document 1.12.2 &lt;code&gt;create_list&lt;/code&gt; 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 &lt;code&gt;indent&lt;/code&gt; and
&lt;code&gt;outdent&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tab at the end of a list’s last item typed a tab character.&lt;/strong&gt; The editor
asked whether the caret was in a list by reading the block at its
character index, which at the end of an item’s text is the paragraph after
it. Tab there typed a tab into the item, or, where a quoted paragraph
followed a quoted list, nested that paragraph’s quote one level deeper for
every press. The check now reads the caret’s own block, as the list
commands do.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;increase_blockquote_depth&lt;/code&gt; quoted a paragraph that was not in a quote.&lt;/strong&gt;
It is documented as the command behind Tab in a quote and as doing nothing
outside one, and now does nothing outside one.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;automation&quot;&gt;Automation&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The curated dialogs probe reported a popover’s focus as lost.&lt;/strong&gt;
&lt;code&gt;example_dialogs.py&lt;/code&gt;, which &lt;code&gt;cargo teksilo probe&lt;/code&gt; 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;workspace&quot;&gt;Workspace&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Cargo.lock&lt;/code&gt; recorded text-document and text-typeset with no source or
checksum.&lt;/strong&gt; A developer’s local &lt;code&gt;[patch.crates-io]&lt;/code&gt; overlay strips both,
and 0.13.1 and 0.14.0 were tagged with such a lockfile: &lt;code&gt;cargo metadata --locked&lt;/code&gt; failed on a fresh clone, and &lt;code&gt;cargo audit&lt;/code&gt; skipped those eleven
crates. The lockfile resolves from crates.io again, a new CI job fails one
that does not, and &lt;code&gt;tools/relock-crates-io.sh&lt;/code&gt; 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Full Changelog&lt;/strong&gt;: https://github.com/FernTech-EU/teksilo/compare/v0.14.0…v0.14.1&lt;/p&gt;
</content>
  </entry>
  
  
  
  <entry>
    <title>Teksilo 0.14.0</title>
    <id>https://github.com/FernTech-EU/teksilo/releases/tag/v0.14.0</id>
    <link href="https://github.com/FernTech-EU/teksilo/releases/tag/v0.14.0" />
    <published>2026-09-26T00:00:00Z</published>
    <updated>2026-09-26T00:00:00Z</updated>
    <content type="html">&lt;h4 id=&quot;added&quot;&gt;Added&lt;/h4&gt;
&lt;h5 id=&quot;accessibility&quot;&gt;Accessibility&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;teksilo_platform::key_report&lt;/code&gt;: each key reported to the AT-SPI registry.&lt;/strong&gt;
&lt;code&gt;KeyReportGate&lt;/code&gt; reports every key press and release to
&lt;code&gt;org.a11y.atspi.DeviceEventController.NotifyListenersSync&lt;/code&gt; while an
assistive technology is attached, and says which keys the screen reader
took, so they are not dispatched. &lt;code&gt;teksilo-app&lt;/code&gt; runs every &lt;code&gt;KeyboardInput&lt;/code&gt;
through it; Linux only, it does nothing on Windows and macOS.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;tools/reader/&lt;/code&gt;: what a screen reader gets from an example, recorded.&lt;/strong&gt;
&lt;code&gt;tools/reader/reader.py&lt;/code&gt; runs an example in a private, invisible desktop
session (its own D-Bus, AT-SPI bus, KWin and runtime directory, no
&lt;code&gt;DISPLAY&lt;/code&gt;, 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; &lt;code&gt;tabwalk&lt;/code&gt; and
&lt;code&gt;tree&lt;/code&gt; need no scenario. Linux only. See
&lt;code&gt;docs/a11y/reader-harness.md&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;Widget::accessibility_proxy&lt;/code&gt;: a composite can publish itself through the
field that holds focus.&lt;/strong&gt; 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 &lt;code&gt;FormLayout&lt;/code&gt;’s
&lt;code&gt;labelled_by&lt;/code&gt;, &lt;code&gt;access_described_by&lt;/code&gt;, 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;accessibility::audit::focusable_nodes_hidden&lt;/code&gt;: a control that takes
focus inside a hidden subtree.&lt;/strong&gt; 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 &lt;code&gt;Action::Focus&lt;/code&gt; 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,
&lt;code&gt;audit::nodes_in_filtered_tree&lt;/code&gt; lists the nodes &lt;code&gt;accesskit_consumer&lt;/code&gt;’s
filter keeps, which are the ones a platform adapter walks and can
announce.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;RichTextEditor&lt;/code&gt;, &lt;code&gt;CodeEditor&lt;/code&gt;, &lt;code&gt;PlainTextEditor&lt;/code&gt; and &lt;code&gt;LogView&lt;/code&gt; take a
&lt;code&gt;.label(..)&lt;/code&gt;.&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;HandlerSet::access_customize&lt;/code&gt;: a widget can finish the accessibility
of a node it reaches by id.&lt;/strong&gt; The twin of
&lt;code&gt;WidgetWithHandlers::access_customize&lt;/code&gt;, for the nodes a widget writes to
through &lt;code&gt;BuildContext::apply_handlers&lt;/code&gt;, and chained after any customization
the node already carries.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;BuildContext::first_focus_capable_descendant&lt;/code&gt;: the control a wrapper
stands for, even while it is disabled.&lt;/strong&gt; Beside
&lt;code&gt;first_focusable_descendant&lt;/code&gt;, 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.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;internationalization&quot;&gt;Internationalization&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A formatted date can name its weekday, or stop at the month.&lt;/strong&gt;
&lt;code&gt;TeksiloDateTimeFormatter::date_fields(DateFields)&lt;/code&gt; picks which fields the
date part names: &lt;code&gt;YearMonthDay&lt;/code&gt;, the default and what every date style
rendered before; &lt;code&gt;YearMonthDayWeekday&lt;/code&gt;, CLDR’s full date at &lt;code&gt;DateStyle::Long&lt;/code&gt;
(“lundi 31 août 2026”, “Monday, August 31, 2026”); or &lt;code&gt;YearMonth&lt;/code&gt;, 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. &lt;code&gt;format_in_locale(value, &amp;amp;lang)&lt;/code&gt; renders one value once, as a plain &lt;code&gt;String&lt;/code&gt;, in a locale the caller
names, for a string computed on demand, such as an accessibility name.
&lt;code&gt;calendar_system(CalendarSystem::Gregorian)&lt;/code&gt; keeps the date on the Gregorian
calendar where CLDR gives the locale another one: by default &lt;code&gt;fa-IR&lt;/code&gt; writes
24 September 2026 as the 2nd of Mehr 1405, and &lt;code&gt;th-TH&lt;/code&gt; counts its year 2569.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;core&quot;&gt;Core&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;EventContext::context_menu_trigger&lt;/code&gt;: what asked for a context menu.&lt;/strong&gt; A
&lt;code&gt;context_menu&lt;/code&gt; factory is handed a point whatever opened the menu, and only
a pointer’s is one the user chose. Inside the factory,
&lt;code&gt;ctx.context_menu_trigger()&lt;/code&gt; now says &lt;code&gt;ContextMenuTrigger::Pointer&lt;/code&gt; (a
secondary click, or a hold), &lt;code&gt;Keyboard&lt;/code&gt; (the Menu key, Shift+F10, Ctrl+Shift+M
on macOS) or &lt;code&gt;Accessibility&lt;/code&gt; (an assistive technology’s &lt;code&gt;ShowContextMenu&lt;/code&gt;),
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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;changed&quot;&gt;Changed&lt;/h4&gt;
&lt;h5 id=&quot;data-views&quot;&gt;Data views&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A single-selection &lt;code&gt;TreeView&lt;/code&gt; over an index &lt;code&gt;SelectionModel&lt;/code&gt; keeps the
selected row selected through a structural change.&lt;/strong&gt; 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. &lt;strong&gt;Behaviour change.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;fixed&quot;&gt;Fixed&lt;/h4&gt;
&lt;h5 id=&quot;core-1&quot;&gt;Core&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tabbing away from a control whose tooltip was showing sent focus back to
it.&lt;/strong&gt; 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 &lt;code&gt;Snackbar&lt;/code&gt; that times out
no longer sends focus back to its trigger after the user has moved on
either.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Orca heard no key typed into a Teksilo window in a Wayland session.&lt;/strong&gt;
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 &lt;code&gt;PasswordField&lt;/code&gt;) 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;An empty text measured by text-typeset had no text run.&lt;/strong&gt;
&lt;code&gt;TextRunSource::from_geometry&lt;/code&gt; 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 &lt;code&gt;TextRunSource::flat&lt;/code&gt; always
did, so the first text such a node gains, and the edit that empties it, are
reported like any other change.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A composite that takes focus itself was announced by its structural
node.&lt;/strong&gt; Where a focusable widget hands its accessibility node to a
descendant (&lt;code&gt;Widget::accessibility_proxy&lt;/code&gt;), 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Focus that a rebuild took away came back in the wrong place.&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A screen reader could act on the page behind a modal.&lt;/strong&gt; While an in-tree
modal was up (a &lt;code&gt;MessageBox&lt;/code&gt;, a &lt;code&gt;Dialog&lt;/code&gt;, any &lt;code&gt;Centered&lt;/code&gt; 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 (&lt;code&gt;invoke_action&lt;/code&gt;, &lt;code&gt;focus_node&lt;/code&gt;,
&lt;code&gt;set_value&lt;/code&gt;, &lt;code&gt;expand&lt;/code&gt;, &lt;code&gt;collapse&lt;/code&gt;) with its unhandled-action error.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Every window was an unnamed “frame” to Orca.&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Orca heard only the first message the framework’s announcer said.&lt;/strong&gt; Every
&lt;code&gt;announce&lt;/code&gt; 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
(&lt;code&gt;teksilo_core::announcer::LINGER&lt;/code&gt;) 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 &lt;code&gt;TreeUpdate&lt;/code&gt; of an idle
tree has two nodes fewer.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A control a reader had met went silent on Orca once it left and came
back.&lt;/strong&gt; 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
&lt;code&gt;sync_accessibility&lt;/code&gt; returns, which the automation bridge and tests read,
are unchanged. An integration that feeds an AccessKit adapter itself hands
it &lt;code&gt;WidgetTree::deliver_accessibility&lt;/code&gt; and routes the adapter’s action
requests through &lt;code&gt;WidgetTree::resolve_adapter_action&lt;/code&gt;. Windows and macOS
keep no defunct state, and nothing changes there that a reader can tell.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;An announcement made as focus moved was cut before it was heard.&lt;/strong&gt; 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 &lt;code&gt;ListView&lt;/code&gt;,
&lt;code&gt;TreeView&lt;/code&gt;, &lt;code&gt;GridView&lt;/code&gt; 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The announcement ring recorded text no platform announces.&lt;/strong&gt;
&lt;code&gt;WidgetTree::announcements_since&lt;/code&gt;, and the automation bridge’s
&lt;code&gt;pull_announcements&lt;/code&gt; that reads it, recorded a live node’s &lt;code&gt;value&lt;/code&gt; before
its label, from every live node the walk emitted, whenever that text changed.
Every AccessKit adapter announces a node’s &lt;em&gt;name&lt;/em&gt; instead, which is its value
only for a &lt;code&gt;Role::Label&lt;/code&gt;, and only for a node in the filtered tree. So a
hidden live region, one inside a hidden subtree, and a &lt;code&gt;Status&lt;/code&gt; or &lt;code&gt;Alert&lt;/code&gt;
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 &lt;code&gt;sync_accessibility&lt;/code&gt;
returns, including the re-placed copy a scroll hands out, through
&lt;code&gt;accesskit_consumer&lt;/code&gt;, 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 &lt;code&gt;WidgetTree::new()&lt;/code&gt;, as every headless tree and every test is,
and the tree of a window &lt;code&gt;teksilo-app&lt;/code&gt; opens only when
&lt;code&gt;install_automation_bridge_in_debug()&lt;/code&gt; installed the bridge, which it does
in a debug build with the &lt;code&gt;automation&lt;/code&gt; feature. The new
&lt;code&gt;WidgetTree::set_records_announcements&lt;/code&gt; switches it. &lt;strong&gt;Behaviour change&lt;/strong&gt;
for anything that read the ring, and for anything that read it from a
window without the automation bridge, where it is now empty.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;announce_unless_widget_speaks&lt;/code&gt; kept quiet beside a live region nobody
could hear.&lt;/strong&gt; 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
&lt;code&gt;Status&lt;/code&gt; 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
&lt;code&gt;accesskit_consumer&lt;/code&gt;, 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;accessibility::announced_text&lt;/code&gt; and the label audits took a value for a
name.&lt;/strong&gt; For any role but &lt;code&gt;Role::Label&lt;/code&gt;, &lt;code&gt;announced_text&lt;/code&gt; fell back to the
node’s &lt;code&gt;value&lt;/code&gt; when it had no label, and &lt;code&gt;audit::duplicate_label_leaks&lt;/code&gt;
and &lt;code&gt;labels_without_text_ranges&lt;/code&gt; read names the same way, while no
platform adapter takes a value as the name of anything but a label. A
&lt;code&gt;Status&lt;/code&gt; 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 &lt;code&gt;Role::Label&lt;/code&gt;’s value and any other node’s label,
and nothing else. &lt;strong&gt;Behaviour change&lt;/strong&gt; for a test that found a node by a
value through &lt;code&gt;announced_text&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A merged name took the text from under a hidden descendant.&lt;/strong&gt;
&lt;code&gt;access_merge_subtree&lt;/code&gt; skipped a hidden descendant’s own name but went on
merging its children, so text an application hid with &lt;code&gt;access_hidden(true)&lt;/code&gt;
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 &lt;code&gt;AccessNodeBuilder::set_hidden&lt;/code&gt;, &lt;code&gt;clear_hidden&lt;/code&gt; and
&lt;code&gt;access_hidden&lt;/code&gt; 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
&lt;code&gt;Role::GenericContainer&lt;/code&gt; instead.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A field’s validation message was never read on arriving at the field.&lt;/strong&gt;
&lt;code&gt;access_described_by&lt;/code&gt;, and the four stock inputs that wire their
&lt;code&gt;ValidationStrip&lt;/code&gt; with it (&lt;code&gt;TextInput&lt;/code&gt;, &lt;code&gt;PasswordField&lt;/code&gt;, &lt;code&gt;DateTimeEdit&lt;/code&gt;,
&lt;code&gt;DateRangeEdit&lt;/code&gt;), 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 &lt;code&gt;described_by&lt;/code&gt; target’s text
into the node’s &lt;code&gt;description&lt;/code&gt;, 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. &lt;strong&gt;Behaviour change&lt;/strong&gt;: a
node carrying &lt;code&gt;described_by&lt;/code&gt; 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).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A throttled frame-tick subscriber held back every frame requested while it
was on screen.&lt;/strong&gt; A widget on &lt;code&gt;subscribe_frame_tick_throttled&lt;/code&gt; set the pace not
only of its own tick but of every &lt;code&gt;request_frame&lt;/code&gt;: 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.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;widgets&quot;&gt;Widgets&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A check box, a switch, a radio button or a slider changed without a
screen reader being told.&lt;/strong&gt; On Space, an arrow key, a click or a screen
reader’s own activation, &lt;code&gt;Checkbox&lt;/code&gt;, &lt;code&gt;Toggle&lt;/code&gt;, &lt;code&gt;RadioButton&lt;/code&gt; and &lt;code&gt;Slider&lt;/code&gt;
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
&lt;code&gt;Checkbox&lt;/code&gt; in a &lt;code&gt;ListView&lt;/code&gt; or &lt;code&gt;TreeView&lt;/code&gt; row checked with Space on the row.
A &lt;code&gt;Slider&lt;/code&gt; also gives the platform its figures as they are written: an
&lt;code&gt;f32&lt;/code&gt; 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 &lt;code&gt;f32&lt;/code&gt; sums come to.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A screen reader heard nothing as the highlight moved through a menu.&lt;/strong&gt; In
every &lt;code&gt;MenuList&lt;/code&gt;, 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”).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A submenu opened from the keyboard closed itself.&lt;/strong&gt; 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 &amp;gt; 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Text fields told a screen reader nothing of the caret, of an empty
field, or of a password.&lt;/strong&gt; In every field built on &lt;code&gt;TextInputField&lt;/code&gt;
(&lt;code&gt;TextInput&lt;/code&gt;, &lt;code&gt;SearchField&lt;/code&gt;, &lt;code&gt;SpinBox&lt;/code&gt;, &lt;code&gt;PasswordField&lt;/code&gt;, 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 &lt;code&gt;NoEcho&lt;/code&gt;
field exposes an empty text and reports nothing typed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A &lt;code&gt;PrivacySettings&lt;/code&gt; with telemetry configured crashed every debug build.&lt;/strong&gt;
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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A date field’s calendar wrote a date over the field that the user had not
chosen.&lt;/strong&gt; The calendar of a &lt;code&gt;DateEdit&lt;/code&gt;, a &lt;code&gt;DateTimeEdit&lt;/code&gt; or a
&lt;code&gt;DateRangeEdit&lt;/code&gt; 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 &lt;code&gt;DateRangeEdit&lt;/code&gt;, the start;
for a &lt;code&gt;DateEdit&lt;/code&gt;, the text typed in it counts), in the day view, and the
reader hears that day. A &lt;code&gt;DateRangeEdit&lt;/code&gt;’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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A calendar’s months and years views could not be used from the keyboard
or by a screen reader.&lt;/strong&gt; 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. &lt;strong&gt;Behaviour change&lt;/strong&gt;: a month cell is named with its year.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The fields of the date and time editors were read without a name, or
without their date.&lt;/strong&gt; The field focus lands on in a &lt;code&gt;DateEdit&lt;/code&gt; or a
&lt;code&gt;TimeEdit&lt;/code&gt; had no name, so Orca said “entry 05/02/2026”; it now carries the
editor’s &lt;code&gt;.label()&lt;/code&gt;, or “Date” and “Time”, and an &lt;code&gt;access_label&lt;/code&gt; or
&lt;code&gt;access_described_by&lt;/code&gt; given to the editor reaches it. A &lt;code&gt;DateTimeEdit&lt;/code&gt;’s
parts and a &lt;code&gt;DateRangeEdit&lt;/code&gt;’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. &lt;strong&gt;Behaviour change&lt;/strong&gt;: those parts are
&lt;code&gt;Role::TextInput&lt;/code&gt;, no longer &lt;code&gt;Role::DateInput&lt;/code&gt; or &lt;code&gt;Role::TimeInput&lt;/code&gt;; the
editor around them keeps its date role.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A custom overlay trigger could not be reached from the keyboard, or was
reached as an unnamed panel.&lt;/strong&gt; The widget handed to &lt;code&gt;Dialog::trigger&lt;/code&gt;,
&lt;code&gt;Snackbar::trigger&lt;/code&gt;, &lt;code&gt;Wizard::trigger&lt;/code&gt; or a &lt;code&gt;PopoverCustom&lt;/code&gt; 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 &lt;code&gt;Button&lt;/code&gt;, 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 &lt;code&gt;Snackbar&lt;/code&gt; had been
named with the snackbar’s message. &lt;strong&gt;Behaviour change&lt;/strong&gt;: a custom trigger
around a widget that takes no focus is now a Tab stop, including the filter
glyph of a filterable &lt;code&gt;TableView&lt;/code&gt; column.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Opening a popover with nothing to focus in it put the reader on a node
with no name and no role.&lt;/strong&gt; 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 &lt;code&gt;surface_name&lt;/code&gt; is now named
by its trigger, where before it had no name. A &lt;code&gt;bare()&lt;/code&gt; popover with
nothing to focus leaves focus on its trigger.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Focus in a &lt;code&gt;RichTextEditor&lt;/code&gt;, &lt;code&gt;CodeEditor&lt;/code&gt;, &lt;code&gt;PlainTextEditor&lt;/code&gt; or &lt;code&gt;LogView&lt;/code&gt;
reached no screen reader.&lt;/strong&gt; 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 &lt;code&gt;.access_label(..)&lt;/code&gt;, &lt;code&gt;.access_labelled_by(..)&lt;/code&gt;,
&lt;code&gt;.access_description(..)&lt;/code&gt; or tooltip attached to one of these widgets now
names or describes the text. The &lt;code&gt;rich-text-editor&lt;/code&gt;, &lt;code&gt;rich-text-viewer&lt;/code&gt;,
&lt;code&gt;code_editor&lt;/code&gt; and &lt;code&gt;log_view&lt;/code&gt; examples name their surfaces.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A keyboard user could not leave a &lt;code&gt;CodeEditor&lt;/code&gt; or a &lt;code&gt;PlainTextEditor&lt;/code&gt;.&lt;/strong&gt;
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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Arrowing through an open &lt;code&gt;ComboBox&lt;/code&gt; list was silent, and changed the value
at every step.&lt;/strong&gt; 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. &lt;code&gt;on_select&lt;/code&gt; therefore fires once, on that commit, and a
&lt;code&gt;LanguageSwitcher&lt;/code&gt; no longer switches the application’s language (nor a
&lt;code&gt;ThemeSwitcher&lt;/code&gt; 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. &lt;strong&gt;Behaviour change&lt;/strong&gt;: the arrows,
&lt;code&gt;Home&lt;/code&gt; / &lt;code&gt;End&lt;/code&gt;, the page keys and type-ahead no longer write the bound
&lt;code&gt;selected&lt;/code&gt; signal or fire &lt;code&gt;on_select&lt;/code&gt;; code that read the value after an
arrow press presses Enter first.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Every change to the toast stack read every toast again and moved
keyboard focus.&lt;/strong&gt; 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
&lt;code&gt;toast-demo&lt;/code&gt;’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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A toast expired under keyboard focus.&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A toast shown while another was up left with it.&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The notification bell and log were rebuilt on every notification.&lt;/strong&gt;
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”.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The chosen segment of a &lt;code&gt;SegmentedControl&lt;/code&gt; was read as “not selected”.&lt;/strong&gt;
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
&lt;code&gt;RadioButton&lt;/code&gt; 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. &lt;strong&gt;Behaviour
change&lt;/strong&gt;: a test or a probe that asserted a segment’s &lt;code&gt;selected&lt;/code&gt; asserts its
&lt;code&gt;toggled&lt;/code&gt; now.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A dialog’s content was hidden from assistive technology.&lt;/strong&gt; The panel
&lt;code&gt;RecipeDialogStyle&lt;/code&gt; draws around a &lt;code&gt;ModalContainer&lt;/code&gt;’s content called
&lt;code&gt;set_hidden()&lt;/code&gt; 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 (&lt;code&gt;getOnScreenObjects&lt;/code&gt; in its
&lt;code&gt;script_utilities.py&lt;/code&gt;), 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 &lt;code&gt;Role::GenericContainer&lt;/code&gt;, which the
adapters drop while keeping its children. Every &lt;code&gt;ModalContainer&lt;/code&gt; is
affected, on the default style and on the macOS and Fluent presets, which
reuse it. A custom &lt;code&gt;DialogStyle&lt;/code&gt; should do the same, as
&lt;code&gt;DialogStyle::make_panel&lt;/code&gt; now says. What that lets through is presented
once: a &lt;code&gt;ModalContainer&lt;/code&gt; whose content is itself a &lt;code&gt;Role::Dialog&lt;/code&gt; or
&lt;code&gt;Role::AlertDialog&lt;/code&gt;, as a &lt;code&gt;MessageBox&lt;/code&gt;’s and a &lt;code&gt;CommandPalette&lt;/code&gt;’s is, now
publishes a bare &lt;code&gt;Role::GenericContainer&lt;/code&gt; instead of a second dialog of the
same name, which Orca would speak as focus entered each; and the
&lt;code&gt;CommandPalette&lt;/code&gt;’s content is &lt;code&gt;Live::Off&lt;/code&gt;, so the polite palette is
announced by its name alone, and not row by row. &lt;strong&gt;Behaviour
change&lt;/strong&gt;: the container of a presented &lt;code&gt;MessageBox&lt;/code&gt; is no longer a
&lt;code&gt;Role::Dialog&lt;/code&gt; named by the title. A test or a probe that found the box’s
buttons under that node finds them under the box’s &lt;code&gt;Role::AlertDialog&lt;/code&gt;,
which holds them on this version and the last.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The content of a &lt;code&gt;Card&lt;/code&gt;, a &lt;code&gt;Panel&lt;/code&gt;, a &lt;code&gt;Toolbar&lt;/code&gt;, a &lt;code&gt;StatusBar&lt;/code&gt;, a
snackbar and a &lt;code&gt;RadioTile&lt;/code&gt; body was hidden from assistive technology.&lt;/strong&gt; The
frames &lt;code&gt;RecipeCardStyle&lt;/code&gt;, &lt;code&gt;RecipePanelStyle&lt;/code&gt;, &lt;code&gt;RecipeSnackbarStyle&lt;/code&gt; and
&lt;code&gt;RecipeRadioTileStyle&lt;/code&gt; draw, and a &lt;code&gt;Panel&lt;/code&gt; marked &lt;code&gt;a11y_presentational&lt;/code&gt;,
called &lt;code&gt;set_hidden()&lt;/code&gt; 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 &lt;code&gt;body&lt;/code&gt;.
Each is now a bare &lt;code&gt;Role::GenericContainer&lt;/code&gt;, and the snackbar’s frame is
also &lt;code&gt;Live::Off&lt;/code&gt;, 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 &lt;code&gt;make_body&lt;/code&gt; of &lt;code&gt;CardStyle&lt;/code&gt;,
&lt;code&gt;PanelStyle&lt;/code&gt;, &lt;code&gt;SnackbarStyle&lt;/code&gt; and &lt;code&gt;RadioTileStyle&lt;/code&gt; now says what a custom
frame has to do.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;An open menu listed none of its items to assistive technology.&lt;/strong&gt; The
panel &lt;code&gt;RecipePopoverStyle&lt;/code&gt; draws for its &lt;code&gt;Menu&lt;/code&gt; variant, under every
&lt;code&gt;MenuList&lt;/code&gt;, &lt;code&gt;ComboBox&lt;/code&gt; drop-down and search suggestion list, called
&lt;code&gt;set_hidden()&lt;/code&gt;. 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 &lt;code&gt;Role::GenericContainer&lt;/code&gt;, and
&lt;code&gt;PopoverStyle::make_body&lt;/code&gt; says what a custom one has to do.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Toasts were never announced and could not be reached.&lt;/strong&gt; The &lt;code&gt;ToastHost&lt;/code&gt;
every toast is mounted under called &lt;code&gt;set_hidden()&lt;/code&gt;, 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 &lt;code&gt;Role::Status&lt;/code&gt; or &lt;code&gt;Role::Alert&lt;/code&gt; 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 &lt;code&gt;Role::GenericContainer&lt;/code&gt;,
and the toast’s content is &lt;code&gt;Live::Off&lt;/code&gt;, 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. &lt;code&gt;ToastStyle::make_body&lt;/code&gt; says a
custom style has to do the same.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A &lt;code&gt;TabWidget&lt;/code&gt;’s panel, a &lt;code&gt;Stepper&lt;/code&gt;’s step and anything inside a
&lt;code&gt;Switcher&lt;/code&gt;, &lt;code&gt;MaxSize&lt;/code&gt; or &lt;code&gt;AspectRatio&lt;/code&gt; was hidden from assistive
technology.&lt;/strong&gt; The three layout primitives called &lt;code&gt;set_hidden()&lt;/code&gt; to publish
no node of their own, and a hidden node takes its whole subtree out of
every platform’s tree. &lt;code&gt;TabWidget&lt;/code&gt; and &lt;code&gt;Stepper&lt;/code&gt; show their pages through a
&lt;code&gt;Switcher&lt;/code&gt;, so the selected &lt;code&gt;Role::TabPanel&lt;/code&gt; and everything in it could not
be reached, and neither could a &lt;code&gt;Calendar&lt;/code&gt;’s day grid, the title bar’s
maximize button or the debug inspector’s panel, while a menu capped by
&lt;code&gt;max_visible_items&lt;/code&gt; lost its rows to the &lt;code&gt;MaxSize&lt;/code&gt; around them. A screen
reader met a control inside them only once focus reached it. All three are
now a bare &lt;code&gt;Role::GenericContainer&lt;/code&gt;, pruned from the tree, and the page a
&lt;code&gt;Switcher&lt;/code&gt; shows takes its place; its other pages stay out of the tree as
before, because they are dormant. &lt;strong&gt;Behaviour change&lt;/strong&gt;: 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 &lt;code&gt;sync_accessibility&lt;/code&gt; returns and in an
automation snapshot alike. The snapshot, which ignores the hidden flag,
held the same pages before, under the hidden node.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A &lt;code&gt;Snackbar&lt;/code&gt;’s button, the notification bell, a &lt;code&gt;TitleBar&lt;/code&gt;’s centre
content, an unlabelled &lt;code&gt;Splitter&lt;/code&gt; pane and every &lt;code&gt;TreeTableView&lt;/code&gt; cell were
hidden from assistive technology.&lt;/strong&gt; Each sits inside a shell that called
&lt;code&gt;set_hidden()&lt;/code&gt; to keep itself out of the tree: the &lt;code&gt;Snackbar&lt;/code&gt; around its
trigger, &lt;code&gt;NotificationCenterButton&lt;/code&gt; around its &lt;code&gt;IconButton&lt;/code&gt;, the title
bar’s drag region around &lt;code&gt;TitleBar::center&lt;/code&gt;, a splitter pane that has no
&lt;code&gt;pane_label&lt;/code&gt; (or a labelled one while collapsed, sliver and all), and the
row inside each &lt;code&gt;TreeTableView&lt;/code&gt; 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 &lt;code&gt;Role::GenericContainer&lt;/code&gt;. &lt;strong&gt;Behaviour change&lt;/strong&gt; for a
test that read the drag region’s hidden flag: it is no longer hidden, and
still no stop.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A &lt;code&gt;Banner&lt;/code&gt; was followed by its description, its action and its dismiss
button.&lt;/strong&gt; 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 &lt;code&gt;Live::Off&lt;/code&gt;: its title is what it announces, once, and the rest
stays reachable. A &lt;code&gt;StatusBar&lt;/code&gt; with &lt;code&gt;announce_changes(true)&lt;/code&gt; keeps the
inheritance, which is what it is for.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Opening a &lt;code&gt;MessageBox&lt;/code&gt; announced its default button, and not its
question.&lt;/strong&gt; 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 &lt;code&gt;Live::Off&lt;/code&gt;: the question, the box’s own
name, is what it announces, once, and its text and buttons stay reachable.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A focused &lt;code&gt;SpinBox&lt;/code&gt; reported a nameless text field.&lt;/strong&gt; 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:
&lt;code&gt;Role::SpinButton&lt;/code&gt;, the &lt;code&gt;label&lt;/code&gt; as its name, the value, range and step,
&lt;code&gt;Increment&lt;/code&gt; and &lt;code&gt;Decrement&lt;/code&gt;, 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 (&lt;code&gt;access_label&lt;/code&gt;, a &lt;code&gt;FormLayout&lt;/code&gt; label,
&lt;code&gt;access_described_by&lt;/code&gt;, a tooltip) reaches that node through the new
&lt;code&gt;Widget::accessibility_proxy&lt;/code&gt;. The accessible value is now exactly the text a
reader can review, so a painted &lt;code&gt;suffix&lt;/code&gt; is no longer in it.
&lt;strong&gt;Behaviour change&lt;/strong&gt;: a test that read the spin button off the &lt;code&gt;SpinBox&lt;/code&gt;’s
own id finds a &lt;code&gt;GenericContainer&lt;/code&gt; there; the spin button is the field,
&lt;code&gt;first_focusable_descendant&lt;/code&gt; of that id.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A &lt;code&gt;SpinBox&lt;/code&gt; with a custom &lt;code&gt;value_from_text&lt;/code&gt; could not be typed in words.&lt;/strong&gt;
The numeric input filter ran whatever the parser, so a month field that reads
&lt;code&gt;&quot;march&quot;&lt;/code&gt; 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A &lt;code&gt;SpinBox&lt;/code&gt; step threw away what had just been typed.&lt;/strong&gt; Up, Down, the
wheel, the step buttons and an assistive &lt;code&gt;Increment&lt;/code&gt; stepped from the value
last committed, so 35 typed over 10 in a field stepping by 5 became 15 on
&lt;code&gt;Up&lt;/code&gt;. A step now reads the typed text first, as &lt;code&gt;Enter&lt;/code&gt; would, and moves on
from it: 40, with one &lt;code&gt;on_value_changed&lt;/code&gt;. Text that cannot be read steps
nothing and the held value is shown again, as in Qt. &lt;code&gt;Enter&lt;/code&gt; 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. &lt;strong&gt;Behaviour change.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;After a live change of language, a &lt;code&gt;SpinBox&lt;/code&gt; wrote wrong values.&lt;/strong&gt; Switched
to French, every spin box showed &lt;code&gt;440,00&lt;/code&gt;, and then went on reading, stepping
and filtering in the language it was built in: focus turned the text back to
&lt;code&gt;440.00&lt;/code&gt;, a step said &lt;code&gt;-0.5&lt;/code&gt;, a grouped &lt;code&gt;1 234 567&lt;/code&gt; was read “1,234,567”,
which is a decimal to a French reader, and the comma typed into a decimal
field was dropped, so &lt;code&gt;12,5&lt;/code&gt; wrote 125. The field now shows, reads, steps,
commits and accepts the number in the language switched to: &lt;code&gt;12,5&lt;/code&gt; writes
12.5, shown and spoken &lt;code&gt;12,50&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A &lt;code&gt;SpinBox&lt;/code&gt; at its &lt;code&gt;special_value_text&lt;/code&gt; told a screen reader “0”.&lt;/strong&gt; 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 &lt;code&gt;specialValueText&lt;/code&gt; 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.”. &lt;strong&gt;Behaviour change&lt;/strong&gt;: a click into the field puts the caret in
the special text, not in the number; select it, or step, to replace it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The calendar spoke English and ISO dates to a user in any language.&lt;/strong&gt;
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
&lt;code&gt;Calendar&lt;/code&gt; 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. &lt;code&gt;DateEdit&lt;/code&gt;, &lt;code&gt;DateRangeEdit&lt;/code&gt; and
&lt;code&gt;DateTimeEdit&lt;/code&gt; open this calendar, so their popovers are fixed with it.
&lt;code&gt;calendar-name-with-month&lt;/code&gt; now takes the month with its year as its one
&lt;code&gt;$month&lt;/code&gt;, the unused &lt;code&gt;calendar-cell-name&lt;/code&gt; is gone, and
&lt;code&gt;calendar-value-with-selection&lt;/code&gt;, &lt;code&gt;calendar-value-on-selection&lt;/code&gt;,
&lt;code&gt;calendar-date-range&lt;/code&gt; and &lt;code&gt;calendar-decade&lt;/code&gt; are new, in all 23 locales.
&lt;strong&gt;Behaviour change&lt;/strong&gt; for anything that read the grid’s value as an ISO
date.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;DateEdit&lt;/code&gt;, &lt;code&gt;DateRangeEdit&lt;/code&gt; and &lt;code&gt;DateTimeEdit&lt;/code&gt; gave assistive technology an
ISO value.&lt;/strong&gt; 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. &lt;strong&gt;Behaviour change&lt;/strong&gt; for
anything that parsed those values.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A &lt;code&gt;Calendar&lt;/code&gt;’s days and header buttons could not be reached, and
arrowing through its days said nothing on Linux.&lt;/strong&gt; The day grid’s body and
the header row called &lt;code&gt;set_hidden()&lt;/code&gt; so as not to be announced beside the
&lt;code&gt;Role::Grid&lt;/code&gt;, 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 &lt;code&gt;Role::GenericContainer&lt;/code&gt;, 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
&lt;code&gt;Action::Click&lt;/code&gt; and, like a grouped &lt;code&gt;RadioTile&lt;/code&gt;, no longer
&lt;code&gt;Action::Focus&lt;/code&gt;: 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. &lt;code&gt;DateEdit&lt;/code&gt;, &lt;code&gt;DateRangeEdit&lt;/code&gt; and
&lt;code&gt;DateTimeEdit&lt;/code&gt; open this calendar and are fixed with it. &lt;strong&gt;Behaviour
change&lt;/strong&gt; for an automation client that focused a day, which is now
reported unhandled.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A calendar said things twice.&lt;/strong&gt; 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 &lt;code&gt;Live::Off&lt;/code&gt;, 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.
&lt;strong&gt;Behaviour change&lt;/strong&gt; for anything that listened to the grid’s live region.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The context menu opened from the keyboard acted somewhere else.&lt;/strong&gt; Shift+F10
or the Menu key in a &lt;code&gt;RichTextEditor&lt;/code&gt;, a &lt;code&gt;CodeEditor&lt;/code&gt; or a single-line field
(&lt;code&gt;TextInput&lt;/code&gt;, &lt;code&gt;PasswordField&lt;/code&gt;, &lt;code&gt;SpinBox&lt;/code&gt;, &lt;code&gt;SearchField&lt;/code&gt;) 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Closing a field’s context menu selected the whole field.&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;data-views-1&quot;&gt;Data views&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A single-selection data view could move its cursor off the selection.&lt;/strong&gt;
&lt;code&gt;Ctrl&lt;/code&gt;+arrow and &lt;code&gt;Ctrl&lt;/code&gt; (⌘ on macOS) + &lt;code&gt;Home&lt;/code&gt; / &lt;code&gt;End&lt;/code&gt; / &lt;code&gt;Page&lt;/code&gt; 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
&lt;code&gt;SelectionMode::Single&lt;/code&gt; model, including one handed to a table in the
default &lt;code&gt;MultiRow&lt;/code&gt; mode, or a &lt;code&gt;TableSelectionMode::SingleRow&lt;/code&gt; / &lt;code&gt;SingleCell&lt;/code&gt;
table. Multi-selection views are unchanged. &lt;strong&gt;Behaviour change.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A selection set by the application left the cursor behind.&lt;/strong&gt; In a
single-selection data view the keyboard cursor stayed on the row the user
last reached, so a screen reader went on announcing it and the next arrow
key started from it. The cursor now moves with the selection when the
application or a model change moves the selection. Multi-selection views are
unchanged. &lt;strong&gt;Behaviour change.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;GridView&lt;/code&gt; never said how many tiles were selected, and announced each
tile it realized.&lt;/strong&gt; 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. &lt;code&gt;grid-view-selection-count&lt;/code&gt; is new, in all 23
locales. &lt;strong&gt;Behaviour change&lt;/strong&gt; for anything that read the value in English.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A screen reader’s focus on a &lt;code&gt;GridView&lt;/code&gt; tile made Enter open another
tile.&lt;/strong&gt; A tile offered a focus of its own, so UIA’s &lt;code&gt;SetFocus&lt;/code&gt;, AT-SPI’s
&lt;code&gt;grab_focus&lt;/code&gt; 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. &lt;strong&gt;Behaviour change&lt;/strong&gt; for anything that focused a tile through
&lt;code&gt;Action::Focus&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every change of a &lt;code&gt;GridView&lt;/code&gt;’s selection was heard as a move of focus.&lt;/strong&gt;
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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A double click on a selected &lt;code&gt;GridView&lt;/code&gt; tile opened nothing.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;internationalization-1&quot;&gt;Internationalization&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A message with a plural or a selector was said as its message id when
no translations were installed.&lt;/strong&gt; Without an &lt;code&gt;I18nManager&lt;/code&gt;, &lt;code&gt;tr!&lt;/code&gt;,
&lt;code&gt;tr_widget!&lt;/code&gt; and &lt;code&gt;tr_signal!&lt;/code&gt; gave a message in the source language only
when it was plain text and &lt;code&gt;{ $var }&lt;/code&gt;; any other came back as its key. A
screen reader said “grid-view-selection-count” where “1 item selected” was
meant, and a &lt;code&gt;CommandPalette&lt;/code&gt; 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;terminal&quot;&gt;Terminal&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;Terminal&lt;/code&gt; never announced its new output.&lt;/strong&gt; Each completed line went to
a polite &lt;code&gt;Role::Status&lt;/code&gt; live region as its value. A &lt;code&gt;Status&lt;/code&gt; 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;automation&quot;&gt;Automation&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;type_text&lt;/code&gt; and &lt;code&gt;type_ime&lt;/code&gt; on a text editor’s own node blurred the editor.&lt;/strong&gt;
A &lt;code&gt;RichTextEditor&lt;/code&gt;, &lt;code&gt;CodeEditor&lt;/code&gt; or &lt;code&gt;LogView&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;list_live_regions&lt;/code&gt; listed live regions no screen reader can reach.&lt;/strong&gt; 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
&lt;code&gt;accesskit_consumer&lt;/code&gt;’s filter keeps, which are the ones a platform adapter
walks and can announce. The set comes from
&lt;code&gt;teksilo_core::accessibility::audit::nodes_in_filtered_tree&lt;/code&gt;.
&lt;strong&gt;Behaviour change.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Full Changelog&lt;/strong&gt;: https://github.com/FernTech-EU/teksilo/compare/v0.13.1…v0.14.0&lt;/p&gt;
</content>
  </entry>
  
  
  
  <entry>
    <title>Teksilo 0.13.1</title>
    <id>https://github.com/FernTech-EU/teksilo/releases/tag/v0.13.1</id>
    <link href="https://github.com/FernTech-EU/teksilo/releases/tag/v0.13.1" />
    <published>2026-09-22T00:00:00Z</published>
    <updated>2026-09-22T00:00:00Z</updated>
    <content type="html">&lt;h4 id=&quot;fixed&quot;&gt;Fixed&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cargo teksilo build-vectors&lt;/code&gt; now runs on a Windows checkout.&lt;/strong&gt; Git for
Windows checks the tree out with CRLF, and &lt;code&gt;corpus/index.json&lt;/code&gt; 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 &lt;code&gt;tools/build_corpus.py&lt;/code&gt;
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. &lt;code&gt;cargo-teksilo&lt;/code&gt;’s two corpus tests failed on Windows only for the
same reason and pass there now.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Line endings are pinned to LF for everyone.&lt;/strong&gt; A new &lt;code&gt;.gitattributes&lt;/code&gt;
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: &lt;code&gt;index.json&lt;/code&gt;
is compared byte for byte, and every file the corpus quotes is reconstructed
line for line by &lt;code&gt;cargo teksilo show&lt;/code&gt;. Existing Windows working trees keep
their CRLF files until refreshed, &lt;code&gt;git rm --cached -r . &amp;amp;&amp;amp; git reset --hard&lt;/code&gt;,
or a fresh clone.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Full Changelog&lt;/strong&gt;: https://github.com/FernTech-EU/teksilo/compare/v0.13.0…v0.13.1&lt;/p&gt;
</content>
  </entry>
  
  
  
  <entry>
    <title>Teksilo 0.13.0</title>
    <id>https://github.com/FernTech-EU/teksilo/releases/tag/v0.13.0</id>
    <link href="https://github.com/FernTech-EU/teksilo/releases/tag/v0.13.0" />
    <published>2026-09-20T00:00:00Z</published>
    <updated>2026-09-20T00:00:00Z</updated>
    <content type="html">&lt;h4 id=&quot;added&quot;&gt;Added&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;cargo teksilo&lt;/code&gt;, agent tooling for apps that depend on Teksilo.&lt;/strong&gt; 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 &lt;em&gt;app&lt;/em&gt; had
none of them: &lt;code&gt;docs/&lt;/code&gt; ships in no crate, every example crate is
&lt;code&gt;publish = false&lt;/code&gt;, and the only probe harness lived in one app’s repository.
&lt;code&gt;cargo install cargo-teksilo&lt;/code&gt; closes that gap, and every answer is matched to
the Teksilo the app actually resolved.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cargo teksilo symbol &amp;lt;Name&amp;gt;&lt;/code&gt;, 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 &lt;strong&gt;every&lt;/strong&gt; teksilo
crate, so &lt;code&gt;cargo teksilo symbol ListModel&lt;/code&gt; answers with teksilo-data’s
without &lt;code&gt;--crate data&lt;/code&gt;; 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 &lt;code&gt;lib.rs&lt;/code&gt; are included, teksilo-webview’s &lt;code&gt;WebView&lt;/code&gt;
among them. This is the one subcommand that needs &lt;code&gt;python3&lt;/code&gt; on PATH.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cargo teksilo search &quot;&amp;lt;question&amp;gt;&quot;&lt;/code&gt;, 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cargo teksilo show &amp;lt;path&amp;gt;&lt;/code&gt;, the document behind a search hit, in full,
offline: &lt;code&gt;cargo teksilo show docs/scroll-area.md&lt;/code&gt;, or just the lines the hit
cited (&lt;code&gt;--lines 166-172&lt;/code&gt;, 1-based and inclusive, as &lt;code&gt;search&lt;/code&gt; prints them).
Nothing is re-fetched, a chunk already carries its own text and the line
range it occupied, so the document is &lt;em&gt;reassembled&lt;/em&gt; 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 &lt;code&gt;blob/main/&lt;/code&gt;
or the published book serves &lt;code&gt;main&lt;/code&gt; rather than the version the app pinned.
&lt;code&gt;search&lt;/code&gt; now says so in a footer line, and &lt;code&gt;--list&lt;/code&gt; prints every path.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cargo teksilo probe&lt;/code&gt;, writes the automation probe harness into
&lt;code&gt;scripts/teksilo_probe/&lt;/code&gt;, 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cargo teksilo setup&lt;/code&gt;, the above, plus the Teksilo briefing for &lt;strong&gt;every
coding agent this project already configures, each in that agent’s own
format&lt;/strong&gt;: the full four-file skill into &lt;code&gt;.claude/skills/teksilo/&lt;/code&gt; where it
is native, and a self-contained ~40-line brief into
&lt;code&gt;.cursor/rules/teksilo.mdc&lt;/code&gt; (MDC frontmatter), &lt;code&gt;.windsurf/rules/teksilo.md&lt;/code&gt;
(&lt;code&gt;trigger:&lt;/code&gt; frontmatter), &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; and &lt;code&gt;AGENTS.md&lt;/code&gt;
(each a &lt;code&gt;&amp;lt;!-- BEGIN teksilo --&amp;gt;&lt;/code&gt; region) where it is not. These tools share no
format, so copying the skill directory into &lt;code&gt;.cursor/&lt;/code&gt; 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, &lt;code&gt;--no-model&lt;/code&gt; to skip), so the first &lt;code&gt;search&lt;/code&gt; does not stall
on a download, and a failed fetch is a warning, because &lt;code&gt;search&lt;/code&gt; degrades
to BM25 by design.&lt;/p&gt;
&lt;p&gt;Three things it now refuses to do. It &lt;strong&gt;never writes &lt;code&gt;$HOME&lt;/code&gt; from project
scope&lt;/strong&gt;, the previous version silently installed into &lt;code&gt;~/.claude/skills/&lt;/code&gt;
when the project had no &lt;code&gt;.claude/&lt;/code&gt; of its own, a machine-wide change from a
project-scoped command; &lt;code&gt;--user&lt;/code&gt; is now the only path that reaches the home
directory. It &lt;strong&gt;never prompts where nobody can answer&lt;/strong&gt;: with no terminal on
stdin the question is an error naming the flag that would have skipped it
(&lt;code&gt;-y&lt;/code&gt;, or &lt;code&gt;--user&lt;/code&gt; when there is no &lt;code&gt;Cargo.toml&lt;/code&gt; anywhere above the working
directory), because CI and agents run this and a hang is worse than a
failure. And it &lt;strong&gt;never writes before showing the plan&lt;/strong&gt;, every path, every
detected vendor, and the download with its size, then a confirmation. Runs
are idempotent: a second run reports &lt;code&gt;unchanged&lt;/code&gt; and leaves the shared files
byte for byte, including whatever the project wrote outside the markers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cline&lt;/strong&gt; is served too, and is the one vendor whose path is read off the
disk rather than fixed: &lt;code&gt;.clinerules/teksilo.md&lt;/code&gt; where that directory
exists, a marker region where &lt;code&gt;.clinerules&lt;/code&gt; is a plain file, which it may
be, and which has nowhere to put a file of our own, and
&lt;code&gt;.cline/rules/teksilo.md&lt;/code&gt; where that is the only layout present.&lt;/p&gt;
&lt;p&gt;The order is deliberately &lt;strong&gt;not&lt;/strong&gt; newest-first. Cline’s source calls
&lt;code&gt;.cline&lt;/code&gt; &lt;code&gt;CLINE_CONFIG_DIR&lt;/code&gt; and &lt;code&gt;.clinerules&lt;/code&gt; &lt;code&gt;DEPRECATED_CONFIG_DIR&lt;/code&gt;, so
the newer path looks like the obvious target; but the VS Code extension was
hardcoded to &lt;code&gt;.clinerules&lt;/code&gt; and ignored &lt;code&gt;.cline/rules/&lt;/code&gt; outright
(cline/cline#14186), the cross-surface fix reached &lt;code&gt;main&lt;/code&gt; 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 &lt;code&gt;.clinerules/&lt;/code&gt;. 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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Two things it refuses to do, both found by adversarially reviewing the
command against itself and reproduced before being fixed. It &lt;strong&gt;will not
rewrite a shared file it cannot read whole&lt;/strong&gt;: &lt;code&gt;AGENTS.md&lt;/code&gt; and
&lt;code&gt;copilot-instructions.md&lt;/code&gt; belong to the project, and reading them with
&lt;code&gt;read_to_string(..).unwrap_or_default()&lt;/code&gt; turned “cannot decode” into “the
file is empty”, one Latin-1 byte was enough for &lt;code&gt;setup -y&lt;/code&gt; 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 &lt;strong&gt;case-insensitively&lt;/strong&gt;: the skill is written through
&lt;code&gt;fs::write&lt;/code&gt; but audited through &lt;code&gt;read_dir&lt;/code&gt;, 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 &lt;code&gt;SKILL.md&lt;/code&gt;
while reporting &lt;code&gt;updated&lt;/code&gt;. Folding can only err towards keeping a stale
file, which is recoverable; deleting a live one is not.&lt;/p&gt;
&lt;p&gt;In user scope it reaches three agents, Claude Code (&lt;code&gt;~/.claude/skills/&lt;/code&gt;),
Mistral Vibe (&lt;code&gt;~/.vibe/AGENTS.md&lt;/code&gt;, or &lt;code&gt;$VIBE_HOME&lt;/code&gt;) and opencode
(&lt;code&gt;~/.config/opencode/AGENTS.md&lt;/code&gt;, or &lt;code&gt;$XDG_CONFIG_HOME&lt;/code&gt;), each &lt;strong&gt;gated on
its directory already existing&lt;/strong&gt;, 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 &lt;code&gt;VIBE_HOME&lt;/code&gt;, 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
&lt;code&gt;~/.codeium/windsurf/memories/global_rules.md&lt;/code&gt;, and a repository-root
&lt;code&gt;AGENTS.md&lt;/code&gt; is per-repository by definition.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;cargo teksilo status&lt;/code&gt;&lt;/strong&gt; reports what is actually installed, every agent, in
both the project and the user scope, plus the search model, the Python 3
interpreter &lt;code&gt;symbol&lt;/code&gt; needs, and where each sits on disk. It reads only, and
its exit code never depends on what it finds.&lt;/p&gt;
&lt;p&gt;It is the &lt;strong&gt;dry run of &lt;code&gt;setup&lt;/code&gt;&lt;/strong&gt;, not a second opinion: every agent row comes
from &lt;code&gt;setup::inspect&lt;/code&gt;, 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, &lt;em&gt;&lt;code&gt;inspect&lt;/code&gt; reports current exactly
when &lt;code&gt;setup&lt;/code&gt; would report &lt;code&gt;unchanged&lt;/code&gt;&lt;/em&gt;, 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.&lt;/p&gt;
&lt;p&gt;Three words (&lt;code&gt;here&lt;/code&gt; / &lt;code&gt;not here&lt;/code&gt; / &lt;code&gt;n/a&lt;/code&gt;) and a reason beside each, because
three words cannot carry the difference between &lt;em&gt;Cursor is not used in this
project&lt;/em&gt; and &lt;em&gt;Cursor is used here and has no brief&lt;/em&gt;, and that difference is
the whole of what to do next. An install from an older release reads &lt;code&gt;here&lt;/code&gt;,
since it is being read right now, with “from another release” in the detail,
calling it anything else would be false. And &lt;code&gt;n/a&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;Its own command rather than &lt;code&gt;setup --status&lt;/code&gt;: it reports on both scopes while
&lt;code&gt;setup&lt;/code&gt; 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 &lt;code&gt;--status -y&lt;/code&gt; to reason about.&lt;/p&gt;
&lt;p&gt;“Reads only” is enforced rather than asserted. It asks cargo for the
resolved version through &lt;code&gt;--locked&lt;/code&gt;, because plain &lt;code&gt;cargo metadata&lt;/code&gt;
&lt;em&gt;resolves&lt;/em&gt;, it creates a missing &lt;code&gt;Cargo.lock&lt;/code&gt; 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 &lt;code&gt;$VIBE_HOME&lt;/code&gt;, &lt;code&gt;$XDG_CONFIG_HOME&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;Version binding is the design constraint, not a detail. The tool reads the
app’s &lt;code&gt;Cargo.lock&lt;/code&gt;, and a minor or major mismatch &lt;strong&gt;refuses&lt;/strong&gt; instead of
answering, serving 0.12 answers to an app on 0.9 is worse than serving
nothing, because &lt;code&gt;SplitView&lt;/code&gt; was deleted outright in favour of &lt;code&gt;Splitter&lt;/code&gt;
between them and the wrong answer reads exactly like the right one. A
patch-level difference warns and proceeds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What a refusal can tell a model to run depends on which side of this
release the app sits on&lt;/strong&gt;, because &lt;code&gt;cargo-teksilo&lt;/code&gt; 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
&lt;code&gt;crates/cargo-teksilo&lt;/code&gt; directory. &lt;code&gt;cargo-teksilo-fmt&lt;/code&gt; is the contrast, it
has been published at every teksilo version since 0.9.0, which is exactly why
a &lt;code&gt;--version &amp;lt;app&amp;gt;&lt;/code&gt; line can be emitted there unconditionally and cannot be
emitted here. So there are three regimes. &lt;strong&gt;Below 0.13.0 no install command
exists at all&lt;/strong&gt;, 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. &lt;strong&gt;At
0.13.0 or above both routes print unconditionally&lt;/strong&gt;, &lt;code&gt;--version &amp;lt;v&amp;gt; --locked&lt;/code&gt; from the registry, or &lt;code&gt;cargo install --path &amp;lt;checkout&amp;gt;/crates/cargo-teksilo --locked&lt;/code&gt; from the framework tree, because
whether a given version reached crates.io is not observable offline, and an
app pinning teksilo by &lt;code&gt;path&lt;/code&gt; or &lt;code&gt;git&lt;/code&gt; resolved a version that was never
published at all. &lt;strong&gt;An unparseable version gets the checkout routes&lt;/strong&gt;, 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 &lt;code&gt;--root &amp;lt;dir&amp;gt;&lt;/code&gt; and putting that
&lt;code&gt;&amp;lt;dir&amp;gt;/bin&lt;/code&gt; first on PATH for that tree, or running the tool straight out of
a checkout with &lt;code&gt;cargo run -p cargo-teksilo&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;teksilo-corpus&lt;/code&gt;&lt;/strong&gt;, the guides and the worked examples, chunked into a
retrieval index and published per release so &lt;code&gt;cargo&lt;/code&gt; resolves the corpus
matching an app’s Teksilo. It is one file: a chunk carries its own text, and
its &lt;code&gt;path&lt;/code&gt; names the original in this repository (&lt;code&gt;docs/scroll-area.md&lt;/code&gt;,
&lt;code&gt;examples/simple_button/src/main.rs&lt;/code&gt;), 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
&lt;code&gt;kind&lt;/code&gt; and excluded from retrieval rather than from the corpus: &lt;code&gt;show&lt;/code&gt;
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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The automation probe harness&lt;/strong&gt;, written into a consumer’s project by
&lt;code&gt;cargo teksilo probe&lt;/code&gt;. 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 &lt;code&gt;TOOL_CATALOG&lt;/code&gt; (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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Three new guides&lt;/strong&gt;: &lt;a href=&quot;docs/agent-tooling.md&quot;&gt;Agent tooling&lt;/a&gt; documents the
above; &lt;a href=&quot;docs/scroll-area.md&quot;&gt;Scroll areas&lt;/a&gt; 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 &lt;a href=&quot;docs/docking.md&quot;&gt;Docking layout&lt;/a&gt; is the consumer
guide for &lt;code&gt;DockingLayout&lt;/code&gt;, 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
&lt;a href=&quot;docs/docking-horizontal-rail.md&quot;&gt;Horizontal activity rail (backlog)&lt;/a&gt;, 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.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;tools/extract_widget_api.py&lt;/code&gt; now covers every crate with a public API&lt;/strong&gt;
(30, up from 4), including the types a crate declares in its own &lt;code&gt;lib.rs&lt;/code&gt;.
mdBook catalog generation stays scoped to the four cataloged crates, so
&lt;code&gt;docs/&lt;/code&gt; is unchanged; the rest are queryable through &lt;code&gt;--crate&lt;/code&gt;, &lt;code&gt;--list&lt;/code&gt;,
&lt;code&gt;--all&lt;/code&gt; and by name.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;cargo-teksilo&lt;/code&gt; ships a README&lt;/strong&gt;, so a crates.io reader learns what the
&lt;code&gt;semantic&lt;/code&gt; default feature pulls in, that &lt;code&gt;--no-default-features&lt;/code&gt; is a fully
working tool, where the model cache lives, and that &lt;code&gt;symbol&lt;/code&gt; needs
&lt;code&gt;python3&lt;/code&gt;, none of which was readable outside this repository.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;changed&quot;&gt;Changed&lt;/h4&gt;
&lt;h5 id=&quot;breaking-changes&quot;&gt;Breaking changes&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;OverlayDismissCallback&lt;/code&gt; is &lt;code&gt;Rc&amp;lt;dyn Fn(DismissReason, &amp;amp;mut EventContext)&amp;gt;&lt;/code&gt;&lt;/strong&gt;,
where it was &lt;code&gt;Rc&amp;lt;dyn Fn()&amp;gt;&lt;/code&gt;. A closure that wants neither gains two ignored
parameters, &lt;code&gt;Rc::new(move |_, _| …)&lt;/code&gt;. The new &lt;code&gt;#[non_exhaustive]&lt;/code&gt;
&lt;code&gt;teksilo_core::overlay::DismissReason&lt;/code&gt; names the route that closed the
overlay: &lt;code&gt;Escape&lt;/code&gt;, &lt;code&gt;OutsidePress&lt;/code&gt;, &lt;code&gt;PointerLeave&lt;/code&gt;, &lt;code&gt;Cascade&lt;/code&gt; or
&lt;code&gt;Programmatic&lt;/code&gt;. Both &lt;code&gt;OverlayRequest::on_dismiss&lt;/code&gt; and
&lt;code&gt;ModalRequest::on_dismiss&lt;/code&gt; carry the new type. The five &lt;code&gt;OverlayManager&lt;/code&gt;
dismissal methods that do not name a reason, &lt;code&gt;dismiss&lt;/code&gt;, &lt;code&gt;dismiss_top&lt;/code&gt;,
&lt;code&gt;dismiss_all&lt;/code&gt;, &lt;code&gt;dismiss_except&lt;/code&gt; and &lt;code&gt;dismiss_with_focus_restore&lt;/code&gt;, keep the
signatures they had and report &lt;code&gt;Programmatic&lt;/code&gt;; their &lt;code&gt;dismiss_because&lt;/code&gt;,
&lt;code&gt;dismiss_top_because&lt;/code&gt;, &lt;code&gt;dismiss_all_because&lt;/code&gt;, &lt;code&gt;dismiss_except_because&lt;/code&gt; and
&lt;code&gt;dismiss_with_focus_restore_because&lt;/code&gt; twins take the reason instead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dismissing an overlay straight through &lt;code&gt;OverlayManager&lt;/code&gt; no longer runs its
&lt;code&gt;on_dismiss&lt;/code&gt;.&lt;/strong&gt; Code reaching through &lt;code&gt;WidgetTree::overlay_manager_mut()&lt;/code&gt; to
dismiss calls &lt;code&gt;WidgetTree::dismiss_overlay&lt;/code&gt; instead. The ordering is
unchanged, the callback still runs during dismissal, before focus returns to
the trigger.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;MessageBoxResult::dismissed_by_escape&lt;/code&gt; is replaced by &lt;code&gt;dismissal&lt;/code&gt;.&lt;/strong&gt;
&lt;code&gt;MessageBoxDismissal&lt;/code&gt; is &lt;code&gt;#[non_exhaustive]&lt;/code&gt; and names the route, &lt;code&gt;Button&lt;/code&gt;,
&lt;code&gt;Escape&lt;/code&gt;, &lt;code&gt;ClickOutside&lt;/code&gt; or &lt;code&gt;Programmatic&lt;/code&gt;, 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. &lt;code&gt;result.dismissed_by_escape&lt;/code&gt; becomes
&lt;code&gt;result.dismissal == MessageBoxDismissal::Escape&lt;/code&gt;; where the flag was read as
“the user did not choose a button”, read &lt;code&gt;result.was_dismissed()&lt;/code&gt;. &lt;code&gt;button&lt;/code&gt;
is unchanged, and still carries the escape-button resolution on every route.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;fixed&quot;&gt;Fixed&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A screen reader can open a &lt;code&gt;Dialog&lt;/code&gt; that has a custom trigger.&lt;/strong&gt; The
trigger published a named &lt;code&gt;Role::Button&lt;/code&gt; 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
&lt;code&gt;Dialog::new(label)&lt;/code&gt;, not only an explicitly hand-built trigger, and the same
shape affected &lt;code&gt;Snackbar&lt;/code&gt; and a &lt;code&gt;PopoverWidget&lt;/code&gt; over a custom trigger. The
node now advertises &lt;code&gt;Action::Click&lt;/code&gt; and acts on it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A modal presented as a native OS window honours its &lt;code&gt;ModalCloseBehavior&lt;/code&gt;.&lt;/strong&gt;
Escape did nothing to it on macOS and Windows, including in the default
configuration, where &lt;code&gt;Dialog&lt;/code&gt; asks for &lt;code&gt;EscapeOrClickOutside&lt;/code&gt; 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 &lt;code&gt;ClickOutside&lt;/code&gt; now means
the same as &lt;code&gt;Manual&lt;/code&gt; for that presentation. Linux and the BSDs were never
affected, they present modals in-tree, where Escape already worked.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;MessageBox&lt;/code&gt; dismissed with Escape or a press outside reports its
answer.&lt;/strong&gt; The dialog closed and the application was told nothing,
&lt;code&gt;on_result&lt;/code&gt; 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 &lt;code&gt;ModalPresentation::Auto&lt;/code&gt; resolves
to on Linux and the BSDs, every unix but macOS, and so was the default
behaviour there; &lt;code&gt;ModalPresentation::InTree&lt;/code&gt; reaches it on any platform. Both
routes now resolve the escape button and report it, with
&lt;code&gt;MessageBoxResult::dismissal&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An &lt;code&gt;InputDialog&lt;/code&gt; dismissed without a button press reports the
cancellation.&lt;/strong&gt; &lt;code&gt;None&lt;/code&gt; 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 &lt;code&gt;None&lt;/code&gt;. The in-tree presentation
only: a natively presented &lt;code&gt;InputDialog&lt;/code&gt;, macOS and Windows, still reports
nothing when Escape closes it, and &lt;code&gt;MessageBox&lt;/code&gt; reported correctly on that
presentation already.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;CommandPalette&lt;/code&gt; runs its &lt;code&gt;on_dismiss&lt;/code&gt; when the palette is dismissed.&lt;/strong&gt;
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. &lt;code&gt;CommandPalette&lt;/code&gt;
presents in-tree on every platform, so every platform was affected. It now
runs on every route, and once only.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Full Changelog&lt;/strong&gt;: https://github.com/FernTech-EU/teksilo/compare/v0.12.1…v0.13.0&lt;/p&gt;
</content>
  </entry>
  
  
  
  <entry>
    <title>Teksilo 0.12.1</title>
    <id>https://github.com/FernTech-EU/teksilo/releases/tag/v0.12.1</id>
    <link href="https://github.com/FernTech-EU/teksilo/releases/tag/v0.12.1" />
    <published>2026-09-18T00:00:00Z</published>
    <updated>2026-09-18T00:00:00Z</updated>
    <content type="html">&lt;p&gt;No changelog entry found for version 0.12.1&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Full Changelog&lt;/strong&gt;: https://github.com/FernTech-EU/teksilo/compare/v0.12.0…v0.12.1&lt;/p&gt;
</content>
  </entry>
  
  
  
  <entry>
    <title>Teksilo 0.12.0</title>
    <id>https://github.com/FernTech-EU/teksilo/releases/tag/v0.12.0</id>
    <link href="https://github.com/FernTech-EU/teksilo/releases/tag/v0.12.0" />
    <published>2026-09-17T00:00:00Z</published>
    <updated>2026-09-17T00:00:00Z</updated>
    <content type="html">&lt;p&gt;&lt;code&gt;teksilo-scene&lt;/code&gt; 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. &lt;code&gt;SceneCard&lt;/code&gt; 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 &lt;em&gt;lightweight&lt;/em&gt; 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 &lt;code&gt;SceneView&lt;/code&gt; with selection or magnetism switched on.&lt;/p&gt;
&lt;p&gt;Breaking, pre-1.0: &lt;code&gt;PathItem::new&lt;/code&gt; takes one argument, &lt;code&gt;SceneItem&lt;/code&gt; loses two
methods and gains one, &lt;code&gt;Path::commands&lt;/code&gt; is a method, &lt;code&gt;item_change_signal&lt;/code&gt;
carries an envelope, and several types the crate hands &lt;em&gt;to&lt;/em&gt; consumer code are
now &lt;code&gt;#[non_exhaustive]&lt;/code&gt;. Each one is listed under &lt;strong&gt;Breaking changes&lt;/strong&gt; below
with what to do about it.&lt;/p&gt;
&lt;h4 id=&quot;added&quot;&gt;Added&lt;/h4&gt;
&lt;h5 id=&quot;moving-resizing-and-rotating-a-selection&quot;&gt;Moving, resizing and rotating a selection&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A selection transform controller&lt;/strong&gt;, opted into per view with
&lt;code&gt;SceneView::transform_controller(TransformConfig)&lt;/code&gt;: 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
&lt;code&gt;chrome&lt;/code&gt; closure for painting the whole thing yourself. &lt;code&gt;on_start&lt;/code&gt; /
&lt;code&gt;on_change&lt;/code&gt; / &lt;code&gt;on_end&lt;/code&gt; report the gesture; &lt;code&gt;LivePreview&lt;/code&gt; decides whether the
content moves under the frame or only the frame does.&lt;/li&gt;
&lt;li&gt;The model is written &lt;strong&gt;once, when the gesture ends&lt;/strong&gt;, through
&lt;code&gt;Scene::apply_transform_delta&lt;/code&gt;. One gesture is one step to reverse, &lt;code&gt;Esc&lt;/code&gt;
cancels with nothing to roll back, and a sample costs a relayout rather than
one model write per selected item.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keyboard&lt;/strong&gt;: the transform key (&lt;code&gt;t&lt;/code&gt; by default) enters the controller, &lt;code&gt;Tab&lt;/code&gt;
cycles the handles, the arrows move or resize by a step, &lt;code&gt;Esc&lt;/code&gt; cancels.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SceneView::transform_session_signal()&lt;/code&gt; publishes the live gesture, for an
app-owned inspector, status bar or size readout.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Assistive technology&lt;/strong&gt;: every handle is published with a role, a name and,
where it drives a single number, a value. &lt;code&gt;Increment&lt;/code&gt; and &lt;code&gt;Decrement&lt;/code&gt; move
the coordinates that handle actually drives, and each two-dimensional handle
also advertises a named action per direction (&lt;code&gt;TransformStep&lt;/code&gt;), so a width can
be changed without a height. &lt;code&gt;TransformLabels&lt;/code&gt; takes &lt;code&gt;tr!&lt;/code&gt; like every other
label.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Both tiers move.&lt;/strong&gt; A heavyweight card is draggable with a pointer at last;
the lightweight drag, the &lt;code&gt;Alt&lt;/code&gt;+arrow nudge and the controller all go through
one door.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SceneView::transform_enabled_signal()&lt;/code&gt; and &lt;code&gt;magnetism_enabled_signal()&lt;/code&gt; for a
toolbar to bind. Turning either off takes its chrome off the screen &lt;em&gt;and&lt;/em&gt; out
of the published accessibility tree.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;scenecard-and-a-card-that-sizes-itself-to-its-content&quot;&gt;&lt;code&gt;SceneCard&lt;/code&gt;, and a card that sizes itself to its content&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SceneCard&lt;/code&gt;&lt;/strong&gt;, the container a heavyweight item usually wants: a surface, a
header that is the grab handle, a selection ring, three &lt;code&gt;CardMode&lt;/code&gt;s
(&lt;code&gt;Idle&lt;/code&gt; / &lt;code&gt;Selected&lt;/code&gt; / &lt;code&gt;Editing&lt;/code&gt;) and one named &lt;code&gt;Role::Group&lt;/code&gt;, 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 &lt;code&gt;surface(..)&lt;/code&gt; replaces
the chrome entirely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SizePolicy&lt;/code&gt;&lt;/strong&gt; on a heavyweight entry, &lt;code&gt;Fixed&lt;/code&gt; (today’s behaviour and the
default), &lt;code&gt;HeightForWidth&lt;/code&gt; (the width is authored, the height follows the
words) and &lt;code&gt;Intrinsic&lt;/code&gt; (the entry shrink-wraps its widget). Set it with
&lt;code&gt;Scene::set_size_policy&lt;/code&gt; / &lt;code&gt;SceneModel::set_size_policy&lt;/code&gt;, or with
&lt;code&gt;SceneCard::height_for_width()&lt;/code&gt;. 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. &lt;code&gt;Scene::set_measured_size&lt;/code&gt; is there for an app measuring
something the framework cannot.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;SceneView&lt;/code&gt; answers &lt;code&gt;ScrollIntoView&lt;/code&gt;.&lt;/strong&gt; A caret moving inside an embedded
editor pans the camera to follow it, with no app wiring, and an outer
&lt;code&gt;ScrollArea&lt;/code&gt; is re-targeted to where the card will land. It honours reduced
motion, which the public &lt;code&gt;SceneView::ensure_visible&lt;/code&gt; cannot.&lt;/li&gt;
&lt;li&gt;Double-clicking a card puts the caret in its body even when that body is a
&lt;code&gt;Switcher&lt;/code&gt; that swaps the editor in on the next pass.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;one-geometry-one-picker&quot;&gt;One geometry, one picker&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SceneItem::shape() -&amp;gt; ItemShape&lt;/code&gt;&lt;/strong&gt; 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 &lt;code&gt;shape_contains&lt;/code&gt;
&lt;em&gt;and&lt;/em&gt; &lt;code&gt;clone_shape_test&lt;/code&gt;, which had to be kept in agreement by hand.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Qt’s four &lt;code&gt;ItemSelectionMode&lt;/code&gt;s&lt;/strong&gt;, &lt;code&gt;IntersectsItemShape&lt;/code&gt; (the default),
&lt;code&gt;ContainsItemShape&lt;/code&gt;, &lt;code&gt;IntersectsItemBoundingRect&lt;/code&gt; and
&lt;code&gt;ContainsItemBoundingRect&lt;/code&gt;, chosen per view with
&lt;code&gt;SceneView::marquee_selection_mode&lt;/code&gt;, or per call on &lt;code&gt;Scene::items_in_region&lt;/code&gt;
and &lt;code&gt;colliding_items_with&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SceneRegion&lt;/code&gt; is a rectangle &lt;em&gt;or&lt;/em&gt; a path&lt;/strong&gt;, so a lasso is expressible:
&lt;code&gt;SceneRegion::lasso(path)&lt;/code&gt;, &lt;code&gt;::stroke(path, width)&lt;/code&gt;, &lt;code&gt;::from_screen_rect(..)&lt;/code&gt;.
A rotated marquee stops over-selecting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One resolver decides every pick.&lt;/strong&gt; &lt;code&gt;PaintKey&lt;/code&gt;, band, then &lt;code&gt;z&lt;/code&gt;, then
insertion order, is the order the tap, the drag, the hover, the cursor, the
tooltip, the hold and &lt;code&gt;Scene::item_at&lt;/code&gt; all read. &lt;code&gt;claims_press&lt;/code&gt; and
&lt;code&gt;hit_testable&lt;/code&gt; are public for a consumer running its own pass.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PathItem&lt;/code&gt; carries a real &lt;code&gt;fill_rule&lt;/code&gt;, used by painting and hit-testing alike,
and &lt;code&gt;hit_stroke_width&lt;/code&gt; for a hairline that should still be clickable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On &lt;code&gt;Path&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;flatten(tolerance)&lt;/code&gt; (a polyline per subpath),
&lt;code&gt;exact_bounds(tolerance)&lt;/code&gt; (a tight box, where &lt;code&gt;bounds()&lt;/code&gt; returns the
control-point hull and so over-reports every curve),
&lt;code&gt;contains_point(p, rule, tolerance)&lt;/code&gt;, the &lt;code&gt;Subpath&lt;/code&gt; type, and &lt;code&gt;arc_to_cubics&lt;/code&gt;
shared rather than duplicated inside the path atlas.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;ink&quot;&gt;Ink&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A third paint band.&lt;/strong&gt; &lt;code&gt;SceneLayer::Interleaved&lt;/code&gt; orders a lightweight item
against the heavyweight cards by &lt;code&gt;z&lt;/code&gt;, 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WetLayer&lt;/code&gt;&lt;/strong&gt;, a surface for content being authored right now, installed
with &lt;code&gt;SceneView::wet_layer(..)&lt;/code&gt;. It repaints on its own without re-running the
band beneath it, and always sits above every card and interleaved item and
below the &lt;code&gt;Over&lt;/code&gt; band. &lt;code&gt;WetLayer::request_repaint(ctx)&lt;/code&gt; from a pointer
handler; &lt;code&gt;WetNode&lt;/code&gt; names the nodes it owns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;EventContext::coalesced()&lt;/code&gt;&lt;/strong&gt;, the positions the OS batched into the packet
being dispatched, oldest first, each with its own timestamp &lt;em&gt;and&lt;/em&gt; its own
pressure and tilt. A drawing surface fans out over it and then over
&lt;code&gt;pointer_position()&lt;/code&gt;. Opt the producer in with
&lt;code&gt;TeksiloAppBuilder::pen_batching(PenBatching::Coalesce)&lt;/code&gt;, which emits one
dispatch per pen drain instead of one per packet and never folds a transition.
Not &lt;code&gt;on_drag&lt;/code&gt;, the drag recogniser swallows every move inside its slop and
reports the press position, so the start of a stroke never arrives there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;docs/ink.md&quot;&gt;docs/ink.md&lt;/a&gt;&lt;/strong&gt;, the shape of a drawing tool on a scene page:
what to read a stroke from, what to put on which band, and what a wet stroke
costs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;a-seam-a-data-layer-can-reverse-a-scene-edit-through&quot;&gt;A seam a data layer can reverse a scene edit through&lt;/h5&gt;
&lt;p&gt;Undo itself stays in the data layer; this crate ships no stack and no &lt;code&gt;undo()&lt;/code&gt;.
What it ships is a record complete enough to invert.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every notification carries a transaction id, a &lt;code&gt;ChangeSource&lt;/code&gt;
(&lt;code&gt;User&lt;/code&gt; / &lt;code&gt;Programmatic&lt;/code&gt; / &lt;code&gt;Remote&lt;/code&gt;), a &lt;code&gt;HistoryMode&lt;/code&gt;
(&lt;code&gt;Record&lt;/code&gt; / &lt;code&gt;RecordPreserveRedo&lt;/code&gt; / &lt;code&gt;Ignore&lt;/code&gt;) and an &lt;code&gt;ephemeral&lt;/code&gt; flag. The
framework stamps its own gesture commits &lt;code&gt;User&lt;/code&gt;, so an app can tell a finished
drag from a programmatic move.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One write scope is one transaction&lt;/strong&gt;, so removing a subtree is one change to
reverse rather than N. Group several calls with &lt;code&gt;SceneModel::transaction&lt;/code&gt; /
&lt;code&gt;user_edit&lt;/code&gt;; nesting joins rather than splits. &lt;code&gt;SceneTransaction::abandon&lt;/code&gt;
tags a cancelled interaction, and &lt;code&gt;squash&lt;/code&gt; (off by default) folds a
transaction’s repeated writes to one quantity into its endpoints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Scene::take&lt;/code&gt; / &lt;code&gt;Scene::restore&lt;/code&gt;&lt;/strong&gt;, an owning salvage door. &lt;code&gt;take&lt;/code&gt; hands
back the item box, its magnets (ids included) and its whole slice of the
logical accessibility tree; &lt;code&gt;restore&lt;/code&gt; puts it back at the &lt;strong&gt;same&lt;/strong&gt; &lt;code&gt;ItemId&lt;/code&gt;
and the same place in the reading order, which is what selection, magnets,
relations and every app side-map are keyed on. &lt;code&gt;Scene::remove&lt;/code&gt; is the same
call with the salvage routed to the edit sink, and &lt;code&gt;RemovedItem::detach()&lt;/code&gt;
forgets the recorded parent so a salvage restores at root level, or into a
different scene.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SceneModel::set_edit_sink&lt;/code&gt;&lt;/strong&gt; receives one owning &lt;code&gt;SceneTransactionRecord&lt;/code&gt;
per committed transaction, with the scene unborrowed so the sink may read and
write it; the sink’s own writes are journaled in turn. &lt;code&gt;transaction_signal&lt;/code&gt;
fires once per transaction, after the sink.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Scene::replace_item&lt;/code&gt; swaps a lightweight item’s box while keeping the entry,
the id and everything keyed on it. &lt;code&gt;Placement { parent, z, local_pos, transform }&lt;/code&gt; is written as one property with &lt;code&gt;set_placement&lt;/code&gt;, plus
&lt;code&gt;reparent_keeping_scene_pos&lt;/code&gt; for a drag into a group that must not move the
item, and &lt;code&gt;z_between&lt;/code&gt;, which reports when &lt;code&gt;f32&lt;/code&gt; precision has run out at a
locus.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ItemChange::is_edit()&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;Accessibility readers to match the writers: &lt;code&gt;Scene::a11y_live_of&lt;/code&gt; /
&lt;code&gt;a11y_landmark_of&lt;/code&gt;, and &lt;code&gt;a11y_relations&lt;/code&gt; / &lt;code&gt;a11y_live_of&lt;/code&gt; /
&lt;code&gt;a11y_landmark_of&lt;/code&gt; / &lt;code&gt;a11y_categories_of&lt;/code&gt; on &lt;code&gt;SceneModel&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;saying-what-a-change-should-become-before-it-is-applied&quot;&gt;Saying what a change should become before it is applied&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A geometry constraint on &lt;code&gt;SceneModel&lt;/code&gt;&lt;/strong&gt;, one closure that rewrites a
gesture’s proposed geometry &lt;em&gt;before&lt;/em&gt; 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 &lt;code&gt;SceneModel::set_geometry_constraint&lt;/code&gt;; it is handed
a &lt;code&gt;ProposedChange&lt;/code&gt; (the scene read-only, the roots the gesture moves, and the
gesture’s start and proposed frames in scene coordinates) and returns a
&lt;code&gt;ChangeVerdict&lt;/code&gt;, &lt;code&gt;Accept&lt;/code&gt;, &lt;code&gt;Adjust(frame)&lt;/code&gt; or &lt;code&gt;Reject&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It governs the transform controller on both tiers, the lightweight item drag
and the &lt;code&gt;Alt&lt;/code&gt;+arrow nudge, for pointer, keyboard and assistive technology
alike. An app driving its own drag calls &lt;code&gt;SceneModel::constrain_move&lt;/code&gt; /
&lt;code&gt;constrain_frame&lt;/code&gt;. Programmatic mutators are never constrained.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SceneModel::downgrade()&lt;/code&gt; and &lt;code&gt;WeakSceneModel&lt;/code&gt;&lt;/strong&gt;, a non-owning handle. A
constraint normally needs no handle at all (&lt;code&gt;ProposedChange::scene&lt;/code&gt; 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;core-platform-and-canvas&quot;&gt;Core, platform and canvas&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Widget::accepts_child_hit(child, point)&lt;/code&gt;&lt;/strong&gt;, a parent can veto one child for
one point, which is the per-point question &lt;code&gt;hit_transparent&lt;/code&gt; (a per-node
declaration) cannot ask.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;EventContext::dispatch_target()&lt;/code&gt;&lt;/strong&gt;, the arena’s own answer to who the press
landed on, so a container need not guess it from a rectangle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;EventContext::release_cursor()&lt;/code&gt;&lt;/strong&gt;, withdraws a handler’s cursor override so
the node-declared one resolves again.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A pan claimant may carry its own drag.&lt;/strong&gt; A node that declares a &lt;code&gt;PanClaim&lt;/code&gt;
and also installs &lt;code&gt;on_drag&lt;/code&gt; now competes as both: the pan wins at &lt;code&gt;pan_slop&lt;/code&gt;,
and a hold arms the node’s own drag. &lt;code&gt;LongPressRole::DragHandle&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every pen sample carries the device’s own time.&lt;/strong&gt; &lt;code&gt;PenPacket::device_time_ms&lt;/code&gt;
plus a shared &lt;code&gt;back_date&lt;/code&gt; rule that places a drained batch on the tree’s
timeline, wrapped 32-bit counters and missing clocks included. Windows now
drains &lt;code&gt;GetPointerPenInfoHistory&lt;/code&gt;, so a digitizer running above the message
rate delivers every sample with its own pressure and tilt rather than one per
message.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AccessNodeBuilder::set_custom_actions&lt;/code&gt; advertises &lt;code&gt;Action::CustomAction&lt;/code&gt;
itself, so a node’s custom actions are reachable rather than decorative.&lt;/li&gt;
&lt;li&gt;A stroke carrying a &lt;code&gt;StrokeStyle&lt;/code&gt;, dashed, dotted, or a custom cap or join,
reaches the screen from every canvas stroke primitive, not only &lt;code&gt;stroke_path&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;accessibility&quot;&gt;Accessibility&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SceneMinimap&lt;/code&gt; is operable.&lt;/strong&gt; Given an &lt;code&gt;on_click&lt;/code&gt; it is focusable, publishes
a &lt;code&gt;Role::Group&lt;/code&gt; node, advertises &lt;code&gt;Click&lt;/code&gt; and the four scroll actions, pans with
the arrow keys and recentres on &lt;code&gt;Home&lt;/code&gt; / &lt;code&gt;Enter&lt;/code&gt; / &lt;code&gt;Space&lt;/code&gt;. &lt;code&gt;access_readout&lt;/code&gt;
supplies the phrasing for where the viewport sits, as a &lt;code&gt;MinimapReadout&lt;/code&gt;, so it
can be localised.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;demos&quot;&gt;Demos&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cargo run -p scene-ink&lt;/code&gt;&lt;/strong&gt;, every pen sample through &lt;code&gt;ctx.coalesced()&lt;/code&gt;, a
&lt;code&gt;WetLayer&lt;/code&gt; that repaints alone, and a dried stroke interleaved between two
notes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cargo run -p scene-corkboard&lt;/code&gt;&lt;/strong&gt;, beats are &lt;code&gt;SceneCard&lt;/code&gt;s 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;changed&quot;&gt;Changed&lt;/h4&gt;
&lt;h5 id=&quot;breaking-changes&quot;&gt;Breaking changes&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PathItem::new(path)&lt;/code&gt;&lt;/strong&gt; takes one argument and derives its own bounds. Drop
the &lt;code&gt;local_bounds&lt;/code&gt; you were passing; if you were widening it to make a hairline
clickable, use &lt;code&gt;hit_stroke_width&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ItemFlags::NEGATIVE_Z_BEHIND_PARENT&lt;/code&gt; is removed.&lt;/strong&gt; It was declared and
documented but read by nothing, and &lt;code&gt;PaintKey&lt;/code&gt; is flat, so it could not have
been honoured without making the key hierarchical. Nothing observable changes;
delete the reference.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PointerSequence::has_deferred_grab()&lt;/code&gt; is now &lt;code&gt;has_deferred_grab_for(id)&lt;/code&gt;.&lt;/strong&gt;
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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SceneItem::shape_contains&lt;/code&gt; and &lt;code&gt;clone_shape_test&lt;/code&gt; are gone&lt;/strong&gt;, replaced by
&lt;code&gt;shape() -&amp;gt; ItemShape&lt;/code&gt;. An item that overrode either overrides &lt;code&gt;shape()&lt;/code&gt; now;
the default returns the item’s &lt;code&gt;local_bounds&lt;/code&gt;, which is what an item that
overrode neither already got.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SceneItem::set_fill&lt;/code&gt; / &lt;code&gt;set_stroke&lt;/code&gt; return &lt;code&gt;AppearanceWrite&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/strong&gt; instead of
&lt;code&gt;bool&lt;/code&gt;, carrying the value they overwrote, the only place a journal can get it
from. &lt;code&gt;AppearanceWrite::Refused&lt;/code&gt; is the default, and is what an item with no
such slot should keep returning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SceneItemA11yContext&lt;/code&gt; publishes scene coordinates.&lt;/strong&gt; &lt;code&gt;screen_bounds&lt;/code&gt;,
&lt;code&gt;local_to_screen&lt;/code&gt;, &lt;code&gt;advertised_bounds&lt;/code&gt; and &lt;code&gt;bounds_space&lt;/code&gt; are replaced by
&lt;code&gt;scene_bounds&lt;/code&gt; and &lt;code&gt;local_to_scene&lt;/code&gt;; 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. &lt;code&gt;A11yBoundsSpace&lt;/code&gt; is removed with them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;item_change_signal&lt;/code&gt; carries a &lt;code&gt;SceneChange&lt;/code&gt;&lt;/strong&gt;, the change plus its
transaction envelope, rather than a bare &lt;code&gt;ItemChange&lt;/code&gt;. Observers read
&lt;code&gt;notification.change&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ItemChange&lt;/code&gt; is no longer &lt;code&gt;Copy&lt;/code&gt; or &lt;code&gt;PartialEq&lt;/code&gt;&lt;/strong&gt; (it carries owned values
now) and gains &lt;code&gt;PlacementChanged&lt;/code&gt;, &lt;code&gt;ItemReplaced&lt;/code&gt;, &lt;code&gt;HandlersChanged&lt;/code&gt;,
&lt;code&gt;MeasuredSizeChanged&lt;/code&gt; and &lt;code&gt;SizePolicyChanged&lt;/code&gt;. &lt;code&gt;TransformChanged&lt;/code&gt;,
&lt;code&gt;PayloadChanged&lt;/code&gt; and &lt;code&gt;AppearanceChanged&lt;/code&gt; carry both sides of the value they
replaced.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Path::commands&lt;/code&gt; is a method.&lt;/strong&gt; Read with &lt;code&gt;commands()&lt;/code&gt;, append with &lt;code&gt;push&lt;/code&gt;,
build from a vector with &lt;code&gt;Path::from_commands&lt;/code&gt;. The field backs a rolling
content stamp that a public &lt;code&gt;Vec&lt;/code&gt; could not be kept in step with.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PointerSample::coalesced&lt;/code&gt; is a &lt;code&gt;Vec&amp;lt;CoalescedSample&amp;gt;&lt;/code&gt;&lt;/strong&gt;, not a tuple, each
batched position keeps its own axes, which the tuple dropped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PenPacket::time: EventTime&lt;/code&gt; is replaced by &lt;code&gt;device_time_ms: Option&amp;lt;u32&amp;gt;&lt;/code&gt;&lt;/strong&gt;,
the device’s own counter rather than a stamp with an unknown epoch, and
&lt;code&gt;ToolEvent::Frame&lt;/code&gt; carries &lt;code&gt;time_ms&lt;/code&gt;. A backend reports what the device said;
&lt;code&gt;back_date&lt;/code&gt; places the batch on the tree’s timeline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;#[non_exhaustive]&lt;/code&gt; on the types the crate hands &lt;em&gt;to&lt;/em&gt; consumer code&lt;/strong&gt;, so a
new field is not a source break next time: &lt;code&gt;ItemChange&lt;/code&gt;, &lt;code&gt;SceneLayer&lt;/code&gt;,
&lt;code&gt;MagnetVisualState&lt;/code&gt;, &lt;code&gt;MagnetRef&lt;/code&gt;, &lt;code&gt;MagnetConnection&lt;/code&gt;, &lt;code&gt;MagnetSnap&lt;/code&gt;,
&lt;code&gt;MagnetMarker&lt;/code&gt;, &lt;code&gt;MagnetFeedback&lt;/code&gt;, &lt;code&gt;SceneItemPaintContext&lt;/code&gt; and
&lt;code&gt;SceneItemA11yContext&lt;/code&gt;. A &lt;code&gt;match&lt;/code&gt; gains a &lt;code&gt;_&lt;/code&gt; arm; a struct literal becomes the
type’s &lt;code&gt;new&lt;/code&gt; constructor, which every one of them now has, with its fields
still public.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WidgetPlacement&lt;/code&gt; gained a &lt;code&gt;dormant&lt;/code&gt; field and is now &lt;code&gt;#[non_exhaustive]&lt;/code&gt;.&lt;/strong&gt;
A &lt;code&gt;place_children&lt;/code&gt; implementation reads and writes the slice it is handed and
is unaffected. What breaks is a struct literal, &lt;code&gt;WidgetPlacement { id, origin, size }&lt;/code&gt;, which becomes &lt;code&gt;WidgetPlacement::new(id, origin, size)&lt;/code&gt;. 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 &lt;code&gt;culls_children&lt;/code&gt; returns
&lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SceneCard&lt;/code&gt; has one method per slot.&lt;/strong&gt; &lt;code&gt;header_id&lt;/code&gt;, &lt;code&gt;header_boxed&lt;/code&gt;,
&lt;code&gt;header_trailing_id&lt;/code&gt;, &lt;code&gt;header_trailing_boxed&lt;/code&gt;, &lt;code&gt;body_id&lt;/code&gt; and &lt;code&gt;body_boxed&lt;/code&gt; are
gone; &lt;code&gt;header&lt;/code&gt;, &lt;code&gt;header_trailing&lt;/code&gt; and &lt;code&gt;body&lt;/code&gt; each take a widget, a
&lt;code&gt;Box&amp;lt;dyn Widget&amp;gt;&lt;/code&gt; or a &lt;code&gt;WidgetId&lt;/code&gt;. Drop the suffix, &lt;code&gt;.body_id(id)&lt;/code&gt; becomes
&lt;code&gt;.body(id)&lt;/code&gt;. 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;performance&quot;&gt;Performance&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A pan costs the viewport, not the model.&lt;/strong&gt; One pan sample over a scene with a
50 000-item &lt;strong&gt;lightweight&lt;/strong&gt; off-screen tail went from &lt;strong&gt;16 ms to 1.8 µs&lt;/strong&gt;,
what a pan over an &lt;em&gt;empty&lt;/em&gt; scene costs. The two hit snapshots are pure
functions of the model, so they are built once and patched per item from the
change stream instead of being rebuilt on every sample.
(&lt;code&gt;crates/teksilo-scene/tests/pan_scaling_probe.rs&lt;/code&gt;.)
A pan over off-screen &lt;strong&gt;heavyweight cards&lt;/strong&gt; 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.
(&lt;code&gt;crates/teksilo-scene/tests/heavyweight_retention_probe.rs&lt;/code&gt; prints both.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Off-screen cards park.&lt;/strong&gt; At 50 000 of them the published accessibility node
count and the Tab-stop count equal the on-screen counts exactly, and the
accessibility walk no longer grows with the tail. A card carrying focus or a
live pointer is pinned wherever the camera goes.
(&lt;code&gt;crates/teksilo-scene/tests/heavyweight_retention_probe.rs&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Moving one item costs the moved subtree, not the scene.&lt;/strong&gt;
&lt;code&gt;Scene::set_local_pos&lt;/code&gt; walks a kept adjacency list instead of rebuilding a
scene-wide parent map on every move.
(&lt;code&gt;crates/teksilo-scene/tests/mutation_scaling_probe.rs&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A hover no longer costs the selection.&lt;/strong&gt; &lt;code&gt;Scene::selection_roots&lt;/code&gt; 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 &lt;code&gt;selection_roots&lt;/code&gt;
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.
(&lt;code&gt;crates/teksilo-scene/tests/selection_roots_scaling_probe.rs&lt;/code&gt;,
&lt;code&gt;transform_scaling_probe.rs&lt;/code&gt; print the tables.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A growing wet stroke is no longer quadratic.&lt;/strong&gt; The renderer’s path-mask cache
keys on a rolling content stamp, so a cache hit does not scale with the path’s
length. (&lt;code&gt;crates/teksilo-render/tests/wet_stroke_cost.rs&lt;/code&gt;.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;scene&quot;&gt;Scene&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;An &lt;code&gt;item_change_signal&lt;/code&gt; observer may read and write the scene.&lt;/strong&gt; Changes are
queued and drained after the mutation’s borrow drops, the &lt;code&gt;teksilo-data&lt;/code&gt;
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 &lt;code&gt;CascadeBudget&lt;/code&gt; bounds one drain’s
observer-generated deliveries (100 000 by default, flat, a knob on
&lt;code&gt;SceneModel::set_cascade_budget&lt;/code&gt;), and its diagnostic names the subject that
piled up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SceneListAdapter&lt;/code&gt; keeps each row’s &lt;code&gt;ItemId&lt;/code&gt;.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ItemChange::VisibilityChanged&lt;/code&gt; is emitted by &lt;strong&gt;every&lt;/strong&gt; door that flips
&lt;code&gt;IS_VISIBLE&lt;/code&gt;, not only &lt;code&gt;set_flag&lt;/code&gt;, 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.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Scene::set_z&lt;/code&gt; ignores a write only when the entry already holds that exact
value. Its old &lt;code&gt;f32::EPSILON&lt;/code&gt; test never fired at 1e6 and swallowed millions of
distinct floats near zero, including the midpoints &lt;code&gt;z_between&lt;/code&gt; hands out, so a
caller was told there was room and then got a silent no-op.&lt;/li&gt;
&lt;li&gt;A rotation applied by &lt;code&gt;apply_transform_delta&lt;/code&gt; that also moves the item now
emits &lt;code&gt;TransformChanged&lt;/code&gt; as well as &lt;code&gt;LocalPosChanged&lt;/code&gt;; &lt;code&gt;set_transform&lt;/code&gt; ignores
a write that changes nothing.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ItemChange::HandlersChanged&lt;/code&gt; carries the handler sets it replaced.
&lt;code&gt;handlers_mut&lt;/code&gt; cannot know what the caller does with the &lt;code&gt;&amp;amp;mut&lt;/code&gt; it hands out,
so it announces without recording.&lt;/li&gt;
&lt;li&gt;A geometry constraint returning &lt;code&gt;Adjust&lt;/code&gt; 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 &lt;code&gt;NaN&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SceneModel::constrain_move&lt;/code&gt; / &lt;code&gt;constrain_frame&lt;/code&gt; called inside an open write
scope panic naming the rule instead of reporting &lt;code&gt;RefCell already mutably borrowed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An &lt;code&gt;Over&lt;/code&gt; item occludes a card only if it would act on a press.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A marquee no longer takes screen-pinned items.&lt;/strong&gt; &lt;code&gt;commit_marquee&lt;/code&gt; runs
through &lt;code&gt;Scene::items_in_region&lt;/code&gt;, 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SceneView::retention_margin(px)&lt;/code&gt;&lt;/strong&gt; 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
&lt;code&gt;current_retention_margin&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;platform&quot;&gt;Platform&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PlatformWindow::reconfigure_surface&lt;/code&gt; answers &lt;code&gt;bool&lt;/code&gt; rather than &lt;code&gt;()&lt;/code&gt;:
&lt;code&gt;false&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FrameOutcome&lt;/code&gt; gains &lt;code&gt;DisplayLost&lt;/code&gt;, distinct from &lt;code&gt;Error&lt;/code&gt; and
&lt;code&gt;NeedsReconfigure&lt;/code&gt; because it is terminal: reconfiguring or redrawing after
it spins the loop.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;fixed&quot;&gt;Fixed&lt;/h4&gt;
&lt;h5 id=&quot;scene-1&quot;&gt;Scene&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;SceneView&lt;/code&gt; with selection or magnetism switched on can be panned with a
finger.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A won pan no longer fires the claimant’s own &lt;code&gt;on_tap&lt;/code&gt;&lt;/strong&gt;, panning a list with
a finger used to tap a row on the way past.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A revoked drag no longer leaves the item displaced.&lt;/strong&gt; The scene had no
&lt;code&gt;PointerCancel&lt;/code&gt; arm at all, so a cancelled touch or a lost capture left the
item wherever the last sample put it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leaving a &lt;code&gt;SceneView&lt;/code&gt; clears what leaving it should clear.&lt;/strong&gt; Its
&lt;code&gt;PointerLeave&lt;/code&gt; arm had never run, so an item stayed hovered for ever, the
tooltip stayed armed, and the cursor stayed on &lt;code&gt;Grab&lt;/code&gt; with the pointer outside
the view.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two &lt;code&gt;ItemFlags&lt;/code&gt; do what their documentation says.&lt;/strong&gt; An item without
&lt;code&gt;IS_VISIBLE&lt;/code&gt; is no longer clickable or draggable, and one without &lt;code&gt;IS_ENABLED&lt;/code&gt;
no longer eats a click.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Paint order and hit order agree.&lt;/strong&gt; Equal-&lt;code&gt;z&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A card’s published rectangle tracks the camera.&lt;/strong&gt; A heavyweight item’s
accessibility bounds were its arena rectangle in scene coordinates while a
lightweight item’s were projected to the screen, so the two tiers disagreed and
a pan, zoom or rotation moved neither. Both are published in scene space now,
under one declared camera transform, and an accessibility hit-test at the
painted position finds the card.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A selection change no pointer made&lt;/strong&gt;, “select all”, a search result, or the
other pane of a shared &lt;code&gt;SceneSelection&lt;/code&gt;, reaches the published accessibility
tree. Both panes kept describing the previous selection, at its previous
position.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An item’s height can be changed from assistive technology.&lt;/strong&gt; &lt;code&gt;Increment&lt;/code&gt; on a
“Resize top” or “Resize bottom” slider reported the action handled, announced
an unchanged number and moved nothing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Esc&lt;/code&gt; cancels a transform the pointer started&lt;/strong&gt;, not only one begun from the
keyboard.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A revoked contact ends the gesture through the cancel path&lt;/strong&gt;: &lt;code&gt;on_end&lt;/code&gt; fires
with &lt;code&gt;TransformOutcome::Cancelled&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TransformConfig::min_size&lt;/code&gt; floors only the axes the dragged handle drives.
Dragging the bottom edge of a 2 × 100 hairline used to widen it to 4.&lt;/li&gt;
&lt;li&gt;A geometry constraint that accepts a change leaves a magnetised drag alone on a
&lt;strong&gt;rotated&lt;/strong&gt; item; the comparison deciding whether the rule had overruled the
magnet was exact across a rotation round-trip that is not bit-exact.&lt;/li&gt;
&lt;li&gt;A banded region, a lasso drawn as a stroke, is answered in the frame where
its band is round, so an anisotropic scale no longer lets a containment query
pick an item the matching shape query then rejects. A zero-length segment, and
an open polyline’s phantom closing chord, are both handled.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;the-minimap&quot;&gt;The minimap&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It stays inside its own frame.&lt;/strong&gt; Zooming in projected the viewport rectangle
outside the widget, and nothing clipped the result, so the minimap painted over
its neighbours, a caller-supplied border by half its own width, unclamped. The
projection now runs through the union of content and viewport, and the widget
clips its own paint.&lt;/li&gt;
&lt;li&gt;Its content extent is computed in one place; paint and click used to carry
separate copies of the same formula.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;rendering-and-canvas&quot;&gt;Rendering and canvas&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A dashed or dotted stroke reaches the screen.&lt;/strong&gt; &lt;code&gt;stroke_rect&lt;/code&gt;,
&lt;code&gt;stroke_rounded_rect&lt;/code&gt;, &lt;code&gt;stroke_circle&lt;/code&gt;, &lt;code&gt;stroke_ellipse&lt;/code&gt; and &lt;code&gt;draw_line&lt;/code&gt; all
accepted a &lt;code&gt;StrokeStyle&lt;/code&gt; and dropped it, along with &lt;code&gt;line_cap&lt;/code&gt; and &lt;code&gt;line_join&lt;/code&gt;.
A styled stroke now routes to the tier that carries the pattern, and a solid
one keeps its cheaper tier.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The renderer no longer paints what hit-testing cannot see.&lt;/strong&gt; A path opening
with an arc was rasterised with an injected &lt;code&gt;MoveTo&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Path::transformed&lt;/code&gt; and &lt;code&gt;Path::flatten&lt;/code&gt; agree about where a subpath begins.
They did not for an arc following a &lt;code&gt;Close&lt;/code&gt;, so rotating a two-loop lasso
changed what it selected.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;input&quot;&gt;Input&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pen samples are spaced by the device’s own clock.&lt;/strong&gt; Both pen backends stamped
zero and the translator then stamped one instant across a whole drained batch,
so velocity, smoothing and prediction were all computed from zero spacing. On
Windows the batch was also genuinely one packet per message, the history
buffer was never drained.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PointerSample::coalesced&lt;/code&gt; reaches a handler.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;platform-1&quot;&gt;Platform&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A compositor that exits no longer takes the application down as a crash.&lt;/strong&gt;
When the display server goes away, the window surface stops answering for the
adapter it was matched against, and the next &lt;code&gt;Surface::configure&lt;/code&gt; failed
inside wgpu’s default error handler, which panics. What the user saw was
&lt;code&gt;wgpu error: Validation Error / In Surface::configure / Surface does not support the adapter&apos;s queue family&lt;/code&gt;, and what that reads like is a GPU
mismatch. It is not one: &lt;code&gt;request_adapter&lt;/code&gt; 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 &lt;code&gt;Surface::configure&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;The same event reaching winit first is no longer a panic either. Both Linux
backends report a failed dispatch or flush as &lt;code&gt;ExitFailure(errno)&lt;/code&gt;, and
&lt;code&gt;run&lt;/code&gt; treated every error alike. A lost display connection now exits
cleanly with one line naming the cause; &lt;code&gt;NotSupported&lt;/code&gt;, &lt;code&gt;Os&lt;/code&gt; and
&lt;code&gt;RecreationAttempt&lt;/code&gt; stay fatal, since those are genuine startup faults where
a backtrace is the useful answer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Opening a window no longer kills a GNOME session on a machine with no
touchscreen.&lt;/strong&gt; The Wayland drag-and-drop backend asked the seat for its
pointer and its touch as soon as a window attached, on the reasoning that a
seat lacking the capability would hand back a proxy that never emits. The
protocol says otherwise: &lt;code&gt;wl_seat::get_touch&lt;/code&gt; without the touch capability is
a &lt;code&gt;missing_capability&lt;/code&gt; 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
&lt;code&gt;MetaWaylandTouch&lt;/code&gt; whose list head is still all-zero, because the &lt;code&gt;wl_list&lt;/code&gt;
is initialised only when a touch device appears, and writes through it.
gnome-shell takes SIGSEGV, and since the compositor &lt;em&gt;is&lt;/em&gt; 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 &lt;code&gt;wl_seat::capabilities&lt;/code&gt;
event instead, and released when a capability is withdrawn, the press
serials &lt;code&gt;start_drag&lt;/code&gt; 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.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;known-limitations&quot;&gt;Known limitations&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;An assistive-technology probe and the pointer still disagree over an
&lt;em&gt;interactive&lt;/em&gt; scene overlay.&lt;/strong&gt; &lt;code&gt;accesskit_consumer&lt;/code&gt; 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
&lt;em&gt;pointer&lt;/em&gt; 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 &lt;code&gt;docs/teksilo-scene-a11y.md&lt;/code&gt; and pinned by
&lt;code&gt;what_the_press_claiming_rule_does_and_does_not_reconcile&lt;/code&gt;, so it cannot drift
silently.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Full Changelog&lt;/strong&gt;: https://github.com/FernTech-EU/teksilo/compare/v0.11.0…v0.12.0&lt;/p&gt;
</content>
  </entry>
  
  
  
  <entry>
    <title>Teksilo 0.11.0</title>
    <id>https://github.com/FernTech-EU/teksilo/releases/tag/v0.11.0</id>
    <link href="https://github.com/FernTech-EU/teksilo/releases/tag/v0.11.0" />
    <published>2026-09-17T00:00:00Z</published>
    <updated>2026-09-17T00:00:00Z</updated>
    <content type="html">&lt;p&gt;The &lt;code&gt;teksu!&lt;/code&gt; DSL reaches the code applications are actually made of: a call to
your own &lt;code&gt;fn row(..) -&amp;gt; impl Widget&lt;/code&gt; 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 &lt;code&gt;WidgetId&lt;/code&gt; or a widget.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h4 id=&quot;added&quot;&gt;Added&lt;/h4&gt;
&lt;h5 id=&quot;teksu&quot;&gt;teksu&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;A lowercase identifier that continues into a call, a method chain or an index
at body position is a child: &lt;code&gt;VStack { my_row(x) }&lt;/code&gt; and
&lt;code&gt;VStack { row(1).spacing(4.0) }&lt;/code&gt; compile. A lowercase identifier standing
alone is still the argument-free property.&lt;/li&gt;
&lt;li&gt;A keyword-rooted head is a child too: &lt;code&gt;self.row(x)&lt;/code&gt;, &lt;code&gt;Self::header()&lt;/code&gt;,
&lt;code&gt;crate::ui::header()&lt;/code&gt;, &lt;code&gt;super::row()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#{ expr }&lt;/code&gt; carries a widget value as well as a &lt;code&gt;WidgetId&lt;/code&gt;, which is how a
Rust struct literal is passed at body position: &lt;code&gt;#{ Card { title: t } }&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;widgets&quot;&gt;Widgets&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;on_change&lt;/code&gt; on &lt;code&gt;Checkbox&lt;/code&gt;, &lt;code&gt;Toggle&lt;/code&gt;, &lt;code&gt;RadioButton&lt;/code&gt; and &lt;code&gt;Slider&lt;/code&gt;, taking the
value the activation produced and an &lt;code&gt;EventContext&lt;/code&gt;, so flipping one can send an intent, set the
theme or open a window, things a bare &lt;code&gt;Signal&lt;/code&gt; write cannot do, because an
observer receives only &lt;code&gt;&amp;amp;T&lt;/code&gt;. They fire for the pointer, for &lt;code&gt;Space&lt;/code&gt;, for an
assistive-technology &lt;code&gt;Click&lt;/code&gt;, and, for a checkbox inside a data-view row, for
&lt;code&gt;Space&lt;/code&gt; on that row. They do &lt;strong&gt;not&lt;/strong&gt; 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 &lt;code&gt;bool&lt;/code&gt; too, since activation
cycles &lt;code&gt;Checked&lt;/code&gt; ↔ &lt;code&gt;Unchecked&lt;/code&gt; only. &lt;code&gt;RadioButton&lt;/code&gt; reports only a real change,
so re-activating the selected button is silent. &lt;code&gt;Slider&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StandardListItem::on_checkbox_toggle&lt;/code&gt; and the &lt;code&gt;StandardTreeItem&lt;/code&gt; forwarder,
which make the embedded checkbox’s &lt;code&gt;on_change&lt;/code&gt; reachable, the canonical row
builds its own checkbox, so it was the one place the callback could not be
installed. To &lt;em&gt;read&lt;/em&gt; check state, &lt;code&gt;CheckedModel&lt;/code&gt; remains the answer: it is the
source of truth and it survives row recycling, which a per-row callback does
not.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;child_opt&lt;/code&gt; on every container that has &lt;code&gt;child&lt;/code&gt;, 38 of them, up from 7. A
bare &lt;code&gt;teksu!&lt;/code&gt; &lt;code&gt;if&lt;/code&gt; now works inside a single-child wrapper, not only inside a
stack.&lt;/li&gt;
&lt;li&gt;45 accumulator builders gained the plural twin that a &lt;code&gt;for&lt;/code&gt; loop needs:
&lt;code&gt;tabs&lt;/code&gt;, &lt;code&gt;static_tabs&lt;/code&gt;, &lt;code&gt;panes&lt;/code&gt;, &lt;code&gt;items&lt;/code&gt;, &lt;code&gt;actions&lt;/code&gt;, &lt;code&gt;lines&lt;/code&gt;, &lt;code&gt;rails&lt;/code&gt;,
&lt;code&gt;docks&lt;/code&gt;, &lt;code&gt;radios&lt;/code&gt;, &lt;code&gt;full_width_rows&lt;/code&gt;, &lt;code&gt;add_children&lt;/code&gt; and others.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;core&quot;&gt;Core&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Behaviour change.&lt;/strong&gt; The row-activation marker a widget publishes with
&lt;code&gt;BuildContext::set_keyboard_toggle&lt;/code&gt;, and the fallback passed to
&lt;code&gt;EventContext::row_space_activate&lt;/code&gt;, are now &lt;code&gt;Rc&amp;lt;dyn Fn(&amp;amp;mut EventContext)&amp;gt;&lt;/code&gt;
rather than &lt;code&gt;Rc&amp;lt;dyn Fn()&amp;gt;&lt;/code&gt;. &lt;code&gt;Space&lt;/code&gt; on a data view’s focused row therefore
runs with a context, which is what lets a row checkbox fire &lt;code&gt;on_change&lt;/code&gt; on
that path instead of only under the pointer.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Box&amp;lt;W&amp;gt;&lt;/code&gt; implements &lt;code&gt;Widget&lt;/code&gt; for any &lt;code&gt;W: Widget + ?Sized&lt;/code&gt;, so a boxed widget
goes wherever a widget goes and adds no arena node.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HandlerSet::merge_under&lt;/code&gt; composes two handler sets, the later declaration
winning.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;documentation&quot;&gt;Documentation&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;teksu!&lt;/code&gt; reference leads with the real widget catalog: worked examples for
&lt;code&gt;ListView&lt;/code&gt;, &lt;code&gt;TreeView&lt;/code&gt;, &lt;code&gt;TableView&lt;/code&gt;, &lt;code&gt;TabWidget&lt;/code&gt;, &lt;code&gt;FormLayout&lt;/code&gt;, &lt;code&gt;MenuList&lt;/code&gt;,
&lt;code&gt;Toolbar&lt;/code&gt;, &lt;code&gt;Switcher&lt;/code&gt; and &lt;code&gt;DockingLayout&lt;/code&gt;, and a section listing only what
genuinely does not work, with the compiler’s own text.&lt;/li&gt;
&lt;li&gt;The spec gains a &lt;strong&gt;Why there is no v4&lt;/strong&gt; section: what the DSL was measured to
cost, the four charges against it that measurement did not support, the three
silent-wrong-program bugs and the one grammar rule that did stand, and the two
exits that stay available if the question reopens. It replaces the standalone
decision note, which is removed.&lt;/li&gt;
&lt;li&gt;The binding trap is written down in both documents: binding names are one flat
namespace per block, so two bindings sharing a name alias, both attach sites
resolving to the later widget while the earlier one is built and attached
nowhere.&lt;/li&gt;
&lt;li&gt;A root &lt;code&gt;NOTICE&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;The trademark policy gains a &lt;strong&gt;Third-party trademarks&lt;/strong&gt; section: it names the
owners of macOS, Fluent, Material Design and Int UI, states that Teksilo is
neither affiliated with nor endorsed by any of them, and separates the
&lt;code&gt;fluent&lt;/code&gt; theme preset from Project Fluent, the unrelated localization system
behind Teksilo’s translations.&lt;/li&gt;
&lt;li&gt;The two bundled-font license files are corrected. &lt;code&gt;roboto-LICENSE.txt&lt;/code&gt; 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. &lt;code&gt;noto-LICENSE.txt&lt;/code&gt; named one project
and one year for two faces; it now carries the copyright line each font’s own
name table declares, &lt;code&gt;notofonts/arabic&lt;/code&gt; 2022 and &lt;code&gt;notofonts/hebrew&lt;/code&gt; 2024.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;changed&quot;&gt;Changed&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;CI runs &lt;code&gt;cargo teksilo-fmt --check&lt;/code&gt; over &lt;code&gt;crates&lt;/code&gt; and &lt;code&gt;examples&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FormLayout::lines&lt;/code&gt; takes &lt;code&gt;impl IntoTeksiChild&lt;/code&gt; in both columns; the field
column used to require &lt;code&gt;impl Widget&lt;/code&gt;, so a loop holding ids could not use it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;teksu-language-spec-v3.md&lt;/code&gt; is the design rationale and names
&lt;code&gt;teksu-macro-reference.md&lt;/code&gt; 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;removed&quot;&gt;Removed&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Behaviour change, breaking.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;StandardTreeItem::on_toggle&lt;/code&gt; / &lt;code&gt;on_toggle_rc&lt;/code&gt; are renamed
&lt;code&gt;on_chevron_toggle&lt;/code&gt; / &lt;code&gt;on_chevron_toggle_rc&lt;/code&gt;. They are the expand / collapse
control, and the name became ambiguous the moment a row gained
&lt;code&gt;on_checkbox_toggle&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;add_child&lt;/code&gt;, &lt;code&gt;child_id&lt;/code&gt;, &lt;code&gt;child_boxed&lt;/code&gt; and every &lt;code&gt;*_id&lt;/code&gt; slot twin, 80 methods.
Call the slot by its own name instead: &lt;code&gt;.child(id)&lt;/code&gt;, &lt;code&gt;.content(id)&lt;/code&gt;,
&lt;code&gt;.header(id)&lt;/code&gt;, &lt;code&gt;.pane(id)&lt;/code&gt;. Every widget-accepting slot method takes
&lt;code&gt;impl IntoTeksiChild&lt;/code&gt;, which is implemented for &lt;code&gt;WidgetId&lt;/code&gt; and for every
&lt;code&gt;Widget&lt;/code&gt;. Not affected: &lt;code&gt;shortcut_id&lt;/code&gt;, &lt;code&gt;static_tab_with_id&lt;/code&gt;, the alternate
constructors (&lt;code&gt;from_id&lt;/code&gt;, &lt;code&gt;new_id&lt;/code&gt;, &lt;code&gt;around_id&lt;/code&gt;, &lt;code&gt;custom_id&lt;/code&gt;,
&lt;code&gt;with_child_id&lt;/code&gt;), and &lt;code&gt;Breadcrumb::item_id&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;WidgetBuilder::dim_when_inactive&lt;/code&gt; and &lt;code&gt;dim_when_inactive_default&lt;/code&gt;. Wrap
instead: &lt;code&gt;DimWhenInactive::new().factor(f).child(w)&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Checkbox::labels_hidden(bool)&lt;/code&gt; is now &lt;code&gt;Checkbox::labelled_externally()&lt;/code&gt;, the
name and shape &lt;code&gt;Toggle&lt;/code&gt; already used for the same thing: the control’s
accessible name comes from an ancestor, so it renders no label of its own.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;FormLayout::line_ids&lt;/code&gt; no longer adds one row from two ids; it takes an
iterator of &lt;code&gt;(label_id, field_id)&lt;/code&gt; pairs and adds a row per pair. The
single-row form is gone because &lt;code&gt;line&lt;/code&gt; takes &lt;code&gt;impl IntoTeksiChild&lt;/code&gt; and accepts
a &lt;code&gt;WidgetId&lt;/code&gt; directly.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;fixed&quot;&gt;Fixed&lt;/h4&gt;
&lt;h5 id=&quot;platform&quot;&gt;Platform&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Teksilo opens a window on a machine with no Vulkan driver.&lt;/strong&gt; OpenGL is the
only backend such a machine has left, an older GPU, a VM whose guest driver
stops at GL, and it was never handed the platform’s display connection, so it
could render offscreen but never present to a window. Startup died before the
first frame with &lt;code&gt;incompatible_surface_backends: GL&lt;/code&gt;. Confirmed fixed on both
Wayland and X11 with the Vulkan driver removed.&lt;/li&gt;
&lt;li&gt;Adapter selection is a search rather than a single request. An adapter that
enumerates but cannot open a device no longer ends the process, and an
explicit software fallback is tried before giving up, the resilience the
offscreen test device already had, and the window path did not.&lt;/li&gt;
&lt;li&gt;wgpu’s own environment variables take effect: &lt;code&gt;WGPU_BACKEND&lt;/code&gt;,
&lt;code&gt;WGPU_POWER_PREF&lt;/code&gt; and the rest were silently ignored, leaving no way to move
off a backend whose driver is the problem.&lt;/li&gt;
&lt;li&gt;The remaining “no GPU” failure names the backends tried, the errors each gave,
and what to install, in place of a &lt;code&gt;Debug&lt;/code&gt;-printed wgpu struct.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;rendering&quot;&gt;Rendering&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Neither atlas asks for a texture the device cannot allocate. Both grow toward
a 4096-pixel ceiling that is this renderer’s own, not a fact about the
hardware, and a downlevel device can sit below it; the path atlas now caps its
growth to what the device reports, and a glyph atlas that arrives oversized is
skipped rather than failing the frame.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;core-1&quot;&gt;Core&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Behaviour change.&lt;/strong&gt; A builder method with no inherent twin on
&lt;code&gt;WidgetWithHandlers&lt;/code&gt; wrapped an already-wrapped widget, and only the outer
handler set reached the node, so &lt;code&gt;.on_tap(cb).clips_children_on(true)&lt;/code&gt; never
fired. Handler sets at any depth now arrive.&lt;/li&gt;
&lt;li&gt;Chaining past &lt;code&gt;dim_when_inactive&lt;/code&gt; retargeted the rest of the chain at the
wrapper, so &lt;code&gt;VStack::new().dim_when_inactive(0.7).child(a).child(b)&lt;/code&gt; built
&lt;code&gt;DimWhenInactive &amp;gt; b&lt;/code&gt; and lost the stack and &lt;code&gt;a&lt;/code&gt;. The method is gone;
&lt;code&gt;teksilo-teksu-guard&lt;/code&gt; fails the build if a &lt;code&gt;WidgetBuilder&lt;/code&gt; method returns a
foreign wrapper again.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ArrowLeft reaches a text editor again when a second one is mounted beside
it.&lt;/strong&gt; The dispatch root reads the inline-start arrow as “back toward the
parent overlay” once two or more non-host overlays are stacked, and it counted
every band alike. Each mounted &lt;code&gt;RichTextEditor&lt;/code&gt; keeps one full-viewport
affordance host in the &lt;code&gt;TextAffordance&lt;/code&gt; 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 &lt;code&gt;OverlayBand::Standard&lt;/code&gt; overlays only, the
same line &lt;code&gt;OverlayBand::dismissed_by_outside_press&lt;/code&gt; already draws for presses.
A submenu over a mounted affordance still closes on the back key.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;teksu-1&quot;&gt;teksu&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;teksilo-fmt&lt;/code&gt; 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Full Changelog&lt;/strong&gt;: https://github.com/FernTech-EU/teksilo/compare/v0.10.0…v0.11.0&lt;/p&gt;
</content>
  </entry>
  
  
  
  <entry>
    <title>Teksilo 0.10.0</title>
    <id>https://github.com/FernTech-EU/teksilo/releases/tag/v0.10.0</id>
    <link href="https://github.com/FernTech-EU/teksilo/releases/tag/v0.10.0" />
    <published>2026-09-15T00:00:00Z</published>
    <updated>2026-09-15T00:00:00Z</updated>
    <content type="html">&lt;p&gt;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, &lt;code&gt;WebViewHandle&lt;/code&gt; gained a required method and &lt;code&gt;MemoryShared&lt;/code&gt; is
non-exhaustive;
&lt;a href=&quot;docs/porting-widgets-to-the-pointer-model.md&quot;&gt;docs/porting-widgets-to-the-pointer-model.md&lt;/a&gt;
is the contract for a widget crossing over.&lt;/p&gt;
&lt;h4 id=&quot;added&quot;&gt;Added&lt;/h4&gt;
&lt;h5 id=&quot;touch-pen-and-density&quot;&gt;Touch, pen and density&lt;/h5&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Touch input, end to end.&lt;/strong&gt; A touchscreen’s contacts reach widgets as real
pointers with their own identities, and a finger scrolls, selects, reorders,
edits text, pinches, drags to and from the OS, and reaches a 24 dp target. Each
contact gets a fresh identity per press, so an OS that reuses slot numbers cannot
make two gestures look like one, and up to a documented cap of simultaneous
contacts are tracked at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pen and stylus.&lt;/strong&gt; Pressure, tilt, barrel rotation, the eraser end, the barrel
button, and proximity as a first-class state, a stylus hovers before it touches.
Wayland through &lt;code&gt;zwp_tablet_v2&lt;/code&gt;, Windows through a &lt;code&gt;WM_POINTER*&lt;/code&gt; window subclass;
no winit upgrade required.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trackpad gestures.&lt;/strong&gt; Pinch, rotate and two-finger scroll reach the widget tree.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;TargetDensity&lt;/code&gt;, Compact, Comfortable and Touch.&lt;/strong&gt; 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 &lt;code&gt;Theme::with_density&lt;/code&gt; or
&lt;code&gt;WidgetTree::set_input_density&lt;/code&gt;; the shipped Material 3, Fluent and macOS presets
project their own dimensions onto the ladder rather than inheriting Teksilo’s.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A 24 dp conformance floor at every density, never scaled&lt;/strong&gt;, WCAG 2.2 SC 2.5.8
(level AA). A build-breaking test measures every named widget fixture against it
at all three densities.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Three hit mechanisms with disjoint jobs&lt;/strong&gt;, so a small control is reachable
without being redrawn: &lt;code&gt;Widget::hit_outset&lt;/code&gt; widens a target for one pointer kind
only, &lt;code&gt;Widget::target_regions&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kinetic scrolling.&lt;/strong&gt; A fling coasts and settles on the platform’s own curve,
Android’s &lt;code&gt;OverScroller&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Touch text editing&lt;/strong&gt; in every editing surface: the caret lands on the release,
a hold selects the word under the finger, two draggable handles adjust the range,
a magnifier shows the text under the handle, and a selection toolbar offers the
clipboard. &lt;code&gt;RichTextEditor&lt;/code&gt;, &lt;code&gt;CodeEditor&lt;/code&gt;, &lt;code&gt;PlainTextEditor&lt;/code&gt;, &lt;code&gt;LogView&lt;/code&gt;,
&lt;code&gt;TextInput&lt;/code&gt;, &lt;code&gt;PasswordField&lt;/code&gt;, &lt;code&gt;SearchField&lt;/code&gt;, &lt;code&gt;SpinBox&lt;/code&gt; and the date/time family.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A hold is a route.&lt;/strong&gt; Where a widget installs no long-press handler of its own,
the tree resolves a hold to the node’s context menu, else its tooltip, else
nothing, so a finger reaches both affordances that were previously mouse-only.
&lt;code&gt;LongPressRole&lt;/code&gt; overrides the choice per subtree.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;touch_action&lt;/code&gt; and &lt;code&gt;pan_claim&lt;/code&gt; node declarations&lt;/strong&gt;, the CSS &lt;code&gt;touch-action&lt;/code&gt;
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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A runtime kill switch.&lt;/strong&gt; With &lt;code&gt;InputTokens::touch_enabled&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Input tracing.&lt;/strong&gt; &lt;code&gt;TEKSILO_TRACE_INPUT=samples|gestures|all&lt;/code&gt; prints every
sample, every arbitration decision and every cancellation, guarded so that a
normal run formats nothing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;examples/touch_playground&lt;/code&gt;&lt;/strong&gt;, 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A Touch tab in &lt;code&gt;examples/widget_catalog&lt;/code&gt;&lt;/strong&gt;, and &lt;code&gt;--density compact|comfortable|touch&lt;/code&gt;
on the catalog itself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;docs/touch-verification.md&quot;&gt;docs/touch-verification.md&lt;/a&gt;&lt;/strong&gt;, the hardware
procedure, and &lt;a href=&quot;docs/touch-verification-signoff.md&quot;&gt;its sign-off sheet&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;automation&quot;&gt;Automation&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;An agent can drive touch, a stylus and two contacts.&lt;/strong&gt; New operations:
a whole multi-touch sequence in one call (reporting the arbitration after every
step), a two-finger pinch, a fling described in simulated time, a long press
held for the active profile’s own threshold, a pointer cancelled the way the
system cancels one, a query of every live pointer with its capture and
arbitration state, and a density switch. &lt;code&gt;inject_pointer&lt;/code&gt;, &lt;code&gt;inject_key&lt;/code&gt; and
&lt;code&gt;type_text&lt;/code&gt; take a &lt;code&gt;command&lt;/code&gt; modifier beside &lt;code&gt;ctrl&lt;/code&gt;, because a chord declared
&lt;code&gt;Ctrl+S&lt;/code&gt; resolves to ⌘S on macOS.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;accessibility&quot;&gt;Accessibility&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The target-size gate measures every shipped preset&lt;/strong&gt;, not the default theme
alone: Int UI, macOS, Fluent and Material 3, each at all three densities, with
light and dark compared for identical geometry. The fixtures moved into a
crate the theme crates can reach, so a preset is audited by the same 68
fixtures the default theme is.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;previewer&quot;&gt;Previewer&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--export-docs&lt;/code&gt; renders at a chosen density&lt;/strong&gt;: &lt;code&gt;--density=compact,touch&lt;/code&gt;
writes &lt;code&gt;docs/widgets/img/&amp;lt;slug&amp;gt;-touch.png&lt;/code&gt; beside the canonical image, and a
catalog page grows a &lt;code&gt;## Density&lt;/code&gt; section when one exists. Compact stays
canonical and keeps every existing filename, so nothing committed moves.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;teksilo_preview::PreviewPass&lt;/code&gt;, the density a preview renders at, and the
image-naming rule that follows from it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;documentation&quot;&gt;Documentation&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;docs/porting-widgets-to-the-pointer-model.md&lt;/code&gt;&lt;/strong&gt;, the contract for moving a
widget onto the pointer model, as numbered clauses, each stating the rule, how
to comply, and what checks it.&lt;/li&gt;
&lt;li&gt;Documented constants are asserted against the pages that document them. The
density ladder, all three gesture profiles and the kinetic constants are read
back from their own tables by a test, so a number lives in one place.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;changed&quot;&gt;Changed&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;text-document&lt;/code&gt; 1.12.2 and &lt;code&gt;text-typeset&lt;/code&gt; 1.11.1.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;core&quot;&gt;Core&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;The pointer-model behaviour changes.&lt;/strong&gt; Each is a token or a knob an application
can override, and none of them changes what a mouse does.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A mouse drag never pans.&lt;/strong&gt; Dragging the content of a scrollable surface
scrolls it only for a touch or a stylus; a mouse scrolls with its wheel, exactly
as before. Widen it per surface with &lt;code&gt;ScrollHandlingOptions::pan_devices&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A touch or pen drag of an &lt;em&gt;item&lt;/em&gt; waits for a long press.&lt;/strong&gt; Reordering a list
row, a grid tile, a tab or a table column with a finger means holding it first,
because the surface underneath has first refusal on a direct pointer’s press. A
mouse still drags from the first few pixels. &lt;code&gt;DragActivation&lt;/code&gt; on the node decides;
&lt;code&gt;Auto&lt;/code&gt; is the default and resolves per pointer kind.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A touch or pen press commits on the release.&lt;/strong&gt; Selection, activation and caret
placement happen when the contact lifts, not when it lands, so a press that
slides off its target commits nothing. A mouse commits on the press, unchanged.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A press outside an overlay dismisses it on the release, and the dismissing
press does not reach what is under it.&lt;/strong&gt; Dismissing a menu and activating the
control beneath it are now two presses, on every pointer kind.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;on_hover&lt;/code&gt; never fires for a touch contact,&lt;/strong&gt; and neither do &lt;code&gt;hover_within&lt;/code&gt;
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.
&lt;code&gt;InputTokens::reveal&lt;/code&gt; promotes those affordances to always-visible at the Touch
density, and the tree’s hold route reaches a tooltip regardless.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A pen in proximity hovers, and hover belongs to one pointer.&lt;/strong&gt; On a machine
with both a mouse and a stylus, each one’s hover follows its own device instead
of a single shared “hovered” notion.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;advance_time&lt;/code&gt; ticks gesture recognizers.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WidgetEvent::PointerDown&lt;/code&gt;, &lt;code&gt;PointerUp&lt;/code&gt;, &lt;code&gt;PointerMove&lt;/code&gt;, &lt;code&gt;PointerEnter&lt;/code&gt; and
&lt;code&gt;PointerLeave&lt;/code&gt; now carry &lt;code&gt;pointer: PointerInfo&lt;/code&gt;&lt;/strong&gt;, so a handler can tell a
finger from a stylus from a mouse without reaching for the context.
&lt;code&gt;PointerEnter&lt;/code&gt; and &lt;code&gt;PointerLeave&lt;/code&gt; become struct variants; a pattern that named
them bare now needs &lt;code&gt;{ .. }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PointerMove&lt;/code&gt; also carries &lt;code&gt;modifiers: Modifiers&lt;/code&gt;.&lt;/strong&gt; A drag reads Shift and
Ctrl from the move rather than from the press, so a modifier pressed mid-drag
reaches the widget.&lt;/li&gt;
&lt;li&gt;New constructors keep a mouse-describing call site to one line and default the
pointer to &lt;code&gt;PointerInfo::mouse&lt;/code&gt; at the epoch: &lt;code&gt;WidgetEvent::pointer_move_with&lt;/code&gt;,
&lt;code&gt;pointer_enter&lt;/code&gt;, &lt;code&gt;pointer_leave&lt;/code&gt;, beside the existing &lt;code&gt;pointer_down&lt;/code&gt;,
&lt;code&gt;pointer_up&lt;/code&gt;, &lt;code&gt;pointer_move&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WidgetEvent::Scroll::position&lt;/code&gt; and &lt;code&gt;PointerCancel::position&lt;/code&gt; are renamed
&lt;code&gt;window_position&lt;/code&gt;.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;GestureEvent::PinchChanged&lt;/code&gt; carries deltas, and says so.&lt;/strong&gt; &lt;code&gt;scale&lt;/code&gt; is the
factor since the &lt;em&gt;previous&lt;/em&gt; sample and &lt;code&gt;rotation&lt;/code&gt; the twist since the previous
sample, in radians; fold each in rather than assigning it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A drag handler is told which device carries the drag at every stage&lt;/strong&gt;,
including the per-layout tick. Those previously reported the mouse, so a finger
never got the wider auto-scroll band.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A coarse pointer’s drag preview is placed clear of the contact patch&lt;/strong&gt;; a
mouse’s is unchanged.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The widget under an inbound OS drag revises the OS accept state&lt;/strong&gt;, so the
cursor no longer promises a drop the target refuses. An OS drag aborted over a
second window of the same application tears that window’s session down, and a
drag exported from a finger on Wayland uses that finger’s press serial.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WindowOps::begin_os_drag&lt;/code&gt; and &lt;code&gt;ExternalDndGuard::begin_drag&lt;/code&gt; take the
device’s &lt;code&gt;PointerKind&lt;/code&gt;&lt;/strong&gt;, and both gain &lt;code&gt;set_drop_accepted&lt;/code&gt;.
&lt;code&gt;ExternalDragEvent&lt;/code&gt; gains a &lt;code&gt;Cancelled&lt;/code&gt; variant.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;TouchSelection::on_long_press&lt;/code&gt; takes a &lt;code&gt;pointer: PointerInfo&lt;/code&gt;.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WebViewHandle::set_input_passthrough&lt;/code&gt; is a new required trait method&lt;/strong&gt;, for
out-of-tree backends. &lt;code&gt;WebViewEvent::EngineFocusChanged&lt;/code&gt; is new.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;MemoryShared&lt;/code&gt; is &lt;code&gt;#[non_exhaustive]&lt;/code&gt;&lt;/strong&gt; and gained four fields.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;InspectorState::toggle()&lt;/code&gt; is public. New:
&lt;code&gt;teksilo_widgets::drop_target::{band_depth, region_at_floored, region_rect_floored}&lt;/code&gt;,
&lt;code&gt;styles::recipe_menu_item_style::menu_item_height&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;widgets&quot;&gt;Widgets&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nine surfaces pan under a finger&lt;/strong&gt; from one implementation: &lt;code&gt;ScrollArea&lt;/code&gt;, the
five data views and the three text surfaces, plus &lt;code&gt;Terminal&lt;/code&gt; and &lt;code&gt;SceneView&lt;/code&gt;,
which claim their own axes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The window chrome answers a finger.&lt;/strong&gt; A resize strip stays 6 dp of paint and
reaches the density’s target size for a coarse pointer; a hold on the title bar
asks the OS for its window menu where the platform has one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;SplitButton&lt;/code&gt; chevron takes a direct pointer’s press up to the conformance
floor&lt;/strong&gt;, borrowing from the action half; a mouse’s boundary stays where it is
painted.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;TableView&lt;/code&gt; header cell’s filter zone is the density’s target size&lt;/strong&gt;, and
the cell reports its three parts through &lt;code&gt;Widget::target_regions&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;DropTarget&lt;/code&gt;’s edge zones, and a docking pane’s five, are floored per
axis and capped at a third of the extent.&lt;/strong&gt; Not pointer-kind-gated: the acting
zone and the painted one must be one rectangle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;List and tree row minimum heights, combo-box dropdown rows, calendar
navigation arrows, menu rows and code-editor completion rows follow the
density ladder.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A finger’s tap on a &lt;code&gt;ToolBox&lt;/code&gt; header or a &lt;code&gt;RadioTile&lt;/code&gt; leaves it untinted&lt;/strong&gt;
rather than resting hovered.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A toast pauses on a press-and-hold and dismisses on a horizontal swipe&lt;/strong&gt; for a
coarse pointer, because hover-to-pause is unreachable with a finger.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every reorder a drag can do, a keyboard and a menu can do too&lt;/strong&gt;, WCAG 2.5.7.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WebView&lt;/code&gt; in the default &lt;code&gt;Native&lt;/code&gt; input mode&lt;/strong&gt; answers a press itself and
revokes the pointer’s interaction, declares no touch default over its rectangle,
and opts out of hit widening; &lt;code&gt;Transparent&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The debug inspector opens from &lt;code&gt;InspectorState::toggle()&lt;/code&gt; or a corner-grip
hold&lt;/strong&gt;, its pressed rows take the density’s target size, and it has a Pointers
tab listing every pointer declaration in the application’s tree.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The widget previewer takes &lt;code&gt;--density&lt;/code&gt;, switches it live, and its canvas zoom
works&lt;/strong&gt; (pinch and Ctrl-wheel).&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;terminal&quot;&gt;Terminal&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A finger pans the scrollback with a kinetic hand-off&lt;/strong&gt;, quantised to lines so
a sub-line sample is banked rather than dropped. Double- and triple-tap select
the word and the line, a hold opens the terminal’s own menu, and selection
handles snap to cell boundaries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A finger is never reported to the child program as a mouse.&lt;/strong&gt; Touch reporting
is its own policy, separate from the mouse reporting a program requests.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;fixed&quot;&gt;Fixed&lt;/h4&gt;
&lt;h5 id=&quot;rendering&quot;&gt;Rendering&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU-rasterized paths, which is every SVG icon, draw pixel-exact.&lt;/strong&gt; A Tier-3
path’s coverage mask is rasterized on its own integer grid, but its quad was
placed at &lt;code&gt;bounds × scale_factor&lt;/code&gt; and sized to &lt;code&gt;ceil&lt;/code&gt; 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 &lt;strong&gt;48 % coverage instead of 100 %&lt;/strong&gt;, 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
&lt;code&gt;QuadVertex::from_glyph_quad_transformed&lt;/code&gt; has always given glyphs, which is why
text was sharp and icons were not. The rect now travels with the raster as one
&lt;code&gt;PathPlacement&lt;/code&gt;, 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two path-atlas entries no longer share an edge.&lt;/strong&gt; The shelf packer placed them
flush, so an edge fragment of any quad that is not pixel-exact on its region
read a texel belonging to the &lt;em&gt;next icon&lt;/em&gt; rather than transparent space. Each
entry now reserves a one-texel transparent gutter, matching the glyph atlas.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;core-1&quot;&gt;Core&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A two-finger pinch reaches the zoom it asked for instead of running into
&lt;code&gt;max_zoom&lt;/code&gt;.&lt;/strong&gt; A spread to twice the starting span now leaves a &lt;code&gt;SceneView&lt;/code&gt; at
exactly twice, whatever the sample rate; before, each sample carried the ratio
to the &lt;em&gt;start&lt;/em&gt; of the gesture and the handler multiplied every one of them in,
so a single spread compounded to the product of its intermediate ratios.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A trackpad twist turns content by the angle the user twisted.&lt;/strong&gt; One degree of
rotation on the trackpad rotated a &lt;code&gt;SceneView&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GestureEvent::PinchChanged&lt;/code&gt; and &lt;code&gt;PinchPhase::Changed&lt;/code&gt; now document &lt;code&gt;scale&lt;/code&gt; and
&lt;code&gt;rotation&lt;/code&gt;: both are deltas against the previous sample, and &lt;code&gt;rotation&lt;/code&gt; is in
radians. Fold each sample in (&lt;code&gt;zoom *= scale&lt;/code&gt;, &lt;code&gt;rotation += rotation&lt;/code&gt;) rather
than assigning it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;TouchPinchRecognizer::scale&lt;/code&gt; and &lt;code&gt;rotation&lt;/code&gt; are renamed
&lt;code&gt;cumulative_scale&lt;/code&gt; and &lt;code&gt;cumulative_rotation&lt;/code&gt;.&lt;/strong&gt; They report the totals since
the gesture started, which is not what a &lt;code&gt;PinchChanged&lt;/code&gt; carries; the names now
say which of the two a caller is reading.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;PointerCancel&lt;/code&gt; for a touch contact reported the mouse, with no position&lt;/strong&gt;,
so an application branching on the cancelled pointer’s kind behaved wrongly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A container that preserves its children across a rebuild destroyed and rebuilt
its whole subtree instead&lt;/strong&gt;, whenever any builder method wrapped it. That covers
every &lt;code&gt;Switcher&lt;/code&gt;, &lt;code&gt;TabWidget&lt;/code&gt;, &lt;code&gt;SceneView&lt;/code&gt;, &lt;code&gt;DockingLayout&lt;/code&gt;, &lt;code&gt;Repeater&lt;/code&gt; and
&lt;code&gt;PopoverWidget&lt;/code&gt; with a handler attached.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A wrapped widget’s declared shortcuts never reached the registry&lt;/strong&gt;, so they
were missing from the rebinding UI as well as from the keyboard; a wrapped widget
that opts out of layout memoisation was memoised anyway; a wrapped title bar
stopped publishing its OS caption regions, a defect its own documentation had
written up as a rule. Wrapped dialog content stopped lending its title to its
shell and stopped directing initial focus; the context-menu key opened the
view’s menu rather than the selected row’s; a table read its body to assistive
technology before its header; a scene stopped grafting its items into the
application’s accessibility tree; empty tooltip content still raised a bubble.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A coarser density could lower a grip’s reach&lt;/strong&gt;: an outset and the miss-only
slop pass were combined in the wrong order.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A caret could be left outside a viewport that shrank&lt;/strong&gt;; a shrink now records a
reveal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two overlay-dismissal defects a mouse could feel&lt;/strong&gt;, and a hold dispatched
under a mouse identity that was never there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A target-size allow-list entry is held to the floor the audit judged against,
not to the generic 24 dp table.&lt;/strong&gt; Under a theme raising
&lt;code&gt;InputTokens::min_target_conformance&lt;/code&gt;, a &lt;code&gt;PinnedDp::ClearsFloor&lt;/code&gt; axis excused
the very failures the raised floor exists to report.
&lt;code&gt;TargetMeasurement&lt;/code&gt;/&lt;code&gt;TargetViolation&lt;/code&gt; carry the measured floor as
&lt;code&gt;conformance_floor&lt;/code&gt;, and every consumer reads it instead of re-deriving one.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;platform&quot;&gt;Platform&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Teksilo could not open a window at all on GLES-3.1 class hardware.&lt;/strong&gt; A
window asked its device for &lt;code&gt;wgpu::Limits::default()&lt;/code&gt;, 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 &lt;code&gt;downlevel_defaults&lt;/code&gt; 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;widgets-1&quot;&gt;Widgets&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A Fluent list or tree row can be clicked again.&lt;/strong&gt; The preset’s selection pill
spans the whole row and was hit-tested ahead of the row’s contents, so nothing
inside a Fluent row, a checkbox, a disclosure arrow, a trailing button, took
a press, by mouse or by finger.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A macOS switch and an icon button meet the 24 dp target floor.&lt;/strong&gt; Both are
drawn at the size the preset asks for, Apple’s 22 dp track and the 18 dp
compact icon button among them; the node around the chrome is what grew.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Three controls showed no pressed appearance for a pointer at all&lt;/strong&gt;, the
&lt;code&gt;ToolBox&lt;/code&gt; header, &lt;code&gt;RadioTile&lt;/code&gt;, and the calendar’s month/year cell, the last only
ever writing &lt;code&gt;false&lt;/code&gt; into its own state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A declared row height inside the row-metrics dead band was silently refused&lt;/strong&gt;,
so an exact-height list reported a different total height from a uniform one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A leaf row in a tree table no longer carries an invisible pointer target.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The previewer’s knob rows were unreachable by any pointer, mouse included&lt;/strong&gt;: an
infinite height cap inside a scroll area measured &lt;code&gt;inf&lt;/code&gt; tall, its &lt;code&gt;y&lt;/code&gt; resolved to
&lt;code&gt;NaN&lt;/code&gt;, and a rectangle containing &lt;code&gt;NaN&lt;/code&gt; contains no point.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;With the debug inspector installed, switching density destroyed the wrapped
application’s tree.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Four dimensions a shipped theme sets now reach the screen.&lt;/strong&gt; A table header
cell pads by the Fluent gutter (12 dp, not 8); a calendar’s navigation arrows
are drawn the size macOS asks for (20 dp, not 24); a search field’s suggestion
rows carry the macOS row gutters (8 x 3 dp, not 10 x 4). Each was written on
the theme’s recipe and discarded, because the widget measured the shipped
Int UI constant instead of asking the style. &lt;code&gt;TableStyle&lt;/code&gt;, &lt;code&gt;CalendarStyle&lt;/code&gt; and
&lt;code&gt;SearchFieldStyle&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A theme may draw a control below the 24 dp target floor without costing the
app its WCAG 2.2 SC 2.5.8 conformance.&lt;/strong&gt; A calendar’s navigation arrow keeps
whatever size the theme asks for and sits inside a box that reaches the floor,
so macOS’s 20 dp stepper is drawn at 20 dp and still answers a 24 dp press,
from a mouse as much as from a finger. Under Int UI, whose arrow is already
the floor, nothing moves at any density.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;terminal-1&quot;&gt;Terminal&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A terminal placed anywhere but the window’s top-left corner accepted no
pointer input at all.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The wheel scrolled the scrollback backwards&lt;/strong&gt;, and its report to the child
program named cell (0, 0) instead of the cell under the pointer, so a
full-screen program that splits its window scrolled the wrong pane.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dragging a selection handle moved the selection to the neighbouring row.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;previewer-1&quot;&gt;Previewer&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The toolbar’s Export PNG ignored the live density&lt;/strong&gt;, so exporting while the
previewer was set to Touch produced a Compact image, over the Compact
filename. The density now reaches both the render and the name.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;text&quot;&gt;Text&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A sentence cut at a paragraph’s end is the sentence, and bold lands on the
selection.&lt;/strong&gt; From text-document 1.12.2. Once anything had been typed earlier
in the document since the last deletion, a use case addressing a range read a
block start the keystrokes had left behind, so Cut and Copy at a paragraph’s
end took the paragraph break and the head of the next paragraph along with
the sentence, and bold, italic, replace and “make a list” landed the same
number of characters late. In a document holding a table the caret now also
reaches the last characters after it, and a selection to End includes them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--all-features&lt;/code&gt; builds.&lt;/strong&gt; Five &lt;code&gt;fonts-*&lt;/code&gt; features named a Noto face that is
not in the repository, and because &lt;code&gt;include_bytes!&lt;/code&gt; resolves at compile time,
enabling one was a hard build error, so those five features, both
&lt;code&gt;fonts-all&lt;/code&gt; meta-features, and any &lt;code&gt;--all-features&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dragging the caret handle moves the on-screen keyboard’s candidate window
with it.&lt;/strong&gt; Every editing surface reported the caret’s position when a press
placed it, and none did when a finger &lt;em&gt;dragged&lt;/em&gt; it, so composing Japanese,
Chinese or Korean after a handle drag put the candidate list at the caret’s
old position. &lt;code&gt;RichTextEditor&lt;/code&gt;, &lt;code&gt;CodeEditor&lt;/code&gt;, &lt;code&gt;PlainTextEditor&lt;/code&gt;, &lt;code&gt;TextInput&lt;/code&gt;,
&lt;code&gt;PasswordField&lt;/code&gt;, &lt;code&gt;SearchField&lt;/code&gt;, &lt;code&gt;SpinBox&lt;/code&gt; and the date/time family; the
assistive-technology route onto the same handle reports it too.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The code editor’s gutter returned from inside a clip scope when no text
backend was installed&lt;/strong&gt;, leaving the render frame unbalanced.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;PlainTextEditor&lt;/code&gt; could neither replace nor suppress the context menu its
documentation offered it&lt;/strong&gt;, and the &lt;code&gt;CodeEditor&lt;/code&gt; family gained the right-click
menu it never had.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--all-features&lt;/code&gt; builds.&lt;/strong&gt; Five &lt;code&gt;fonts-*&lt;/code&gt; features named a Noto face that is
not in the repository, and because &lt;code&gt;include_bytes!&lt;/code&gt; resolves at compile time,
enabling one was a hard build error, so those five features, both
&lt;code&gt;fonts-all&lt;/code&gt; meta-features, and any &lt;code&gt;--all-features&lt;/code&gt; 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;documentation-1&quot;&gt;Documentation&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;ScrollArea&lt;/code&gt; in the &lt;code&gt;Thin&lt;/code&gt; scroll-bar mode documented a keyboard route it
does not have.&lt;/strong&gt; The bar’s arrow / &lt;code&gt;Home&lt;/code&gt; / &lt;code&gt;End&lt;/code&gt; / &lt;code&gt;Page&lt;/code&gt; handlers sit on a node
that cannot take focus, under every mode, the same limit the
&lt;code&gt;ScrollBarPolicy::AlwaysOff&lt;/code&gt; documentation states from the other side.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PointerInfo::primary&lt;/code&gt;’s documentation described the wrong one of three
similarly-named things.&lt;/strong&gt; The field is the W3C &lt;em&gt;per-kind&lt;/em&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;StandardListItem&lt;/code&gt;’s documentation claimed the row reads its recipe’s
projected heights.&lt;/strong&gt; It projects the module constants instead; those two recipe
fields have no reader.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The soft-keyboard documentation claimed a touch-placed caret leaves a stale
IME area standing.&lt;/strong&gt; Each editing stack reports the area from its own touch path;
the unused core method is superseded rather than missing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two committed catalog images had gone stale&lt;/strong&gt;, one of them contradicting a
safety fix: the message-box preview showed &lt;strong&gt;Yes&lt;/strong&gt; as the accented default
after the widget had switched to &lt;strong&gt;No&lt;/strong&gt;, and the colour-picker preview predated
the widget growing its HSV row.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Catalog pages linked to rustdoc module pages that do not exist on docs.rs&lt;/strong&gt;,
because the module is crate-internal; each link now resolves to the nearest
published module.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;teksu&quot;&gt;teksu&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;teksu!&lt;/code&gt; bodies could not place &lt;code&gt;gesture_dead_zone&lt;/code&gt; or &lt;code&gt;keyboard_capture&lt;/code&gt;
before a child&lt;/strong&gt;; both failed with “no method named &lt;code&gt;child&lt;/code&gt;”. A compile-time
guard now keeps the DSL’s builder-method list complete.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;31 &lt;code&gt;teksu!&lt;/code&gt; UI fixtures had never run&lt;/strong&gt;, 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A bare child in any popover produced the generic error&lt;/strong&gt; instead of the slot
hint.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;known-limitations&quot;&gt;Known limitations&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The hardware sign-off is not done.&lt;/strong&gt; Everything above that a headless Linux
host can check is checked by the suite;
&lt;a href=&quot;docs/touch-verification.md&quot;&gt;docs/touch-verification.md&lt;/a&gt; is the procedure for
the rest, and its &lt;a href=&quot;docs/touch-verification-signoff.md&quot;&gt;sign-off sheet&lt;/a&gt; is empty.
One question in it can only be answered on a macOS trackpad: whether a positive
trackpad rotation delta should be negated at the platform seam.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A running application does not switch density because a finger arrived.&lt;/strong&gt;
&lt;code&gt;DensityPolicy::FollowLastPointer&lt;/code&gt; has an ingress and no writer; what a stray
tap should cost and when hysteresis commits are unanswered.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Overscroll is published but not painted.&lt;/strong&gt; &lt;code&gt;ScrollableAxes::overscroll&lt;/code&gt;
carries the value; nothing renders a stretch or a glow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A plain &lt;code&gt;ScrollArea&lt;/code&gt; has no keyboard scroll route.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A finger cannot drag a window on Wayland.&lt;/strong&gt; No protocol for it exists.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The target-size gate is green for three crates’ named fixtures&lt;/strong&gt;, not for the
framework: four crates that own targets have no fixture list, and the lists
install one preset.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reordering a data-view row with a finger is unreliable on short rows.&lt;/strong&gt; A
deferred drag is revoked if the first sample after the hold leaves the pressed
row, and the touch drag slop is wider than a default tree row.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cargo check --workspace --all-features&lt;/code&gt; does not build on any revision&lt;/strong&gt;,
including before this release: five of the seven optional fallback font faces
are named by the build and were never committed.&lt;/li&gt;
&lt;li&gt;The full ledger, each entry with its measurement, is
&lt;a href=&quot;docs/touch-and-pen.md&quot;&gt;Touch &amp;amp; pen&lt;/a&gt; §10.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Full Changelog&lt;/strong&gt;: https://github.com/FernTech-EU/teksilo/compare/v0.9.5…v0.10.0&lt;/p&gt;
</content>
  </entry>
  
  
</feed>
