makeover_layout/act.rs
1use crate::{State, Tone};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4pub struct Act<'a> {
5 /// What the control says.
6 pub label: &'a str,
7 /// The key that reaches it where a host has keys.
8 ///
9 /// The one member written for a terminal before there was one. A webview
10 /// hangs it off `accesskey` or ignores it; a terminal has nothing else to
11 /// offer, so this is the whole of how a control is reached there.
12 pub key: Option<&'a str>,
13 /// What pressing it means. [`Tone::Danger`] is the destructive one.
14 pub tone: Tone,
15 /// Disabled, or nothing said.
16 ///
17 /// [`State::Disabled`] is what changes what a renderer may do: see
18 /// [`State::suppresses_interaction`], which is what says a disabled control
19 /// is drawn and not reachable. A control's focus is not sayable here at
20 /// all: see the crate header, "Reach, focus and the focus ring".
21 pub state: Option<State>,
22 /// A sentence that is always true of this control, shown rather than hunted
23 /// for.
24 ///
25 /// Standing help, not a message and not a tooltip. Half the hosts that read
26 /// this have no pointer: a hover is one spelling of it, and the shipped
27 /// apps reached for that spelling only because egui and a browser both had
28 /// one. What is being said is that the sentence is true, never that it is
29 /// hidden until a pointer arrives.
30 ///
31 /// # Why it is here rather than a layer up
32 ///
33 /// A hint left to `quasi_router::Act` alone means each renderer draws it
34 /// for itself: `makeover_tui` had no hint to read, so quasi-tui built
35 /// the muted line, and quasi-immediate called `on_hover_text` outside
36 /// [`crate::Act`] rather than inside it. `Field::hint` was here the whole
37 /// time, so the same idea sat at two layers depending on which thing
38 /// carried it, and a host that was not quasi could say it of a field and
39 /// not of a control.
40 ///
41 /// What kept it out was price rather than doubt: this crate declares
42 /// `links`, so a member here moves 25 manifests across 12 repos. That is a
43 /// release's forward-fix pass, which is a cost and was being read as a
44 /// barrier.
45 ///
46 /// # What a renderer owes it
47 ///
48 /// Somewhere to put it, or nothing. Dropping it is legitimate; drawing it
49 /// *instead of* the label is not, and neither is drawing it in a way that
50 /// takes it out of the accessible tree, which is the failure `title` alone
51 /// has on a browser. Nothing may live only in a hint.
52 ///
53 /// `None` is a control whose label is the whole of it, which is nearly all
54 /// of them.
55 pub hint: Option<&'a str>,
56 /// Whether pressing it commits: the one control that locks in what the
57 /// reader has staged.
58 ///
59 /// A form's submit, or the verb over a set of ticked rows. Wiki
60 /// `explicit-commit-affordance` is the ruling: changing a control stages a
61 /// change, and a separate, visible control commits it, and the commit is
62 /// what the reader sees. So the control has to be told apart from the
63 /// controls beside it that only navigate or stage, and wiki
64 /// `look-restoration` says how far that goes: the mark goes on the control
65 /// that commits and on nothing else, not on every control that writes.
66 ///
67 /// # What a renderer owes it
68 ///
69 /// A spelling the reader can pick out at a glance, and the same one every
70 /// time. A webview draws the default-button ring the Platinum specimen
71 /// picked and never shipped; a terminal writes `[ Label ]` where an
72 /// ordinary control is `< Label >`; an immediate-mode host strokes a frame
73 /// round the button. Nothing else changes: a committing control takes its
74 /// tone, its key, its state and its hint exactly as any other.
75 ///
76 /// # Why a member and not a renderer's inference
77 ///
78 /// Each renderer could guess it from the node a control sits in, and each
79 /// was left to: `makeover-tui` grew `filled_act` for a form's submit, which
80 /// no caller in the tree reached, while the webview drew every submit like
81 /// every other button. A fact the description has is said here once, which
82 /// is the reason [`hint`](Self::hint) is here too.
83 ///
84 /// `false` on nearly every control.
85 pub commits: bool,
86 /// Whether this is the act the screen is *for*: the one thing a reader
87 /// came to this screen to do.
88 ///
89 /// Reply on a mail reader, Start or Complete on a task, Queue on a
90 /// compose. At most one per screen, and most screens have none, because a
91 /// list is for reading.
92 ///
93 /// # Why it is not [`commits`](Self::commits)
94 ///
95 /// They answer different questions and a screen can want both on different
96 /// controls. `commits` is "pressing this locks in what you staged", which
97 /// wiki `explicit-commit-affordance` and `look-restoration` reserve for the
98 /// control that commits *and nothing else*. A sub-form's Add commits
99 /// honestly under that rule, so on goingson's task overview the boldest
100 /// control on the screen was a subtask's Add while the screen's own
101 /// Start/Complete was an ordinary button. Widening `commits` to mean
102 /// "primary" would break a settled ruling and put every sub-form's Add back
103 /// in competition; this is the other axis instead.
104 ///
105 /// The two compose. A form that *is* the screen's work carries both, and a
106 /// renderer draws both marks on it.
107 ///
108 /// # Why a member and not a renderer's inference
109 ///
110 /// [`commits`](Self::commits)' own test, applied to itself: which act a
111 /// screen is for is a fact the description has and no renderer can work
112 /// out. The nearest guess, that a band's acts outrank a body's, gets the
113 /// mail reader and the compose window wrong: their primary act sits in a
114 /// pane. It also makes emphasis a function of where a control happens to
115 /// sit.
116 ///
117 /// # What a renderer owes it
118 ///
119 /// A spelling distinct from the commit mark, since a control may wear
120 /// both. The suite's answer is that **leading fills and committing
121 /// outlines**: a webview gives it the `--action` ground with
122 /// `--content-on-action` on it while the commit ring stays a frame outside
123 /// the bevel; a terminal reverses it, keeping `[ ]` for the commit; an
124 /// immediate-mode host fills the button rather than stroking it. Fill and
125 /// frame are separable, so a control that is both reads as both.
126 ///
127 /// `false` on nearly every control, and on every control of a screen that
128 /// is only for reading.
129 pub leading: bool,
130}
131
132impl<'a> Act<'a> {
133 /// An ordinary control, reachable, with no key.
134 #[must_use]
135 pub const fn new(label: &'a str) -> Self {
136 Self {
137 label,
138 key: None,
139 tone: Tone::Neutral,
140 state: None,
141 hint: None,
142 commits: false,
143 leading: false,
144 }
145 }
146
147 /// The control that commits what the reader staged; see
148 /// [`commits`](Self::commits).
149 #[must_use]
150 pub const fn committing(mut self) -> Self {
151 self.commits = true;
152 self
153 }
154
155 /// The act this screen is for; see [`leading`](Self::leading).
156 ///
157 /// Separate from [`committing`](Self::committing) and freely combined with
158 /// it: a form that is the screen's own work is both.
159 #[must_use]
160 pub const fn leading(mut self) -> Self {
161 self.leading = true;
162 self
163 }
164
165 /// The sentence that is always true of it; see [`hint`](Self::hint).
166 ///
167 /// A renderer with nowhere to put it drops it, so this must never be the
168 /// only place a fact appears.
169 #[must_use]
170 pub const fn hinted(mut self, hint: &'a str) -> Self {
171 self.hint = Some(hint);
172 self
173 }
174
175 /// The key that reaches it.
176 #[must_use]
177 pub const fn key(mut self, key: &'a str) -> Self {
178 self.key = Some(key);
179 self
180 }
181
182 /// What pressing it means.
183 #[must_use]
184 pub const fn tone(mut self, tone: Tone) -> Self {
185 self.tone = tone;
186 self
187 }
188
189 /// Focus, or disabled.
190 #[must_use]
191 pub const fn state(mut self, state: State) -> Self {
192 self.state = Some(state);
193 self
194 }
195
196 /// Whether the control is drawn and does not answer.
197 #[must_use]
198 pub fn disabled(&self) -> bool {
199 self.state.is_some_and(State::suppresses_interaction)
200 }
201}