Skip to main content

max / makeover-layout

12.1 KB · 295 lines History Blame Raw
1 // Names this module's prose links to, resolved for rustdoc.
2 #[allow(unused_imports)]
3 use crate::{Field, FieldKind, Fill, Region, RowPart};
4
5 /// How much room a placement asks for.
6 ///
7 /// A column says it, and so does a [`Field`]. An intent, so the actual floor
8 /// stays with `makeover-geometry`. goingson's task table spells these as
9 /// `minmax(200px, 1fr)`, `140px` and content-sized; only the first three words
10 /// of that survive deferral.
11 /// `#[non_exhaustive]`, for the reason [`Fill`] and [`FieldKind`] are: a
12 /// renderer matches on this and a vocabulary that grows must not break every
13 /// renderer when it does.
14 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15 #[non_exhaustive]
16 pub enum Width {
17 /// Takes what it needs and no more.
18 Content,
19 /// A fixed share, the same at every width.
20 Fixed,
21 /// Absorbs whatever is left over.
22 ///
23 /// **Several fills divide what is left equally.** Stated because it would
24 /// otherwise be undefined and each renderer would invent something, and
25 /// stated this way because equal division is the only sharing rule that
26 /// answers to "Any width, one answer" without a tiebreak: allocating in
27 /// declaration order makes the result depend on the order the description
28 /// was written in, which is a fact about the source file and not about the
29 /// screen. It documents what both renderers already do — CSS grid gives
30 /// `1fr 1fr`, ratatui gives each a `Constraint::Fill(1)` — rather than
31 /// changing anything.
32 ///
33 /// So a row of fills is a legal thing to describe, and there is no rule
34 /// against it.
35 Fill,
36 }
37
38 /// What a member is worth when there is not room for all of them.
39 ///
40 /// Written for table columns and no longer only theirs. Three shapes ask the
41 /// same question and this answers all three: a table too narrow for its
42 /// columns, a row too narrow for its parts (see [`RowPart::priority`]), and a
43 /// group of regions sharing one run of room -- goingson's tab strip and the
44 /// [`Region::Band`] beside it, which is the case wiki `layout-room-and-fallback`
45 /// was ruled on. It is what any member of a group is worth, not a table
46 /// concept, and [`Fallback::Shed`] is what reads it.
47 ///
48 /// The doc below is the column argument, which is where the type was measured;
49 /// the sentence that gave it away is [`Priority::Essential`]'s, which was
50 /// already written about a row.
51 ///
52 /// Ordered: [`Priority::Optional`] drops first, [`Priority::Essential`] never
53 /// drops. This replaces addressing columns by position, which is what both
54 /// webview apps do today and is a live bug rather than only verbosity. goingson
55 /// hides mobile columns with `nth-child(n+5)` against a seven-column table, so
56 /// inserting a column silently hides the wrong one.
57 /// `#[non_exhaustive]`, same reasoning as [`Width`]. Note the ordering is the
58 /// whole point of the type, so a new tier has to be declared in its place in
59 /// the sequence rather than appended.
60 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
61 #[non_exhaustive]
62 pub enum Priority {
63 /// Dropped first.
64 Optional,
65 /// Dropped once the optional members are gone.
66 Secondary,
67 /// Never dropped. Without it the group does not identify itself.
68 Essential,
69 }
70
71 /// What a group does when it runs out of room.
72 ///
73 /// Authored, and required: the field carrying this has no `Default` and a group
74 /// cannot be described without saying what it does when it runs out of room.
75 /// Max ruled on that: more intentionality from layout designers is
76 /// acceptable so long as the constraints are solvable, because the goal is
77 /// enabling good layouts rather than rescuing bad ones. A default here would be
78 /// the crate guessing, and the guess would be silently wrong on the screens
79 /// that matter.
80 ///
81 /// Relief resolves inside-out. A group asks its children to fall back before
82 /// falling back itself, or an outer group collapses while an inner one still
83 /// had slack.
84 ///
85 /// # No `Swap`
86 ///
87 /// An authored alternate group for the tight case is deliberately out of the
88 /// first cut. It doubles the description for that group and the two halves can
89 /// drift, which is the failure this vocabulary exists to end. Add it when a
90 /// site proves it needs one.
91 ///
92 /// `#[non_exhaustive]`, [`Width`]'s reasoning. Unlike [`Priority`] there is no
93 /// order to preserve, so a member can be appended.
94 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
95 #[non_exhaustive]
96 pub enum Fallback {
97 /// One row becomes two. Every member stays, in the order described.
98 Wrap,
99 /// A row becomes a column. Every member stays, full width.
100 Stack,
101 /// Members drop by [`Priority`], down to [`Priority::Essential`].
102 ///
103 /// What a narrow table already does with its columns, applied to a group.
104 /// What drops is gone from the screen, so this is right when the dropped
105 /// members are facts the reader can do without and wrong when they are the
106 /// only way to act.
107 Shed,
108 /// The members [`Shed`](Self::Shed) would drop move into one overflow
109 /// control instead.
110 ///
111 /// The answer when a group holds actions. A control is not a fact: dropping
112 /// it does not cost the reader a detail, it costs them the only way to act,
113 /// which is [`RowPart::priority`]'s argument one level up.
114 Menu,
115 }
116
117 /// One column of a table.
118 ///
119 /// Described once. The grid track, the cell order and the drop behaviour are
120 /// all derived from this, rather than being three hand-written encodings that
121 /// must agree and are never checked against each other.
122 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123 pub struct Column<'a> {
124 /// The heading, and the name the cell is addressed by.
125 pub name: &'a str,
126 /// How much room it asks for.
127 pub width: Width,
128 /// What it is worth when room runs out.
129 pub priority: Priority,
130 /// Whether the user can reorder the table by this column.
131 ///
132 /// What reordering *calls* is not here — that is an address, and this
133 /// crate names none — so a host pairs this with the route the way it pairs
134 /// a row's parts with the row's activation. This says the affordance
135 /// exists, which is what a renderer needs to draw a header a user can
136 /// press rather than a heading they cannot.
137 pub sortable: bool,
138 /// Which way the table is ordered by this column, if it is.
139 ///
140 /// `None` on every column but the one in force. A renderer draws the caret
141 /// from this and a webview sets `aria-sort`, which is why it is per column
142 /// rather than a single fact on the table: the host idiom is a property of
143 /// the header cell.
144 ///
145 /// Independent of [`sortable`](Self::sortable) rather than implied by it,
146 /// because both combinations mean something. A column sorted and not
147 /// sortable is a list ordered by a key the user cannot change, which is a
148 /// real thing to describe and a caret worth drawing.
149 pub sorted: Option<Sort>,
150 }
151
152 /// Which way a column is ordered.
153 ///
154 /// Two, because there is no third. "Unsorted" is [`Column::sorted`] being
155 /// `None`, and folding it in here would be the same absence said twice.
156 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
157 pub enum Sort {
158 /// Smallest, earliest or first alphabetically at the top.
159 Ascending,
160 /// The other way.
161 Descending,
162 }
163
164 impl Sort {
165 /// The other direction, for a header that flips when pressed.
166 #[must_use]
167 pub const fn reversed(self) -> Self {
168 match self {
169 Self::Ascending => Self::Descending,
170 Self::Descending => Self::Ascending,
171 }
172 }
173
174 /// What a webview writes into `aria-sort`.
175 ///
176 /// Named here rather than in the webview renderer because a terminal and an
177 /// immediate-mode painter both want the same two words for a caret's label,
178 /// and three renderers picking their own is the drift this crate ends.
179 #[must_use]
180 pub const fn as_str(self) -> &'static str {
181 match self {
182 Self::Ascending => "ascending",
183 Self::Descending => "descending",
184 }
185 }
186
187 /// The caret a renderer draws for this direction.
188 ///
189 /// Here for [`as_str`](Self::as_str)'s reason, said about a glyph rather
190 /// than a word: three renderers picking their own is the drift this crate
191 /// ends. They had picked their own — two on the solid triangles and
192 /// `makeover-webview` on the arrows U+2191/U+2193 — and agreeing by
193 /// coincidence in three files is not agreement.
194 ///
195 /// The reason generalizes past this pair and is the house rule now —
196 /// prefer the bolder, simpler glyph over the thinner or more complicated
197 /// one. A third spelling is not open for re-argument.
198 ///
199 /// **Bare, with no spacing.** Where the gap goes is each renderer's
200 /// business: `makeover-tui` and `makeover-immediate` carry a leading space
201 /// inside their `TableStyle` string and a webview emits its own in
202 /// `content`, so folding a space in here would make one of the two wrong.
203 ///
204 /// Neither face the web apps self-host carries these — IBM Plex Mono has one
205 /// glyph in the whole geometric-shapes block and Lato has none — so a
206 /// browser falls back per glyph until the in-house face ships with them
207 /// drawn in (wiki `typography-standard`). Cosmetic
208 /// drift in one renderer, not a reason to spell it three ways.
209 #[must_use]
210 pub const fn glyph(self) -> &'static str {
211 match self {
212 Self::Ascending => "\u{25B2}",
213 Self::Descending => "\u{25BC}",
214 }
215 }
216 }
217
218 impl<'a> Column<'a> {
219 /// A column that absorbs slack and drops after the optional ones.
220 #[must_use]
221 pub const fn new(name: &'a str) -> Self {
222 Self {
223 name,
224 width: Width::Fill,
225 priority: Priority::Secondary,
226 sortable: false,
227 sorted: None,
228 }
229 }
230
231 /// Whether this column survives at the given cutoff.
232 ///
233 /// A renderer narrows by raising the cutoff, and never by counting
234 /// positions.
235 #[must_use]
236 pub const fn kept_at(&self, cutoff: Priority) -> bool {
237 (self.priority as u8) >= (cutoff as u8)
238 }
239 }
240
241 /// What a table cell holds.
242 ///
243 /// [`RowPart`] for tables, and it exists for the same reason: a part that
244 /// carries a control is not text, and a renderer with one class for the whole
245 /// cell paints it as though it were: a button in a cell inherits the cell's
246 /// content colour, which is the drift [`RowPart::intent`] prevents for rows.
247 ///
248 /// Four members, and the count is what quasi's `Cell` was measured to carry: a
249 /// value, tokens, actions and a link. Nothing was added past what something
250 /// holds.
251 ///
252 /// `#[non_exhaustive]` for [`RowPart`]'s reason: growth here must not be a
253 /// lockstep event across three renderers.
254 ///
255 /// # No hover-reveal
256 ///
257 /// This enum never gets one. A cell's actions are shown at rest in every
258 /// consumer measured, and a member nothing uses is one three renderers owe an
259 /// answer for.
260 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
261 #[non_exhaustive]
262 pub enum CellPart {
263 /// The cell's own text.
264 Value,
265 /// Small labelled things in the cell: a status badge, a chip.
266 Tokens,
267 /// Controls that act on what the row is about.
268 Actions,
269 /// The cell's value, where the value is itself a link.
270 Link,
271 }
272
273 impl CellPart {
274 /// The content intent the part takes.
275 ///
276 /// One part is text and three are not, so three answer with the intent
277 /// inheriting already gives. That is [`RowPart::intent`]'s shape with the
278 /// text side narrower: a cell's secondary and muted readings are the
279 /// column's business, not the cell's.
280 #[must_use]
281 pub const fn intent(self) -> &'static str {
282 match self {
283 Self::Value => "content",
284 // A token carries its own tone, and a part-level intent underneath
285 // it would fight the token sitting on it.
286 Self::Tokens => "content",
287 // Actions carry controls rather than text.
288 Self::Actions => "content",
289 // A link takes the action colour from the control it is, rather
290 // than the cell's text colour from the cell it sits in.
291 Self::Link => "content",
292 }
293 }
294 }
295