| 6 |
6 |
|
use super::widgets;
|
| 7 |
7 |
|
use crate::state::BrowserState;
|
| 8 |
8 |
|
|
|
9 |
+ |
/// Classification filter buttons, grouped: drums (percussive), tonal (pitched),
|
|
10 |
+ |
/// other (non-instrument). The old hard-coded flat order buried `music` and
|
|
11 |
+ |
/// `noise` at the tail; grouping gives a scannable left-to-right reading
|
|
12 |
+ |
/// without alphabetising away the drum-first reflex (p-1).
|
|
13 |
+ |
///
|
|
14 |
+ |
/// This must cover every class the analysis pipeline can assign. It previously
|
|
15 |
+ |
/// omitted `impact`, `ambience`, `foley` and `texture`, so four classes the
|
|
16 |
+ |
/// classifier actively produced could not be filtered for at all. Lifted out of
|
|
17 |
+ |
/// the draw closure so a test can hold it against the colour table, which is
|
|
18 |
+ |
/// the other place the class list is written down and the one it drifted from.
|
|
19 |
+ |
pub(super) const CLASS_GROUPS: &[(&str, &[&str])] = &[
|
|
20 |
+ |
("Drums", &["kick", "snare", "hihat", "cymbal", "percussion"]),
|
|
21 |
+ |
("Tonal", &["bass", "vocal", "synth", "pad", "music"]),
|
|
22 |
+ |
(
|
|
23 |
+ |
"Other",
|
|
24 |
+ |
&["impact", "ambience", "foley", "texture", "misc", "noise"],
|
|
25 |
+ |
),
|
|
26 |
+ |
];
|
|
27 |
+ |
|
| 9 |
28 |
|
/// Debounce numeric range filters: re-run the search only when a DragValue
|
| 10 |
29 |
|
/// drag finishes or the field loses keyboard focus, not on every intermediate
|
| 11 |
30 |
|
/// tick. The filter value still updates live on `.changed()` (so the widget
|
| 190 |
209 |
|
state.search.search_filter.classifications.clear();
|
| 191 |
210 |
|
changed = true;
|
| 192 |
211 |
|
}
|
| 193 |
|
- |
// Grouped: drums (percussive) | tonal (pitched) | other. The hard-coded
|
| 194 |
|
- |
// flat order made `music` and `noise` invisible at the tail; grouping
|
| 195 |
|
- |
// gives a scannable left-to-right reading without alphabetising away
|
| 196 |
|
- |
// the drum-first reflex (p-1).
|
| 197 |
|
- |
let groups: &[(&str, &[&str])] = &[
|
| 198 |
|
- |
("Drums", &["kick", "snare", "hihat", "cymbal", "percussion"]),
|
| 199 |
|
- |
("Tonal", &["bass", "vocal", "synth", "pad", "music"]),
|
| 200 |
|
- |
("Other", &["misc", "noise"]),
|
| 201 |
|
- |
];
|
| 202 |
|
- |
for (label, classes) in groups {
|
|
212 |
+ |
for (label, classes) in CLASS_GROUPS {
|
| 203 |
213 |
|
ui.label(
|
| 204 |
214 |
|
egui::RichText::new(*label)
|
| 205 |
215 |
|
.small()
|
| 430 |
440 |
|
mod tests {
|
| 431 |
441 |
|
use super::*;
|
| 432 |
442 |
|
|
|
443 |
+ |
#[test]
|
|
444 |
+ |
fn every_filterable_class_has_its_own_colour() {
|
|
445 |
+ |
// The bug this guards: the filter list and the colour table are two
|
|
446 |
+ |
// separate hand-written lists of the same classes, and they drifted.
|
|
447 |
+ |
// impact/ambience/foley/texture were in neither, so the classifier
|
|
448 |
+ |
// produced labels that could not be filtered and rendered in the
|
|
449 |
+ |
// fallback grey, indistinguishable from an unrecognised value.
|
|
450 |
+ |
let fallback = theme::content_secondary();
|
|
451 |
+ |
for (group, classes) in CLASS_GROUPS {
|
|
452 |
+ |
for class in *classes {
|
|
453 |
+ |
assert_ne!(
|
|
454 |
+ |
theme::classification_color(class),
|
|
455 |
+ |
fallback,
|
|
456 |
+ |
"class '{class}' in group '{group}' has no colour of its own"
|
|
457 |
+ |
);
|
|
458 |
+ |
}
|
|
459 |
+ |
}
|
|
460 |
+ |
}
|
|
461 |
+ |
|
|
462 |
+ |
#[test]
|
|
463 |
+ |
fn class_groups_have_no_duplicates() {
|
|
464 |
+ |
let mut seen: Vec<&str> = CLASS_GROUPS
|
|
465 |
+ |
.iter()
|
|
466 |
+ |
.flat_map(|(_, c)| c.iter().copied())
|
|
467 |
+ |
.collect();
|
|
468 |
+ |
let before = seen.len();
|
|
469 |
+ |
seen.sort_unstable();
|
|
470 |
+ |
seen.dedup();
|
|
471 |
+ |
assert_eq!(before, seen.len(), "a class appears in more than one group");
|
|
472 |
+ |
}
|
|
473 |
+ |
|
| 433 |
474 |
|
#[test]
|
| 434 |
475 |
|
fn range_bounds_treats_sentinel_edges_as_unbounded() {
|
| 435 |
476 |
|
// BPM: 0 = no lower bound, 300 = no upper bound.
|