//! 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. 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 std::time::Instant; use makeover_layout as layout; use quasi_router::{Action, Candidate, Chrome, Frame, Params, Screen}; use crate::focus::{FieldSpot, Spot}; use crate::{Hidden, Local}; /// The host's side of an outstanding wait. /// /// Two `Option`s rather than an `Option` of a pair, because they arrive at /// different moments: the clock starts when the call goes out and a delivery /// count arrives only if something is watching. Most waits never get one. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] struct Awaited { /// When the call went out. since: Option, /// How much has arrived, in the unit the description counted. delivered: Option, } /// 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, /// The value a field's [`writes`](quasi_router::Field::writes) last fired /// with, by field name. /// /// `changes` fires when the value is complete rather than on every /// keystroke, so "complete" needs a baseline: leaving a box nobody altered /// must not write, which is what a browser's `change` already promises and /// what `quasi-webview` therefore already does. /// /// Beside `edits` rather than derived from it, because they answer different /// questions: `edits` is what is in the box and this is what the route has /// been told. written: 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). /// /// 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 branches the reader has folded or unfolded, by /// [`Outline::key`](quasi_router::Outline::key). /// /// And the same discovery as [`shown`](Self::shown) one node kind along: /// the description says a branch is open and that is where the outline /// starts, not where it stays. A browser owns this too and never made /// anyone notice, because folding a row there is an attribute on markup /// the document already holds. /// /// Absent means the description's own answer still stands, so a screen /// arriving with a branch shut draws it shut until the reader says /// otherwise. opened: BTreeMap, /// Which child each region is showing, by /// [`Slot::id`](quasi_router::Slot::id). /// /// 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, /// The action this screen is waiting on, when one is outstanding. /// /// And the seventh of the same discovery: a control that has been pressed /// and has not been answered yet is a fact about this moment, which is why /// it lives here and not in the description. A browser owns it too, and /// htmx expresses it as a class on the element that made the request. /// /// Only an [`Action`] carrying [`Action::awaiting`] ever lands here. The /// rest resolve fast enough that a terminal drawing them busy would be a /// flicker, and the description says which those are. outstanding: Option, /// When the outstanding call went out, and how much of it has landed. /// /// Wiki `loading-and-progress-standard`, rule 1. `Action::awaiting` carries /// the size of the payload; nothing can describe how much of it has arrived, /// because that is a fact about bytes in flight. So it is the host's to /// report and it lands here beside the action it belongs to. /// /// Both `None` for a host that is not watching, which is the honest common /// case rather than a gap. A bar drawn out of a total alone would be /// claiming somebody is counting. progress: Awaited, /// Which control inside the focused table row the caret has stepped onto, /// as an index into [`Spot::Row`]'s `inside`. /// /// And the eighth of the same discovery. `None` is the ordinary state: the /// caret is on the row itself, which is every stop on every other kind of /// node. A table row is the one stop that has an inside, because /// `makeover_tui::table` answers no coordinates back and its cells /// therefore cannot be stops of their own; see [`crate::focus`]'s header. /// /// Held beside [`focus`](Self::focus) rather than folded into it, so that /// nothing counting reachable things has to know a table is different. Every /// walk in the crate still sees one number per row. inside: Option, /// The candidates a field's suggestion route answered with, and which one /// the caret is on. /// /// And the ninth of the same discovery this type's header lists: what a /// route has just suggested is a fact about this moment, not about the /// screen the handler described. A browser owns it as a list of elements /// in the document; a terminal owns nothing, so it is here. /// /// One at a time, keyed by field name rather than a map of every field's /// list. A list belongs to the box being typed into, and there is one of /// those: keeping a second field's stale candidates would draw a list under /// a box nobody is in. suggesting: Option, /// How many slots a repeating question stands in, by /// [`Field::name`](quasi_router::Field::name), where the reader has changed /// it. /// /// And the same discovery as everything else here: the description says /// how many answers it was given, and how many boxes there are *now* is a /// fact about what the reader has done since. A browser owns it as /// elements in the document, so nobody had to name it there; there is no /// document here. /// /// Absent means the description's own count still stands, which is what /// makes an untouched screen draw what the handler said. slots: BTreeMap, } /// One field's open suggestion list. /// /// [`View::suggesting`]. The candidates as the route answered them, plus which /// one the caret is on — nothing else, because everything else about the list /// is the description's or the drawing's. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Suggesting { /// The [`Field::name`](quasi_router::Field::name) whose list this is. pub field: String, /// The candidates, in the order the route offered them. pub options: Vec, /// Which one the caret is on, or `None` before an arrow has been pressed. /// /// Nothing highlighted is the state a list arrives in, and it is why Enter /// belongs to the form until an arrow has been pressed: a list that /// highlighted its first entry on arrival would take the Enter that submits /// a form the moment the user paused typing. pub at: Option, } impl View { /// Nothing typed, the first thing focused, nothing scrolled. #[must_use] pub fn new() -> Self { Self::default() } /// How many slots this repeating question stands in right now. /// /// `described` is what the description offered, which is /// [`Field::slots`](quasi_router::Field::slots). What the reader has added /// or taken away wins, and the answer is held to the question's own floor /// and ceiling so that no walk can offer a slot the description refuses. #[must_use] pub fn standing(&self, field: &quasi_router::Field) -> usize { let Some(repeat) = &field.repeats else { return 1; }; let described = repeat.standing(); let count = self.slots.get(&field.name).copied().unwrap_or(described); count .max(repeat.least) .min(repeat.most.unwrap_or(usize::MAX)) } /// Add a slot to a repeating question, if its ceiling allows another. /// /// No request, which is the third of the three things the member is for: /// the reader creates and destroys slots, and the box that appears is a box /// nothing was asked for. pub fn add_slot(&mut self, field: &quasi_router::Field) { let Some(repeat) = &field.repeats else { return; }; let standing = self.standing(field); if !repeat.more(standing) { return; } self.slots.insert(field.name.clone(), standing + 1); } /// Take one slot out of a repeating question, if its floor allows one /// fewer. /// /// The answers after it move up, buffer and all, because the names are /// positional: leaving them where they were would submit a hole under the /// name the reader had just emptied and drop the last answer off the end. /// That is the same renumbering `repeat.js` does in the browser. pub fn remove_slot(&mut self, field: &quasi_router::Field, at: usize) { let Some(repeat) = &field.repeats else { return; }; let standing = self.standing(field); if at >= standing || !repeat.fewer(standing) { return; } // Every name the slot submits under, which is one for an ordinary // repeating question and one per part for a grouped one. Read off // `Field::instance_fields` rather than spelled here, so this moves the // same names the drawing and the caret walk use. for slot in at..standing - 1 { for (here, next) in field .instance_fields(slot) .iter() .zip(field.instance_fields(slot + 1).iter()) { let carried = self .edits .get(&next.name) .cloned() .or_else(|| next.value.clone()); match carried { Some(value) => { self.edits.insert(here.name.clone(), value); } None => { self.edits.remove(&here.name); } } } } for last in field.instance_fields(standing - 1) { self.edits.remove(&last.name); } self.slots.insert(field.name.clone(), standing - 1); } /// 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()) } /// The action this view is waiting on. /// /// What the drawing consults to mute a control that has been pressed, and /// what the runtime consults to refuse a second press of it. #[must_use] pub const fn outstanding(&self) -> Option<&Action> { self.outstanding.as_ref() } /// Say that this action is outstanding, or that nothing is. /// /// The runtime's to set. A host driving this crate without one is drawing a /// screen it never dispatched from, so there is nothing for it to say here. pub(crate) fn awaiting(&mut self, action: Option) { self.awaiting_at(action, Instant::now()); } /// [`awaiting`](Self::awaiting) with the clock handed in, for tests. pub(crate) fn awaiting_at(&mut self, action: Option, now: Instant) { self.progress = Awaited { since: action.as_ref().map(|_| now), delivered: None, }; self.outstanding = action; } /// Say how much of the outstanding call has arrived. /// /// The host's to call, as often as it likes, from whatever it is watching: /// bytes off a socket, rows out of an import. In the unit the description /// counted, which is the app's business either way -- see /// `makeover_layout::Awaiting::amount`. /// /// Ignored when nothing is outstanding. A delivery count with no wait /// attached would be drawn against the next call to go out, which is a /// number belonging to the wrong wait. pub fn delivered(&mut self, amount: u64) { if self.outstanding.is_some() { self.progress.delivered = Some(amount); } } /// What is known about the wait that is running, at `now`. /// /// Empty when nothing is outstanding, which is what makes the drawing /// degrade to the activity mark rather than to a bar of nothing. #[must_use] pub fn progress_at(&self, now: Instant) -> makeover_tui::piece::Progress { makeover_tui::piece::Progress { delivered: self.progress.delivered, elapsed: self .progress .since .map(|since| now.saturating_duration_since(since)), } } /// Whether this is the control that was pressed and has not been answered. #[must_use] pub fn busy(&self, action: &Action) -> bool { self.outstanding.as_ref() == Some(action) } /// [`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) } /// The open suggestion list, if it belongs to this field. /// /// Named rather than returned bare so a caller cannot draw one field's /// candidates under another's box. #[must_use] pub fn suggesting(&self, name: &str) -> Option<&Suggesting> { self.suggesting.as_ref().filter(|open| open.field == name) } /// The open suggestion list, whichever field owns it. /// /// What the drawing reads once the field that owns it has said where the /// list goes. [`suggesting`](Self::suggesting) is the question a caller /// holding a field asks; this is the one the drawing asks afterwards, and /// keeping them apart is what stops a list being drawn under the wrong box. #[must_use] pub fn suggesting_here(&self) -> Option<&Suggesting> { self.suggesting.as_ref() } /// What a field's suggestion route answered with. /// /// Replaces whatever was open, including a list belonging to another field: /// an answer is about the box being typed into, and the previous one is /// about a box the user has left. Highlight starts at nothing, for /// [`Suggesting::at`]'s reason. /// /// An empty answer closes the list rather than opening an empty one. A /// route with nothing to suggest and a route that was never asked leave the /// screen in the same state, which is the honest reading of both. pub(crate) fn suggested(&mut self, field: impl Into, options: Vec) { let field = field.into(); self.suggesting = (!options.is_empty()).then_some(Suggesting { field, options, at: None, }); } /// Move the highlight, wrapping, and skipping nothing. /// /// From nothing, one step forward lands on the first candidate and one back /// on the last, which is the terminal's own idiom and the webview's /// program's. pub(crate) fn highlight(&mut self, by: isize) { let Some(open) = self.suggesting.as_mut() else { return; }; let count = open.options.len(); if count == 0 { return; } let from = match open.at { Some(at) => at as isize, None if by > 0 => -1, None => 0, }; let there = (from + by).rem_euclid(count as isize); open.at = Some(there as usize); } /// Take the highlighted candidate, closing the list. /// /// `None` only when nothing is highlighted, which leaves the list open. /// /// The whole candidate rather than its value, because what picking does is /// not always to write: [`Candidate::picks`] carries an action, and only /// the caller can perform one. Absent one the caller writes /// [`Candidate::value`], which is the default and every site that exists /// today. /// /// [`Candidate`] carries no `unavailable`: a suggestion that cannot be /// picked is a row a route should not have offered. pub(crate) fn pick(&mut self) -> Option { let open = self.suggesting.as_ref()?; let candidate = open.options.get(open.at?)?.clone(); self.suggesting = None; Some(candidate) } /// Put the list away. pub(crate) fn unsuggest(&mut self) { self.suggesting = None; } /// Put a value in a box. pub fn set(&mut self, name: impl Into, value: impl Into) { self.edits.insert(name.into(), value.into()); } /// Whether this value is one the field's `changes` has already fired with. /// /// Falls back to what the description offered, so walking through a box /// without altering it writes nothing. #[must_use] pub fn unwritten(&self, name: &str, value: &str, described: Option<&str>) -> bool { match self.written.get(name) { Some(written) => written != value, None => described.unwrap_or_default() != value, } } /// Remember what a field's `changes` fired with. pub fn wrote(&mut self, name: &str, value: &str) { self.written.insert(name.to_owned(), value.to_owned()); } /// 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) { // Leaving the row leaves what is inside it. Stepping in is a move // within one stop, so moving to another stop cannot carry it along. self.inside = None; 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; self.inside = None; } } /// Which control inside the focused table row the caret has stepped onto. /// /// `None` on every other stop, and on a row the caret is standing on /// without having stepped in. See [`crate::focus`]'s header for why a table /// row is the one stop with an inside. #[must_use] pub const fn inside(&self) -> Option { self.inside } /// Step into the focused row, or along the controls in it, wrapping at both /// ends. /// /// The first step in lands on the first control going forward and on the /// last going back, which is what stepping into a run means from either /// side. After that it cycles, for [`advance`](Self::advance)'s reason: a /// terminal has nothing to show you that you are at the last control, so a /// key that stops dead reads as a broken key. Leaving is /// [`leave`](Self::leave)'s, on a key of its own. pub fn step_inside(&mut self, steps: isize, controls: usize) { if controls == 0 { self.inside = None; return; } let count = controls as isize; let at = match self.inside { Some(at) => (at.min(controls - 1) as isize + steps).rem_euclid(count), None if steps < 0 => count - 1, None => 0, }; self.inside = Some(at as usize); } /// Step back out of a row, and say whether the caret was in one. /// /// The answer is what lets Escape mean one thing at a time: it leaves the /// row if the caret is inside one, and otherwise it goes on to mean what it /// meant before. pub fn leave(&mut self) -> bool { self.inside.take().is_some() } /// 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 a branch is open, given what its description says. /// /// [`shown`](Self::shown)'s shape, one node kind along, and for the same /// reason: the description says where the outline starts and the reader /// says where it is now. See /// [`Row::open`](quasi_router::Row::open) for why folding a branch asks the /// app nothing. #[must_use] pub fn open(&self, key: &str, described: bool) -> bool { self.opened.get(key).copied().unwrap_or(described) } /// Fold an open branch, or open a shut one. pub fn fold(&mut self, key: &str, described: bool) { let open = self.open(key, described); self.opened.insert(key.to_string(), !open); } /// 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, &Local::none()) .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; self.inside = None; // A screen that has arrived is the answer to whatever was outstanding, // or is a different place entirely. Either way nothing on it has been // pressed yet. self.outstanding = None; self.progress = Awaited::default(); } /// 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. /// The frame is passed because the caret may legitimately be standing on /// one of its verbs, which is a place past the end of the screen's own /// spots. Clamping to the screen alone would take the caret off Send every /// time a fragment landed anywhere on the page. The chrome is passed for /// the same reason one lifetime along: a panel's controls sit past the /// frame's verbs, and a field inside one is not the screen's to drop. pub fn prune(&mut self, screen: &Screen, frame: &Frame, chrome: &Chrome) { // Every described spot, including the ones inside a region that does // not apply right now: a section the reader has toggled shut still // holds what they typed into it, and dropping those buffers would lose // the draft on the way back. `079a011e`. let spots: Vec = crate::focus::reaches_chromed(screen, frame, chrome, &Local::of(&Hidden::none(), self)) .into_iter() .map(|reach| reach.spot) .collect(); // The caret, though, is an index into what is on the screen. let hidden = crate::reveal::hidden(screen, chrome, self); let stops = crate::focus::reaches_chromed(screen, frame, chrome, &Local::of(&hidden, self)).len(); 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())); // The questions still on the screen that repeat, read back off the // names their slots submit under. `60d1753c`: a count held for a // question a fragment took away would decide how many boxes a later // screen's question of the same name stands in. // // After the buffers, and reading the same walk: the walk was made with // the counts still in place, so a slot the reader added is a live name // and its buffer survives. let repeating: Vec<&str> = spots .iter() .filter_map(|spot| match spot { // The question's own controls name it whatever the reader has // done, including having removed every slot: a count of zero is // the state a walk over the boxes alone cannot see, and // dropping it here would bring the slots back on the next // fragment. Spot::Repeat { field, .. } => Some(field.name.as_str()), _ => None, }) .collect(); self.slots .retain(|name, _| repeating.contains(&name.as_str())); let was = self.focus; self.focus = self.focus.min(stops.saturating_sub(1)); // A fragment that moved the caret moved it to another stop, and the // control it had stepped into belonged to the row it left. Clamping the // index against the new row instead would keep the ring inside a row the // user is no longer on. if self.focus != was { self.inside = None; } } }