makeover_layout/column.rs
1// Names this module's prose links to, resolved for rustdoc.
2#[allow(unused_imports)]
3use 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]
16pub 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]
62pub 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
71impl Priority {
72 /// Every cutoff, weakest first: the order a narrowing renderer raises
73 /// through.
74 ///
75 /// The one list of tiers. A renderer walks this rather than writing its
76 /// own, so a tier added here is one every renderer narrows to.
77 pub const RAISING: &'static [Self] = &[Self::Optional, Self::Secondary, Self::Essential];
78}
79
80/// What the columns kept at a cutoff ask for.
81///
82/// Counted in `ch` and in columns, and never in a host's unit: each renderer
83/// turns it into its own with [`Budget::width`], which is the one gap model
84/// every native renderer budgets by.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
86#[non_exhaustive]
87pub struct Budget {
88 /// How many columns are kept.
89 pub columns: u32,
90 /// Their [`floor`](Column::floor)s, summed, in `ch`.
91 pub floors: u32,
92}
93
94impl Budget {
95 /// The width these columns need, at `ch` per `ch` and `gap` between
96 /// adjacent columns and at each edge.
97 ///
98 /// `floors * ch + gap * (columns + 1)`. A terminal passes one cell per `ch`
99 /// and its column spacing; an immediate-mode painter passes the width of a
100 /// figure and its cell padding on both sides. One formula, so a table
101 /// drops a column at the same declared width in both.
102 #[must_use]
103 pub fn width(self, ch: f32, gap: f32) -> f32 {
104 self.floors as f32 * ch + gap * (self.columns + 1) as f32
105 }
106}
107
108/// What the columns kept at `cutoff` ask for.
109#[must_use]
110pub fn budget(columns: &[Column<'_>], cutoff: Priority) -> Budget {
111 let rooms: Vec<ColumnRoom<'_>> = columns.iter().map(Column::room).collect();
112 ColumnRoom::budget(&rooms, cutoff)
113}
114
115/// The weakest cutoff whose kept columns `fits` accepts.
116///
117/// Raised through [`Priority::RAISING`] until the columns fit, and never past
118/// the strongest priority any column declares: the essential columns are what
119/// makes a row identify itself, so a window too narrow for them gets them
120/// squeezed rather than dropped. A table with no essential column therefore
121/// never rises to [`Priority::Essential`], where the budget would keep no
122/// column at all. Nothing here counts positions, so which column drops is a
123/// property of the column.
124#[must_use]
125pub fn cutoff_for(columns: &[Column<'_>], fits: impl Fn(Budget) -> bool) -> Priority {
126 let rooms: Vec<ColumnRoom<'_>> = columns.iter().map(Column::room).collect();
127 ColumnRoom::cutoff_for(&rooms, fits)
128}
129
130/// The lengths a description deferred, in a renderer's own unit.
131///
132/// [`Width`] says `Content`, `Fixed` or `Fill` and carries no magnitude,
133/// because a magnitude is an answer for one renderer. Each renderer holds this
134/// over its own unit (cells, points, CSS lengths) and looks a column up by
135/// name, because an app's columns are not all one size.
136#[derive(Debug, Clone, Copy, PartialEq, Default)]
137#[non_exhaustive]
138pub struct Sizing<'a, L> {
139 /// `(column name, length)`: the track for a [`Width::Fixed`] column and the
140 /// floor for a [`Width::Fill`] one.
141 pub lengths: &'a [(&'a str, L)],
142 /// Used for a column with no entry above.
143 pub fallback: L,
144}
145
146impl<'a, L: Copy> Sizing<'a, L> {
147 /// These lengths, and `fallback` for a column not among them.
148 #[must_use]
149 pub const fn new(lengths: &'a [(&'a str, L)], fallback: L) -> Self {
150 Self { lengths, fallback }
151 }
152
153 /// The length for a named column.
154 #[must_use]
155 pub fn length_for(&self, name: &str) -> L {
156 self.lengths
157 .iter()
158 .find(|(column, _)| *column == name)
159 .map_or(self.fallback, |(_, length)| *length)
160 }
161}
162
163/// What a group does when it runs out of room.
164///
165/// Authored, and required: the field carrying this has no `Default` and a group
166/// cannot be described without saying what it does when it runs out of room.
167/// More intentionality from layout designers is acceptable so long as the constraints are solvable, because the goal is
168/// enabling good layouts rather than rescuing bad ones. A default here would be
169/// the crate guessing, and the guess would be silently wrong on the screens
170/// that matter.
171///
172/// Relief resolves inside-out. A group asks its children to fall back before
173/// falling back itself, or an outer group collapses while an inner one still
174/// had slack.
175///
176/// # No `Swap`
177///
178/// An authored alternate group for the tight case is deliberately out of the
179/// first cut. It doubles the description for that group and the two halves can
180/// drift, which is the failure this vocabulary exists to end. Add it when a
181/// site proves it needs one.
182///
183/// `#[non_exhaustive]`, [`Width`]'s reasoning. Unlike [`Priority`] there is no
184/// order to preserve, so a member can be appended.
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
186#[non_exhaustive]
187pub enum Fallback {
188 /// One row becomes two. Every member stays, in the order described.
189 Wrap,
190 /// A row becomes a column. Every member stays, full width.
191 Stack,
192 /// Members drop by [`Priority`], down to [`Priority::Essential`].
193 ///
194 /// What a narrow table already does with its columns, applied to a group.
195 /// What drops is gone from the screen, so this is right when the dropped
196 /// members are facts the reader can do without and wrong when they are the
197 /// only way to act.
198 Shed,
199 /// The members [`Shed`](Self::Shed) would drop move into one overflow
200 /// control instead.
201 ///
202 /// The answer when a group holds actions. A control is not a fact: dropping
203 /// it does not cost the reader a detail, it costs them the only way to act,
204 /// which is [`RowPart::priority`]'s argument one level up.
205 Menu,
206}
207
208/// The widest a column's [`floor`](Column::floor) may be, in `ch`.
209///
210/// A bound so a webview can name every floor as a class from a fixed ladder:
211/// a container condition cannot read a custom property, so the length a
212/// column hides its contents under has to be written into the stylesheet.
213/// Wider than any column the tree declares, and a column wanting more is a
214/// [`Width::Fill`] with this as its floor.
215pub const MIN_CEILING: u16 = 64;
216
217/// The floor an undeclared [`Width::Fill`] column takes, in `ch`.
218const FILL_FLOOR: u16 = 16;
219
220/// The number of characters in a heading, for a `ch`-based floor.
221///
222/// `str::chars().count()` is not const and [`Column::floor`] is, so this counts
223/// UTF-8 code points by hand. `len()` counts bytes, which over-reserves a
224/// multi-byte heading: a Cyrillic `Статус` reads as 12 `ch` rather than 6.
225const fn char_len(s: &str) -> usize {
226 let bytes = s.as_bytes();
227 let mut chars = 0;
228 let mut i = 0;
229 while i < bytes.len() {
230 chars += 1;
231 let byte = bytes[i];
232 i += if byte < 0x80 {
233 1
234 } else if byte < 0xE0 {
235 2
236 } else if byte < 0xF0 {
237 3
238 } else {
239 4
240 };
241 }
242 chars
243}
244
245/// One column of a table.
246///
247/// Described once. The grid track, the cell order and the drop behaviour are
248/// all derived from this, rather than being three hand-written encodings that
249/// must agree and are never checked against each other.
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
251#[non_exhaustive]
252pub struct Column<'a> {
253 /// The heading, and the name the cell is addressed by.
254 pub name: &'a str,
255 /// How much room it asks for.
256 pub width: Width,
257 /// What it is worth when room runs out.
258 pub priority: Priority,
259 /// What the column holds, which is what carries its look.
260 pub kind: ColumnKind,
261 /// The narrowest the column may be before it drops, in `ch`.
262 ///
263 /// wiki `frontend-unification`: collapse points derive from declared
264 /// minimums only, so every host narrows at the same declared width rather
265 /// than at its own breakpoints. `ch` because every host has a direct
266 /// answer for it: a terminal cell is one, an immediate-mode painter
267 /// measures one off its face, and a webview draws one as nine sixteenths
268 /// of the base, a figure at a table's text size with room to spare. Not
269 /// the CSS `ch`
270 /// itself, because a container condition resolves font-relative units
271 /// against a different font than the cell's, and the column would hide at
272 /// one width and be sized at another.
273 ///
274 /// `None` derives it from the kind and the heading, which is what most
275 /// columns want. Read it through [`floor`](Self::floor), never directly:
276 /// that is where the derivation, the rounding and the ceiling live, and a
277 /// host reading this field would narrow somewhere the other two do not.
278 pub min: Option<u16>,
279 /// Whether the user can reorder the table by this column.
280 ///
281 /// What reordering *calls* is not here (that is an address, and this
282 /// crate names none) so a host pairs this with the route the way it pairs
283 /// a row's parts with the row's activation. This says the affordance
284 /// exists, which is what a renderer needs to draw a header a user can
285 /// press rather than a heading they cannot.
286 pub sortable: bool,
287 /// Which way the table is ordered by this column, if it is.
288 ///
289 /// `None` on every column but the one in force. A renderer draws the caret
290 /// from this and a webview sets `aria-sort`, which is why it is per column
291 /// rather than a single fact on the table: the host idiom is a property of
292 /// the header cell.
293 ///
294 /// Independent of [`sortable`](Self::sortable) rather than implied by it,
295 /// because both combinations mean something. A column sorted and not
296 /// sortable is a list ordered by a key the user cannot change, which is a
297 /// real thing to describe and a caret worth drawing.
298 pub sorted: Option<Sort>,
299}
300
301/// What a column holds.
302///
303/// wiki `table-model`: the kind is what carries a column's look, and four of
304/// the reference table's six rules were per kind rather than per table. A
305/// description states what the column is; each renderer decides what that
306/// looks like in its host, from the facts below rather than from the name.
307///
308/// `#[non_exhaustive]`, [`Width`]'s reasoning.
309#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
310#[non_exhaustive]
311pub enum ColumnKind {
312 /// Prose. Wraps. What a column is when it says nothing else.
313 #[default]
314 Text,
315 /// A name a machine made: a fingerprint, a hash, a slug. Monospace, and
316 /// breaks mid-string rather than widening the table.
317 Identifier,
318 /// A point in time. Never wraps, so a date cannot print over its
319 /// neighbour, and its figures line up down the column.
320 Date,
321 /// A quantity. Figures line up and the column aligns to its end, so
322 /// magnitudes compare by length.
323 Number,
324 /// Source text, one row per line. Monospace and never wrapped. A table
325 /// holding one is read as code, so its rows are not striped or spaced as
326 /// records.
327 Code,
328 /// A state the row is in, drawn as a chip rather than as coloured text.
329 Status,
330 /// The row's controls. Aligns to its end and shrinks to what it holds.
331 Actions,
332}
333
334impl ColumnKind {
335 /// The narrowest a column of this kind reads at, in `ch`, when it declares
336 /// nothing narrower.
337 ///
338 /// A date is ten characters in the form every host writes, and a number
339 /// cut short is a different number, so the kinds that cannot truncate
340 /// floor at what they hold. Prose and identifiers wrap or break, so theirs
341 /// is only enough to be recognisable.
342 #[must_use]
343 pub const fn floor(self) -> u16 {
344 match self {
345 Self::Text | Self::Code | Self::Status | Self::Actions => 8,
346 Self::Identifier => 12,
347 Self::Date => 10,
348 Self::Number => 6,
349 }
350 }
351
352 /// Whether the column aligns to its end rather than its start.
353 #[must_use]
354 pub const fn aligns_end(self) -> bool {
355 matches!(self, Self::Number | Self::Actions)
356 }
357
358 /// Whether the column's text may wrap onto a second line.
359 ///
360 /// An identifier wraps by breaking mid-string, which is still a wrap: the
361 /// alternative is a fingerprint forcing the table wider than its pane.
362 #[must_use]
363 pub const fn wraps(self) -> bool {
364 matches!(self, Self::Text | Self::Identifier)
365 }
366
367 /// Whether the column is set in the monospace face.
368 #[must_use]
369 pub const fn monospace(self) -> bool {
370 matches!(self, Self::Identifier | Self::Code)
371 }
372
373 /// Whether figures in the column take equal widths.
374 #[must_use]
375 pub const fn tabular(self) -> bool {
376 matches!(self, Self::Date | Self::Number)
377 }
378}
379
380/// Which way a column is ordered.
381///
382/// Two, because there is no third. "Unsorted" is [`Column::sorted`] being
383/// `None`, and folding it in here would be the same absence said twice.
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
385#[non_exhaustive]
386pub enum Sort {
387 /// Smallest, earliest or first alphabetically at the top.
388 Ascending,
389 /// The other way.
390 Descending,
391}
392
393impl Sort {
394 /// The other direction, for a header that flips when pressed.
395 #[must_use]
396 pub const fn reversed(self) -> Self {
397 match self {
398 Self::Ascending => Self::Descending,
399 Self::Descending => Self::Ascending,
400 }
401 }
402
403 /// What a webview writes into `aria-sort`.
404 ///
405 /// Named here rather than in the webview renderer because a terminal and an
406 /// immediate-mode painter both want the same two words for a caret's label,
407 /// and three renderers picking their own is the drift this crate ends.
408 #[must_use]
409 pub const fn as_str(self) -> &'static str {
410 match self {
411 Self::Ascending => "ascending",
412 Self::Descending => "descending",
413 }
414 }
415
416 /// The caret a renderer draws for this direction.
417 ///
418 /// Here for [`as_str`](Self::as_str)'s reason, said about a glyph rather
419 /// than a word: three renderers picking their own is the drift this crate
420 /// ends. They had picked their own (two on the solid triangles and
421 /// `makeover-webview` on the arrows U+2191/U+2193) and agreeing by
422 /// coincidence in three files is not agreement.
423 ///
424 /// The reason generalizes past this pair and is the house rule now,
425 /// prefer the bolder, simpler glyph over the thinner or more complicated
426 /// one. A third spelling is not open for re-argument.
427 ///
428 /// **Bare, with no spacing.** Where the gap goes is each renderer's
429 /// business: `makeover-tui` and `makeover-immediate` carry a leading space
430 /// inside their `TableStyle` string and a webview emits its own in
431 /// `content`, so folding a space in here would make one of the two wrong.
432 ///
433 /// Neither face the web apps self-host carries these (IBM Plex Mono has one
434 /// glyph in the whole geometric-shapes block and Lato has none) so a
435 /// browser falls back per glyph until the in-house face ships with them
436 /// drawn in (wiki `typography-standard`). Cosmetic
437 /// drift in one renderer, not a reason to spell it three ways.
438 #[must_use]
439 pub const fn glyph(self) -> &'static str {
440 match self {
441 Self::Ascending => "\u{25B2}",
442 Self::Descending => "\u{25BC}",
443 }
444 }
445}
446
447impl<'a> Column<'a> {
448 /// A column that absorbs slack and never drops.
449 ///
450 /// [`Priority::Essential`], and the default is the whole of the argument.
451 /// A table that states no priorities used to empty itself wherever a
452 /// renderer narrows (head and cells together, every column having
453 /// inherited a droppable rank) which is what goingson's task table did.
454 /// Undeclared now means kept, so a column goes only when someone demotes
455 /// it, and the remaining failure is a cramped table rather than a blank
456 /// one. That is the same judgment [`kept_at`](Self::kept_at)'s renderers
457 /// make about a rank they have not learned: of the two ways to be wrong,
458 /// showing a column that should have gone is the one a reader can see and
459 /// work around.
460 #[must_use]
461 pub const fn new(name: &'a str) -> Self {
462 Self {
463 name,
464 width: Width::Fill,
465 priority: Priority::Essential,
466 kind: ColumnKind::Text,
467 min: None,
468 sortable: false,
469 sorted: None,
470 }
471 }
472
473 /// The same column, declaring the narrowest it may be before it drops.
474 #[must_use]
475 pub const fn min(mut self, ch: u16) -> Self {
476 self.min = Some(ch);
477 self
478 }
479
480 /// The same column, asking for this much room.
481 #[must_use]
482 pub const fn width(mut self, width: Width) -> Self {
483 self.width = width;
484 self
485 }
486
487 /// The same column, worth this much when room runs out.
488 #[must_use]
489 pub const fn priority(mut self, priority: Priority) -> Self {
490 self.priority = priority;
491 self
492 }
493
494 /// The same column, holding this kind of value.
495 #[must_use]
496 pub const fn kind(mut self, kind: ColumnKind) -> Self {
497 self.kind = kind;
498 self
499 }
500
501 /// The same column, reorderable by the user or not.
502 #[must_use]
503 pub const fn sortable(mut self, sortable: bool) -> Self {
504 self.sortable = sortable;
505 self
506 }
507
508 /// The same column, the one the table is ordered by, in this direction.
509 #[must_use]
510 pub const fn sorted(mut self, sort: Sort) -> Self {
511 self.sorted = Some(sort);
512 self
513 }
514
515 /// What this column asks the sizer for. See [`ColumnRoom`].
516 #[must_use]
517 pub const fn room(&self) -> ColumnRoom<'a> {
518 ColumnRoom {
519 heading: self.name,
520 width: self.width,
521 priority: self.priority,
522 kind: self.kind,
523 min: self.min,
524 caret: self.sortable || self.sorted.is_some(),
525 }
526 }
527
528 /// The narrowest this column is drawn, in `ch`. See [`ColumnRoom::floor`].
529 #[must_use]
530 pub const fn floor(&self) -> u16 {
531 self.room().floor()
532 }
533
534 /// Whether this column is kept at `cutoff`.
535 #[must_use]
536 pub const fn kept_at(&self, cutoff: Priority) -> bool {
537 self.room().kept_at(cutoff)
538 }
539}
540
541/// What one column asks of the sizer: the room it needs and what it is worth.
542///
543/// A layout record and nothing else. It holds the heading only because the
544/// heading is what a column must at least fit, and it says whether a sort caret
545/// sits beside it for the same reason; what the column holds, how it is
546/// ordered and what pressing it does are its describer's. Every renderer sizes
547/// a table from these, so the floor and the cutoff are one rule on every host
548/// whatever type the columns were described in.
549#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
550pub struct ColumnRoom<'a> {
551 /// The heading the column must at least fit.
552 pub heading: &'a str,
553 /// How much room it asks for.
554 pub width: Width,
555 /// What it is worth when room runs out.
556 pub priority: Priority,
557 /// What it holds, which carries a floor of its own.
558 pub kind: ColumnKind,
559 /// The narrowest it may be, in `ch`, when declared. `None` derives it.
560 pub min: Option<u16>,
561 /// Whether a sort caret sits beside the heading: the column is sortable,
562 /// or is the one the table is ordered by.
563 pub caret: bool,
564}
565
566impl<'a> ColumnRoom<'a> {
567 /// A text column under `heading` that fills and is essential, the defaults
568 /// a column takes when it says nothing.
569 #[must_use]
570 pub const fn new(heading: &'a str) -> Self {
571 Self {
572 heading,
573 width: Width::Fill,
574 priority: Priority::Essential,
575 kind: ColumnKind::Text,
576 min: None,
577 caret: false,
578 }
579 }
580
581 /// The narrowest this column may be before it drops, in `ch`.
582 ///
583 /// What every host narrows by: a column is kept at a cutoff only while the
584 /// floors of every column kept there fit. The declared [`min`](Self::min),
585 /// or else the widest of the kind's [`floor`](ColumnKind::floor), the
586 /// heading with two for its capitals and two more for a caret, and sixteen
587 /// for a [`Width::Fill`] column. Rounded up to an even count and held under
588 /// [`MIN_CEILING`], so a webview can carry the whole range as a fixed ladder
589 /// of classes and the other two hosts round the same way.
590 #[must_use]
591 pub const fn floor(&self) -> u16 {
592 let raw = match self.min {
593 Some(ch) => ch,
594 None => {
595 let caret = if self.caret { 2 } else { 0 };
596 // Characters, not bytes: `ch` is the width of a glyph, so a
597 // multi-byte heading counted by `len()` over-reserves (`Статус`
598 // reads as 12 ch rather than 6) and a non-Latin table drops
599 // columns sooner than the same table in another locale. Two more
600 // for the heading's capitals and tracking, which run wider than
601 // the figures a `ch` is measured off: `AMOUNT` at six ch is cut.
602 let chars = char_len(self.heading);
603 let heading = if chars > MIN_CEILING as usize {
604 MIN_CEILING
605 } else {
606 chars as u16 + 2 + caret
607 };
608 let kind = self.kind.floor();
609 // A column that takes the slack is the one holding prose, and
610 // floored at its kind it wraps a title onto three lines while
611 // the columns worth less still stand at full width.
612 let fill = if matches!(self.width, Width::Fill) {
613 FILL_FLOOR
614 } else {
615 0
616 };
617 let wider = if heading > kind { heading } else { kind };
618 if fill > wider { fill } else { wider }
619 }
620 };
621 // Clamped before rounding, so a declared `min(u16::MAX)` cannot
622 // overflow on its way to being held under the ceiling.
623 let raw = if raw > MIN_CEILING { MIN_CEILING } else { raw };
624 let even = raw + raw % 2;
625 if even < 2 {
626 2
627 } else if even > MIN_CEILING {
628 MIN_CEILING
629 } else {
630 even
631 }
632 }
633
634 /// Whether this column survives at the given cutoff.
635 ///
636 /// A renderer narrows by raising the cutoff, and never by counting
637 /// positions.
638 /// Whether this column is kept at `cutoff`.
639 #[must_use]
640 pub const fn kept_at(&self, cutoff: Priority) -> bool {
641 (self.priority as u8) >= (cutoff as u8)
642 }
643
644 /// What the columns kept at `cutoff` ask for.
645 #[must_use]
646 pub fn budget(rooms: &[Self], cutoff: Priority) -> Budget {
647 rooms
648 .iter()
649 .filter(|room| room.kept_at(cutoff))
650 .fold(Budget::default(), |budget, room| Budget {
651 columns: budget.columns + 1,
652 floors: budget.floors + u32::from(room.floor()),
653 })
654 }
655
656 /// The weakest cutoff whose kept columns `fits` accepts. See
657 /// [`cutoff_for`] for the rule.
658 #[must_use]
659 pub fn cutoff_for(rooms: &[Self], fits: impl Fn(Budget) -> bool) -> Priority {
660 let strongest = rooms
661 .iter()
662 .map(|room| room.priority)
663 .max()
664 .unwrap_or(Priority::Essential);
665 Priority::RAISING
666 .iter()
667 .copied()
668 .take_while(|&cutoff| cutoff <= strongest)
669 .find(|&cutoff| fits(Self::budget(rooms, cutoff)))
670 .unwrap_or(strongest)
671 }
672}
673
674/// What a table cell holds.
675///
676/// [`RowPart`] for tables, and it exists for the same reason: a part that
677/// carries a control is not text, and a renderer with one class for the whole
678/// cell paints it as though it were: a button in a cell inherits the cell's
679/// content colour, which is the drift [`RowPart::intent`] prevents for rows.
680///
681/// Four members, and the count is what quasi's `Cell` was measured to carry: a
682/// value, tokens, actions and a link. Nothing was added past what something
683/// holds.
684///
685/// `#[non_exhaustive]` for [`RowPart`]'s reason: growth here must not be a
686/// lockstep event across three renderers.
687///
688/// # No hover-reveal
689///
690/// This enum never gets one. A cell's actions are shown at rest in every
691/// consumer measured, and a member nothing uses is one three renderers owe an
692/// answer for.
693#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
694#[non_exhaustive]
695pub enum CellPart {
696 /// The cell's own text.
697 Value,
698 /// Small labelled things in the cell: a status badge, a chip.
699 Tokens,
700 /// Controls that act on what the row is about.
701 Actions,
702 /// The cell's value, where the value is itself a link.
703 Link,
704}
705
706impl CellPart {
707 /// The content intent the part takes.
708 ///
709 /// One part is text and three are not, so three answer with the intent
710 /// inheriting already gives. That is [`RowPart::intent`]'s shape with the
711 /// text side narrower: a cell's secondary and muted readings are the
712 /// column's business, not the cell's.
713 #[must_use]
714 pub const fn intent(self) -> &'static str {
715 match self {
716 Self::Value => "content",
717 // A token carries its own tone, and a part-level intent underneath
718 // it would fight the token sitting on it.
719 Self::Tokens => "content",
720 // Actions carry controls rather than text.
721 Self::Actions => "content",
722 // A link in a cell is its row's title, so it reads in the ink the
723 // values beside it do.
724 Self::Link => "content",
725 }
726 }
727}