max / audiofiles
| 1 | //! The main window, described: the file list with a status band under it. |
| 2 | //! |
| 3 | //! The eighth audiofiles port, and the first with **more than one region**. |
| 4 | //! Every described screen before this is a single `RegionKind::Pane`, which |
| 5 | //! meant `Screen::sidebar_content`'s arrangement had nothing to arrange and |
| 6 | //! `RegionKind::Band` and `Sidebar` had never been written by this app at all. |
| 7 | //! A description layer whose every screen is one box is not yet describing a |
| 8 | //! layout. |
| 9 | //! |
| 10 | //! # What composing a region turns out to cost: nothing |
| 11 | //! |
| 12 | //! `files::body` was a private function building the list's slot and is now |
| 13 | //! public, unchanged. The standalone `/files` window wraps it in a screen; this |
| 14 | //! screen puts it beside a band. **Two callers, one description**, and the list |
| 15 | //! did not have to learn that it might not be alone. That is the property a |
| 16 | //! region is for, and it is worth stating because the alternative — a |
| 17 | //! `files::screen` and a `files::embedded_screen` — is what every app that lacks |
| 18 | //! regions ends up writing. |
| 19 | //! |
| 20 | //! # The footer, and the three things it stops storing |
| 21 | //! |
| 22 | //! `ui::footer::draw_footer` is 373 lines and carries a surprising amount of |
| 23 | //! state that is not the app's: |
| 24 | //! |
| 25 | //! - **A fade timer.** The status line fades after five seconds and hides after |
| 26 | //! thirty, tracked with `status_set_at`, two `Duration` constants, an |
| 27 | //! `egui::Id` round-trip through `ctx.data` to notice the text changed, and a |
| 28 | //! `request_repaint_after` to land the transition. All of it is renderer |
| 29 | //! policy, which `Message::undo` settled in the vocabulary already: "How long |
| 30 | //! an undo stays offered is renderer policy". The description says the status |
| 31 | //! and its tone; how long a host keeps it up is the host's. |
| 32 | //! - **A width breakpoint.** `let narrow = ctx.content_rect().width() < 1000.0` |
| 33 | //! decides whether three items sit on the first row or a second one. That is a |
| 34 | //! layout decision made from a pixel measurement inside a drawing function, |
| 35 | //! and it is the clearest case in this app of the thing the description layer |
| 36 | //! exists to take away. |
| 37 | //! |
| 38 | //! Worth being exact about what replaced it, because the obvious answer is |
| 39 | //! the wrong one. `Ranked` (quasi 0.18.0) lets a placement say what it is |
| 40 | //! worth when room runs out, and the toolbar's `< 900` collapse became three |
| 41 | //! priorities. This band's `< 1000` did not, and should not: it *reflows*, |
| 42 | //! moving three items to a second row and dropping nothing. Ranking them |
| 43 | //! would delete facts the shipped footer keeps. Wrapping a row that does not |
| 44 | //! fit is the host's arithmetic in its own units, which is what the deletion |
| 45 | //! above already claimed, so nothing is owed here. |
| 46 | //! |
| 47 | //! The one member that is genuinely droppable is the tag badges, and they |
| 48 | //! say so. |
| 49 | //! - **A hand-painted progress bar.** Twenty lines of `rect_filled`, a bevel and |
| 50 | //! a click-to-seek hit test. The bar is [`Meter`]; the seek is not (see |
| 51 | //! below). |
| 52 | //! |
| 53 | //! # THE FINDING: `Meter` refuses the second of the two proportions here |
| 54 | //! |
| 55 | //! This band has two, and the vocabulary treats them differently: |
| 56 | //! |
| 57 | //! - **Analysis coverage** — "142/200 analyzed" — is a proportion of a set and |
| 58 | //! exactly what [`Meter`] was added for. No argument. |
| 59 | //! - **Playback position** is refused by `Meter`'s own header: "This is a |
| 60 | //! proportion of a set and not the progress of an operation. A running timer |
| 61 | //! or a fetch is imperative and live, and a screen is described once per |
| 62 | //! answer." |
| 63 | //! |
| 64 | //! It is described as a `Meter` here anyway, and the reason is the finding the |
| 65 | //! export port already filed (`quasi:docs:meter-refuses-progress`): the premise |
| 66 | //! moved when `Runtime::reload` landed. A screen is no longer described once; it |
| 67 | //! is described whenever the host asks, and this host asks every frame for |
| 68 | //! exactly the reason the export flow does. Playback is the **second consumer** |
| 69 | //! and the sharper one — the export progress at least moves when a worker |
| 70 | //! finishes a file, and this moves at the sample clock with nobody touching |
| 71 | //! anything. |
| 72 | //! |
| 73 | //! The type is right and the paragraph is wrong. Nothing here changes the type. |
| 74 | //! |
| 75 | //! # What is deliberately not described |
| 76 | //! |
| 77 | //! - **Click-to-seek on the progress bar.** The waveform's rule: a click that |
| 78 | //! maps a pixel to a frame and writes into a mutex an audio thread is filling |
| 79 | //! is a host fact, not a fact about a sample. |
| 80 | //! - **The toolbar and the sidebar.** Two more regions and the reason this |
| 81 | //! module is named for the shell rather than the footer: `toolbar.rs` (706) |
| 82 | //! and `sidebar.rs` (722) are the other two bands of the same window, and each |
| 83 | //! is its own pass. The screen below has the two regions that exist. |
| 84 | //! |
| 85 | //! # The migration strip, and why it is here rather than anywhere else |
| 86 | //! |
| 87 | //! `ui/layout_strip.rs` is 62 lines and its header argues, correctly, that |
| 88 | //! moving blobs into hash-prefix shards deserves a strip rather than a modal or |
| 89 | //! a mode: the migration auto-starts at vault open, the library stays usable |
| 90 | //! while it runs because reads resolve both layouts, and seizing the window |
| 91 | //! would be the wrong trade. That argument is about *where the fact goes*, which |
| 92 | //! makes it this module's: it is a band of the main window, declared after the |
| 93 | //! footer so it stacks above it. |
| 94 | //! |
| 95 | //! So there is no `/storage` address and no fourteenth capability for it. It is |
| 96 | //! [`Shell::migrating`](super::Shell::migrating), a band that is there while the |
| 97 | //! fact is true, and a described screen answering that band every time it is |
| 98 | //! asked is what makes "appears while a job runs" sayable at all. Compare the |
| 99 | //! loose-files warning three paragraphs down: the shipped app raises *that* as |
| 100 | //! an overlay, which is the thing no description can do, and this one was |
| 101 | //! already a band. |
| 102 | //! |
| 103 | //! Pause is honest here in a way a cancel usually is not, and the description |
| 104 | //! says so on the control: the sweep is resumable and records nothing until a |
| 105 | //! pass verifies the root is clean, so stopping defers the remainder rather than |
| 106 | //! abandoning it. |
| 107 | |
| 108 | use ; |
| 109 | use |
| 110 | Act, Action, Figure, Meter, Node, RegionKind, Request, Response, RouteError, Router, Screen, |
| 111 | Slot, Tag, |
| 112 | ; |
| 113 | |
| 114 | use ; |
| 115 | |
| 116 | /// The band under the list. |
| 117 | const FOOT: &str = "shell-foot"; |
| 118 | |
| 119 | /// The band above it, while blobs are being moved. |
| 120 | const STRIP: &str = "shell-migration"; |
| 121 | |
| 122 | /// Register the main screen's routes. |
| 123 | |
| 124 | router |
| 125 | .get |
| 126 | .post |
| 127 | .post |
| 128 | .post |
| 129 | |
| 130 | |
| 131 | /// `GET /` |
| 132 | |
| 133 | Ok |
| 134 | |
| 135 | |
| 136 | /// `POST /playback/stop` |
| 137 | |
| 138 | state.shell.stop; |
| 139 | Ok |
| 140 | |
| 141 | |
| 142 | /// `POST /hint/dismiss` |
| 143 | |
| 144 | state.shell.dismiss_hint; |
| 145 | Ok |
| 146 | |
| 147 | |
| 148 | /// `POST /storage/pause` |
| 149 | /// |
| 150 | /// Refused when nothing is migrating, for the sweep's reason: the address is |
| 151 | /// reachable by typing and pausing nothing is not a thing that happened. |
| 152 | |
| 153 | if state.shell.migrating.is_none |
| 154 | return Err; |
| 155 | |
| 156 | state.shell.pause_migration; |
| 157 | Ok |
| 158 | |
| 159 | |
| 160 | /// The window: what you can filter by, what is in it, and what it is doing. |
| 161 | /// |
| 162 | /// `pub(super)` because the sidebar's routes answer it. Every control in |
| 163 | /// [`library`](super::library) changes what the *list* shows — choosing a vault, |
| 164 | /// applying a tag filter, opening a collection — so the answer is the window |
| 165 | /// rather than the corner of it that was pressed. |
| 166 | pub |
| 167 | let screen = sidebar_content |
| 168 | .with |
| 169 | .with |
| 170 | .with; |
| 171 | |
| 172 | // Above the footer, which is where the shipped strip declares itself, and |
| 173 | // only while there is something to say. See the header. |
| 174 | match migrating |
| 175 | Some => screen.with.with, |
| 176 | None => screen.with, |
| 177 | |
| 178 | |
| 179 | |
| 180 | /// Blobs being moved into their shards, while any are. |
| 181 | |
| 182 | let running = state.shell.migrating?; |
| 183 | Some |
| 184 | new |
| 185 | .with |
| 186 | .with |
| 187 | new.label, |
| 188 | |
| 189 | .with |
| 190 | new.confirm |
| 191 | "Stop for now. The remainder resumes the next time this vault opens. Pause?", |
| 192 | , |
| 193 | , |
| 194 | |
| 195 | |
| 196 | |
| 197 | /// A count as the meter carries one. |
| 198 | |
| 199 | u32try_from.unwrap_or |
| 200 | |
| 201 | |
| 202 | /// The status band. |
| 203 | |
| 204 | let mut band = new; |
| 205 | |
| 206 | if let Some = state.shell.playing |
| 207 | band = transport; |
| 208 | |
| 209 | |
| 210 | let chosen = state.shell.chosen; |
| 211 | if chosen > 1 |
| 212 | band = band.with; |
| 213 | |
| 214 | |
| 215 | band = coverage; |
| 216 | band = missing; |
| 217 | band = saying; |
| 218 | |
| 219 | if let Some = state.shell.device |
| 220 | band = band.with; |
| 221 | else |
| 222 | // Said rather than omitted, because a silent preview with no device is |
| 223 | // the case this line exists to make diagnosable without opening |
| 224 | // Settings. |
| 225 | band = band.with |
| 226 | text: "Preview: no device".to_owned, |
| 227 | tone: Warning, |
| 228 | ; |
| 229 | |
| 230 | |
| 231 | // The focused sample's tags, inert. The shipped footer renders these as |
| 232 | // muted text rather than chips on purpose -- "these are inert |
| 233 | // (informational only), so the affordance contract should not invite a |
| 234 | // click" -- which is exactly what a badge is and a chip is not. |
| 235 | // |
| 236 | // Optional, and they are the only thing in this band that is. A badge here |
| 237 | // repeats a fact the detail panel states in full, so a window with no room |
| 238 | // for everything loses the repetition first. Everything else in the footer |
| 239 | // is a fact stated nowhere else on the screen. |
| 240 | for tag in state.shell.tags |
| 241 | band = band.with_ranked; |
| 242 | |
| 243 | |
| 244 | band |
| 245 | |
| 246 | |
| 247 | /// What is playing, and how far through. |
| 248 | |
| 249 | band.with |
| 250 | .with |
| 251 | new.label, |
| 252 | |
| 253 | .with |
| 254 | "{}/{}" |
| 255 | clock, |
| 256 | clock |
| 257 | ) |
| 258 | .with |
| 259 | new.key, |
| 260 | |
| 261 | |
| 262 | |
| 263 | /// How much of what is on screen has been analysed. |
| 264 | /// |
| 265 | /// A [`Meter`] and a [`Figure`], where the shipped footer has two coloured |
| 266 | /// strings. The proportion is the meter; the untagged count is a separate fact |
| 267 | /// about the same set rather than a second proportion of it, which is why it is |
| 268 | /// a figure and not a second bar. |
| 269 | |
| 270 | let seen = state.shell.analysed; |
| 271 | if seen.samples == 0 |
| 272 | return band; |
| 273 | |
| 274 | |
| 275 | let done = seen.analysed == seen.samples; |
| 276 | let mut band = band.with |
| 277 | new |
| 278 | .label |
| 279 | .tone, |
| 280 | ; |
| 281 | |
| 282 | // Suppressed until analysis has produced something, which is the shipped |
| 283 | // footer's rule: before the first result every sample is untagged and the |
| 284 | // count says nothing. |
| 285 | if seen.analysed > 0 && seen.untagged > 0 |
| 286 | band = band.with |
| 287 | seen.untagged.to_string, |
| 288 | "untagged", |
| 289 | ; |
| 290 | |
| 291 | band |
| 292 | |
| 293 | |
| 294 | /// The samples that have lost their files, and the way in to what can be done. |
| 295 | /// |
| 296 | /// **The band says it because no description can raise the overlay that says |
| 297 | /// it.** The shipped app puts the warning up over whatever the user was doing, |
| 298 | /// as soon as a vault load finds anything missing, and nothing a route answers |
| 299 | /// can do that: `Outcome::Over` exists because something was pressed. See |
| 300 | /// [`integrity`](super::integrity)'s header and the finding it shares with the |
| 301 | /// import preflight. So the fact is stated where the app states its other facts |
| 302 | /// and the modal is one act away. |
| 303 | |
| 304 | let missing = state.integrity.missing; |
| 305 | if missing == 0 |
| 306 | return band; |
| 307 | |
| 308 | band.with |
| 309 | kind: Banner, |
| 310 | tone: Warning, |
| 311 | text: format! |
| 312 | "{missing} sample{} cannot find {} file." |
| 313 | if missing == 1 else , |
| 314 | if missing == 1 else , |
| 315 | ), |
| 316 | |
| 317 | .with |
| 318 | "What is missing", |
| 319 | get, |
| 320 | |
| 321 | |
| 322 | |
| 323 | /// Whatever the app is telling the user. |
| 324 | /// |
| 325 | /// Tone rather than a timer. The shipped footer decides the colour from |
| 326 | /// `is_error_status`, a substring match over the message it is about to draw, |
| 327 | /// and then decides how long to keep it up from two constants and an elapsed |
| 328 | /// `Instant`. The first half is a fact the app knows when it writes the message |
| 329 | /// and is what `Tone` carries; the second half is renderer policy and is gone. |
| 330 | |
| 331 | if let Some = state.shell.status |
| 332 | return band.with |
| 333 | kind: Toast, |
| 334 | tone: match saying |
| 335 | Failed => Danger, |
| 336 | Ordinary => Neutral, |
| 337 | , |
| 338 | text, |
| 339 | ; |
| 340 | |
| 341 | if state.shell.hinting |
| 342 | return band |
| 343 | .with |
| 344 | .with |
| 345 | "Dismiss", |
| 346 | post, |
| 347 | ; |
| 348 | |
| 349 | band |
| 350 | |
| 351 | |
| 352 | /// Seconds as the transport writes them. |
| 353 | |
| 354 | format! |
| 355 | |
| 356 |