Skip to main content

max / audiofiles

Describe the Add Library form's fields Takes makeover-layout 0.8.2 and makeover-immediate 0.5.2, and puts the first two audiofiles call sites onto the description. The name input becomes a Field: a well, a label, a required marker, and an error line. The error is new behaviour and closes a hole -- with a folder chosen and no name, "Create New" disabled itself and said nothing about why. The storage style becomes a FieldKind::Radio, the kind added upstream for this call site. The comment above it already said why the choice must not sit behind a closed control (copying samples in or referencing them in place cannot be changed later); the description can say that now instead of the app enforcing it by hand. The one fact that applies to the whole question moves into the field's hint, where it is visible rather than hover-only. The folder picker stays hand-rolled. A path-picker kind would be decoration in both renderers -- the value is a string either way and the dialog is the app's job -- and this vocabulary's bar is that a kind changes what a renderer does. theme.rs: extreme_bg_color was the page and is now the well. egui fills what you look into with it, text edits above all, so a described field and all 21 widgets::text_field sites were painting two different shades of well. It was the last place still substituting the page for a well token, which makeover 2.3.0 derived and palette() already documents having outgrown. widgets::field wraps the renderer with this app's palette and geometry, the way raised_frame and inset_well already do. Bare checkboxes deliberately stay as ui.checkbox: under field() one is ui.checkbox plus a required marker at three times the source. A described field asks a labelled question with help, validation, options or a well; a bare toggle is a control. dialog.rs: fold a match into ?, which clippy had been flagging.
Author: Max Johnson <me@maxj.phd> · 2026-08-05 20:16 UTC
Signed with PGP, not checked
Commit: fb7db74ec415e09b80b2d6ac7776730923ef7ad8
Parent: c0e6843
6 files changed, +140 insertions, -31 deletions
M Cargo.lock +8 -8
@@ -2990,9 +2990,9 @@
2990 2990
2991 2991 [[package]]
2992 2992 name = "makeover-immediate"
2993 - version = "0.5.0"
2993 + version = "0.5.2"
2994 2994 source = "registry+https://github.com/rust-lang/crates.io-index"
2995 - checksum = "84f4962445845b0f436ef8f60035dbf0748bddda84f1e060183e82b3e2cafc7b"
2995 + checksum = "66eefff77a8f3b3c36afc479c193c46df37c21d924451b47e35a3f08469a5355"
2996 2996 dependencies = [
2997 2997 "egui",
2998 2998 "makeover-layout",
@@ -3000,9 +3000,9 @@
3000 3000
3001 3001 [[package]]
3002 3002 name = "makeover-layout"
3003 - version = "0.8.0"
3003 + version = "0.8.2"
3004 3004 source = "registry+https://github.com/rust-lang/crates.io-index"
3005 - checksum = "ab87cfbb9bb3278d647159ee2089ed4bcabc66d77a6433e65563ae784943ce02"
3005 + checksum = "9883c75a9d26fce10be2b979c01a74f8c07513800a70ec6bf598de1e7d411f1b"
3006 3006
3007 3007 [[package]]
3008 3008 name = "matchers"
@@ -7297,10 +7297,6 @@
7297 7297 "winnow 1.0.4",
7298 7298 ]
7299 7299
7300 - [[patch.unused]]
7301 - name = "supernote-push"
7302 - version = "0.1.0"
7303 -
7304 7300 [[patch.unused]]
7305 7301 name = "kberg"
7306 7302 version = "0.1.0"
@@ -7312,3 +7308,7 @@
7312 7308 [[patch.unused]]
7313 7309 name = "docengine"
7314 7310 version = "0.4.0"
7311 +
7312 + [[patch.unused]]
7313 + name = "supernote-push"
7314 + version = "0.1.0"
M Cargo.toml +2 -2
@@ -12,8 +12,8 @@
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 - makeover-layout = "0.8.0"
16 - makeover-immediate = "0.5.0"
15 + makeover-layout = "0.8.2"
16 + makeover-immediate = "0.5.2"
17 17 egui = { version = "0.35", default-features = false, features = ["default_fonts"] }
18 18 egui_extras = { version = "0.35", default-features = false }
19 19 eframe = { version = "0.35", default-features = false, features = ["default_fonts", "glow"] }
@@ -194,10 +194,7 @@
194 194 /// to apply (on the GUI thread, with `&mut BrowserState`).
195 195 pub fn take_completed(&self) -> Option<(DialogHandler, Vec<PathBuf>)> {
196 196 let mut slot = self.pending.lock();
197 - let result = match slot.as_ref() {
198 - Some(p) => p.rx.try_recv(),
199 - None => return None,
200 - };
197 + let result = slot.as_ref()?.rx.try_recv();
201 198 match result {
202 199 Ok(paths) => {
203 200 let pending = slot.take().expect("pending checked above");
@@ -375,10 +375,34 @@
375 375
376 376 // Create new library
377 377 ui.label(egui::RichText::new("Add Library").strong());
378 - ui.horizontal(|ui| {
379 - ui.label("Name:");
380 - ui.text_edit_singleline(&mut state.settings.create_name);
381 - });
378 + // The name is the one input in this form the description can say.
379 + // The folder picker is a button and a path, and the storage style
380 + // is a radio pair; `FieldKind` has neither, so both stay
381 + // hand-rolled below and `makeover_immediate::group` cannot be used
382 + // for a form with one describable field out of three. Filed on
383 + // GoingsOn `bebfd112` rather than worked around here.
384 + //
385 + // The error is the state the old form left unexplained: a folder
386 + // chosen and no name, where "Create New" disables itself and says
387 + // nothing about why.
388 + let name_missing = state.settings.create_path.is_some()
389 + && state.settings.create_name.trim().is_empty();
390 + let name_field = makeover_layout::Field {
391 + required: true,
392 + error: name_missing.then_some("A library needs a name."),
393 + ..makeover_layout::Field::new(
394 + makeover_layout::FieldKind::Text,
395 + "create_name",
396 + "Name",
397 + )
398 + };
399 + widgets::field(
400 + ui,
401 + &name_field,
402 + makeover_immediate::Filling::Text(&mut state.settings.create_name),
403 + None,
404 + );
405 + ui.add_space(theme::space::bound());
382 406 ui.horizontal(|ui| {
383 407 if ui.button("Choose folder...").clicked() {
384 408 state.dialogs.pick_folder("Choose folder", |s, p| {
@@ -396,19 +420,54 @@
396 420 // Storage style is a significant choice (copy vs reference in
397 421 // place), promote it from a buried checkbox to an explicit radio
398 422 // choice so users opt into loose-files mode deliberately.
399 - ui.label(egui::RichText::new("Storage style:").small().color(theme::content_secondary()));
400 - let mut style = state.settings.create_loose_files;
401 - if ui.radio_value(&mut style, false, "Copy samples into library (recommended)")
402 - .on_hover_text("Samples are duplicated into the library's content-addressed store. Originals can be moved or deleted safely.")
403 - .changed()
423 + //
424 + // A described field now, and specifically a `Radio` rather than a
425 + // `Select`: this is the call site `FieldKind::Radio` was added for
426 + // in makeover-layout 0.8.1. The reason the comment above gives is
427 + // the reason the kind exists — the alternatives to an irreversible
428 + // choice have to be readable without opening anything — and it was
429 + // being enforced by hand here because the description could not say
430 + // it.
431 + const STORAGE_STYLES: [makeover_layout::Choice<'static>; 2] = [
432 + makeover_layout::Choice {
433 + value: "copy",
434 + label: "Copy samples into library (recommended)",
435 + },
436 + makeover_layout::Choice {
437 + value: "reference",
438 + label: "Reference samples in place (loose-files mode)",
439 + },
440 + ];
441 + // The description names its options by string and this app holds a
442 + // bool, so the two are marshalled here rather than by reshaping the
443 + // state. "Which of a fixed set" is the question; the bool is this
444 + // app's private encoding of the answer, and it stays private.
445 + let mut storage = String::from(if state.settings.create_loose_files {
446 + "reference"
447 + } else {
448 + "copy"
449 + });
450 + let storage_field = makeover_layout::Field {
451 + // Was the tail of one option's hover text. It is the one fact
452 + // that applies to the whole question rather than to either
453 + // answer, and standing help is shown without being hunted for,
454 + // which an irreversible choice deserves.
455 + hint: Some("Cannot be changed after the library is created."),
456 + ..makeover_layout::Field::radio(
457 + "create_storage_style",
458 + "Storage style",
459 + &STORAGE_STYLES,
460 + )
461 + };
462 + if widgets::field(
463 + ui,
464 + &storage_field,
465 + makeover_immediate::Filling::Text(&mut storage),
466 + None,
467 + )
468 + .is_some_and(|response| response.changed())
404 469 {
405 - state.settings.create_loose_files = style;
406 - }
407 - if ui.radio_value(&mut style, true, "Reference samples in place (loose-files mode)")
408 - .on_hover_text("Reference files in place instead of duplicating. Saves disk space but samples break if originals are moved or deleted. Cannot be changed later.")
409 - .changed()
410 - {
411 - state.settings.create_loose_files = style;
470 + state.settings.create_loose_files = storage == "reference";
412 471 }
413 472 if state.settings.create_loose_files {
414 473 ui.label(
@@ -862,7 +862,17 @@
862 862
863 863 visuals.panel_fill = t.surface_overlay;
864 864 visuals.window_fill = t.surface_overlay;
865 - visuals.extreme_bg_color = t.surface_page;
865 + // egui's fill for the things you look *into*, text edits above all, so it
866 + // is the well and not the page. It said page until makeover 2.3.0 derived
867 + // `surface-well`, which is the same substitution `palette()` documents
868 + // having outgrown: the well was the page because there was no well token,
869 + // and this is the last place that stayed behind after there was one.
870 + //
871 + // Load-bearing now rather than cosmetic. `makeover_immediate::field` fills
872 + // its well from `palette().well`, so leaving this on the page would put a
873 + // described field and a hand-built one at two different colours, which is
874 + // exactly the inconsistency `widgets::text_field` was written to prevent.
875 + visuals.extreme_bg_color = t.surface_well;
866 876 visuals.faint_bg_color = lerp_color(t.surface_page, t.surface_overlay, 0.3);
867 877
868 878 visuals.selection.bg_fill = lerp_color(t.surface_page, t.action, 0.3);
@@ -408,6 +408,49 @@
408 408 )
409 409 }
410 410
411 + // --- Form fields -------------------------------------------------------------
412 +
413 + /// The geometry a described form field is drawn with.
414 + ///
415 + /// The well is [`inset_well`]'s frame at field size: same radius, same
416 + /// one-point margin holding the text off the bevel, same stroke. That sameness
417 + /// is the point, and it is the rule [`text_field`] was written to keep — a
418 + /// field is a well at a different size, so it looks like one.
419 + ///
420 + /// The two gaps are named relationships, not numbers. `gap` separates a label
421 + /// from its own control, which is `space::bound` by definition; `group_gap`
422 + /// separates one field from the next, which is `space::peer`.
423 + fn field_style() -> makeover_immediate::FieldStyle {
424 + makeover_immediate::FieldStyle {
425 + frame: makeover_immediate::FrameStyle {
426 + radius: theme::radius_container(),
427 + margin: egui::Margin::same(theme::stroke::DEFAULT as i8),
428 + stroke: theme::stroke::DEFAULT,
429 + },
430 + gap: theme::space::bound(),
431 + group_gap: theme::space::peer(),
432 + required_marker: "*",
433 + }
434 + }
435 +
436 + /// One described form field, drawn with this app's palette and geometry.
437 + ///
438 + /// The thin wrapper [`raised_frame`] and [`inset_well`] are, for the same
439 + /// reason: `makeover-immediate` takes the palette and the geometry as values,
440 + /// and every call site would otherwise repeat both.
441 + ///
442 + /// What it deliberately does not do is hold the value. That stays with the
443 + /// caller and arrives borrowed as a `Filling`, which is the division the
444 + /// description draws: the question is described, the answer is not.
445 + pub fn field(
446 + ui: &mut egui::Ui,
447 + field: &makeover_layout::Field<'_>,
448 + filling: makeover_immediate::Filling<'_>,
449 + state: Option<makeover_layout::State>,
450 + ) -> Option<egui::Response> {
451 + makeover_immediate::field(ui, field, filling, state, &theme::palette(), &field_style())
452 + }
453 +
411 454 /// Inline informational banner: raised card, body text in `content_secondary`.
412 455 /// Used for one-time tips and unobtrusive panel notices.
413 456 pub fn info_banner(ui: &mut egui::Ui, body: &str) {