Repository navigation
demos: the tour keeps its layout, and a carried tab stops flickering - #669
Merged
Merged
Conversation
foxnne
force-pushed
the
demos/tour-steady
branch
2 times, most recently
from
October 19, 2026 21:44
30ac227 to
c2292ff
Compare
foxnne
marked this pull request as ready for review
October 19, 2026 21:44
foxnne
force-pushed
the
demos/tour-steady
branch
from
October 19, 2026 21:58
c2292ff to
18c1d67
Compare
Two bugs the maintainer saw in the tour (the "A Tour of Fizzy" tape on the web build), and what replaying the tour without its mid-tour cuts turned up. ## 1. "When dragging the document tabs, the drop zone flickers rapidly" **Cause.** A glide finds its target again every frame (`Sequencer.advanceSpans` → `locate`). While a button is held, the thing under the pointer moves *because* of what is carried. The tour's first drag carries README onto `src/main.zig`'s tab, and the strip makes room for it, so `main.zig`'s tab moves away. The glide then chased it, and the room closed again. Measured headless at 16 ms frames: for the last ~300 ms of that drag the pointer jumped between x≈156 and x≈402 every other frame, so the strip's insertion gap flickered. Frame pacing doesn't matter: it happens at any frame rate, the web's included. **Fix (`sdk/tape/Sequencer.zig`).** A glide that begins with a button held (`Glide.carried`) follows its target until it has seen it in the same place on two frames, then stops following (`locked`). A person aims where they saw the target, too. With nothing held, a glide still follows its target, as a place easing open needs. The `drop:right` drag (op 13–15) now lights centre, then right, and holds. ## 2. "The tape adjusts the layout and snaps it back" **Cause.** `tour.zig` cut to a fresh keyframe at **Places** (`.reset`: Main's split undone, explorer and panel opened), at **Floats** (`.reset`: the sidebar split undone) and at **History** (`.focused`: explorer and panel shut). That is three snaps. Those keyframes existed because a snapshot does not hold the layout: a seek back across a split had to land on a cut. **Fix.** - `tour.zig` has one keyframe. Each chapter carries on in the window the last one left: Places opens the explorer from the rail, the Floats chapter floats one half of the split sidebar, and History clicks README's tab with the store left open beside it. Headless layout timeline: `Pane 0` → `+Pane 1` (Tabs) → `+Sidebar` (Places) → `+Sidebar/b1` → `+Float 1` (Floats) → `−Float 1` (Materials, by its close button). Nothing reverts. - `Demo.layoutPrint`: the app's places (name, shut or not, what each shows), how many panes the documents have, and how many floats. A snapshot keeps it, and `restore` refuses to put a snapshot back into another arrangement. A seek back across a split replays from the keyframe instead of landing on a window arranged the later way. On main, a restore across a float or split put the documents back and left the window as it was, which nothing checked. ## What replaying through the whole tour turned up (each fixed here) With no mid-tour keyframes, every seek replays the tour's drags. Four replay bugs the cuts had been hiding: - **A drop's front document settled a frame late** (`Workspace.setTabs`). `active` was set only when the pane drew, so a pane a drop had just opened had no active document. Live play had drawn it by the next calm moment, but a seek arrived first, and the replay's fingerprint differed. `setTabs` now sets `active` along with the selection. - **A replayed glide aimed at a place still moving** (`Sequencer.Sink.VTable.still`, `Player.targetStill`). A replay covers a glide in one call and finds its target once. While seeking, a glide onto a tagged target is held until the target stops moving: two runs at the same point, at most `Settling.max_runs`, with the backend clock moved a frame per run so animations advance. - **A replayed drag lost most of its carry** (`Sequencer.replaying`). A float moved by its header follows the frames it is drawn in. With a whole glide in one frame it moved 2 px rather than across the window. While replaying, a carried `.move` takes a frame per 16 ms path step. Traces (strokes) don't, and live play is unchanged. - **A forward seek from mid-drag carried on after letting go** (`Player.letGo`, `goBack`). Letting go now marks the app diverged, so if no restore can then be made the seek replays from the keyframe. ## Tests - `tests/demos.zig` `Steady`, run on every bundled demo's straight play: - no chapter opens on another `layoutPrint` than the last one left; - a carried glide's pointer never moves further from where it lands (it fails on main's tab drag); - a drop's lit bubble, once left, is not lit again within the drag. - Seek moments are no longer taken mid-drag (button held), the same rule the player's snapshots follow. - `Sequencer` unit tests: a carried glide goes where its target settled and a free one follows it; a carried glide replays a frame per step. ## Seek cost, measured Headless, ReleaseFast, dvui's testing backend. Each row is a seek from the tour's end back to 200 ms into the chapter: the app runs it took (displayed + silent) and the CPU time to land. | chapter | main: runs / ms | this PR: runs / ms | |---|---|---| | Welcome | 4 / 2.1 | 4 / 2.1 | | Tabs | 4 / 1.9 | 6 / 2.2 | | Places | 4 / 2.1 | 217 / 23.9 | | Floats | 4 / 2.1 | 241 / 29.4 | | Materials | 4 / 0.7 | 376 / 59.5 | | Plugins | 9 / 1.5 | 387 / 64.5 | | History | 4 / 2.0 | 4 / 0.7 | | Your turn | 9 / 1.1 | 9 / 1.7 | A seek back into Places through Plugins is now a replay from the start. On main it was a cut to that chapter's own keyframe. The replay plays the tour's drags a frame per step and holds each glide until its target is still. Against the player's 8 ms budget that is up to about 8 displayed frames on this machine (about 65 ms of work), where main took one. It is correct, but slower. The way back to one-frame seeks is to put the layout itself (assignments, splits, floats) into the snapshot, so a restore can put it back rather than refuse. That is a follow-up: `Demo.Snapshot` is the seam, and `layoutPrint` is what it would replace. ## Review (code-review at medium, two passes on this PR) Fixed: - `paneUnsettled` could hold `idle` forever. It is replaced by the root fix: `Workspace.setTabs` sets `active`, null for a surface that is not a document. - `targetStill` saw no motion between runs on a microsecond clock. A held run now moves the backend clock 16 ms. - The hold is skipped for untagged targets and for `.trace`, so a drawing's strokes cost no extra runs. - `aiming` is now cleared on rewind and restore. - A duplicated comment is gone. - Steady's flicker check gave a false positive when several bubbles were lit at once. It now fires only on lit, then off, then lit again. - `letGo` marked the app diverged, which turned a later forward scrub into a seek back. It is now a per-seek `let_go`. - The clock during a step-per-frame replayed drag now advances a step per run (`Sequencer.nextAt`). Declined, or left as follow-ups: - Snapshots holding the layout (above). - A carried glide can lock on a target before its easing has begun. - Steady checks only the boundary frame of a chapter. - A `.via` drag that legitimately returns to a bubble would trip the flicker check (no bundled demo does that). ## Not done / follow-ups - A float dragged by its header still lands somewhere else after a seek than in straight play. Even a frame per step moves it less than live does. `layoutPrint` counts floats but has no float positions, so a restore puts a float back without moving it. That is a fidelity gap (fingerprints do not cover it) that main had too. - The glass aim offset (`dropCenter`, 0.55 R down-right of the pointer) applies only where `LiquidField.ready()`. Headless tests can't exercise it. The fix doesn't depend on it. Coordination: #665 (tape-own-stage) has merged. This PR is rebased on it with no conflicts. These bugs are separate from #665. ## SDK impact - [x] None (fingerprint unchanged; `test-sdk-version` passes) ## Verified macOS, headless: `scripts/check-fmt.sh`, `zig build`, `zig build test`, `zig build test-integration` (all steps), `zig build check-web`, `zig build test-sdk-version`, `zig build check-tests-reached`. No fizzy launched. The flicker and the snaps were measured with a temporary 16 ms-frame probe of the tour: the pointer x per frame and the region list per chapter, quoted above. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
force-pushed
the
demos/tour-steady
branch
from
October 19, 2026 21:58
18c1d67 to
6e79981
Compare
foxnne
enabled auto-merge
October 19, 2026 21:59
2 of 5 tasks
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.
Two bugs the maintainer saw in the tour (the "A Tour of Fizzy" tape on the web build), and what replaying the tour without its mid-tour cuts turned up.
1. "When dragging the document tabs, the drop zone flickers rapidly"
Cause. A glide finds its target again every frame (
Sequencer.advanceSpans→locate). While a button is held, the thing under the pointer moves because of what is carried. The tour's first drag carries README ontosrc/main.zig's tab, and the strip makes room for it, somain.zig's tab moves away. The glide then chased it, and the room closed again. Measured headless at 16 ms frames: for the last ~300 ms of that drag the pointer jumped between x≈156 and x≈402 every other frame, so the strip's insertion gap flickered. Frame pacing doesn't matter: it happens at any frame rate, the web's included.Fix (
sdk/tape/Sequencer.zig). A glide that begins with a button held (Glide.carried) follows its target until it has seen it in the same place on two frames, then stops following (locked). A person aims where they saw the target, too. With nothing held, a glide still follows its target, as a place easing open needs. Thedrop:rightdrag (op 13–15) now lights centre, then right, and holds.2. "The tape adjusts the layout and snaps it back"
Cause.
tour.zigcut to a fresh keyframe at Places (.reset: Main's split undone, explorer and panel opened), at Floats (.reset: the sidebar split undone) and at History (.focused: explorer and panel shut). That is three snaps. Those keyframes existed because a snapshot does not hold the layout: a seek back across a split had to land on a cut.Fix.
tour.zighas one keyframe. Each chapter carries on in the window the last one left: Places opens the explorer from the rail, the Floats chapter floats one half of the split sidebar, and History clicks README's tab with the store left open beside it. Headless layout timeline:Pane 0→+Pane 1(Tabs) →+Sidebar(Places) →+Sidebar/b1→+Float 1(Floats) →−Float 1(Materials, by its close button). Nothing reverts.Demo.layoutPrint: the app's places (name, shut or not, what each shows), how many panes the documents have, and how many floats. A snapshot keeps it, andrestorerefuses to put a snapshot back into another arrangement. A seek back across a split replays from the keyframe instead of landing on a window arranged the later way. On main, a restore across a float or split put the documents back and left the window as it was, which nothing checked.What replaying through the whole tour turned up (each fixed here)
With no mid-tour keyframes, every seek replays the tour's drags. Four replay bugs the cuts had been hiding:
Workspace.setTabs).activewas set only when the pane drew, so a pane a drop had just opened had no active document. Live play had drawn it by the next calm moment, but a seek arrived first, and the replay's fingerprint differed.setTabsnow setsactivealong with the selection.Sequencer.Sink.VTable.still,Player.targetStill). A replay covers a glide in one call and finds its target once. While seeking, a glide onto a tagged target is held until the target stops moving: two runs at the same point, at mostSettling.max_runs, with the backend clock moved a frame per run so animations advance.Sequencer.replaying). A float moved by its header follows the frames it is drawn in. With a whole glide in one frame it moved 2 px rather than across the window. While replaying, a carried.movetakes a frame per 16 ms path step. Traces (strokes) don't, and live play is unchanged.Player.letGo,goBack). Letting go now marks the app diverged, so if no restore can then be made the seek replays from the keyframe.Tests
tests/demos.zigSteady, run on every bundled demo's straight play:layoutPrintthan the last one left;Sequencerunit tests: a carried glide goes where its target settled and a free one follows it; a carried glide replays a frame per step.Seek cost, measured
Headless, ReleaseFast, dvui's testing backend. Each row is a seek from the tour's end back to 200 ms into the chapter: the app runs it took (displayed + silent) and the CPU time to land.
A seek back into Places through Plugins is now a replay from the start. On main it was a cut to that chapter's own keyframe. The replay plays the tour's drags a frame per step and holds each glide until its target is still. Against the player's 8 ms budget that is up to about 8 displayed frames on this machine (about 65 ms of work), where main took one. It is correct, but slower. The way back to one-frame seeks is to put the layout itself (assignments, splits, floats) into the snapshot, so a restore can put it back rather than refuse. That is a follow-up:
Demo.Snapshotis the seam, andlayoutPrintis what it would replace.Review (code-review at medium, two passes on this PR)
Fixed:
paneUnsettledcould holdidleforever. It is replaced by the root fix:Workspace.setTabssetsactive, null for a surface that is not a document.targetStillsaw no motion between runs on a microsecond clock. A held run now moves the backend clock 16 ms..trace, so a drawing's strokes cost no extra runs.aimingis now cleared on rewind and restore.letGomarked the app diverged, which turned a later forward scrub into a seek back. It is now a per-seeklet_go.Sequencer.nextAt).Declined, or left as follow-ups:
.viadrag that legitimately returns to a bubble would trip the flicker check (no bundled demo does that).Not done / follow-ups
layoutPrintcounts floats but has no float positions, so a restore puts a float back without moving it. That is a fidelity gap (fingerprints do not cover it) that main had too.dropCenter, 0.55 R down-right of the pointer) applies only whereLiquidField.ready(). Headless tests can't exercise it. The fix doesn't depend on it.Coordination: #665 (tape-own-stage) has merged. This PR is rebased on it with no conflicts. These bugs are separate from #665.
SDK impact
test-sdk-versionpasses)Verified
macOS, headless:
scripts/check-fmt.sh,zig build,zig build test,zig build test-integration(all steps),zig build check-web,zig build test-sdk-version,zig build check-tests-reached. No fizzy launched. The flicker and the snaps were measured with a temporary 16 ms-frame probe of the tour: the pointer x per frame and the region list per chapter, quoted above.🤖 Generated with Claude Code