Skip to content

demos: the tour keeps its layout, and a carried tab stops flickering - #669

Merged
foxnne merged 1 commit into
mainfrom
demos/tour-steady
Oct 19, 2026
Merged

foxnne merged 1 commit into
mainfrom
demos/tour-steady

Conversation

@foxnne

@foxnne foxnne commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

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

  • 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.

🤖 Generated with Claude Code

@foxnne
foxnne force-pushed the demos/tour-steady branch 2 times, most recently from 30ac227 to c2292ff Compare October 19, 2026 21:44
@foxnne
foxnne marked this pull request as ready for review October 19, 2026 21:44
@foxnne
foxnne force-pushed the demos/tour-steady branch from c2292ff to 18c1d67 Compare October 19, 2026 21:58
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
foxnne force-pushed the demos/tour-steady branch from 18c1d67 to 6e79981 Compare October 19, 2026 21:58
@foxnne
foxnne enabled auto-merge October 19, 2026 21:59
@foxnne
foxnne added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit 385e0cb Oct 19, 2026
11 checks passed
@foxnne
foxnne deleted the demos/tour-steady branch October 19, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant