Skip to main content

max / audiofiles

Describe the way off Settings and Cloud Sync; delete panel::leave quasicoherent 33c27e81 landed Destination::Back, and this is what it replaces. Both screens replace the shell's, and the only exit was a host affordance: leave() read Escape itself, so a pointer could not reach it and a touch gesture had nothing to bind to. Its own header said so and named this task as what would retire it. Both now carry a Close act with Action::back(), which every host answers from its own history. A test asserts it by destination rather than by label: what makes a control a way back is where it goes. Deleting leave() also removes a latent double-action. quasi-immediate's Runtime::show has read Escape itself for a while -- dismiss what is on top, else go back -- so on a frame where dismissing left no overlay, Escape both closed the modal and navigated off the screen behind it.
Author: Max Johnson <me@maxj.phd> · 2026-08-30 20:11 UTC
Signed with PGP, not checked
Commit: df005367dff0d377c135e5070753b4520c3421b9
Parent: 94ac45c
6 files changed, +69 insertions, -56 deletions
M Cargo.lock +12 -12
@@ -4256,7 +4256,7 @@
4256 4256
4257 4257 [[package]]
4258 4258 name = "quasi-immediate"
4259 - version = "0.84.0"
4259 + version = "0.85.0"
4260 4260 dependencies = [
4261 4261 "docengine",
4262 4262 "egui",
@@ -4267,7 +4267,7 @@
4267 4267
4268 4268 [[package]]
4269 4269 name = "quasi-router"
4270 - version = "0.84.0"
4270 + version = "0.85.0"
4271 4271 dependencies = [
4272 4272 "makeover-layout",
4273 4273 ]
@@ -7556,25 +7556,21 @@
7556 7556 "winnow 1.0.4",
7557 7557 ]
7558 7558
7559 - [[patch.unused]]
7560 - name = "quasi-type"
7561 - version = "0.1.3"
7562 -
7563 7559 [[patch.unused]]
7564 7560 name = "quasi-axum"
7565 - version = "0.84.0"
7561 + version = "0.85.0"
7566 7562
7567 7563 [[patch.unused]]
7568 7564 name = "quasi-basics"
7569 - version = "0.84.0"
7565 + version = "0.85.0"
7570 7566
7571 7567 [[patch.unused]]
7572 7568 name = "quasi-http"
7573 - version = "0.84.0"
7569 + version = "0.85.0"
7574 7570
7575 7571 [[patch.unused]]
7576 7572 name = "quasi-notifs"
7577 - version = "0.84.0"
7573 + version = "0.85.0"
7578 7574
7579 7575 [[patch.unused]]
7580 7576 name = "quasi-store"
@@ -7582,11 +7578,11 @@
7582 7578
7583 7579 [[patch.unused]]
7584 7580 name = "quasi-tauri"
7585 - version = "0.84.0"
7581 + version = "0.85.0"
7586 7582
7587 7583 [[patch.unused]]
7588 7584 name = "quasi-webview"
7589 - version = "0.84.0"
7585 + version = "0.85.0"
7590 7586
7591 7587 [[patch.unused]]
7592 7588 name = "kberg"
@@ -7599,3 +7595,7 @@
7599 7595 [[patch.unused]]
7600 7596 name = "painhours"
7601 7597 version = "0.1.0"
7598 +
7599 + [[patch.unused]]
7600 + name = "quasi-type"
7601 + version = "0.1.3"
M Cargo.toml +2 -2
@@ -26,8 +26,8 @@
26 26 # The described screens, which are audiofiles-browser's screens. By git URL with
27 27 # a version requirement, per the tree's rule for cross-repo deps. They were
28 28 # behind a `quasi` feature until 2026-08-25, when the last flip landed.
29 - quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.84" }
30 - quasi-immediate = { git = "https://makenot.work/git/max/quasi.git", version = "0.84" }
29 + quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.85" }
30 + quasi-immediate = { git = "https://makenot.work/git/max/quasi.git", version = "0.85" }
31 31 egui = { version = "0.35", default-features = false, features = ["default_fonts"] }
32 32 egui_extras = { version = "0.35", default-features = false }
33 33 eframe = { version = "0.35", default-features = false, features = ["default_fonts", "glow"] }
@@ -2385,9 +2385,7 @@
2385 2385 }
2386 2386
2387 2387 let step = runtime.show(ui, &immediate);
2388 - let taken = !matches!(step, Step::Idle);
2389 2388 perform(runtime, ui, host, step);
2390 - leave(runtime, ui, host, taken);
2391 2389
2392 2390 // One drain for every described window, rather than one per
2393 2391 // `draw_*`: a file is produced by a route and a route is reachable
@@ -2398,45 +2396,6 @@
2398 2396 locate(runtime, host.state, home);
2399 2397 }
2400 2398
2401 - /// Escape goes back, when nothing on the screen wanted it.
2402 - ///
2403 - /// **The way out of a described screen**, ruled by Max on 2026-08-25 after the
2404 - /// door sweep found that `/settings` and `/sync` replace the shell's screen and
2405 - /// nothing described a way off them. `Runtime::back` was written, popped
2406 - /// `history`, and had never been called.
2407 - ///
2408 - /// Three conditions, and each is the answer to a way this could be wrong:
2409 - ///
2410 - /// - **The description gets first refusal.** `runtime.show` answers a `Step`
2411 - /// when a described control claimed the press, and every modal in this app
2412 - /// carries `Act::key("esc")` on its Cancel. Firing as well would cancel the
2413 - /// modal *and* navigate off what was under it.
2414 - /// - **Never while an overlay is open.** An overlay is not a place and never
2415 - /// entered `history`, so going back from one would leave the layer stack up
2416 - /// and change the screen underneath it. Dismissing is the overlay's own
2417 - /// Cancel, which is the first condition.
2418 - /// - **Only where there is somewhere to go.** `back` answers `Step::Idle` on an
2419 - /// empty history, so the shell's own home is not a screen you can leave.
2420 - ///
2421 - /// # What this is not, and it is worth saying at the binding
2422 - ///
2423 - /// A host affordance rather than a described fact. Nothing in the vocabulary
2424 - /// says "this is the way back": `Chrome::bind` carries an `Action`, and an
2425 - /// action names a route rather than a movement through history. So a keyboard
2426 - /// host gets this and **a touch host gets nothing** -- its system back gesture
2427 - /// has no described thing to bind to. Measured 2026-08-25 and filed as
2428 - /// quasicoherent `33c27e81`; when that lands, this function is what it replaces.
2429 - fn leave(runtime: &mut Runtime, ui: &mut egui::Ui, host: &Host<'_>, taken: bool) {
2430 - if taken || runtime.overlaid() {
2431 - return;
2432 - }
2433 - if !ui.ctx().input(|input| input.key_pressed(egui::Key::Escape)) {
2434 - return;
2435 - }
2436 - let step = runtime.back();
2437 - perform(runtime, ui, host, step);
2438 - }
2439 -
2440 2399 /// One described window: draw it, act on it, and say whether it was closed.
2441 2400 fn window(
2442 2401 ctx: &egui::Context,
@@ -183,6 +183,13 @@
183 183 pub(super) fn screen(state: &Panels<'_>) -> Result<Screen, RouteError> {
184 184 let mut body = Slot::new(BODY, RegionKind::Pane)
185 185 .with(Node::page("Settings"))
186 + // The way off. quasicoherent `33c27e81`: this screen replaces the
187 + // shell's, so without a described way back the only exit was a host
188 + // affordance -- an Escape this app read itself, which a pointer could
189 + // not reach and a touch gesture had nothing to bind to. `Action::back`
190 + // is the described fact, and every host answers it from its own
191 + // history.
192 + .with(Node::Act(Act::new("Close", Action::back())))
186 193 .with(Node::section("Appearance"))
187 194 .with(appearance(state))
188 195 .with(Node::section("Preview"))
@@ -304,7 +304,10 @@
304 304 // `quasi_immediate::CADENCE`, and the host no longer owns a timer.
305 305 let mut body = Slot::new(BODY, RegionKind::Pane)
306 306 .live()
307 - .with(Node::page("Cloud Sync"));
307 + .with(Node::page("Cloud Sync"))
308 + // The way off, for `settings::screen`'s reason: this screen replaces
309 + // the shell's and nothing described an exit from it.
310 + .with(Node::Act(Act::new("Close", Action::back())));
308 311
309 312 // Nothing to offer when there is nothing to offer it against. See
310 313 // [`Sync::available`]: an unavailable manager is not a disconnected one, and
@@ -843,6 +843,50 @@
843 843 assert!(String::from_utf8_lossy(bytes).contains("[color]"));
844 844 }
845 845
846 + /// The way back a screen offers, if it offers one.
847 + ///
848 + /// Found by destination rather than by label: what makes this a way back is
849 + /// `Destination::Back`, and a test that matched on the word "Close" would pass
850 + /// for a control that went somewhere else entirely.
851 + fn goes_back(screen: &Screen) -> bool {
852 + fn walk(node: &Node) -> bool {
853 + match node {
854 + Node::Act(act) => act.action.destination.is_back(),
855 + Node::Region(slot) => slot.body.iter().any(|placed| walk(&placed.node)),
856 + _ => false,
857 + }
858 + }
859 + nodes(screen).iter().any(|node| walk(node))
860 + }
861 +
862 + #[test]
863 + fn a_screen_that_replaced_the_shells_describes_the_way_off_it() {
864 + // quasicoherent `33c27e81`. Both of these replace the shell's screen, and
865 + // the only exit used to be a host affordance: `panel::leave` read Escape
866 + // itself, so a pointer could not reach it and a touch gesture had nothing
867 + // to bind to. It is deleted; the way back is described now, and every host
868 + // answers it from its own history.
869 + let store = Store::default();
870 + let settings = match settling(&store, Request::get("/settings"))
871 + .expect("settings answers")
872 + .outcome
873 + {
874 + Outcome::Screen(screen) => screen,
875 + other => panic!("expected a screen, got {other:?}"),
876 + };
877 + assert!(goes_back(&settings), "settings has no described way off it");
878 +
879 + let sync = FakeSync::in_state(State::Disconnected);
880 + let cloud = match syncing(&sync, Request::get("/sync"))
881 + .expect("sync answers")
882 + .outcome
883 + {
884 + Outcome::Screen(screen) => screen,
885 + other => panic!("expected a screen, got {other:?}"),
886 + };
887 + assert!(goes_back(&cloud), "cloud sync has no described way off it");
888 + }
889 +
846 890 #[test]
847 891 fn a_theme_with_no_readable_source_is_not_offered_for_export() {
848 892 // `nord` in the fixture has `source: None`, which is a built-in with nothing