//! 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. //! //! # 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. //! //! A description used to be able to claim focus for a control, and //! `makeover-layout` removed the member in 0.19.0 on the grounds that focus is //! fundamentally different per host. 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, Field, Node, Part, Row, Screen, Slot}; /// 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, }, /// 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, /// 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, }, /// One option of a selector. Choice { /// What picking it calls. action: Action, }, /// The way to the rows a list is not showing. More { /// What asking for more calls. action: Action, }, } /// 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 changing it calls, for a control that writes on its own. pub changes: 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::Choice { action } | Self::More { action } => Some(action), Self::Row { activate, .. } => activate.as_ref(), Self::Field(_) => 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. #[must_use] pub fn reaches(screen: &Screen) -> Vec { let mut found = Vec::new(); for slot in crate::region::reachable(screen) { slot_spots(slot, &mut found); } found } /// Everything reachable on `screen`, in draw order. #[must_use] pub fn spots(screen: &Screen) -> Vec { reaches(screen) .into_iter() .map(|reach| reach.spot) .collect() } /// 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, found: &mut Vec) { if matches!(slot.readiness, layout::Readiness::Pending) { return; } for node in &slot.body { node_spots(node, &slot.id, found); } } /// One node's reachable things, in the order it draws them. pub(crate) fn node_spots(node: &Node, region: &str, 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, 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, }); } } Node::StandIn { act, .. } => { if let Some(act) = act { push_act(act, region, found); } } Node::Field(field) => push_field(field, region, found), Node::Form { action, fields, submit: _, } => { for field in fields { push_field(field, region, found); } push!(Spot::Submit { action: action.clone(), names: fields.iter().map(|field| field.name.clone()).collect(), }); } Node::List { rows, more } => { for row in rows { push_row(row, region, found); } if let Some(rest) = more { push!(Spot::More { action: rest.action.clone(), }); } } // A table's rows are reachable and its cells are not. A cell holding a // control is drawn through `makeover_tui::table`, which lays cells out // by column width and answers no coordinates back, so there is nothing // here that could say where inside a row a control ended up. Reaching // the row is what a terminal can do honestly; reaching the third // control in the fourth cell is a finding. Node::Table { rows, .. } => { for cells in rows { if let Some(activate) = cells.activate.clone() { push!(Spot::Row { activate: Some(activate), toggle: None, ticked: None, value: None, menu: Vec::new(), }); } } } Node::Select { options, action, .. } => { for (choice, own) in options { // An option carrying nothing falls back to the strip's action // with its value substituted, which is what the description // says the fallback is. An option with neither is a label. let call = own.clone().or_else(|| { action .clone() .map(|action| action.with(Node::SELECTED, choice.value.clone())) }); if let Some(action) = call { push!(Spot::Choice { action }); } } } Node::Region(slot) => slot_spots(slot, found), // Readouts and prose. Nothing to call, so nothing to stop on. Node::Heading { .. } | Node::Text { .. } | Node::Rich { .. } | Node::Figure(_) | Node::Notice { .. } | Node::Meter(_) | Node::Stats { .. } => {} } } /// A control, unless it is disabled. fn push_act(act: &Act, region: &str, found: &mut Vec) { if act.state.is_some_and(layout::State::suppresses_interaction) { return; } 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(), }, }); } /// 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. fn push_field(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, changes: field.changes.clone(), })), }); } /// 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, found: &mut Vec) { if row.activate.is_some() || row.toggle.is_some() || row.selected.is_some() || !row.menu.is_empty() { found.push(Reach { region: region.to_string(), spot: Spot::Row { activate: row.activate.clone(), toggle: row.toggle.clone(), ticked: row.selected, value: row.value.clone(), menu: row.menu.clone(), }, }); } for Part { node, .. } in &row.parts { node_spots(node, region, found); } }