//! What the user can reach, and the order they reach it in. //! //! Nothing in a description says this. A webview never had to ask: the browser //! builds the tab order out of the document, and the document is the drawing, so //! the order falls out of the markup a renderer already emitted. A terminal //! draws cells, and a cell knows nothing about the one before it. //! //! So focus order is this renderer's policy, and the policy is: **draw order**. //! A thing is reachable when the description gives it something to call, and it //! comes after whatever was drawn above it. That is the same rule the browser //! applies to a document with no `tabindex` in it, which is the shape every //! screen here has. //! //! The walk below mirrors [`crate::node::draw`] step for step, and it has to: //! the drawing counts reachable things as it passes them and lights the one //! whose number matches, so a walk that visited them in another order would //! light the wrong one. The two are kept together deliberately rather than //! being derived from one traversal, because the drawing needs a rect and this //! needs nothing, and threading a rect through a walk that has no use for one //! was the worse of the two couplings. //! //! # What is reachable //! //! Anything the description gives an address to, plus the two affordances that //! are addresses in everything but name: a row that can be ticked, and a field //! that takes typing. A [`Node::Meter`] and a [`Node::Figure`] are readouts and //! are skipped, and a disabled [`Act`] is drawn and passed over, which is what //! `disabled` means on every host. //! //! # A table row is one stop, and its controls are inside it //! //! Everywhere else a reachable thing is its own stop. A table row is the one //! exception: it is a single stop, and the controls in its cells hang off it as //! [`Spot::Row`]'s `inside`. A key steps in, cycles them, and steps back out. //! //! Not a preference. `makeover_tui::table` decides column widths, drops the //! columns a narrow window cannot fit, and answers no coordinates back, so //! nothing here knows where in a row a control was drawn and a flat focus //! order would be a list of places with no places in it. Two steps buys what a //! flat order cannot, and it is close to what a screen reader already gives //! this shape, so a reader moving between the terminal and the webview finds //! the same model. It is preferred over adding per-cell rects to a published //! `makeover-tui` and over hand-laying the table here; per-cell rects stay //! available underneath this later without changing what a user does. //! //! # Reach is this module; focus is the view's //! //! Both are this renderer's, and neither is describable. **Reach** is what this //! module computes: which things can take focus, and in what order. **Focus** is //! which reached thing holds the keyboard right now, and it lives in //! [`crate::View`] because it is a fact about where the user has walked rather //! than about the screen. The **focus ring** is what the drawing paints on it. //! //! The runtime now starts on the first reach unconditionally, which is what it //! did in practice anyway once the user pressed anything. The three terms are //! defined once in `makeover_layout`'s crate header, "Reach, focus and the //! focus ring". use makeover_layout as layout; use quasi_router::{Act, Action, Chrome, Consult, Field, Frame, Node, Prefill, Row, Screen, Slot}; use crate::Local; /// One thing the user can reach, and what reaching it offers. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Spot { /// A control. Enter calls it, after its confirmation when it has one. Act { /// What it calls. action: Action, /// What to ask first, if anything. confirm: Option, /// The key that reaches it without walking there. /// /// The one place the description already anticipated a terminal, and /// the runtime is what finally binds it. key: Option, /// The screen's selection this acts on, if it acts on one. /// /// The commit half of a staged tick. The runtime reads the set the view /// is holding and sends it with the call, which is the whole of what /// makes a bulk action work without a line of gathering code. over: Option, /// The names of the values it asked for first, in order. /// /// `Act::asks`. The boxes stand beside the control here rather than /// behind a disclosure, so they are ordinary field stops; what the /// press needs is which of them belong to it, and this is that list. /// Empty for the ordinary control. asks: Vec, /// The box the press writes into, and what lands there. /// /// `Act::fills`. Where inside the box is this renderer's, and a /// terminal has no caret inside a field to insert at -- `d52884b0` is /// why the view holds none -- so the value goes on the end. The /// vocabulary says that is not wrong: a description names the /// destination and never the position. fills: Option, /// The value the press puts on the clipboard. /// /// `Act::copies`. A terminal owns no clipboard any more than it owns a /// route, so this reaches the host as [`Step::Copy`](crate::Step::Copy) /// rather than being written here. copies: Option, }, /// Text that goes somewhere. Enter follows it. Link { /// Where it goes. action: Action, }, /// A question. Typing edits it; Enter leaves it alone. Field(Box), /// The control that answers a whole form. Submit { /// Where the answers go. action: Action, /// The names the form submits, in order, so the runtime can gather the /// values it is holding for them. names: Vec, }, /// A row of a list. Row { /// What opening it calls. activate: Option, /// What ticking it calls, when the tick is itself the write. toggle: Option, /// Whether it is ticked, and whether it can be. ticked: Option, /// Whether it is part of a live selection, and whether it can be. /// /// `Row::chosen`'s three states, carried through so the runtime can /// bind the two keys a terminal has for building one and say which of /// them was pressed. Distinct from /// [`ticked`](Self::Row::ticked) all the way down: a tick stages and a /// choice is in force. chosen: Option, /// What its tick contributes to the screen's selection. /// /// `None` on a row that names nothing, which on a screen holding a /// selection is the dead affordance `5f2b8753` was filed for: the box /// is drawn, the key is bound, and the tick has nowhere to go. The /// runtime declines to bind the key in that case rather than binding it /// to nothing. value: Option, /// What it offers without showing: reached by a key here, by /// right-click on a pointer host. menu: Vec, /// The branch this row is, keyed the way a fold is remembered. /// /// `None` on a leaf, which is every row described before `Row::open` /// existed. A branch is reachable even when nothing opens or ticks it: /// the disclosure is the affordance, and a row drawing a chevron the /// caret cannot land on is the dead affordance `5f2b8753` was filed /// for, one member along. branch: Option, /// Whether that branch is open, as the reader has left it. open: bool, /// The controls drawn inside the row, in the order they are drawn. /// /// And only ever populated for a table row. A list row's run /// contributes stops of its own right after the row, because a /// terminal draws a list itself and knows where every part of a line /// ended up. A table is laid out by `makeover_tui::table`, which /// decides column widths, drops the columns a narrow window has no /// room for and answers no coordinates back, so nothing here can say /// where in a row a control landed. The row is therefore one stop and /// these are what stepping into it reaches: **the row is the stop, and /// a key cycles what is inside it**, rather than per-cell rects or /// hand-laid tables. inside: Vec, }, /// The way to the rows a list is not showing. More { /// What asking for more calls. action: Action, }, /// The control that adds a slot to a repeating question, or takes one away. /// /// A stop rather than a key binding, because the description says what /// both controls are called and a named control is what a reader can find; /// a chord would be a fact about this renderer that no description could /// have written. It calls no route: pressing it is a change to what the /// reader is holding, which is the third of the three things the member is /// for. Repeat { /// The question, as it was described. /// /// The whole field rather than its name, because the floor, the ceiling /// and the values the description offered all decide what a press does, /// and the runtime holding only a name would have to walk the screen /// again to find them. field: Box, /// The slot this takes away, or `None` for the control that adds one. at: Option, }, } /// A question, and everything the runtime needs to hold what is typed into it. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FieldSpot { /// The name the value is submitted under. pub name: String, /// What kind of value it takes. pub kind: layout::FieldKind, /// What the description offers back, which is what an untouched buffer /// starts from. /// /// Always `None` for a [`layout::FieldKind::Secret`], and that is the whole /// of `39057019`: the description refuses to carry one, on purpose, so the /// runtime's buffer is the only place the typed value has ever lived. pub value: Option, /// The values on offer, for the kinds that offer any. pub options: Vec, /// The longest value it will take, in characters. pub max_length: Option, /// What setting it calls, for a control that writes on its own. pub writes: Option, /// What it asks about its own value as it is typed, how long each waits /// first, and what rides along. [`quasi_router::Field::consults`]. /// /// Several where the box raises several questions, and empty for the field /// that asks nothing, which is nearly all of them. pub consults: Vec, /// The question whose answer is this field's own list of candidates. /// /// [`quasi_router::Field::suggests`]. Beside [`consults`](Self::consults) /// rather than in it, because the answer belongs to this control instead of /// to a region: the runtime holds the candidates on the view, the drawing /// puts them under the box, and picking one writes the value. pub suggests: Option, } impl Spot { /// What Enter does here, when it does anything. /// /// A field answers `None`: Enter in a text box is not a submit here, the /// way it is in a browser, because a terminal has no implicit submit and /// guessing one would fire a form from the first field the user typed in. #[must_use] pub fn enters(&self) -> Option<&Action> { match self { Self::Act { action, .. } | Self::Link { action } | Self::Submit { action, .. } | Self::More { action } => Some(action), Self::Row { activate, .. } => activate.as_ref(), // Neither answers with a route: a field is typed into, and a // repeat control changes how many boxes there are. Self::Field(_) | Self::Repeat { .. } => None, } } /// The question this stands on, when it is one. #[must_use] pub const fn field(&self) -> Option<&FieldSpot> { match self { Self::Field(spot) => Some(spot), _ => None, } } } /// A reachable thing, and the region it is in. /// /// The region is here because scrolling needs it. A key that scrolls has to /// scroll something, and the only non-arbitrary answer is the region the user is /// working in, which is the region their focus is in. Carrying it on the walk /// that already visits every reachable thing is cheaper than a second walk that /// would be free to disagree with this one. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Reach { /// The [`Slot::id`] of the region holding it. pub region: String, /// What it is. pub spot: Spot, } /// Everything reachable on `screen`, in draw order, with its region. /// /// `hidden` names the regions that do not apply right now, from /// [`crate::reveal::hidden`], and they are walked past: a control the drawing /// left out is not a place the caret can go. An empty slice walks everything /// the screen describes, which is what a caller reading the description rather /// than the picture wants -- pruning what was typed, seeding what was ticked, /// and gathering what a submit sends. #[must_use] pub fn reaches(screen: &Screen, local: &Local<'_>) -> Vec { // A frame that rests nothing, so every notice floats and every notice is // drawn at the top. [`reaches_framed`]'s own docs already say this is that // function with a frame that offers nothing; sharing the walk is what makes // the sentence true rather than nearly true. reaches_framed(screen, &Frame::new(), local) } /// Everything reachable on `screen`, in draw order. See [`reaches`] for /// `hidden`. #[must_use] pub fn spots(screen: &Screen, local: &Local<'_>) -> Vec { reaches(screen, local) .into_iter() .map(|reach| reach.spot) .collect() } /// The region a frame's verbs are reported under. /// /// A frame is not a [`Slot`] and has no id of its own, so [`Reach::region`] /// needs a name for it. Reserved rather than derived: a screen carrying a /// region with this id would collide, and a name nothing in the tree uses is /// cheaper than a rule about which ids a screen may not have. pub const FRAME_REGION: &str = "quasi-frame"; /// The region a notice's own control is reported under. /// /// Reserved for [`FRAME_REGION`]'s reason. A notice belongs to the screen /// rather than to a place in it -- that is why every renderer draws them /// outside the regions -- so there is no [`Slot::id`] to report one under. /// /// Nothing aims a fragment here. A notice arrives with the answer that raised /// it, so there is no address for one to be replaced at. pub const NOTICE_REGION: &str = "quasi-notice"; /// The region the band's own contents are reported under. /// /// Reserved for [`FRAME_REGION`]'s reason. The band is chrome and has no /// address: it is built once beside the router, and nothing answers into it. pub const BAND_REGION: &str = "quasi-band"; /// The region a nav place's reach is reported under. /// /// A reserved name for the same reason [`FRAME_REGION`] is one: the nav has no /// address, because nothing answers into it. It is built once and never /// replaced, so there is nothing for a fragment to aim at. pub const NAV_REGION: &str = "quasi-nav"; /// Everything reachable on `screen`, then the verbs `frame` offers. /// /// The frame's verbs come last because they are drawn last: the order here is /// the order the drawing counts in, and the two disagreeing lights the wrong /// control. /// /// [`reaches`] is this with the frame that offers nothing, so a host that #[must_use] pub fn reaches_framed(screen: &Screen, frame: &Frame, local: &Local<'_>) -> Vec { let mut found = Vec::new(); // The floating notices first, because they are drawn first: above the // regions and below the tab line. `bde35298` is what put anything reachable // in one -- a notice was a sentence until `Node::Notice` grew an act, so // this walk had nothing to find here and did not look. for notice in screen.notices.iter().filter(|one| !frame.holds(one)) { node_spots(notice, NOTICE_REGION, local, &mut found); } for slot in crate::region::reachable(screen) { slot_spots(slot, local, &mut found); } // Then the footer, in the order `crate::frame::draw` writes it: the notices // the mount rests, then its verbs. for notice in screen.notices.iter().filter(|one| frame.holds(one)) { node_spots(notice, NOTICE_REGION, local, &mut found); } for verb in &frame.verbs { push_act(verb, FRAME_REGION, local, &mut found); } found } /// Everything reachable on `screen`, the verbs `frame` offers, then the panel /// the app keeps on screen. /// /// The panel comes last because it is drawn last, which is the rule /// [`reaches_framed`] already states: the order here is the order the drawing /// counts in. /// /// Its reaches are reported under the panel's own id rather than a reserved /// name. That is the difference from a frame: a frame has no address and needs /// [`FRAME_REGION`], and a panel has one because an answer aims at it. /// /// [`reaches_framed`] is this with the chrome that keeps nothing on screen, so #[must_use] pub fn reaches_chromed( screen: &Screen, frame: &Frame, chrome: &Chrome, local: &Local<'_>, ) -> Vec { let mut found = Vec::new(); // The tab line first, because it is drawn first. A terminal user reaches // the places by the same walk that reaches everything else; without this // the nav would be a row they can read and cannot use, which is the failure // `Screen::selection` was added to end one level down. // // Only the row that is drawn: the places, and the sub-places of the one the // screen is in. Counting a sub-place of a tab the user is not on would put // a stop in the walk with nothing on screen at it. // The brand before the places, because it is drawn at the head of that same // row. It goes somewhere -- home, for every app that has ever had one -- so // a walk that skipped it would draw a link the reader cannot follow. if let Some(brand) = chrome.band.as_ref().and_then(|band| band.brand.as_ref()) { found.push(Reach { region: BAND_REGION.to_string(), spot: Spot::Act { action: brand.action.clone(), confirm: None, key: None, over: None, asks: Vec::new(), fills: None, copies: None, }, }); } for place in &chrome.nav { push_place(place, &mut found); } if let Some(open) = chrome .nav .iter() .find(|place| screen.place.as_deref().is_some_and(|key| place.holds(key))) { for inner in &open.within { push_place(inner, &mut found); } } // Then the band's own box, because it is drawn on the row under the places. // A search field in the header is a field like any other -- that is what // `Band::search` being a `Field` buys -- so it is reached by the same walk // and edited by the same buffer, rather than being a row the reader can see // and not use. if let Some(search) = chrome.band.as_ref().and_then(|band| band.search.as_ref()) { node_spots( &quasi_router::Node::Field(Box::new(search.clone())), BAND_REGION, local, &mut found, ); } found.extend(reaches_framed(screen, frame, local)); for panel in &chrome.panels { node_spots(&panel.content, &panel.id, local, &mut found); } found } /// One place, as somewhere the walk can stop. fn push_place(place: &quasi_router::Place, found: &mut Vec) { found.push(Reach { region: NAV_REGION.to_string(), spot: Spot::Act { action: place.action.clone(), confirm: None, key: None, over: None, asks: Vec::new(), fills: None, copies: None, }, }); } /// A region's reachable things. /// /// A region that is still loading has none. It is drawn as the word "Loading" /// and nothing under it is on screen, so anything counted here would be a /// focusable the user cannot see. fn slot_spots(slot: &Slot, local: &Local<'_>, found: &mut Vec) { if matches!(slot.readiness, layout::Readiness::Pending) { return; } // A region that does not apply right now is not drawn, so nothing in it is // a place the caret can go. `079a011e`, and the same list the drawing // reads: the two walks agreeing about which regions are on the screen is // what keeps the highlighted control the one the reader is on. if local.out(&slot.id) { return; } // Every member, at every width. The walk has no width and deliberately // keeps none: its other two callers prune what the user typed and seed // what they ticked, and both of those would be wrong to forget a field // because the window is narrow at the moment they run. // // The cost is that a member ranked below `Priority::Essential` can still // take focus in a window narrow enough to have dropped it, so Tab reaches // something that is not on the screen. Recorded rather than papered over: // fixing it means the navigation walk knowing the width the drawing knows, // which is a wider change than the member that revealed it. // The row the region says its members share, then its body, which is the // order they are drawn in. The caret and the drawing agreeing about that // order is the invariant this walk exists to keep. for placed in slot.run.iter().flat_map(|run| run.members.iter()) { node_spots(&placed.node, &slot.id, local, found); } // A region whose children are answers to one question reaches two controls // the body does not hold: each slot's own remove, and the add under the // lot. `f7abbc08`. In the order `region::repeating_body` draws them, which // is the invariant the walk-matches-drawing test checks. if let Some(repeating) = slot.repeating.as_deref() { let standing = slot.body.len(); for placed in slot.body.iter() { node_spots(&placed.node, &slot.id, local, found); if let Some(removes) = crate::region::removes_of(&placed.node) { let act = crate::region::bounded(removes, repeating.may_remove(standing)); push_act(&act, &slot.id, local, found); } } let add = crate::region::bounded(&repeating.add, repeating.may_add(standing)); push_act(&add, &slot.id, local, found); return; } for placed in slot.body.iter() { node_spots(&placed.node, &slot.id, local, found); } } /// One node's reachable things, in the order it draws them. pub(crate) fn node_spots(node: &Node, region: &str, local: &Local<'_>, found: &mut Vec) { // Everything below reads better saying what it found rather than how it is // recorded, and the region is the same for every one of them. macro_rules! push { ($spot:expr) => { found.push(Reach { region: region.to_string(), spot: $spot, }) }; } match node { Node::Act(act) => push_act(act, region, local, found), Node::Link { action, .. } => push!(Spot::Link { action: action.clone(), }), // A chip carries a route and is drawn as a bracketed label with no // second target in it, which `node.rs` already declined: the `x` a // webview hangs on a chip is a control inside a span. Reaching the chip // is reaching its action, which is the part a terminal can honour. Node::Token(tag) => { if let layout::Token::Chip { .. } = tag.kind && let Some(action) = tag.action.clone() { push!(Spot::Act { action, confirm: None, key: None, over: None, asks: Vec::new(), fills: None, copies: None, }); } } // A stand-in's way out, when it has one. Spelt as two arms rather than // one holding an `if let`, so that the empty case is named here and not // swept into the catch-all below with the members this renderer has // never heard of. Node::StandIn { act: Some(act), .. } | Node::Notice { act: Some(act), .. } => { push_act(act, region, local, found); } Node::StandIn { act: None, .. } => {} Node::Field(field) => push_field(field, region, local, found), Node::Form { action, fields, .. } => { for field in fields { push_field(field, region, local, found); } push!(Spot::Submit { action: action.clone(), // Every name the form sends, which for a question answered N // times is N of them. `60d1753c`: one submit carries every // instance, so the names are asked of the same count the boxes // were drawn from. names: fields .iter() .flat_map(|field| submitted(field, local)) .collect(), }); } // Reachable exactly as a list's rows are. A placement changes where a // row is drawn, not whether it can be reached, and the terminal draws // these in the order given -- so tab order and reading order agree // without this having to know anything about the axis. Node::Timeline { entries, .. } => { for entry in entries { push_row(&entry.row, region, local, found); } } // A table's rows are reachable and its cells are not, so a control in a // cell is reached by stepping into the row rather than by tabbing to // it. See [`Spot::Row`]'s `inside` for why the layout leaves no other // way, and [`inside`] for the walk. Node::Table { columns, rows, more, .. } => { // A row a shut branch covers is not on the screen, so it is not // reachable and takes no stop. The drawing makes the same reading // from the same function, which is what keeps the caret on the row // it is painted on. for (_, cells) in crate::outline::showing(rows, local.view()) { // **How a row is entered is the one thing the two arrangements // disagree about.** A list row's controls are stops of their // own, walked straight after the row; a table row's cells are // reached by stepping *into* the row, because the grid leaves // no other way. One node since the 2026-09-06 collapse, and // this is where it forks. if columns.is_empty() { push_row(cells, region, local, found); continue; } // A tickable row is reachable even when nothing opens it: the // tick is the affordance, and a row that draws a box a reader // cannot reach is the dead affordance `5f2b8753` was filed for, // one node over. A row that only offers a menu is reachable for // the same reason -- the menu is the affordance, and in a // terminal it is reached from the row and nowhere else, so a row // the caret cannot land on holds acts nothing can get at. A row // that only carries controls in its cells is the third case and // is `27f2331e`: nothing opens it and nothing ticks it, and // until the caret could land on it the Remove button in its // last cell was drawn and unreachable. // `toggle` stays `None` because a table row's tick is always a // member of a set -- `Row::ticking` is the only constructor for // it. if row_reachable(cells) { push!(Spot::Row { activate: cells.activate.clone(), toggle: None, ticked: cells.selected, chosen: cells.chosen, value: cells.value.clone(), menu: cells.menu.clone(), branch: branch_key(cells), open: open_now(cells, local), inside: inside(cells) .into_iter() .filter_map(|(column, part)| { inside_spot(&cells.cells[column].content[part]) }) .collect(), }); } } // Prev, then the pages, then Next -- `node::rest_pieces`' own // order, which is the order the line is drawn in and the order the // webview prints them. Read off that function rather than written // out again, because the drawing claims one position per reachable // piece and a second list here is a second answer to how many there // are. // // This was the list arm's alone until the collapse, and `draw_table` // has drawn a table's pager the whole time. So a paged table's Prev // and Next were on the screen and the caret could not land on // either. Same class as `27f2331e` one node over: drawn by one pass // and unknown to the other. if let Some(rest) = more { for piece in crate::node::rest_pieces(rest) { if let Some(action) = piece.action { push!(Spot::More { action: action.clone(), }); } } } } Node::Region(slot) => slot_spots(slot, local, found), // Readouts and prose. Nothing to call, so nothing to stop on. Node::Heading { .. } | Node::Text { .. } | Node::Rich { .. } | Node::Figure(_) // A picture carries no address of its own -- `src` is where the bytes // are, not somewhere the reader goes -- so there is nothing to stop on. // A picture that is meant to be clicked is one inside a `Link`. | Node::Image(_) | Node::Notice { .. } | Node::Meter(_) | Node::Stats { .. } => {} // A member added since this renderer last learned the vocabulary. // `Node` is `#[non_exhaustive]`, so this arm is what lets that land // without a lockstep release here. // // Contributing no reach is the safe read, and it is the only honest // one: a `Reach` says where a key press goes, and this cannot know // where. Guessing a `Spot` would send a press to the wrong action, // whereas an unreached node is merely something the reader tabs past. // `node.rs` draws `UNDRAWN` in the same case, so the reader sees that // something is there and sees that it does not respond, rather than // being handed a stop that goes nowhere. _ => {} } } /// Whether the caret can land on this table row at all. /// /// One function because two would drift: [`node_spots`] decides what the caret /// can land on and `node.rs` decides what the drawing counts, and a row counted /// in one and not the other shifts every stop below the table by one. /// Whether a row offers anything of its own. /// /// The row's members only: what it holds in its cells is a separate question, /// and the two containers answer it differently on purpose. A table row steps /// into its cells ([`inside`]); a list row's controls are stops of their own, /// walked straight after it. /// /// One predicate since the 2026-09-05 collapse, and it closes a gap. This was /// written twice, and the copy here did not test `toggle` because a table row /// could not carry one. Now that a row is a row, a table row that is a /// checklist item can, and without this it would have been unreachable in a /// terminal: nothing else about such a row is set, so no other clause fires. pub(crate) fn row_offers(row: &Row) -> bool { row.open.is_some() || row.activate.is_some() || row.toggle.is_some() || row.selected.is_some() || row.chosen.is_some() || !row.menu.is_empty() } pub(crate) fn row_reachable(cells: &Row) -> bool { row_offers(cells) || !inside(cells).is_empty() } /// The parts of a table row the caret can step into, as `(column, part)` /// positions into [`Row::cells`] and [`Cell::content`]. /// /// Positions rather than [`Spot`]s, for the same reason [`row_reachable`] is one /// function: the drawing needs to know *which* part to paint lit and the reach /// walk needs to know what pressing it calls, and the two have to be the same /// list read twice or the ring lands on the wrong control. /// /// [`Cell::parts`]: quasi_router::Cell pub(crate) fn inside(cells: &Row) -> Vec<(usize, usize)> { let mut found = Vec::new(); for (column, cell) in cells.cells.iter().enumerate() { for (part, node) in cell.content.iter().enumerate() { if inside_spot(node).is_some() { found.push((column, part)); } } } found } /// What stepping onto one part of a cell reaches, when it reaches anything. /// /// The three inline members that carry an address, matching what [`node_spots`] /// makes reachable in a list row's run: a control, text that goes somewhere, and /// a chip that is a route rather than a label. A disabled control is drawn and /// passed over here exactly as it is everywhere else. /// /// # An act that asks for a value first is not reachable here /// /// [`Act::asks`] puts boxes beside the control, and a cell is one line inside a /// column of a width this walk does not know, so the boxes are not drawn. A stop /// on the control would fire it with nothing gathered under the names it asked /// for, which is worse than not stopping: the row still opens, and the reader /// can see that the control did not answer. No described screen puts one in a /// cell today -- the measured consumers are bulk bars -- and the day one does, /// what it wants is the cell's own line rather than a stop bound to a silent /// send. fn inside_spot(node: &Node) -> Option { match node { Node::Act(act) if !act.state.is_some_and(layout::State::suppresses_interaction) && act.asks.is_empty() => { Some(Spot::Act { action: act.action.clone(), confirm: act.confirm.clone(), key: act.key.clone(), over: act.over.clone(), asks: Vec::new(), fills: act.fills.clone(), copies: act.copies.clone(), }) } Node::Link { action, .. } => Some(Spot::Link { action: action.clone(), }), Node::Token(tag) => match (&tag.kind, &tag.action) { (layout::Token::Chip { .. }, Some(action)) => Some(Spot::Act { action: action.clone(), confirm: None, key: None, over: None, asks: Vec::new(), fills: None, copies: None, }), _ => None, }, _ => None, } } /// A control, unless it is disabled, and the questions it asks before it fires. /// /// The questions come first, in the order they are drawn: they sit above the /// control in `node.rs`, and a reader tabbing forward should reach the box /// before the button that sends what is in it. fn push_act(act: &Act, region: &str, local: &Local<'_>, found: &mut Vec) { if act.state.is_some_and(layout::State::suppresses_interaction) { return; } for field in &act.asks { push_field(&field.as_asked(), region, local, found); } found.push(Reach { region: region.to_string(), spot: Spot::Act { action: act.action.clone(), confirm: act.confirm.clone(), key: act.key.clone(), over: act.over.clone(), // Every name the press sends, which for a question answered N // times is N of them, exactly as a form's submit gathers them. asks: act .asks .iter() .flat_map(|field| submitted(&field.as_asked(), local)) .collect(), fills: act.fills.clone(), copies: act.copies.clone(), }, }); } /// A question, unless it is hidden. /// /// A hidden field draws nothing and is submitted with the form, so stopping on /// it would be a stop on a blank row. /// /// A question answered N times is N stops and the controls that change N, in /// the order the drawing paints them: `60d1753c`. Both walks read the same /// count off the same [`Local`], which is what keeps the caret and the picture /// agreeing about how many stops a repeating question has. fn push_field(field: &Field, region: &str, local: &Local<'_>, found: &mut Vec) { // A question that does not apply is not on the screen, so the caret does // not stop on it. `8fdb814c`, and it is the region rule one level down: the // walk that reads a submission passes `Local::none`, so what was typed into // it is kept and still sent. if local.field_out(&field.name) { return; } let Some(repeat) = &field.repeats else { push_one(field, region, found); return; }; let standing = local.standing(field); for at in 0..standing { for slot in field.instance_fields(at) { push_one(&slot, region, found); } if repeat.fewer(standing) { found.push(Reach { region: region.to_string(), spot: Spot::Repeat { field: Box::new(field.clone()), at: Some(at), }, }); } } if repeat.more(standing) { found.push(Reach { region: region.to_string(), spot: Spot::Repeat { field: Box::new(field.clone()), at: None, }, }); } } /// The names one field submits under: its own, or one per slot. /// /// The count is the reader's, so this is not a function of the description /// alone, which is the whole reason a submit's name list is built during the /// walk rather than off the form. fn submitted(field: &Field, local: &Local<'_>) -> Vec { if field.repeats.is_none() { return vec![field.name.clone()]; } (0..local.standing(field)) .flat_map(|at| { field .instance_fields(at) .into_iter() .map(|slot| slot.name) .collect::>() }) .collect() } /// One box, whether it stands alone or is one slot of a repeating question. fn push_one(field: &Field, region: &str, found: &mut Vec) { if matches!(field.kind, layout::FieldKind::Hidden) { return; } found.push(Reach { region: region.to_string(), spot: Spot::Field(Box::new(FieldSpot { name: field.name.clone(), kind: field.kind, value: field.value.clone(), options: field .options .iter() .map(|choice| choice.value.clone()) .collect(), max_length: field.max_length, writes: field.writes.clone(), consults: field.consults.clone(), suggests: field.suggests.clone(), })), }); } /// What a fold on this row is remembered by, if it is a branch at all. fn branch_key(row: &impl quasi_router::Outline) -> Option { row.open().is_some().then(|| row.key()) } /// Whether this row's branch is open, as the reader has left it. /// /// `false` on a leaf, which nothing reads: the runtime looks at `branch` first. fn open_now(row: &impl quasi_router::Outline, local: &Local<'_>) -> bool { match (row.open(), local.view()) { (Some(described), Some(view)) => view.open(&row.key(), described), (described, _) => described.unwrap_or(false), } } /// A row: the row itself when the description gives it something to do, then /// whatever its run carries. /// /// Two stops and not one, because they are two things. A row that opens a /// detail pane and also shows a Remove button offers both, and a terminal that /// collapsed them would make the button unreachable or the row unopenable. A /// row that only shows things is passed over entirely, which is the difference /// between a list and a menu. /// /// The row comes first because it is the whole line and the controls sit on it. fn push_row(row: &Row, region: &str, local: &Local<'_>, found: &mut Vec) { if row_offers(row) { found.push(Reach { region: region.to_string(), spot: Spot::Row { activate: row.activate.clone(), toggle: row.toggle.clone(), ticked: row.selected, chosen: row.chosen, value: row.value.clone(), menu: row.menu.clone(), branch: branch_key(row), open: open_now(row, local), // Empty, always. The run below is walked straight after this, // so a list row's controls are stops of their own and there is // nothing to step into. inside: Vec::new(), }, }); } for cell in &row.cells { for node in &cell.content { node_spots(node, region, local, found); } } }