|
| 1 | +# Issue #197 — Persisted Sound-Tile Sorting |
| 2 | + |
| 3 | +Date: 2026-07-23 · Status: approved |
| 4 | + |
| 5 | +## Why |
| 6 | + |
| 7 | +The main sound grid needs predictable, user-selectable ordering without coupling sorting to its |
| 8 | +session-only filter. The first consumer establishes reusable list-control state and widgets for |
| 9 | +later slot, shortcut, and macro views while keeping grouping out of scope until user-defined tags |
| 10 | +exist. |
| 11 | + |
| 12 | +The current folder-derived category is presented as **Folder**. “Tag” remains reserved for #201. |
| 13 | + |
| 14 | +## Data Shapes |
| 15 | + |
| 16 | +- `ui::list_controls::sort::Direction::{Ascending, Descending}` controls display order. |
| 17 | +- `ui::list_controls::sort::SortState<K> { key, direction }` owns one view's active ordering. |
| 18 | +- `ui::list_controls::sort::SortKey<T>` supplies a label, primary comparison, and an optional |
| 19 | + unknown-value predicate. `SortState` applies direction only within known/unknown buckets so |
| 20 | + unknown values remain last in both directions. |
| 21 | +- `app::sorting::SoundSortKey::{Name, Length, Folder, Modified, Added}` implements the sound-grid |
| 22 | + ordering contract. The view default is Name ascending. |
| 23 | +- `state::config::SortPref` is a persistence DTO containing string key and direction values. |
| 24 | + `AppConfig.sort_prefs` stores these by view ID; the main grid uses `"tiles"`. |
| 25 | +- The sort menu anchor is transient app state. It is never persisted. |
| 26 | + |
| 27 | +Keeping the persisted DTO separate from the runtime enums lets old or future config values load |
| 28 | +without making the whole config unreadable. A missing or unrecognized preference resolves to that |
| 29 | +view's complete default. |
| 30 | + |
| 31 | +## Interfaces and Touch Map |
| 32 | + |
| 33 | +- `src/ui/list_controls/sort.rs` |
| 34 | + - pure generic sort controller and direction handling; |
| 35 | + - `view_sort_chip` with separate label and chevron actions; |
| 36 | + - `view_sort_menu_overlay` with option selection and a full-window dismiss backdrop. |
| 37 | +- `src/state/config/sort.rs` |
| 38 | + - tolerant persistence DTO. |
| 39 | +- `src/state/config.rs` |
| 40 | + - extract the existing test module first so the production file is below 400 lines; |
| 41 | + - add `sort_prefs` with a Serde default. |
| 42 | +- `src/app/sorting.rs` |
| 43 | + - preference conversion, sound comparison, app update helpers, and boundary tests. |
| 44 | +- `src/app/header.rs` |
| 45 | + - extract the existing header from oversized `app/mod.rs` and place the sort chip directly beside |
| 46 | + the main search input. |
| 47 | +- `src/app/filtering.rs` |
| 48 | + - compose category/query filtering with sound ordering and give an open sort menu Escape priority. |
| 49 | +- `src/app/mod.rs` |
| 50 | + - declarations and narrow delegation only; the extracted header ensures the already-oversized |
| 51 | + file shrinks overall. |
| 52 | + |
| 53 | +No dependency is added. |
| 54 | + |
| 55 | +## Ordering Contract |
| 56 | + |
| 57 | +1. Name compares the customized display name when present, otherwise the scanned name. |
| 58 | +2. Name and Folder comparisons are Unicode-lowercased before comparison. |
| 59 | +3. Every primary-key tie is resolved by path and then sound ID, producing deterministic output. |
| 60 | +4. Length compares milliseconds; Folder compares the current folder-derived category; Modified |
| 61 | + uses the scan timestamp; Added uses the persisted first-seen timestamp. |
| 62 | +5. Missing Length, Modified, or Added values form an unknown bucket after all known values. |
| 63 | + Descending reverses ordering within each bucket, never the bucket placement. |
| 64 | +6. Filtering and category selection happen before sorting and do not alter the persisted |
| 65 | + preference. |
| 66 | + |
| 67 | +## Interaction Contract |
| 68 | + |
| 69 | +- Clicking the chip label toggles the options menu. |
| 70 | +- Clicking only the chevron toggles direction and persists immediately. |
| 71 | +- Choosing a key persists it and closes the menu. |
| 72 | +- Clicking outside the menu or pressing Escape closes it without changing the preference. |
| 73 | +- Filter text remains session-only. |
| 74 | +- The menu exposes sorting as its own section/function so a later grouping section can be appended |
| 75 | + without changing `SortState`; grouping itself is not implemented. |
| 76 | + |
| 77 | +## Iced API Finding |
| 78 | + |
| 79 | +No throwaway spike is required. `sound_grid::context_menu_overlay` already demonstrates the exact |
| 80 | +Iced 0.14 API needed here: a stable window-level `Stack`, a full-size `mouse_area` dismiss layer, |
| 81 | +and the interactive menu above it. The sort menu reuses this proven pattern and clamps its captured |
| 82 | +cursor anchor to the window bounds. |
| 83 | + |
| 84 | +## Invariants |
| 85 | + |
| 86 | +1. Name ascending is the default for missing, malformed, or unknown tile preferences. |
| 87 | +2. A valid preference survives `AppConfig` serialization and reload. |
| 88 | +3. Unknown persisted keys or directions never fail config deserialization and never partially |
| 89 | + influence runtime state. |
| 90 | +4. Unknown dates always sort last for both ascending and descending directions. |
| 91 | +5. Case-insensitive name ties are deterministic by path and ID. |
| 92 | +6. Changing direction or key never mutates the sound library or metadata. |
| 93 | +7. Opening or dismissing the menu never changes the active ordering. |
| 94 | +8. The main filter query is never written to config. |
| 95 | + |
| 96 | +## TDD Sequence |
| 97 | + |
| 98 | +1. Add failing pure tests for per-key ordering, case-insensitive ties, direction, and unknown values. |
| 99 | +2. Add failing config tests for preference round trips and tolerant unknown data. |
| 100 | +3. Implement runtime/persistence types and the pure sound sorter. |
| 101 | +4. Add app-boundary tests proving preference load, selection, toggle, dismissal, and ordered |
| 102 | + filtered results. |
| 103 | +5. Wire the chip and overlay using the established Iced stack pattern. |
| 104 | +6. Format, run focused tests, the complete test suite, Clippy with warnings denied, and review the |
| 105 | + complete branch diff. |
0 commit comments