//! 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,
/// Which child each region is showing, by
/// [`Slot::id`](quasi_router::Slot::id).
///
/// `4dcd241b`, and the sixth of the same discovery. A description says a
/// region shows one of its children at a time and says which one it started
/// on; where the reader has moved to since is this renderer's, exactly as
/// [`scroll`](Self::scroll) is. A browser owns this too and never made
/// anyone notice, because moving a carousel there is a class on an element
/// the document already holds.
///
/// Absent means the description's own answer still stands, which is what
/// makes an untouched screen draw what the handler said.
shown: BTreeMap,
}
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);
}
/// Which child a region is showing, given what its description says.
///
/// [`scroll`](Self::scroll)'s shape with one difference: a scroll has an
/// obvious zero and this does not, so the description's own answer is the
/// floor rather than the top of the region.
#[must_use]
pub fn shown(&self, slot: &quasi_router::Slot) -> Option {
match self.shown.get(&slot.id) {
Some(at) => Some((*at).min(slot.body.len().saturating_sub(1))),
None => slot.current(),
}
}
/// Move a region to another of its children, wrapping at both ends.
///
/// Wrapping for [`advance`](Self::advance)'s reason: a terminal has nothing
/// to show you that you are at the last frame, so a next key that stops
/// dead reads as a broken key rather than as the end of the gallery.
///
/// A closed dismissible region opens on its first child, which is the only
/// reading of "next" that does anything from closed.
pub fn show_by(&mut self, slot: &quasi_router::Slot, steps: isize) {
let count = slot.body.len();
if count == 0 {
return;
}
let at = match self.shown(slot) {
Some(at) => (at as isize + steps).rem_euclid(count as isize) as usize,
None => 0,
};
self.shown.insert(slot.id.clone(), at);
}
/// Show a particular child of a region.
pub fn show(&mut self, region: &str, at: usize) {
self.shown.insert(region.to_string(), at);
}
/// 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.shown.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));
}
}