tokens.rs raw

//! Everything a markdown document *looks* like, as tokens a theme sets.
//!
//! Nothing below is painted from a literal. A heading's size, a code block's
//! background, a quote's rule, the bullet glyph — every one of them is a
//! convention rather than a law, so every one of them is a `(token …)` in
//! `markdown.gdw` and a slot here, read out of the computed style through the
//! same [`StyleReader`] every builtin uses. Paint code reads these and knows no
//! colors of its own, exactly as a button's does.
//!
//! ## Where the floor comes from, and why it is here
//!
//! A builtin's token floor is [`guiduck_core::style::fallback_theme`]: one
//! table, stated as a theme, with a two-way coverage test tying it to the
//! registry's declared token set. **An application's widget cannot contribute
//! to it** — the fallback theme is built in `guiduck-core` from types
//! (`Rule`, `StyleValue`) it does not export a constructor for, and the
//! registry a theme is checked against is not something a widget crate holds.
//! So this table is markdown's floor, and it is a Rust literal table rather
//! than a theme.
//!
//! It is still *one* table, which is the property that matters: a token is
//! written down here and nowhere else. What is genuinely missing relative to a
//! builtin is the coverage test — nothing forces `markdown.gdw`'s `(token …)`
//! list and the fields below to agree, so a token could be declared and never
//! read, or read and never declarable. See the crate docs.

use guiduck_core::style::{ComputedStyle, StyleReader};
use guiduck_core::text::LinkStyle;
use guiduck_scene::paint::{Brush, Color};

/// The number of heading levels markdown has. Six, because CommonMark says so —
/// this one is a law, not a convention, which is why it is a constant and not a
/// token.
pub const HEADING_LEVELS: usize = 6;

pub struct MarkdownTokens {
    /// Body prose.
    pub color: Brush,
    pub font_size: f64,
    pub font_family: String,

    /// What a `[text](target)` looks like. The same two token names a `text`
    /// reads, because it is the same question and
    /// [`LinkStyle`] is the same answer.
    pub link: LinkStyle,

    /// The type scale, by level: `heading-1-font-size` … `heading-6-font-size`.
    ///
    /// Six numbers rather than one ratio: a type scale is a designer's table,
    /// and a geometric progression is one *particular* table. Making it a ratio
    /// would move the choice of scale into paint code, which is the thing this
    /// module exists to prevent.
    pub heading_font_sizes: [f64; HEADING_LEVELS],
    pub heading_color: Brush,
    /// 400 regular, 700 bold. That headings are bold at all is a convention.
    pub heading_weight: f64,

    pub code_color: Brush,
    pub code_background: Brush,
    pub code_font_family: String,
    pub code_font_size: f64,
    /// The inset between a code block's background and its text.
    pub code_padding: f64,

    /// The rule down the left of a block quote, and how far the quote's content
    /// is pushed in past it.
    pub quote_color: Brush,
    pub quote_width: f64,
    pub quote_indent: f64,

    /// A thematic break (`---`).
    pub rule_color: Brush,
    pub rule_width: f64,

    /// A table's grid lines, the shading behind its header, the inset between a
    /// cell's border and its text, and the least space a column is given before
    /// its text starts to wrap.
    pub table_border_color: Brush,
    pub table_border_width: f64,
    pub table_header_background: Brush,
    pub table_cell_padding: f64,
    pub table_min_column_width: f64,

    /// The vertical space between two blocks.
    pub block_gap: f64,
    /// How far a list item's body sits in from its marker.
    pub list_indent: f64,
    /// The marker of an unordered item. A glyph, because "•" is a convention —
    /// a theme may want "–" or "▪" and should not need a rebuild to say so.
    pub bullet: String,
}

impl Default for MarkdownTokens {
    fn default() -> Self {
        Self {
            color: Color::from_rgba8(0x00, 0x00, 0x00, 0xff).into(),
            font_size: 14.0,
            // The generic families, not a face: what "sans-serif" resolves to
            // is the font collection's business, and naming a face here would
            // be this crate deciding what is installed.
            font_family: "sans-serif".to_owned(),
            link: LinkStyle {
                color: Color::from_rgba8(0x30, 0x60, 0xc0, 0xff).into(),
                underline_width: 1.0,
            },
            heading_font_sizes: [28.0, 22.0, 18.0, 16.0, 14.0, 13.0],
            heading_color: Color::from_rgba8(0x00, 0x00, 0x00, 0xff).into(),
            heading_weight: 700.0,
            code_color: Color::from_rgba8(0x20, 0x20, 0x20, 0xff).into(),
            code_background: Color::from_rgba8(0xf0, 0xf0, 0xf0, 0xff).into(),
            code_font_family: "monospace".to_owned(),
            code_font_size: 13.0,
            code_padding: 6.0,
            quote_color: Color::from_rgba8(0xa0, 0xa0, 0xa0, 0xff).into(),
            quote_width: 3.0,
            quote_indent: 12.0,
            rule_color: Color::from_rgba8(0xc0, 0xc0, 0xc0, 0xff).into(),
            rule_width: 1.0,
            table_border_color: Color::from_rgba8(0xc0, 0xc0, 0xc0, 0xff).into(),
            table_border_width: 1.0,
            table_header_background: Color::from_rgba8(0xf0, 0xf0, 0xf0, 0xff).into(),
            table_cell_padding: 6.0,
            table_min_column_width: 48.0,
            block_gap: 8.0,
            list_indent: 18.0,
            bullet: "\u{2022}".to_owned(),
        }
    }
}

impl MarkdownTokens {
    /// Copy every token out of a computed style, reporting whether any of them
    /// landed differently. A token the theme does not carry leaves the slot
    /// alone — the standing [`StyleReader`] contract.
    pub fn read(&mut self, style: &ComputedStyle) -> bool {
        let mut reader = StyleReader::new(style);
        reader.brush(&mut self.color, "color");
        reader.number(&mut self.font_size, "font-size");
        reader.string(&mut self.font_family, "font-family");
        for (level, size) in self.heading_font_sizes.iter_mut().enumerate() {
            reader.number(size, HEADING_FONT_SIZE_TOKENS[level]);
        }
        reader.brush(&mut self.heading_color, "heading-color");
        reader.number(&mut self.heading_weight, "heading-weight");
        reader.brush(&mut self.code_color, "code-color");
        reader.brush(&mut self.code_background, "code-background");
        reader.string(&mut self.code_font_family, "code-font-family");
        reader.number(&mut self.code_font_size, "code-font-size");
        reader.number(&mut self.code_padding, "code-padding");
        reader.brush(&mut self.quote_color, "quote-color");
        reader.number(&mut self.quote_width, "quote-width");
        reader.number(&mut self.quote_indent, "quote-indent");
        reader.brush(&mut self.rule_color, "rule-color");
        reader.number(&mut self.rule_width, "rule-width");
        reader.brush(&mut self.table_border_color, "table-border-color");
        reader.number(&mut self.table_border_width, "table-border-width");
        reader.brush(&mut self.table_header_background, "table-header-background");
        reader.number(&mut self.table_cell_padding, "table-cell-padding");
        reader.number(&mut self.table_min_column_width, "table-min-column-width");
        reader.number(&mut self.block_gap, "block-gap");
        reader.number(&mut self.list_indent, "list-indent");
        reader.string(&mut self.bullet, "bullet");
        // `LinkStyle` reads its own two, so `link-color` means the same thing
        // on a `markdown` as it does on a `text`.
        let link_changed = self.link.read(style);
        reader.changed() || link_changed
    }

    /// The size of a heading at `level` (1–6), clamped to the levels that
    /// exist.
    pub fn heading_font_size(&self, level: u8) -> f64 {
        let index = usize::from(level).clamp(1, HEADING_LEVELS) - 1;
        self.heading_font_sizes[index]
    }
}

/// The token name of each heading level's size, indexed by level - 1. The
/// widget's only statement of these names; `markdown.gdw` declares the same
/// six.
pub const HEADING_FONT_SIZE_TOKENS: [&str; HEADING_LEVELS] = [
    "heading-1-font-size",
    "heading-2-font-size",
    "heading-3-font-size",
    "heading-4-font-size",
    "heading-5-font-size",
    "heading-6-font-size",
];