// Names this module's prose links to, resolved for rustdoc. #[allow(unused_imports)] use crate::{Field, FieldKind, Fill, Region, RowPart}; /// How much room a placement asks for. /// /// A column says it, and so does a [`Field`]. An intent, so the actual floor /// stays with `makeover-geometry`. goingson's task table spells these as /// `minmax(200px, 1fr)`, `140px` and content-sized; only the first three words /// of that survive deferral. /// `#[non_exhaustive]`, for the reason [`Fill`] and [`FieldKind`] are: a /// renderer matches on this and a vocabulary that grows must not break every /// renderer when it does. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum Width { /// Takes what it needs and no more. Content, /// A fixed share, the same at every width. Fixed, /// Absorbs whatever is left over. /// /// **Several fills divide what is left equally.** Stated because it would /// otherwise be undefined and each renderer would invent something, and /// stated this way because equal division is the only sharing rule that /// answers to "Any width, one answer" without a tiebreak: allocating in /// declaration order makes the result depend on the order the description /// was written in, which is a fact about the source file and not about the /// screen. It documents what both renderers already do — CSS grid gives /// `1fr 1fr`, ratatui gives each a `Constraint::Fill(1)` — rather than /// changing anything. /// /// So a row of fills is a legal thing to describe, and there is no rule /// against it. Fill, } /// What a member is worth when there is not room for all of them. /// /// Written for table columns and no longer only theirs. Three shapes ask the /// same question and this answers all three: a table too narrow for its /// columns, a row too narrow for its parts (see [`RowPart::priority`]), and a /// group of regions sharing one run of room -- goingson's tab strip and the /// [`Region::Band`] beside it, which is the case wiki `layout-room-and-fallback` /// was ruled on. It is what any member of a group is worth, not a table /// concept, and [`Fallback::Shed`] is what reads it. /// /// The doc below is the column argument, which is where the type was measured; /// the sentence that gave it away is [`Priority::Essential`]'s, which was /// already written about a row. /// /// Ordered: [`Priority::Optional`] drops first, [`Priority::Essential`] never /// drops. This replaces addressing columns by position, which is what both /// webview apps do today and is a live bug rather than only verbosity. goingson /// hides mobile columns with `nth-child(n+5)` against a seven-column table, so /// inserting a column silently hides the wrong one. /// `#[non_exhaustive]`, same reasoning as [`Width`]. Note the ordering is the /// whole point of the type, so a new tier has to be declared in its place in /// the sequence rather than appended. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[non_exhaustive] pub enum Priority { /// Dropped first. Optional, /// Dropped once the optional members are gone. Secondary, /// Never dropped. Without it the group does not identify itself. Essential, } /// What a group does when it runs out of room. /// /// Authored, and required: the field carrying this has no `Default` and a group /// cannot be described without saying what it does when it runs out of room. /// Max ruled on that: more intentionality from layout designers is /// acceptable so long as the constraints are solvable, because the goal is /// enabling good layouts rather than rescuing bad ones. A default here would be /// the crate guessing, and the guess would be silently wrong on the screens /// that matter. /// /// Relief resolves inside-out. A group asks its children to fall back before /// falling back itself, or an outer group collapses while an inner one still /// had slack. /// /// # No `Swap` /// /// An authored alternate group for the tight case is deliberately out of the /// first cut. It doubles the description for that group and the two halves can /// drift, which is the failure this vocabulary exists to end. Add it when a /// site proves it needs one. /// /// `#[non_exhaustive]`, [`Width`]'s reasoning. Unlike [`Priority`] there is no /// order to preserve, so a member can be appended. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum Fallback { /// One row becomes two. Every member stays, in the order described. Wrap, /// A row becomes a column. Every member stays, full width. Stack, /// Members drop by [`Priority`], down to [`Priority::Essential`]. /// /// What a narrow table already does with its columns, applied to a group. /// What drops is gone from the screen, so this is right when the dropped /// members are facts the reader can do without and wrong when they are the /// only way to act. Shed, /// The members [`Shed`](Self::Shed) would drop move into one overflow /// control instead. /// /// The answer when a group holds actions. A control is not a fact: dropping /// it does not cost the reader a detail, it costs them the only way to act, /// which is [`RowPart::priority`]'s argument one level up. Menu, } /// One column of a table. /// /// Described once. The grid track, the cell order and the drop behaviour are /// all derived from this, rather than being three hand-written encodings that /// must agree and are never checked against each other. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Column<'a> { /// The heading, and the name the cell is addressed by. pub name: &'a str, /// How much room it asks for. pub width: Width, /// What it is worth when room runs out. pub priority: Priority, /// Whether the user can reorder the table by this column. /// /// What reordering *calls* is not here — that is an address, and this /// crate names none — so a host pairs this with the route the way it pairs /// a row's parts with the row's activation. This says the affordance /// exists, which is what a renderer needs to draw a header a user can /// press rather than a heading they cannot. pub sortable: bool, /// Which way the table is ordered by this column, if it is. /// /// `None` on every column but the one in force. A renderer draws the caret /// from this and a webview sets `aria-sort`, which is why it is per column /// rather than a single fact on the table: the host idiom is a property of /// the header cell. /// /// Independent of [`sortable`](Self::sortable) rather than implied by it, /// because both combinations mean something. A column sorted and not /// sortable is a list ordered by a key the user cannot change, which is a /// real thing to describe and a caret worth drawing. pub sorted: Option, } /// Which way a column is ordered. /// /// Two, because there is no third. "Unsorted" is [`Column::sorted`] being /// `None`, and folding it in here would be the same absence said twice. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Sort { /// Smallest, earliest or first alphabetically at the top. Ascending, /// The other way. Descending, } impl Sort { /// The other direction, for a header that flips when pressed. #[must_use] pub const fn reversed(self) -> Self { match self { Self::Ascending => Self::Descending, Self::Descending => Self::Ascending, } } /// What a webview writes into `aria-sort`. /// /// Named here rather than in the webview renderer because a terminal and an /// immediate-mode painter both want the same two words for a caret's label, /// and three renderers picking their own is the drift this crate ends. #[must_use] pub const fn as_str(self) -> &'static str { match self { Self::Ascending => "ascending", Self::Descending => "descending", } } /// The caret a renderer draws for this direction. /// /// Here for [`as_str`](Self::as_str)'s reason, said about a glyph rather /// than a word: three renderers picking their own is the drift this crate /// ends. They had picked their own — two on the solid triangles and /// `makeover-webview` on the arrows U+2191/U+2193 — and agreeing by /// coincidence in three files is not agreement. /// /// The reason generalizes past this pair and is the house rule now — /// prefer the bolder, simpler glyph over the thinner or more complicated /// one. A third spelling is not open for re-argument. /// /// **Bare, with no spacing.** Where the gap goes is each renderer's /// business: `makeover-tui` and `makeover-immediate` carry a leading space /// inside their `TableStyle` string and a webview emits its own in /// `content`, so folding a space in here would make one of the two wrong. /// /// Neither face the web apps self-host carries these — IBM Plex Mono has one /// glyph in the whole geometric-shapes block and Lato has none — so a /// browser falls back per glyph until the in-house face ships with them /// drawn in (wiki `typography-standard`). Cosmetic /// drift in one renderer, not a reason to spell it three ways. #[must_use] pub const fn glyph(self) -> &'static str { match self { Self::Ascending => "\u{25B2}", Self::Descending => "\u{25BC}", } } } impl<'a> Column<'a> { /// A column that absorbs slack and drops after the optional ones. #[must_use] pub const fn new(name: &'a str) -> Self { Self { name, width: Width::Fill, priority: Priority::Secondary, sortable: false, sorted: None, } } /// Whether this column survives at the given cutoff. /// /// A renderer narrows by raising the cutoff, and never by counting /// positions. #[must_use] pub const fn kept_at(&self, cutoff: Priority) -> bool { (self.priority as u8) >= (cutoff as u8) } } /// What a table cell holds. /// /// [`RowPart`] for tables, and it exists for the same reason: a part that /// carries a control is not text, and a renderer with one class for the whole /// cell paints it as though it were: a button in a cell inherits the cell's /// content colour, which is the drift [`RowPart::intent`] prevents for rows. /// /// Four members, and the count is what quasi's `Cell` was measured to carry: a /// value, tokens, actions and a link. Nothing was added past what something /// holds. /// /// `#[non_exhaustive]` for [`RowPart`]'s reason: growth here must not be a /// lockstep event across three renderers. /// /// # No hover-reveal /// /// This enum never gets one. A cell's actions are shown at rest in every /// consumer measured, and a member nothing uses is one three renderers owe an /// answer for. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum CellPart { /// The cell's own text. Value, /// Small labelled things in the cell: a status badge, a chip. Tokens, /// Controls that act on what the row is about. Actions, /// The cell's value, where the value is itself a link. Link, } impl CellPart { /// The content intent the part takes. /// /// One part is text and three are not, so three answer with the intent /// inheriting already gives. That is [`RowPart::intent`]'s shape with the /// text side narrower: a cell's secondary and muted readings are the /// column's business, not the cell's. #[must_use] pub const fn intent(self) -> &'static str { match self { Self::Value => "content", // A token carries its own tone, and a part-level intent underneath // it would fight the token sitting on it. Self::Tokens => "content", // Actions carry controls rather than text. Self::Actions => "content", // A link takes the action colour from the control it is, rather // than the cell's text colour from the cell it sits in. Self::Link => "content", } } }