Skip to main content

max / audiofiles

theme: audiofiles accessors purely intent-specified Renamed the theme accessors (and ThemeColors fields) from the color/position vocabulary to intents across all crates: bg_* -> surface_*, text_* -> content*, accent_red/yellow/green/blue -> danger/warning/success/action (AF uses these semantically — errors, stale warnings, success/contrast states, primary emphasis), accent_purple/cyan -> category_five/six (decorative dots), border_default -> border. parse_theme now reads the semantic intents for the status/action accents (was reading the category ramp). Validates the intent set on the egui medium: it's sufficient with zero additions. Waveform peaks use content on surface-page; classification/piano/ spacing stay correctly app-local. Findings in _private docs.
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-13 02:17 UTC
Signed with PGP, not checked
Commit: 8bce2dbb222c233c9cef89d91dadbcacaf06d6c9
Parent: 1ec02c9
24 files changed, +428 insertions, -428 deletions
@@ -76,7 +76,7 @@
76 76 ui.label(
77 77 egui::RichText::new("Activate a license below to continue.")
78 78 .small()
79 - .color(theme::text_secondary()),
79 + .color(theme::content_secondary()),
80 80 );
81 81 }
82 82
@@ -87,7 +87,7 @@
87 87 // License key entry
88 88 ui.label(
89 89 egui::RichText::new("Already have a license?")
90 - .color(theme::text_secondary()),
90 + .color(theme::content_secondary()),
91 91 );
92 92 ui.add_space(theme::space::SM);
93 93
@@ -127,7 +127,7 @@
127 127
128 128 if let Some(err) = self.activation_error.clone() {
129 129 ui.add_space(theme::space::MD);
130 - ui.colored_label(theme::accent_red(), err.to_string());
130 + ui.colored_label(theme::danger(), err.to_string());
131 131 ui.add_space(theme::space::SM);
132 132 // Per-class recovery affordance.
133 133 match err {
@@ -739,7 +739,7 @@
739 739 painter.rect_stroke(
740 740 rect,
741 741 4.0,
742 - egui::Stroke::new(2.0, theme::accent_blue()),
742 + egui::Stroke::new(2.0, theme::action()),
743 743 egui::StrokeKind::Inside,
744 744 );
745 745 let label = if hovered_count == 1 {
@@ -750,13 +750,13 @@
750 750 let label_pos = egui::pos2(rect.center().x, rect.top() + 32.0);
751 751 // Background pill keeps the label readable on any theme.
752 752 let bg_rect = egui::Rect::from_center_size(label_pos, egui::vec2(280.0, 36.0));
753 - painter.rect_filled(bg_rect, 8.0, theme::bg_tertiary());
753 + painter.rect_filled(bg_rect, 8.0, theme::surface_sunken());
754 754 painter.text(
755 755 label_pos,
756 756 egui::Align2::CENTER_CENTER,
757 757 &label,
758 758 egui::FontId::proportional(18.0),
759 - theme::accent_blue(),
759 + theme::action(),
760 760 );
761 761 }
762 762 } else {
@@ -781,14 +781,14 @@
781 781 ui.label(
782 782 egui::RichText::new(err)
783 783 .small()
784 - .color(audiofiles_browser::ui::theme::text_muted()),
784 + .color(audiofiles_browser::ui::theme::content_muted()),
785 785 );
786 786 }
787 787 ui.add_space(16.0);
788 788 ui.label(
789 789 egui::RichText::new(format!("Vault location: {}", self.data_dir.display()))
790 790 .small()
791 - .color(audiofiles_browser::ui::theme::text_muted()),
791 + .color(audiofiles_browser::ui::theme::content_muted()),
792 792 );
793 793 ui.add_space(16.0);
794 794
@@ -867,7 +867,7 @@
867 867 architecture. License activation is always user-initiated.",
868 868 )
869 869 .small()
870 - .color(audiofiles_browser::ui::theme::text_muted()),
870 + .color(audiofiles_browser::ui::theme::content_muted()),
871 871 );
872 872 ui.add_space(8.0);
873 873 ui.label(
@@ -876,7 +876,7 @@
876 876 self.config_dir.join("preferences.json").display()
877 877 ))
878 878 .small()
879 - .color(audiofiles_browser::ui::theme::text_muted()),
879 + .color(audiofiles_browser::ui::theme::content_muted()),
880 880 );
881 881 ui.add_space(12.0);
882 882 ui.vertical_centered(|ui| {
@@ -38,7 +38,7 @@
38 38 "Your existing library at {} will be opened.",
39 39 default_path.display()
40 40 ))
41 - .color(theme::text_secondary()),
41 + .color(theme::content_secondary()),
42 42 );
43 43 ui.add_space(theme::space::SECTION);
44 44 } else {
@@ -68,7 +68,7 @@
68 68 widgets::accent_strong(chosen_path.display().to_string())
69 69 } else {
70 70 egui::RichText::new(chosen_path.display().to_string())
71 - .color(theme::text_secondary())
71 + .color(theme::content_secondary())
72 72 };
73 73 ui.label(path_text);
74 74 });
@@ -24,7 +24,7 @@
24 24 let painter = ui.painter_at(rect);
25 25
26 26 // Dark background
27 - painter.rect_filled(rect, 4.0, theme::bg_primary());
27 + painter.rect_filled(rect, 4.0, theme::surface_page());
28 28
29 29 // Center line (zero crossing)
30 30 let center_y = rect.center().y;
@@ -33,7 +33,7 @@
33 33 egui::pos2(rect.left(), center_y),
34 34 egui::pos2(rect.right(), center_y),
35 35 ],
36 - egui::Stroke::new(0.5, theme::text_muted()),
36 + egui::Stroke::new(0.5, theme::content_muted()),
37 37 );
38 38
39 39 // Draw waveform peaks
@@ -56,7 +56,7 @@
56 56
57 57 painter.line_segment(
58 58 [egui::pos2(x, y_min), egui::pos2(x, y_max)],
59 - egui::Stroke::new(1.0, theme::accent_green()),
59 + egui::Stroke::new(1.0, theme::success()),
60 60 );
61 61 }
62 62 }
@@ -66,7 +66,7 @@
66 66 let x = rect.left() + pos.clamp(0.0, 1.0) * rect.width();
67 67 painter.line_segment(
68 68 [egui::pos2(x, rect.top()), egui::pos2(x, rect.bottom())],
69 - egui::Stroke::new(1.5, theme::text_primary()),
69 + egui::Stroke::new(1.5, theme::content()),
70 70 );
71 71 }
72 72
@@ -53,7 +53,7 @@
53 53 };
54 54
55 55 let resp = waveform::draw_waveform(ui, waveform_data, playback_pos, 120.0);
56 - // Hover indicator: paint a vertical accent_blue line at the cursor X
56 + // Hover indicator: paint a vertical action line at the cursor X
57 57 // and a time label above it so the user can see where a click-to-seek
58 58 // would land before committing.
59 59 if resp.hovered()
@@ -67,7 +67,7 @@
67 67 egui::pos2(pos.x, rect.top()),
68 68 egui::pos2(pos.x, rect.bottom()),
69 69 ],
70 - egui::Stroke::new(1.0, theme::accent_blue()),
70 + egui::Stroke::new(1.0, theme::action()),
71 71 );
72 72 let label = format!(
73 73 "{:.0}:{:02.0}",
@@ -79,7 +79,7 @@
79 79 egui::Align2::CENTER_BOTTOM,
80 80 label,
81 81 egui::FontId::proportional(10.0),
82 - theme::text_secondary(),
82 + theme::content_secondary(),
83 83 );
84 84 }
85 85 // Click-to-seek: map the click's X position to a 0.0–1.0 fraction
@@ -120,56 +120,56 @@
120 120 .num_columns(2)
121 121 .spacing([8.0, theme::grid_row_spacing()])
122 122 .show(ui, |ui| {
123 - ui.label(egui::RichText::new("Duration").color(theme::text_secondary()));
123 + ui.label(egui::RichText::new("Duration").color(theme::content_secondary()));
124 124 ui.label(widgets::format_duration(analysis.duration));
125 125 ui.end_row();
126 126
127 127 if let Some(bpm) = analysis.bpm {
128 - ui.label(egui::RichText::new("BPM").color(theme::text_secondary()));
128 + ui.label(egui::RichText::new("BPM").color(theme::content_secondary()));
129 129 ui.label(widgets::format_bpm(bpm));
130 130 ui.end_row();
131 131 }
132 132
133 133 if let Some(ref key) = analysis.musical_key {
134 - ui.label(egui::RichText::new("Key").color(theme::text_secondary()));
134 + ui.label(egui::RichText::new("Key").color(theme::content_secondary()));
135 135 ui.label(key);
136 136 ui.end_row();
137 137 }
138 138
139 139 if let Some(ref class) = analysis.classification {
140 - ui.label(egui::RichText::new("Class").color(theme::text_secondary()));
140 + ui.label(egui::RichText::new("Class").color(theme::content_secondary()));
141 141 widgets::classification_badge(ui, class.as_str());
142 142 ui.end_row();
143 143 }
144 144
145 - ui.label(egui::RichText::new("Sample Rate").color(theme::text_secondary()));
145 + ui.label(egui::RichText::new("Sample Rate").color(theme::content_secondary()));
146 146 ui.label(format!("{} Hz", analysis.sample_rate));
147 147 ui.end_row();
148 148
149 - ui.label(egui::RichText::new("Channels").color(theme::text_secondary()));
149 + ui.label(egui::RichText::new("Channels").color(theme::content_secondary()));
150 150 ui.label(format!("{}", analysis.channels));
151 151 ui.end_row();
152 152
153 153 if let Some(peak) = analysis.peak_db {
154 - ui.label(egui::RichText::new("Peak").color(theme::text_secondary()));
154 + ui.label(egui::RichText::new("Peak").color(theme::content_secondary()));
155 155 ui.label(format!("{:.1} dB", peak));
156 156 ui.end_row();
157 157 }
158 158
159 159 if let Some(rms) = analysis.rms_db {
160 - ui.label(egui::RichText::new("RMS").color(theme::text_secondary()));
160 + ui.label(egui::RichText::new("RMS").color(theme::content_secondary()));
161 161 ui.label(format!("{:.1} dB", rms));
162 162 ui.end_row();
163 163 }
164 164
165 165 if let Some(lufs) = analysis.lufs {
166 - ui.label(egui::RichText::new("LUFS").color(theme::text_secondary()));
166 + ui.label(egui::RichText::new("LUFS").color(theme::content_secondary()));
167 167 ui.label(format!("{:.1}", lufs));
168 168 ui.end_row();
169 169 }
170 170
171 171 if let Some(is_loop) = analysis.is_loop {
172 - ui.label(egui::RichText::new("Loop").color(theme::text_secondary()));
172 + ui.label(egui::RichText::new("Loop").color(theme::content_secondary()));
173 173 ui.label(if is_loop { "Yes" } else { "No" });
174 174 ui.end_row();
175 175 }
@@ -184,7 +184,7 @@
184 184 .default_open(true)
185 185 .show(ui, |ui| {
186 186 if state.selected_tags.is_empty() {
187 - ui.label(egui::RichText::new("No tags").color(theme::text_muted()));
187 + ui.label(egui::RichText::new("No tags").color(theme::content_muted()));
188 188 } else {
189 189 ui.horizontal_wrapped(|ui| {
190 190 let tags = state.selected_tags.clone();
@@ -258,14 +258,14 @@
258 258 ui.label(
259 259 egui::RichText::new(format!("Suggest (from {class_str}):"))
260 260 .small()
261 - .color(theme::text_muted()),
261 + .color(theme::content_muted()),
262 262 );
263 263 for sug in &suggestions {
264 264 if ui
265 265 .small_button(
266 266 egui::RichText::new(format!("+{sug}"))
267 267 .small()
268 - .color(theme::accent_blue()),
268 + .color(theme::action()),
269 269 )
270 270 .on_hover_text(format!("Add tag: {sug}"))
271 271 .clicked()
@@ -289,9 +289,9 @@
289 289 let p3 = egui::pos2(icon_rect.min.x + pad, icon_rect.max.y - pad);
290 290 let p4 = egui::pos2(icon_rect.max.x - pad, icon_rect.min.y + pad);
291 291 let stroke_color = if icon_resp.hovered() {
292 - theme::text_secondary()
292 + theme::content_secondary()
293 293 } else {
294 - theme::text_muted()
294 + theme::content_muted()
295 295 };
296 296 let stroke = egui::Stroke::new(1.2, stroke_color);
297 297 let painter = ui.painter();
@@ -325,13 +325,13 @@
325 325 ui.label(
326 326 egui::RichText::new(format!("Muted \"{tag}\" for {class_str}."))
327 327 .small()
328 - .color(theme::text_muted()),
328 + .color(theme::content_muted()),
329 329 );
330 330 if ui
331 331 .link(
332 332 egui::RichText::new("Undo")
333 333 .small()
334 - .color(theme::accent_blue()),
334 + .color(theme::action()),
335 335 )
336 336 .clicked()
337 337 {
@@ -478,7 +478,7 @@
478 478 .spacing([8.0, theme::grid_row_spacing()])
479 479 .show(ui, |ui| {
480 480 let bpm = summarize(&samples, |n| n.bpm);
481 - ui.label(egui::RichText::new("BPM").color(theme::text_secondary()));
481 + ui.label(egui::RichText::new("BPM").color(theme::content_secondary()));
482 482 ui.label(match bpm {
483 483 Some(Ok(v)) => widgets::format_bpm(v),
484 484 Some(Err(())) => "varies".to_string(),
@@ -487,7 +487,7 @@
487 487 ui.end_row();
488 488
489 489 let key = summarize(&samples, |n| n.musical_key.clone());
490 - ui.label(egui::RichText::new("Key").color(theme::text_secondary()));
490 + ui.label(egui::RichText::new("Key").color(theme::content_secondary()));
491 491 ui.label(match key {
492 492 Some(Ok(v)) => v,
493 493 Some(Err(())) => "varies".to_string(),
@@ -496,7 +496,7 @@
496 496 ui.end_row();
497 497
498 498 let class = summarize(&samples, |n| n.classification.clone());
499 - ui.label(egui::RichText::new("Class").color(theme::text_secondary()));
499 + ui.label(egui::RichText::new("Class").color(theme::content_secondary()));
500 500 match class {
501 501 Some(Ok(v)) => widgets::classification_badge(ui, &v),
502 502 Some(Err(())) => {
@@ -509,7 +509,7 @@
509 509 ui.end_row();
510 510
511 511 let dur = summarize(&samples, |n| n.duration);
512 - ui.label(egui::RichText::new("Duration").color(theme::text_secondary()));
512 + ui.label(egui::RichText::new("Duration").color(theme::content_secondary()));
513 513 ui.label(match dur {
514 514 Some(Ok(v)) => widgets::format_duration(v),
515 515 Some(Err(())) => "varies".to_string(),
@@ -532,7 +532,7 @@
532 532 }
533 533 }
534 534 if tag_counts.is_empty() {
535 - ui.label(egui::RichText::new("No tags").color(theme::text_muted()));
535 + ui.label(egui::RichText::new("No tags").color(theme::content_muted()));
536 536 } else {
537 537 let mut entries: Vec<_> = tag_counts.into_iter().collect();
538 538 entries.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
@@ -569,7 +569,7 @@
569 569 .label(
570 570 egui::RichText::new(label)
571 571 .small()
572 - .color(theme::accent_blue()),
572 + .color(theme::action()),
573 573 )
574 574 .on_hover_text(hover);
575 575 resp.context_menu(|ui| {
@@ -215,7 +215,7 @@
215 215 let width = if active { 2.5 } else { 1.0 };
216 216 ui.painter_at(rect).line_segment(
217 217 [egui::pos2(x, rect.top()), egui::pos2(x, rect.bottom())],
218 - egui::Stroke::new(width, theme::accent_yellow()),
218 + egui::Stroke::new(width, theme::warning()),
219 219 );
220 220 (new_frac, resp.dragged())
221 221 }
@@ -238,7 +238,7 @@
238 238 ui.label(egui::RichText::new(&name).strong().size(12.0));
239 239 ui.label(
240 240 egui::RichText::new(format!("{} Hz \u{00B7} {:.3}s{}", sr, duration, peak_suffix))
241 - .color(theme::text_muted())
241 + .color(theme::content_muted())
242 242 .size(11.0),
243 243 );
244 244 });
@@ -262,13 +262,13 @@
262 262 ui.horizontal(|ui| {
263 263 ui.label("Start:");
264 264 ui.add_enabled(!disabled, egui::Slider::new(&mut state.edit.trim_start, 0.0..=1.0).show_value(false));
265 - ui.label(egui::RichText::new(format!("{:.3}s", start_time)).color(theme::text_muted()));
265 + ui.label(egui::RichText::new(format!("{:.3}s", start_time)).color(theme::content_muted()));
266 266 });
267 267
268 268 ui.horizontal(|ui| {
269 269 ui.label("End:");
270 270 ui.add_enabled(!disabled, egui::Slider::new(&mut state.edit.trim_end, 0.0..=1.0).show_value(false));
271 - ui.label(egui::RichText::new(format!("{:.3}s", end_time)).color(theme::text_muted()));
271 + ui.label(egui::RichText::new(format!("{:.3}s", end_time)).color(theme::content_muted()));
272 272 });
273 273
274 274 // Clamp start < end
@@ -295,7 +295,7 @@
295 295 let predicted = peak + state.edit.gain_db;
296 296 if predicted > 0.0 {
297 297 ui.colored_label(
298 - theme::accent_red(),
298 + theme::danger(),
299 299 format!("Peak: {:.1} dB \u{2192} {:.1} dB (clips!)", peak, predicted),
300 300 );
301 301 }
@@ -467,13 +467,13 @@
467 467 ui.label(
468 468 egui::RichText::new(format!("Batch \u{00B7} {} samples", selected_count))
469 469 .small()
470 - .color(theme::text_muted()),
470 + .color(theme::content_muted()),
471 471 );
472 472 });
473 473 ui.label(
474 474 egui::RichText::new("Apply to all selected samples at once")
475 475 .small()
476 - .color(theme::text_muted()),
476 + .color(theme::content_muted()),
477 477 );
478 478 ui.add_space(theme::space::SM);
479 479
@@ -580,7 +580,7 @@
580 580 ui.label(
581 581 egui::RichText::new(format!("Last edit: {op_label}"))
582 582 .small()
583 - .color(theme::text_muted()),
583 + .color(theme::content_muted()),
584 584 );
585 585 if ui.small_button("Undo").clicked() {
586 586 state.undo_last_edit();
@@ -109,7 +109,7 @@
109 109 safe_secs / 60.0,
110 110 ))
111 111 .small()
112 - .color(theme::accent_yellow()),
112 + .color(theme::warning()),
113 113 );
114 114 }
115 115 }
@@ -144,7 +144,7 @@
144 144 )
145 145 };
146 146 ui.label(
147 - egui::RichText::new(msg).small().color(theme::accent_red()),
147 + egui::RichText::new(msg).small().color(theme::danger()),
148 148 );
149 149 }
150 150 }
@@ -169,7 +169,7 @@
169 169 estimated_bytes as f64 / 1_073_741_824.0,
170 170 ))
171 171 .small()
172 - .color(theme::accent_yellow()),
172 + .color(theme::warning()),
173 173 );
174 174 }
175 175 }
@@ -201,7 +201,7 @@
201 201 ui.label(
202 202 egui::RichText::new(format!("\u{00B7} {} device profiles available", profile_count))
203 203 .small()
204 - .color(theme::text_muted()),
204 + .color(theme::content_muted()),
205 205 );
206 206 }
207 207 });
@@ -262,27 +262,27 @@
262 262 ui.label(
263 263 egui::RichText::new(format!("by {}", profile.manufacturer))
264 264 .small()
265 - .color(theme::text_muted()),
265 + .color(theme::content_muted()),
266 266 );
267 267 if let Some(ref summary) = profile.format_summary {
268 268 ui.label(
269 269 egui::RichText::new(summary)
270 270 .small()
271 - .color(theme::text_muted()),
271 + .color(theme::content_muted()),
272 272 );
273 273 }
274 274 if let Some(ref category) = profile.category {
275 275 ui.label(
276 276 egui::RichText::new(category)
277 277 .small()
278 - .color(theme::text_muted()),
278 + .color(theme::content_muted()),
279 279 );
280 280 }
281 281 if let Some(ref notes) = profile.notes {
282 282 ui.label(
283 283 egui::RichText::new(notes)
284 284 .small()
285 - .color(theme::text_muted()),
285 + .color(theme::content_muted()),
286 286 );
287 287 }
288 288 }
@@ -329,7 +329,7 @@
329 329 Choose Original to preserve them.",
330 330 )
331 331 .small()
332 - .color(theme::accent_yellow()),
332 + .color(theme::warning()),
333 333 );
334 334 }
335 335 }
@@ -438,7 +438,7 @@
438 438 ui.label(
439 439 egui::RichText::new("Tokens:")
440 440 .small()
441 - .color(theme::text_muted()),
441 + .color(theme::content_muted()),
442 442 );
443 443 const TOKENS: &[&str] = &[
444 444 "{name}", "{bpm}", "{key}", "{class}", "{duration}",
@@ -489,7 +489,7 @@
489 489 ui.label(
490 490 egui::RichText::new(format!("Preview: {preview}"))
491 491 .small()
492 - .color(theme::text_muted()),
492 + .color(theme::content_muted()),
493 493 );
494 494 }
495 495 }
@@ -497,7 +497,7 @@
497 497 ui.label(
498 498 egui::RichText::new(format!("Pattern: {e}"))
499 499 .small()
500 - .color(theme::accent_yellow()),
500 + .color(theme::warning()),
501 501 );
502 502 }
503 503 }
@@ -559,7 +559,7 @@
559 559 ui.label(
560 560 egui::RichText::new(format!("Exporting: {current_name}"))
561 561 .small()
562 - .color(theme::text_muted()),
562 + .color(theme::content_muted()),
563 563 );
564 564 }
565 565
@@ -594,19 +594,19 @@
594 594 .max_height(200.0)
595 595 .show(ui, |ui| {
596 596 for (name, err) in errors {
597 - // m-8: name in accent_red + body in text_secondary
597 + // m-8: name in danger + body in content_secondary
598 598 // so errors don't blend with hint text. Mirrors the
599 599 // two-label layout in progress.rs / summary.rs.
600 600 ui.horizontal(|ui| {
601 601 ui.label(
602 602 egui::RichText::new(name)
603 603 .small()
604 - .color(theme::accent_red()),
604 + .color(theme::danger()),
605 605 );
606 606 ui.label(
607 607 egui::RichText::new(err)
608 608 .small()
609 - .color(theme::text_secondary()),
609 + .color(theme::content_secondary()),
610 610 );
611 611 });
612 612 }
@@ -47,12 +47,12 @@
47 47 ui.label(
48 48 egui::RichText::new("Welcome to audiofiles")
49 49 .size(22.0)
50 - .color(theme::text_primary()),
50 + .color(theme::content()),
51 51 );
52 52 ui.add_space(theme::space::SECTION);
53 53 ui.label(
54 54 egui::RichText::new("Three steps to your first sample:")
55 - .color(theme::text_secondary()),
55 + .color(theme::content_secondary()),
56 56 );
57 57 ui.add_space(theme::space::LG);
58 58
@@ -84,17 +84,17 @@
84 84 ui.label(
85 85 egui::RichText::new("Your files stay where they are \u{2014} audiofiles only indexes them.")
86 86 .small()
87 - .color(theme::text_muted()),
87 + .color(theme::content_muted()),
88 88 );
89 89 ui.add_space(theme::space::MD);
90 90 ui.label(
91 91 egui::RichText::new("Press F1 for shortcuts \u{00B7} Right-click samples for options")
92 92 .small()
93 - .color(theme::text_muted()),
93 + .color(theme::content_muted()),
94 94 );
95 95 ui.add_space(theme::space::SM);
96 96 if ui
97 - .link(egui::RichText::new("Dismiss").small().color(theme::text_muted()))
97 + .link(egui::RichText::new("Dismiss").small().color(theme::content_muted()))
98 98 .on_hover_text("Hide this welcome. Re-enable from Settings.")
99 99 .clicked()
100 100 {
@@ -121,7 +121,7 @@
121 121 // Quiet link to bring the welcome screen back if the user dismissed it.
122 122 ui.vertical_centered(|ui| {
123 123 ui.add_space(theme::space::LG);
124 - if ui.link(egui::RichText::new("Show welcome").small().color(theme::text_muted())).clicked() {
124 + if ui.link(egui::RichText::new("Show welcome").small().color(theme::content_muted())).clicked() {
125 125 state.show_welcome();
126 126 }
127 127 });
@@ -271,16 +271,16 @@
271 271 }
272 272 if show_peak_db {
273 273 header.col(|ui| {
274 - ui.label(egui::RichText::new("Peak").color(theme::text_secondary()));
274 + ui.label(egui::RichText::new("Peak").color(theme::content_secondary()));
275 275 });
276 276 }
277 277 if show_tags {
278 278 header.col(|ui| {
279 - ui.label(egui::RichText::new("Tags").color(theme::text_secondary()));
279 + ui.label(egui::RichText::new("Tags").color(theme::content_secondary()));
280 280 });
281 281 }
282 282 header.col(|ui| {
283 - ui.label(egui::RichText::new("Play").color(theme::text_muted()));
283 + ui.label(egui::RichText::new("Play").color(theme::content_muted()));
284 284 });
285 285 })
286 286 .body(|mut body| {
@@ -296,7 +296,7 @@
296 296 let resp = ui.selectable_label(
297 297 selected,
298 298 egui::RichText::new(" Up")
299 - .color(theme::text_secondary()),
299 + .color(theme::content_secondary()),
300 300 );
301 301 if resp.clicked() {
302 302 handle_click(state, 0, ui);
@@ -444,7 +444,7 @@
444 444 ) {
445 445 // Directories get a trailing "/" (Unix convention). Samples get no prefix
446 446 // — the name is the data, no decorative noise. Cloud-only samples already
447 - // render in `theme::text_muted()` below, which carries the signal without
447 + // render in `theme::content_muted()` below, which carries the signal without
448 448 // emoji glyphs (brand rule).
449 449 let label = match node.node.node_type {
450 450 NodeType::Directory => format!("{}/", node.node.name),
@@ -453,7 +453,7 @@
453 453 let resp = if node.cloud_only {
454 454 ui.selectable_label(
455 455 selected,
456 - egui::RichText::new(&label).color(theme::text_muted()),
456 + egui::RichText::new(&label).color(theme::content_muted()),
457 457 )
458 458 } else {
459 459 ui.selectable_label(selected, &label)
@@ -576,7 +576,7 @@
576 576 if let Some(dur) = node.duration {
577 577 ui.label(
578 578 egui::RichText::new(widgets::format_duration(dur))
579 - .color(theme::text_secondary()),
579 + .color(theme::content_secondary()),
580 580 );
581 581 }
582 582 });
@@ -597,7 +597,7 @@
597 597 if let Some(bpm) = node.bpm {
598 598 ui.label(
599 599 egui::RichText::new(widgets::format_bpm(bpm))
600 - .color(theme::text_secondary()),
600 + .color(theme::content_secondary()),
601 601 );
602 602 }
603 603 });
@@ -609,7 +609,7 @@
609 609 if let Some(ref key) = node.musical_key {
610 610 ui.label(
611 611 egui::RichText::new(key.as_str())
612 - .color(theme::text_secondary()),
612 + .color(theme::content_secondary()),
613 613 );
614 614 }
615 615 });
@@ -621,7 +621,7 @@
621 621 if let Some(peak) = node.peak_db {
622 622 ui.label(
623 623 egui::RichText::new(format!("{:.1}", peak))
624 - .color(theme::text_secondary()),
624 + .color(theme::content_secondary()),
625 625 );
626 626 }
627 627 });
@@ -635,7 +635,7 @@
635 635 ui.label(
636 636 egui::RichText::new(tag_str)
637 637 .small()
638 - .color(theme::text_secondary()),
638 + .color(theme::content_secondary()),
639 639 );
640 640 }
641 641 });
@@ -673,7 +673,7 @@
673 673 // surfaces — otherwise the user clicks an inert header and gets silence.
674 674 ui.add_enabled(
675 675 false,
676 - egui::Label::new(egui::RichText::new(text).color(theme::text_muted()))
676 + egui::Label::new(egui::RichText::new(text).color(theme::content_muted()))
677 677 .sense(egui::Sense::click()),
678 678 )
679 679 .on_disabled_hover_text(
@@ -26,7 +26,7 @@
26 26 if node.cloud_only {
27 27 ui.label(
28 28 egui::RichText::new("Cloud-only sample")
29 - .color(theme::text_muted())
29 + .color(theme::content_muted())
30 30 .italics(),
31 31 );
32 32 // Targeted download for the row under the cursor. Falls back
@@ -22,7 +22,7 @@
22 22 let mut clicked = false;
23 23 ui.horizontal(|ui| {
24 24 if ui
25 - .small_button(egui::RichText::new("clear").color(theme::text_muted()))
25 + .small_button(egui::RichText::new("clear").color(theme::content_muted()))
26 26 .on_hover_text("Clear this filter category")
27 27 .clicked()
28 28 {
@@ -147,7 +147,7 @@
147 147 ("Other", &["misc", "noise"]),
148 148 ];
149 149 for (label, classes) in groups {
150 - ui.label(egui::RichText::new(*label).small().color(theme::text_muted()));
150 + ui.label(egui::RichText::new(*label).small().color(theme::content_muted()));
151 151 ui.horizontal_wrapped(|ui| {
152 152 for class in *classes {
153 153 let active = state.search_filter.classifications.contains(&class.to_string());
@@ -201,7 +201,7 @@
201 201 ]),
202 202 ];
203 203 for (label, keys) in key_groups {
204 - ui.label(egui::RichText::new(*label).small().color(theme::text_muted()));
204 + ui.label(egui::RichText::new(*label).small().color(theme::content_muted()));
205 205 ui.horizontal_wrapped(|ui| {
206 206 for key in *keys {
207 207 let note = key.split(' ').next().unwrap_or(key);
@@ -256,7 +256,7 @@
256 256 let tags = state.search_filter.required_tags.clone();
257 257 for tag in &tags {
258 258 ui.horizontal(|ui| {
259 - ui.label(egui::RichText::new(tag).color(theme::accent_blue()));
259 + ui.label(egui::RichText::new(tag).color(theme::action()));
260 260 if ui.small_button("x").on_hover_text("Remove tag filter").clicked() {
261 261 state.search_filter.required_tags.retain(|t| t != tag);
262 262 changed = true;
@@ -292,14 +292,14 @@
292 292 if file_count == 1 { "" } else { "s" }
293 293 ))
294 294 .small()
295 - .color(theme::text_muted()),
295 + .color(theme::content_muted()),
296 296 );
297 297
298 298 if state.search_filter.is_active() {
299 299 ui.add_space(theme::space::MD);
300 300 ui.separator();
301 301 widgets::subsection_label(ui, "Save as Collection");
302 - ui.label(egui::RichText::new("Save current filters as a dynamic collection").small().color(theme::text_muted()));
302 + ui.label(egui::RichText::new("Save current filters as a dynamic collection").small().color(theme::content_muted()));
303 303 ui.add_space(theme::space::SM);
304 304 ui.horizontal(|ui| {
305 305 let auto_name = state.search_filter.describe();
@@ -8,13 +8,13 @@
8 8 use super::theme;
9 9 use super::widgets;
10 10
11 - /// Status message fade threshold — after this, status renders in `text_muted`.
11 + /// Status message fade threshold — after this, status renders in `content_muted`.
12 12 const STATUS_FADE_AFTER: Duration = Duration::from_secs(5);
13 13 /// Status message hide threshold — after this, the status disappears entirely.
14 14 const STATUS_HIDE_AFTER: Duration = Duration::from_secs(30);
15 15
16 16 /// Heuristic: does this status line report a failure? Error statuses are kept
17 - /// visible (never auto-hidden) and rendered in `accent_red`, since a silently
17 + /// visible (never auto-hidden) and rendered in `danger`, since a silently
18 18 /// expiring error is the one message the user most needs to catch.
19 19 fn is_error_status(s: &str) -> bool {
20 20 let l = s.to_ascii_lowercase();
@@ -33,7 +33,7 @@
33 33 ui.label(
34 34 egui::RichText::new("\u{00B7}")
35 35 .small()
36 - .color(theme::text_muted()),
36 + .color(theme::content_muted()),
37 37 );
38 38 }
39 39
@@ -89,7 +89,7 @@
89 89 }
90 90
91 91 ui.label(
92 - egui::RichText::new(format!("Playing: {name}")).color(theme::text_primary()),
92 + egui::RichText::new(format!("Playing: {name}")).color(theme::content()),
93 93 );
94 94
95 95 // Visual progress bar
@@ -100,12 +100,12 @@
100 100 egui::Sense::click(),
101 101 );
102 102 if ui.is_rect_visible(rect) {
103 - ui.painter().rect_filled(rect, 3.0, theme::bg_primary());
103 + ui.painter().rect_filled(rect, 3.0, theme::surface_page());
104 104 let fill_rect = egui::Rect::from_min_size(
105 105 rect.min,
106 106 egui::vec2(rect.width() * progress, rect.height()),
107 107 );
108 - ui.painter().rect_filled(fill_rect, 3.0, theme::accent_blue());
108 + ui.painter().rect_filled(fill_rect, 3.0, theme::action());
109 109 }
110 110
111 111 // Click-to-seek on progress bar
@@ -128,7 +128,7 @@
128 128 total_secs / 60.0,
129 129 total_secs % 60.0,
130 130 ))
131 - .color(theme::text_secondary())
131 + .color(theme::content_secondary())
132 132 .small(),
133 133 );
134 134 }
@@ -146,7 +146,7 @@
146 146 dot(ui);
147 147 ui.label(
148 148 egui::RichText::new(format!("{sel_count} selected"))
149 - .color(theme::text_secondary()),
149 + .color(theme::content_secondary()),
150 150 );
151 151 }
152 152
@@ -186,11 +186,11 @@
186 186 if is_error || elapsed < STATUS_HIDE_AFTER {
187 187 dot(ui);
188 188 let color = if is_error {
189 - theme::accent_red()
189 + theme::danger()
190 190 } else if elapsed >= STATUS_FADE_AFTER {
191 - theme::text_muted()
191 + theme::content_muted()
192 192 } else {
193 - theme::text_secondary()
193 + theme::content_secondary()
194 194 };
195 195 ui.label(egui::RichText::new(&state.status).color(color));
196 196
@@ -214,7 +214,7 @@
214 214 ui.label(
215 215 egui::RichText::new("Right-click for options \u{00B7} F1 for shortcuts")
216 216 .small()
217 - .color(theme::text_muted()),
217 + .color(theme::content_muted()),
218 218 );
219 219 if ui.small_button("Dismiss").on_hover_text("Dismiss").clicked() {
220 220 state.dismiss_first_launch_hint();
@@ -243,7 +243,7 @@
243 243 ui.label(
244 244 egui::RichText::new(format!("{sel_count} selected"))
245 245 .small()
246 - .color(theme::text_secondary()),
246 + .color(theme::content_secondary()),
247 247 );
248 248 dot(ui);
249 249 }
@@ -265,13 +265,13 @@
265 265 ui.label(
266 266 egui::RichText::new("\u{00B7}")
267 267 .small()
268 - .color(theme::text_muted()),
268 + .color(theme::content_muted()),
269 269 );
270 270 }
271 271 ui.label(
272 272 egui::RichText::new(tag)
273 273 .small()
274 - .color(theme::text_muted()),
274 + .color(theme::content_muted()),
275 275 );
276 276 }
277 277 });
@@ -305,13 +305,13 @@
305 305 ui.label(
306 306 egui::RichText::new(format!("{analyzed}/{total_samples} analyzed"))
307 307 .small()
308 - .color(theme::text_muted()),
308 + .color(theme::content_muted()),
309 309 );
310 310 } else {
311 311 ui.label(
312 312 egui::RichText::new(format!("{total_samples} analyzed"))
313 313 .small()
314 - .color(theme::accent_green()),
314 + .color(theme::success()),
315 315 );
316 316 }
317 317 // m-12: suppress untagged count until analysis has produced output.
@@ -319,7 +319,7 @@
319 319 ui.label(
320 320 egui::RichText::new(format!("\u{00B7} {untagged} untagged"))
321 321 .small()
322 - .color(theme::text_muted()),
322 + .color(theme::content_muted()),
323 323 );
324 324 }
325 325 }
@@ -337,7 +337,7 @@
337 337 ui.label(
338 338 egui::RichText::new(device_label)
339 339 .small()
340 - .color(theme::text_muted()),
340 + .color(theme::content_muted()),
341 341 )
342 342 .on_hover_text("Audio output device used for sample preview");
343 343 }
@@ -56,7 +56,7 @@
56 56 let x = rect.left() + rect.width() * frac.clamp(0.0, 1.0);
57 57 painter.line_segment(
58 58 [egui::pos2(x, rect.top()), egui::pos2(x, rect.bottom())],
59 - egui::Stroke::new(1.0, theme::accent_yellow()),
59 + egui::Stroke::new(1.0, theme::warning()),
60 60 );
61 61 }
62 62 }
@@ -69,7 +69,7 @@
69 69 ui.label(egui::RichText::new(name).strong().size(12.0));
70 70 ui.label(
71 71 egui::RichText::new(format!("{rate} Hz"))
72 - .color(theme::text_muted())
72 + .color(theme::content_muted())
73 73 .size(11.0),
74 74 );
75 75 });
@@ -187,7 +187,7 @@
187 187 ui.label(
188 188 egui::RichText::new("Slices are written into a new folder beside this sample.")
189 189 .small()
190 - .color(theme::text_muted()),
190 + .color(theme::content_muted()),
191 191 );
192 192 }
193 193
@@ -200,7 +200,7 @@
200 200 ui.label(
201 201 egui::RichText::new("No device profiles available.")
202 202 .small()
203 - .color(theme::text_muted()),
203 + .color(theme::content_muted()),
204 204 );
205 205 return;
206 206 }
@@ -239,7 +239,7 @@
239 239 ui.label(
240 240 egui::RichText::new("Resamples and converts bit depth to match the device, as a new sample.")
241 241 .small()
242 - .color(theme::text_muted()),
242 + .color(theme::content_muted()),
243 243 );
244 244 }
245 245
@@ -252,7 +252,7 @@
252 252 ui.label(
253 253 egui::RichText::new("Select 2+ samples to batch trim silence.")
254 254 .small()
255 - .color(theme::text_muted()),
255 + .color(theme::content_muted()),
256 256 );
257 257 return;
258 258 }
@@ -282,6 +282,6 @@
282 282 egui::RichText::new("Plugin processing (CLAP/VST) — coming soon")
283 283 .small()
284 284 .italics()
285 - .color(theme::text_muted()),
285 + .color(theme::content_muted()),
286 286 );
287 287 }
@@ -30,12 +30,12 @@
30 30 ui.add_space(theme::space::MD);
31 31 ui.label(
32 32 egui::RichText::new("No sample loaded")
33 - .color(theme::text_secondary()),
33 + .color(theme::content_secondary()),
34 34 );
35 35 ui.label(
36 36 egui::RichText::new("Right-click a sample \u{2192} \"Play as Instrument\", or drag samples onto the keyboard below")
37 37 .small()
38 - .color(theme::text_muted()),
38 + .color(theme::content_muted()),
39 39 );
40 40 ui.add_space(theme::space::MD);
41 41 });
@@ -87,7 +87,7 @@
87 87 ui.horizontal(|ui| {
88 88 ui.label(
89 89 egui::RichText::new("No MIDI inputs detected")
90 - .color(theme::text_muted()),
90 + .color(theme::content_muted()),
91 91 );
92 92 if ui.small_button("Refresh").clicked() {
93 93 state.midi_pending_action = Some(MidiAction::RefreshPorts);
@@ -97,7 +97,7 @@
97 97 }
98 98
99 99 ui.horizontal(|ui| {
100 - ui.label(egui::RichText::new("MIDI Input").color(theme::text_primary()));
100 + ui.label(egui::RichText::new("MIDI Input").color(theme::content()));
101 101
102 102 if ui.small_button("Refresh").clicked() {
103 103 state.midi_pending_action = Some(MidiAction::RefreshPorts);
@@ -148,13 +148,13 @@
148 148 ui.label(
149 149 egui::RichText::new(idle_text)
150 150 .small()
151 - .color(theme::text_muted()),
151 + .color(theme::content_muted()),
152 152 );
153 153 } else {
154 154 for note in state.midi_state.recent_notes.iter().rev().take(8) {
155 155 let age = now.duration_since(note.timestamp).as_secs_f32();
156 156 let alpha = ((2.0 - age) / 2.0).clamp(0.0, 1.0);
157 - let color = theme::text_primary().linear_multiply(alpha);
157 + let color = theme::content().linear_multiply(alpha);
158 158 ui.label(
159 159 egui::RichText::new(format!("{} v{}", note.note_name, note.velocity))
160 160 .small()
@@ -189,7 +189,7 @@
189 189
190 190 ui.label(
191 191 egui::RichText::new(format!("Root: {}", note_name(state.instrument_root_note)))
192 - .color(theme::text_secondary()),
192 + .color(theme::content_secondary()),
193 193 );
194 194
195 195 ui.separator();
@@ -205,7 +205,7 @@
205 205 ui.label(
206 206 egui::RichText::new("Multi-sample activates after you drop a second sample onto the keyboard.")
207 207 .small()
208 - .color(theme::text_muted()),
208 + .color(theme::content_muted()),
209 209 );
210 210 }
211 211 }
@@ -231,7 +231,7 @@
231 231 ui.label(
232 232 egui::RichText::new(format!("Oct {}", base_octave))
233 233 .small()
234 - .color(theme::text_secondary()),
234 + .color(theme::content_secondary()),
235 235 );
236 236 if ui
237 237 .button("+")
@@ -448,17 +448,17 @@
448 448 let is_root = note == state.instrument_root_note;
449 449
450 450 let fill = if is_active {
451 - theme::accent_blue()
451 + theme::action()
452 452 } else {
453 453 theme::piano_white_key()
454 454 };
455 455
456 456 painter.rect_filled(key_rect, 2.0, fill);
457 - painter.rect_stroke(key_rect, 2.0, egui::Stroke::new(1.0, theme::border_default()), egui::StrokeKind::Outside);
457 + painter.rect_stroke(key_rect, 2.0, egui::Stroke::new(1.0, theme::border()), egui::StrokeKind::Outside);
458 458
459 459 if is_root {
460 460 let dot_center = key_rect.center_bottom() - egui::vec2(0.0, 8.0);
461 - painter.circle_filled(dot_center, 3.0, theme::accent_purple());
461 + painter.circle_filled(dot_center, 3.0, theme::category_five());
462 462 }
463 463
464 464 white_x += key_width;
@@ -470,7 +470,7 @@
470 470 let is_root = black_note == state.instrument_root_note;
471 471
472 472 let fill = if is_active {
473 - theme::accent_blue()
473 + theme::action()
474 474 } else {
475 475 theme::piano_black_key()
476 476 };
@@ -479,7 +479,7 @@
479 479
480 480 if is_root {
481 481 let dot_center = key_rect.center_bottom() - egui::vec2(0.0, 5.0);
482 - painter.circle_filled(dot_center, 3.0, theme::accent_purple());
482 + painter.circle_filled(dot_center, 3.0, theme::category_five());
483 483 }
484 484 }
485 485
@@ -488,12 +488,12 @@
488 488 // chip_clicked_index for the click consumption.
489 489 if num_zones > 1 {
490 490 let zone_colors = [
491 - theme::accent_blue(),
492 - theme::accent_green(),
493 - theme::accent_yellow(),
494 - theme::accent_purple(),
495 - theme::accent_cyan(),
496 - theme::accent_red(),
491 + theme::action(),
492 + theme::success(),
493 + theme::warning(),
494 + theme::category_five(),
495 + theme::category_six(),
496 + theme::danger(),
497 497 ];
498 498
499 499 for (i, (low, high, _root)) in zone_ranges.iter().enumerate() {
@@ -516,7 +516,7 @@
516 516 let p2 = chip_rect.max - egui::vec2(pad, pad);
517 517 let p3 = egui::pos2(chip_rect.min.x + pad, chip_rect.max.y - pad);
518 518 let p4 = egui::pos2(chip_rect.max.x - pad, chip_rect.min.y + pad);
519 - let stroke = egui::Stroke::new(1.2, theme::text_primary());
519 + let stroke = egui::Stroke::new(1.2, theme::content());
520 520 painter.line_segment([p1, p2], stroke);
521 521 painter.line_segment([p3, p4], stroke);
522 522 }
@@ -546,7 +546,7 @@
546 546 painter.rect_filled(
547 547 hover_rect,
548 548 2.0,
549 - theme::accent_blue().linear_multiply(0.3),
549 + theme::action().linear_multiply(0.3),
550 550 );
551 551 egui::Tooltip::always_open(
552 552 ui.ctx().clone(),
@@ -598,7 +598,7 @@
598 598 ("Stab", 0.001, 0.05, 0.0, 0.05),
599 599 ];
600 600 ui.horizontal(|ui| {
601 - ui.label(egui::RichText::new("Preset").small().color(theme::text_secondary()));
601 + ui.label(egui::RichText::new("Preset").small().color(theme::content_secondary()));
602 602 for (label, a, d, s, r) in presets {
603 603 // Highlight a preset when the envelope matches it exactly. Float
604 604 // compare is intentional: any user-driven slider edit drops the
@@ -619,7 +619,7 @@
619 619 draw_adsr_envelope_shape(ui, envelope.attack, envelope.decay, envelope.sustain, envelope.release);
620 620
621 621 ui.horizontal(|ui| {
622 - ui.label(egui::RichText::new("A").small().color(theme::text_secondary()))
622 + ui.label(egui::RichText::new("A").small().color(theme::content_secondary()))
623 623 .on_hover_text("Attack — time to reach full volume after key press");
624 624 let slider = egui::Slider::new(&mut envelope.attack, 0.001..=5.0)
625 625 .logarithmic(true)
@@ -627,7 +627,7 @@
627 627 .suffix("s");
628 628 ui.add(slider);
629 629
630 - ui.label(egui::RichText::new("D").small().color(theme::text_secondary()))
630 + ui.label(egui::RichText::new("D").small().color(theme::content_secondary()))
631 631 .on_hover_text("Decay — time to fall from peak to sustain level");
632 632 let slider = egui::Slider::new(&mut envelope.decay, 0.001..=5.0)
633 633 .logarithmic(true)
@@ -637,13 +637,13 @@
637 637 });
638 638
639 639 ui.horizontal(|ui| {
640 - ui.label(egui::RichText::new("S").small().color(theme::text_secondary()))
640 + ui.label(egui::RichText::new("S").small().color(theme::content_secondary()))
641 641 .on_hover_text("Sustain — held volume level while the key is down (0 to 1)");
642 642 let slider = egui::Slider::new(&mut envelope.sustain, 0.0..=1.0)
643 643 .max_decimals(2);
644 644 ui.add(slider);
645 645
646 - ui.label(egui::RichText::new("R").small().color(theme::text_secondary()))
646 + ui.label(egui::RichText::new("R").small().color(theme::content_secondary()))
647 647 .on_hover_text("Release — time to fade to silence after key release");
648 648 let slider = egui::Slider::new(&mut envelope.release, 0.001..=10.0)
649 649 .logarithmic(true)
@@ -664,7 +664,7 @@
664 664 let painter = ui.painter_at(rect);
665 665
666 666 // Background frame
667 - painter.rect_filled(rect, 2.0, theme::bg_tertiary());
667 + painter.rect_filled(rect, 2.0, theme::surface_sunken());
668 668
669 669 // Map each phase to a horizontal slice. Use a soft log so 0.001s isn't a
670 670 // single pixel: weight = (1 + t).ln() with t in seconds, clamped.
@@ -687,7 +687,7 @@
687 687 let y_base = rect.bottom() - 4.0;
688 688 let y_sustain = y_base + (y_peak - y_base) * sustain.clamp(0.0, 1.0);
689 689
690 - let stroke = egui::Stroke::new(1.5, theme::accent_blue());
690 + let stroke = egui::Stroke::new(1.5, theme::action());
691 691 let p0 = egui::pos2(x0, y_base);
692 692 let p_a = egui::pos2(x_a, y_peak);
693 693 let p_d = egui::pos2(x_d, y_sustain);