//! The state a terminal owns because nothing else will. //! //! This type is the answer to `39057019`, and the finding is worth restating //! because the answer only makes sense next to it. `Field::value` is what a //! handler re-offers after a refused write. It is not what is in the box right //! now, and for a [`layout::FieldKind::Secret`] it is nothing at all, on //! purpose: a password that comes back down the wire is a password in a page //! and in a proxy log. A browser never made anyone notice, because a browser //! owns the contents of an `` and redraws it on every keystroke without //! asking the description for permission. //! //! A terminal owns nothing. So the drawing of an editable screen is not a //! function of the description alone, and the two ways to admit that were: //! hand the renderer a second argument, or have the runtime rewrite the //! description before drawing it. //! //! **The second argument won.** Rewriting keeps [`crate::Tui`] a pure function //! of one argument by making the runtime lie about what the handler said, and //! the lie is not free: `Field::value` refuses to hold a secret, so a runtime //! that wrote the typed password into the description would have had to defeat //! that refusal to draw the dots. The guarantee that no renderer emits a secret //! is worth more than the pure signature, and this way the two facts stay //! separate: the description says what the server offers, and this says what the //! user has done since. //! //! Once it exists it holds the rest of what the browser was quietly providing, //! because it turns out to be the same discovery four times: what is typed, //! what has focus, how far a pane is scrolled, and where the back button goes. //! None of the four is in a description and none of them should be. //! //! An overlay holds a second one of these rather than a fifth field being added //! to this one. `Outcome::Over` draws a whole screen over another, and the //! screen underneath keeps its own reach, focus, edits and scroll while it is //! covered: sharing one `View` between the two would mean dismissing a palette //! took the user's typing and scroll position with it. See `Runtime`'s `under` //! stack, which holds the pair. //! //! One more is of the same kind and is deliberately not held here: where the //! caret sits inside a field. `d52884b0`, decided 2026-08-12. It belongs on //! this list by nature, and it is absent because no described screen //! needs it yet: the one measured consumer is goingson's `search.js`, whose //! completion list depends on which token the caret is inside, and that file //! stays JS. Saying so here keeps the boundary explicit, so the next screen that //! wants caret-dependent completion knows this is where it would land rather //! than re-asking whether a description should carry one. It should not. use std::collections::{BTreeMap, BTreeSet}; use makeover_layout as layout; use quasi_router::{Params, Screen}; use crate::focus::{FieldSpot, Spot}; /// What the user has done to a screen since it arrived. /// /// A host makes one beside the screen it is holding and keeps the two together. /// Empty is the honest starting state and it draws exactly what the description /// says, which is what every test that predates this passes. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct View { /// What has been typed, by [`Field::name`](quasi_router::Field::name). /// /// Absent means untouched, which is different from present and empty: one /// draws the description's value and the other draws a box the user has /// cleared. edits: BTreeMap, /// Which reached thing has focus, as an index into [`crate::focus::spots`]. /// /// Focus is this renderer's and lives here rather than in a description, /// which is why it survives a redraw: reach is recomputed from the screen, /// focus is a fact about where the user has walked. See /// [`crate::focus`]'s header for the three terms. focus: usize, /// How far each region has been scrolled, in rows, by /// [`Slot::id`](quasi_router::Slot::id). scroll: BTreeMap, /// What has been ticked, by [`Row::value`](quasi_router::Row::value). /// /// `5f2b8753`. The fifth thing the browser was quietly providing, and it /// arrived last because it is the one a browser does *not* fully provide: /// a checkbox owns its own checked state, but nothing gathers the boxes /// back up, so every app wrote that part by hand. Here there is no /// checkbox to own anything, which is what made the hole visible. /// /// A set rather than a map from name to bool. Absent is not ticked, and /// the two spellings of that would otherwise drift. /// /// Only the current screen's set, because a screen names one /// ([`Screen::selection`](quasi_router::Screen::selection)) and a new /// screen is a new set. Which set it is does not need storing: the screen /// beside this one says. ticked: BTreeSet, } impl View { /// Nothing typed, the first thing focused, nothing scrolled. #[must_use] pub fn new() -> Self { Self::default() } /// What is in the box: what has been typed, or what the description offers, /// or nothing. /// /// The order is the whole of the type's job. An untouched field shows what /// the handler put there; a touched one shows what the user did, including /// when what they did was empty it. #[must_use] pub fn typed<'a>(&'a self, field: &'a FieldSpot) -> &'a str { self.showing(&field.name, field.value.as_deref()) } /// [`typed`](Self::typed) for a caller holding the described field itself /// rather than a walk's record of it, which is what the drawing has. #[must_use] pub fn showing<'a>(&'a self, name: &str, described: Option<&'a str>) -> &'a str { self.edits .get(name) .map(String::as_str) .or(described) .unwrap_or_default() } /// What has been typed into a field by name, if anything has. #[must_use] pub fn edit(&self, name: &str) -> Option<&str> { self.edits.get(name).map(String::as_str) } /// Put a value in a box. pub fn set(&mut self, name: impl Into, value: impl Into) { self.edits.insert(name.into(), value.into()); } /// Add a character to a box, starting from whatever is showing in it. pub fn push(&mut self, field: &FieldSpot, ch: char) { let mut value = self.typed(field).to_string(); value.push(ch); self.set(&field.name, value); } /// Take the last character back out of a box. pub fn backspace(&mut self, field: &FieldSpot) { let mut value = self.typed(field).to_string(); value.pop(); self.set(&field.name, value); } /// Which reachable thing has focus. #[must_use] pub const fn focus(&self) -> usize { self.focus } /// Move focus by `steps`, wrapping at both ends. /// /// Wrapping rather than stopping, because a terminal has no scrollbar to /// tell you that you are at the end of the reachable things and pressing tab /// against a dead stop reads as a broken key. pub fn advance(&mut self, steps: isize, reachable: usize) { if reachable == 0 { self.focus = 0; return; } let count = reachable as isize; let at = self.focus.min(reachable - 1) as isize; self.focus = (at + steps).rem_euclid(count) as usize; } /// Focus something in particular, if it is there. pub fn focus_on(&mut self, at: usize, reachable: usize) { if at < reachable { self.focus = at; } } /// How far a region has been scrolled. #[must_use] pub fn scroll(&self, region: &str) -> u16 { self.scroll.get(region).copied().unwrap_or(0) } /// Scroll a region, never above its top. /// /// There is no bottom stop here, and that is deliberate: how far a region /// can scroll is how tall its content is at the width it was given, which /// is a fact the drawing knows and this does not. [`crate::Tui::clamp`] is /// where it gets trimmed, once per draw, with the rect in hand. pub fn scroll_by(&mut self, region: &str, rows: i32) { let at = i32::from(self.scroll(region)); let next = u16::try_from((at + rows).max(0)).unwrap_or(u16::MAX); self.scroll.insert(region.to_string(), next); } /// Hold a region at this offset. pub fn scrolled_to(&mut self, region: &str, rows: u16) { self.scroll.insert(region.to_string(), rows); } /// Whether this value is ticked. #[must_use] pub fn is_ticked(&self, value: &str) -> bool { self.ticked.contains(value) } /// Tick it if it is not, untick it if it is. /// /// Staging, never a write. Wiki `explicit-commit-affordance`: the commit /// control is what locks a change in, and a tick that wrote on its own /// would be the change happening with nothing to mark it. pub fn tick(&mut self, value: &str) { if !self.ticked.remove(value) { self.ticked.insert(value.to_owned()); } } /// Everything ticked, in order. /// /// Ordered because it is a `BTreeSet`, and that is worth relying on: a /// handler reading [`Params::get_all`] gets the same sequence every run, so /// a test over a bulk action is not sorting the answer first. pub fn ticks(&self) -> impl Iterator { self.ticked.iter().map(String::as_str) } /// Start the described ticks off, for the rows that arrive already ticked. /// /// A description can say a row is ticked, and on a screen that has just /// arrived that claim is the only thing there is. Applied on arrival rather /// than read on every draw, because after that the user's ticks are the /// truth and a redraw that went back to the description would undo them. pub fn seed(&mut self, screen: &Screen) { self.ticked = crate::focus::spots(screen) .iter() .filter_map(|spot| match spot { Spot::Row { ticked: Some(true), value: Some(value), .. } => Some(value.clone()), _ => None, }) .collect(); } /// Forget everything typed and scrolled, and go back to the top. /// /// What a whole new screen means. The boxes on it are different boxes, and /// carrying a buffer across would put what was typed into a password field /// into whatever field happens to share its name on the next screen. A /// selection goes the same way and for the same reason: the rows are /// different rows. pub fn reset(&mut self) { self.edits.clear(); self.scroll.clear(); self.ticked.clear(); self.focus = 0; } /// The values a form submits, gathered for `names` in the order given. /// /// A checkbox is here by presence, the way HTML submits one, so a box that /// is not ticked sends nothing rather than sending an empty string. That is /// [`Field::value`](quasi_router::Field::value)'s own convention read back /// out. #[must_use] pub fn submission(&self, names: &[String], spots: &[Spot]) -> Params { let mut params = Params::new(); for name in names { let Some(field) = spots .iter() .filter_map(Spot::field) .find(|field| &field.name == name) else { continue; }; let value = self.typed(field); if matches!(field.kind, layout::FieldKind::Checkbox) && value != quasi_router::Node::SELECTED { continue; } params.insert(name.clone(), value.to_string()); } params } /// Drop anything held for a field the screen no longer has. /// /// A fragment can replace a region holding half a form, and the buffers for /// the fields that went away would otherwise ride along and be submitted by /// the next form that happens to name one of them. pub fn prune(&mut self, screen: &Screen) { let spots = crate::focus::spots(screen); let live: Vec<&str> = spots .iter() .filter_map(Spot::field) .map(|field| field.name.as_str()) .collect(); self.edits.retain(|name, _| live.contains(&name.as_str())); self.focus = self.focus.min(spots.len().saturating_sub(1)); } }