//! Text layout for slide text bodies. //! //! [`layout`] resolves every run's formatting through the placeholder/style chain and lays each //! paragraph out in the order bidirectional text needs: //! 1. embedding levels for the whole logical paragraph (UAX #9, base level from `a:pPr/@rtl`); //! 2. itemization by level, script, face (per-cluster fallback through the `a:latin`/`a:ea`/`a:cs` //! slots and the theme's script fonts) and shaping style, each item shaped with the whole //! paragraph as context; paint-only attributes (colour, highlight, underline) never split a //! word, so Arabic joining and ligatures survive them; //! 3. line breaking in logical order (UAX #14 opportunities at cluster boundaries, greedy fill); //! 4. per line, rules L1/L2 reorder the characters visually (L4 mirroring is done by the shaper). //! //! It then places bullets and numbers from the paragraph's start edge (the right edge for RTL //! paragraphs), applies indents, spacing and alignment (kashida for `justLow`), anchors the block //! in the shape's text box, and shrinks text on overflow when the body asks for it. The result is //! positioned glyph runs in shape-local coordinates plus per-line caret geometry for editing: //! bidirectional carets with affinity, logical and visual movement, and selection as the union of //! visual boxes. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![forbid(unsafe_code)] mod bidi; pub mod datetime; use std::collections::HashMap; use std::sync::Arc; use deckcraft_color::Rgba; use deckcraft_fonts::script::{self, joins_following, joins_preceding}; use deckcraft_fonts::{FontDb, FontFace, FontSlot, ScriptTag, ShapeParams}; use deckcraft_geom::{Point, Rect}; use deckcraft_model::resolve::{self, Ctx}; use deckcraft_model::text::{Align, Anchor, AutoFit, BodyProps, Bullet, Caps, ParaProps, RunKind, RunProps, Spacing, Strike, TextBody, TextDir}; use deckcraft_model::{Fill, Shape}; use unicode_segmentation::UnicodeSegmentation; /// A run of glyphs in one face, size and colour. #[derive(Clone)] pub struct GlyphRun { pub face: Arc, /// Font size in points (after autofit scaling). pub size: f64, pub color: Rgba, /// Glyph id and pen position of its origin (baseline), in shape-local points. pub glyphs: Vec<(u32, f64, f64)>, /// Per glyph: the paragraph character it draws (index, char); empty for bullets. pub cells: Vec<(usize, char)>, /// Draw a heavier stroke because the family has no bold face. pub fake_bold: bool, /// Slant because the family has no italic face. pub fake_italic: bool, pub outline: Option<(Rgba, f64)>, /// Paragraph and character range this run covers (for hyperlinks, selection colours). pub para: usize, pub chars: (usize, usize), pub link: bool, /// Alpha of the fill (0–1). pub alpha: f64, pub gradient: Option, } impl std::fmt::Debug for GlyphRun { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("GlyphRun").field("family", &self.face.family).field("size", &self.size).field("glyphs", &self.glyphs.len()).finish() } } /// An underline, strikethrough or highlight box. #[derive(Clone, Debug)] pub struct Deco { pub rect: Rect, pub color: Rgba, /// Highlights draw behind text. pub behind: bool, /// Paragraph the decoration belongs to (by-paragraph animation). pub para: usize, } #[derive(Clone, Debug, Default)] pub struct LineInfo { pub para: usize, /// Character range [start, end) in the paragraph (end excludes the line's break/newline). pub start: usize, pub end: usize, pub top: f64, pub baseline: f64, pub bottom: f64, /// Caret x for each character boundary start..=end (len = end - start + 1), downstream: the /// leading edge of the character after the boundary (its right edge if it is right-to-left); /// the last entry is the trailing edge of the line's last character. pub caret_x: Vec, /// Column index (multi-column bodies). pub column: usize, /// Right-to-left paragraph. pub rtl: bool, /// Bidi level of each character start..end after rule L1 (odd = right-to-left). pub levels: Vec, /// Visual box (left, right) of each character start..end. Characters inside a grapheme have /// empty boxes; a ligature's characters share its width (GDEF ligature carets). pub edges: Vec<(f64, f64)>, /// Whether a caret may stand before each character start..end (grapheme boundaries). pub boundary: Vec, /// Visual extent (left, right) of the line's text. pub extent: (f64, f64), /// Distance from the paragraph's start edge to the line's text (indent, margin, bullet). pub start_offset: f64, } #[derive(Clone, Debug, Default)] pub struct TextLayout { pub runs: Vec, pub decos: Vec, pub lines: Vec, /// Inner text box (insets applied), shape-local. pub inner: Rect, /// Height of the laid-out text (before anchoring). pub content_height: f64, /// Widest line. pub content_width: f64, /// Autofit font scale used (1 = none). pub font_scale: f64, pub line_reduction: f64, pub overflow: bool, /// Text rotation about the shape centre, degrees (vertical text = 90/270). pub rotation: f64, /// Per paragraph: (first line index, line count). pub para_lines: Vec<(usize, usize)>, /// Per paragraph: whether a caret may stand at each character offset 0..=len (grapheme /// boundaries), for logical caret movement. pub boundaries: Vec>, } /// Values for fields (`slidenum`, `datetime…`, `footer`). `None` keeps the field's saved text. pub trait Fields { fn field(&self, kind: &str) -> Option; /// The value of field `kind` in a run of language `lang` (BCP 47, e.g. `en-US`): dates are /// written the language's way. fn field_in(&self, kind: &str, lang: Option<&str>) -> Option { let _ = lang; self.field(kind) } } pub struct NoFields; impl Fields for NoFields { fn field(&self, kind: &str) -> Option { if kind == "slidenum" { Some("1".into()) } else { None } } } pub struct Opts<'a> { /// The text box: the preset's text rectangle in shape-local space. pub rect: Rect, pub fields: &'a dyn Fields, /// Draw this text instead of the body (placeholder prompts), in this colour. pub prompt_color: Option, /// Ignore autofit shrinking (used to measure natural height). pub no_shrink: bool, } struct Style { face: Arc, size: f64, color: Rgba, alpha: f64, fake_bold: bool, fake_italic: bool, baseline: f64, spacing: f64, caps: Caps, underline: Option, strike: Option, highlight: Option, outline: Option<(Rgba, f64)>, link: bool, gradient: Option, /// "Regular", "Bold"… for the slot and fallback faces. style_name: &'static str, /// BCP 47 language (`a:rPr/@lang`), for shaping. lang: Option, /// The `a:ea` and `a:cs` families (theme references resolved), if the document names one. ea: Option, cs: Option, /// Heading font (`+mj-…`): the theme's major font set applies. major: bool, } /// One character of a paragraph after shaping. #[derive(Clone)] struct Cell { ch: char, style: usize, face: usize, /// Resolved bidi embedding level (before the line rules). level: u8, /// Glyphs of the shaping cluster this cell heads, from the cluster's visual left edge (gid, /// x, y). Empty for the other cells of a cluster. glyphs: Vec<(u32, f64, f64)>, /// This character's advance: its share of the cluster (ligature carets); 0 inside a grapheme. adv: f64, /// Index of the cell heading this character's shaping cluster. head: usize, /// A grapheme starts here: carets stand and lines break only before such cells. boundary: bool, } impl Cell { /// A line may start before this cell: a grapheme that starts a shaping cluster. fn breakable(&self, i: usize) -> bool { self.boundary && self.head == i } } struct ParaShaped { cells: Vec, styles: Vec