Skip to main content

max / quasi

5.9 KB · 163 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 stylesheet is why the condition sits on the region.**
8 //! Neither client renderer hides a region the way a browser hides an element:
9 //! the honest reading here is "this region does not apply right now", which is
10 //! a 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 `quasi-tui` picked, so a screen described once and drawn by
13 //! both client renderers says the same thing.
14 //!
15 //! # Why the answer is a set of ids rather than a question asked per region
16 //!
17 //! Cost, and the borrow. [`Screen::holds`] searches the screen for a control,
18 //! and this renderer redraws every frame; answering once per frame for every
19 //! conditional region on the screen is the same work the drawing would do and
20 //! is done before the drawing starts. The `Pass` holds the view mutably --
21 //! egui's text controls write through it -- so a question asked mid-draw could
22 //! not read the view anyway.
23
24 use quasi_router::{Chrome, Field, Node, Screen, Slot};
25
26 use crate::View;
27
28 /// What does not apply right now, on one screen, as the reader has left it.
29 ///
30 /// Regions and questions in one type because they are one answer computed in
31 /// one walk: a form's questions are a flat list, so a single conditional
32 /// question inside one carries the condition itself rather than being wrapped
33 /// in a region that could.
34 #[derive(Debug, Clone, Default)]
35 pub(crate) struct Hidden<'a> {
36 /// The regions that are not out, by [`Slot::id`].
37 pub(crate) regions: Vec<&'a str>,
38 /// The questions that are not out, by [`Field::name`].
39 pub(crate) fields: Vec<&'a str>,
40 }
41
42 impl Hidden<'_> {
43 /// Everything applies.
44 pub(crate) const fn none() -> Self {
45 Self {
46 regions: Vec::new(),
47 fields: Vec::new(),
48 }
49 }
50
51 /// Whether this region does not apply right now.
52 pub(crate) fn out(&self, id: &str) -> bool {
53 self.regions.contains(&id)
54 }
55
56 /// Whether this question does not apply right now.
57 pub(crate) fn field_out(&self, name: &str) -> bool {
58 self.fields.contains(&name)
59 }
60 }
61
62 /// The ids of the regions on this screen that do not apply right now.
63 ///
64 /// Empty for a screen with no conditional region, which is nearly all of them,
65 /// and empty is what every caller with no view to read hands on: a region whose
66 /// condition nobody evaluated is drawn, which is the same direction the webview
67 /// degrades in when its script is not served.
68 pub(crate) fn hidden<'a>(screen: &'a Screen, chrome: &Chrome, view: &View) -> Hidden<'a> {
69 let mut found = Hidden::none();
70 for slot in &screen.slots {
71 walk(slot, screen, chrome, view, &mut found);
72 }
73 found
74 }
75
76 /// Whether one question is out, on the same terms as a region.
77 fn asked(field: &Field, screen: &Screen, chrome: &Chrome, view: &View) -> bool {
78 let Some(control) = field.watches() else {
79 return true;
80 };
81 field.revealed(held(control, screen, chrome, view))
82 }
83
84 /// The questions in one node that do not apply, added to the list.
85 ///
86 /// A form's fields and a standalone control. A field asked by an act is not
87 /// here: it is answered by the press that asked for it rather than drawn on the
88 /// screen.
89 fn questions<'a>(
90 node: &'a Node,
91 screen: &Screen,
92 chrome: &Chrome,
93 view: &View,
94 found: &mut Hidden<'a>,
95 ) {
96 let fields: &[Field] = match node {
97 Node::Field(field) => std::slice::from_ref(field.as_ref()),
98 Node::Form { fields, .. } => fields,
99 _ => &[],
100 };
101 for field in fields {
102 if !asked(field, screen, chrome, view) {
103 found.fields.push(field.name.as_str());
104 }
105 }
106 }
107
108 /// This region and the regions inside it, adding the ones that are not out.
109 ///
110 /// A region inside one that does not apply is not visited: it is not drawn
111 /// either way, and its own condition is a question about a screen it is not on.
112 fn walk<'a>(slot: &'a Slot, screen: &Screen, chrome: &Chrome, view: &View, found: &mut Hidden<'a>) {
113 if !out(slot, screen, chrome, view) {
114 found.regions.push(slot.id.as_str());
115 return;
116 }
117 for placed in slot
118 .run
119 .iter()
120 .flat_map(|run| run.members.iter())
121 .chain(slot.body.iter())
122 {
123 if let Node::Region(inner) = &placed.node {
124 walk(inner, screen, chrome, view, found);
125 } else {
126 questions(&placed.node, screen, chrome, view, found);
127 }
128 }
129 }
130
131 /// Whether one region is out, given what the reader has done so far.
132 fn out(slot: &Slot, screen: &Screen, chrome: &Chrome, view: &View) -> bool {
133 let Some(control) = slot.watches() else {
134 return true;
135 };
136 slot.revealed(held(control, screen, chrome, view))
137 }
138
139 /// What a control is holding: what was typed, or what the description offered.
140 ///
141 /// The same order a submit reads a form in, so a region comes out on an
142 /// untouched select resting on the value its description named, rather than
143 /// waiting for the reader to pick the value it is already showing.
144 ///
145 /// The chrome is searched after the screen because a panel outlives the screen
146 /// under it: a form kept on screen from everywhere can gate a section of
147 /// itself, and its fields are in no screen for [`Screen::holds`] to find.
148 fn held<'a>(
149 control: &str,
150 screen: &'a Screen,
151 chrome: &'a Chrome,
152 view: &'a View,
153 ) -> Option<&'a str> {
154 view.edit(control).or_else(|| {
155 screen.holds(control).or_else(|| {
156 chrome
157 .panels
158 .iter()
159 .find_map(|panel| panel.content.holds(control))
160 })
161 })
162 }
163