Skip to main content

blockworx_egui/
text.rs

1//! epaint's text engine behind [`TextLayout`], twice over: the context's own
2//! fonts at the display's resolution, which is what a frame drawn on screen
3//! is measured through, and a standalone engine at one pixel per point for a
4//! backendless consumer (the exporters), handed the layout glyph for glyph.
5
6use std::cell::RefCell;
7
8use blockworx_geom::WorldPx;
9use blockworx_paint::text::{Glyph, Layout, Row, TextLayout};
10use blockworx_paint::{Color, Font, FontChoice};
11use egui::epaint::{
12    Galley,
13    text::{Fonts, TextOptions},
14};
15
16use crate::convert::{IntoEgui as _, IntoGeom as _};
17use crate::font::build_fonts;
18
19/// Text is laid out resolution-independently, so no display's DPI is baked
20/// into what the layout says.
21const PIXELS_PER_POINT: f32 = 1.0;
22
23/// A standalone [`Fonts`] with the app's faces installed — the very engine
24/// that lays the diagram out on screen, driven without a `Context`.
25pub struct EpaintLayout {
26    typeface: FontChoice,
27    /// Laying out needs `&mut`; the font atlas it fills along the way is
28    /// never read.
29    fonts: RefCell<Fonts>,
30}
31
32impl EpaintLayout {
33    #[must_use]
34    pub fn new(typeface: FontChoice) -> Self {
35        Self {
36            typeface,
37            fonts: RefCell::new(Fonts::new(TextOptions::default(), build_fonts(typeface))),
38        }
39    }
40}
41
42impl TextLayout for EpaintLayout {
43    fn typeface(&self) -> FontChoice {
44        self.typeface
45    }
46
47    fn layout(&self, text: &str, font: &Font, wrap: WorldPx) -> Layout {
48        let galley = self
49            .fonts
50            .borrow_mut()
51            .with_pixels_per_point(PIXELS_PER_POINT)
52            .layout(
53                text.to_owned(),
54                font.egui(),
55                Color::PLACEHOLDER.egui(),
56                wrap.get(),
57            );
58        layout_of(&galley)
59    }
60}
61
62/// The fonts a context has installed, behind [`TextLayout`] — the very engine
63/// the screen draws with, at the display's resolution, so a run recorded
64/// through it is the size the replay draws it. Not an export's engine: what
65/// this measures inherits the display's DPI, which is the point of it.
66pub struct ContextLayout {
67    ctx: egui::Context,
68    typeface: FontChoice,
69}
70
71impl ContextLayout {
72    /// `typeface` is what the context's fonts were built from — a context
73    /// does not remember, and an export traced through this would need to.
74    #[must_use]
75    pub fn new(ctx: &egui::Context, typeface: FontChoice) -> Self {
76        Self {
77            ctx: ctx.clone(),
78            typeface,
79        }
80    }
81}
82
83impl TextLayout for ContextLayout {
84    fn typeface(&self) -> FontChoice {
85        self.typeface
86    }
87
88    fn layout(&self, text: &str, font: &Font, wrap: WorldPx) -> Layout {
89        let galley = self.ctx.fonts_mut(|fonts| {
90            fonts.layout(
91                text.to_owned(),
92                font.egui(),
93                Color::PLACEHOLDER.egui(),
94                wrap.get(),
95            )
96        });
97        layout_of(&galley)
98    }
99}
100
101/// A galley as the trait states it. Both engines lay out to be measured or
102/// traced, never blitted: whatever draws the glyphs sets its own ink, so
103/// they are laid out in epaint's "recolor me" sentinel rather than a color.
104fn layout_of(galley: &Galley) -> Layout {
105    Layout {
106        size: galley.size().geom(),
107        rows: galley
108            .rows
109            .iter()
110            .map(|row| Row {
111                pos: row.pos.to_vec2().geom(),
112                height: row.size.y,
113                ends_with_newline: row.ends_with_newline,
114                glyphs: row
115                    .glyphs
116                    .iter()
117                    .map(|glyph| Glyph {
118                        chr: glyph.chr,
119                        // epaint reports the character, never the glyph it
120                        // shaped; a consumer that needs the identity re-shapes.
121                        id: None,
122                        pos: glyph.pos.to_vec2().geom(),
123                        advance: glyph.advance_width,
124                        ascent: glyph.font_ascent,
125                    })
126                    .collect(),
127            })
128            .collect(),
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    /// `layout_no_wrap` is `layout` at an infinite width — the same
137    /// `LayoutJob`, so [`WorldPx::UNBOUNDED`] needs no method of its own.
138    #[test]
139    fn an_unbounded_wrap_is_the_no_wrap_layout() {
140        let engine = EpaintLayout::new(FontChoice::Basic);
141        let font = Font::canvas(12.0);
142        let unbounded = engine.layout("the quick brown fox", &font, WorldPx::UNBOUNDED);
143        let no_wrap = engine
144            .fonts
145            .borrow_mut()
146            .with_pixels_per_point(PIXELS_PER_POINT)
147            .layout_no_wrap(
148                "the quick brown fox".to_owned(),
149                (&font).egui(),
150                Color::PLACEHOLDER.egui(),
151            );
152        assert_eq!(unbounded.size, no_wrap.size().geom());
153        assert_eq!(
154            unbounded.rows.len(),
155            no_wrap.rows.len(),
156            "one row, unwrapped, either way",
157        );
158    }
159}