v0.10.0
One strand above all: the input model is a pointer model. A touchscreen, a pen
and a trackpad reach the widget tree as real pointers, a density knob resizes
every target with a 24 dp conformance floor at each density, and a mouse behaves
exactly as it always has. Around it: touch and a stylus in the automation
bridge, a target-size gate over every shipped preset, a previewer that exports
at a chosen density, every SVG icon drawn pixel-exact, and a window that opens
on GLES-3.1 class hardware. The rich text editor takes text-document 1.12.2,
so a sentence cut at a paragraph's end is the sentence and nothing more.
Breaking, pre-1.0: the pointer-event variants gained fields, two positions were
renamed, WebViewHandle gained a required method and MemoryShared is
non-exhaustive;
docs/porting-widgets-to-the-pointer-model.md
is the contract for a widget crossing over.
Added
Touch, pen and density
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.
- Touch input, end to end. 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. - Pen and stylus. Pressure, tilt, barrel rotation, the eraser end, the barrel
button, and proximity as a first-class state — a stylus hovers before it touches.
Wayland throughzwp_tablet_v2, Windows through aWM_POINTER*window subclass;
no winit upgrade required. - Trackpad gestures. Pinch, rotate and two-finger scroll reach the widget tree.
TargetDensity— Compact, Comfortable and Touch. One knob resizes every
interactive target, gap and padding in the application: 24 / 32 / 44 dp targets,
6 / 10 / 16 dp grab handles, spacing ×1.00 / 1.15 / 1.30. Compact is the default
and is today's layout unchanged. Set it withTheme::with_densityor
WidgetTree::set_input_density; the shipped Material 3, Fluent and macOS presets
project their own dimensions onto the ladder rather than inheriting Teksilo's.- A 24 dp conformance floor at every density, never scaled — WCAG 2.2 SC 2.5.8
(level AA). A build-breaking test measures every named widget fixture against it
at all three densities. - Three hit mechanisms with disjoint jobs, so a small control is reachable
without being redrawn:Widget::hit_outsetwidens a target for one pointer kind
only,Widget::target_regionslets 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. - Kinetic scrolling. A fling coasts and settles on the platform's own curve —
Android'sOverScrolleror iOS's bouncing simulation, chosen per host by default
— with rubber-band overscroll available per surface. Nine scrollable surfaces
adopt it from one implementation. - Touch text editing 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.RichTextEditor,CodeEditor,PlainTextEditor,LogView,
TextInput,PasswordField,SearchField,SpinBoxand the date/time family. - A hold is a route. 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.
LongPressRoleoverrides the choice per subtree. touch_actionandpan_claimnode declarations, the CSStouch-action
model: a subtree can forbid panning or pinching over itself, and a scrollable
declares which axes it claims. A press freezes the effective value, so nothing
a widget does mid-gesture can change what that gesture was allowed to become.- A runtime kill switch. With
InputTokens::touch_enabledoff, the platform
translator drops touch input and the router installs no touch-only recognizers —
a mouse-only fallback with no rebuild of the binary. - Input tracing.
TEKSILO_TRACE_INPUT=samples|gestures|allprints every
sample, every arbitration decision and every cancellation, guarded so that a
normal run formats nothing. examples/touch_playground— a live pointer inspector (identity, kind,
primary flag, pressure, tilt, twist, contact patch, speed, the frozen touch
action, the press and the capture), five arbitration scenarios each naming which
contender won the press, a density toggle, an editable kinetic-constants panel,
and a touch-text surface.- A Touch tab in
examples/widget_catalog, and--density compact|comfortable|touch
on the catalog itself. - docs/touch-verification.md — the hardware
procedure, and its sign-off sheet.
Automation
- An agent can drive touch, a stylus and two contacts. 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.inject_pointer,inject_keyand
type_texttake acommandmodifier besidectrl, because a chord declared
Ctrl+Sresolves to ⌘S on macOS.
Accessibility
- The target-size gate measures every shipped preset, 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.
Previewer
--export-docsrenders at a chosen density:--density=compact,touch
writesdocs/widgets/img/<slug>-touch.pngbeside the canonical image, and a
catalog page grows a## Densitysection when one exists. Compact stays
canonical and keeps every existing filename, so nothing committed moves.teksilo_preview::PreviewPass— the density a preview renders at, and the
image-naming rule that follows from it.
Documentation
docs/porting-widgets-to-the-pointer-model.md— the contract for moving a
widget onto the pointer model, as numbered clauses, each stating the rule, how
to comply, and what checks it.- 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.
Changed
text-document1.12.2 andtext-typeset1.11.1.
Core
The pointer-model behaviour changes. Each is a token or a knob an application
can override, and none of them changes what a mouse does.
- A mouse drag never pans. 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 withScrollHandlingOptions::pan_devices. - A touch or pen drag of an item waits for a long press. 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.DragActivationon the node decides;
Autois the default and resolves per pointer kind. - A touch or pen press commits on the release. 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. - A press outside an overlay dismisses it on the release, and the dismissing
press does not reach what is under it. Dismissing a menu and activating the
control beneath it are now two presses, on every pointer kind. on_hovernever fires for a touch contact, and neither dohover_within
and the hovered signals. A contact does not hover — that is what a contact is —
so an affordance revealed only on hover is unreachable with a finger.
InputTokens::revealpromotes those affordances to always-visible at the Touch
density, and the tree's hold route reaches a tooltip regardless.- A pen in proximity hovers, and hover belongs to one pointer. 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. advance_timeticks gesture recognizers. A test that advances the virtual
clock now sees a long press ripen, a fling coast and a hold dispatch; the clock
is one axis for animation, gestures and kinetics rather than three.WidgetEvent::PointerDown,PointerUp,PointerMove,PointerEnterand
PointerLeavenow carrypointer: PointerInfo, so a handler can tell a
finger from a stylus from a mouse without reaching for the context.
PointerEnterandPointerLeavebecome struct variants; a pattern that named
them bare now needs{ .. }.PointerMovealso carriesmodifiers: Modifiers. A drag reads Shift and
Ctrl from the move rather than from the press, so a modifier pressed mid-drag
reaches the widget.- New constructors keep a mouse-describing call site to one line and default the
pointer toPointerInfo::mouseat the epoch:WidgetEvent::pointer_move_with,
pointer_enter,pointer_leave, beside the existingpointer_down,
pointer_up,pointer_move. WidgetEvent::Scroll::positionandPointerCancel::positionare renamed
window_position. Both stay in window-logical coordinates — the router
routes by the first, and the kinetic tracker behind a pan follows the pointer
rather than the widget — and they are the only positional fields a handler
receives that are not localised to it. The frame is now in the name, so a
reader that needs content coordinates is told to convert at every use.GestureEvent::PinchChangedcarries deltas, and says so.scaleis the
factor since the previous sample androtationthe twist since the previous
sample, in radians; fold each in rather than assigning it.- A drag handler is told which device carries the drag at every stage,
including the per-layout tick. Those previously reported the mouse, so a finger
never got the wider auto-scroll band. - A coarse pointer's drag preview is placed clear of the contact patch; a
mouse's is unchanged. - The widget under an inbound OS drag revises the OS accept state, 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. WindowOps::begin_os_dragandExternalDndGuard::begin_dragtake the
device'sPointerKind, and both gainset_drop_accepted.
ExternalDragEventgains aCancelledvariant.TouchSelection::on_long_presstakes apointer: PointerInfo. A hold is
recognised by a timer, so the context it is dispatched under had no device to
report; the holding contact's identity is now an argument.WebViewHandle::set_input_passthroughis a new required trait method, for
out-of-tree backends.WebViewEvent::EngineFocusChangedis new.MemorySharedis#[non_exhaustive]and gained four fields.InspectorState::toggle()is public. New:
teksilo_widgets::drop_target::{band_depth, region_at_floored, region_rect_floored},
styles::recipe_menu_item_style::menu_item_height.
Widgets
- Nine surfaces pan under a finger from one implementation:
ScrollArea, the
five data views and the three text surfaces, plusTerminalandSceneView,
which claim their own axes. - The window chrome answers a finger. 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. - A
SplitButtonchevron takes a direct pointer's press up to the conformance
floor, borrowing from the action half; a mouse's boundary stays where it is
painted. - A
TableViewheader cell's filter zone is the density's target size, and
the cell reports its three parts throughWidget::target_regions. - A
DropTarget's edge zones — and a docking pane's five — are floored per
axis and capped at a third of the extent. Not pointer-kind-gated: the acting
zone and the painted one must be one rectangle. - List and tree row minimum heights, combo-box dropdown rows, calendar
navigation arrows, menu rows and code-editor completion rows follow the
density ladder. - A finger's tap on a
ToolBoxheader or aRadioTileleaves it untinted
rather than resting hovered. - A toast pauses on a press-and-hold and dismisses on a horizontal swipe for a
coarse pointer, because hover-to-pause is unreachable with a finger. - Every reorder a drag can do, a keyboard and a menu can do too — WCAG 2.5.7.
WebViewin the defaultNativeinput mode answers a press itself and
revokes the pointer's interaction, declares no touch default over its rectangle,
and opts out of hit widening;Transparentasks 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.- The debug inspector opens from
InspectorState::toggle()or a corner-grip
hold, its pressed rows take the density's target size, and it has a Pointers
tab listing every pointer declaration in the application's tree. - The widget previewer takes
--density, switches it live, and its canvas zoom
works (pinch and Ctrl-wheel).
Terminal
- A finger pans the scrollback with a kinetic hand-off, 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. - A finger is never reported to the child program as a mouse. Touch reporting
is its own policy, separate from the mouse reporting a program requests.
Fixed
Rendering
- CPU-rasterized paths, which is every SVG icon, draw pixel-exact. A Tier-3
path's coverage mask is rasterized on its own integer grid, but its quad was
placed atbounds × scale_factorand sized toceilof that — so the quad sat
at a fractional device position, and at a fractional display scale it was not
even the same size as its atlas region.
Sampled through the atlas's linear filter, both errors smear. A 1 px hairline
drawn this way peaked at 48 % coverage instead of 100 %, and a 16 dp dashed
ring lost its gaps entirely and read as a grey haze. The path pipeline now snaps
the quad out to whole device pixels and bakes the bitmap against that same
origin, so one texel lands on one pixel — the guarantee
QuadVertex::from_glyph_quad_transformedhas always given glyphs, which is why
text was sharp and icons were not. The rect now travels with the raster as one
PathPlacement, so the two can no longer be derived apart. Snapping is skipped
under a transform, where the mask is being resampled anyway and rounding would
make a translating path step between pixels instead of gliding; a gradient's
geometry is re-based onto the snapped quad, so it lands in the same place either
way. - Two path-atlas entries no longer share an edge. 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 next icon rather than transparent space. Each
entry now reserves a one-texel transparent gutter, matching the glyph atlas.
Core
- A two-finger pinch reaches the zoom it asked for instead of running into
max_zoom. A spread to twice the starting span now leaves aSceneViewat
exactly twice, whatever the sample rate; before, each sample carried the ratio
to the start of the gesture and the handler multiplied every one of them in,
so a single spread compounded to the product of its intermediate ratios. - A trackpad twist turns content by the angle the user twisted. One degree of
rotation on the trackpad rotated aSceneViewby one radian — about 57° —
because winit reports degrees and the payload is read as radians. The
conversion now happens where the incoming unit is known. GestureEvent::PinchChangedandPinchPhase::Changednow documentscaleand
rotation: both are deltas against the previous sample, androtationis in
radians. Fold each sample in (zoom *= scale,rotation += rotation) rather
than assigning it.TouchPinchRecognizer::scaleandrotationare renamed
cumulative_scaleandcumulative_rotation. They report the totals since
the gesture started, which is not what aPinchChangedcarries; the names now
say which of the two a caller is reading.- A
PointerCancelfor a touch contact reported the mouse, with no position,
so an application branching on the cancelled pointer's kind behaved wrongly. - A container that preserves its children across a rebuild destroyed and rebuilt
its whole subtree instead, whenever any builder method wrapped it. That covers
everySwitcher,TabWidget,SceneView,DockingLayout,Repeaterand
PopoverWidgetwith a handler attached. - A wrapped widget's declared shortcuts never reached the registry, 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. - A coarser density could lower a grip's reach: an outset and the miss-only
slop pass were combined in the wrong order. - A caret could be left outside a viewport that shrank; a shrink now records a
reveal. - Two overlay-dismissal defects a mouse could feel, and a hold dispatched
under a mouse identity that was never there. - A target-size allow-list entry is held to the floor the audit judged against,
not to the generic 24 dp table. Under a theme raising
InputTokens::min_target_conformance, aPinnedDp::ClearsFlooraxis excused
the very failures the raised floor exists to report.
TargetMeasurement/TargetViolationcarry the measured floor as
conformance_floor, and every consumer reads it instead of re-deriving one.
Platform
- Teksilo could not open a window at all on GLES-3.1 class hardware. A
window asked its device forwgpu::Limits::default(), which demands eight
colour attachments. A Raspberry Pi 4's V3D driver allows four, so device
creation was refused and the application panicked before its first window
existed. Nothing in the renderer wanted that headroom: every render pass has
one colour attachment, it binds no storage buffers, its widest uniform
binding is 8 KiB and its widest shader carries ten inter-stage variables. A
window now asks fordownlevel_defaultswith the texture-dimension limits
lifted to the adapter's own, which is the set the offscreen test renderer
already opens with, so a frame that renders in a test renders in a window.
An adapter sitting below even that floor gets one retry with its own reported
limits, which cannot be refused on limit grounds.
Widgets
- A Fluent list or tree row can be clicked again. 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. - A macOS switch and an icon button meet the 24 dp target floor. 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. - Three controls showed no pressed appearance for a pointer at all — the
ToolBoxheader,RadioTile, and the calendar's month/year cell, the last only
ever writingfalseinto its own state. - A declared row height inside the row-metrics dead band was silently refused,
so an exact-height list reported a different total height from a uniform one. - A leaf row in a tree table no longer carries an invisible pointer target.
- The previewer's knob rows were unreachable by any pointer, mouse included: an
infinite height cap inside a scroll area measuredinftall, itsyresolved to
NaN, and a rectangle containingNaNcontains no point. - With the debug inspector installed, switching density destroyed the wrapped
application's tree. - Four dimensions a shipped theme sets now reach the screen. 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.TableStyle,CalendarStyleand
SearchFieldStylenow 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. - 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. 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.
Terminal
- A terminal placed anywhere but the window's top-left corner accepted no
pointer input at all. - The wheel scrolled the scrollback backwards, 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. - Dragging a selection handle moved the selection to the neighbouring row.
Previewer
- The toolbar's Export PNG ignored the live density, 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.
Text
- A sentence cut at a paragraph's end is the sentence, and bold lands on the
selection. 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. --all-featuresbuilds. Fivefonts-*features named a Noto face that is
not in the repository, and becauseinclude_bytes!resolves at compile time,
enabling one was a hard build error — so those five features, both
fonts-allmeta-features, and any--all-featuresbuild 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.- Dragging the caret handle moves the on-screen keyboard's candidate window
with it. Every editing surface reported the caret's position when a press
placed it, and none did when a finger dragged it, so composing Japanese,
Chinese or Korean after a handle drag put the candidate list at the caret's
old position.RichTextEditor,CodeEditor,PlainTextEditor,TextInput,
PasswordField,SearchField,SpinBoxand the date/time family; the
assistive-technology route onto the same handle reports it too. - The code editor's gutter returned from inside a clip scope when no text
backend was installed, leaving the render frame unbalanced. - A
PlainTextEditorcould neither replace nor suppress the context menu its
documentation offered it, and theCodeEditorfamily gained the right-click
menu it never had. --all-featuresbuilds. Fivefonts-*features named a Noto face that is
not in the repository, and becauseinclude_bytes!resolves at compile time,
enabling one was a hard build error — so those five features, both
fonts-allmeta-features, and any--all-featuresbuild of the workspace
had never compiled on any revision. A face is now embedded only if its file
is present; enabling a feature without it warns and names the path to drop it
at, rather than failing the build.
Documentation
- A
ScrollAreain theThinscroll-bar mode documented a keyboard route it
does not have. The bar's arrow /Home/End/Pagehandlers sit on a node
that cannot take focus, under every mode — the same limit the
ScrollBarPolicy::AlwaysOffdocumentation states from the other side. PointerInfo::primary's documentation described the wrong one of three
similarly-named things. The field is the W3C per-kind flag — a mouse and a
first finger are both primary at once on a hybrid machine — and the rule it
carried ("exactly one live pointer, a mouse always wins") belongs to the pointer
table's own election.StandardListItem's documentation claimed the row reads its recipe's
projected heights. It projects the module constants instead; those two recipe
fields have no reader.- The soft-keyboard documentation claimed a touch-placed caret leaves a stale
IME area standing. Each editing stack reports the area from its own touch path;
the unused core method is superseded rather than missing. - Two committed catalog images had gone stale, one of them contradicting a
safety fix: the message-box preview showed Yes as the accented default
after the widget had switched to No, and the colour-picker preview predated
the widget growing its HSV row. - Catalog pages linked to rustdoc module pages that do not exist on docs.rs,
because the module is crate-internal; each link now resolves to the nearest
published module.
teksu
teksu!bodies could not placegesture_dead_zoneorkeyboard_capture
before a child; both failed with "no method namedchild". A compile-time
guard now keeps the DSL's builder-method list complete.- 31
teksu!UI fixtures had never run — a glob naming a directory that does
not exist, which trybuild reports as "no tests enabled yet" and then passes —
and one of them was asserting a caret column rustc does not emit. - A bare child in any popover produced the generic error instead of the slot
hint.
Known limitations
- The hardware sign-off is not done. Everything above that a headless Linux
host can check is checked by the suite;
docs/touch-verification.md is the procedure for
the rest, and its sign-off sheet 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. - A running application does not switch density because a finger arrived.
DensityPolicy::FollowLastPointerhas an ingress and no writer; what a stray
tap should cost and when hysteresis commits are unanswered. - Overscroll is published but not painted.
ScrollableAxes::overscroll
carries the value; nothing renders a stretch or a glow. - A plain
ScrollAreahas no keyboard scroll route. Assistive technology can
scroll it and the data views bring their own key handling, but a keyboard user
facing a scroll region whose content holds no focus has none — a pre-existing
WCAG 2.1.1 gap, now written down. - A finger cannot drag a window on Wayland. No protocol for it exists.
- The target-size gate is green for three crates' named fixtures, not for the
framework: four crates that own targets have no fixture list, and the lists
install one preset. - Reordering a data-view row with a finger is unreliable on short rows. 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. cargo check --workspace --all-featuresdoes not build on any revision,
including before this release: five of the seven optional fallback font faces
are named by the build and were never committed.- The full ledger, each entry with its measurement, is
Touch & pen §10.
Full Changelog: v0.9.5...v0.10.0