//! The Sample Forge, described: three things you can make out of one sample. //! //! The fourteenth port and one of the two smallest, which is why it is worth //! being clear about what it is not. `ui/forge_panel.rs` is 346 lines and the //! description is not much shorter, because almost none of those lines are //! layout: the window is three sections of live controls over one sample, and //! the port is a straight reading of what each control asks. //! //! # One shape, not several, and it is the first screen here that is //! //! Every flow ported before this answers a different screen per state. //! [`export`](super::export) has five, [`importing`](super::importing) nine, //! [`edit`](super::edit) two. This has one: the shipped window keeps drawing //! every control while a chop or a conform is in flight and greys them, because //! the sample is still the subject and nothing has been arrived at. So `busy` is //! a field on [`Forging`](super::Forging) rather than a shape, and it is what //! deadens the controls -- as far as the vocabulary lets it, which turns out to //! be the acts and not the fields. See the finding below. //! //! That is the rule stated from the other side for once, and it is worth having //! both halves written down: **a state a reader arrived at is a shape, and a //! property of the subject is a field.** "An export is running" is the first; //! "this sample is busy" is the second. //! //! # What the description deletes: the fourth piggyback //! //! `state.forge.conform_device` is an `Option` written back out of the //! draw every frame, with the empty string meaning nothing chosen and a comment //! saying so. It is a buffer for a picker, which is [`bulk`](super::bulk)'s //! eleven `BulkModal` fields and [`edit`](super::edit)'s twelve knobs for the //! fourth time: what is being chosen in a described screen is the runtime's, and //! it arrives with the act that used it. //! //! `slice_marks` is **not** in that class and stays. It is the result of work the //! app did, not a control's buffer, and the description carries what the controls //! need of it — how many slices a preview found — as //! [`Forging::slices`](super::Forging::slices). //! //! # THE FINDING: a field cannot be disabled at all //! //! [`Act`](quasi_router::Act) carries a //! [`State`](quasi_router::layout::State) and //! [`Act::disabled`](quasi_router::Act::disabled) sets it. //! [`Field`](quasi_router::Field) carries no such member: its fourteen fields //! are kind, name, label, hint, error, placeholder, options, required, //! max_length, min, max, step, extended and width, and none of them is "not //! answering right now". //! //! Every control in this window is greyed while a chop or a conform runs, which //! is nine `add_enabled(!disabled, ..)` calls in the shipped file. The acts can //! say it and **the sensitivity slider, the BPM dial, the device picker and the //! trim threshold cannot**, so a described forge mid-run offers four live-looking //! controls whose writes the routes then have to refuse. //! //! This is sharper than the neighbouring gap rather than the same one: //! `quasi:vocabulary:disabled-reason` is a control that says it is dead without //! saying why, and this is a control with no way to say it is dead. Filed as //! `quasi:vocabulary:field-state`, four consumers in this one window. The //! degradation is the routes, which refuse the write and are what the reader //! would have been stopped from making. //! //! # A second consumer for `quasi:vocabulary:disabled-reason` //! //! Chop is disabled until a preview has run, and the shipped button explains //! itself in an `on_disabled_hover_text`: "Preview the slices first to see how //! many will be created." That is the gap [`importing`](super::importing) filed //! this pass with four consumers of its own — //! [`Act::disabled`](quasi_router::Act::disabled) carries no reason where //! [`Choice::unless`](quasi_router::Choice::unless) does — and this is the fifth. //! Degraded the same way: the sentence is a line of its own beside the control. //! //! The count on the label survives, and it is the better half of that button //! anyway. "Chop into 14 slices" says the blast radius before the press, which //! is the correction the shipped screen made to itself (AF-9) and the same one //! the review screen's "Apply 3 Tags" is. //! //! # What is deliberately not described //! //! - **The waveform and its slice markers.** [`edit`](super::edit)'s exclusion, //! unchanged and for its reason: a rendered picture of samples, with lines //! painted over it at pixel positions derived from fractions. Domain //! rendering. What the description keeps is the number of slices, which is the //! only thing any control here reads off it. //! - **The plugin-host foreshadow.** "Plugin processing (CLAP/VST): coming soon" //! is marketing copy for something that does not exist, and a description of a //! screen should not carry a description of a screen that has not been built. //! The shipped window may keep it; there is nothing to port. use quasi_router::layout::{Selector, Tone}; use quasi_router::{ Act, Action, Choice, Field, Node, RegionKind, Request, Response, RouteError, Router, Screen, Slot, }; use super::{Chop, DeviceChoice, Forging, Knob, Panels}; /// The region the window answers into. const BODY: &str = "forge-body"; /// The name the device picker submits under. const DEVICE: &str = "device"; /// Register the forge's routes. pub fn routes(router: Router>) -> Router> { router .get("/forge", index) .post("/forge/slice/{how}", slice_by) .post("/forge/set/{knob}", turn) .post("/forge/preview", preview) .post("/forge/chop", chop) .post("/forge/device", choose_device) .post("/forge/conform", conform) .post("/forge/trim", trim_silence) } /// `GET /forge` fn index(state: &Panels<'_>, _request: Request) -> Result { Ok(screen(state).into()) } /// `POST /forge/slice/{how}` fn slice_by(state: &Panels<'_>, request: Request) -> Result { let name = request.captures.require("how")?; let how = Chop::from_key(name).ok_or_else(|| RouteError::not_found("no such chop method"))?; state.forge.slice_by(how); Ok(screen(state).into()) } /// `POST /forge/set/{knob}` /// /// One route for five controls across two sections, which is [`export`]'s /// arrangement and [`Knob`](super::Knob) is what closes the set. /// /// [`export`]: super::export fn turn(state: &Panels<'_>, request: Request) -> Result { let name = request.captures.require("knob")?; let knob = Knob::from_key(name).ok_or_else(|| RouteError::not_found("no such control"))?; let value = request .payload .get(name) .or_else(|| request.payload.get(Node::SELECTED)) .unwrap_or_default(); state.forge.turn(knob, value); Ok(screen(state).into()) } /// `POST /forge/preview` fn preview(state: &Panels<'_>, _request: Request) -> Result { forging(state)?; state.forge.preview(); Ok(screen(state).into()) } /// `POST /forge/chop` /// /// Refused without a preview, which is what the shipped button is disabled on /// and for the reason it was made to be (AF-9): committing to an unknown slice /// count is the thing the preview exists to stop. Changing any chop parameter /// clears the marks, so the gate re-arms itself. fn chop(state: &Panels<'_>, _request: Request) -> Result { let forging = forging(state)?; if forging.slices == 0 { return Err(RouteError::not_found("preview the slices first")); } state.forge.chop(); Ok(screen(state).into()) } /// `POST /forge/device` /// /// An empty value is "nothing chosen" rather than a device named the empty /// string, which is the reading the shipped write-back makes. A name no profile /// carries is a refusal: the address is reachable by typing. fn choose_device(state: &Panels<'_>, request: Request) -> Result { let forging = forging(state)?; let chosen = request .payload .get(DEVICE) .or_else(|| request.payload.get(Node::SELECTED)) .unwrap_or_default(); if !chosen.is_empty() && !forging.devices.iter().any(|device| device.name == chosen) { return Err(RouteError::not_found("no such device profile")); } state.forge.choose_device(chosen); Ok(screen(state).into()) } /// `POST /forge/conform` fn conform(state: &Panels<'_>, _request: Request) -> Result { let forging = forging(state)?; if forging.device.is_none() { return Err(RouteError::not_found("choose a device first")); } state.forge.conform(); Ok(screen(state).into()) } /// `POST /forge/trim` /// /// Refused under two samples, which is what the shipped section is hidden /// behind: trimming a batch of one is the single-sample operation wearing the /// batch's label. fn trim_silence(state: &Panels<'_>, _request: Request) -> Result { let forging = forging(state)?; if forging.chosen < 2 { return Err(RouteError::not_found("choose two or more samples")); } state.forge.trim_silence(); Ok(screen(state).into()) } /// The sample in the forge, refusing every write when there is none. fn forging(state: &Panels<'_>) -> Result { state .forge .forging() .ok_or_else(|| RouteError::not_found("no sample is in the forge")) } /// The window. fn screen(state: &Panels<'_>) -> Screen { let body = match state.forge.forging() { Some(forging) => loaded(&forging), None => Slot::new(BODY, RegionKind::Pane).with(Node::empty( "Select a sample and open the forge to chop, conform, or batch-process it.", )), }; Screen::sidebar_content("Sample Forge").with(body) } /// A sample, and the three things that can be made out of it. fn loaded(forging: &Forging) -> Slot { let mut body = Slot::new(BODY, RegionKind::Pane) .with(Node::page(forging.name.clone())) .with(Node::text(format!("{} Hz", forging.rate))); // Said rather than drawn as a spinner beside a separator: what a reader // needs from it is that every control below is currently inert, and the // controls say that themselves through `State::Disabled`. if forging.busy { body = body.with(Node::banner(Tone::Info, "Working...")); } body = body.with(Node::Region(chopping(forging))); body = body.with(Node::Region(conforming(forging))); body.with(Node::Region(batching(forging))) } /// Slicing one sample into several. fn chopping(forging: &Forging) -> Slot { let mut group = Slot::new("forge-chop", RegionKind::Group) .with(Node::section("Chop")) .with(Node::Select { kind: Selector::Segmented, options: Chop::ALL .into_iter() .map(|how| (Choice::new(how.as_str(), how.label()), None)) .collect(), chosen: Some(forging.how.as_str().to_owned()), action: Some(Action::post(format!( "/forge/slice/{}", forging.how.as_str() ))), }); // Only the parameters the chosen method reads, which is the shipped // window's own `match` and the settings screen's line: a control that // cannot be used is worse than one that is not there. group = match forging.how { Chop::Transient => group.with(dial( forging, Field::range(Knob::Sensitivity.as_str(), "Sensitivity", "0", "1") .step("0.01") .value(format!("{:.2}", forging.sensitivity)), )), Chop::Equal => group.with(strip( forging, Knob::Divisions, &forging.divisions.to_string(), [2_usize, 4, 8, 16, 32].map(|n| (n.to_string(), n.to_string())), )), Chop::Bpm => group .with(dial( forging, Field::range(Knob::Bpm.as_str(), "BPM", "20", "300") .step("0.5") .value(format!("{:.1}", forging.bpm)), )) .with(strip( forging, Knob::Subdivisions, &forging.subdivisions.to_string(), [("1", "1/4"), ("2", "1/8"), ("4", "1/16")] .map(|(value, label)| (value.to_owned(), label.to_owned())), )), }; group = group.with(Node::Act(live( forging, Act::new("Preview slices", Action::post("/forge/preview")), ))); // The count on the label, which is the correction the shipped button made to // itself: a commit says its blast radius before it is pressed. let mut go = Act::new( if forging.slices == 0 { "Chop".to_owned() } else { format!( "Chop into {} slice{}", forging.slices, if forging.slices == 1 { "" } else { "s" } ) }, Action::post("/forge/chop"), ); if forging.slices == 0 { // The fifth consumer of `quasi:vocabulary:disabled-reason`: the shipped // button says this to a pointer and nothing else can. group = group.with(Node::text( "Preview the slices first to see how many will be created.", )); go = go.disabled(); } else { go = live(forging, go); } group.with(Node::Act(go)).with(Node::text( "Slices are written into a new folder beside this sample.", )) } /// Making one sample fit a piece of hardware. fn conforming(forging: &Forging) -> Slot { let group = Slot::new("forge-conform", RegionKind::Group); if forging.devices.is_empty() { return group .with(Node::section("Conform for device")) .with(Node::empty("No device profiles available.")); } let mut field = Field::select( DEVICE, "Conform for device", forging .devices .iter() .map(|device| Choice::new(device.name.clone(), describe(device))) .collect(), ) .changes(Action::post("/forge/device")); // The instruction is the picker's ghost text rather than a disabled button's // job, which is the call the shipped screen already made: a select with // nothing chosen reads as an empty box, and the greyed control beside it is // the wrong place to explain that. field.placeholder = Some("Select device...".to_owned()); field.value = Some(forging.device.clone().unwrap_or_default()); let mut go = Act::new("Conform", Action::post("/forge/conform")); if forging.device.is_none() { go = go.disabled(); } else { go = live(forging, go); } group .with(Node::Field(Box::new(field))) .with(Node::Act(go)) .with(Node::text( "Resamples and converts bit depth to match the device, as a new sample.", )) } /// The one operation here that is about the selection rather than the sample. fn batching(forging: &Forging) -> Slot { let group = Slot::new("forge-batch", RegionKind::Group).with(Node::section("Batch")); if forging.chosen < 2 { return group.with(Node::empty("Select 2+ samples to batch trim silence.")); } group .with(Node::Field(Box::new( Field::range(Knob::Threshold.as_str(), "Threshold", "-96", "-20") .unit("dBFS") .step("1") .value(format!("{:.0}", forging.threshold_db)) .changes(writes(Knob::Threshold)), ))) .with(Node::Act(live( forging, Act::new( format!("Trim silence on {} samples", forging.chosen), Action::post("/forge/trim"), ), ))) } /// What a device profile says about itself, as one option. fn describe(device: &DeviceChoice) -> String { if device.summary.is_empty() { device.name.clone() } else { format!("{} ({})", device.name, device.summary) } } /// A number the slicing reads, live unless a run is in flight. fn dial(forging: &Forging, field: Field) -> Node { let _ = forging; let name = field.name.clone(); Node::Field(Box::new( field.changes(Action::post(format!("/forge/set/{name}"))), )) } /// A handful of values that do not fold away. /// /// `Selector::Segmented` rather than a `Field`, which is the line `settings.rs` /// drew and the shipped window agrees with: five slice counts and three /// subdivisions are drawn as rows of selectable buttons, and naming "exactly one /// of these few" is describing the choice rather than choosing the widget. fn strip( forging: &Forging, knob: Knob, chosen: &str, options: impl IntoIterator, ) -> Node { let _ = forging; Node::Select { kind: Selector::Segmented, options: options .into_iter() .map(|(value, label)| (Choice::new(value, label), None)) .collect(), chosen: Some(chosen.to_owned()), action: Some(writes(knob)), } } /// The address a control changing this number calls. fn writes(knob: Knob) -> Action { Action::post(format!("/forge/set/{}", knob.as_str())) } /// The same control, dead while a run is in flight. fn live(forging: &Forging, act: Act) -> Act { if forging.busy { act.disabled() } else { act } }