feat(client): the quick-action ring, editors and virtual controller #448
Merged
enricobuehler
merged 42 commits from 2026-08-30 20:41:17 +00:00
touch/dial-model into main
42
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3a6f594754 |
fix(ci): the touch cap and the standby-sink knob join the env baseline
ci / bun-nix (pull_request) Successful in 24s
ci / web (pull_request) Successful in 57s
android / android (pull_request) Failing after 1m49s
ci / docs-drift (pull_request) Successful in 1m37s
apple / swift (pull_request) Successful in 2m7s
ci / docs-site (pull_request) Successful in 2m14s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 2m26s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Failing after 2m42s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Failing after 3m32s
ci / rust (pull_request) Failing after 10m19s
The docs-drift gate flags PUNKTFUNK_* names in code that the docs never mention, and this branch carries two: `PUNKTFUNK_HOST_CAP2_TOUCH`, the v28 ABI capability bit — a quoted literal only because cbindgen.toml renames the C constant, not an env var anyone sets — and `PUNKTFUNK_STANDBY_SINK_KEEP`, the vdisplay standby-sink neutraliser's operator opt-out, which is not this branch's feature to document. Both take the gate's own escape for deliberately internal knobs: a line in scripts/ci/docs-undocumented-env-baseline.txt. The check runs green locally with them. |
||
|
|
ac7fa7e260 |
feat(console): Lucide icons on the dial; the glyphs speak one language
ci / bun-nix (pull_request) Successful in 29s
ci / docs-drift (pull_request) Failing after 30s
ci / docs-site (pull_request) Successful in 57s
ci / web (pull_request) Successful in 1m8s
apple / swift (pull_request) Successful in 2m18s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 2m36s
android / android (pull_request) Failing after 3m30s
ci / rust (pull_request) Canceled after 5m15s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Canceled after 5m20s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Canceled after 0s
"All text looks boring and ugly" — the dial's discs carried short words because the console has no icon font. It still has none: the icons are Lucide v0.462.0 (ISC), each one carried as its 24×24 SVG path data in `icons.rs` — every shape of the source SVG folded into one path string by scripts/gen-lucide-icons.py (a leading relative `m` is absolutised, since concatenation would re-anchor it) — parsed by Skia's own SVG-path parser and stroked at Lucide's native width 2 with round caps, scaled to whatever box the caller gives. A designed, consistent set, rendered exactly as drawn, for a page of string constants: no font ships, no dependency lands, and every mark takes the console's paints. On the dial: End stream, Leave, Touch mode, Keyboard, Stats, Mic (and its struck twin while muted), Virtual controller, Send text, the three host powers (moon, cycle, power) and the centre's ellipsis. A shortcut keeps its stacked keycap chord — its content IS text — and an unknown host action falls back to its label, so nothing the set cannot name goes blank. An empty slot in the editor shows a faint plus; in-stream it stays plain glass. The hint bar's gamepad glyphs joined the same language, because they were three dialects: solid floating triangles beside badged letters, two ink alphas, three container treatments, and a keycap hairline that never scaled. Now the directional marks are the set's chevrons (a direction is not a button, so they stay badge-free), the remote's back arrow is the set's return mark inside its badge, every container wears the same scaled hairline, and the PlayStation shapes grew to match the letters' weight, round-joined. Licensing: LUCIDE-LICENSE sits in the crate and the notices generator lists it as vendored third-party source; THIRD-PARTY-NOTICES.txt is regenerated, which also catches the file up with this branch's real lockfile (nine crates left, chacha20 moved — the committed baseline was stale). Tests: every icon parses and stays inside its 24-unit box; every built-in slot maps to an icon while shortcuts and unknown host actions stay text. |
||
|
|
807bb3492a |
fix(console): no second card behind the dial; the phone-size ring
Two from the Nothing Phone's gamepad UI, both scale bugs the Deck's scale of 1 could never show. The "second fill with a different border radius" behind the dial was the stage's focus halo. `focus_halo` takes its corner in DESIGN units and scales it itself, like `panel` — every other caller passes it that way — but the editor handed it an already-scaled corner, so the halo's rounding grew with the SQUARE of the display scale: identical to the stage at scale 1, a visibly rounder ghost card at the phone's ~2. The dial was tiny because the fit reserved 140 design units for the shortcut rows below the stage. A phone's console scale is max(min(w,h)/800, density·0.75) — landscape leaves ~380 units of content height, and the reservation crushed the ring to about half size. Now, when the fit-below falls under 0.75 and the screen is wide enough, the shortcut rows move BESIDE the stage and the ring scales to the height instead — near full size on a landscape phone. The couch layout is untouched: its fit-below is over 1, so the rows stay under the stage exactly as the Deck shows them. |
||
|
|
21be74b4b7 |
fix(console): the picker dims the whole screen; the ring always fits
Two more from the Deck. The picker's scrim stopped at the content rect, so the heading above it stayed bright — a band instead of a modal. The shell only translates the canvas (insets, transitions), it never clips a screen to its rect, so the scrim now overshoots every edge by more than any heading, hint bar or inset can be. The hint bar draws after the screen and stays legible; it already carries the picker's own controls while it is up. The dial was cut off at the top — and the cause was the overshoot clamp of the previous commit, not the stage. The stagger normalisation divides the per-slot lag out, so `q_raw` sits ABOVE 1 for the early slots whenever `shown` is near 1; the old code clamped travel at 1, the new clamp at 1.15 parked slots 0–2 up to 15 % past their seats FOR GOOD, and the 12 o'clock disc rode out of the stage's clip. Only the spring's excess past 1 overshoots now (`q + (shown − 1).max(0)`), so at rest every disc sits exactly on its seat, in the stream and in the editor alike; the centre's pop follows the same rule. And for every window the stage cannot fit at full size: the editor computes a fit from the rect's height (keeping room for the caption and two shortcut rows) and the stage's width, scales the stage by it and draws the ring at the same reduced scale — so the ring is never clipped, whatever the screen; on the Deck the fit is 1 and nothing changes. |
||
|
|
d70e6f64ef |
fix(console): the ring animates, the sheet fills, the editor is rows
Five things the Deck showed in one session, and one it could not. The sheet opened empty. The stream overlay redraws only when `Ring::damage` changes, and that key held only discrete state — the sheet's `MenuList` fans its rows in on a mount entrance, its focus eases and springs, none of which the key knew about, so the rows sat at their first frame (unrisen, invisible) until a cursor move forced one more redraw. The same gate quietly stalled the ring's own arrival short of 1 once its steps fell under the key's quantisation. `MenuList` now reports `animating()` from a settled flag it computes at the end of every render (no entrance, every ease landed, every spring at rest, the scroll where it wants to be — the eases are landed explicitly, an approach never arrives on its own), and `Ring::animating()` folds it in with its own spring, its highlight eases and its wind-in; while any of that is true the key hashes a frame counter that `tick` advances, so the overlay draws every frame until it has all landed and then goes back to costing nothing. With the loop fixed the ring could be made to look like something. The arrival is a spring (`springs::RING`, a whisker of overshoot the six discs carry into their travel and their size, staggered as before), the discs sit on a soft shadow with a rim of light on their top edge, the pad's highlight is a glow and a ring at an eased amount per disc so it travels between them instead of snapping, the scrim is a pool of shade around the ring thinning to a veil, the label rides in with the ring, the sheet rises into its seat under a shadow while its rows fan in, and a closed ring winds back in and fades instead of vanishing — closed for input at once (`open`), drawn a moment longer (`visible`). Reduce motion keeps the crossfade it had. The theme owns the four new paints. The editor handed focus to the shortcut list when the stick pointed at 6 o'clock: the walk fired on any `Move(Down)` at slot 3, and the stick's own move rides behind its sector in the same sample. It now walks on a D-pad Down there, or on the stick pushed down a second time once its first push put the highlight there; never on the move behind a sector (`stick_engaged`), and the move that follows a sector-walk is swallowed so the list is not stepped twice. Coming back up, the ring adopts a still-held stick so its repeats step nothing until it lets go. The shortcut editor was its own thing — chips, a hand-drawn grid, a name box — with a focus halo nothing else on the shell uses, which on a Deck is invisible. It is now the console's form grammar: a `MenuList` of rows (Name, Key, Hold with Ctrl/Alt/Shift/Win as stepped On/Off rows, Add/Save, Remove) under the disc as the ring will draw it; the name opens the keyboard tray as the add-host screen does, the key opens a key tray of the same shape — the 66 keys laid out like a keyboard, drawn the keyboard tray's way: flat keys, the focused one filled with the accent, the chosen one washed with it. Row focus is the list's own lift and tint; key focus is a solid accent face. Touch on the Deck: the user's twist did nothing in Gaming Mode, and the log shows why it cannot be diagnosed yet — no T17 notice, no finger, nothing. The presenter now logs SDL's touch devices (id, kind, name) at connect, the first direct finger of a session, and once, a finger from a non-direct device it ignores. §5.5 stays unverified; the handoff carries the evidence and the question. Tests: the ring's damage key changes per tick while animating and not once settled; the shortcut rows follow the draft; the key tray walks like a keyboard. The grid, draft and column tests stay. |
||
|
|
0c4d503b09 |
fix(console): the stick points at a ring slot; the editor's stage fits
Three things the Deck showed in the gamepad UI's Quick actions editor. The stick stepped the highlight one slot per flick, like a list. The design (§2.6, D12) wants the weapon-wheel idiom: the stick's angle is the slot, neutral is the centre, the D-pad steps. The stick only ever reached the ring as a four-way `MenuEvent::Move`, quantised inside `MenuNav::poll`. `menu_nav::ring_sector` now reads the raw sample: past the deadzone by magnitude (a diagonal counts) the angle falls into one of six 60° sectors centred on the slots; an engaged sector holds until the stick drops under `MENU_RELEASE` or the angle leaves it by 5°, so a stick resting on a boundary never flickers between two slots. The poll emits `MenuEvent::Sector(Option<u8>)` on change, BEFORE the same sample's buttons and `Move`, so the ring has engaged the stick by the time the move that would have stepped it arrives and an A in the same sample lands on the slot the stick points at; a sector the snapshot adopted silently releases silently. While engaged the ring ignores `Move` (the stick's own repeats included) and the D-pad steps again once the stick lets go. In-stream, neutral highlights the centre; in the editor the centre is inert, so neutral leaves the slot where it is. Every list ignores the new variant through its wildcard arm; Android maps integers INTO `MenuEvent` and is untouched. Android and Apple still step four-way — their routers quantise the stick before the ring sees it — and owe the same event. The stage under the ring was a purple-to-teal gradient. It read as decoration on the Deck, so it is the flat card face every other card on the shell uses; `theme::stage_gradient` is gone. The label under the ring — the highlighted slot's name, or the editor's "press A" hint — sat at centre + radius + slot diameter, 328 units into a 330-unit stage clipped to its rounded rect, so only its top edge showed. The stage now sizes itself from the ring's own geometry (the top slot, the bottom slot, `ring::LABEL_H` under it, one pad each side) and the ring sits one pad below the stage's top, so nothing of the ring is ever clipped. The caption says the stick points. Tests: `ring_sector` for the six centres, the magnitude deadzone, the overlap hysteresis and the release threshold; the poll's ordering and change-only emission; the ring's stick/D-pad handover in-stream and in the editor. |
||
|
|
dda1e1e9c7 |
feat(client/windows): the quick-action ring's editor, native in WinUI
Design §3.3 in the Windows client, a Quick actions section of the settings page drawn with the reactor's own elements. Six discs sit on the ring's geometry over a gradient stage in a `Canvas`; the stage's pointer handlers own the carry (a pointer that leaves a disc stops reporting to it, so the discs cannot), a click on a disc opens its picker, a disc carried onto another swaps the two. A row of six buttons under the ring gives a keyboard and a screen reader every slot the pointer reaches. The picker is a card: the catalogue by group, the current pick accented, an entry this shell cannot serve disabled with its note, and "Swap with…" as a menu. The shortcuts follow: a row each with its keycap, Add, and Reset behind a menu (the second press). The editor: the disc as the ring will draw it, the name, the four modifiers as toggles, the key on a keyboard-shaped grid of toggles — or "Press the chord…", which arms an accelerator per key and Ctrl/Alt/Shift mix and fills the chord from the one that fires (the Windows key stays a toggle: the shell never sees a chord Windows owns). Save, Remove, Cancel. Every edit commits at once through the page's `commit`, so the profile scope's absorb records the whole ring (D10) and the page's override marker shows it. WinUI's brushes here are flat, so the stage gradient is a 24-bit BMP written once beside the art cache and loaded as a file URI, with a flat tint if it cannot be written. Gates: clippy `-D warnings` and the section's unit tests on the .133 Windows box. Not yet on glass. |
||
|
|
d9787009c8 |
feat(client/linux): the quick-action ring's editor, native in GTK
Design §3.3 in the GTK shell, on the toolkit's own widgets so its focus, drag-and-drop and screen reader carry the editor. A Quick actions row on the Input page opens a subpage: six round buttons on the ring's own geometry over a gradient stage, the inert centre, and the name of the button under the pointer or the focus. A click picks what the button holds from a popover — the catalogue by group, each entry's availability note, the current pick marked, and "Swap with…" for a keyboard; a button dragged onto another swaps the two (GTK4 `DragSource`/`DropTarget`, the slot as a `u32`). Under the ring the shortcuts as rows with their keycap, Add shortcut, and Reset to default behind an alert. The shortcut editor is its own subpage: the disc as the ring will draw it with its legend, the name as an `EntryRow`, the four modifiers as toggles, the key on a keyboard-shaped grid of toggles — or "Press the chord…", which takes the next real key press and fills the modifiers and the key from it (`gdk::Key` → the chord's names). Save, and Remove for an existing one. The blob lives in the dialog's state and is written with the other rows when the dialog closes, so it obeys the dialog's scope model: in a profile the row carries the override marker on the first edit, `commit_profile` writes the whole ring (D10), and the reset puts the inherited blob back. The row's subtitle is the ring in order. Gates: container fmt, clippy `-D warnings` and build for `punktfunk-client-linux`; its tests under Xvfb, with two new ones for the key names and the summary. Not yet on glass. |
||
|
|
a45ba525f7 |
refactor(client): the ring's catalogue, geometry and shortcut edits are one model
Three desktop editors of the ring are about to exist — the Skia
console's, the GTK shell's and the Windows client's — and design
tenet 3 says share the model, not the renderer. So `pf-client-core`
now owns what every editor was restating:
- `overlay_actions::catalogue(cfg, platform)`: the groups, entries and
availability notes an editor lists, in one order, with the desktop's
notes (no virtual controller, no typed text) where the phones have
none.
- `ring::{RING_RADIUS, SLOT_DIAMETER, CENTRE_DIAMETER, slot_angle_deg,
slot_offset}`: the ring's geometry at 100 %, so three drawings of it
cannot drift apart (tenet 8).
- `OverlayConfig::{upsert_shortcut, remove_shortcut}`: a new shortcut
takes the next id and the first empty slot; an existing one changes
in place; a removed one empties its slot — the same on every client.
The console's editors read these instead of their own copies; the
tests moved with the code.
|
||
|
|
4ca580755a |
feat(console): the ring editor is the ring, and a real shortcut editor
Design §3.3 on the Skia console, done the way the phones have it. The
six "Ring, N o'clock" rows stepped with ◀ ▶ and the form that stepped
a key name through sixty entries are gone; one Quick actions row opens
`RingEditorScreen`: the in-stream `Ring` — the same type, in a new
editing mode — full size over a gradient stage. With a pad, the stick
or D-pad walks the slots, A opens the catalogue by group (Session,
Input, View, Audio, Host, Shortcuts, Empty, each with its availability
note and the current pick marked), Y lifts a disc and A drops it on
another to swap, B puts it down or goes back. With a pointer, a click
on a disc picks, a carry onto another disc swaps — `PointerKind::
Release` finally has its gesture. Under the ring the shortcuts sit as
rows with New shortcut and Reset to default (two presses); Down past
6 o'clock walks into them, Up from the top walks back.
`Ring` gains `edit_at` and an `Editing` state: no scrim, no idle
close, the centre dimmed and inert, the three power slots previewed
as a host that offers them would show them (a dimmed "does not offer
it" lied about the slot), a lifted disc raised, a carried disc under
the pointer, and `EditEvent::{Pick, Swap}` out through `take_edit`.
Nothing fires. The one desktop-only thing it read, the host-action
cache, is behind a cfg now, so the ring — and with it the editor —
compiles for the Android console too, which is the pad editor there.
Shortcut discs draw as stacked keycaps (the modifiers small on top,
the key large under them) in-stream and in the editors alike.
`ShortcutEditorScreen` replaces `RingShortcutsScreen`: the disc as
the ring will draw it with its legend, the name on the on-screen
keyboard (Steam's on a Deck), the four modifiers as chips, and the
key on a keyboard-shaped grid — every name `key_vk` knows, six rows
laid out as a keyboard lays them, walked with the stick (Up and Down
keep the column) or clicked — then Save, and Remove for an existing
one. A new shortcut still takes the first empty slot.
Left as it was: the console edits the global blob only (D9's scope
picker is still owed), and a swap on the console has no spring — the
discs change in place.
Gates: container fmt, clippy `-D warnings`, `pf-console-ui` 221 tests
(the ring's two editor tests, the catalogue, the draft round trip,
the grid's coverage and column rule; `every_row_has_exactly_one_tab`
55 → 48); the Android kit build through cargo-ndk, which compiles
this crate for the Android console. Not yet on glass.
|
||
|
|
3052e560a9 |
fix(client): Gaming Mode's touch-as-mouse no longer walks the cursor (T17)
Design §5.5: on a Deck in Gaming Mode, Steam Input owns the touchscreen and replays it as a mouse whose "relative" deltas are absolute positions — a field capture read 0 finger events against 341 mouse motions of 300 to 450 px. Under the stream's relative-mouse lock every one of those walked the host cursor further into a corner, and since SDL saw no fingers, all three touch models were dead together. `SDL_TOUCH_MOUSE_EVENTS=0` never reached them: they are Steam's, not SDL's. `SteamTouchMouse` (touch.rs) recognises that shape — a gamescope session (`overlay_focus::gamescope_session`, now pub), no direct-touch finger yet this session, a delta of 150 px or more that no finger drag produces — and the presenter drops those motions instead of summing them. The first one raises the session notice once: "Steam Input is sending the touchscreen as a mouse — pick the Punktfunk controller layout for touch". A real mouse on a docked Deck keeps working (its deltas are small), and the first real finger to reach SDL switches the guard off for the session. The lever the notice names is the one the Deck docs already teach: the Steam Input layout the Decky plugin installs, whose whole job is real touch. §5.5 still marks it unverified on glass — the docs gain a line on what happens without it, and the notice is the reminder. T18 (reading the touchscreen from evdev) stays deferred. `build-flatpak.sh` gains `BRANCH=` so a Deck tracking the hosted `canary` can take a test build in place of it; a second branch beside it would win the plain `flatpak run` the plugin issues. Gates: `cargo test -p pf-presenter --lib touch` on the Mac (19, the new `gaming_mode_drops_leaked_touch_positions_until_a_finger_is_seen` among them); container build + clippy `-D warnings` for `pf-presenter` and `pf-client-core` and their tests; fmt. Not yet on a Deck. |
||
|
|
8b45652e12 |
feat(client/apple): the virtual controller on iPhone and iPad (T15)
Design §4 on Apple, matching Android: the ring's Virtual controller slot toggles a controller over the stream. While it is up the session model holds one `VirtualPadWire` — the lowest free wire index from `GamepadManager`'s one allocator (the way `Sc2Capture` claims one), declared an Xbox 360 pad, its Arrival before any input, held state flushed and its Remove on close — so the host sees one controller arrive and one leave beside any real pad (§9). The ring's mask reaches it through `setRingOpen`: everything held is released on the host when the ring opens, nothing is sent until it closes, nothing is replayed after. Toggled from the ring, it starts masked. The layer is UIKit: SwiftUI has no multi-touch, and UIKit keeps a touch with the view it began in, which is exactly the ownership a stick needs. Each control is one `PadControlUIView` placed by SwiftUI at its preset position; the layer draws nothing between them, so a finger there never reaches it and falls through to `StreamLayerUIView` — tap-to-click keeps working beside the pad. The geometry and the three input rules (`padControls`, `dpadBits`, `stickWire`, `triggerWire`) live in the kit with the same numbers as the Android `VirtualPad.kt`, so the two pads feel the same: four face discs in one view (a thumb rolls from A to B), an eight-way D-pad by angle, a stick that follows its first finger from wherever it landed, a trigger read off the finger's position down its pill. A light impact per press. tvOS keeps the slot dimmed — "Apple TV has no touch screen" — the way the Touch mode slot is; the layer file is `#if os(iOS)`. Editor: a Virtual controller section under the ring — Layout picker, Opacity and Scale sliders that write on release — through the same `blob` binding the ring uses. The picker's note no longer promises a later release. Gates: `pf-touch-apple-check.sh ios|tvos PunktfunkClient` both `swift exit: 0`; `VirtualPadTests` (the Swift twin of the Android test) written in step but not run — the bundle holds the 5-slice iOS/tvOS core and the macOS test host needs it rebuilt. The Release xcodebuild for the iPhone succeeded, installed and launched on the device — not yet seen on glass. |
||
|
|
da73d2e973 |
docs(client): the quick-action ring and the virtual controller
Neither had a page. `input.md` gains "The quick-action ring" — the twist, the per-shell openers, `Select+A` — and under it "Virtual controller": where it is shown from, that the host sees a real arrival and removal on the next free pad index, the fall-through rule, how the sticks, D-pad and triggers read a finger, and where the three settings live. `client-settings.md` gains the Quick actions and Virtual controller rows with their availability (the controller is Android and iPhone/iPad only; Apple TV, a Deck and the desktops say why not), and stops counting the Input section's rows — it said five and listed ten. |
||
|
|
acd61b31da |
feat(client/android): the virtual controller over the stream (T14)
Design §4 on Android: the ring's Virtual controller slot is a toggle that draws a controller over the stream and hides it again. While it is up it holds one wire pad on the router — `openExternal(XBOX360)`, the slot model the capture links already use — so the host sees one controller arrive and one leave, on the lowest free index beside any real pad (§9). Its buttons ride `slotButton`, so the chords (`Select+A` opens the ring, the exit hold, the mic and stats chords) and the ring's mask work on it exactly as on a real pad. Every control is its own pointer-input node above the gesture layer and below the ring: a finger on a control is taken, a finger between them never reaches the layer and falls through to the touch mode beneath, so tap-to-click keeps working beside the pad. The face buttons are one node of four discs (a thumb rolling from A to B presses B); the D-pad reads eight ways by angle; a stick is owned by its first finger and follows it from wherever it landed, with a fixed dead zone; a trigger reads the finger's position down its pill, so a slow press is a slow press. A tick per press. Three presets with fixed positions; a narrow layer (a phone upright) stacks the clusters instead of spreading them. Select, Guide and Start are on every preset — a pad that cannot pause is a bug. Router: `ExternalPad.axis` is dropped while the ring owns the pad, like a button. `setRingOpen` had zeroed the axes on the wire, and a stick still held under the ring wrote over that; a capture-link stick had the same leak. Editor: a Virtual controller group under the ring — Layout (Full, Sticks and shoulders, D-pad and face buttons), Opacity and Scale sliders that write on release — through the same `onChange` the ring uses; `SettingDropdown` goes internal for it. The picker's note no longer promises a later release. Left out on purpose: stick clicks (no control for them), a drag-to-position editor, per-game layouts, gyro and touchpad emulation (§4.3). The pad is per session and starts hidden. Gates: `:kit` + `:app` compile through cargo-ndk; `VirtualPadTest` 5/5 (every preset fits and never overlaps at three sizes, the D-pad sectors, the stick travel, the trigger pull) and `OverlayActionsTest` 6/6; the debug APK is installed on the Nothing Phone, which was locked — not yet seen on glass. |
||
|
|
fa0d138df9 |
fix(client/android): the editor's ring opens at the stage's centre
ci / bun-nix (pull_request) Successful in 26s
ci / docs-drift (pull_request) Failing after 36s
ci / docs-site (pull_request) Successful in 57s
android / android (pull_request) Failing after 2m3s
ci / rust-arm64 (pull_request) Successful in 1m56s
apple / swift (pull_request) Successful in 2m16s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / rust (pull_request) Failing after 7m11s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 10m44s
ci / web (pull_request) Failing after 11m20s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 4m20s
The first open used a centre computed at composition, one step behind the size state, so the ring opened at (0, 0) and the clamp pinned it to the margin — up and left of the card by a slot radius. Both effects that open the ring read the live size now. Verified by screenshot on the Nothing Phone. |
||
|
|
6621e5adb1 |
fix(client): the swap settles without a flash; Android's ring is carded
The swap's reset fired at 450 ms, inside the spring's settle, so the two discs jumped their last few points as the contents swapped — a flash. The spring is stiffer (response 0.35 / damping 0.82 on iOS, StiffnessMedium / 0.82 in Compose) and the reset waits 650 ms, past the settle, on both. Android: the ring is in a card again like every other field — an `OutlinedCard` without the group's 16 dp inset, so the stage keeps the width the ring needs and stays centred; the caption sits under it. Gates: Android debug APK on the Nothing Phone; Release xcodebuild on the iPhone. |
||
|
|
4f59fd4267 |
feat(client): a swap in the ring editor plays as a spring
Dropping a disc on another slot swapped the two contents in place. Now the two discs travel to each other's slots on a spring (the dragged one from wherever it was released); once they land the blob is written and the drawing order snaps back to identity with the contents swapped, so that write draws nothing. iOS keeps a per-disc `order` animated with `.animation(value:)` and resets it in a transaction with animations off; Compose animates each disc's position with a spring only while a swap plays and a snap otherwise, so the twist and a drag still follow the finger. Two looks on the editor's stage, both platforms: the ring's scrim (18 % black while open) is off in the editor — it read as a fill — and a stage narrower than two ring margins centres the ring instead of pinning it to one side (the clamp assumed a screen). Android's stage stands outside the settings card, with its caption under it: the card's inset left it narrower than the ring, and a card around a control that draws its own discs read as a fill too. Gates: Android debug APK on the Nothing Phone; Release xcodebuild on the iPhone. |
||
|
|
22cba0cac1 |
fix(client): the ring editor's drag on Android, and four looks
On glass, five things about the quick-actions editor: - Android: dragging a disc onto another slot did nothing. The twist surface sat on the PARENT of the ring, and the stream's touch handler consumes every change it sees, so the discs' drag detectors only ever saw consumed moves and cancelled; a tap survived because it has no moves. The surface is a sibling under the ring now, the way the stream's gesture layer is under the in-stream ring. - The centre disc was live in the editor on both platforms, opening a preview sheet of something that is not editable there. It is drawn dimmed and inert while editing. - The "Tap a button to change it…" line sat on top of the discs. It is the field's caption under it now on both platforms, like every other field's. - Android: the stage had a fill of its own inside the card; it is transparent like the other fields. - Android: the key chips in the shortcut editor laid their label out from the start of a fixed-width chip; the label is centred. Gates: Android debug APK on the Nothing Phone; Release xcodebuild on the iPhone. |
||
|
|
cf63f6cf41 |
feat(console): a shortcut editor for the quick-action ring
The console could point a ring slot at a chord the blob already carried but could not make one. Design §3.3's pad form, on the existing widgets: a "Quick-action shortcuts" action row in the Input tab opens `RingShortcutsScreen` — the blob's chords as a list (name and legend), and an editor for one: a Name field on the on-screen keyboard tray (Steam's on a Deck), Ctrl / Alt / Shift / Win as On/Off rows, the Key stepped with ◀ ▶ through everything `key_vk` knows (wrapping on A), Save, and Remove for an existing one. A new shortcut takes the first empty slot, as on the phones. B peels back: keyboard, editor, screen. Writes rebase on the store's file and save, like the settings screen — this is one more whole-file writer. The blob logic (`apply_draft`, `remove_shortcut`) is pure, and the test drives it directly: the store is one real file shared by every test in the process, and a round trip through it races the settings tests (the same race behind the known `reduce_motion_freezes…` flake). `every_row_has_exactly_one_tab` learns the row (54 → 55). Gates: container `pf-console-ui` tests (216), fmt, clippy; the Android kit build through cargo-ndk, which compiles this crate too. |
||
|
|
7ce448d0c9 |
feat(console): the ring honours Reduce motion
Design §2.3: with reduce_motion on, the Skia ring crossfades in place — the commit snaps to open instead of springing, and the discs sit at their slots from the first frame and only fade; the twist still opens it. The Android and iOS rings already did this (iOS via accessibilityReduceMotion). Gate: container pf-console-ui tests (215), fmt, clippy. |
||
|
|
1c34358d21 |
feat(client/android): the quick-actions editor is the ring itself
Design §3.3 on Android, matching the iPhone: the settings editor shows the in-stream `RingOverlay` — the same composable — full size over a backdrop that runs the real two-finger twist (`streamTouchInput` with a `0` handle: `send_event` drops every wire call, the dial still arms, commits and cancels). Tap a slot to pick its action from the catalogue by group with availability notes; drag a disc onto another to swap; tap the centre to see depth two with inert actions. Whatever closes the ring it springs back open, so the editor is never left with nothing to tap. The backdrop is the theme's `surfaceVariant`, one more field. `RingEditing` on the Compose `RingOverlay`: a slot tap picks instead of firing, an empty slot is a pick target, the scrim takes nothing, the 8 s idle close is off, and `detectDragGestures` on each disc resolves the drop to a sector — it consumes past touch slop, which is what keeps the tap from also firing. `ChordKeycap` and `KeycapDisc` are shared with the editor's previews. Shortcuts match the iPhone's editor: a row per shortcut with its disc, name and legend; tapping it (or Add) opens a screen with the disc and legend previewed live, a name field, the four modifiers as chips, and the key on a grouped keyboard of chips (word keys on wider chips). Remove lives there; a new shortcut takes the first empty slot. Gate: `:app:compileDebugKotlin`; the debug APK for the Nothing Phone. |
||
|
|
a02c574497 |
feat(client/apple): the ring on tvOS, opened by a short Back
Design §2.5: the Apple TV's entry to the quick-action ring is a short press of the remote's Back; a long hold still ends the session. The ring is the same `RingOverlay` the iPhone draws, driven by the pad path that already existed (`RingState.handleNav`, `Select+A`, the capture's `ringOpen` gate) — there is no twist, tap or drag on a TV, so those three paths are `#if os(iOS)` inside the file and the rest compiles for both. What carries it: - `SiriRemotePointer.onShortBack`, fired on a Back released under the 1 s disconnect hold; the model routes it into `onRingChord`, the hook the pad chord already used, so the view wires one closure for both. On tvOS that closure toggles: open at the screen centre, close if it is up. - `ContentView` mounts the overlay on tvOS as iOS does (only while open), pre-fetches host actions at session start, and shares `ringActions`; the twist sink stays iOS-only. - `TouchInputMode` moves out of `TouchMouse.swift`'s iOS-only block: the enum is plain data every platform can name, the gesture machine under it stays iOS. On tvOS the Touch mode slot is dimmed with "Apple TV has no touch screen". Not on an Apple TV yet — no device here. Gates: SwiftPM tvOS, iOS and macOS client targets; a Release xcodebuild of Punktfunk-tvOS for a generic device (no signing), which is the build that has no DEBUG. |
||
|
|
b9838f0024 |
fix(client/apple): a dragged disc no longer also fires its pick
In the editor the disc's Button fired on release after a drag-to-swap, so the picker sheet opened on top of the swap. One high-priority DragGesture(minimumDistance: 0) owns the disc while editing: a touch that stays within 8 pt is the pick, one carried onto another slot is the swap. In-stream the gesture mask is .subviews, which leaves the Button exactly as it was. Gate: Release xcodebuild on the iPhone, installed and launched. |
||
|
|
e4c8e2d838 |
fix(client/apple): keycaps in the shortcut editor keep one height
The word keys (Backspace, PrtSc, PgDn) wrapped inside the 44 pt grid cells, so those caps grew taller than their neighbours. Every cap now keeps one line at a fixed 30 pt and shrinks its text to fit, and the groups that hold word keys (editing, navigation, other) lay out on 80 pt columns; letters, numbers and function keys keep the tight 44. Gate: Release xcodebuild on the iPhone. |
||
|
|
01db9fcdb6 |
feat(client): readable key legends, keycap discs, a shortcut editor
Four things the quick-actions ring and its editor got wrong on glass: - Legends. `chord_chip` printed "❖" for Win/Super, "⇧" for Shift, "↵" for Enter — symbols that read as nothing to most people. It now prints the word a keyboard prints (Ctrl, Alt, Shift, Win, Esc, Enter, Backspace, Del, Ins, PgUp, PgDn, PrtSc, Caps), arrows stay arrows, and `key_legend` exposes the per-key word. Rust, Kotlin and Swift in lockstep, with the contract tests moved to the new text. - A shortcut on a disc drew that legend as one line, which ran to the disc's edge and past it. It is a stacked keycap now: modifiers small on top (on Apple the compact glyphs its keyboards print, ⌃ ⌥ ⇧ ⌘), the key large under them, shrinking to fit — `ChordKeycap` on iOS, the same shape in Compose. - The three host power actions shared one "power" glyph and were indistinguishable on the ring. Sleep is a moon, restart a circular arrow, shut down the power symbol, on iOS and Android. - The iOS shortcut editor was a bare add sheet. Each shortcut is a row with its disc, name and legend; tapping it (or Add) opens an editor with the disc and legend previewed live, a name field, the four modifiers as chips, and the key on a grouped keyboard you tap (function, letters, numbers, editing, navigation). Remove lives on the same sheet; a new shortcut still takes the first empty slot. Also `every_row_has_exactly_one_tab` learns the seven ring rows the T19 commit added (47 → 54): that test had not run since — the container script tests only `pf-presenter` unless `TEST_EXTRA` names more crates. Gates: Release xcodebuild on the iPhone; container `pf-client-core` (237) and `pf-console-ui` (215) tests, fmt, clippy; Android compile + `OverlayActionsTest` 6/6; Swift `OverlayActionsTests` updated in step but not run (needs the macOS core rebuilt). |
||
|
|
4cfc50ee2b |
fix(client/apple): the quick-actions editor is a sheet, not a push
Leaving the editor on the iPhone dropped the user on the category list and no category could be entered again. The settings detail column is deliberately not a NavigationStack (an inner one doubles the title bar on iPad), so a NavigationLink pushed from it rode the collapsed NavigationSplitView's own stack and, on pop, took the `List(selection:)` binding with it. AboutView's Shortcuts and Acknowledgements rows had met the same thing and are sheets for it; Quick actions follows them: a plain row that presents the editor in its own NavigationStack with a Done button. The backdrop is the Form's cell colour (`secondarySystemGroupedBackground`, resolved dark under the editor's scheme), so it reads as one more field rather than a bright stage — the grey was too bright. Trap: the new `@State` first landed inside `#if DEBUG && !os(tvOS)`, so the SwiftPM (debug) gate passed while the Release xcodebuild failed. It sits with the unconditional properties now. Gates: `pf-touch-apple-check.sh ios|tvos PunktfunkClient` exit 0; the Release xcodebuild for the iPhone succeeded and is on the device. |
||
|
|
4d613ce033 |
fix(client/apple): the editor's ring no longer closes on a pick
On the iPhone, after one or two picks the editor went dead: nothing on the ring answered a tap. UIKit hands a tap on a disc to the backdrop's `DialCatcherView` as well as to the SwiftUI button above it, so its tap recognizer ran `ring.close()` on every pick. The ring wound in behind the picker sheet, and a closed ring's discs are not hit-testable. The backdrop tap now only dismisses the preview sheet, and whatever closes the editor's ring — a twist wound back past the commit angle, a preview row that ends the stream — the ring springs back open once the wind-in has played (`onChange(of: ring.closing)`), so the editor is never left with nothing to tap. The caption no longer promises a twist-to-open state that cannot last. The backdrop is a plain grey now, as asked, instead of the gradient. Gate: `pf-touch-apple-check.sh ios PunktfunkClient` exit 0. |
||
|
|
7526e6f2b3 |
feat(client/apple): the quick-actions editor is the ring itself
Design §3.3, "the editor is the ring": the settings editor now shows the in-stream `RingOverlay` — the same type, not a copy — full size over a gradient backdrop, and it answers the real two-finger twist with the real thresholds. Tap a slot to pick its action from the catalogue by group (Session, Input, View, Audio, Host, Shortcuts, Empty, each with its availability note); drag a disc onto another slot to swap the two; tap the centre to see depth two with inert actions. The shortcuts list and Reset to default stay under the ring. In a profile that owns its ring, a line under it says so. What carries it: - `DialCatcher` (PunktfunkKit): a UIKit surface running the stream's `TouchMouse` with its wire unplugged, so only the dial's arm, commit and cancel come out; a one-finger tap reports separately and closes the ring (or the sheet) in the editor. - `RingEditing` on `RingOverlay`: a slot tap picks instead of firing, an empty slot becomes a pick target, the scrim passes touches through to the backdrop, the 8 s idle close is off, and a `DragGesture` on each disc resolves the drop to a sector (released near the centre or on its own slot it springs home). - `HostAction` gains a public init so the editor previews the three power slots as offered. The six-Picker list form from the previous commit is gone; the ring on appear plays the same spring it does in-stream. Gates: `pf-touch-apple-check.sh ios|tvos PunktfunkClient` both exit 0. On the iPhone for feel; drag-to-swap and the twist over the backdrop are the two things to try. |
||
|
|
b1e63bff02 |
feat(client): a quick-actions editor on all three shells (T19)
The `overlay_actions` blob (design/touch-client-overlay.md §3.2) had parsers and profile plumbing on every shell but no way to change it. This is the list form of §3.3: pick what each of the six slots holds, add or remove custom shortcut chords, and reset to the platform ring. The ring-as-editor with drag-to-swap and the twist tutorial is still owed; this is the smallest editor that makes the blob reachable. iOS: a "Quick actions" row under Touch & pointer, bound through `scoped(SettingsFields.overlayActions)` like every other profileable row, with the row's override marker and Reset. The editor (`QuickActionsEditor.swift`) is six Pickers, a shortcuts list with swipe-to-delete, an add sheet (label, modifier toggles, key picker, chord preview) and a Reset to default that drops the override in profile scope or clears the global. A new shortcut takes the first empty slot (§3.3). Android: the same shape as a deep sub-screen (`QuickActionsScreen.kt`, opened from a Quick actions row in Input with its override badge), writing through the screen's `update` so the profile diff records the whole blob. `SettingsGroup` goes internal for it to reuse. Desktop and the Android console (Skia): six `RingSlot` rows and a `RingReset` action row in the Input tab. ◀ ▶ step a slot through the catalogue — empty, the built-ins, the three host power actions, then the blob's own shortcuts — and write the whole blob back. Shortcut CREATION is not on the console yet (it needs the on-screen keyboard flow); a slot can point at a chord the blob already carries. The console edits the global blob only: it never edits profiles, and D9's per-row scope picker is new machinery left for later. One test drives the stepping, the clamp at Empty and the shortcut option. The slot catalogue is the same list in the same order on all three shells; host actions are the three advertised power ids. Gates: `pf-touch-apple-check.sh ios PunktfunkClient` exit 0; Android `:kit`+`:app` compile through cargo-ndk, JNI floor clean; `cargo fmt`; `pf-console-ui` tests in the container. Not yet on glass. |
||
|
|
6618d07869 |
fix(client/android): the sheet's mode row re-reads the live mode
The Resolution and Refresh rows showed the mode the sheet last asked for, on the belief that Android had no live read-back of the negotiated mode. It has one: `nativeVideoSize` reads `NativeClient::mode()`, which the control task overwrites with the host's `Reconfigured` ack (`pump/control_task.rs`). So a rejected request left the row lying for the rest of the session. The request still writes the asked-for mode at once (the ack lands off the composition), then re-reads `nativeVideoSize` 500 ms later so a rejection shows through. The Kotlin doc on `nativeVideoSize` no longer calls the value fixed for the session. No new JNI: the `nativeCurrentMode` the plan proposed would have duplicated it. Gate: `:kit:compileDebugKotlin :app:compileDebugKotlin` through cargo-ndk, JNI floor check clean. |
||
|
|
5adb5ad99e |
feat(client/apple): the ring's discs spring out one by one
On iOS the six slot discs and the centre now each ride their own spring from the centre to their ring position when the ring opens: the centre first, each slot 25 ms behind the last, on a bouncier settle (response 0.4, damping 0.62) than the whole-ring spring had. Scale and opacity ride the same spring. Before, one scalar placed every disc and the discs were inserted with `if q > 0`, so a disc that first appeared at commit had nothing to animate from: the twist path sprang, the corner disc and `Select+A` popped in place. The discs now stay in the tree at 0 while the overlay is mounted, and a `phase` (twist / open / closing) picks each disc's target and animation through `.animation(_:value:)`. During the twist they still follow the finger frame by frame with no animation; the spring takes over at commit from wherever they are. Closing: `RingState.close()` on an open ring raises `closing` for 140 ms, so the overlay stays mounted while the discs ease back in (ease-in 120 ms) and then leaves the view hierarchy as before (tenet 1). A cancel short of commit still leaves at once. Reduce Motion keeps the plain 120 ms fade with no spring and no stagger. tvOS is untouched (the file is `#if os(iOS)`); Android and the Skia ring are unchanged. Gates: `pf-touch-apple-check.sh ios|tvos PunktfunkClient` both exit 0. Not yet on glass. |
||
|
|
83cefd3c3f |
fix(client): a drifting twist no longer locks itself into a scroll
On the Nothing Phone the two-finger twist never opened the ring. Real fingers never pivot about a fixed point: the centroid drifts a few px per sample while the pair turns, and the Android scroll path fires a notch per 4 px of centroid travel. The first notch is final (scrollEmitted), so the gesture was a scroll before it could turn 10°. The Rust engine shares the 4 px notch; Swift's is 10 pt against a 16 pt slop, so the iPhone mostly got away with it. While the pair is undecided — under DIAL_SLOP of travel and under DIAL_ARM_DEG of turn — no notch goes out, and the scroll anchor follows the centroid so a real scroll starts smoothly once it crosses the slop. This is what the design's slop rule meant (§2.1: travel past the slop before arming means scroll); the engines just ran the notch first. A new Rust test drives a twist whose centroid drifts ~14 px over five samples: no Scroll act, the dial arms, and 35° commits. The pan test that mixed a scroll with a rotation now pans 30 px, past the slop. |
||
|
|
577def855a |
feat(client): Select+A opens the ring, and the pad drives it
ci / bun-nix (pull_request) Successful in 31s
ci / docs-drift (pull_request) Failing after 38s
ci / web (pull_request) Successful in 57s
ci / rust-arm64 (pull_request) Failing after 1m9s
ci / docs-site (pull_request) Successful in 1m10s
android / android (pull_request) Failing after 2m18s
apple / swift (pull_request) Successful in 2m6s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / rust (pull_request) Failing after 6m59s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 3m35s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m33s
A pad user had no way into the ring: every existing in-stream chord forwards its buttons (a game sees the X in Select+X), and nothing in-stream consumed pad input on any client (design/touch-client-overlay.md §2.6). `Select+A`, Select first, is the first swallowed chord. All three routers already hold a lone Select back for the guide hold; an A inside that window opens the ring and neither press nor release reaches the host. While the ring is up the pad belongs to it: the desktop masks the pads and polls the first into menu events, Android and Apple release held state on the host and adopt the hardware state silently; on close nothing is replayed. The D-pad and left stick step the highlight (Right clockwise, Up to 12 o'clock, Down to 6, Y to the centre), A fires it, B closes, and the sheet becomes a cursor list on every shell so the same pad reaches the resolution row. |
||
|
|
86c8980b66 |
fix(console): the Skia ring is desktop-only
ci / docs-drift (pull_request) Failing after 38s
ci / bun-nix (pull_request) Successful in 52s
ci / docs-site (pull_request) Successful in 1m10s
apple / swift (pull_request) Successful in 2m18s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 56s
android / android (pull_request) Failing after 2m25s
ci / rust-arm64 (pull_request) Successful in 5m22s
ci / rust (pull_request) Failing after 10m42s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 3m13s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m43s
pf-console-ui also builds for the Android GL host, where the ring's host-action cache is configured out; the Android JNI crate then failed to link the console. The Compose ring serves Android. |
||
|
|
4c5d462dad | Merge touch/phase-0 into touch/dial-model | ||
|
|
4f1b71edf6 |
feat(console): the quick-action ring and its sheet in-stream on Skia
android / android (pull_request) Failing after 2m12s
apple / swift (pull_request) Successful in 2m8s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m37s
ci / web (pull_request) Successful in 2m22s
ci / bun-nix (pull_request) Successful in 1m1s
ci / rust-arm64 (pull_request) Successful in 3m52s
ci / docs-drift (pull_request) Failing after 1m18s
ci / rust (pull_request) Canceled after 8m17s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Canceled after 3m9s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Canceled after 0s
The desktop session had no in-stream menu: the ⌃⌥⇧ chords were the only way to end a stream, cycle stats or mute, and a Deck touch user had none of them (design/touch-client-overlay.md §0.1). The ring opens under the fingers from the two-finger twist, frame by frame, or at the window centre from ⌃⌥⇧O; End stream is a slot behind a two-press arm. Six translucent discs carry short text labels (the console has no icon font); the centre opens the sheet — a MenuList with the full catalogue, where Left/Right on the Resolution and Refresh rows cycle the presets over request_mode. While the ring is up it owns the glass: pointer and key events go to it, touch fingers stop feeding the gesture engine, and it takes the damage key so a settled ring redraws nothing. The presenter side is one portable contract in pf-client-core::ring (RingInput / RingCommand / RingFacts), three default methods on the Overlay trait, per-frame facts on FrameCtx, and SessionOpts carrying the profile's overlay_actions. Host-action slots are pre-fetched at connect and ride the console's own command bus. Touch mode becomes switchable mid-stream (Capture::set_touch_mode) and a shortcut chord goes out as VKs down then up. |
||
|
|
67bdadda9b |
feat(apple): the quick-action ring and its sheet in-stream on iOS
A touch user had one ✕ disc that ended the session and nothing else: no resolution switch, no host actions, no shortcuts, no way to change the touch model mid-stream (design/touch-client-overlay.md §0.1). The ring opens under the fingers from the two-finger twist, frame by frame, or from the corner disc, which now opens it instead of ending the session; End stream is a slot behind a two-press arm. Six glass slots come from the resolved profile's overlay_actions; the centre opens the sheet with the full catalogue and the resolution presets over requestMode. It leaves the hierarchy entirely when closed (tenet 1). The dial reaches SwiftUI through a public DialEvent and a StreamView.onDial hop; the Keyboard slot summons the soft keyboard by notification; Touch mode gets a session-scoped override. Haptics: a tick when the twist arms, a thump at commit, a tap per press, a firm "no" on a dimmed button, a warning when a destructive slot arms. |
||
|
|
7e073472b8 |
feat(android): the ring's haptic vocabulary
One thump at commit was the only feedback. The ring now ticks when the twist arms, taps on every press, thuds firmly on a dimmed button, and warns when a destructive slot arms — through the console's ConsoleHaptics, so it follows the system haptics setting. |
||
|
|
8696407897 |
feat(android): the quick-action ring and its sheet in-stream
A touch user had no in-stream menu: no way to end the stream except an unconfirmed back gesture, no resolution switch, no host actions, no shortcuts (design/touch-client-overlay.md §0.1). The ring opens under the fingers from the two-finger twist, frame by frame, or at the screen centre from Back — which no longer ends the session; End stream is a slot behind a two-press arm. Six slots come from the resolved profile's overlay_actions; the centre opens the sheet with the full catalogue and the resolution presets over nativeRequestMode. Toggles leave the ring open, commands close it, a tap outside or 8 s idle closes it, and it leaves the composition entirely when closed. Host actions are pre-fetched on the session tick, never when the ring opens. Touch mode becomes live per session. |
||
|
|
f910f88879 |
feat(core): shortcut key names map to virtual-key codes
A ring shortcut stores key NAMES so one profile fires on every client; the wire speaks Windows VKs. key_vk (Rust, Kotlin) is the shared table: modifiers, navigation keys, f1–f24, letters and digits; an unknown name means the chord does not fire and the editor says so. |
||
|
|
4c493de046 |
feat(client): a two-finger twist opens the quick-action ring
No gesture engine tracked the angle between two fingers; two-finger handling was a centroid, and the ring needed an opener no game, host or OS claims (design/touch-client-overlay.md §2.1). Each engine now keeps the finger-to-finger vector from the second finger's landing and compares it on every move: 10° arms the dial and suppresses scroll, 30° commits (the ring stays open after the lift), a lift short of commit cancels and sends nothing, a scroll notch already sent means the gesture is a scroll for its lifetime, a pinch is nothing. Progress and direction reach the shell as Dial events; the ring views consume them next. SDL reports the two fingers of one frame as separate events, so the desktop judges the twist only when the other finger's position is from the same frame or older than a pivot's stillness — halfway through a plain scroll step the vector reads as a 60° turn. |
||
|
|
5f54a1755b |
feat(core): overlay_actions, the quick-action ring's one setting
The in-stream ring (design/touch-client-overlay.md §3) is configured by ONE JSON blob, never by a dozen booleans: every cross-client setting costs about six hand edits per client, and a missed edit ships a feature on some clients only. `overlay_actions` is a tier-P field on Settings and SettingsOverlay (the whole blob overrides, D10), mirrored in Swift and Kotlin with the existing profile plumbing. The parser is lenient by contract: six slots, unknown ids and dangling shortcuts become empty slots, a broken blob is the platform default. The Rust tests are the contract; the Swift and Kotlin ports carry the same five tests. |