Repository navigation
layout: a place's corner is glass, and turns into its button - #555
Merged
Merged
Conversation
foxnne
force-pushed
the
layout/corner-dot
branch
from
October 18, 2026 15:46
3d6be35 to
b7cf4bf
Compare
foxnne
force-pushed
the
layout/corner-dot
branch
from
October 18, 2026 15:48
b7cf4bf to
1b44ef0
Compare
This was referenced Oct 18, 2026
foxnne
added a commit
that referenced
this pull request
Oct 18, 2026
Fixes #547. Serves #555 (plans#27), whose corner button needs to tell touch from mouse. ## What changes - **Hand cursor:** panel tabs and rail icons (everything drawn through `core.widgets.Tabs`) and document tabs show the hand under a mouse. - **Splits keep their arrow:** within a split's reach of its sash, the split's resize arrow still wins over a tab's hand. - **No hover after a tap:** on a touchscreen, a tapped tab, rail icon, menu row or button no longer stays lit, and its tooltip no longer comes up once the finger lifts. - **macOS trackpad:** a trackpad's fingers no longer reach dvui as `touch0` presses at the finger's position on the pad. They are mouse input only, as they already were through SDL's mouse events. **For plugins:** `dvui.pointerKind()` (`.touch` or `.mouse`) and `dvui.cursorSetPriority(cursor, .normal | .resize | .drag)`, both from dvui-dev. #555 should read `dvui.pointerKind() == .touch`. ## Design: the pointer kind and cursor priority seam Two questions every hover look and cursor needs answered once a frame: *is this pointer a finger or a mouse*, and *does a resize handle claim the cursor here*. **Pointer kind: from the event source, or inferred by fizzy?** - **A. dvui's window says it (chosen).** dvui-dev's `Window` sets `pointer_kind` as the backend adds events: a touch button (`touch0`..`touch9`, from `addEventPointer` or `addEventTouchMotion`) is `.touch`; mouse motion, a mouse button or the wheel is `.mouse`. Read with `dvui.pointerKind()`. The window is the one every plugin dylib and wasm side module already draws into, so they all read the same answer, with no frame key and no module global. dvui also keeps which touch buttons are down: after a touch with none down, the frame's `.position` event goes off every window and to no widget. A lifted finger hovers nothing, so dvui's buttons, menu rows, tooltips and `core.widgets.hovered` all stop showing hover after a tap without a check of their own. The backend decides what a touch is. SDL reports a macOS trackpad's fingers as touches (`SDL_TOUCH_DEVICE_INDIRECT_*`, at the pad's normalized point), and fizzy's backend passed them on as `touch0` presses at that point mapped into the window. Nothing in fizzy or the store plugins read them as anything else: every touch consumer (`CanvasWidget`, `Tabs`, `ContextWidget`, `viz`, pixi, atlas) reads touch buttons as fingers on the screen. So the backend gives only `SDL_TOUCH_DEVICE_DIRECT` fingers a touch button, one per finger (`touch_slots.zig`), and the trackpad stays the mouse it already is. - **B. fizzy infers it** (`core.input`, scanning each frame's events for touch buttons). This is what #542 tried. State in a module is per image (each dylib and wasm side module has its own), a per-frame key needs a frame id that is unique on every backend, and a hover look in a dvui widget (a menu row) would still need dvui changed. Putting it on `Host` instead would add an SDK seam for something dvui already sees. **Cursor: priority in dvui, or ordering in fizzy?** - **A. `dvui.cursorSetPriority(cursor, priority)` (chosen).** The highest asked in a frame wins, and between equals the first. `cursorSet` is `.normal`, as before. A resize handle asks at `.resize` (fizzy's `Split`, float edges; dvui's `PanedWidget`, `GridWidget` columns and floating window edges), and the carried view at `.drag`. A tab's `.hand` can't take a split's arrow, whichever is drawn first, and *which widget takes a press doesn't change*. - **B. The split claims ahead**: it handles the pointer before the region beside it draws. #542 tried this and dropped it, because it took presses from scrollbars near a sash and took the cursor during other widgets' captures. A tab checking how close it is to a split instead would tie the tab to the layout. **For #555 (plans#27):** the read is `dvui.pointerKind() == .touch`. It replaces the corner's `_chooser_touch` bool. A finger that lifted leaves no `.position` hit, so the corner's mouse-hover reveal won't fire after a tap. ### The pieces - **dvui-dev** (`fizzy-dev`, tag `fizzy-sdk-0.2.30-5`, `docs/DEPENDENCIES.md` items 11 and 12): - `e04bccc` Window: the pointer kind, and a lifted finger hovers nothing - `20e56f1` cursorSet: a priority, so a resize handle's cursor wins - **Backend:** `backend/src/touch_slots.zig` is std-only and unit-tested. Each finger on the screen keeps its own touch button from down to up, so two fingers are two buttons on native touchscreens too. Before, every finger was `touch0`, and `FINGER_CANCELED` was dropped. - **Users:** - `Tabs.Tab` asks for `.hand` on its `.position`. - The workbench's document tab asks for `.hand` when hovered. - `Split.grab` and the float's resize edges ask at `.resize`. - `Layout.init`'s view-drag cursor asks at `.drag`. The cursor priority doesn't change routing. A press over the edge of a tab inside a split's reach still goes to the tab, drawn first, as it did before this PR (a test pins this). That mismatch, an arrow shown where the press goes to the tab, already existed. Changing it means changing routing, which #542 showed has costs. It's left as a follow-up. ## SDK impact - [ ] None - [ ] Core-only or additive: reaches plugins at the next SDK release - [x] Fingerprint moved: recorded in `sdk/src/version.zig`, `sdk_version` untouched, PR labelled `sdk` - [ ] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the last `sdk-v*` tag The dvui pin moves from `fizzy-sdk-0.2.30-4` to `fizzy-sdk-0.2.30-5`, because `Window` gained `pointer_kind`, `touches_down` and `cursor_priority`. The fingerprint is now `0x35bac8377071cd23`, on top of #538's. For plugin authors: - `dvui.pointerKind()` and `dvui.cursorSetPriority` are new. - `cursorSet` behaves as before. - A plugin's own hover looks lose their hover after a tap with no change on the plugin's side. ## Verified - [x] macOS: - **Gates:** `scripts/check-fmt.sh`, `zig build`, `zig build test` (107/107 steps), `zig build test-integration` (54/54 steps, 601/601), `zig build check-web`, `zig build test-sdk-version`, `zig build check-tests-reached`, `scripts/check-examples.sh` (all 8 ok). - **dvui-dev:** `zig build test -Dbackend=testing`, 268/268. New tests: - "a finger that lifts hovers nothing, and a mouse hovers again" - "cursorSetPriority: the higher asked wins, whichever asked first" - **New fizzy tests** (`core/widgets/Tabs.zig`, dvui testing backend, under `fizzy-sizing-tests`): - "a tab shows the hand" - "beside a split, the split's arrow wins over the hand, and a press goes where it did". With `Split` set back to plain `cursorSet`, this one fails with "expected .arrow_w_e, found .hand". - "a tapped tab is left without a hover look" - **Backend unit tests** (`backend-touch-slots-tests`): - "a trackpad's fingers get no slot" - "each finger on the screen keeps its own slot until it lifts" - "an eleventh finger is not seen" - **Not seen on screen:** the hand and the arrow in a running fizzy, and a trackpad in the sandbox. - [ ] Windows: builds in CI only. The direct-touch path is untested on a touchscreen. - [ ] Linux: - [ ] Web: `check-web` only. The phone tap that leaves no hover look needs seeing on a phone. ## Follow-ups - **Ad-hoc touch tracking** in `core/widgets/CanvasWidget.zig` (`last_input_was_touch`) and `core/viz/viz.zig` (`touch_pointer`) could read `dvui.pointerKind()` instead. - **Arrow shown, press to the tab:** inside a split's reach, over the tab drawn before it, the arrow shows but a press goes to the tab. This was already the case before this PR (see above). - **Stale comment:** `app/store/PluginStore.zig` says "last cursorSet call wins". It is the first, so its `.arrow` override doesn't do what the comment says. - **Upstream:** propose both dvui-dev changes to upstream dvui. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A filled place's card wears a piece of glass in its top-right corner, cut to the card's curve. Only it brings the button up, the piece itself turning into the button and back: the pointer on it, or a tap. A second tap opens the picker; the button goes after 3s or at the next touch elsewhere. A pointer or touch merely near the corner no longer does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
force-pushed
the
layout/corner-dot
branch
from
October 18, 2026 18:39
1b44ef0 to
a82af81
Compare
foxnne
marked this pull request as ready for review
October 18, 2026 18:39
foxnne
added a commit
that referenced
this pull request
Oct 18, 2026
This was referenced Oct 18, 2026
foxnne
added a commit
that referenced
this pull request
Oct 18, 2026
Part of fizzyedit/plans#5 (`plans/ZIG_0_17_PLAN.md`, step 2): fizzy, its backend and its bundled plugins move to Zig 0.17. Every piece was reviewed as its own PR into `zig/0.17`, and this PR lands that branch. `main` is already merged in (through #555), so it carries no conflicts. ## What changes **The move to 0.17** (`minimum_zig_version = "0.17.0"` everywhere), as reviewed in: - #437: the switch, scaffolded with marked stand-ins - #438: the build's custom steps become helper programs - #448: release packaging on 0.17, with Velopack 1.2.161 (+ #452's zon arenas) - #488: settings and command schemas read 0.17's `@typeInfo`, with a test that the SDK fingerprint hashes the same facts as on 0.16 - #489: per-pointer capture through `captureMouseGet` - #494: the Windows traps in `docs/PITFALLS.md` - #496: `blitData` without an `extern struct` `@bitCast` - #498: `fizzy.plugin.installBeside`, for a plugin's helper program - #505: the 0.16 inflate workaround goes; the docs say 0.17 - #561: review fixes. A settings payload with pointers is a compile error, and build reads go through `b.root`. - #573: the plugin install helper compiles once, in Debug - the catch-ups with `main` (#486, #503, #558, #574, and the last merge here), each with `scripts/zig-0.17/rewrite.sh` re-run. That script is deleted in the last commit. Most of the diff is mechanical: `zig fmt`'s upgrades (`@backingInt`, `std.lang`, …) and one-answer renames. The hand migrations are the build (configure/make split, no custom steps), the zon arenas, `@typeInfo`, translate-c for `@cImport`, and the `@bitCast` audit. A high-effort review of the whole `main`…`zig/0.17` diff found five things; the three correctness ones are fixed in #561, and one efficiency one in #573. **Also on the branch, merged there by the maintainer:** the windowing work. - #480–#483: plain dvui apps on macOS - #492, #501, #504, #510: OS windows for floating windows, full screen, pointer routing - #546: window materials **Forks:** dvui-dev at its `zig-0.17` head `1b29a16`, which carries fizzy-dev's patches on upstream's 0.17 `main`, including #540, #544 and #563's dvui commits. Also assetpack, md4zig, sdl_zig and zig-objc on their `zig-0.17` branches, and velopack-zig `3d553b7` (tagged `fizzy-zig0.17`). ## Measured macOS arm64, under load from other sessions, each step alternated between the two. 0.16 is `main` #509; 0.17 is `zig/0.17` with #561. | | Zig 0.16 | Zig 0.17 | |---|---|---| | App, cold Debug build | 94.4 s | 78.6 s | | App, warm no-op rebuild | 0.7 s | 0.4 s | | `test-integration` (compile + run) | 108.9 s | 90.4 s | | App, ReleaseFast build | 142.8 s | 131.5 s | | Image plugin, cold standalone build | 11.6 s | 14.0 s (with #573) | | Startup, Debug (median of 5) | 565 ms | 466 ms | | Startup, ReleaseFast (median of 5) | 175 ms | 177 ms | | Text, ReleaseFast, µs/frame: idle / mid / scroll / typing | 305 / 424 / 435 / 403 | 240 / 306 / 286 / 290 | | Markdown, ReleaseFast, 4 docs, median of 5, µs/frame: idle top / mid / scroll / resize | 1177 / 1735 / 2104 / 1319 | 1270 / 1699 / 1833 / 1192 | The one slowdown is markdown idle at the top of a document, +8%. The rest of the plugin build's gap is dvui's own translate-c on 0.17. ## The plan's gate - **ZLS:** no 0.17 release yet (latest 0.16.0). zigtools/zls#3270 is on ZLS's master. Completions across `fizzy_sdk` from a plugin are not checked. - **Minimum OS:** 0.17 needs Darwin 15+ (OS X 10.11); fizzy's macOS minimum is 11.0, so no change. Windows 10+ and Linux 5.10+ are not stated by fizzy. - **Debug leak reports:** still print (`DebugAllocator`, deprecated, unchanged). The move to `SafeAllocator` is a follow-up with its own Debug frame-time check. - **Velopack:** packaging proven by a test release on macOS (signed and notarized), Windows and Linux. An update in place was not tested. - **Every target builds:** in CI. Windows and Linux were not run by hand on the VMs. ## SDK impact - [x] Fingerprint moved (`0x5f9c818a6972efd4`, recorded in `sdk/src/version.zig`); `sdk_version` untouched. `sdk: release 0.2.30` follows this at once. Plugins need Zig 0.17 to build against it: there's no overlap window. - Plugin authors see `minimum_zig_version = "0.17.0"` and `fizzy.plugin.installBeside`. A settings payload holding pointers other than a string is now a compile error. `docs/PLUGINS.md` says 0.17.0. ## Right after this merges 1. fizzyedit/example-app#8 and fizzyedit/example-plugin#13 merge. Then a one-line PR points `scripts/check-examples.sh` back at `main` (it pins the examples' `zig-0.17` branches until then, so `main`'s CI is never red). 2. `sdk: release 0.2.30`. 3. The store plugins' 0.17 PRs repin and release: fizzyedit/pixi#51, zig#15, atlas#16, drive#13, ghostty#14, agent#23, chat#16. ## Verified - [x] macOS (arm64, Zig 0.17.0), on this tree: - `scripts/check-fmt.sh` - `zig build`: 71/71 - `zig build test`: 109/109 steps, 637/638 (1 skipped) - `zig build check-tests-reached`: 998 tests in 122 files - `zig build check-web`: 42/42 - `zig build test-sdk-version`: 35/35 - `zig build test-integration`: 78/78 steps, 608/608 - [x] All seven store plugins built against `zig/0.17`'s `sdk/` and loaded together in a 0.17 fizzy (Debug, sandbox profile). - [ ] Windows, Linux, the example repos: CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2 of 5 tasks
foxnne
added a commit
that referenced
this pull request
Oct 18, 2026
No conflicts, and nothing to port: the branch builds and passes its gates with Zig 0.17.0 as merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
added a commit
that referenced
this pull request
Oct 18, 2026
Now on Zig 0.17: `main` (with #575, the 0.17 switch, and #555) is merged in, and the gates pass with 0.17.0. Part of #523 ## What changes Opening a place's view picker (its corner button, or Layout settings) no longer pays for every view's picture in one or two frames. - **The picker chooses, one view at a time.** As the picker draws its cards, it chooses the next view to draw offscreen when none is in flight (`Picker.nextOffscreen`, `State.offscreen`). The choice is a card the list laid out in view, whose view may be drawn offscreen (`Layout.mayDrawOffscreen`, the one rule the capture step checks too): capture works here, the view has no picture or failed capture, isn't hidden, and drew nowhere this frame. A card doesn't count on its own first frame, when every card packs into the top of a list without sizes. A view scrolled or filtered out of the list before it is chosen is never drawn for it. - **The capture step stays where it was**: after the shape, before the picker, as on `main`. `captureUnplaced` warms the chosen view and then captures it, one offscreen draw a frame. The warm-up is at least 10 frames and at least a reveal's fade at the interface's speed, so a fast display doesn't capture mid-fade. - Running before the picker means whatever the view opens (a popup, a float) stacks under the picker, and a picture taken there shows on its card that same frame, with no extra refresh. - The view is found by its registry index, checked against its id's hash, and rescanned only if the registry changed. Its offscreen box is keyed by the id. - It is dropped once that rule no longer holds, for example when it is drawn on screen this frame. It is never drawn on screen and offscreen in one frame. - If the view's card leaves the list early in the warm-up (filtered out or scrolled away), the view is dropped, to be chosen again if the card comes back. Later in the warm-up, it is finished. This is decided by frame stamps: the card's `card_ns` against `Picker.list_ns`, when the list last laid its cards out. A picker draw that returns before its list never drops a view. - **Failures** are recorded for the picker's session in `State` and reset when it reopens. - A view whose capture fails (its picture couldn't start, or gave no texture) goes in an unbounded set (`recordCaptureFailed`, `captureFailed`) and is never retried. Nothing else is affected. - Capture is given up for the session (`State.capture_unavailable`) only after three offscreen captures in a row couldn't start (`noteOffscreenStart`). Their target is small and fixed, so that means the backend has no texture targets. Once given up, nothing more is captured, on screen or off, and no frame is asked for one. - An on-screen view that couldn't start (a too-large target, say) is only that view's failure. - No timers, and no retry loop. - **Frames are asked for** while a view is in flight, and while a view on screen lacks its picture, for example one that drew before the picker opened. A failed view, or one in a place with no room, asks for none: its card shows an empty tile until it has size. - **On-screen views** are still captured where they draw, as on `main`, unless capture has failed for them or here at all. They render that frame anyway, and in the measurements below they never showed in a frame over 9 ms. - **Profiler scopes:** the picker's own work shows in the profiler (devtools dashboard, `devtools.stats`) as `picker`, `picker: capture unplaced` and `picker: capture`. They cost one branch each while the profiler is off. Nothing changes while the picker is closed: `captureUnplaced` still returns on `snapshots_wanted`, and `drawSolid` adds two checks behind it. ### What was measured #523 was filed as a pixel flash: the on-screen views redrawn offscreen and blitted back for a frame. On macOS I could not reproduce that. - I read the frame target back for frames N-2..N+15 around a picker open, with `drawCaptured` on and with it skipped. The frame where the on-screen views were captured matched the frame before to within one level out of 255 (65k pixels at ±1, 6 at ±2; the rest identical). - Settings: default, and with float windows, native glass, menus and dialogs all off. - Content: a folder, a text document, and an image with its glass Fit button. What was there is a stall. These are profiler numbers per frame over 9 ms, with a temporary probe opening the Sidebar's picker. The machine was shared and loaded (1-minute load 10–18), Debug build, two runs each, before and after alternated. | frame | before (main) | after (this branch, first version) | |---|---|---| | picker opens | 19–23 ms: devtools dashboard, store and settings all drawn for the first time (pie 6 ms, store 2.5–3 ms, settings 1.7 ms) plus the picker's first draw (3 ms) | 15–20 ms: one of them (the dashboard: pie 6–8.5 ms) plus the picker (3.7–5 ms) | | ~10 frames later | 20–26 ms: all three captured at once | 17–29 ms: the dashboard's capture alone; store and settings (2–4 ms each) on frames after it | The "after" column is an earlier version of this branch, so it is not this branch's final number. Since then: - The final form draws nothing for an unplaced view on the frame the picker opens, or on its list's first frame. It then draws one view a frame instead of warming several at once, so the dashboard's first draw and its capture each still land in a frame of their own. - The on-screen copy from the last frame was dropped: it bought little, since on-screen captures never showed in frames over 9 ms. - The review rounds: one view in flight chosen by the picker, failures remembered per session. None of these were re-measured in the app: no visible sandbox runs. **Why a capture costs so much:** it is the one draw of an offscreen view that renders anything. The warm-up draws at its offscreen position are clipped away and lay out only. So a capture costs a full render of the view: 8–12 ms each for the devtools dashboard's pie and table in Debug. Tried and dropped: photographing the on-screen views from the last frame (`core.FrameTarget.snapshot`, as `ViewDrag.photographFromFrame` does) on the frame the picker opens. On macOS it removed no visible flash, and it needed per-frame state. ## SDK impact - [x] None ## Verified - [x] macOS: before/after above, profiler read through `devtools.stats` over the control socket from a sandbox fizzy (`--profile`), picker opened by a temporary probe, never committed. New integration test, which fails with the in-view check removed. It uses only views no place shows, so the first capture is offscreen. - Nothing is drawn for a card on the picker's opening frame or its list's first frame. - No frame draws two of the views. - The cards in view are a prefix of the list, some but not all of it, measured from the cards' tags rather than assumed from the window size. - The window is checked to be the shim's 600 by 400, so the fold is fixed. - The first three cards in view are drawn in turn. No offscreen capture can start in the testing backend, so after three capture is given up, and nothing more is drawn. - Reopened with the views in view already failed, none past the fold is drawn. - Taking those views out of the list (hidden; the testing backend's wheel and typed input don't reach the popup) brings the next card into view, and its view is drawn. - Gates, on the merge with `main` under Zig 0.17.0: `scripts/check-fmt.sh`, `zig build`, `zig build test`, `zig build test-integration`, `zig build check-web`, `zig build check-tests-reached`. - [ ] Windows: - [ ] Linux: the pixel flash #523 describes is unconfirmed (no Linux run); this PR does not address it, so #523 stays open. - [ ] Web: builds (`check-web`); not run. ## Follow-ups - **The devtools dashboard's capture is still one 10+ ms frame** (Debug) while its card is in view: its pie and table cost that much to render once. Capturing at a smaller size, or the dashboard drawing less when not on screen, would cut it. - **The explorer's filter box costs 6.6–9 ms on one frame shortly after the picker opens**, before and after this change. The Output view spikes on that frame too, at about 3 ms. My guess, untested: dvui's font cache growing for glyphs the picker's text introduced, a one-time cost. - **The pixel flash on Linux/X11** from #523 is unconfirmed either way. - **A view lifted out of the picker before its capture lands has no drag picture.** That is the trade-off of spreading the captures: its card is still an empty tile. - **The testing backend can't exercise a successful capture**: it has no render targets. So the capture path itself, one capture after another, is covered by reasoning. The test exercises failed views and "capture unavailable". - **The test can't scroll or type into the picker:** dvui's testing backend delivered neither a wheel nor typed text to the popup in `PickerFrame`, so it takes views out of the list instead. Worth a look on its own. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
added a commit
that referenced
this pull request
Oct 18, 2026
Part of fizzyedit/plans#27 (the corner). Follows #555. ## What changes The corner #555 put on every filled place looked wrong on `main` (seen by the user): "doesn't look like a bubble filling the top right corner, it's got sharp edges, doesn't cover the corner area, and has a weird single corner cut". **The cause:** `cornerButton` runs inside the place's *content* clip, the card inset by its padding (8 pt). The resting piece and its morph into the button were drawn under that clip, so their top and right 8 pt were cut off. The piece sat inset from the card's edge, and its outer curve, the card's own, never showed. The ring beside it already drew under the card's rect (`clipSet(rs.r)`) for the same reason. #555's screenshot showed the cut, and it was misread as the shape. **The corner is now what the user asked for after seeing the options:** a small round bubble that "moves behind/under the region's rounded corner, peeking out". - **The bubble:** 16 pt round, its centre halfway round the card's top-right curve, so half lies over the card and half peeks past the curve. - **Square cards:** with no curve, its centre is the corner point itself. - **Clipping:** it is drawn, and morphed into the button, under the card's rect outset by half the bubble, not the content clip. It is kept whole inside the window, for a card that runs to the window's edge. - **Hover:** the pointer target is the bubble plus 8 pt round it (`CornerPiece.hit`). The finger's target is unchanged: 44 pt, centred on the card's outer corner. - **Glyph:** centred in the bubble, smaller. - **Why not a flush piece:** a shape with the card's top and right edges can't be small. Its outer corner has to match the card's radius (24 pt), and a glass corner is held to half its side, so it would be at least 48 pt across. Prototype by the session working on the history transport (it measured the clip), taken over, checked and finished here. ## SDK impact - [x] None - [ ] Core-only or additive: reaches plugins at the next SDK release - [ ] Fingerprint moved: recorded in `sdk/src/version.zig`, `sdk_version` untouched, PR labelled `sdk` - [ ] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the last `sdk-v*` tag ## Verified - [x] macOS: - **Gates:** `scripts/check-fmt.sh`, `zig build`, `zig build check-web` all pass. `zig build test`: 107/107 steps. `zig build test-integration`: 54/54 steps, 605/605 tests. - **On screen** (sandbox fizzy, `--profile`, captured): - default rounding: the bubble on the main view's and the panel's curve, peeking out; - `corner_roundness = 0`: the bubble whole, centred on the square corner at the window's edge; - hovered, by the prototype's author: the bubble turning into the button. - **Explorer:** the explorer has no card of its own, so its bubble sits on the place's corner over the window's chrome. It marks the place, but reads more detached than on a card. - **Not seen:** a tap on a touchscreen. - **Unchanged by this PR, not re-checked:** the empty-place path (hatch and button), which this doesn't touch (`resting` is false there); and the 44 pt touch target. - [ ] Windows: - [ ] Linux: - [ ] Web: `check-web` only ## Follow-ups - The corner reads `dvui.pointerKind()` once #563 merges (it already has a follow-up note on #555). - Whether the explorer, which has no card, should have a corner at all, or one placed differently. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of fizzyedit/plans#27 (option A, the corner).
What changes
A report from the phone web build: a focused chip in a place's top-right corner sat under the place's corner button. The draw-order half was #540. This is the trigger half: the button came up whenever the pointer was within 64pt of the corner. On touch the last touch is the pointer, so tapping a control in that corner put the button on top of it.
witness), since what is under it is the card's own padding.dvui.timerwakes the frame), or at the next touch anywhere else.core.dialogs.glassBubbles, which now takes per-cornerradii(each held to half the shorter side; the frosted-pane and plain-fill fallbacks take the same corners). Where there is no glass program or no blur, it is frosted, a transparent tint or an opaque fill, in the same shape, as plans#27's material says once that lands.Prototypes: https://claude.ai/artifact/MotGphK5UyeH9sQ6NHeYZk (option A). The piece's inner corner is rounded to half its side (the most a shape's corner can be), not a full quarter-circle, so it reads as the corner filled with glass rather than a quarter disc.
SDK impact
sdk/src/version.zig,sdk_versionuntouched, PR labelledsdksdk_version, lists thesdkPRs since the lastsdk-v*tagVerified
scripts/check-fmt.sh,zig buildandzig build check-weball pass.zig build test: 105/105 steps.zig build test-integration: 54/54 steps, 594/594 tests.--profile), captured at rest. The corner piece shows in the explorer, the main view and the panel, its outer curve on each card's, with the glyph.check-webonly. The phone flow (tap, tap, hide) is what this is for, and needs seeing on a phone.Follow-ups
Once core: tabs show the hand, and a tapped item is left unlit #563 merges, the corner reads
dvui.pointerKind() == .touchin place of its own_chooser_touchflag, in a small PR of its own.Cost not measured: every filled place now draws its corner glass each frame it draws, with its frost read once (
witness). That is one small pane draw per place.The hint beside the button on a first tap ("Tap to arrange · hold to move", in the prototypes) is not drawn here.
Option C, the button docked in a tab strip.
A demo tape that hovers and taps the corner, so this is covered by a test.
🤖 Generated with Claude Code