max / audiofiles
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01AAbx8dxVmuVeyKbKoRrUL2
8 files changed,
+221 insertions,
-125 deletions
| @@ -1272,7 +1272,7 @@ | |||
| 1272 | 1272 | "libc", | |
| 1273 | 1273 | "option-ext", | |
| 1274 | 1274 | "redox_users", | |
| 1275 | - | "windows-sys 0.59.0", | |
| 1275 | + | "windows-sys 0.61.2", | |
| 1276 | 1276 | ] | |
| 1277 | 1277 | ||
| 1278 | 1278 | [[package]] | |
| @@ -1617,7 +1617,7 @@ | |||
| 1617 | 1617 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" | |
| 1618 | 1618 | dependencies = [ | |
| 1619 | 1619 | "libc", | |
| 1620 | - | "windows-sys 0.59.0", | |
| 1620 | + | "windows-sys 0.61.2", | |
| 1621 | 1621 | ] | |
| 1622 | 1622 | ||
| 1623 | 1623 | [[package]] | |
| @@ -3057,9 +3057,9 @@ | |||
| 3057 | 3057 | ||
| 3058 | 3058 | [[package]] | |
| 3059 | 3059 | name = "makeover" | |
| 3060 | - | version = "3.0.0" | |
| 3060 | + | version = "3.1.0" | |
| 3061 | 3061 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3062 | - | checksum = "3e6da78c376074e48eb2324394004afc847e674e05ed2ea4bec1331073990b3f" | |
| 3062 | + | checksum = "d0fba4022268d1d73423bb16126dfdaa714a63bd4ff24f826dd645255eb0aad4" | |
| 3063 | 3063 | dependencies = [ | |
| 3064 | 3064 | "include_dir", | |
| 3065 | 3065 | "serde", | |
| @@ -3074,9 +3074,9 @@ | |||
| 3074 | 3074 | ||
| 3075 | 3075 | [[package]] | |
| 3076 | 3076 | name = "makeover-immediate" | |
| 3077 | - | version = "0.38.0" | |
| 3077 | + | version = "0.39.0" | |
| 3078 | 3078 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3079 | - | checksum = "7ee9cb9fe796db8803518e8726e01708fafd64fc52595fd7d8f040fb9f4345fe" | |
| 3079 | + | checksum = "628f2e478a4bd00a305cc7b43ee1bdc229ccd8dd9c7077ba7d7cc1f85ba6994c" | |
| 3080 | 3080 | dependencies = [ | |
| 3081 | 3081 | "egui", | |
| 3082 | 3082 | "egui_extras", | |
| @@ -3086,9 +3086,9 @@ | |||
| 3086 | 3086 | ||
| 3087 | 3087 | [[package]] | |
| 3088 | 3088 | name = "makeover-layout" | |
| 3089 | - | version = "0.37.0" | |
| 3089 | + | version = "0.38.0" | |
| 3090 | 3090 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3091 | - | checksum = "369bc3c3e0ee91d24b3552f48f1ecb1764bca953725fb817df33b2dc80f2d34f" | |
| 3091 | + | checksum = "73ad4a03ed87470b838bd84be586739682f0a1210db1d7ad4a0146cb9ef7734b" | |
| 3092 | 3092 | ||
| 3093 | 3093 | [[package]] | |
| 3094 | 3094 | name = "makeover-timing" | |
| @@ -3225,7 +3225,7 @@ | |||
| 3225 | 3225 | "once_cell", | |
| 3226 | 3226 | "png", | |
| 3227 | 3227 | "thiserror 2.0.20", | |
| 3228 | - | "windows-sys 0.60.2", | |
| 3228 | + | "windows-sys 0.61.2", | |
| 3229 | 3229 | ] | |
| 3230 | 3230 | ||
| 3231 | 3231 | [[package]] | |
| @@ -3310,7 +3310,7 @@ | |||
| 3310 | 3310 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3311 | 3311 | checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" | |
| 3312 | 3312 | dependencies = [ | |
| 3313 | - | "windows-sys 0.59.0", | |
| 3313 | + | "windows-sys 0.61.2", | |
| 3314 | 3314 | ] | |
| 3315 | 3315 | ||
| 3316 | 3316 | [[package]] | |
| @@ -3413,7 +3413,7 @@ | |||
| 3413 | 3413 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3414 | 3414 | checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" | |
| 3415 | 3415 | dependencies = [ | |
| 3416 | - | "proc-macro-crate 1.3.1", | |
| 3416 | + | "proc-macro-crate 3.5.0", | |
| 3417 | 3417 | "proc-macro2", | |
| 3418 | 3418 | "quote", | |
| 3419 | 3419 | "syn 2.0.119", | |
| @@ -4256,7 +4256,7 @@ | |||
| 4256 | 4256 | ||
| 4257 | 4257 | [[package]] | |
| 4258 | 4258 | name = "quasi-immediate" | |
| 4259 | - | version = "0.74.0" | |
| 4259 | + | version = "0.75.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.74.0" | |
| 4270 | + | version = "0.75.0" | |
| 4271 | 4271 | dependencies = [ | |
| 4272 | 4272 | "makeover-layout", | |
| 4273 | 4273 | ] | |
| @@ -4667,7 +4667,7 @@ | |||
| 4667 | 4667 | "errno", | |
| 4668 | 4668 | "libc", | |
| 4669 | 4669 | "linux-raw-sys 0.12.1", | |
| 4670 | - | "windows-sys 0.59.0", | |
| 4670 | + | "windows-sys 0.61.2", | |
| 4671 | 4671 | ] | |
| 4672 | 4672 | ||
| 4673 | 4673 | [[package]] | |
| @@ -4724,7 +4724,7 @@ | |||
| 4724 | 4724 | "security-framework", | |
| 4725 | 4725 | "security-framework-sys", | |
| 4726 | 4726 | "webpki-root-certs", | |
| 4727 | - | "windows-sys 0.59.0", | |
| 4727 | + | "windows-sys 0.61.2", | |
| 4728 | 4728 | ] | |
| 4729 | 4729 | ||
| 4730 | 4730 | [[package]] | |
| @@ -5120,7 +5120,7 @@ | |||
| 5120 | 5120 | checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" | |
| 5121 | 5121 | dependencies = [ | |
| 5122 | 5122 | "libc", | |
| 5123 | - | "windows-sys 0.60.2", | |
| 5123 | + | "windows-sys 0.61.2", | |
| 5124 | 5124 | ] | |
| 5125 | 5125 | ||
| 5126 | 5126 | [[package]] | |
| @@ -5714,10 +5714,10 @@ | |||
| 5714 | 5714 | checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" | |
| 5715 | 5715 | dependencies = [ | |
| 5716 | 5716 | "fastrand", | |
| 5717 | - | "getrandom 0.3.4", | |
| 5717 | + | "getrandom 0.4.3", | |
| 5718 | 5718 | "once_cell", | |
| 5719 | 5719 | "rustix 1.1.4", | |
| 5720 | - | "windows-sys 0.59.0", | |
| 5720 | + | "windows-sys 0.61.2", | |
| 5721 | 5721 | ] | |
| 5722 | 5722 | ||
| 5723 | 5723 | [[package]] | |
| @@ -6124,7 +6124,7 @@ | |||
| 6124 | 6124 | "once_cell", | |
| 6125 | 6125 | "png", | |
| 6126 | 6126 | "thiserror 2.0.20", | |
| 6127 | - | "windows-sys 0.60.2", | |
| 6127 | + | "windows-sys 0.61.2", | |
| 6128 | 6128 | ] | |
| 6129 | 6129 | ||
| 6130 | 6130 | [[package]] | |
| @@ -6156,7 +6156,7 @@ | |||
| 6156 | 6156 | dependencies = [ | |
| 6157 | 6157 | "memoffset", | |
| 6158 | 6158 | "tempfile", | |
| 6159 | - | "windows-sys 0.60.2", | |
| 6159 | + | "windows-sys 0.61.2", | |
| 6160 | 6160 | ] | |
| 6161 | 6161 | ||
| 6162 | 6162 | [[package]] | |
| @@ -6748,7 +6748,7 @@ | |||
| 6748 | 6748 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6749 | 6749 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" | |
| 6750 | 6750 | dependencies = [ | |
| 6751 | - | "windows-sys 0.59.0", | |
| 6751 | + | "windows-sys 0.61.2", | |
| 6752 | 6752 | ] | |
| 6753 | 6753 | ||
| 6754 | 6754 | [[package]] | |
| @@ -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.1" | |
| 7562 | - | ||
| 7563 | 7559 | [[patch.unused]] | |
| 7564 | 7560 | name = "quasi-axum" | |
| 7565 | - | version = "0.74.0" | |
| 7561 | + | version = "0.75.0" | |
| 7566 | 7562 | ||
| 7567 | 7563 | [[patch.unused]] | |
| 7568 | 7564 | name = "quasi-basics" | |
| 7569 | - | version = "0.74.0" | |
| 7565 | + | version = "0.75.0" | |
| 7570 | 7566 | ||
| 7571 | 7567 | [[patch.unused]] | |
| 7572 | 7568 | name = "quasi-http" | |
| 7573 | - | version = "0.74.0" | |
| 7569 | + | version = "0.75.0" | |
| 7574 | 7570 | ||
| 7575 | 7571 | [[patch.unused]] | |
| 7576 | 7572 | name = "quasi-notifs" | |
| 7577 | - | version = "0.74.0" | |
| 7573 | + | version = "0.75.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.74.0" | |
| 7581 | + | version = "0.75.0" | |
| 7586 | 7582 | ||
| 7587 | 7583 | [[patch.unused]] | |
| 7588 | 7584 | name = "quasi-webview" | |
| 7589 | - | version = "0.74.0" | |
| 7585 | + | version = "0.75.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.2" |
| @@ -17,8 +17,8 @@ | |||
| 17 | 17 | # to compile against an API added in a later one. The two move together -- | |
| 18 | 18 | # makeover-immediate re-exports nothing, so the `Column` the app describes and | |
| 19 | 19 | # the `Column` the renderer matches on have to be the same type. | |
| 20 | - | makeover-layout = "0.37.0" | |
| 21 | - | makeover-immediate = "0.38.0" | |
| 20 | + | makeover-layout = "0.38.0" | |
| 21 | + | makeover-immediate = "0.39.0" | |
| 22 | 22 | # The time axis. Minor rather than exact patch: it re-exports nothing and no | |
| 23 | 23 | # type of its crosses a renderer boundary, so it is under the same rule as | |
| 24 | 24 | # makeover-geometry above rather than the layout/immediate pair below. | |
| @@ -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.74" } | |
| 30 | - | quasi-immediate = { git = "https://makenot.work/git/max/quasi.git", version = "0.74" } | |
| 29 | + | quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.75" } | |
| 30 | + | quasi-immediate = { git = "https://makenot.work/git/max/quasi.git", version = "0.75" } | |
| 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"] } | |
| @@ -72,7 +72,7 @@ | |||
| 72 | 72 | libc = "0.2" | |
| 73 | 73 | midir = "0.11" | |
| 74 | 74 | tagtree = { git = "https://makenot.work/git/max/makenotwork.git", version = "0.4" } | |
| 75 | - | makeover = "3.0" | |
| 75 | + | makeover = "3.1" | |
| 76 | 76 | # The invariant half of the design system: relational spacing. makeover | |
| 77 | 77 | # resolves colour, which a theme may override; this carries what no theme may. | |
| 78 | 78 | makeover-geometry = "0.7" |
| @@ -7053,18 +7053,31 @@ | |||
| 7053 | 7053 | ||
| 7054 | 7054 | /// A theme the host resolved, as the description needs to name it. | |
| 7055 | 7055 | /// | |
| 7056 | - | /// Three strings rather than the app's own `ThemeMeta`, so the described screen | |
| 7057 | - | /// does not depend on the shape of the theme loader: what a `Choice` needs is a | |
| 7058 | - | /// value and something to show, and the variant is what the grouping finding is | |
| 7059 | - | /// about. | |
| 7056 | + | /// Its own type rather than the app's own `ThemeMeta`, so the described screen | |
| 7057 | + | /// does not depend on the shape of the theme loader. | |
| 7058 | + | /// | |
| 7059 | + | /// The variant and the contrast tier are values rather than strings as of | |
| 7060 | + | /// makeover-layout 0.38.0, which is the release that gave the picker somewhere | |
| 7061 | + | /// to put them. Before it, the grouping finding recorded here said a set of | |
| 7062 | + | /// choices cannot be grouped and the variant had to ride in the label; the tier | |
| 7063 | + | /// this app measured for its egui picker was lost outright by the port. Both | |
| 7064 | + | /// are carried again, and neither is derived twice: `makeover` resolves them | |
| 7065 | + | /// and this hands them across. | |
| 7060 | 7066 | #[derive(Debug, Clone, PartialEq, Eq)] | |
| 7061 | 7067 | pub struct ThemeChoice { | |
| 7062 | 7068 | /// The id stored under `ConfigKey::Theme`. | |
| 7063 | 7069 | pub id: String, | |
| 7064 | 7070 | /// What the picker shows. | |
| 7065 | 7071 | pub name: String, | |
| 7066 | - | /// `dark`, `light` or `high-contrast`. | |
| 7067 | - | pub variant: String, | |
| 7072 | + | /// Which group the picker files it under. | |
| 7073 | + | pub variant: makeover::Variant, | |
| 7074 | + | /// How legible its muted text measured, for the badge beside the name. | |
| 7075 | + | /// | |
| 7076 | + | /// A host fact on the same footing as [`source`](Self::source): it comes | |
| 7077 | + | /// off the theme's resolved colours, so the party that loaded the theme is | |
| 7078 | + | /// the only one holding it, and a description that recomputed it would be | |
| 7079 | + | /// parsing every theme file a second time. | |
| 7080 | + | pub contrast: makeover::ContrastTier, | |
| 7068 | 7081 | /// This theme's TOML, when the host could read it. | |
| 7069 | 7082 | /// | |
| 7070 | 7083 | /// Here rather than behind a capability method for the reason the rest of |
| @@ -2817,15 +2817,41 @@ | |||
| 2817 | 2817 | .map_err(|error| error.message.clone()) | |
| 2818 | 2818 | } | |
| 2819 | 2819 | ||
| 2820 | - | /// The themes the host has resolved, as the description names them. | |
| 2820 | + | /// The themes the host has resolved, in the order a picker offers them. | |
| 2821 | + | /// | |
| 2822 | + | /// The order is `makeover::order_theme_options`' and not this app's, which is | |
| 2823 | + | /// the point of it being reachable on its own: audiofiles embeds its shipped | |
| 2824 | + | /// themes at compile time and reads only its custom ones off disk, so | |
| 2825 | + | /// `makeover::theme_options` cannot see most of what this offers and the scan | |
| 2826 | + | /// half is no use here. The sort half is, and sharing it is what stops three | |
| 2827 | + | /// apps ordering one picker three ways. | |
| 2828 | + | /// | |
| 2829 | + | /// The contrast tier is measured here rather than carried in the theme file, | |
| 2830 | + | /// which is `makeover::ContrastTier`'s own position: an author cannot | |
| 2831 | + | /// mis-declare it, and a theme edited on disk re-measures on the next read. | |
| 2832 | + | /// This app had its own copy of that measurement for its egui picker and the | |
| 2833 | + | /// quasi port dropped it; what is left is one implementation, in the crate that | |
| 2834 | + | /// resolves the colours. | |
| 2821 | 2835 | fn themes() -> Vec<ThemeChoice> { | |
| 2822 | - | theme::list_themes() | |
| 2836 | + | let mut options: Vec<makeover::ThemeOption> = theme::list_themes() | |
| 2823 | 2837 | .into_iter() | |
| 2824 | - | .map(|meta| ThemeChoice { | |
| 2825 | - | source: theme::export_theme_content(&meta.id), | |
| 2838 | + | .map(|meta| makeover::ThemeOption { | |
| 2839 | + | variant: meta.kind(), | |
| 2840 | + | contrast: theme::contrast_tier(&meta.id), | |
| 2826 | 2841 | id: meta.id, | |
| 2827 | 2842 | name: meta.name, | |
| 2828 | - | variant: meta.variant, | |
| 2843 | + | }) | |
| 2844 | + | .collect(); | |
| 2845 | + | makeover::order_theme_options(&mut options); | |
| 2846 | + | ||
| 2847 | + | options | |
| 2848 | + | .into_iter() | |
| 2849 | + | .map(|option| ThemeChoice { | |
| 2850 | + | source: theme::export_theme_content(&option.id), | |
| 2851 | + | id: option.id, | |
| 2852 | + | name: option.name, | |
| 2853 | + | variant: option.variant, | |
| 2854 | + | contrast: option.contrast, | |
| 2829 | 2855 | }) | |
| 2830 | 2856 | .collect() | |
| 2831 | 2857 | } |
| @@ -239,7 +239,13 @@ | |||
| 239 | 239 | use layout::FieldKind as K; | |
| 240 | 240 | match kind { | |
| 241 | 241 | K::Checkbox => Role::Check, | |
| 242 | - | K::Select | K::Radio => Role::Choice, | |
| 242 | + | // `Theme` joins them at makeover-layout 0.38.0: it is a box you open, | |
| 243 | + | // and `makeover_immediate::control_shape` sends it to `Control::Themed` | |
| 244 | + | // -- a `ComboBox`, which reaches the tree as the same thing a `Select` | |
| 245 | + | // does. Absent from this arm it fell to `Role::Text`, which is what the | |
| 246 | + | // shipped picker being reported as an unmatched choice would have | |
| 247 | + | // meant. | |
| 248 | + | K::Select | K::Radio | K::Theme => Role::Choice, | |
| 243 | 249 | // A slider and an interval's two ends. A bare `Number` is not here: | |
| 244 | 250 | // `makeover_immediate::control_shape` sends everything it cannot draw | |
| 245 | 251 | // natively to `Control::Typed`, so a number is a well you type into and |
| @@ -72,16 +72,24 @@ | |||
| 72 | 72 | //! Import Theme and the library mirror are [`advanced`](super::advanced), which | |
| 73 | 73 | //! is why this module opens the section and hands the rest over. | |
| 74 | 74 | //! | |
| 75 | - | //! # The finding this port adds | |
| 75 | + | //! # The finding this port added, and how it was answered | |
| 76 | 76 | //! | |
| 77 | - | //! **A set of choices cannot be grouped.** `draw_appearance_section` builds its | |
| 77 | + | //! **A set of choices cannot be grouped.** `draw_appearance_section` built its | |
| 78 | 78 | //! theme picker as four groups (Dark, Light, High Contrast, plus Follow the | |
| 79 | - | //! system) and badges each theme with a contrast tier. [`Choice`] is a value and | |
| 80 | - | //! a label, so the described version puts the variant in the label and loses the | |
| 81 | - | //! structure. That is goingson's own settings finding, and audiofiles is its | |
| 79 | + | //! system) and badged each theme with a contrast tier. `Choice` is a value and | |
| 80 | + | //! a label, so the described version put the variant in the label and lost the | |
| 81 | + | //! structure. That was goingson's own settings finding, and audiofiles was its | |
| 82 | 82 | //! **second consumer** with a stronger case: goingson grouped four `<optgroup>`s | |
| 83 | - | //! in a webview, and this one groups *and* sorts within each group by measured | |
| 84 | - | //! contrast. | |
| 83 | + | //! in a webview, and this one grouped *and* sorted within each group by | |
| 84 | + | //! measured contrast. | |
| 85 | + | //! | |
| 86 | + | //! The answer was not a group on the choice, which is what both filings | |
| 87 | + | //! proposed. Grouping was counted across the tree on 2026-08-28 and it recurs | |
| 88 | + | //! at exactly one live site, so what recurs is the theme picker rather than | |
| 89 | + | //! option lists that group. makeover-layout 0.38.0 describes the picker itself | |
| 90 | + | //! (`FieldKind::Theme`, ruled by Max on `70028e00`), and both facts come back: | |
| 91 | + | //! the grouping as a described order, and the tier as a value this app no | |
| 92 | + | //! longer measures for itself. | |
| 85 | 93 | //! | |
| 86 | 94 | //! # One write route for the whole screen | |
| 87 | 95 | //! | |
| @@ -92,10 +100,14 @@ | |||
| 92 | 100 | ||
| 93 | 101 | use audiofiles_core::config_key::ConfigKey; | |
| 94 | 102 | use quasi_router::layout::FieldKind; | |
| 103 | + | use quasi_router::layout::{Contrast, ThemeVariant}; | |
| 95 | 104 | use quasi_router::{ | |
| 96 | - | Act, Action, Choice, Field, Node, RegionKind, Request, Response, RouteError, Router, Screen, | |
| 97 | - | Slot, | |
| 105 | + | Act, Action, Field, Node, RegionKind, Request, Response, RouteError, Router, Screen, Slot, | |
| 98 | 106 | }; | |
| 107 | + | // Aliased: this module's `super::ThemeChoice` is the host's own resolved theme, | |
| 108 | + | // and the router's is how a description names one. Both are in scope here | |
| 109 | + | // because this function is exactly the seam between them. | |
| 110 | + | use quasi_router::ThemeChoice as DescribedTheme; | |
| 99 | 111 | ||
| 100 | 112 | use super::Panels; | |
| 101 | 113 | ||
| @@ -288,32 +300,72 @@ | |||
| 288 | 300 | /// A `Field` and not a `Node::Select`, which is worth saying because the wrong | |
| 289 | 301 | /// one is the obvious one: `Selector` is `Segmented | Toggle | Tabs`, a strip of | |
| 290 | 302 | /// a handful of choices, and a theme picker is thirty-odd options that has to | |
| 291 | - | /// collapse. That is a dropdown, which is `FieldKind::Select`. The vocabulary | |
| 292 | - | /// draws the line at how many there are and whether they fold away, not at what | |
| 293 | - | /// the thing means. | |
| 303 | + | /// collapse. That is a dropdown, and as of makeover-layout 0.38.0 it is | |
| 304 | + | /// `FieldKind::Theme` rather than `FieldKind::Select`. | |
| 294 | 305 | /// | |
| 295 | - | /// Flat, and the finding in the module header is why: the shipped picker groups | |
| 296 | - | /// by variant and sorts by contrast tier within each group, and `Choice` carries | |
| 297 | - | /// a value and a label. The variant goes in the label so the fact survives; the | |
| 298 | - | /// structure does not. | |
| 306 | + | /// # The finding in the module header is closed | |
| 307 | + | /// | |
| 308 | + | /// It said a set of choices cannot be grouped, and recorded this app as the | |
| 309 | + | /// **second consumer** of goingson's finding with the stronger case: the | |
| 310 | + | /// shipped egui picker grouped by variant *and* sorted by contrast tier within | |
| 311 | + | /// each group, and `Choice` is a value and a label, so the described version | |
| 312 | + | /// flattened it and lost the tier. | |
| 313 | + | /// | |
| 314 | + | /// The answer was not a group on the choice. Grouping was measured across the | |
| 315 | + | /// tree on 2026-08-28 and it recurs at exactly one live site, so the thing that | |
| 316 | + | /// recurs is this picker rather than option lists that group. `Field::theme` | |
| 317 | + | /// takes entries carrying their variant and their measured tier as values, and | |
| 318 | + | /// both facts come back. | |
| 319 | + | /// | |
| 320 | + | /// Nothing here groups and nothing here sorts. `panel::themes` hands the list | |
| 321 | + | /// over in `makeover::order_theme_options`' order, which is the order the | |
| 322 | + | /// renderer reads the groups out of. | |
| 299 | 323 | fn appearance(state: &Panels<'_>) -> Node { | |
| 300 | 324 | let chosen = state.config.get(ConfigKey::Theme).ok().flatten(); | |
| 301 | - | let options = state | |
| 325 | + | let themes = state | |
| 302 | 326 | .themes | |
| 303 | 327 | .iter() | |
| 304 | 328 | .map(|theme| { | |
| 305 | - | Choice::new( | |
| 329 | + | DescribedTheme::new( | |
| 306 | 330 | theme.id.clone(), | |
| 307 | - | format!("{} ({})", theme.name, theme.variant), | |
| 331 | + | theme.name.clone(), | |
| 332 | + | variant_of(theme.variant), | |
| 333 | + | tier_of(theme.contrast), | |
| 308 | 334 | ) | |
| 309 | 335 | }) | |
| 310 | 336 | .collect(); | |
| 311 | - | let mut field = Field::select(ConfigKey::Theme.as_str(), "Theme", options) | |
| 337 | + | let mut field = Field::theme(ConfigKey::Theme.as_str(), "Theme", themes) | |
| 312 | 338 | .changes(Action::post("/settings/config/theme")); | |
| 313 | 339 | field.value = chosen; | |
| 314 | 340 | Node::Field(Box::new(field)) | |
| 315 | 341 | } | |
| 316 | 342 | ||
| 343 | + | /// `makeover`'s variant as the description layer's own. | |
| 344 | + | /// | |
| 345 | + | /// The seam the layering costs, and it is three arms. `makeover-layout` takes | |
| 346 | + | /// no dependencies by charter, so it cannot name `makeover::Variant`, and a | |
| 347 | + | /// renderer that groups a picker needs the groups as values. Exhaustive rather | |
| 348 | + | /// than wildcarded on purpose: a fourth ambient mode should stop the build here | |
| 349 | + | /// rather than quietly file itself under dark. | |
| 350 | + | fn variant_of(variant: makeover::Variant) -> ThemeVariant { | |
| 351 | + | match variant { | |
| 352 | + | makeover::Variant::Light => ThemeVariant::Light, | |
| 353 | + | makeover::Variant::Dark => ThemeVariant::Dark, | |
| 354 | + | makeover::Variant::HighContrast => ThemeVariant::HighContrast, | |
| 355 | + | } | |
| 356 | + | } | |
| 357 | + | ||
| 358 | + | /// `makeover`'s measured tier as the description layer's own. | |
| 359 | + | /// | |
| 360 | + | /// [`variant_of`]'s twin, for its reason and with its exhaustiveness. | |
| 361 | + | fn tier_of(tier: makeover::ContrastTier) -> Contrast { | |
| 362 | + | match tier { | |
| 363 | + | makeover::ContrastTier::Low => Contrast::Low, | |
| 364 | + | makeover::ContrastTier::Standard => Contrast::Standard, | |
| 365 | + | makeover::ContrastTier::High => Contrast::High, | |
| 366 | + | } | |
| 367 | + | } | |
| 368 | + | ||
| 317 | 369 | /// A boolean setting as a checkbox that writes when it changes. | |
| 318 | 370 | /// | |
| 319 | 371 | /// `Field::changes` rather than a form: this screen has no submit and never |
| @@ -597,7 +597,8 @@ | |||
| 597 | 597 | ThemeChoice { | |
| 598 | 598 | id: "audiofiles".into(), | |
| 599 | 599 | name: "audiofiles".into(), | |
| 600 | - | variant: "light".into(), | |
| 600 | + | variant: makeover::Variant::Light, | |
| 601 | + | contrast: makeover::ContrastTier::High, | |
| 601 | 602 | source: Some("[color]\nink = \"#111111\"\n".into()), | |
| 602 | 603 | }, | |
| 603 | 604 | // No source, which is the built-in-with-nothing-to-read case: Export | |
| @@ -605,7 +606,8 @@ | |||
| 605 | 606 | ThemeChoice { | |
| 606 | 607 | id: "nord".into(), | |
| 607 | 608 | name: "Nord".into(), | |
| 608 | - | variant: "dark".into(), | |
| 609 | + | variant: makeover::Variant::Dark, | |
| 610 | + | contrast: makeover::ContrastTier::Low, | |
| 609 | 611 | source: None, | |
| 610 | 612 | }, | |
| 611 | 613 | ] | |
| @@ -1032,18 +1034,28 @@ | |||
| 1032 | 1034 | }) | |
| 1033 | 1035 | .expect("the screen offers a theme picker"); | |
| 1034 | 1036 | ||
| 1035 | - | assert_eq!(picker.kind, quasi_router::layout::FieldKind::Select); | |
| 1037 | + | // Grouping arrived, so this is the assertion that had to change. It read | |
| 1038 | + | // `FieldKind::Select`, `options.len() == 2`, and that the variant "survived | |
| 1039 | + | // only by riding in the label" -- the finding asserted as the loss it was. | |
| 1040 | + | // makeover-layout 0.38.0's `Field::theme` is what closed it. | |
| 1041 | + | assert_eq!(picker.kind, quasi_router::layout::FieldKind::Theme); | |
| 1036 | 1042 | assert_eq!(picker.value.as_deref(), Some("nord")); | |
| 1037 | - | assert_eq!(picker.options.len(), 2); | |
| 1038 | - | // The finding, asserted rather than described in prose: the variant is in | |
| 1039 | - | // the label because `Choice` has nowhere else to put it. When grouping | |
| 1040 | - | // arrives, this assertion is what should have to change. | |
| 1043 | + | // No `Choice`es at all: a theme picker offers themes, and a renderer | |
| 1044 | + | // walking `options` for one would draw an empty control. | |
| 1045 | + | assert!(picker.options.is_empty()); | |
| 1046 | + | assert_eq!(picker.themes.len(), 2); | |
| 1047 | + | ||
| 1048 | + | // Both facts the flat list could not carry, now values rather than prose. | |
| 1049 | + | let nord = picker | |
| 1050 | + | .themes | |
| 1051 | + | .iter() | |
| 1052 | + | .find(|theme| theme.id == "nord") | |
| 1053 | + | .expect("the host resolved it"); | |
| 1054 | + | assert_eq!(nord.variant, quasi_router::layout::ThemeVariant::Dark); | |
| 1055 | + | assert_eq!(nord.contrast, quasi_router::layout::Contrast::Low); | |
| 1041 | 1056 | assert!( | |
| 1042 | - | picker | |
| 1043 | - | .options | |
| 1044 | - | .iter() | |
| 1045 | - | .any(|choice| choice.label.contains("dark")), | |
| 1046 | - | "the variant survived only by riding in the label" | |
| 1057 | + | !nord.name.contains("dark"), | |
| 1058 | + | "the variant is a member now, not a suffix on the name" | |
| 1047 | 1059 | ); | |
| 1048 | 1060 | } | |
| 1049 | 1061 |
| @@ -328,7 +328,7 @@ | |||
| 328 | 328 | // Color-math primitives live in `super::color`; the theme calls them through | |
| 329 | 329 | // that seam so the makeover↔egui adapter can be pulled into its own crate if | |
| 330 | 330 | // another project ever needs it. | |
| 331 | - | use super::color::{contrast_ratio, is_light, lerp_color}; | |
| 331 | + | use super::color::{is_light, lerp_color}; | |
| 332 | 332 | ||
| 333 | 333 | // --- Public accessors --- | |
| 334 | 334 | ||
| @@ -601,51 +601,38 @@ | |||
| 601 | 601 | .map(|(_, content)| (*content).to_string()) | |
| 602 | 602 | } | |
| 603 | 603 | ||
| 604 | - | /// Accessibility tier for a theme's muted (tertiary) text, by WCAG contrast of | |
| 605 | - | /// `content_muted` against the theme's darkest and lightest panel backgrounds. We | |
| 606 | - | /// surface this in the picker rather than overriding curated palettes' colors, | |
| 607 | - | /// so users can choose informed while the themes keep their identity. | |
| 608 | - | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] | |
| 609 | - | pub enum ContrastTier { | |
| 610 | - | /// Muted text below the 3:1 WCAG floor for large text / UI components. | |
| 611 | - | Low, | |
| 612 | - | /// Muted text meets 3:1 but not the 4.5:1 normal-text bar. | |
| 613 | - | Standard, | |
| 614 | - | /// Muted text meets WCAG AA (>= 4.5:1) on every panel surface. | |
| 615 | - | High, | |
| 616 | - | } | |
| 617 | - | ||
| 618 | - | impl ContrastTier { | |
| 619 | - | /// Short badge shown next to the theme name. | |
| 620 | - | pub fn badge(self) -> &'static str { | |
| 621 | - | match self { | |
| 622 | - | ContrastTier::High => "AA", | |
| 623 | - | ContrastTier::Standard => "OK", | |
| 624 | - | ContrastTier::Low => "low", | |
| 625 | - | } | |
| 626 | - | } | |
| 627 | - | } | |
| 628 | - | ||
| 629 | - | /// Load a theme's full color set by id (bundled or custom on disk). | |
| 630 | - | fn theme_colors_for(id: &str) -> Option<ThemeColors> { | |
| 631 | - | parse_theme(&theme_source(id)?).ok() | |
| 632 | - | } | |
| 633 | - | ||
| 634 | - | /// Compute the muted-text contrast tier for a theme id. Defaults to `Standard` | |
| 635 | - | /// if the theme can't be loaded. | |
| 636 | - | pub fn theme_contrast_tier(id: &str) -> ContrastTier { | |
| 637 | - | let Some(c) = theme_colors_for(id) else { | |
| 638 | - | return ContrastTier::Standard; | |
| 604 | + | /// How legible a theme's muted text is, for the badge beside its name. | |
| 605 | + | /// | |
| 606 | + | /// # The measurement moved to `makeover`, and the enum with it | |
| 607 | + | /// | |
| 608 | + | /// This was `ContrastTier` and `theme_contrast_tier` here: an enum, a badge | |
| 609 | + | /// spelling, and the WCAG comparison of `content.muted` against the theme's two | |
| 610 | + | /// panel grounds. All three were this app's alone, drawn in | |
| 611 | + | /// `draw_appearance_section` and lost outright when that picker was described, | |
| 612 | + | /// because a `Choice` is a value and a label and there was nowhere to put a | |
| 613 | + | /// tier. | |
| 614 | + | /// | |
| 615 | + | /// makeover-layout 0.38.0's theme picker is where it went. The enum is | |
| 616 | + | /// `makeover::ContrastTier`, the badge spelling is | |
| 617 | + | /// `makeover_layout::Contrast::badge` so that three renderers cannot invent | |
| 618 | + | /// three of them, and the comparison is `makeover::ContrastTier::of` — which is | |
| 619 | + | /// the crate that resolved the colours, and is therefore the only party not | |
| 620 | + | /// parsing every theme file a second time to learn what it already knew. | |
| 621 | + | /// | |
| 622 | + | /// What is left here is the seam: this app's themes are not all on disk, so it | |
| 623 | + | /// hands the loader the source it holds rather than a directory. | |
| 624 | + | /// | |
| 625 | + | /// `Standard` for a theme whose source is missing or will not parse, which is | |
| 626 | + | /// `ContrastTier::of`'s own answer for a theme it cannot measure: the scan | |
| 627 | + | /// failed, and badging the theme `Low` for that would say something untrue | |
| 628 | + | /// about the theme. | |
| 629 | + | pub fn contrast_tier(id: &str) -> makeover::ContrastTier { | |
| 630 | + | let Some(source) = theme_source(id) else { | |
| 631 | + | return makeover::ContrastTier::Standard; | |
| 639 | 632 | }; | |
| 640 | - | let worst = contrast_ratio(c.content_muted, c.surface_page) | |
| 641 | - | .min(contrast_ratio(c.content_muted, c.surface_sunken)); | |
| 642 | - | if worst >= 4.5 { | |
| 643 | - | ContrastTier::High | |
| 644 | - | } else if worst >= 3.0 { | |
| 645 | - | ContrastTier::Standard | |
| 646 | - | } else { | |
| 647 | - | ContrastTier::Low | |
| 648 | - | } | |
| 633 | + | makeover::parse_theme_str(id, &source, false) | |
| 634 | + | .as_ref() | |
| 635 | + | .map_or(makeover::ContrastTier::Standard, makeover::ContrastTier::of) | |
| 649 | 636 | } | |
| 650 | 637 | ||
| 651 | 638 | /// List all available themes (bundled + custom). Custom themes override bundled by ID. |