blockworx_paint/text.rs
1//! Text layout: the one thing a drawing cannot do for itself.
2//!
3//! Shaping, kerning and line breaking come from the host's own text engine
4//! rather than from an approximation of it, so a diagram exported with
5//! different line breaks than the canvas showed is unrepresentable. The
6//! backend implements [`TextLayout`]; the export consumes it.
7
8use blockworx_geom::{Vec2, WorldPx};
9
10use crate::{Font, FontChoice, Zoom};
11
12/// The glyph one column of a font is as wide as — the one CSS's `ch` unit
13/// measures.
14pub const COLUMN_GLYPH: &str = "0";
15
16/// A host's text engine, as the exporters need to read it.
17pub trait TextLayout {
18 /// The typeface this engine lays text out in. The SVG export traces its
19 /// outlines from that face's own bytes, so an engine that answered for a
20 /// different one would place one font's glyphs at another's pen positions.
21 fn typeface(&self) -> FontChoice;
22
23 /// Lay `text` out in `font`, word-wrapped at `wrap`
24 /// ([`WorldPx::UNBOUNDED`] for an unwrapped run). An exporter's engine
25 /// lays out at one pixel per point, so a resolution-independent format
26 /// inherits no display's DPI; the screen's lays out at the display's, so
27 /// a mark recorded through it lands where the screen draws it.
28 fn layout(&self, text: &str, font: &Font, wrap: WorldPx) -> Layout;
29}
30
31impl<T: TextLayout + ?Sized> TextLayout for &T {
32 fn typeface(&self) -> FontChoice {
33 (**self).typeface()
34 }
35
36 fn layout(&self, text: &str, font: &Font, wrap: WorldPx) -> Layout {
37 (**self).layout(text, font, wrap)
38 }
39}
40
41/// A laid-out run: what it measures, and where every glyph in it landed.
42#[derive(Clone, Debug)]
43pub struct Layout {
44 /// The run's bounding size — the widest row by the stack of all of them.
45 pub size: Vec2,
46 pub rows: Vec<Row>,
47}
48
49impl Layout {
50 /// This layout with every length divided by `zoom`: a run laid out at a
51 /// zoomed font, stated in the units it was asked for in.
52 #[must_use]
53 pub fn scaled_down(mut self, zoom: Zoom) -> Self {
54 let by = zoom.get();
55 self.size /= by;
56 for row in &mut self.rows {
57 row.pos /= by;
58 row.height /= by;
59 for glyph in &mut row.glyphs {
60 glyph.pos /= by;
61 glyph.advance /= by;
62 glyph.ascent /= by;
63 }
64 }
65 self
66 }
67}
68
69/// One row of a [`Layout`].
70#[derive(Clone, Debug)]
71pub struct Row {
72 /// The row's origin, relative to the layout's.
73 pub pos: Vec2,
74 /// The row's height — the line's, whether or not a glyph is on it.
75 pub height: f32,
76 /// Whether this row closes a paragraph — a row broken by a `\n` rather
77 /// than by the wrap width. The newline itself is no glyph of its own.
78 pub ends_with_newline: bool,
79 pub glyphs: Vec<Glyph>,
80}
81
82/// A glyph's index in its face — the identity a shaper resolves, which the
83/// character alone does not carry: `fi` in a face with `liga` is one glyph
84/// filed under two characters.
85#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
86pub struct GlyphId(pub u16);
87
88/// One placed glyph. A layout records the *character* a glyph stands for, not
89/// the glyph a shaper chose for it, so a many-to-one substitution arrives as
90/// its head plus zero-[`advance`](Self::advance) continuations.
91#[derive(Clone, Copy, Debug)]
92pub struct Glyph {
93 pub chr: char,
94 /// The glyph the shaper chose, where the engine reports one: `None` at a
95 /// continuation, whose ink is already drawn at its head, and throughout a
96 /// layout from an engine that reports characters only.
97 pub id: Option<GlyphId>,
98 /// Baseline position, relative to the row's origin.
99 pub pos: Vec2,
100 /// The pen advance this glyph carries.
101 pub advance: f32,
102 /// How far the font's baseline sits below the line's top edge.
103 pub ascent: f32,
104}