Skip to main content

max / audiofiles

Take quasi 0.18, and rank what the toolbar was collapsing at 900px The consumer proof for `Ranked`. Six panel toggles say what each is worth when the toolbar runs out of room, in place of the shipped `screen_w < 900.0` that put all six behind a View menu at a pixel width this app chose. Sidebar and Detail never drop, because a window narrow enough to want fewer controls is where closing one of them matters most; Filters holds at Secondary because it carries the count of what is on; the three inspector toggles drop first. Help drops before Settings and Cloud Sync because `f1` still reaches it and a key is a route a narrow window cannot take away. The footer's `< 1000` did not become priorities and should not. It reflows, moving three items to a second row and dropping nothing, so ranking them would delete facts the shipped app keeps. Wrapping a row is the host's arithmetic in its own units. The one genuinely droppable member of that band is the tag badges, which repeat what the detail panel states in full, and they say so. Fixes the Detail toggle's colour, which is two bugs in one line. It painted content_muted both for "on but the window is too narrow to show it" and for "off", so the one control that could tell you whether the panel is on told you nothing at either end. content_muted is also the tone that claims a control will not answer a press, and this one answers in every state. Whether the panel is on is the app's fact and is what the colour says now; whether the window is wide enough is this frame's, and it says so in the tooltip, where a cause belongs. The View menu already kept the two apart. 489 tests with the quasi feature on, 367 with it off, clippy clean both ways.
Author: Max Johnson <me@maxj.phd> · 2026-08-16 22:00 UTC
Signed with PGP, not checked
Commit: 4cfcef754cb9a6e43c9c42074935d03437b2f92a
Parent: 0875622
5 files changed, +194 insertions, -40 deletions
@@ -2100,6 +2100,36 @@
2100 2100 Self::Filters,
2101 2101 ];
2102 2102
2103 + /// What the toggle is worth when the toolbar runs out of room.
2104 + ///
2105 + /// The declared replacement for the shipped `screen_w < 900.0`, which
2106 + /// collapsed all six into a View menu at one pixel width this file had no
2107 + /// say in. Ranked rather than collapsed, so a narrow window loses the
2108 + /// toggles nobody reaches for and keeps the two that decide the shape of
2109 + /// the window.
2110 + ///
2111 + /// - **Sidebar and Detail never drop.** They are the two structural panes,
2112 + /// and a window narrow enough to want fewer controls is exactly the
2113 + /// window where being able to close one of them matters most.
2114 + /// - **Filters is Secondary.** It carries the count of what is on, so
2115 + /// dropping it while a filter is applied would hide why the list is
2116 + /// short. It survives everything but the narrowest class.
2117 + /// - **Edit, Instrument and Loop drop first.** Each opens an inspector for
2118 + /// the selected sample, which is work a phone-width window is not where
2119 + /// you do.
2120 + ///
2121 + /// What this does not say is a width. Which class drops which rank is the
2122 + /// renderer's, and it is the same rank in all three of them.
2123 + #[must_use]
2124 + pub const fn worth(self) -> quasi_router::layout::Priority {
2125 + use quasi_router::layout::Priority;
2126 + match self {
2127 + Self::Sidebar | Self::Detail => Priority::Essential,
2128 + Self::Filters => Priority::Secondary,
2129 + Self::Edit | Self::Instrument | Self::Loop => Priority::Optional,
2130 + }
2131 + }
2132 +
2103 2133 /// The name an address is built from.
2104 2134 #[must_use]
2105 2135 pub const fn as_str(self) -> &'static str {
@@ -34,6 +34,18 @@
34 34 //! layout decision made from a pixel measurement inside a drawing function,
35 35 //! and it is the clearest case in this app of the thing the description layer
36 36 //! exists to take away.
37 + //!
38 + //! Worth being exact about what replaced it, because the obvious answer is
39 + //! the wrong one. `Ranked` (quasi 0.18.0) lets a placement say what it is
40 + //! worth when room runs out, and the toolbar's `< 900` collapse became three
41 + //! priorities. This band's `< 1000` did not, and should not: it *reflows*,
42 + //! moving three items to a second row and dropping nothing. Ranking them
43 + //! would delete facts the shipped footer keeps. Wrapping a row that does not
44 + //! fit is the host's arithmetic in its own units, which is what the deletion
45 + //! above already claimed, so nothing is owed here.
46 + //!
47 + //! The one member that is genuinely droppable is the tag badges, and they
48 + //! say so.
37 49 //! - **A hand-painted progress bar.** Twenty lines of `rect_filled`, a bevel and
38 50 //! a click-to-seek hit test. The bar is [`Meter`]; the seek is not (see
39 51 //! below).
@@ -70,7 +82,7 @@
70 82 //! and `sidebar.rs` (722) are the other two bands of the same window, and each
71 83 //! is its own pass. The screen below has the two regions that exist.
72 84
73 - use quasi_router::layout::{Notice, Tone};
85 + use quasi_router::layout::{Notice, Priority, Tone};
74 86 use quasi_router::{
75 87 Act, Action, Figure, Meter, Node, RegionKind, Request, Response, RouteError, Router, Screen,
76 88 Slot, Tag,
@@ -152,8 +164,13 @@
152 164 // muted text rather than chips on purpose -- "these are inert
153 165 // (informational only), so the affordance contract should not invite a
154 166 // click" -- which is exactly what a badge is and a chip is not.
167 + //
168 + // Optional, and they are the only thing in this band that is. A badge here
169 + // repeats a fact the detail panel states in full, so a window with no room
170 + // for everything loses the repetition first. Everything else in the footer
171 + // is a fact stated nowhere else on the screen.
155 172 for tag in state.shell.tags() {
156 - band = band.with(Node::Token(Tag::badge(tag)));
173 + band = band.with_ranked(Node::Token(Tag::badge(tag)), Priority::Optional);
157 174 }
158 175
159 176 band
@@ -230,7 +230,12 @@
230 230
231 231 /// Every node on a screen, in order.
232 232 fn nodes(screen: &Screen) -> Vec<&Node> {
233 - screen.slots.iter().flat_map(|slot| &slot.body).collect()
233 + screen
234 + .slots
235 + .iter()
236 + .flat_map(|slot| &slot.body)
237 + .map(|placed| &placed.node)
238 + .collect()
234 239 }
235 240
236 241 /// The text of every prose and notice node on a screen, joined.
@@ -290,9 +295,11 @@
290 295 /// `nodes` only walks the screen's own slots. The rename preview lives in one so
291 296 /// that a fragment can replace it.
292 297 fn table_of(screen: &Screen) -> (Vec<quasi_router::Column>, Vec<quasi_router::Cells>) {
293 - fn find(body: &[Node]) -> Option<(Vec<quasi_router::Column>, Vec<quasi_router::Cells>)> {
294 - for node in body {
295 - match node {
298 + fn find(
299 + body: &[quasi_router::Ranked],
300 + ) -> Option<(Vec<quasi_router::Column>, Vec<quasi_router::Cells>)> {
301 + for placed in body {
302 + match &placed.node {
296 303 Node::Table { columns, rows, .. } => {
297 304 return Some((columns.clone(), rows.clone()));
298 305 }
@@ -389,8 +396,8 @@
389 396 fn fields(screen: &Screen) -> BTreeMap<String, Option<String>> {
390 397 let mut found = BTreeMap::new();
391 398 for slot in &screen.slots {
392 - for node in &slot.body {
393 - match node {
399 + for placed in &slot.body {
400 + match &placed.node {
394 401 Node::Field(field) => {
395 402 found.insert(field.name.clone(), field.value.clone());
396 403 }
@@ -611,7 +618,7 @@
611 618 .slots
612 619 .iter()
613 620 .flat_map(|slot| &slot.body)
614 - .find_map(|node| match node {
621 + .find_map(|placed| match &placed.node {
615 622 Node::Field(field) if field.name == ConfigKey::Theme.as_str() => Some(field),
616 623 _ => None,
617 624 })
@@ -814,7 +821,7 @@
814 821 .slots
815 822 .iter()
816 823 .flat_map(|slot| &slot.body)
817 - .filter_map(|node| match node {
824 + .filter_map(|placed| match &placed.node {
818 825 Node::Act(act) => Some(act.label.clone()),
819 826 _ => None,
820 827 })
@@ -905,7 +912,7 @@
905 912 .slots
906 913 .iter()
907 914 .flat_map(|slot| &slot.body)
908 - .filter_map(|node| match node {
915 + .filter_map(|placed| match &placed.node {
909 916 Node::Form { fields, .. } => Some(fields.clone()),
910 917 _ => None,
911 918 })
@@ -929,7 +936,7 @@
929 936 .slots
930 937 .iter()
931 938 .flat_map(|slot| &slot.body)
932 - .find_map(|node| match node {
939 + .find_map(|placed| match &placed.node {
933 940 Node::Act(act) if act.label == "Sync now" => Some(act),
934 941 _ => None,
935 942 })
@@ -972,11 +979,9 @@
972 979 let response = syncing(&sync, Request::get("/sync")).expect("answered");
973 980 let screen = screen_of(&response);
974 981
975 - let said = screen
976 - .slots
977 - .iter()
978 - .flat_map(|slot| &slot.body)
979 - .any(|node| matches!(node, Node::Notice { text, .. } if text.contains("said no")));
982 + let said = screen.slots.iter().flat_map(|slot| &slot.body).any(
983 + |placed| matches!(&placed.node, Node::Notice { text, .. } if text.contains("said no")),
984 + );
980 985 assert!(said, "{state:?} swallowed the error");
981 986
982 987 let labels = acts(screen);
@@ -995,8 +1000,8 @@
995 1000 .slots
996 1001 .iter()
997 1002 .flat_map(|slot| &slot.body)
998 - .flat_map(|node| match node {
999 - Node::Region(slot) => slot.body.clone(),
1003 + .flat_map(|placed| match &placed.node {
1004 + Node::Region(slot) => slot.body.iter().map(|inner| inner.node.clone()).collect(),
1000 1005 other => vec![other.clone()],
1001 1006 })
1002 1007 .filter_map(|node| match node {
@@ -1024,7 +1029,7 @@
1024 1029 .slots
1025 1030 .iter()
1026 1031 .flat_map(|slot| &slot.body)
1027 - .find_map(|node| match node {
1032 + .find_map(|placed| match &placed.node {
1028 1033 Node::Region(slot) if slot.id == "subscription" => Some(slot),
1029 1034 _ => None,
1030 1035 })
@@ -1135,8 +1140,8 @@
1135 1140 .slots
1136 1141 .iter()
1137 1142 .flat_map(|slot| &slot.body)
1138 - .flat_map(|node| match node {
1139 - Node::Region(slot) => slot.body.clone(),
1143 + .flat_map(|placed| match &placed.node {
1144 + Node::Region(slot) => slot.body.iter().map(|inner| inner.node.clone()).collect(),
1140 1145 other => vec![other.clone()],
1141 1146 })
1142 1147 .find_map(|node| match node {
@@ -1171,8 +1176,8 @@
1171 1176 .slots
1172 1177 .iter()
1173 1178 .flat_map(|slot| &slot.body)
1174 - .flat_map(|node| match node {
1175 - Node::Region(slot) => slot.body.clone(),
1179 + .flat_map(|placed| match &placed.node {
1180 + Node::Region(slot) => slot.body.iter().map(|inner| inner.node.clone()).collect(),
1176 1181 other => vec![other.clone()],
1177 1182 })
1178 1183 .find_map(|node| match node {
@@ -1280,7 +1285,7 @@
1280 1285 .slots
1281 1286 .iter()
1282 1287 .flat_map(|slot| &slot.body)
1283 - .find_map(|node| match node {
1288 + .find_map(|placed| match &placed.node {
1284 1289 Node::StandIn { message, act, .. } => Some((message.clone(), act.clone())),
1285 1290 _ => None,
1286 1291 })
@@ -3545,7 +3550,7 @@
3545 3550 .iter()
3546 3551 .filter(|slot| slot.id == "library-side")
3547 3552 .flat_map(|slot| &slot.body)
3548 - .filter_map(|node| match node {
3553 + .filter_map(|placed| match &placed.node {
3549 3554 Node::Token(tag) => Some(tag.label.clone()),
3550 3555 _ => None,
3551 3556 })
@@ -4092,3 +4097,79 @@
4092 4097 .expect("the toolbar has a search field");
4093 4098 assert_eq!(asked, quasi_router::layout::Width::Fill);
4094 4099 }
4100 +
4101 + /// What every member of a region is worth, by the text it carries.
4102 + fn worths(screen: &Screen, region: &str) -> Vec<(String, quasi_router::layout::Priority)> {
4103 + screen
4104 + .slots
4105 + .iter()
4106 + .find(|slot| slot.id == region)
4107 + .expect("the region is on the screen")
4108 + .body
4109 + .iter()
4110 + .map(|placed| {
4111 + let name = match &placed.node {
4112 + Node::Token(tag) => tag.label.clone(),
4113 + Node::Act(act) => act.label.clone(),
4114 + Node::Text { text, .. } => text.clone(),
4115 + other => format!("{other:?}"),
4116 + };
4117 + (name, placed.priority)
4118 + })
4119 + .collect()
4120 + }
4121 +
4122 + #[test]
4123 + fn the_panel_toggles_say_what_they_are_worth_instead_of_collapsing_at_900px() {
4124 + // The described replacement for `screen_w < 900.0`, which put all six
4125 + // toggles into a View menu at a width this app chose. Ranked now, so a
4126 + // narrow window keeps the two toggles that decide the shape of the window
4127 + // and loses the three that open an inspector.
4128 + use quasi_router::layout::Priority;
4129 +
4130 + let bar = FakeBar::deep();
4131 + let worth = worths(&topped(&bar), "toolbar-bar");
4132 + let of = |label: &str| {
4133 + worth
4134 + .iter()
4135 + .find(|(name, _)| name.starts_with(label))
4136 + .unwrap_or_else(|| panic!("{label} is on the toolbar: {worth:?}"))
4137 + .1
4138 + };
4139 +
4140 + assert_eq!(of("Sidebar"), Priority::Essential);
4141 + assert_eq!(of("Detail"), Priority::Essential);
4142 + assert_eq!(of("Filters"), Priority::Secondary);
4143 + for inspector in ["Edit", "Instrument", "Loop"] {
4144 + assert_eq!(of(inspector), Priority::Optional, "{inspector}");
4145 + }
4146 +
4147 + // Help drops first of the three addresses because `f1` still reaches it.
4148 + assert_eq!(of("Settings"), Priority::Secondary);
4149 + assert_eq!(of("Cloud Sync"), Priority::Secondary);
4150 + assert_eq!(of("Help"), Priority::Optional);
4151 +
4152 + // And nothing was hidden behind a width. The description names no pixels.
4153 + assert!(!format!("{worth:?}").contains("900"));
4154 + }
4155 +
4156 + #[test]
4157 + fn the_footer_drops_only_what_the_detail_panel_says_twice() {
4158 + // The footer's own `< 1000` reflows rather than drops, so ranking its
4159 + // items would delete facts the shipped app keeps. The tag badges are the
4160 + // exception: they repeat what the detail panel states in full.
4161 + use quasi_router::layout::Priority;
4162 +
4163 + let shell = FakeShell {
4164 + tags: vec!["drums".to_owned(), "loop".to_owned()],
4165 + ..Default::default()
4166 + };
4167 + let worth = worths(&shown(&shell), "shell-foot");
4168 +
4169 + let optional: Vec<&String> = worth
4170 + .iter()
4171 + .filter(|(_, priority)| *priority != Priority::Essential)
4172 + .map(|(name, _)| name)
4173 + .collect();
4174 + assert_eq!(optional, ["drums", "loop"], "{worth:?}");
4175 + }
@@ -82,7 +82,7 @@
82 82 //! host draws it rather than what it holds, so no finding — but a vocabulary
83 83 //! that grows anchoring should know this was the first place it mattered.
84 84
85 - use quasi_router::layout::{FieldKind, Selector, Tone, Width};
85 + use quasi_router::layout::{FieldKind, Priority, Selector, Tone, Width};
86 86 use quasi_router::{
87 87 Act, Action, Choice, Field, Node, RegionKind, Request, Response, RouteError, Router, Screen,
88 88 Slot,
@@ -376,10 +376,23 @@
376 376 if panel == Panel::Filters && state.bar.searching().filters > 0 {
377 377 chip.label = format!("{} ({})", chip.label, state.bar.searching().filters);
378 378 }
379 - bar = bar.with(Node::Token(chip));
379 + bar = bar.with_ranked(Node::Token(chip), panel.worth());
380 380 }
381 381
382 - bar.with(Node::Act(Act::new("Settings", Action::get("/settings"))))
383 - .with(Node::Act(Act::new("Cloud Sync", Action::get("/sync"))))
384 - .with(Node::Act(Act::new("Help", Action::get("/help")).key("f1")))
382 + // Settings and Cloud Sync are how you reach two whole screens and have no
383 + // other route in, so they hold at Secondary. Help drops first because it
384 + // is the one control here that keeps working when it is not on the screen:
385 + // it carries `f1`, and a key is a route a narrow window cannot take away.
386 + bar.with_ranked(
387 + Node::Act(Act::new("Settings", Action::get("/settings"))),
388 + Priority::Secondary,
389 + )
390 + .with_ranked(
391 + Node::Act(Act::new("Cloud Sync", Action::get("/sync"))),
392 + Priority::Secondary,
393 + )
394 + .with_ranked(
395 + Node::Act(Act::new("Help", Action::get("/help")).key("f1")),
396 + Priority::Optional,
397 + )
385 398 }
@@ -175,8 +175,12 @@
175 175 let screen_w = ui.ctx().content_rect().width();
176 176 let collapse_toggles = screen_w < 900.0;
177 177 // M-13 input (shared by both layouts).
178 - let detail_too_narrow = screen_w < 700.0;
179 - let detail_hidden = state.detail.detail_visible && detail_too_narrow;
178 + // On, and too narrow to be showing. Both halves are read from this
179 + // frame -- the first off persisted config the user set, the second off
180 + // the width right now -- so the answer is the same at a given width
181 + // whatever widths came before it. It decides what the tooltip says and
182 + // nothing else.
183 + let detail_hidden = state.detail.detail_visible && screen_w < 700.0;
180 184
181 185 if collapse_toggles {
182 186 draw_view_menu(ui, state, detail_hidden);
@@ -218,20 +222,29 @@
218 222 state.toggle_sidebar();
219 223 }
220 224
221 - // M-13: the Detail toggle conveys "active but hidden" via a muted colour
222 - // and a tooltip explaining the cause; otherwise behaves like the other
223 - // toolbar toggles.
225 + // M-13: the Detail toggle says "active but hidden" in its tooltip. It used
226 + // to say it in its colour as well, and that was two bugs in one line.
227 + //
228 + // The colour was `content_muted` both for "on but the window is too narrow"
229 + // and for "off", so the one control that could tell you whether the panel
230 + // is on told you nothing at either end. And `content_muted` is the tone
231 + // that claims a control will not answer a press (`makeover-layout`, the
232 + // three tones): this one answers in every state, so wearing it was a lie
233 + // that makes a user stop trying.
234 + //
235 + // Two facts, kept apart. Whether the panel is on is the app's, and it is
236 + // what the colour says. Whether the window is wide enough to show it is
237 + // this frame's, derived here and said in the tooltip, where a cause
238 + // belongs.
224 239 let detail_tooltip = if detail_hidden {
225 240 "Detail panel hidden \u{2014} widen the window to show it. (D)"
226 241 } else {
227 242 "Toggle detail panel (D)"
228 243 };
229 - let detail_colour = if detail_hidden {
230 - theme::content_muted()
231 - } else if state.detail.detail_visible {
244 + let detail_colour = if state.detail.detail_visible {
232 245 theme::action()
233 246 } else {
234 - theme::content_muted()
247 + theme::content_secondary()
235 248 };
236 249 if ui
237 250 .button(egui::RichText::new("Detail").color(detail_colour))