Skip to main content

max / audiofiles

Render the bevel through makeover-layout and makeover-egui First consumer of the description layer. theme::bevel::Bevel is now a re-export of makeover_layout::Bevel rather than a local enum with the same two variants, so the terminal and webview renderers will invert a bevel the same way this app does instead of each restating the rule. raised_frame and inset_well become makeover_egui::frame calls at Depth::Raised and Depth::Well. Fill and bevel arrive as one thing, which is the point: this file's own doc comment records the bug where banners were filled surface_sunken while shaped like neither a well nor a card, and Depth makes that unrepresentable rather than something each app rediscovers. theme::palette() is the whole makeover-to-renderer adapter, a six-color snapshot read under one lock. Its well field is None because ThemeColors has no surface_well on makeover 2.2.0; makeover-egui supplies surface-page in its place, which is exactly what inset_well filled by hand before, so behaviour is unchanged and the fallback is a transcription rather than a new choice. The inversion test moved to makeover-layout where the rule lives. What is tested here is the adapter: that this app's palette maps the description's edge intents onto the theme colors it resolved. 345 tests pass, clippy clean across the workspace.
Author: Max Johnson <me@maxj.phd> · 2026-07-28 21:23 UTC
Signed with PGP, not checked
Commit: bf8690d2954e8358ae38608ee0f9c0f638ec264b
Parent: d716f94
5 files changed, +95 insertions, -75 deletions
M Cargo.lock +17 -1
@@ -434,7 +434,9 @@
434 434 "hound",
435 435 "libc",
436 436 "makeover",
437 + "makeover-egui",
437 438 "makeover-geometry",
439 + "makeover-layout",
438 440 "objc2 0.6.4",
439 441 "objc2-app-kit 0.3.2",
440 442 "objc2-foundation 0.3.2",
@@ -2971,19 +2973,31 @@
2971 2973
2972 2974 [[package]]
2973 2975 name = "makeover"
2974 - version = "2.2.0"
2976 + version = "2.3.0"
2975 2977 dependencies = [
2976 2978 "include_dir",
2977 2979 "serde",
2978 2980 "toml 1.1.3+spec-1.1.0",
2979 2981 ]
2980 2982
2983 + [[package]]
2984 + name = "makeover-egui"
2985 + version = "0.1.0"
2986 + dependencies = [
2987 + "egui",
2988 + "makeover-layout",
2989 + ]
2990 +
2981 2991 [[package]]
2982 2992 name = "makeover-geometry"
2983 2993 version = "0.1.0"
2984 2994 source = "registry+https://github.com/rust-lang/crates.io-index"
2985 2995 checksum = "9d8b94e51333489b0a79bb63ad4100ca49726f2048e2477f6ee3d6dd822f9327"
2986 2996
2997 + [[package]]
2998 + name = "makeover-layout"
2999 + version = "0.1.0"
3000 +
2987 3001 [[package]]
2988 3002 name = "matchers"
2989 3003 version = "0.2.0"
@@ -5355,6 +5369,7 @@
5355 5369 name = "synckit-client"
5356 5370 version = "0.6.0"
5357 5371 dependencies = [
5372 + "apple-native-keyring-store",
5358 5373 "argon2",
5359 5374 "base64",
5360 5375 "bytes",
@@ -5362,6 +5377,7 @@
5362 5377 "chrono",
5363 5378 "hex",
5364 5379 "keyring",
5380 + "keyring-core",
5365 5381 "parking_lot",
5366 5382 "rand",
5367 5383 "reqwest",
M Cargo.toml +5
@@ -12,6 +12,11 @@
12 12 audiofiles-browser = { path = "crates/audiofiles-browser" }
13 13 audiofiles-sync = { path = "crates/audiofiles-sync" }
14 14 audiofiles-rhai = { path = "crates/audiofiles-rhai" }
15 + # The description layer and its egui renderer. Path deps while both are
16 + # unpublished; they become version deps when makeover-layout 0.1.0 ships,
17 + # which waits on a second renderer exercising the vocabulary.
18 + makeover-layout = { path = "../../Libraries/makeover-layout" }
19 + makeover-egui = { path = "../../Libraries/makeover-egui" }
15 20 egui = { version = "0.35", default-features = false, features = ["default_fonts"] }
16 21 egui_extras = { version = "0.35", default-features = false }
17 22 eframe = { version = "0.35", default-features = false, features = ["default_fonts", "glow"] }
@@ -12,6 +12,8 @@
12 12 audiofiles-rhai = { workspace = true, optional = true }
13 13 audiofiles-sync = { workspace = true }
14 14 egui = { workspace = true }
15 + makeover-layout = { workspace = true }
16 + makeover-egui = { workspace = true }
15 17 egui_extras = { workspace = true }
16 18 symphonia = { workspace = true }
17 19 hound = { workspace = true }
@@ -347,6 +347,29 @@
347 347 THEME.read().border
348 348 }
349 349
350 + /// This app's resolved theme, as the palette `makeover-egui` renders against.
351 + ///
352 + /// The whole makeover-to-renderer adapter, and deliberately a snapshot rather
353 + /// than a handle: it is six `Copy` colors read under one lock, so a caller
354 + /// painting several regions in a frame is not taking the lock per edge.
355 + ///
356 + /// `well` is `None` because `ThemeColors` has no `surface_well`: that intent is
357 + /// derived by makeover 2.3.0, which is bumped in-tree and unpublished, and this
358 + /// app is on 2.2.0. The renderer supplies `surface-page` in its place, which is
359 + /// exactly what [`super::widgets::inset_well`] filled by hand before, so the
360 + /// fallback is a transcription of current behaviour rather than a new choice.
361 + pub fn palette() -> makeover_egui::Palette {
362 + let t = THEME.read();
363 + makeover_egui::Palette {
364 + page: t.surface_page,
365 + raised: t.surface_raised,
366 + overlay: t.surface_overlay,
367 + well: None,
368 + bevel_light: t.bevel_light,
369 + bevel_dark: t.bevel_dark,
370 + }
371 + }
372 +
350 373 /// The lit edge of a bevel: top and left on a raised surface.
351 374 pub fn bevel_light() -> Color32 {
352 375 THEME.read().bevel_light
@@ -378,75 +401,49 @@
378 401 /// `Visuals.widgets.*.bg_stroke` is a single stroke with no per-side control,
379 402 /// so no amount of theme tuning produces a two-tone frame.
380 403 pub mod bevel {
381 - use super::{bevel_dark, bevel_light, stroke};
404 + use super::{palette, stroke};
382 405
383 406 /// 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 407 ///
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 - }
408 + /// Re-exported rather than defined: this used to be a local enum with the
409 + /// same two variants and the same inversion rule. `makeover-layout` owns
410 + /// the vocabulary now, so the terminal and webview renderers invert a
411 + /// bevel the same way this one does instead of each restating it.
412 + pub use makeover_layout::Bevel;
403 413
404 414 /// Paint a 1px two-tone frame just inside `rect`.
405 415 ///
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.
416 + /// Now a thin call into `makeover-egui`, which holds the polyline geometry
417 + /// and the half-stroke inset. What stays here is the choice of stroke
418 + /// width, which is a value and so belongs to the app until
419 + /// `makeover-geometry` carries border widths.
410 420 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 - ));
421 + makeover_egui::paint_bevel(painter, rect, kind, &palette(), stroke::DEFAULT);
429 422 }
430 423
431 424 #[cfg(test)]
432 425 mod tests {
433 426 use super::*;
434 427
428 + // The inversion itself is now asserted in `makeover-layout`, where the
429 + // rule lives. What is worth testing here is the adapter: that this
430 + // app's palette maps the description's edge intents onto the theme
431 + // colors it actually resolved, and does not quietly swap them.
435 432 #[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));
433 + fn a_raised_edge_is_lit_from_the_top_left() {
434 + let p = palette();
435 + let (top_left, bottom_right) = Bevel::Raised.edges();
436 + assert_eq!(p.edge(top_left), crate::ui::theme::bevel_light());
437 + assert_eq!(p.edge(bottom_right), crate::ui::theme::bevel_dark());
443 438 }
444 439
445 440 #[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());
441 + fn pressing_swaps_which_theme_color_each_side_takes() {
442 + let p = palette();
443 + let (tl, _) = Bevel::Raised.edges();
444 + let (ptl, _) = Bevel::Raised.pressed().edges();
445 + assert_eq!(p.edge(tl), crate::ui::theme::bevel_light());
446 + assert_eq!(p.edge(ptl), crate::ui::theme::bevel_dark());
450 447 }
451 448 }
452 449 }
@@ -364,17 +364,17 @@
364 364 /// the bug this replaced — the banners were filled `surface_sunken`, which is
365 365 /// the well colour, while being shaped like neither a well nor a card.
366 366 pub fn raised_frame<R>(ui: &mut egui::Ui, add_contents: impl FnOnce(&mut egui::Ui) -> R) -> R {
367 - let framed = egui::Frame::new()
368 - .fill(theme::surface_raised())
369 - .corner_radius(theme::radius_container())
370 - .inner_margin(egui::Margin::same(theme::space::group() as i8))
371 - .show(ui, add_contents);
372 - theme::bevel::paint(
373 - ui.painter(),
374 - framed.response.rect,
375 - theme::bevel::Bevel::Raised,
376 - );
377 - framed.inner
367 + makeover_egui::frame(
368 + ui,
369 + makeover_layout::Depth::Raised,
370 + &theme::palette(),
371 + makeover_egui::FrameStyle {
372 + radius: theme::radius_container(),
373 + margin: egui::Margin::same(theme::space::group() as i8),
374 + stroke: theme::stroke::DEFAULT,
375 + },
376 + add_contents,
377 + )
378 378 }
379 379
380 380 /// A recessed container: a hole in the panel with content down inside it.
@@ -386,17 +386,17 @@
386 386 /// The one-point inner margin keeps content off the frame. Without it the first
387 387 /// row of a table sits underneath the bevel and the well loses its top edge.
388 388 pub fn inset_well<R>(ui: &mut egui::Ui, add_contents: impl FnOnce(&mut egui::Ui) -> R) -> R {
389 - let framed = egui::Frame::new()
390 - .fill(theme::surface_page())
391 - .corner_radius(theme::radius_container())
392 - .inner_margin(egui::Margin::same(theme::stroke::DEFAULT as i8))
393 - .show(ui, add_contents);
394 - theme::bevel::paint(
395 - ui.painter(),
396 - framed.response.rect,
397 - theme::bevel::Bevel::Inset,
398 - );
399 - framed.inner
389 + makeover_egui::frame(
390 + ui,
391 + makeover_layout::Depth::Well,
392 + &theme::palette(),
393 + makeover_egui::FrameStyle {
394 + radius: theme::radius_container(),
395 + margin: egui::Margin::same(theme::stroke::DEFAULT as i8),
396 + stroke: theme::stroke::DEFAULT,
397 + },
398 + add_contents,
399 + )
400 400 }
401 401
402 402 /// Inline informational banner: raised card, body text in `content_secondary`.