//! Which regions and questions are not applicable right now. //! //! A region says which control and which value bring it out, and a renderer //! answers that from what it already holds. No request, no fragment, no re- //! render of a form the reader is midway through. //! //! **The host with no cursor is why the condition sits on the region.** A //! terminal does not hide a region the way a browser hides an element: the //! honest reading here is "this region does not apply right now", which is a //! property of the region. This renderer answers it by leaving the region out, //! which is one of the three the ruling names -- dim it, omit it, or explain it //! -- and is the one that costs the reader nothing to skip. //! //! # Why the answer is a set of ids rather than a question asked per region //! //! Because the drawing and the focus walk have to agree exactly. The caret is //! an index into the walk and the drawing counts as it paints, so a region left //! out of one and not the other lights the wrong control. Both read this list, //! computed once from the screen and the view, and neither is in a position to //! evaluate a condition differently from the other. //! //! It also keeps the walk cheap: [`Screen::holds`] searches the screen for a //! control, and asking it once per conditional region per frame is the same //! work whatever the drawing is doing. use quasi_router::{Chrome, Field, Node, Screen, Slot}; use crate::{Hidden, View}; /// The ids of the regions on this screen that do not apply right now. /// /// Empty for a screen with no conditional region, which is nearly all of them, /// and empty is what every caller with no view to read hands on: a region whose /// condition nobody evaluated is drawn, which is the same direction the webview /// degrades in when its script is not served. pub(crate) fn hidden<'a>(screen: &'a Screen, chrome: &Chrome, view: &View) -> Hidden<'a> { let mut found = Hidden::none(); for slot in &screen.slots { walk(slot, screen, chrome, view, &mut found); } found } /// Whether one question is out, on the same terms as a region. /// /// A form's questions are a flat list, so a single conditional question inside /// one carries the condition itself rather than being wrapped in a region that /// could. Answered from the same [`held`] as a region's, so the two cannot /// come apart over what a control is holding. fn asked(field: &Field, screen: &Screen, chrome: &Chrome, view: &View) -> bool { let Some(control) = field.watches() else { return true; }; field.revealed(held(control, screen, chrome, view)) } /// The questions in one node that do not apply, added to the list. /// /// A form's fields and a standalone control, which is where a described field /// is. A field asked by an act is not here: it is answered by the press that /// asked for it rather than drawn on the screen. fn questions<'a>( node: &'a Node, screen: &Screen, chrome: &Chrome, view: &View, found: &mut Hidden<'a>, ) { let fields: &[Field] = match node { Node::Field(field) => std::slice::from_ref(field.as_ref()), Node::Form { fields, .. } => fields, _ => &[], }; for field in fields { if !asked(field, screen, chrome, view) { found.fields.push(field.name.as_str()); } } } /// This region and the regions inside it, adding the ones that are not out. /// /// A region inside one that does not apply is not visited: it is not on the /// screen either way, and naming it would say the caret should skip something /// it was never going to reach. fn walk<'a>(slot: &'a Slot, screen: &Screen, chrome: &Chrome, view: &View, found: &mut Hidden<'a>) { if !out(slot, screen, chrome, view) { found.regions.push(slot.id.as_str()); return; } for placed in slot .run .iter() .flat_map(|run| run.members.iter()) .chain(slot.body.iter()) { if let Node::Region(inner) = &placed.node { walk(inner, screen, chrome, view, found); } else { questions(&placed.node, screen, chrome, view, found); } } } /// Whether one region is out, given what the reader has done so far. fn out(slot: &Slot, screen: &Screen, chrome: &Chrome, view: &View) -> bool { let Some(control) = slot.watches() else { return true; }; slot.revealed(held(control, screen, chrome, view)) } /// What a control is holding: what was typed, or what the description offered. /// /// The same order a submit reads a form in, so a region comes out on an /// untouched select resting on the value its description named, rather than /// waiting for the reader to pick the value it is already showing. /// /// The chrome is searched after the screen because a panel outlives the screen /// under it: a form kept on screen from everywhere can gate a section of /// itself, and its fields are in no screen for [`Screen::holds`] to find. fn held<'a>( control: &str, screen: &'a Screen, chrome: &'a Chrome, view: &'a View, ) -> Option<&'a str> { view.edit(control).or_else(|| { screen.holds(control).or_else(|| { chrome .panels .iter() .find_map(|panel| panel.content.holds(control)) }) }) }