notedeck · crates/notedeck_headway/src/lib.rs · 89aac407aca4

One Open, One Entry

When you click a Headway card from inside Dave, the global history should grow by exactly one entry, and one back press should return you to Dave. This commit gives Headway the half that makes that possible: it can now say, up front, which route an open should land on.

stack after open: 2 entries (was 3) back presses to source app: 1 (was 2) 2 new tests · 1 helper hoisted from 2 copies commit 4 of 5 · inert until commit 5

What the hook is

App::open_note_route(ctx, note_id) -> Option<Rc<dyn Any>> is a question the chrome asks the app that owns a clicked note: “what route should this open land on?” The app does whatever state setup the route depends on but doesn't carry (for Headway, which board is active) and hands back its own route token. The chrome then pushes one entry, tagged with the owning app's slot, which the app itself never learns. Commit 1 added the defaulted hook. This commit is Headway's implementation of it.

1. The stack, before and after

2. Why the diff stays quiet

Headway turns UI movement into nav requests by diffing the view position before and after each render (reconcile_nav(before, after)). The old path started that diff from an untyped () token, which reads as Board. The pending open then selected a card, the diff read Board → Card, and Headway pushed a second entry. Now the token already is the card, so both ends of the diff agree.

render_nav set_selected(Some(c)) before = Card(c) process_ pending_open open_card(c) after = Card(c) reconcile_nav(Card(c), Card(c)) → None no push, no back: the chrome's push is the only entry token → HeadwayRoute::card(c, title)
The single-entry invariant. The retry that open_note_route leaves pending (self.open(note_id)) selects the card the token already names. Its only other job is to correct active for a card that was moved across boards before its new placement has folded in, and active isn't part of the diff.

3. The frames between the open and the detail

The card can be on a board that isn't the active one. activate_open_target switches active straight away, but that board's view folds in on a later frame. Until it does, find_card misses. Before commit 2, that miss cleared the selection. The diff then read Card → Board and popped the entry that had just been pushed. The headline test uses a card from a second roadmap board for this reason: it walks through the gap between the open and the fold.

open_note_route active := roadmap push [Headway/Card] frames before the fold find_card misses → selection HELD (detail_for ≠ selected) fold lands detail renders “← Back” +5 frames stack.len() == 2 stack.len() is 2 at every frame on this line
What chrome_nav_loop_cross_app_open_pushes_one_entry pins. It seeds a foreign root AppId(1) in place of Dave, pushes the minted token as AppId(0), and drives the shared chrome_frame loop. It asserts that the title came off the issue event ("roadmap-only card"), that the detail renders, that the stack is still 2 after 5 more frames, and that go_to_route(0) leaves one entry whose app is the source app.

A board reference

A board note mints HeadwayRoute::Board. Switching the board was the whole job, so nothing is left pending. open_note_route_for_a_board_note_returns_the_board_route draws that token and waits for the roadmap board's only card to appear, which proves the switch happened. It also checks that no detail opened.

What it looks like when it goes wrong

Mutation check, run before committing: make open_note_route return HeadwayRoute::Board for a card, which is the untyped shape the chrome pushes today. The headline test fails on its first assertion:

assertion `left == right` failed: a cross-app open is one history entry
  left: 3
 right: 2

Three entries: [Source/(), Headway/Board, Headway/Card]. That is the stack users get today, where the first back lands on the Headway board instead of the app they came from.

Not verified

Nothing in the real app calls this yet. The chrome's AppAction::Note arm still does switch_to_headway() followed by open(). The fix users will see is commit 5 (headway:notedeck/duck-echo-chronic). Until then, clicking a card chip from Dave still takes two back presses. Everything on this page was checked in the chrome-less test harness, which copies what apply_nav_requests does. It does not run the real chrome.

Where this model drifts

Only one render pass per frame. During an animated transition, egui_nav also draws the background route, so render_nav runs twice on those frames. The harness never animates. The plan lists this as an out-of-scope card.

Title is a snapshot. The title comes from the issue event's original subject, so it can lag a rename. That is true of every other Headway history label as well.

Board switches don't go into history. The switch in activate_open_target isn't recorded in any entry. So if a later back or forward lands on an older Headway Card entry from a different board, that card is resolved against whichever board is active then. This is also listed as out of scope.