Skip to main content

max / audiofiles

Describe the vault name and the edit result mode Two forms the earlier passes did not name. The vault-name input was the last bare text_edit_singleline in either crate, painting no well, and the Result radios decide whether the original survives an edit, which is the question FieldKind::Radio exists for. The vault name loses its pre-filled "Library" for a placeholder saying the same thing: finalize_vault_setup already falls back to that name when the field is blank, so the ghost text is the true answer to what you get by typing nothing, without the select-all-and-delete first. EditResultMode grows as_value/from_value. The stored config value and the described Choice::value are the same string, and were two literals kept in step by hand; three tests hold the pairing. Not converted, with reasons: the forge device picker (a chooser cannot say what to do while unanswered, makeover-layout 661ff8ba) and the instrument mode selector (an option offered but not yet available carries no reason, makeover-layout e761833e). 1347 tests pass, clippy clean, fmt clean.
Author: Max Johnson <me@maxj.phd> · 2026-08-16 18:42 UTC
Signed with PGP, not checked
Commit: 990e929a9a0c5c10a242ff68beb992da06425900
Parent: 55bc1ab
5 files changed, +147 insertions, -40 deletions
@@ -379,7 +379,9 @@
379 379 audio_retry_at: None,
380 380 vault_registry,
381 381 vault_setup_path: None,
382 - vault_setup_name: "Library".to_string(),
382 + // Empty, so the setup screen's ghost text is what says the name
383 + // falls back to "Library". See `draw_vault_setup_screen`.
384 + vault_setup_name: String::new(),
383 385 machine_id,
384 386 license_key_input: String::new(),
385 387 activation_result: Arc::new(Mutex::new(None)),
@@ -16,11 +16,6 @@
16 16 let default_path = vault::default_vault_path();
17 17 let existing_db = default_path.join("audiofiles.db").exists();
18 18
19 - // Pre-fill the vault-name field on first entry so the fallback is visible.
20 - if self.vault_setup_name.is_empty() {
21 - self.vault_setup_name = DEFAULT_VAULT_NAME.to_string();
22 - }
23 -
24 19 egui::CentralPanel::default().show(ui, |ui| {
25 20 let available = ui.available_size();
26 21 ui.add_space((available.y * 0.20).max(40.0));
@@ -45,11 +40,34 @@
45 40 ui.add_space(theme::space::bound());
46 41 }
47 42
48 - // Vault name
49 - ui.horizontal(|ui| {
50 - ui.label("Name:");
51 - ui.text_edit_singleline(&mut self.vault_setup_name);
52 - });
43 + // The library's name, as a described field. The bare
44 + // `text_edit_singleline` it replaces was the last input
45 + // in either crate painting no well, the same state
46 + // `classifier.rs`'s rule name was in before it was
47 + // described.
48 + //
49 + // The name used to be pre-filled with "Library" so the
50 + // fallback was visible, which is what a placeholder
51 + // says without also committing the text: `finalize_vault_setup`
52 + // already falls back to the same name when the field is
53 + // left blank, so the ghost text is the true answer to
54 + // "what do I get if I type nothing" rather than a
55 + // default the user has to select and delete first.
56 + let name_field = makeover_layout::Field {
57 + hint: Some("Shown wherever you switch between libraries."),
58 + placeholder: Some(DEFAULT_VAULT_NAME),
59 + ..makeover_layout::Field::new(
60 + makeover_layout::FieldKind::Text,
61 + "vault_name",
62 + "Name",
63 + )
64 + };
65 + widgets::field(
66 + ui,
67 + &name_field,
68 + makeover_immediate::Filling::Text(&mut self.vault_setup_name),
69 + None,
70 + );
53 71
54 72 ui.add_space(theme::space::peer());
55 73
@@ -28,17 +28,13 @@
28 28 self.edit.fade_curve = audiofiles_core::edit::FadeCurve::Linear;
29 29
30 30 // Cache result mode from user_config
31 - self.edit.result_mode = match self
31 + self.edit.result_mode = self
32 32 .backend
33 33 .get_config(crate::backend::ConfigKey::EditResultMode)
34 34 .ok()
35 35 .flatten()
36 36 .as_deref()
37 - {
38 - Some("replace") => Some(super::EditResultMode::Replace),
39 - Some("sibling") => Some(super::EditResultMode::Sibling),
40 - _ => None,
41 - };
37 + .and_then(super::EditResultMode::from_value);
42 38 }
43 39
44 40 /// Close the floating sample editor.
@@ -286,14 +282,10 @@
286 282
287 283 /// Save the user's preferred edit result mode.
288 284 pub fn set_edit_result_mode(&mut self, mode: super::EditResultMode) {
289 - let mode_str = match mode {
290 - super::EditResultMode::Replace => "replace",
291 - super::EditResultMode::Sibling => "sibling",
292 - };
293 285 super::log_backend_err(
294 286 "set_config edit_result_mode",
295 287 self.backend
296 - .set_config(crate::backend::ConfigKey::EditResultMode, mode_str),
288 + .set_config(crate::backend::ConfigKey::EditResultMode, mode.as_value()),
297 289 );
298 290 self.edit.result_mode = Some(mode);
299 291 }
@@ -19,6 +19,35 @@
19 19 Sibling,
20 20 }
21 21
22 + impl EditResultMode {
23 + /// The name this mode is stored and described under.
24 + ///
25 + /// One name, three uses: the value written to `ConfigKey::EditResultMode`,
26 + /// the value read back from it, and the `Choice::value` the edit panel's
27 + /// described field offers. The first two were separate string literals in
28 + /// `edit_workflow`, and describing the field would have made a third copy
29 + /// — one whose disagreement with the other two is not a compile error but a
30 + /// mode that round-trips to nothing on the next open.
31 + #[must_use]
32 + pub const fn as_value(self) -> &'static str {
33 + match self {
34 + Self::Replace => "replace",
35 + Self::Sibling => "sibling",
36 + }
37 + }
38 +
39 + /// [`as_value`](Self::as_value) the other way. `None` for anything else,
40 + /// which is the state "the user has not chosen yet" is already stored as.
41 + #[must_use]
42 + pub fn from_value(value: &str) -> Option<Self> {
43 + match value {
44 + "replace" => Some(Self::Replace),
45 + "sibling" => Some(Self::Sibling),
46 + _ => None,
47 + }
48 + }
49 + }
50 +
22 51 /// Data from a completed edit, pending user choice (replace vs sibling).
23 52 pub struct PendingEditResult {
24 53 pub source_hash: String,
@@ -599,27 +599,53 @@
599 599 });
600 600 }
601 601
602 + /// The two answers to "Result", as the described field offers them.
603 + ///
604 + /// The values are [`EditResultMode::as_value`], which is also what the mode is
605 + /// stored under, so the described answer and the persisted one cannot drift
606 + /// apart.
607 + const RESULT_MODES: [makeover_layout::Choice<'static>; 2] = [
608 + makeover_layout::Choice {
609 + value: EditResultMode::Replace.as_value(),
610 + label: "Replace original",
611 + },
612 + makeover_layout::Choice {
613 + value: EditResultMode::Sibling.as_value(),
614 + label: "Create sibling",
615 + },
616 + ];
617 +
602 618 /// Result mode section: replace original vs create sibling.
603 619 fn draw_result_section(ui: &mut egui::Ui, state: &mut BrowserState) {
604 - ui.label(egui::RichText::new("Result").strong());
620 + // A `Radio` rather than a `Select` for the reason the kind exists: this
621 + // decides whether the original survives the edit, so both answers have to
622 + // be readable without opening anything. The label the section used to draw
623 + // itself is the field's now.
624 + let result_field = makeover_layout::Field::radio("edit_result", "Result", &RESULT_MODES);
605 625
606 - let mut mode = state.edit.result_mode;
607 - ui.horizontal(|ui| {
608 - if ui
609 - .radio_value(&mut mode, Some(EditResultMode::Replace), "Replace original")
610 - .changed()
611 - && let Some(m) = mode
612 - {
613 - state.set_edit_result_mode(m);
614 - }
615 - if ui
616 - .radio_value(&mut mode, Some(EditResultMode::Sibling), "Create sibling")
617 - .changed()
618 - && let Some(m) = mode
619 - {
620 - state.set_edit_result_mode(m);
621 - }
622 - });
626 + // No mode chosen yet is a value no option carries, which leaves both
627 + // buttons unfilled — the same thing the `Option<EditResultMode>` radios
628 + // showed, and what the prompt after the first edit is there to resolve.
629 + let mut value = state
630 + .edit
631 + .result_mode
632 + .map(EditResultMode::as_value)
633 + .unwrap_or_default()
634 + .to_string();
635 + widgets::field(
636 + ui,
637 + &result_field,
638 + makeover_immediate::Filling::Text(&mut value),
639 + None,
640 + );
641 + // Written back on a real change only: `set_edit_result_mode` reaches the
642 + // config store, so re-deriving it every frame the way the import strategy
643 + // does would write on every frame.
644 + if let Some(picked) = EditResultMode::from_value(&value)
645 + && Some(picked) != state.edit.result_mode
646 + {
647 + state.set_edit_result_mode(picked);
648 + }
623 649 // Replace mode advisory. The previous "no in-app undo" copy was retired
624 650 // when the inline Undo affordance landed (C-1 part 2). The hint still
625 651 // points the user at Create sibling as the non-destructive default for
@@ -698,3 +724,43 @@
698 724 });
699 725 });
700 726 }
727 +
728 + #[cfg(test)]
729 + mod tests {
730 + use super::*;
731 +
732 + /// The pairing the described field rests on: what the radio submits is what
733 + /// `ConfigKey::EditResultMode` stores, so a mode picked in one session is
734 + /// the mode selected on the next open. Asserted rather than assumed
735 + /// because the pairing used to be two string literals kept in step by
736 + /// hand.
737 + #[test]
738 + fn every_offered_answer_is_a_stored_mode() {
739 + for choice in &RESULT_MODES {
740 + let mode = EditResultMode::from_value(choice.value)
741 + .expect("an offered value the app cannot read back is a value it will drop");
742 + assert_eq!(mode.as_value(), choice.value);
743 + }
744 + }
745 +
746 + /// Both modes are offered. A mode the field does not name is unreachable
747 + /// from the panel, whatever the config store holds.
748 + #[test]
749 + fn both_modes_are_offered() {
750 + for mode in [EditResultMode::Replace, EditResultMode::Sibling] {
751 + assert!(
752 + RESULT_MODES.iter().any(|c| c.value == mode.as_value()),
753 + "{} is not offered by the Result field",
754 + mode.as_value()
755 + );
756 + }
757 + }
758 +
759 + /// No mode chosen yet is a value no option carries, which is what leaves
760 + /// both buttons unfilled instead of silently answering the question.
761 + #[test]
762 + fn the_unchosen_state_matches_no_option() {
763 + assert!(EditResultMode::from_value("").is_none());
764 + assert!(!RESULT_MODES.iter().any(|c| c.value.is_empty()));
765 + }
766 + }