Skip to main content

max / quasi

5.2 KB · 137 lines History Blame Raw
1 //! Which regions and questions are not applicable right now.
2 //!
3 //! A region says which control and which value bring it out, and a renderer
4 //! answers that from what it already holds. No request, no fragment, no re-
5 //! render of a form the reader is midway through.
6 //!
7 //! **The host with no cursor is why the condition sits on the region.** A
8 //! terminal does not hide a region the way a browser hides an element: the
9 //! honest reading here is "this region does not apply right now", which is a
10 //! property of the region. This renderer answers it by leaving the region out,
11 //! which is one of the three the ruling names -- dim it, omit it, or explain it
12 //! -- and is the one that costs the reader nothing to skip.
13 //!
14 //! # Why the answer is a set of ids rather than a question asked per region
15 //!
16 //! Because the drawing and the focus walk have to agree exactly. The caret is
17 //! an index into the walk and the drawing counts as it paints, so a region left
18 //! out of one and not the other lights the wrong control. Both read this list,
19 //! computed once from the screen and the view, and neither is in a position to
20 //! evaluate a condition differently from the other.
21 //!
22 //! It also keeps the walk cheap: [`Screen::holds`] searches the screen for a
23 //! control, and asking it once per conditional region per frame is the same
24 //! work whatever the drawing is doing.
25
26 use quasi_router::{Chrome, Field, Node, Screen, Slot};
27
28 use crate::{Hidden, View};
29
30 /// The ids of the regions on this screen that do not apply right now.
31 ///
32 /// Empty for a screen with no conditional region, which is nearly all of them,
33 /// and empty is what every caller with no view to read hands on: a region whose
34 /// condition nobody evaluated is drawn, which is the same direction the webview
35 /// degrades in when its script is not served.
36 pub(crate) fn hidden<'a>(screen: &'a Screen, chrome: &Chrome, view: &View) -> Hidden<'a> {
37 let mut found = Hidden::none();
38 for slot in &screen.slots {
39 walk(slot, screen, chrome, view, &mut found);
40 }
41 found
42 }
43
44 /// Whether one question is out, on the same terms as a region.
45 ///
46 /// A form's questions are a flat list, so a single conditional question inside
47 /// one carries the condition itself rather than being wrapped in a region that
48 /// could. Answered from the same [`held`] as a region's, so the two cannot
49 /// come apart over what a control is holding.
50 fn asked(field: &Field, screen: &Screen, chrome: &Chrome, view: &View) -> bool {
51 let Some(control) = field.watches() else {
52 return true;
53 };
54 field.revealed(held(control, screen, chrome, view))
55 }
56
57 /// The questions in one node that do not apply, added to the list.
58 ///
59 /// A form's fields and a standalone control, which is where a described field
60 /// is. A field asked by an act is not here: it is answered by the press that
61 /// asked for it rather than drawn on the screen.
62 fn questions<'a>(
63 node: &'a Node,
64 screen: &Screen,
65 chrome: &Chrome,
66 view: &View,
67 found: &mut Hidden<'a>,
68 ) {
69 let fields: &[Field] = match node {
70 Node::Field(field) => std::slice::from_ref(field.as_ref()),
71 Node::Form { fields, .. } => fields,
72 _ => &[],
73 };
74 for field in fields {
75 if !asked(field, screen, chrome, view) {
76 found.fields.push(field.name.as_str());
77 }
78 }
79 }
80
81 /// This region and the regions inside it, adding the ones that are not out.
82 ///
83 /// A region inside one that does not apply is not visited: it is not on the
84 /// screen either way, and naming it would say the caret should skip something
85 /// it was never going to reach.
86 fn walk<'a>(slot: &'a Slot, screen: &Screen, chrome: &Chrome, view: &View, found: &mut Hidden<'a>) {
87 if !out(slot, screen, chrome, view) {
88 found.regions.push(slot.id.as_str());
89 return;
90 }
91 for placed in slot
92 .run
93 .iter()
94 .flat_map(|run| run.members.iter())
95 .chain(slot.body.iter())
96 {
97 if let Node::Region(inner) = &placed.node {
98 walk(inner, screen, chrome, view, found);
99 } else {
100 questions(&placed.node, screen, chrome, view, found);
101 }
102 }
103 }
104
105 /// Whether one region is out, given what the reader has done so far.
106 fn out(slot: &Slot, screen: &Screen, chrome: &Chrome, view: &View) -> bool {
107 let Some(control) = slot.watches() else {
108 return true;
109 };
110 slot.revealed(held(control, screen, chrome, view))
111 }
112
113 /// What a control is holding: what was typed, or what the description offered.
114 ///
115 /// The same order a submit reads a form in, so a region comes out on an
116 /// untouched select resting on the value its description named, rather than
117 /// waiting for the reader to pick the value it is already showing.
118 ///
119 /// The chrome is searched after the screen because a panel outlives the screen
120 /// under it: a form kept on screen from everywhere can gate a section of
121 /// itself, and its fields are in no screen for [`Screen::holds`] to find.
122 fn held<'a>(
123 control: &str,
124 screen: &'a Screen,
125 chrome: &'a Chrome,
126 view: &'a View,
127 ) -> Option<&'a str> {
128 view.edit(control).or_else(|| {
129 screen.holds(control).or_else(|| {
130 chrome
131 .panels
132 .iter()
133 .find_map(|panel| panel.content.holds(control))
134 })
135 })
136 }
137