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