Skip to content

feat(defaults): add a wheel profile, board layouts, and a canvas node - #46

Open
inayayousfi wants to merge 16 commits into
feat/settings-stripfrom
feat/wheel-profile
Open

inayayousfi wants to merge 16 commits into
feat/settings-stripfrom
feat/wheel-profile

Conversation

@inayayousfi

@inayayousfi inayayousfi commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

This sits on top of #44 and adds a third bundled profile, wheel, which types through a ring of slices instead of a key grid. The idea comes from a web prototype where the pointer picks a slice around a center, and each slice either opens a smaller menu or sends its key.

The engine stays keyboard-agnostic and gains two generic pieces. A new canvas node places each child by its shape, a polygon in pixels, centers the child's box on an optional anchor point, and clips the child to the shape, so a button child is still an ordinary button whose clicks, hover and dwell targeting follow its outline. Shape and anchor are values like any other and can be bindings. They are read through a new pair of node kind hooks, decode_live_placement and place_child, so the canvas owns its children's placement the way a grid owns its cells, and a kind that sets only one of the two is refused at registration. std.wheel turns on report_geometry, from #36, so the engine writes the canvas's size to nodes..size, which is how a profile fits shapes to the space it gets. Buttons take a list of label parts, which they spread in rows over their shape, draw their border along a shape, and shrink any label to fit while never growing it past the size their style gives. A single label is still drawn by the Qt style, scaled down when it must shrink, so stylesheet text rules such as a :pressed color keep working.

Everything keyboard-specific lives in axidev_osk.std. std.board names the positions of an 80% board the way XKB does (AD01 is where US Q sits), and a table in std.layouts says what each position is: its legend, its Shift legend and the literal key it sends. US and Dvorak ship. keys.behavior turns an entry into how it acts as a key (press, hold-to-repeat, release, latching modifiers, lock lighting), so a grid key and a wheel slice holding the same entry behave the same, and keys.on_layout resolves those behaviors for whichever layout a binding names. The default keyboard is rebuilt on this and reads a shared variable, settings.layout, which the settings strip now switches through a new Layout menu.

std.wheel draws the whole wheel itself. It reads the canvas's reported size and computes every wedge and the center as plain lists of points, so the wheel stretches into an ellipse with its window. It has 10 slices: digits, the other symbols with Tab and Enter, navigation, Escape with the function and rare keys, Backspace, Space, the modifiers, and the three letter rows. A menu slice previews the keys inside it spread over its wedge. Picking a key sends it and goes back to the top, modifiers latch like on the grid, and the center returns to the top from any menu.

The default keyboard changes on purpose in two ways. Its key unit drops from 48 to 32 px and the theme gets smaller paddings and fonts, so the minimum sizes in tests/test_default_profile.py are rescaled; cells, legends and outputs keep their original measurements. Its node IDs are now positions, so key:A is key:AC01. Separately, a window with the settings strip now gives its spare height to its content again: since #44 the strip's spacer was taking it, so keys stopped growing with the window.

One fix outside the profiles: on quit, KeyboardService now stops listening before it releases held keys. axidev_io's stop_listener waits for its listener thread while holding the GIL, and a release arriving mid-stop could hang the app until systemd killed it, which happened twice here with latched Ctrl keys. A physical key event during the stop can still hit the race; axide-dev/axidev-io-python#3 tracks the binding fix.

The full suite (516 tests) and flake8 pass. Pyright reports one error, the unresolved axidev_io import in services/keyboard/io.py, because that package is missing from the local virtual environment. The branch has run as the installed app on KDE Plasma Wayland, including the lock-screen supervisor reload, but a real lock and unlock cycle and the shutdown fix with held keys are not verified yet. No platform-specific code changed besides the shutdown order.

CI: the Windows check sets QT_QPA_FONTDIR so offscreen Qt finds the system fonts, and the label color test compares average colors, so subpixel font smoothing on Ubuntu no longer fails it.

@inayayousfi
inayayousfi added this pull request to stack #45 October 12, 2026 18:04
@inayayousfi inayayousfi changed the title feat(defaults): add a wheel profile built on board positions and layouts feat(defaults): add a wheel profile, board layouts, and a canvas node Oct 12, 2026
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

A new wheel node kind places its children as the slices of a disc,
slice 0 on top and numbered clockwise, or as its center, and clips each
child to its shape. A button child stays an ordinary button: clicks,
hover, and dwell targeting follow its wedge. Node kinds can now check
their children's placements together at load, which the wheel uses to
allow one child per slice.

Keys are now described in two layers. std.board names the positions of
an 80% board the way XKB does, and a std.layouts table says what each
position is: its legend, its Shift legend, and the literal key it sends.
keys.at builds a key for a position that follows a layout variable, and
the default keyboard is rebuilt this way on settings.layout, a shared
root variable set to "us". Its key IDs are now positions, such as
key:AC01 for A; the parity data for legends and outputs is unchanged.

std.wheel walks a hand-written map of the board on 6 slices: numbers
with the function row inside them, three letter slices by board column,
navigation, and modifiers. Picking a key sends it and returns to the
top, a modifier waits for the next key, and the center goes back to the
top. The new wheel profile shows it with the same strip, dwell, pointer
glow, hot corners, and lock-screen panel as the default keyboard.
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

The engine no longer knows about wheels. The wheel node kind and the
check_children hook are gone, replaced by a canvas node that places
each child by its shape, a polygon in pixels, centers its box on an
optional anchor point, and clips it to the shape. Both are ordinary
values, so bindings can move them. Any node can set report_size, and
the engine then writes its size to nodes.<id>.size through a new
node.resized event.

std.wheel now computes every wedge and the center itself from the
canvas's reported size, so the wheel stretches into an ellipse with its
window. It has 10 slices: digits, the other symbols with Tab and Enter,
navigation, Escape with the function and rare keys, Backspace, Space,
the modifiers, and the three letter rows. A slice behaves exactly like
the key button for the same entry through keys.behavior, which replaces
keys.tap: holding repeats and modifiers latch. A menu slice previews its
keys spread over the wedge.

Buttons take a list of label parts, which they spread in rows over
their shape, and draw their border along a shape a canvas gives them.
Every label shrinks to fit its button and never grows past the size
its style gives it.

A Dvorak table joins the US layout, and the settings strip gains a
Layout menu that switches both keyboards. The default keyboard's key
unit drops from 48 to 32 px with a smaller theme, so the minimum sizes
in the parity data are rescaled; cells, legends, and outputs are
unchanged. A window with the strip now gives its spare height to its
content again, so keys grow with the window.
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

A node kind whose children's placement may follow state now declares
decode_live_placement and place_child, and builds its children with
builder.build(child, into=its widget). Setting only one of the two
hooks fails at registration, and building a child without into fails
at build, so a placement can no longer be ignored silently. The canvas
is the only code that knows shape and anchor, and either one under
another parent is an unknown key. The canvas gives the outline to any
child with a set_shape method instead of checking for Button.

A single button label is drawn by the style again, scaled down when it
must shrink, so state rules such as a :pressed text color apply. A
label that changes to an empty list now shows nothing. The border color
of a shaped button is sampled once per state, and the docstring says
what the sample assumes about the theme.

keys.on_layout builds each position's behavior in every layout up front;
keys.at and std.wheel both use it, which removes the wheel's lazy cache.
The default profile shares one status label between its keyboards, and
the osk docstring and the purification plan are corrected.
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

axidev_io's stop_listener waits for its listener thread while holding
the GIL, and that thread needs the GIL to deliver an event. Releasing
held keys right before the stop could put such an event in flight, so
the app hung on quit until systemd killed it. Shutdown now stops
listening first. A physical key event during the stop can still hit
the race; axide-dev/axidev-io-python#3 tracks the binding fix.
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

A wheel whose nested menu has another slice count than its top level,
or an item that is not a position, a menu, or None, now fails when the
config is built instead of breaking slices when the menu opens. menu
and row take the slice count, so each tree's top level is the one
source. A layout table missing a position the keys need fails the same
way; an unknown layout name set at runtime is left to the config
check.

Label parts land only in the widest stretch a shape covers on every
line of their row, so a dented shape no longer puts them in its gap,
and they are drawn by the style like a single label, so state rules
such as a pressed text color reach them. When no row layout fits, the
parts are drawn as one label instead of disappearing.

Smaller cleanups: the unused wheel.column is gone, names no longer
shadow the layouts module or the middle function, and AGENTS.md and
the plan describe the strip's Layout menu and list labels.
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

The node kind recipe names decode_live_placement, and a new recipe says
where a layout goes. The canvas, board positions, layouts, and wheel are
described in their own modules' docstrings.
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

Brings in timers, hover, pointer reports, std.lit, and the strip's
lit controls. std.keys.behavior still names active and latched, and
the places that turn it into a widget (from_entry, at, and the wheel
slice) now light it through std.lit instead of the removed button
options. A node may set both report_size and reports_pointer, and a
canvas child's live placement decodes beside the engine-wide style
check.
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

The pressed-color test compared every text pixel, so the colored edge
pixels of subpixel font smoothing on the Ubuntu runner failed it. It
now compares the text's average color, which still tells white from
blue.

The offscreen platform finds no fonts on Windows by itself, so label
sizes read as -1 there. The Windows check now points it at the system
font folder with QT_QPA_FONTDIR.
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

Brings in the dwell tests' wait for clicks.
@inayayousfi
inayayousfi marked this pull request as ready for review October 13, 2026 15:16
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

Brings in the dwell indicator options and the close_label window value.
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

Brings in the interaction reports: mouse buttons, title-bar and grip
drags, and finished dwells, with std giving them their usual meaning.
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

Brings in optional built-ins and their raw reports: any-button drags,
hover, wheel, and geometry reports, hot-corner and dwell options,
std.locator, and window anchors.

report_geometry replaces report_size and node.resized: it records
nodes.<id>.size as before, plus nodes.<id>.position, and std.wheel
turns it on. The keyboard and the wheel color their buttons through
std.locator. Five navigation keys change locator color, matching what
the old engine palette gives the 32-pixel keys.
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

Brings the shape audit fixes and keep_position; nothing needed
adapting.
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

The button option label_min_px (default 1) is the smallest pixel size a
label shrinks to when it does not fit; below it the label is cut
instead of shrinking further.
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

Brings the Windows mouse-button fix.
inayayousfi directed the work and made every decision.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.

Brings plain-window moves and resizes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant