max / audiofiles
6 files changed,
+69 insertions,
-56 deletions
| @@ -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" |
| @@ -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 |