Skip to main content

max / audiofiles

Platinum phase 2: the egui bevel primitive and the radius split theme.rs gains `pub mod bevel`: a `Bevel::{Raised, Inset}` free function painting a 1px two-tone frame as two mitered polylines. Hand-painted because egui has no `box-shadow: inset` and `bg_stroke` is one stroke with no per-side control, so no amount of theme tuning gets there. It paints lines and nothing else, which is what lets it compose over a TextEdit after `ui.add` in phase 3. `bevel_light` / `bevel_dark` are read as intents rather than re-derived locally: makeover 2.2.0 already derives them from `surface-raised`, so the DerivedColors fallback the spec allowed for is unnecessary. One `rounding` becomes `radius_control` (4, buttons and chips) and `radius_container` (0, panels, wells, modals, scrollbars, separators). egui's noninteractive state plus window and menu take the container radius; the four interactive states take the control one. A theme still setting `rounding` gets it as the control radius. The three `CornerRadius::same(4)` literals are gone, so the charter's no-literal rule now holds for radius as it does for colour. `theme_colors_default_is_audiofiles` now asserts the fallback equals what parsing the bundled skin produces, rather than restating four hex values, which is how it passed unchanged through a ramp move that changed all four. 342 tests green, clippy at its prior warning count, hard rule 1 holds.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-27 18:04 UTC
Signed with PGP, not checked
Commit: 90a5f760f284955a3dc9ea6b3bfff36aefdb320c
Parent: 58ac453
4 files changed, +302 insertions, -35 deletions
@@ -32,10 +32,16 @@
32 32
33 33 All UI colour reads must go through these accessors. Every name is an intent, not
34 34 an appearance: `action` says what the colour is for, `accent_blue` said what it
35 - looked like on one theme and lied on the next. The 14 base slots come from
35 + looked like on one theme and lied on the next. The 16 base slots come from
36 36 `ThemeColors`, parsed from the theme TOML. Derived slots are blended once per
37 37 theme install (`DerivedColors`) rather than per read.
38 38
39 + Two of the sixteen are not written by any theme author: `bevel-light` and
40 + `bevel-dark` are derived inside `makeover::resolve` from `surface-raised`, so
41 + every shipped theme carries a light model without opting in. They are base slots
42 + here because this app reads them like any other intent, not because a TOML file
43 + sets them.
44 +
39 45 | Token | Kind | Use |
40 46 |-----------------------|----------|--------------------------------------------------------------------|
41 47 | `surface_page()` | base | Deepest layer (table body, scroll background). |
@@ -52,6 +58,8 @@
52 58 | `category_five()` | base | Classification palette only. |
53 59 | `category_six()` | base | Classification palette only. |
54 60 | `border()` | base | Separator, panel boundary. |
61 + | `bevel_light()` | base | The lit edge of a bevel: top and left when raised. From makeover. |
62 + | `bevel_dark()` | base | The shadowed edge: bottom and right when raised. From makeover. |
55 63 | `row_even()` | derived | Striped row alternation (even) = `lerp(page, overlay, 0.3)`. |
56 64 | `row_odd()` | alias | Striped row alternation (odd) = `surface_page`. |
57 65 | `hover_surface()` | alias | = `surface_sunken`. Shared row/button hover. |
@@ -96,16 +104,48 @@
96 104
97 105 The values come from the [`makeover-geometry`](https://makenot.work/git/max/makeover-geometry) crate, which is where Balanced Breakfast and GoingsOn get the same vocabulary. Each is a ratio of a base unit rather than a pixel count, resolved against a surface whose quantum is one physical pixel, so the layout is honest at any display density.
98 106
99 - **Spacing is not themeable, and this is the one hard rule.** A theme overrides colour; geometry is invariant, which is exactly why it belongs to a shared crate rather than to this app. The old `[spacing]` TOML section is gone, along with the `section_spacing`, `grid_row_spacing`, `item_spacing_x/y` and `button_padding_x/y` keys. `rounding` survives under a `[geometry]` section because the crate carries spacing only.
107 + **Spacing is not themeable, and this is the one hard rule.** A theme overrides colour; geometry is invariant, which is exactly why it belongs to a shared crate rather than to this app. The old `[spacing]` TOML section is gone, along with the `section_spacing`, `grid_row_spacing`, `item_spacing_x/y` and `button_padding_x/y` keys. Corner radius survives under a `[geometry]` section because the crate carries spacing only.
100 108
101 109 ### Stroke / radius
102 110
103 - | Token | Value (default) | Use |
104 - |--------------------|-----------------|--------------------------------------------------|
105 - | `rounding` | 4.0 (TOML) | All widget corner radii. |
106 - | `border_thin` | 0.5 px | Separators, inactive widget border. |
107 - | `border_default` | 1.0 px | Hovered/active widget border, window stroke. |
108 - | `focus_ring` | 1.5 px `action` | New token: focus outline on text fields. |
111 + | Token | Value (default) | Use |
112 + |------------------------|-----------------|-----------------------------------------------|
113 + | `radius_control()` | 4.0 (TOML) | Buttons, chips, and anything else you press. |
114 + | `radius_container()` | 0.0 (TOML) | Panels, cards, wells, modals, scrollbars, separators. |
115 + | `border_thin` | 0.5 px | Separators, inactive widget border. |
116 + | `border_default` | 1.0 px | Hovered/active widget border, window stroke. |
117 + | `focus_ring` | 1.5 px `action` | Focus outline on text fields. |
118 +
119 + One radius became two because Platinum wants square containers and push buttons
120 + that still keep a touch of a corner. Both stay overridable under `[geometry]`.
121 + A theme that still sets the old single `rounding` key gets it as the control
122 + radius and square containers, which is the closest reading of what it asked for.
123 +
124 + **Never a `CornerRadius::same(N)` literal outside `theme.rs`.** Pick the token
125 + that says what the surface is; the number follows. This is the same rule as the
126 + spacing one and it exists for the same reason: a literal is a value edit waiting
127 + to happen, and the whole radius map moves at once or not at all.
128 +
129 + ### Bevel
130 +
131 + The Platinum light model. A raised surface is lit from the top left, so its top
132 + and left edges take `bevel_light()` and its bottom and right edges `bevel_dark()`.
133 + Inverting the pair reads as recessed, which is also the pressed state of anything
134 + raised.
135 +
136 + ```rust
137 + pub enum Bevel { Raised, Inset }
138 + pub fn paint(painter: &egui::Painter, rect: egui::Rect, kind: Bevel);
139 + ```
140 +
141 + Hand-painted, because egui has no `box-shadow: inset` and
142 + `Visuals.widgets.*.bg_stroke` is one stroke with no per-side control. No amount
143 + of theme tuning gets a two-tone frame, so this is a free function in `theme.rs`
144 + rather than anything declarative. It paints two 3-point polylines inside `rect`
145 + and nothing else: no fill, no clearing. Fill first, bevel after.
146 +
147 + Which surfaces take which is phases 3 and 4 of the conversion (wiki
148 + `af-platinum`); this section is the primitive only.
109 149
110 150 ---
111 151
@@ -156,7 +196,7 @@
156 196
157 197 ### Banners and notifications
158 198
159 - - **`info_banner(ui, body)`**: frame with `surface_sunken()`, `corner_radius(4)`, `space::group()` inset; used by the existing VFS first-run banner and any future inline tips.
199 + - **`info_banner(ui, body)`**: frame with `surface_sunken()`, `radius_container()`, `space::group()` inset; used by the existing VFS first-run banner and any future inline tips.
160 200 - **`toast(ctx, severity, body)`**: *new* primitive. Timed transient notification surfaced from a state-owned queue. Replaces the current "set `state.status = '...'`" pattern for errors and ephemeral confirmations (rename success, copy-to-clipboard). The existing footer status label is retained for *persistent* state ("Sync: 3 pending") but should no longer be the channel for transient error feedback.
161 201 - **`loading_spinner(ui)` / `busy_indicator(ui, label)`**: *new* primitive for in-flight operations that aren't full-screen (sidebar refresh, sync running). Import has its own progress screen and is out of scope here.
162 202
@@ -116,7 +116,7 @@
116 116 ui.add_space(theme::space::bound());
117 117 egui::Frame::new()
118 118 .fill(theme::surface_sunken())
119 - .corner_radius(egui::CornerRadius::same(4))
119 + .corner_radius(theme::radius_container())
120 120 .inner_margin(egui::Margin::same(theme::space::group() as i8))
121 121 .show(ui, |ui| {
122 122 ui.label(egui::RichText::new(err).color(theme::danger()));
@@ -143,7 +143,7 @@
143 143 LazyLock::new(|| makeover::embedded_themes().collect());
144 144
145 145 /// The resolved theme colors (intent vocabulary) for egui.
146 - #[derive(Debug, Clone)]
146 + #[derive(Debug, Clone, PartialEq)]
147 147 pub struct ThemeColors {
148 148 // Background
149 149 pub surface_page: Color32,
@@ -163,10 +163,19 @@
163 163 pub category_six: Color32,
164 164 // Border
165 165 pub border: Color32,
166 + // Bevel. Derived inside `makeover::resolve` from `surface-raised`, so no
167 + // theme author writes them and every shipped theme has a light model.
168 + // Read here like any other intent.
169 + pub bevel_light: Color32,
170 + pub bevel_dark: Color32,
166 171 // Corner radius. Not spacing: the geometry crate is spacing-only today,
167 172 // and a theme setting its own radius is a standing audiofiles quirk that
168 173 // this change does not decide either way.
169 - pub rounding: f32,
174 + //
175 + // Two radii rather than one because Platinum wants square containers and
176 + // push buttons that keep a touch of a corner. See `docs/design-system.md`.
177 + pub radius_control: f32,
178 + pub radius_container: f32,
170 179 }
171 180
172 181 impl Default for ThemeColors {
@@ -174,10 +183,10 @@
174 183 // audiofiles default: Mac OS 8 Platinum (keep in sync with
175 184 // themes/audiofiles.toml, this is the pre-load Rust fallback).
176 185 Self {
177 - surface_page: Color32::from_rgb(0xDD, 0xDD, 0xDD),
178 - surface_overlay: Color32::from_rgb(0xEE, 0xEE, 0xEE),
179 - surface_sunken: Color32::from_rgb(0xB0, 0xB0, 0xB0),
180 - surface_raised: Color32::from_rgb(0xFF, 0xFF, 0xFF),
186 + surface_page: Color32::from_rgb(0xC8, 0xC8, 0xC8),
187 + surface_overlay: Color32::from_rgb(0xD4, 0xD4, 0xD4),
188 + surface_sunken: Color32::from_rgb(0xA8, 0xA8, 0xA8),
189 + surface_raised: Color32::from_rgb(0xDD, 0xDD, 0xDD),
181 190 content: Color32::from_rgb(0x00, 0x00, 0x00),
182 191 content_secondary: Color32::from_rgb(0x33, 0x33, 0x33),
183 192 content_muted: Color32::from_rgb(0x80, 0x80, 0x80),
@@ -188,7 +197,13 @@
188 197 category_five: Color32::from_rgb(0x00, 0x9C, 0xDF),
189 198 category_six: Color32::from_rgb(0x97, 0x39, 0x99),
190 199 border: Color32::from_rgb(0x80, 0x80, 0x80),
191 - rounding: 4.0,
200 + // What `makeover::resolve` derives from the raised surface above:
201 + // the lightening clamps at the top of the ramp, which is the white
202 + // highlight the Platinum tribute wanted.
203 + bevel_light: Color32::from_rgb(0xFF, 0xFF, 0xFF),
204 + bevel_dark: Color32::from_rgb(0xA4, 0xA4, 0xA4),
205 + radius_control: 4.0,
206 + radius_container: 0.0,
192 207 }
193 208 }
194 209 }
@@ -332,6 +347,110 @@
332 347 THEME.read().border
333 348 }
334 349
350 + /// The lit edge of a bevel: top and left on a raised surface.
351 + pub fn bevel_light() -> Color32 {
352 + THEME.read().bevel_light
353 + }
354 + /// The shadowed edge of a bevel: bottom and right on a raised surface.
355 + pub fn bevel_dark() -> Color32 {
356 + THEME.read().bevel_dark
357 + }
358 +
359 + /// Corner radius for things you press: buttons, chips, toggles.
360 + pub fn radius_control() -> egui::CornerRadius {
361 + egui::CornerRadius::same(THEME.read().radius_control as u8)
362 + }
363 + /// Corner radius for things that hold other things: panels, cards, wells,
364 + /// modals, scrollbars, separators. Square under the Platinum default.
365 + pub fn radius_container() -> egui::CornerRadius {
366 + egui::CornerRadius::same(THEME.read().radius_container as u8)
367 + }
368 +
369 + /// The Platinum light model: a two-tone 1px frame that makes a rectangle read
370 + /// as raised off its background or recessed into it.
371 + ///
372 + /// A free function rather than a widget, per the layering table in
373 + /// `docs/design-system.md`: `theme.rs` holds tokens and the primitives that
374 + /// paint them, `widgets.rs` holds anything that allocates space or answers a
375 + /// click.
376 + ///
377 + /// Hand-painted because egui has no `box-shadow: inset` and
378 + /// `Visuals.widgets.*.bg_stroke` is a single stroke with no per-side control,
379 + /// so no amount of theme tuning produces a two-tone frame.
380 + pub mod bevel {
381 + use super::{bevel_dark, bevel_light, stroke};
382 +
383 + /// Which way the light falls.
384 + #[derive(Debug, Clone, Copy, PartialEq, Eq)]
385 + pub enum Bevel {
386 + /// Lit from the top left: light top and left, dark bottom and right.
387 + Raised,
388 + /// The same frame inverted, which is also the pressed state of
389 + /// anything that draws itself `Raised`.
390 + Inset,
391 + }
392 +
393 + /// The (top-left, bottom-right) edge colors for a bevel.
394 + ///
395 + /// Split out from [`paint`] because the inversion is the load-bearing part
396 + /// and a `Painter` needs a live render context to build.
397 + fn edges(kind: Bevel) -> (egui::Color32, egui::Color32) {
398 + match kind {
399 + Bevel::Raised => (bevel_light(), bevel_dark()),
400 + Bevel::Inset => (bevel_dark(), bevel_light()),
401 + }
402 + }
403 +
404 + /// Paint a 1px two-tone frame just inside `rect`.
405 + ///
406 + /// Fill first, bevel after: this paints two polylines and nothing else, so
407 + /// it composes onto whatever is already there rather than clearing it.
408 + /// That is what lets it go over an `egui::TextEdit` after `ui.add`, where
409 + /// the widget's own fill has already landed.
410 + pub fn paint(painter: &egui::Painter, rect: egui::Rect, kind: Bevel) {
411 + let (light, dark) = edges(kind);
412 +
413 + // Inset by half a stroke so the 1px line lands inside `rect` rather
414 + // than straddling its edge, which on a fractional-scale display is the
415 + // difference between one crisp pixel and two dim ones.
416 + let r = rect.shrink(stroke::DEFAULT / 2.0);
417 +
418 + // Two 3-point polylines meeting at the opposite corners. A polyline
419 + // rather than three segments so the corner joins are mitered by egui
420 + // instead of leaving a notch.
421 + painter.add(egui::Shape::line(
422 + vec![r.left_bottom(), r.left_top(), r.right_top()],
423 + egui::Stroke::new(stroke::DEFAULT, light),
424 + ));
425 + painter.add(egui::Shape::line(
426 + vec![r.right_top(), r.right_bottom(), r.left_bottom()],
427 + egui::Stroke::new(stroke::DEFAULT, dark),
428 + ));
429 + }
430 +
431 + #[cfg(test)]
432 + mod tests {
433 + use super::*;
434 +
435 + #[test]
436 + fn inset_is_raised_with_the_light_moved() {
437 + // The whole idiom in one assertion: the pressed state of a raised
438 + // control is the same frame with its two edges swapped, which is
439 + // why a bevel is cheap to invert and expensive to fake.
440 + let (rl, rd) = edges(Bevel::Raised);
441 + let (il, id) = edges(Bevel::Inset);
442 + assert_eq!((rl, rd), (id, il));
443 + }
444 +
445 + #[test]
446 + fn a_raised_edge_is_lit_from_the_top_left() {
447 + let (top_left, bottom_right) = edges(Bevel::Raised);
448 + assert_eq!(top_left, bevel_light());
449 + assert_eq!(bottom_right, bevel_dark());
450 + }
451 + }
452 + }
453 +
335 454 /// Section spacing for detail panel (between waveform, metadata, tags, actions).
336 455 ///
337 456 /// Kept as a named function because the detail panel reads it in several
@@ -524,6 +643,13 @@
524 643 .unwrap_or(default)
525 644 };
526 645
646 + // `rounding` was the single radius before it split in two. A theme still
647 + // setting it asked for that radius on the things it could see, which are
648 + // the controls; containers go square with everything else. Reading it as
649 + // the control default rather than ignoring it keeps a user's own theme
650 + // file working without freezing the old key into the vocabulary.
651 + let legacy_rounding = get_f32("rounding", 4.0);
652 +
527 653 Ok(ThemeColors {
528 654 surface_page: c("surface-page", Color32::BLACK),
529 655 surface_overlay: c("surface-overlay", Color32::BLACK),
@@ -539,7 +665,13 @@
539 665 category_five: c("category-five", Color32::from_rgb(0xBD, 0x93, 0xF9)),
540 666 category_six: c("category-six", Color32::from_rgb(0x88, 0xC0, 0xD0)),
541 667 border: c("border", Color32::DARK_GRAY),
542 - rounding: get_f32("rounding", 4.0),
668 + // No fallback worth naming: makeover derives both from
669 + // `surface-raised`, so a theme reaching here without them has no
670 + // raised surface either and the bevel has nothing to sit on.
671 + bevel_light: c("bevel-light", Color32::WHITE),
672 + bevel_dark: c("bevel-dark", Color32::DARK_GRAY),
673 + radius_control: get_f32("radius_control", legacy_rounding),
674 + radius_container: get_f32("radius_container", 0.0),
543 675 })
544 676 }
545 677
@@ -779,13 +911,19 @@
779 911 visuals.window_stroke = egui::Stroke::new(1.0, t.border);
780 912 visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, t.border);
781 913
782 - // Softer edges on all widgets
783 - let rounding = egui::CornerRadius::same(t.rounding as u8);
784 - visuals.widgets.noninteractive.corner_radius = rounding;
785 - visuals.widgets.inactive.corner_radius = rounding;
786 - visuals.widgets.hovered.corner_radius = rounding;
787 - visuals.widgets.active.corner_radius = rounding;
788 - visuals.widgets.open.corner_radius = rounding;
914 + // The radius split. egui's `noninteractive` state is what panels, frames
915 + // and separators draw themselves with, so it takes the container radius;
916 + // the four interactive states are buttons and take the control radius.
917 + // Windows and menus are containers by any reading.
918 + let control = egui::CornerRadius::same(t.radius_control as u8);
919 + let container = egui::CornerRadius::same(t.radius_container as u8);
920 + visuals.widgets.noninteractive.corner_radius = container;
921 + visuals.widgets.inactive.corner_radius = control;
922 + visuals.widgets.hovered.corner_radius = control;
923 + visuals.widgets.active.corner_radius = control;
924 + visuals.widgets.open.corner_radius = control;
925 + visuals.window_corner_radius = container;
926 + visuals.menu_corner_radius = container;
789 927
790 928 // Softer widget borders: thinner strokes on inactive/hover states
791 929 visuals.widgets.inactive.bg_stroke =
@@ -1058,6 +1196,84 @@
1058 1196 assert_eq!(theme.surface_page, Color32::from_rgb(0xaa, 0xbb, 0xcc));
1059 1197 }
1060 1198
1199 + // Bevel and the radius split
1200 +
1201 + #[test]
1202 + fn parse_theme_reads_the_derived_bevel_pair() {
1203 + // No theme file writes these: makeover derives them from
1204 + // `surface-raised`, so a theme with a raised surface has a light model
1205 + // whether or not its author thought about one.
1206 + let theme = parse_theme(
1207 + r##"
1208 + [surface]
1209 + raised = "#808080"
1210 + "##,
1211 + )
1212 + .unwrap();
1213 + assert_ne!(theme.bevel_light, theme.surface_raised);
1214 + assert_ne!(theme.bevel_dark, theme.surface_raised);
1215 + assert_ne!(theme.bevel_light, theme.bevel_dark);
1216 + }
1217 +
1218 + #[test]
1219 + fn the_bundled_skin_can_hold_a_bevel() {
1220 + // The reason the ramp moved: a highlight derived from white is white,
1221 + // so a white `raised` bevels on two sides and never resolves as a lit
1222 + // object. makeover has its own version of this assertion over the
1223 + // whole shipped set; this one guards the skin this app defaults to.
1224 + let d = ThemeColors::default();
1225 + assert_ne!(
1226 + d.bevel_light, d.surface_raised,
1227 + "the lit edge vanished into the face it sits on"
1228 + );
1229 + assert_ne!(
1230 + d.bevel_dark, d.surface_raised,
1231 + "the shadowed edge vanished into the face it sits on"
1232 + );
1233 + }
1234 +
1235 + /// The two radii a theme source parses to.
1236 + ///
1237 + /// A helper rather than four inline pairs of `assert_eq!` because the
1238 + /// comparison is exact and clippy is right to ask about that in general:
1239 + /// a radius is a small integer that made a round trip through TOML and
1240 + /// `f32`, so it is exactly representable and an epsilon would be a weaker
1241 + /// assertion, not a safer one. Saying that once beats saying it seven
1242 + /// times.
1243 + #[allow(clippy::float_cmp)]
1244 + fn assert_radii(source: &str, control: f32, container: f32) {
1245 + let theme = parse_theme(source).unwrap();
1246 + assert_eq!(theme.radius_control, control, "radius_control");
1247 + assert_eq!(theme.radius_container, container, "radius_container");
1248 + }
1249 +
1250 + #[test]
1251 + fn radius_splits_into_control_and_container() {
1252 + assert_radii(
1253 + "[geometry]\nradius_control = 6\nradius_container = 2\n",
1254 + 6.0,
1255 + 2.0,
1256 + );
1257 + }
1258 +
1259 + #[test]
1260 + fn radius_defaults_are_platinum() {
1261 + // Square containers, a touch of a corner on things you press.
1262 + assert_radii("", 4.0, 0.0);
1263 + }
1264 +
1265 + #[test]
1266 + fn the_old_rounding_key_still_means_something() {
1267 + // A user's custom theme predating the split asked for one radius on
1268 + // the things it could see, which were the controls.
1269 + assert_radii("[geometry]\nrounding = 10\n", 10.0, 0.0);
1270 + }
1271 +
1272 + #[test]
1273 + fn the_new_keys_win_over_the_old_one() {
1274 + assert_radii("[geometry]\nrounding = 10\nradius_control = 3\n", 3.0, 0.0);
1275 + }
1276 +
1061 1277 #[test]
1062 1278 fn parse_theme_invalid_hex_in_field_uses_fallback() {
1063 1279 let toml = r##"
@@ -1150,13 +1366,24 @@
1150 1366
1151 1367 #[test]
1152 1368 fn theme_colors_default_is_audiofiles() {
1153 - let d = ThemeColors::default();
1154 - // Mac OS 8 Platinum: warm-neutral chrome, white wells, black text,
1155 - // Appearance-Manager navy for the action ring.
1156 - assert_eq!(d.surface_page, Color32::from_rgb(0xDD, 0xDD, 0xDD));
1157 - assert_eq!(d.content, Color32::from_rgb(0x00, 0x00, 0x00));
1158 - assert_eq!(d.action, Color32::from_rgb(0x3B, 0x5A, 0x9F));
1159 - assert_eq!(d.border, Color32::from_rgb(0x80, 0x80, 0x80));
1369 + // The default is the pre-load fallback for this app's own skin, so it
1370 + // is only correct while it equals what parsing that skin produces.
1371 + // Asserting the two are equal rather than restating the hex values
1372 + // means a change to `themes/audiofiles.toml` in makeover fails here
1373 + // instead of drifting: the old version of this test named four
1374 + // literals and passed happily through a ramp change that moved all of
1375 + // them.
1376 + let bundled = BUNDLED_THEMES
1377 + .iter()
1378 + .find(|(id, _)| *id == DEFAULT_THEME_ID)
1379 + .map(|(_, content)| *content)
1380 + .expect("the app's own skin is bundled");
1381 +
1382 + assert_eq!(
1383 + parse_theme(bundled).expect("the app's own skin parses"),
1384 + ThemeColors::default(),
1385 + "ThemeColors::default() has drifted from themes/{DEFAULT_THEME_ID}.toml"
1386 + );
1160 1387 }
1161 1388
1162 1389 // Bundled theme parsing (round-trip all embedded themes)
@@ -320,12 +320,12 @@
320 320 }
321 321 }
322 322
323 - /// Inline informational banner: rounded frame, `surface_sunken` fill, body text
323 + /// Inline informational banner: container-radius frame, `surface_sunken` fill, body text
324 324 /// in `content_secondary`. Used for one-time tips and unobtrusive panel notices.
325 325 pub fn info_banner(ui: &mut egui::Ui, body: &str) {
326 326 egui::Frame::new()
327 327 .fill(theme::surface_sunken())
328 - .corner_radius(egui::CornerRadius::same(4))
328 + .corner_radius(theme::radius_container())
329 329 .inner_margin(egui::Margin::same(theme::space::group() as i8))
330 330 .show(ui, |ui| {
331 331 ui.label(
@@ -343,7 +343,7 @@
343 343 pub fn warning_banner(ui: &mut egui::Ui, body: &str) {
344 344 egui::Frame::new()
345 345 .fill(theme::surface_sunken())
346 - .corner_radius(egui::CornerRadius::same(4))
346 + .corner_radius(theme::radius_container())
347 347 .inner_margin(egui::Margin::same(theme::space::group() as i8))
348 348 .show(ui, |ui| {
349 349 ui.label(egui::RichText::new(body).color(theme::warning()));