makeover-layout 0.48.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::{Column, Figure, RowPart};

/// One labelled fact: what it is called, and what it says.
///
/// The definition-list pair every record screen has and no member named. Five
/// goingson panes were saying it as `list { row "Label" { meta value } }` --
/// the task pane's metadata, events, contacts, the mail reader and settings'
/// About -- and two of them carry a comment admitting the vocabulary had no
/// word for a definition list. The task pane's own doc calls a row "the nearest
/// thing the vocabulary has to a definition list", which is a workaround
/// describing itself.
///
/// # Why this is not a [`Column`] pair, or a one-row table
///
/// A table's columns are a promise about *many* rows: they earn their headings,
/// their sort, their narrowing rules and their floors because the same shape
/// repeats down the screen. A pane of facts has one of each, so every one of
/// those is machinery with nothing to do, and the heading row a table wants is
/// exactly what a facts pane must not draw.
///
/// The measured symptom, which is what makes this a member rather than a
/// preference: rendered as rows, each value starts after its own label, so
/// there is no value column and the eye cannot run down it. Four widths of a
/// ui-fuzz run found the same ragged edge on About, the event pane and the mail
/// reader.
///
/// # The rule a renderer owes it
///
/// **The values line up.** That is the whole point and the one thing a row list
/// cannot do: one label column wide enough for the longest label, every value
/// starting at the same place. A webview does it with a description list, a
/// terminal with two padded columns, egui with a two-column grid. None of them
/// may fall back to running the value straight after the label.
///
/// **The value is the fact and reads like one.** goingson had this backwards:
/// its values were drawn muted and its labels were not, so the part a person
/// came for was the quieter of the two. The label is the noun and reads back;
/// the value takes content. Same rule [`RowPart`] already states for a row's
/// primary against its meta, and the same one [`Figure`] states for a value
/// against its caption.
///
/// # What it is not
///
/// A form. A fact is read, not edited; the moment one of these needs to change
/// it is a `Field` and it belongs in a `form`. Nothing here carries a name to
/// submit under, deliberately, so the two cannot be confused at the call site.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Fact<'a> {
    /// What the fact is called. The noun, which reads back.
    pub label: &'a str,
    /// What it says, already formatted the way the app means it to read.
    ///
    /// Text, for [`Figure::value`]'s reason: only the app knows whether a date
    /// reads as `2d ago` or `20 Sep`, and a renderer handed a timestamp would
    /// have to guess. The five sites this member replaces were all formatting
    /// before they built the row.
    pub value: &'a str,
}

impl<'a> Fact<'a> {
    /// A fact, from its label and its value.
    #[must_use]
    pub const fn new(label: &'a str, value: &'a str) -> Self {
        Self { label, value }
    }

    /// Whether the value is worth drawing.
    ///
    /// A record screen reads its facts off a row that has holes in it -- no
    /// project, no due date, no timezone -- and every one of the five sites was
    /// guarding its row with a `when` for exactly that. A renderer skips an
    /// empty fact rather than drawing a label with nothing after it, which is
    /// the one reading that is never wanted: a labelled blank says the value is
    /// missing, when what is true is that there is no such fact.
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.value.is_empty()
    }
}