max / audiofiles
1 file changed,
+219 insertions,
-200 deletions
| @@ -86,11 +86,9 @@ | |||
| 86 | 86 | //! screen should not carry a description of a screen that has not been built. | |
| 87 | 87 | //! The shipped window may keep it; there is nothing to port. | |
| 88 | 88 | ||
| 89 | + | use quasi_declare::declare; | |
| 89 | 90 | use quasi_router::layout::Tone; | |
| 90 | - | use quasi_router::{ | |
| 91 | - | Act, Action, Choice, Field, Node, RegionKind, Request, Response, RouteError, Router, Screen, | |
| 92 | - | Slot, | |
| 93 | - | }; | |
| 91 | + | use quasi_router::{Action, Choice, Node, Request, Response, RouteError, Router}; | |
| 94 | 92 | ||
| 95 | 93 | use super::{Chop, DeviceChoice, Forging, Knob, Panels}; | |
| 96 | 94 | ||
| @@ -118,7 +116,7 @@ | |||
| 118 | 116 | ||
| 119 | 117 | /// `GET /forge` | |
| 120 | 118 | fn index(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 121 | - | Ok(screen(state).into()) | |
| 119 | + | Ok(showing(state)) | |
| 122 | 120 | } | |
| 123 | 121 | ||
| 124 | 122 | /// `POST /forge/slice` | |
| @@ -135,7 +133,7 @@ | |||
| 135 | 133 | .unwrap_or_default(); | |
| 136 | 134 | let how = Chop::from_key(name).ok_or_else(|| RouteError::not_found("no such chop method"))?; | |
| 137 | 135 | state.forge.slice_by(how); | |
| 138 | - | Ok(screen(state).into()) | |
| 136 | + | Ok(showing(state)) | |
| 139 | 137 | } | |
| 140 | 138 | ||
| 141 | 139 | /// `POST /forge/set/{knob}` | |
| @@ -153,14 +151,14 @@ | |||
| 153 | 151 | .or_else(|| request.payload.get(Node::SELECTED)) | |
| 154 | 152 | .unwrap_or_default(); | |
| 155 | 153 | state.forge.turn(knob, value); | |
| 156 | - | Ok(screen(state).into()) | |
| 154 | + | Ok(showing(state)) | |
| 157 | 155 | } | |
| 158 | 156 | ||
| 159 | 157 | /// `POST /forge/preview` | |
| 160 | 158 | fn preview(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 161 | 159 | forging(state)?; | |
| 162 | 160 | state.forge.preview(); | |
| 163 | - | Ok(screen(state).into()) | |
| 161 | + | Ok(showing(state)) | |
| 164 | 162 | } | |
| 165 | 163 | ||
| 166 | 164 | /// `POST /forge/chop` | |
| @@ -175,7 +173,7 @@ | |||
| 175 | 173 | return Err(RouteError::not_found("preview the slices first")); | |
| 176 | 174 | } | |
| 177 | 175 | state.forge.chop(); | |
| 178 | - | Ok(screen(state).into()) | |
| 176 | + | Ok(showing(state)) | |
| 179 | 177 | } | |
| 180 | 178 | ||
| 181 | 179 | /// `POST /forge/device` | |
| @@ -194,7 +192,7 @@ | |||
| 194 | 192 | return Err(RouteError::not_found("no such device profile")); | |
| 195 | 193 | } | |
| 196 | 194 | state.forge.choose_device(chosen); | |
| 197 | - | Ok(screen(state).into()) | |
| 195 | + | Ok(showing(state)) | |
| 198 | 196 | } | |
| 199 | 197 | ||
| 200 | 198 | /// `POST /forge/conform` | |
| @@ -204,7 +202,7 @@ | |||
| 204 | 202 | return Err(RouteError::not_found("choose a device first")); | |
| 205 | 203 | } | |
| 206 | 204 | state.forge.conform(); | |
| 207 | - | Ok(screen(state).into()) | |
| 205 | + | Ok(showing(state)) | |
| 208 | 206 | } | |
| 209 | 207 | ||
| 210 | 208 | /// `POST /forge/trim` | |
| @@ -218,7 +216,7 @@ | |||
| 218 | 216 | return Err(RouteError::not_found("choose two or more samples")); | |
| 219 | 217 | } | |
| 220 | 218 | state.forge.trim_silence(); | |
| 221 | - | Ok(screen(state).into()) | |
| 219 | + | Ok(showing(state)) | |
| 222 | 220 | } | |
| 223 | 221 | ||
| 224 | 222 | /// The sample in the forge, refusing every write when there is none. | |
| @@ -229,93 +227,209 @@ | |||
| 229 | 227 | .ok_or_else(|| RouteError::not_found("no sample is in the forge")) | |
| 230 | 228 | } | |
| 231 | 229 | ||
| 232 | - | /// The window. | |
| 233 | - | fn screen(state: &Panels<'_>) -> Screen { | |
| 234 | - | let body = match state.forge.forging() { | |
| 235 | - | Some(forging) => loaded(&forging), | |
| 236 | - | None => Slot::new(BODY, RegionKind::Pane).with(Node::empty( | |
| 237 | - | "Select a sample and open the forge to chop, conform, or batch-process it.", | |
| 238 | - | )), | |
| 239 | - | }; | |
| 240 | - | Screen::sidebar_content("Sample Forge").with(body) | |
| 230 | + | /// The window, read and then described. | |
| 231 | + | pub(super) fn showing(state: &Panels<'_>) -> Response { | |
| 232 | + | Response::from(screen(&read(state))) | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | /// What the window draws, read off the app. | |
| 236 | + | struct Forge { | |
| 237 | + | /// The sample in the forge, while one is in it. | |
| 238 | + | loaded: Option<Loaded>, | |
| 241 | 239 | } | |
| 242 | 240 | ||
| 243 | 241 | /// A sample, and the three things that can be made out of it. | |
| 244 | - | fn loaded(forging: &Forging) -> Slot { | |
| 245 | - | let mut body = Slot::new(BODY, RegionKind::Pane) | |
| 246 | - | .with(Node::page(forging.name.clone())) | |
| 247 | - | .with(Node::text(format!("{} Hz", forging.rate))); | |
| 248 | - | ||
| 249 | - | // Said rather than drawn as a spinner beside a separator: what a reader | |
| 250 | - | // needs from it is that every control below is currently inert, and the | |
| 251 | - | // controls say that themselves through `State::Disabled`. | |
| 252 | - | if forging.busy { | |
| 253 | - | body = body.with(Node::banner(Tone::Info, "Working...")); | |
| 254 | - | } | |
| 255 | - | ||
| 256 | - | body = body.with(Node::Region(chopping(forging))); | |
| 257 | - | body = body.with(Node::Region(conforming(forging))); | |
| 258 | - | body.with(Node::Region(batching(forging))) | |
| 242 | + | struct Loaded { | |
| 243 | + | /// What the sample is called. | |
| 244 | + | name: String, | |
| 245 | + | /// What it was recorded at. | |
| 246 | + | rate: String, | |
| 247 | + | /// Whether a chop or a conform is in flight. | |
| 248 | + | /// | |
| 249 | + | /// Said rather than drawn as a spinner beside a separator: what a reader | |
| 250 | + | /// needs from it is that every control below is currently inert, and the | |
| 251 | + | /// controls say that themselves through `State::Disabled`. | |
| 252 | + | busy: bool, | |
| 253 | + | /// Slicing one sample into several. | |
| 254 | + | chop: Chopping, | |
| 255 | + | /// The device profiles a conform could target, while there are any. | |
| 256 | + | conform: Option<Conform>, | |
| 257 | + | /// Batch trimming, while enough samples are chosen for it to mean anything. | |
| 258 | + | batch: Option<Batch>, | |
| 259 | 259 | } | |
| 260 | 260 | ||
| 261 | 261 | /// Slicing one sample into several. | |
| 262 | - | fn chopping(forging: &Forging) -> Slot { | |
| 263 | - | // The section heading became the field's label: one name for one question, | |
| 264 | - | // rather than a heading above a control that could not say what it asked. | |
| 265 | - | let mut group = Slot::new("forge-chop", RegionKind::Group).with(Node::Field(Box::new( | |
| 266 | - | Field::radio( | |
| 267 | - | HOW, | |
| 268 | - | "Chop", | |
| 269 | - | Chop::ALL | |
| 270 | - | .into_iter() | |
| 271 | - | .map(|how| Choice::new(how.as_str(), how.label())) | |
| 272 | - | .collect(), | |
| 273 | - | ) | |
| 274 | - | .value(forging.how.as_str()) | |
| 275 | - | .writes(Action::post("/forge/slice")), | |
| 276 | - | ))); | |
| 262 | + | struct Chopping { | |
| 263 | + | /// The method chosen, as the picker's value. | |
| 264 | + | how: &'static str, | |
| 265 | + | /// Every method it could be. | |
| 266 | + | methods: Vec<Choice>, | |
| 267 | + | /// The number the chosen method reads, where it reads one. | |
| 268 | + | /// | |
| 269 | + | /// Only the parameters the chosen method reads are here at all, which is the | |
| 270 | + | /// shipped window's own `match` and the settings screen's line: a control | |
| 271 | + | /// that cannot be used is worse than one that is not there. At most one of | |
| 272 | + | /// each, and the number goes before the choice in the one method that has | |
| 273 | + | /// both. | |
| 274 | + | number: Option<Dial>, | |
| 275 | + | /// The choice it reads, where it reads one. | |
| 276 | + | choice: Option<Strip>, | |
| 277 | + | /// What the commit reads. | |
| 278 | + | /// | |
| 279 | + | /// The count on the label, which is the correction the shipped button made | |
| 280 | + | /// to itself: a commit says its blast radius before it is pressed. | |
| 281 | + | go: String, | |
| 282 | + | /// Whether the commit is dead, for want of a preview or for a run in flight. | |
| 283 | + | stuck: bool, | |
| 284 | + | /// Whether it is dead for want of a preview specifically. | |
| 285 | + | /// | |
| 286 | + | /// The fifth consumer of `quasi:vocabulary:disabled-reason`: the shipped | |
| 287 | + | /// button says the reason to a pointer and nothing else can, so it is a line | |
| 288 | + | /// of its own beside the control. | |
| 289 | + | unpreviewed: bool, | |
| 290 | + | } | |
| 277 | 291 | ||
| 278 | - | // Only the parameters the chosen method reads, which is the shipped | |
| 279 | - | // window's own `match` and the settings screen's line: a control that | |
| 280 | - | // cannot be used is worse than one that is not there. | |
| 281 | - | group = match forging.how { | |
| 282 | - | Chop::Transient => group.with(dial( | |
| 283 | - | forging, | |
| 284 | - | Field::range(Knob::Sensitivity.as_str(), "Sensitivity", "0", "1") | |
| 285 | - | .step("0.01") | |
| 286 | - | .value(format!("{:.2}", forging.sensitivity)), | |
| 287 | - | )), | |
| 288 | - | Chop::Equal => group.with(strip( | |
| 289 | - | Knob::Divisions, | |
| 290 | - | "Slices", | |
| 291 | - | &forging.divisions.to_string(), | |
| 292 | - | [2_usize, 4, 8, 16, 32].map(|n| (n.to_string(), n.to_string())), | |
| 293 | - | )), | |
| 294 | - | Chop::Bpm => group | |
| 295 | - | .with(dial( | |
| 296 | - | forging, | |
| 297 | - | Field::range(Knob::Bpm.as_str(), "BPM", "20", "300") | |
| 298 | - | .step("0.5") | |
| 299 | - | .value(format!("{:.1}", forging.bpm)), | |
| 300 | - | )) | |
| 301 | - | .with(strip( | |
| 302 | - | Knob::Subdivisions, | |
| 303 | - | "Subdivision", | |
| 304 | - | &forging.subdivisions.to_string(), | |
| 305 | - | [("1", "1/4"), ("2", "1/8"), ("4", "1/16")] | |
| 306 | - | .map(|(value, label)| (value.to_owned(), label.to_owned())), | |
| 307 | - | )), | |
| 292 | + | /// A number a control turns. | |
| 293 | + | struct Dial { | |
| 294 | + | /// The name it submits under, which is also what its address is built from. | |
| 295 | + | name: &'static str, | |
| 296 | + | /// What the question is called. | |
| 297 | + | label: &'static str, | |
| 298 | + | /// The ends of the slider. | |
| 299 | + | min: &'static str, | |
| 300 | + | max: &'static str, | |
| 301 | + | /// The granularity it moves in. | |
| 302 | + | step: &'static str, | |
| 303 | + | /// The value showing, written at the precision the control uses. | |
| 304 | + | value: String, | |
| 305 | + | /// What the number is measured in, where it is measured in anything. | |
| 306 | + | unit: Option<&'static str>, | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | /// A handful of values that do not fold away, asked as a question. | |
| 310 | + | /// | |
| 311 | + | /// Five slice counts and three subdivisions are one choice each, written | |
| 312 | + | /// immediately. Being a field is what gives them a label; a bare strip of | |
| 313 | + | /// options had to borrow one from a section heading above. | |
| 314 | + | struct Strip { | |
| 315 | + | /// The name it submits under, which is also what its address is built from. | |
| 316 | + | name: &'static str, | |
| 317 | + | /// What the question is called. | |
| 318 | + | label: &'static str, | |
| 319 | + | /// The value showing. | |
| 320 | + | chosen: String, | |
| 321 | + | /// What it could be. | |
| 322 | + | options: Vec<Choice>, | |
| 323 | + | } | |
| 324 | + | ||
| 325 | + | /// Making one sample fit a piece of hardware. | |
| 326 | + | struct Conform { | |
| 327 | + | /// The profiles, as the picker's options. | |
| 328 | + | devices: Vec<Choice>, | |
| 329 | + | /// Which is chosen, empty for none. | |
| 330 | + | chosen: String, | |
| 331 | + | /// Whether the commit is dead: nothing chosen, or a run in flight. | |
| 332 | + | stuck: bool, | |
| 333 | + | } | |
| 334 | + | ||
| 335 | + | /// The one operation here that is about the selection rather than the sample. | |
| 336 | + | struct Batch { | |
| 337 | + | /// The level below which batch trim treats audio as silence. | |
| 338 | + | threshold: Dial, | |
| 339 | + | /// What the button reads. | |
| 340 | + | go: String, | |
| 341 | + | } | |
| 342 | + | ||
| 343 | + | /// What the window draws, read off the app. | |
| 344 | + | fn read(state: &Panels<'_>) -> Forge { | |
| 345 | + | Forge { | |
| 346 | + | loaded: state.forge.forging().map(|forging| Loaded { | |
| 347 | + | name: forging.name.clone(), | |
| 348 | + | rate: format!("{} Hz", forging.rate), | |
| 349 | + | busy: forging.busy, | |
| 350 | + | chop: chopping_read(&forging), | |
| 351 | + | conform: (!forging.devices.is_empty()).then(|| Conform { | |
| 352 | + | devices: forging | |
| 353 | + | .devices | |
| 354 | + | .iter() | |
| 355 | + | .map(|device| Choice::new(device.name.clone(), describe(device))) | |
| 356 | + | .collect(), | |
| 357 | + | chosen: forging.device.clone().unwrap_or_default(), | |
| 358 | + | stuck: forging.device.is_none() || forging.busy, | |
| 359 | + | }), | |
| 360 | + | batch: (forging.chosen >= 2).then(|| Batch { | |
| 361 | + | threshold: Dial { | |
| 362 | + | name: Knob::Threshold.as_str(), | |
| 363 | + | label: "Threshold", | |
| 364 | + | min: "-96", | |
| 365 | + | max: "-20", | |
| 366 | + | step: "1", | |
| 367 | + | value: format!("{:.0}", forging.threshold_db), | |
| 368 | + | unit: Some("dBFS"), | |
| 369 | + | }, | |
| 370 | + | go: format!("Trim silence on {} samples", forging.chosen), | |
| 371 | + | }), | |
| 372 | + | }), | |
| 373 | + | } | |
| 374 | + | } | |
| 375 | + | ||
| 376 | + | /// The chop section, read off the sample. | |
| 377 | + | fn chopping_read(forging: &Forging) -> Chopping { | |
| 378 | + | let (number, choice) = match forging.how { | |
| 379 | + | Chop::Transient => ( | |
| 380 | + | Some(Dial { | |
| 381 | + | name: Knob::Sensitivity.as_str(), | |
| 382 | + | label: "Sensitivity", | |
| 383 | + | min: "0", | |
| 384 | + | max: "1", | |
| 385 | + | step: "0.01", | |
| 386 | + | value: format!("{:.2}", forging.sensitivity), | |
| 387 | + | unit: None, | |
| 388 | + | }), | |
| 389 | + | None, | |
| 390 | + | ), | |
| 391 | + | Chop::Equal => ( | |
| 392 | + | None, | |
| 393 | + | Some(Strip { | |
| 394 | + | name: Knob::Divisions.as_str(), | |
| 395 | + | label: "Slices", | |
| 396 | + | chosen: forging.divisions.to_string(), | |
| 397 | + | options: [2_usize, 4, 8, 16, 32] | |
| 398 | + | .into_iter() | |
| 399 | + | .map(|n| Choice::new(n.to_string(), n.to_string())) | |
| 400 | + | .collect(), | |
| 401 | + | }), | |
| 402 | + | ), | |
| 403 | + | Chop::Bpm => ( | |
| 404 | + | Some(Dial { | |
| 405 | + | name: Knob::Bpm.as_str(), | |
| 406 | + | label: "BPM", | |
| 407 | + | min: "20", | |
| 408 | + | max: "300", | |
| 409 | + | step: "0.5", | |
| 410 | + | value: format!("{:.1}", forging.bpm), | |
| 411 | + | unit: None, | |
| 412 | + | }), | |
| 413 | + | Some(Strip { | |
| 414 | + | name: Knob::Subdivisions.as_str(), | |
| 415 | + | label: "Subdivision", | |
| 416 | + | chosen: forging.subdivisions.to_string(), | |
| 417 | + | options: [("1", "1/4"), ("2", "1/8"), ("4", "1/16")] | |
| 418 | + | .into_iter() | |
| 419 | + | .map(|(value, label)| Choice::new(value, label)) | |
| 420 | + | .collect(), | |
| 421 | + | }), | |
| 422 | + | ), | |
| 308 | 423 | }; | |
| 309 | - | ||
| 310 | - | group = group.with(Node::Act(live( | |
| 311 | - | forging, | |
| 312 | - | Act::new("Preview slices", Action::post("/forge/preview")), | |
| 313 | - | ))); | |
| 314 | - | ||
| 315 | - | // The count on the label, which is the correction the shipped button made to | |
| 316 | - | // itself: a commit says its blast radius before it is pressed. | |
| 317 | - | let mut go = Act::new( | |
| 318 | - | if forging.slices == 0 { | |
| 424 | + | Chopping { | |
| 425 | + | how: forging.how.as_str(), | |
| 426 | + | methods: Chop::ALL | |
| 427 | + | .into_iter() | |
| 428 | + | .map(|how| Choice::new(how.as_str(), how.label())) | |
| 429 | + | .collect(), | |
| 430 | + | number, | |
| 431 | + | choice, | |
| 432 | + | go: if forging.slices == 0 { | |
| 319 | 433 | "Chop".to_owned() | |
| 320 | 434 | } else { | |
| 321 | 435 | format!( | |
| @@ -324,89 +438,9 @@ | |||
| 324 | 438 | if forging.slices == 1 { "" } else { "s" } | |
| 325 | 439 | ) | |
| 326 | 440 | }, | |
| 327 | - | Action::post("/forge/chop"), | |
| 328 | - | ); | |
| 329 | - | if forging.slices == 0 { | |
| 330 | - | // The fifth consumer of `quasi:vocabulary:disabled-reason`: the shipped | |
| 331 | - | // button says this to a pointer and nothing else can. | |
| 332 | - | group = group.with(Node::text( | |
| 333 | - | "Preview the slices first to see how many will be created.", | |
| 334 | - | )); | |
| 335 | - | go = go.disabled(); | |
| 336 | - | } else { | |
| 337 | - | go = live(forging, go); | |
| 441 | + | stuck: forging.slices == 0 || forging.busy, | |
| 442 | + | unpreviewed: forging.slices == 0, | |
| 338 | 443 | } | |
| 339 | - | ||
| 340 | - | group.with(Node::Act(go)).with(Node::text( | |
| 341 | - | "Slices are written into a new folder beside this sample.", | |
| 342 | - | )) | |
| 343 | - | } | |
| 344 | - | ||
| 345 | - | /// Making one sample fit a piece of hardware. | |
| 346 | - | fn conforming(forging: &Forging) -> Slot { | |
| 347 | - | let group = Slot::new("forge-conform", RegionKind::Group); | |
| 348 | - | ||
| 349 | - | if forging.devices.is_empty() { | |
| 350 | - | return group | |
| 351 | - | .with(Node::section("Conform for device")) | |
| 352 | - | .with(Node::empty("No device profiles available.")); | |
| 353 | - | } | |
| 354 | - | ||
| 355 | - | let mut field = Field::select( | |
| 356 | - | DEVICE, | |
| 357 | - | "Conform for device", | |
| 358 | - | forging | |
| 359 | - | .devices | |
| 360 | - | .iter() | |
| 361 | - | .map(|device| Choice::new(device.name.clone(), describe(device))) | |
| 362 | - | .collect(), | |
| 363 | - | ) | |
| 364 | - | .writes(Action::post("/forge/device")); | |
| 365 | - | // The instruction is the picker's ghost text rather than a disabled button's | |
| 366 | - | // job, which is the call the shipped screen already made: a select with | |
| 367 | - | // nothing chosen reads as an empty box, and the greyed control beside it is | |
| 368 | - | // the wrong place to explain that. | |
| 369 | - | field.placeholder = Some("Select device...".to_owned()); | |
| 370 | - | field.value = Some(forging.device.clone().unwrap_or_default()); | |
| 371 | - | ||
| 372 | - | let mut go = Act::new("Conform", Action::post("/forge/conform")); | |
| 373 | - | if forging.device.is_none() { | |
| 374 | - | go = go.disabled(); | |
| 375 | - | } else { | |
| 376 | - | go = live(forging, go); | |
| 377 | - | } | |
| 378 | - | ||
| 379 | - | group | |
| 380 | - | .with(Node::Field(Box::new(field))) | |
| 381 | - | .with(Node::Act(go)) | |
| 382 | - | .with(Node::text( | |
| 383 | - | "Resamples and converts bit depth to match the device, as a new sample.", | |
| 384 | - | )) | |
| 385 | - | } | |
| 386 | - | ||
| 387 | - | /// The one operation here that is about the selection rather than the sample. | |
| 388 | - | fn batching(forging: &Forging) -> Slot { | |
| 389 | - | let group = Slot::new("forge-batch", RegionKind::Group).with(Node::section("Batch")); | |
| 390 | - | ||
| 391 | - | if forging.chosen < 2 { | |
| 392 | - | return group.with(Node::empty("Select 2+ samples to batch trim silence.")); | |
| 393 | - | } | |
| 394 | - | ||
| 395 | - | group | |
| 396 | - | .with(Node::Field(Box::new( | |
| 397 | - | Field::range(Knob::Threshold.as_str(), "Threshold", "-96", "-20") | |
| 398 | - | .unit("dBFS") | |
| 399 | - | .step("1") | |
| 400 | - | .value(format!("{:.0}", forging.threshold_db)) | |
| 401 | - | .writes(knob_route(Knob::Threshold)), | |
| 402 | - | ))) | |
| 403 | - | .with(Node::Act(live( | |
| 404 | - | forging, | |
| 405 | - | Act::new( | |
| 406 | - | format!("Trim silence on {} samples", forging.chosen), | |
| 407 | - | Action::post("/forge/trim"), | |
| 408 | - | ), | |
| 409 | - | ))) | |
| 410 | 444 | } | |
| 411 | 445 | ||
| 412 | 446 | /// What a device profile says about itself, as one option. | |
| @@ -418,43 +452,148 @@ | |||
| 418 | 452 | } | |
| 419 | 453 | } | |
| 420 | 454 | ||
| 421 | - | /// A number the slicing reads, live unless a run is in flight. | |
| 422 | - | fn dial(forging: &Forging, field: Field) -> Node { | |
| 423 | - | let _ = forging; | |
| 424 | - | let name = field.name.clone(); | |
| 425 | - | Node::Field(Box::new( | |
| 426 | - | field.writes(Action::post(format!("/forge/set/{name}"))), | |
| 427 | - | )) | |
| 455 | + | declare! { | |
| 456 | + | /// The window. | |
| 457 | + | /// | |
| 458 | + | /// One shape rather than several, which is the header's argument: the | |
| 459 | + | /// shipped window keeps drawing every control while a chop or a conform is | |
| 460 | + | /// in flight and greys them, because the sample is still the subject and | |
| 461 | + | /// nothing has been arrived at. | |
| 462 | + | shape screen(forge: &Forge) -> Screen; | |
| 463 | + | ||
| 464 | + | screen sidebar_content "Sample Forge" { | |
| 465 | + | include loaded(forge); | |
| 466 | + | } | |
| 428 | 467 | } | |
| 429 | 468 | ||
| 430 | - | /// A handful of values that do not fold away, asked as a question. | |
| 431 | - | /// | |
| 432 | - | /// Five slice counts and three subdivisions are one choice each, written | |
| 433 | - | /// immediately. Being a field is what gives them a label; a bare strip of | |
| 434 | - | /// options had to borrow one from a section heading above. | |
| 435 | - | fn strip( | |
| 436 | - | knob: Knob, | |
| 437 | - | label: &str, | |
| 438 | - | chosen: &str, | |
| 439 | - | options: impl IntoIterator<Item = (String, String)>, | |
| 440 | - | ) -> Node { | |
| 441 | - | let options = options | |
| 442 | - | .into_iter() | |
| 443 | - | .map(|(value, label)| Choice::new(value, label)) | |
| 444 | - | .collect(); | |
| 445 | - | Node::Field(Box::new( | |
| 446 | - | Field::radio(knob.as_str(), label, options) | |
| 447 | - | .value(chosen) | |
| 448 | - | .writes(knob_route(knob)), | |
| 449 | - | )) |
Lines truncated