max / audiofiles
| 1 | //! The help overlay, described rather than built, and the app's chrome beside it. |
| 2 | //! |
| 3 | //! The seventh audiofiles port, and the second untested surface it reaches: |
| 4 | //! `Runtime::with_chrome` had never been called by this port, so |
| 5 | //! [`Chrome`](quasi_router::Chrome) was written, answered by the renderer, and |
| 6 | //! consumed by nothing. |
| 7 | //! |
| 8 | //! # The whole point: the table exists once |
| 9 | //! |
| 10 | //! `Binding`'s own header says what this file is for — "a help overlay that |
| 11 | //! lists the bindings is otherwise a second, hand-written copy of them, free to |
| 12 | //! drift from what the keys actually do" — and `ui::overlays::draw_shortcuts_tab` |
| 13 | //! is that second copy, twenty-six rows of it, in seven hand-grouped arrays. The |
| 14 | //! keys it names are handled in `editor::handle_keyboard`, several hundred lines |
| 15 | //! away, and nothing checks that the two agree. |
| 16 | //! |
| 17 | //! Here [`chrome`] is the only table. The host binds it, so the keys work; the |
| 18 | //! help screen lists it, so the help is what the keys are. Neither reads the |
| 19 | //! other's copy because there is not one. |
| 20 | //! |
| 21 | //! # THE FINDING, and what it looks like answered |
| 22 | //! |
| 23 | //! This table was four rows for five days, and the four were function keys and |
| 24 | //! command chords: `Runtime::pressed_binding` read raw input before the screen |
| 25 | //! was drawn with no focus guard, so a bare letter declared here would have been |
| 26 | //! eaten out of the tag field, the rename pattern and the search box. Sixteen of |
| 27 | //! the shipped app's twenty-six shortcuts are bare letters, and none of them |
| 28 | //! could be said. |
| 29 | //! |
| 30 | //! quasi-immediate 0.52.0 (`bc5528e2`) put the guard where the finding said it |
| 31 | //! belonged — in the renderer, not in each description — and the letters below |
| 32 | //! are what it bought. A box with the caret answers its own keys; ctrl, alt and |
| 33 | //! command produce no character, so those keep working mid-word, which the |
| 34 | //! shipped app's broader "anything focused" rule would have lost. |
| 35 | //! |
| 36 | //! It also settled a second thing nobody could see while bare keys were |
| 37 | //! undeclarable: `f` and `shift+f` are two entries, and the renderer matched |
| 38 | //! them with `matches_logically`, which ignores a shift the pattern never asked |
| 39 | //! for. The bare one answered both. Exact matching now, and it is this table |
| 40 | //! that has the collision — the forge and Find similar. |
| 41 | //! |
| 42 | //! # What is still not here, and why |
| 43 | //! |
| 44 | //! Nine of the sixteen are declared. The rest are not the guard's business: |
| 45 | //! |
| 46 | //! - **`/` to focus the search box.** Focus is the renderer's, and nothing in |
| 47 | //! the vocabulary says "put the caret in that field". The one shortcut here |
| 48 | //! whose absence is a gap rather than a shape. |
| 49 | //! - **`j`, `k`, Enter, Backspace, Space.** Walking a list and playing what is |
| 50 | //! under the cursor. egui's reach already walks its own widgets, and a second |
| 51 | //! party moving the keyboard is what `quasi_immediate::runtime`'s header says |
| 52 | //! this renderer does not do. |
| 53 | //! - **Delete, and `cmd+A`.** Both act on the selection, and a `Binding` carries |
| 54 | //! an address with no payload. The bulk acts that need the ticked set reach it |
| 55 | //! through their own screens. |
| 56 | //! |
| 57 | //! # One more thing this screen could not say |
| 58 | //! |
| 59 | //! - **A `Binding` had no group, and now has one.** The shipped tab sorts |
| 60 | //! twenty-six rows into Navigation, Selection, Bulk, Search, Discovery, |
| 61 | //! Toggles and System, which for a list that long is the difference between a |
| 62 | //! reference and a wall. The described one was flat, and at thirteen rows it |
| 63 | //! started to want them. `cf7872dc` answered it: `Chrome::bind_in` says the |
| 64 | //! heading and `Chrome::grouped` gathers the runs, so the table below is four |
| 65 | //! sections in the order this file bound them. Three of the shipped headings |
| 66 | //! have no rows here, which is the nine-of-sixteen above rather than anything |
| 67 | //! the member cannot say. |
| 68 | //! - **An action cannot sit inside a sentence.** The features tab writes "Use |
| 69 | //! `/` to focus the search bar" with `/` as a live link that closes the help |
| 70 | //! and focuses the field. `Node::Link` is a leaf and prose is a `Node::Text`, |
| 71 | //! so a run of prose with a control in the middle of it is two nodes here and |
| 72 | //! reads as one sentence cut in half. The links are dropped rather than faked; |
| 73 | //! what they did is said in words. |
| 74 | //! |
| 75 | //! # And a second consumer for the overlay-refresh finding |
| 76 | //! |
| 77 | //! The shipped help has two tabs. Switching one inside an overlay cannot answer |
| 78 | //! a screen — that clears the layer stack — and cannot answer `Over` again — |
| 79 | //! that stacks a second copy. So the tab body is its own region and the switch |
| 80 | //! answers `Outcome::Fragment`, which is the same shape the rename preview |
| 81 | //! landed on. Two consumers now for `63cb3462`: **a tabbed overlay is not |
| 82 | //! buildable without fragments, and nothing says so.** |
| 83 | |
| 84 | use Selector; |
| 85 | use |
| 86 | Action, Cell, Cells, Choice, Chrome, Column, Node, Outcome, RegionKind, Request, Response, |
| 87 | RouteError, Router, Screen, Slot, |
| 88 | ; |
| 89 | |
| 90 | use ; |
| 91 | |
| 92 | /// The region the overlay answers into. |
| 93 | const BODY: &str = "help-body"; |
| 94 | /// The region a tab's contents land in. |
| 95 | const TAB: &str = "help-tab"; |
| 96 | /// The region the grouped key tables sit in. |
| 97 | /// |
| 98 | /// A region rather than a bare run because a tab's contents are one node, and |
| 99 | /// grouping turned this tab from one table into a heading and a table per |
| 100 | /// group. |
| 101 | const SHORTCUTS_BODY: &str = "help-shortcuts"; |
| 102 | |
| 103 | /// Which tab is showing. |
| 104 | const SHORTCUTS: &str = "shortcuts"; |
| 105 | /// The other one. |
| 106 | const FEATURES: &str = "features"; |
| 107 | |
| 108 | /// The keys that work from every described screen. |
| 109 | /// |
| 110 | /// **The only table.** [`routes`] lists it and [`panel`](super::panel) binds it, |
| 111 | /// so what the help says and what the keys do cannot disagree. See this module's |
| 112 | /// header for why it is thirteen rows and not twenty-six. |
| 113 | /// |
| 114 | /// Every action is an address this router serves, which is the other half of |
| 115 | /// "cannot disagree": a binding pointing at a route that does not exist would be |
| 116 | /// a `NotFound` the first time it was pressed rather than a lie in a table. |
| 117 | |
| 118 | |
| 119 | new |
| 120 | // Four of the shipped tab's seven headings, in its order. `cf7872dc` |
| 121 | // landed `Chrome::bind_in`, and the reason this table wanted it is the |
| 122 | // reason the shipped one has arrays: thirteen rows read as a wall, and |
| 123 | // a reader looking for the tagging key should not have to scan past the |
| 124 | // panel toggles to find it. |
| 125 | // |
| 126 | // Navigation, Selection and Search have no rows here. See this module's |
| 127 | // header for the seven shortcuts that are not the guard's business, and |
| 128 | // for `/` -- the one absence that is a gap rather than a shape. |
| 129 | .bind_in |
| 130 | BULK, |
| 131 | "f2", |
| 132 | "Rename the selection", |
| 133 | get, |
| 134 | |
| 135 | .bind_in |
| 136 | BULK, |
| 137 | "ctrl+t", |
| 138 | "Tag the selection", |
| 139 | get, |
| 140 | |
| 141 | .bind_in |
| 142 | BULK, |
| 143 | "ctrl+shift+m", |
| 144 | "Move the selection", |
| 145 | get, |
| 146 | |
| 147 | .bind_in |
| 148 | BULK, |
| 149 | "ctrl+z", |
| 150 | "Undo the last bulk action", |
| 151 | post, |
| 152 | |
| 153 | // Shift's two. They were declared beside `f` while this table was flat, |
| 154 | // because `shift+f` sitting directly behind it is what made the |
| 155 | // renderer's matching exact (quasi-immediate 0.52.0); the grouping |
| 156 | // separates them and the collision is unchanged, since matching reads |
| 157 | // the key and not the neighbour. |
| 158 | .bind_in |
| 159 | DISCOVERY, |
| 160 | "shift+f", |
| 161 | "Find similar samples", |
| 162 | post, |
| 163 | |
| 164 | .bind_in |
| 165 | DISCOVERY, |
| 166 | "shift+d", |
| 167 | "Find duplicates", |
| 168 | post, |
| 169 | |
| 170 | // The five panels the toolbar toggles, by the name an address is built |
| 171 | // from. `Panel::as_str` is that name, so a panel renamed here and there |
| 172 | // is one edit rather than two. |
| 173 | .bind_in |
| 174 | .bind_in |
| 175 | TOGGLES, |
| 176 | "d", |
| 177 | "Toggle the detail panel", |
| 178 | toggling, |
| 179 | |
| 180 | .bind_in |
| 181 | TOGGLES, |
| 182 | "e", |
| 183 | "Toggle the sample editor", |
| 184 | toggling, |
| 185 | |
| 186 | .bind_in |
| 187 | TOGGLES, |
| 188 | "i", |
| 189 | "Toggle the instrument panel", |
| 190 | toggling, |
| 191 | |
| 192 | .bind_in |
| 193 | // The forge is a screen rather than a panel, which is why this one is |
| 194 | // not built the same way as the five above it. It is still a Toggle to |
| 195 | // a reader, which is the whole reason the group is said rather than |
| 196 | // derived from the address: `/panels/sidebar` and `/forge` are siblings |
| 197 | // in nothing. |
| 198 | .bind_in |
| 199 | .bind_in |
| 200 | |
| 201 | |
| 202 | /// The shipped tab's headings, for the four this table has rows under. |
| 203 | /// |
| 204 | /// Named rather than written at each call site: a heading spelt two ways is two |
| 205 | /// groups, and the failure is a table that reads almost right. |
| 206 | const BULK: &str = "Bulk"; |
| 207 | /// Finding samples by what they are like. |
| 208 | const DISCOVERY: &str = "Discovery"; |
| 209 | /// Showing and hiding the app's own furniture. |
| 210 | const TOGGLES: &str = "Toggles"; |
| 211 | /// The app itself. |
| 212 | const SYSTEM: &str = "System"; |
| 213 | |
| 214 | /// Toggling one panel, addressed by the name the panel answers to. |
| 215 | /// |
| 216 | /// Named for what it does rather than for its argument, so it does not read as |
| 217 | /// a second [`panel`](super::panel) beside the host module of that name. |
| 218 | |
| 219 | post |
| 220 | |
| 221 | |
| 222 | /// Register the help overlay's routes. |
| 223 | |
| 224 | router.get.post |
| 225 | |
| 226 | |
| 227 | /// `GET /help` |
| 228 | |
| 229 | Ok |
| 230 | |
| 231 | |
| 232 | /// `POST /help/tab` |
| 233 | /// |
| 234 | /// A fragment, because this overlay is already open. See the header: neither |
| 235 | /// outcome that carries a whole screen can replace one layer of a stack. |
| 236 | |
| 237 | let chosen = request.payload.get.unwrap_or; |
| 238 | if chosen != SHORTCUTS && chosen != FEATURES |
| 239 | return Err; |
| 240 | |
| 241 | Ok |
| 242 | region: TAB.to_owned, |
| 243 | node: showing, |
| 244 | |
| 245 | |
| 246 | |
| 247 | /// The overlay. |
| 248 | |
| 249 | let body = new |
| 250 | .with |
| 251 | .with |
| 252 | kind: Tabs, |
| 253 | options: vec! |
| 254 | , |
| 255 | , |
| 256 | ], |
| 257 | chosen: Some, |
| 258 | action: Some, |
| 259 | |
| 260 | .with |
| 261 | new.with, |
| 262 | ; |
| 263 | |
| 264 | sidebar_content.with |
| 265 | |
| 266 | |
| 267 | /// Whichever tab is chosen. |
| 268 | |
| 269 | if chosen == FEATURES |
| 270 | features |
| 271 | else |
| 272 | shortcuts |
| 273 | |
| 274 | |
| 275 | |
| 276 | /// Every key that works, read off the one table. |
| 277 | /// |
| 278 | /// No filter box. The shipped tab has one because twenty-six rows in a |
| 279 | /// fixed-height scroll area need it; narrowing a list a screen was handed is |
| 280 | /// what a host does, which is the rule the bulk port's tag completions and |
| 281 | /// folder filter both follow. |
| 282 | |
| 283 | // A heading and a table per group, in the order the app bound them, which |
| 284 | // is what `Chrome::grouped` answers. Not sorted here: the reading order is |
| 285 | // the shipped tab's and belongs to whoever wrote the table. |
| 286 | // |
| 287 | // Several tables rather than one with a group column, because a group is a |
| 288 | // heading and not a value: a column repeating "Toggles" six times says the |
| 289 | // same thing six times and is still one wall. |
| 290 | let mut sections = Vecnew; |
| 291 | for in chrome.grouped |
| 292 | // `None` is the ungrouped run, which this table has none of today and |
| 293 | // would have again the moment somebody added a `bind`. Drawn without a |
| 294 | // heading rather than under an invented one. |
| 295 | if let Some = group |
| 296 | sections.push; |
| 297 | |
| 298 | sections.push |
| 299 | columns: vec!, |
| 300 | rows: bindings |
| 301 | .iter |
| 302 | .map |
| 303 | new |
| 304 | .activate |
| 305 | |
| 306 | .collect, |
| 307 | // Every key that is bound is listed, which is the whole claim of |
| 308 | // this screen. A shortcuts table with something withheld would be |
| 309 | // the drift it exists to end. |
| 310 | more: None, |
| 311 | ; |
| 312 | |
| 313 | let mut body = new; |
| 314 | for node in sections |
| 315 | body = body.with; |
| 316 | |
| 317 | Region |
| 318 | |
| 319 | |
| 320 | /// What the app does, in prose. |
| 321 | /// |
| 322 | /// One `Node::Rich` rather than nine headings and nine paragraphs, because it is |
| 323 | /// a document: markdown source is what `Rich` carries and every renderer turns |
| 324 | /// it into its own markup, which is the member's whole argument. The shipped tab |
| 325 | /// builds the same thing out of `ui.heading` and `ui.label` calls, so the |
| 326 | /// structure is there and is not written down anywhere a renderer can read. |
| 327 | |
| 328 | rich |
| 329 | |
| 330 | |
| 331 | /// The features tab, as the document it is. |
| 332 | /// |
| 333 | /// Taken from `ui::overlays::draw_features_tab` with its three live links |
| 334 | /// written out in words: see this module's header on why an action cannot sit |
| 335 | /// inside a sentence. |
| 336 | const FEATURES_MD: &str = "\ |
| 337 | ## Search and filter |
| 338 | |
| 339 | Press `/` to focus the search bar. Filter by BPM range, duration, loudness, key \ |
| 340 | and tags from the filter panel. Save any filter combination as a dynamic \ |
| 341 | collection. |
| 342 | |
| 343 | ## Collections |
| 344 | |
| 345 | Manual collections: right-click samples, then Add to Collection. Dynamic \ |
| 346 | collections: set filters, then click Save. A dynamic collection updates itself \ |
| 347 | when new samples match. |
| 348 | |
| 349 | ## Tags |
| 350 | |
| 351 | Use dot notation for hierarchy: `drums.kick`, `genre.house`. Filter by tag in \ |
| 352 | the sidebar tag tree, and tag a whole selection at once with `Ctrl+T`. Tag \ |
| 353 | suggestions appear in the detail panel, drawn from similar samples you have \ |
| 354 | already tagged. |
| 355 | |
| 356 | ## Import |
| 357 | |
| 358 | Quick Import indexes and analyses a whole folder. Files stay where they are \ |
| 359 | rather than being copied, and duplicates are skipped by content hash. |
| 360 | |
| 361 | ## Export |
| 362 | |
| 363 | Export to hardware samplers with device profiles: SP-404, Digitakt, MPC and the \ |
| 364 | rest. A profile sets the format, sample rate and naming rules for you. Or export \ |
| 365 | manually with your own settings. |
| 366 | |
| 367 | ## Instrument and MIDI |
| 368 | |
| 369 | The instrument panel plays a sample chromatically. Right-click a sample, then \ |
| 370 | Play as Instrument, to load it; right-click a key to set the root note. Connect \ |
| 371 | a MIDI controller for external playback. |
| 372 | |
| 373 | ## Sample editor |
| 374 | |
| 375 | The editor trims, normalises to peak or LUFS, applies gain, reverses, and fades \ |
| 376 | in or out. Select several samples to normalise, gain or reverse them together. \ |
| 377 | Its result mode decides whether the original is replaced or a sibling is made. |
| 378 | |
| 379 | ## Drag and drop |
| 380 | |
| 381 | Drag samples from the file list straight into your DAW or your file manager. \ |
| 382 | Drop audio files or folders onto the window to import them. |
| 383 | |
| 384 | ## Cloud sync |
| 385 | |
| 386 | Sync metadata -- tags and organisation -- across devices. Metadata sync is free; \ |
| 387 | syncing the sample files themselves is tiered by storage. |
| 388 | |
| 389 | ## System tray |
| 390 | |
| 391 | audiofiles keeps running in the tray when the window closes. Playback continues \ |
| 392 | while it is there. |
| 393 | "; |
| 394 |