makeover-layout 0.44.2

The renderer-agnostic half of the make-family design system: what a thing IS, named as intents and relationships and never as values. Colour defers to makeover, spacing to makeover-geometry; what is left is composition.
Documentation
// 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<Sort>,
}

/// 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",
        }
    }
}