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",
];