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