//! 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 stylesheet is why the condition sits on the region.** //! Neither client renderer hides 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 `quasi-tui` picked, so a screen described once and drawn by //! both client renderers says the same thing. //! //! # Why the answer is a set of ids rather than a question asked per region //! //! Cost, and the borrow. [`Screen::holds`] searches the screen for a control, //! and this renderer redraws every frame; answering once per frame for every //! conditional region on the screen is the same work the drawing would do and //! is done before the drawing starts. The `Pass` holds the view mutably -- //! egui's text controls write through it -- so a question asked mid-draw could //! not read the view anyway. use quasi_router::{Chrome, Field, Node, Screen, Slot}; use crate::View; /// What does not apply right now, on one screen, as the reader has left it. /// /// Regions and questions in one type because they are one answer computed in /// one walk: 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. #[derive(Debug, Clone, Default)] pub(crate) struct Hidden<'a> { /// The regions that are not out, by [`Slot::id`]. pub(crate) regions: Vec<&'a str>, /// The questions that are not out, by [`Field::name`]. pub(crate) fields: Vec<&'a str>, } impl Hidden<'_> { /// Everything applies. pub(crate) const fn none() -> Self { Self { regions: Vec::new(), fields: Vec::new(), } } /// Whether this region does not apply right now. pub(crate) fn out(&self, id: &str) -> bool { self.regions.contains(&id) } /// Whether this question does not apply right now. pub(crate) fn field_out(&self, name: &str) -> bool { self.fields.contains(&name) } } /// 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. 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. 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 drawn /// either way, and its own condition is a question about a screen it is not on. 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)) }) }) }