Skip to main content

TextEngine

Struct TextEngine 

Source
pub struct TextEngine { /* private fields */ }
Expand description

Fonts, a bounded glyph cache, and everything that needs both.

One of these per application. It is &mut for measurement as well as drawing, because measuring is what populates the cache: a label measured during layout and drawn a moment later rasterises its glyphs once, and a label measured on every layout pass and never redrawn pays a cache lookup rather than an outline computation each time.

Implementations§

Source§

impl TextEngine

Source

pub fn new() -> Self

An engine with the built-in bitmap font registered as FontId(0), and a 64 KB glyph cache.

FontId(0) is always the built-in font, in every configuration, so a widget that names no font gets something that certainly exists.

Examples found in repository?
examples/specimen.rs (line 38)
29fn main() -> std::io::Result<()> {
30    let mut args = std::env::args().skip(1);
31    let path = args.next().unwrap_or_else(|| "specimen.ppm".to_owned());
32    let font_path = args.next();
33    // A third argument replaces the sample, which is how you check whether a
34    // given tier can actually draw the script a panel has to show.
35    let sample = args.next();
36    let sample = sample.as_deref().unwrap_or(SAMPLE);
37
38    let mut engine = TextEngine::new();
39    // `mut` only when a second face can be added, which is feature-dependent.
40    #[allow(unused_mut)]
41    let mut faces = vec![(TextStyle::built_in(16).font, "built-in 5x7".to_owned())];
42
43    #[cfg(feature = "truetype")]
44    if let Some(font_path) = &font_path {
45        let data = std::fs::read(font_path)?;
46        match denise_text::TrueTypeSource::from_bytes(font_path, &data) {
47            Ok(source) => {
48                let name = source.name().to_owned();
49                let id = engine.add_font(Box::new(source));
50                faces.push((id, name));
51            }
52            Err(error) => eprintln!("could not parse {font_path}: {error}"),
53        }
54    }
55    #[cfg(feature = "shaping")]
56    if let Some(font_path) = &font_path {
57        let data = std::fs::read(font_path)?;
58        match denise_text::ShapedSource::from_fonts("shaped", [data]) {
59            Ok(source) => {
60                let id = engine.add_font(Box::new(source));
61                faces.push((id, "shaped (cosmic-text)".to_owned()));
62            }
63            Err(error) => eprintln!("could not build a shaper: {error}"),
64        }
65    }
66    #[cfg(not(any(feature = "truetype", feature = "shaping")))]
67    if font_path.is_some() {
68        eprintln!("built without --features truetype; ignoring the font path");
69    }
70
71    let theme: Theme = theme::DARK;
72    let mut pixels = vec![0u32; (SIZE.width * SIZE.height) as usize];
73    {
74        let mut frame = Frame::new(
75            &mut pixels,
76            SIZE,
77            SIZE.width,
78            PixelFormat::Xrgb8888,
79            BufferAge::Undefined,
80        )
81        .expect("frame");
82        let mut raster = Canvas::new(&mut frame);
83        let mut canvas = raster.pen();
84        canvas.clear(theme.color(Role::Base100));
85
86        let mut y = 16;
87        for (font, name) in &faces {
88            let heading = TextStyle {
89                font: *font,
90                size_px: 16,
91            };
92            engine.draw(
93                &mut canvas,
94                heading,
95                Point::new(16, y),
96                name,
97                theme.color(Role::Accent),
98            );
99            y += engine.line_height(heading) + 6;
100
101            for size in SIZES {
102                let style = TextStyle {
103                    font: *font,
104                    size_px: size,
105                };
106                let snapped = engine.snap_size(style);
107                let label = format!("{size}px");
108                engine.draw(
109                    &mut canvas,
110                    TextStyle::built_in(8),
111                    Point::new(16, y + 4),
112                    &label,
113                    theme.color(Role::Base300),
114                );
115                engine.draw(
116                    &mut canvas,
117                    style,
118                    Point::new(64, y),
119                    sample,
120                    theme.color(Role::BaseContent),
121                );
122                if snapped != size {
123                    engine.draw(
124                        &mut canvas,
125                        TextStyle::built_in(8),
126                        Point::new(16, y + 14),
127                        &format!("→{snapped}"),
128                        theme.color(Role::Warning),
129                    );
130                }
131                y += engine.line_height(style).max(12) + 4;
132            }
133
134            // A pangram at a readable size, to show spacing rather than shapes.
135            let body = TextStyle {
136                font: *font,
137                size_px: 16,
138            };
139            let width = engine.measure_line(body, PANGRAM);
140            engine.draw(
141                &mut canvas,
142                body,
143                Point::new(16, y),
144                PANGRAM,
145                theme.color(Role::BaseContent),
146            );
147            y += engine.line_height(body) + 16;
148            eprintln!("{name}: pangram is {width} px wide at 16 px");
149        }
150
151        // A frame around the last line, to show that measurement and ink agree.
152        let stats = engine.stats();
153        eprintln!(
154            "{} glyphs cached, {} hits, {} misses, {} resets",
155            engine.atlas().len(),
156            stats.hits,
157            stats.misses,
158            stats.resets
159        );
160        canvas.stroke_rect(
161            Rect::new(8, 8, SIZE.width as i32 - 16, y.min(SIZE.height as i32) - 8),
162            1,
163            theme.color(Role::Base300),
164        );
165    }
166
167    let mut out = std::io::BufWriter::new(std::fs::File::create(&path)?);
168    write!(out, "P6\n{} {}\n255\n", SIZE.width, SIZE.height)?;
169    for word in &pixels {
170        out.write_all(&[(word >> 16) as u8, (word >> 8) as u8, *word as u8])?;
171    }
172    out.flush()?;
173    eprintln!("wrote {path}");
174    Ok(())
175}
Source

pub fn with_atlas(atlas: GlyphAtlas) -> Self

As TextEngine::new, with a cache of a chosen size.

Source

pub fn add_font(&mut self, source: Box<dyn GlyphSource>) -> FontId

Registers a font and returns its id.

Examples found in repository?
examples/specimen.rs (line 49)
29fn main() -> std::io::Result<()> {
30    let mut args = std::env::args().skip(1);
31    let path = args.next().unwrap_or_else(|| "specimen.ppm".to_owned());
32    let font_path = args.next();
33    // A third argument replaces the sample, which is how you check whether a
34    // given tier can actually draw the script a panel has to show.
35    let sample = args.next();
36    let sample = sample.as_deref().unwrap_or(SAMPLE);
37
38    let mut engine = TextEngine::new();
39    // `mut` only when a second face can be added, which is feature-dependent.
40    #[allow(unused_mut)]
41    let mut faces = vec![(TextStyle::built_in(16).font, "built-in 5x7".to_owned())];
42
43    #[cfg(feature = "truetype")]
44    if let Some(font_path) = &font_path {
45        let data = std::fs::read(font_path)?;
46        match denise_text::TrueTypeSource::from_bytes(font_path, &data) {
47            Ok(source) => {
48                let name = source.name().to_owned();
49                let id = engine.add_font(Box::new(source));
50                faces.push((id, name));
51            }
52            Err(error) => eprintln!("could not parse {font_path}: {error}"),
53        }
54    }
55    #[cfg(feature = "shaping")]
56    if let Some(font_path) = &font_path {
57        let data = std::fs::read(font_path)?;
58        match denise_text::ShapedSource::from_fonts("shaped", [data]) {
59            Ok(source) => {
60                let id = engine.add_font(Box::new(source));
61                faces.push((id, "shaped (cosmic-text)".to_owned()));
62            }
63            Err(error) => eprintln!("could not build a shaper: {error}"),
64        }
65    }
66    #[cfg(not(any(feature = "truetype", feature = "shaping")))]
67    if font_path.is_some() {
68        eprintln!("built without --features truetype; ignoring the font path");
69    }
70
71    let theme: Theme = theme::DARK;
72    let mut pixels = vec![0u32; (SIZE.width * SIZE.height) as usize];
73    {
74        let mut frame = Frame::new(
75            &mut pixels,
76            SIZE,
77            SIZE.width,
78            PixelFormat::Xrgb8888,
79            BufferAge::Undefined,
80        )
81        .expect("frame");
82        let mut raster = Canvas::new(&mut frame);
83        let mut canvas = raster.pen();
84        canvas.clear(theme.color(Role::Base100));
85
86        let mut y = 16;
87        for (font, name) in &faces {
88            let heading = TextStyle {
89                font: *font,
90                size_px: 16,
91            };
92            engine.draw(
93                &mut canvas,
94                heading,
95                Point::new(16, y),
96                name,
97                theme.color(Role::Accent),
98            );
99            y += engine.line_height(heading) + 6;
100
101            for size in SIZES {
102                let style = TextStyle {
103                    font: *font,
104                    size_px: size,
105                };
106                let snapped = engine.snap_size(style);
107                let label = format!("{size}px");
108                engine.draw(
109                    &mut canvas,
110                    TextStyle::built_in(8),
111                    Point::new(16, y + 4),
112                    &label,
113                    theme.color(Role::Base300),
114                );
115                engine.draw(
116                    &mut canvas,
117                    style,
118                    Point::new(64, y),
119                    sample,
120                    theme.color(Role::BaseContent),
121                );
122                if snapped != size {
123                    engine.draw(
124                        &mut canvas,
125                        TextStyle::built_in(8),
126                        Point::new(16, y + 14),
127                        &format!("→{snapped}"),
128                        theme.color(Role::Warning),
129                    );
130                }
131                y += engine.line_height(style).max(12) + 4;
132            }
133
134            // A pangram at a readable size, to show spacing rather than shapes.
135            let body = TextStyle {
136                font: *font,
137                size_px: 16,
138            };
139            let width = engine.measure_line(body, PANGRAM);
140            engine.draw(
141                &mut canvas,
142                body,
143                Point::new(16, y),
144                PANGRAM,
145                theme.color(Role::BaseContent),
146            );
147            y += engine.line_height(body) + 16;
148            eprintln!("{name}: pangram is {width} px wide at 16 px");
149        }
150
151        // A frame around the last line, to show that measurement and ink agree.
152        let stats = engine.stats();
153        eprintln!(
154            "{} glyphs cached, {} hits, {} misses, {} resets",
155            engine.atlas().len(),
156            stats.hits,
157            stats.misses,
158            stats.resets
159        );
160        canvas.stroke_rect(
161            Rect::new(8, 8, SIZE.width as i32 - 16, y.min(SIZE.height as i32) - 8),
162            1,
163            theme.color(Role::Base300),
164        );
165    }
166
167    let mut out = std::io::BufWriter::new(std::fs::File::create(&path)?);
168    write!(out, "P6\n{} {}\n255\n", SIZE.width, SIZE.height)?;
169    for word in &pixels {
170        out.write_all(&[(word >> 16) as u8, (word >> 8) as u8, *word as u8])?;
171    }
172    out.flush()?;
173    eprintln!("wrote {path}");
174    Ok(())
175}
Source

pub fn set_default_font(&mut self, font: FontId)

Draws every style that names no font in this face.

Without this, FontId(0) is the built-in 5x7 bitmap and there is no way to ask for anything else: every widget in this workspace carries TextStyle::built_in or TextStyle::default, both of which name FontId::DEFAULT, so registering a face with add_font alone registers something nothing refers to. That was #130.

One indirection, resolved when a style becomes glyphs — so no widget, no TextStyle and no form file changes, and an application that wants a real face says two lines instead of threading a style through everything it builds.

An id that was never registered is ignored, because the alternative is a panel that draws nothing.

let mut engine = TextEngine::new();
// Nothing registered but the built-in, so the default is the built-in.
assert_eq!(engine.default_font(), FontId::DEFAULT);

// An id nobody registered changes nothing.
engine.set_default_font(FontId(9));
assert_eq!(engine.default_font(), FontId::DEFAULT);
Source

pub const fn default_font(&self) -> FontId

Which face FontId::DEFAULT currently stands for.

Source

pub fn font_count(&self) -> usize

Number of registered fonts.

Source

pub fn font_name(&self, font: FontId) -> Option<&str>

Name of a registered font.

Source

pub fn font_contains(&self, font: FontId, ch: char) -> bool

Whether a registered font has a glyph of its own for ch.

The question an application asks before choosing what to draw: a keyboard that would like on its Backspace key, a status line that would like °. Drawing a character the font lacks is not an error — it comes out as the missing-character box — so this is what turns a silent row of tofu into a legible fallback the author picked.

false for an unregistered id, and for a font that can only render ch through shaping — see GlyphSource::glyph_id.

Source

pub const fn atlas(&self) -> &GlyphAtlas

The glyph cache.

Examples found in repository?
examples/specimen.rs (line 155)
29fn main() -> std::io::Result<()> {
30    let mut args = std::env::args().skip(1);
31    let path = args.next().unwrap_or_else(|| "specimen.ppm".to_owned());
32    let font_path = args.next();
33    // A third argument replaces the sample, which is how you check whether a
34    // given tier can actually draw the script a panel has to show.
35    let sample = args.next();
36    let sample = sample.as_deref().unwrap_or(SAMPLE);
37
38    let mut engine = TextEngine::new();
39    // `mut` only when a second face can be added, which is feature-dependent.
40    #[allow(unused_mut)]
41    let mut faces = vec![(TextStyle::built_in(16).font, "built-in 5x7".to_owned())];
42
43    #[cfg(feature = "truetype")]
44    if let Some(font_path) = &font_path {
45        let data = std::fs::read(font_path)?;
46        match denise_text::TrueTypeSource::from_bytes(font_path, &data) {
47            Ok(source) => {
48                let name = source.name().to_owned();
49                let id = engine.add_font(Box::new(source));
50                faces.push((id, name));
51            }
52            Err(error) => eprintln!("could not parse {font_path}: {error}"),
53        }
54    }
55    #[cfg(feature = "shaping")]
56    if let Some(font_path) = &font_path {
57        let data = std::fs::read(font_path)?;
58        match denise_text::ShapedSource::from_fonts("shaped", [data]) {
59            Ok(source) => {
60                let id = engine.add_font(Box::new(source));
61                faces.push((id, "shaped (cosmic-text)".to_owned()));
62            }
63            Err(error) => eprintln!("could not build a shaper: {error}"),
64        }
65    }
66    #[cfg(not(any(feature = "truetype", feature = "shaping")))]
67    if font_path.is_some() {
68        eprintln!("built without --features truetype; ignoring the font path");
69    }
70
71    let theme: Theme = theme::DARK;
72    let mut pixels = vec![0u32; (SIZE.width * SIZE.height) as usize];
73    {
74        let mut frame = Frame::new(
75            &mut pixels,
76            SIZE,
77            SIZE.width,
78            PixelFormat::Xrgb8888,
79            BufferAge::Undefined,
80        )
81        .expect("frame");
82        let mut raster = Canvas::new(&mut frame);
83        let mut canvas = raster.pen();
84        canvas.clear(theme.color(Role::Base100));
85
86        let mut y = 16;
87        for (font, name) in &faces {
88            let heading = TextStyle {
89                font: *font,
90                size_px: 16,
91            };
92            engine.draw(
93                &mut canvas,
94                heading,
95                Point::new(16, y),
96                name,
97                theme.color(Role::Accent),
98            );
99            y += engine.line_height(heading) + 6;
100
101            for size in SIZES {
102                let style = TextStyle {
103                    font: *font,
104                    size_px: size,
105                };
106                let snapped = engine.snap_size(style);
107                let label = format!("{size}px");
108                engine.draw(
109                    &mut canvas,
110                    TextStyle::built_in(8),
111                    Point::new(16, y + 4),
112                    &label,
113                    theme.color(Role::Base300),
114                );
115                engine.draw(
116                    &mut canvas,
117                    style,
118                    Point::new(64, y),
119                    sample,
120                    theme.color(Role::BaseContent),
121                );
122                if snapped != size {
123                    engine.draw(
124                        &mut canvas,
125                        TextStyle::built_in(8),
126                        Point::new(16, y + 14),
127                        &format!("→{snapped}"),
128                        theme.color(Role::Warning),
129                    );
130                }
131                y += engine.line_height(style).max(12) + 4;
132            }
133
134            // A pangram at a readable size, to show spacing rather than shapes.
135            let body = TextStyle {
136                font: *font,
137                size_px: 16,
138            };
139            let width = engine.measure_line(body, PANGRAM);
140            engine.draw(
141                &mut canvas,
142                body,
143                Point::new(16, y),
144                PANGRAM,
145                theme.color(Role::BaseContent),
146            );
147            y += engine.line_height(body) + 16;
148            eprintln!("{name}: pangram is {width} px wide at 16 px");
149        }
150
151        // A frame around the last line, to show that measurement and ink agree.
152        let stats = engine.stats();
153        eprintln!(
154            "{} glyphs cached, {} hits, {} misses, {} resets",
155            engine.atlas().len(),
156            stats.hits,
157            stats.misses,
158            stats.resets
159        );
160        canvas.stroke_rect(
161            Rect::new(8, 8, SIZE.width as i32 - 16, y.min(SIZE.height as i32) - 8),
162            1,
163            theme.color(Role::Base300),
164        );
165    }
166
167    let mut out = std::io::BufWriter::new(std::fs::File::create(&path)?);
168    write!(out, "P6\n{} {}\n255\n", SIZE.width, SIZE.height)?;
169    for word in &pixels {
170        out.write_all(&[(word >> 16) as u8, (word >> 8) as u8, *word as u8])?;
171    }
172    out.flush()?;
173    eprintln!("wrote {path}");
174    Ok(())
175}
Source

pub const fn stats(&self) -> AtlasStats

Cache statistics.

Examples found in repository?
examples/specimen.rs (line 152)
29fn main() -> std::io::Result<()> {
30    let mut args = std::env::args().skip(1);
31    let path = args.next().unwrap_or_else(|| "specimen.ppm".to_owned());
32    let font_path = args.next();
33    // A third argument replaces the sample, which is how you check whether a
34    // given tier can actually draw the script a panel has to show.
35    let sample = args.next();
36    let sample = sample.as_deref().unwrap_or(SAMPLE);
37
38    let mut engine = TextEngine::new();
39    // `mut` only when a second face can be added, which is feature-dependent.
40    #[allow(unused_mut)]
41    let mut faces = vec![(TextStyle::built_in(16).font, "built-in 5x7".to_owned())];
42
43    #[cfg(feature = "truetype")]
44    if let Some(font_path) = &font_path {
45        let data = std::fs::read(font_path)?;
46        match denise_text::TrueTypeSource::from_bytes(font_path, &data) {
47            Ok(source) => {
48                let name = source.name().to_owned();
49                let id = engine.add_font(Box::new(source));
50                faces.push((id, name));
51            }
52            Err(error) => eprintln!("could not parse {font_path}: {error}"),
53        }
54    }
55    #[cfg(feature = "shaping")]
56    if let Some(font_path) = &font_path {
57        let data = std::fs::read(font_path)?;
58        match denise_text::ShapedSource::from_fonts("shaped", [data]) {
59            Ok(source) => {
60                let id = engine.add_font(Box::new(source));
61                faces.push((id, "shaped (cosmic-text)".to_owned()));
62            }
63            Err(error) => eprintln!("could not build a shaper: {error}"),
64        }
65    }
66    #[cfg(not(any(feature = "truetype", feature = "shaping")))]
67    if font_path.is_some() {
68        eprintln!("built without --features truetype; ignoring the font path");
69    }
70
71    let theme: Theme = theme::DARK;
72    let mut pixels = vec![0u32; (SIZE.width * SIZE.height) as usize];
73    {
74        let mut frame = Frame::new(
75            &mut pixels,
76            SIZE,
77            SIZE.width,
78            PixelFormat::Xrgb8888,
79            BufferAge::Undefined,
80        )
81        .expect("frame");
82        let mut raster = Canvas::new(&mut frame);
83        let mut canvas = raster.pen();
84        canvas.clear(theme.color(Role::Base100));
85
86        let mut y = 16;
87        for (font, name) in &faces {
88            let heading = TextStyle {
89                font: *font,
90                size_px: 16,
91            };
92            engine.draw(
93                &mut canvas,
94                heading,
95                Point::new(16, y),
96                name,
97                theme.color(Role::Accent),
98            );
99            y += engine.line_height(heading) + 6;
100
101            for size in SIZES {
102                let style = TextStyle {
103                    font: *font,
104                    size_px: size,
105                };
106                let snapped = engine.snap_size(style);
107                let label = format!("{size}px");
108                engine.draw(
109                    &mut canvas,
110                    TextStyle::built_in(8),
111                    Point::new(16, y + 4),
112                    &label,
113                    theme.color(Role::Base300),
114                );
115                engine.draw(
116                    &mut canvas,
117                    style,
118                    Point::new(64, y),
119                    sample,
120                    theme.color(Role::BaseContent),
121                );
122                if snapped != size {
123                    engine.draw(
124                        &mut canvas,
125                        TextStyle::built_in(8),
126                        Point::new(16, y + 14),
127                        &format!("→{snapped}"),
128                        theme.color(Role::Warning),
129                    );
130                }
131                y += engine.line_height(style).max(12) + 4;
132            }
133
134            // A pangram at a readable size, to show spacing rather than shapes.
135            let body = TextStyle {
136                font: *font,
137                size_px: 16,
138            };
139            let width = engine.measure_line(body, PANGRAM);
140            engine.draw(
141                &mut canvas,
142                body,
143                Point::new(16, y),
144                PANGRAM,
145                theme.color(Role::BaseContent),
146            );
147            y += engine.line_height(body) + 16;
148            eprintln!("{name}: pangram is {width} px wide at 16 px");
149        }
150
151        // A frame around the last line, to show that measurement and ink agree.
152        let stats = engine.stats();
153        eprintln!(
154            "{} glyphs cached, {} hits, {} misses, {} resets",
155            engine.atlas().len(),
156            stats.hits,
157            stats.misses,
158            stats.resets
159        );
160        canvas.stroke_rect(
161            Rect::new(8, 8, SIZE.width as i32 - 16, y.min(SIZE.height as i32) - 8),
162            1,
163            theme.color(Role::Base300),
164        );
165    }
166
167    let mut out = std::io::BufWriter::new(std::fs::File::create(&path)?);
168    write!(out, "P6\n{} {}\n255\n", SIZE.width, SIZE.height)?;
169    for word in &pixels {
170        out.write_all(&[(word >> 16) as u8, (word >> 8) as u8, *word as u8])?;
171    }
172    out.flush()?;
173    eprintln!("wrote {path}");
174    Ok(())
175}
Source

pub fn clear_cache(&mut self)

Empties the glyph cache. Needed after nothing; useful in benches.

Source

pub fn metrics(&self, style: TextStyle) -> FontMetrics

Vertical metrics for a style.

Source

pub fn snap_size(&self, style: TextStyle) -> u16

The size this style will actually be drawn at.

Examples found in repository?
examples/specimen.rs (line 106)
29fn main() -> std::io::Result<()> {
30    let mut args = std::env::args().skip(1);
31    let path = args.next().unwrap_or_else(|| "specimen.ppm".to_owned());
32    let font_path = args.next();
33    // A third argument replaces the sample, which is how you check whether a
34    // given tier can actually draw the script a panel has to show.
35    let sample = args.next();
36    let sample = sample.as_deref().unwrap_or(SAMPLE);
37
38    let mut engine = TextEngine::new();
39    // `mut` only when a second face can be added, which is feature-dependent.
40    #[allow(unused_mut)]
41    let mut faces = vec![(TextStyle::built_in(16).font, "built-in 5x7".to_owned())];
42
43    #[cfg(feature = "truetype")]
44    if let Some(font_path) = &font_path {
45        let data = std::fs::read(font_path)?;
46        match denise_text::TrueTypeSource::from_bytes(font_path, &data) {
47            Ok(source) => {
48                let name = source.name().to_owned();
49                let id = engine.add_font(Box::new(source));
50                faces.push((id, name));
51            }
52            Err(error) => eprintln!("could not parse {font_path}: {error}"),
53        }
54    }
55    #[cfg(feature = "shaping")]
56    if let Some(font_path) = &font_path {
57        let data = std::fs::read(font_path)?;
58        match denise_text::ShapedSource::from_fonts("shaped", [data]) {
59            Ok(source) => {
60                let id = engine.add_font(Box::new(source));
61                faces.push((id, "shaped (cosmic-text)".to_owned()));
62            }
63            Err(error) => eprintln!("could not build a shaper: {error}"),
64        }
65    }
66    #[cfg(not(any(feature = "truetype", feature = "shaping")))]
67    if font_path.is_some() {
68        eprintln!("built without --features truetype; ignoring the font path");
69    }
70
71    let theme: Theme = theme::DARK;
72    let mut pixels = vec![0u32; (SIZE.width * SIZE.height) as usize];
73    {
74        let mut frame = Frame::new(
75            &mut pixels,
76            SIZE,
77            SIZE.width,
78            PixelFormat::Xrgb8888,
79            BufferAge::Undefined,
80        )
81        .expect("frame");
82        let mut raster = Canvas::new(&mut frame);
83        let mut canvas = raster.pen();
84        canvas.clear(theme.color(Role::Base100));
85
86        let mut y = 16;
87        for (font, name) in &faces {
88            let heading = TextStyle {
89                font: *font,
90                size_px: 16,
91            };
92            engine.draw(
93                &mut canvas,
94                heading,
95                Point::new(16, y),
96                name,
97                theme.color(Role::Accent),
98            );
99            y += engine.line_height(heading) + 6;
100
101            for size in SIZES {
102                let style = TextStyle {
103                    font: *font,
104                    size_px: size,
105                };
106                let snapped = engine.snap_size(style);
107                let label = format!("{size}px");
108                engine.draw(
109                    &mut canvas,
110                    TextStyle::built_in(8),
111                    Point::new(16, y + 4),
112                    &label,
113                    theme.color(Role::Base300),
114                );
115                engine.draw(
116                    &mut canvas,
117                    style,
118                    Point::new(64, y),
119                    sample,
120                    theme.color(Role::BaseContent),
121                );
122                if snapped != size {
123                    engine.draw(
124                        &mut canvas,
125                        TextStyle::built_in(8),
126                        Point::new(16, y + 14),
127                        &format!("→{snapped}"),
128                        theme.color(Role::Warning),
129                    );
130                }
131                y += engine.line_height(style).max(12) + 4;
132            }
133
134            // A pangram at a readable size, to show spacing rather than shapes.
135            let body = TextStyle {
136                font: *font,
137                size_px: 16,
138            };
139            let width = engine.measure_line(body, PANGRAM);
140            engine.draw(
141                &mut canvas,
142                body,
143                Point::new(16, y),
144                PANGRAM,
145                theme.color(Role::BaseContent),
146            );
147            y += engine.line_height(body) + 16;
148            eprintln!("{name}: pangram is {width} px wide at 16 px");
149        }
150
151        // A frame around the last line, to show that measurement and ink agree.
152        let stats = engine.stats();
153        eprintln!(
154            "{} glyphs cached, {} hits, {} misses, {} resets",
155            engine.atlas().len(),
156            stats.hits,
157            stats.misses,
158            stats.resets
159        );
160        canvas.stroke_rect(
161            Rect::new(8, 8, SIZE.width as i32 - 16, y.min(SIZE.height as i32) - 8),
162            1,
163            theme.color(Role::Base300),
164        );
165    }
166
167    let mut out = std::io::BufWriter::new(std::fs::File::create(&path)?);
168    write!(out, "P6\n{} {}\n255\n", SIZE.width, SIZE.height)?;
169    for word in &pixels {
170        out.write_all(&[(word >> 16) as u8, (word >> 8) as u8, *word as u8])?;
171    }
172    out.flush()?;
173    eprintln!("wrote {path}");
174    Ok(())
175}
Source

pub fn line_height(&self, style: TextStyle) -> i32

Baseline-to-baseline distance for a style.

Examples found in repository?
examples/specimen.rs (line 99)
29fn main() -> std::io::Result<()> {
30    let mut args = std::env::args().skip(1);
31    let path = args.next().unwrap_or_else(|| "specimen.ppm".to_owned());
32    let font_path = args.next();
33    // A third argument replaces the sample, which is how you check whether a
34    // given tier can actually draw the script a panel has to show.
35    let sample = args.next();
36    let sample = sample.as_deref().unwrap_or(SAMPLE);
37
38    let mut engine = TextEngine::new();
39    // `mut` only when a second face can be added, which is feature-dependent.
40    #[allow(unused_mut)]
41    let mut faces = vec![(TextStyle::built_in(16).font, "built-in 5x7".to_owned())];
42
43    #[cfg(feature = "truetype")]
44    if let Some(font_path) = &font_path {
45        let data = std::fs::read(font_path)?;
46        match denise_text::TrueTypeSource::from_bytes(font_path, &data) {
47            Ok(source) => {
48                let name = source.name().to_owned();
49                let id = engine.add_font(Box::new(source));
50                faces.push((id, name));
51            }
52            Err(error) => eprintln!("could not parse {font_path}: {error}"),
53        }
54    }
55    #[cfg(feature = "shaping")]
56    if let Some(font_path) = &font_path {
57        let data = std::fs::read(font_path)?;
58        match denise_text::ShapedSource::from_fonts("shaped", [data]) {
59            Ok(source) => {
60                let id = engine.add_font(Box::new(source));
61                faces.push((id, "shaped (cosmic-text)".to_owned()));
62            }
63            Err(error) => eprintln!("could not build a shaper: {error}"),
64        }
65    }
66    #[cfg(not(any(feature = "truetype", feature = "shaping")))]
67    if font_path.is_some() {
68        eprintln!("built without --features truetype; ignoring the font path");
69    }
70
71    let theme: Theme = theme::DARK;
72    let mut pixels = vec![0u32; (SIZE.width * SIZE.height) as usize];
73    {
74        let mut frame = Frame::new(
75            &mut pixels,
76            SIZE,
77            SIZE.width,
78            PixelFormat::Xrgb8888,
79            BufferAge::Undefined,
80        )
81        .expect("frame");
82        let mut raster = Canvas::new(&mut frame);
83        let mut canvas = raster.pen();
84        canvas.clear(theme.color(Role::Base100));
85
86        let mut y = 16;
87        for (font, name) in &faces {
88            let heading = TextStyle {
89                font: *font,
90                size_px: 16,
91            };
92            engine.draw(
93                &mut canvas,
94                heading,
95                Point::new(16, y),
96                name,
97                theme.color(Role::Accent),
98            );
99            y += engine.line_height(heading) + 6;
100
101            for size in SIZES {
102                let style = TextStyle {
103                    font: *font,
104                    size_px: size,
105                };
106                let snapped = engine.snap_size(style);
107                let label = format!("{size}px");
108                engine.draw(
109                    &mut canvas,
110                    TextStyle::built_in(8),
111                    Point::new(16, y + 4),
112                    &label,
113                    theme.color(Role::Base300),
114                );
115                engine.draw(
116                    &mut canvas,
117                    style,
118                    Point::new(64, y),
119                    sample,
120                    theme.color(Role::BaseContent),
121                );
122                if snapped != size {
123                    engine.draw(
124                        &mut canvas,
125                        TextStyle::built_in(8),
126                        Point::new(16, y + 14),
127                        &format!("→{snapped}"),
128                        theme.color(Role::Warning),
129                    );
130                }
131                y += engine.line_height(style).max(12) + 4;
132            }
133
134            // A pangram at a readable size, to show spacing rather than shapes.
135            let body = TextStyle {
136                font: *font,
137                size_px: 16,
138            };
139            let width = engine.measure_line(body, PANGRAM);
140            engine.draw(
141                &mut canvas,
142                body,
143                Point::new(16, y),
144                PANGRAM,
145                theme.color(Role::BaseContent),
146            );
147            y += engine.line_height(body) + 16;
148            eprintln!("{name}: pangram is {width} px wide at 16 px");
149        }
150
151        // A frame around the last line, to show that measurement and ink agree.
152        let stats = engine.stats();
153        eprintln!(
154            "{} glyphs cached, {} hits, {} misses, {} resets",
155            engine.atlas().len(),
156            stats.hits,
157            stats.misses,
158            stats.resets
159        );
160        canvas.stroke_rect(
161            Rect::new(8, 8, SIZE.width as i32 - 16, y.min(SIZE.height as i32) - 8),
162            1,
163            theme.color(Role::Base300),
164        );
165    }
166
167    let mut out = std::io::BufWriter::new(std::fs::File::create(&path)?);
168    write!(out, "P6\n{} {}\n255\n", SIZE.width, SIZE.height)?;
169    for word in &pixels {
170        out.write_all(&[(word >> 16) as u8, (word >> 8) as u8, *word as u8])?;
171    }
172    out.flush()?;
173    eprintln!("wrote {path}");
174    Ok(())
175}
Source

pub fn layout_line( &mut self, style: TextStyle, text: &str, f: impl FnMut(PositionedGlyph), ) -> i32

Lays out one line, calling f for each glyph that has ink.

Returns the total advance. Positions are relative to the line’s start, with bounds.y measured from the baseline — so a caller places the line by translating, and never has to know how the font was measured.

Source

pub fn measure_line(&mut self, style: TextStyle, text: &str) -> i32

Width of one line, ignoring \n.

Examples found in repository?
examples/specimen.rs (line 139)
29fn main() -> std::io::Result<()> {
30    let mut args = std::env::args().skip(1);
31    let path = args.next().unwrap_or_else(|| "specimen.ppm".to_owned());
32    let font_path = args.next();
33    // A third argument replaces the sample, which is how you check whether a
34    // given tier can actually draw the script a panel has to show.
35    let sample = args.next();
36    let sample = sample.as_deref().unwrap_or(SAMPLE);
37
38    let mut engine = TextEngine::new();
39    // `mut` only when a second face can be added, which is feature-dependent.
40    #[allow(unused_mut)]
41    let mut faces = vec![(TextStyle::built_in(16).font, "built-in 5x7".to_owned())];
42
43    #[cfg(feature = "truetype")]
44    if let Some(font_path) = &font_path {
45        let data = std::fs::read(font_path)?;
46        match denise_text::TrueTypeSource::from_bytes(font_path, &data) {
47            Ok(source) => {
48                let name = source.name().to_owned();
49                let id = engine.add_font(Box::new(source));
50                faces.push((id, name));
51            }
52            Err(error) => eprintln!("could not parse {font_path}: {error}"),
53        }
54    }
55    #[cfg(feature = "shaping")]
56    if let Some(font_path) = &font_path {
57        let data = std::fs::read(font_path)?;
58        match denise_text::ShapedSource::from_fonts("shaped", [data]) {
59            Ok(source) => {
60                let id = engine.add_font(Box::new(source));
61                faces.push((id, "shaped (cosmic-text)".to_owned()));
62            }
63            Err(error) => eprintln!("could not build a shaper: {error}"),
64        }
65    }
66    #[cfg(not(any(feature = "truetype", feature = "shaping")))]
67    if font_path.is_some() {
68        eprintln!("built without --features truetype; ignoring the font path");
69    }
70
71    let theme: Theme = theme::DARK;
72    let mut pixels = vec![0u32; (SIZE.width * SIZE.height) as usize];
73    {
74        let mut frame = Frame::new(
75            &mut pixels,
76            SIZE,
77            SIZE.width,
78            PixelFormat::Xrgb8888,
79            BufferAge::Undefined,
80        )
81        .expect("frame");
82        let mut raster = Canvas::new(&mut frame);
83        let mut canvas = raster.pen();
84        canvas.clear(theme.color(Role::Base100));
85
86        let mut y = 16;
87        for (font, name) in &faces {
88            let heading = TextStyle {
89                font: *font,
90                size_px: 16,
91            };
92            engine.draw(
93                &mut canvas,
94                heading,
95                Point::new(16, y),
96                name,
97                theme.color(Role::Accent),
98            );
99            y += engine.line_height(heading) + 6;
100
101            for size in SIZES {
102                let style = TextStyle {
103                    font: *font,
104                    size_px: size,
105                };
106                let snapped = engine.snap_size(style);
107                let label = format!("{size}px");
108                engine.draw(
109                    &mut canvas,
110                    TextStyle::built_in(8),
111                    Point::new(16, y + 4),
112                    &label,
113                    theme.color(Role::Base300),
114                );
115                engine.draw(
116                    &mut canvas,
117                    style,
118                    Point::new(64, y),
119                    sample,
120                    theme.color(Role::BaseContent),
121                );
122                if snapped != size {
123                    engine.draw(
124                        &mut canvas,
125                        TextStyle::built_in(8),
126                        Point::new(16, y + 14),
127                        &format!("→{snapped}"),
128                        theme.color(Role::Warning),
129                    );
130                }
131                y += engine.line_height(style).max(12) + 4;
132            }
133
134            // A pangram at a readable size, to show spacing rather than shapes.
135            let body = TextStyle {
136                font: *font,
137                size_px: 16,
138            };
139            let width = engine.measure_line(body, PANGRAM);
140            engine.draw(
141                &mut canvas,
142                body,
143                Point::new(16, y),
144                PANGRAM,
145                theme.color(Role::BaseContent),
146            );
147            y += engine.line_height(body) + 16;
148            eprintln!("{name}: pangram is {width} px wide at 16 px");
149        }
150
151        // A frame around the last line, to show that measurement and ink agree.
152        let stats = engine.stats();
153        eprintln!(
154            "{} glyphs cached, {} hits, {} misses, {} resets",
155            engine.atlas().len(),
156            stats.hits,
157            stats.misses,
158            stats.resets
159        );
160        canvas.stroke_rect(
161            Rect::new(8, 8, SIZE.width as i32 - 16, y.min(SIZE.height as i32) - 8),
162            1,
163            theme.color(Role::Base300),
164        );
165    }
166
167    let mut out = std::io::BufWriter::new(std::fs::File::create(&path)?);
168    write!(out, "P6\n{} {}\n255\n", SIZE.width, SIZE.height)?;
169    for word in &pixels {
170        out.write_all(&[(word >> 16) as u8, (word >> 8) as u8, *word as u8])?;
171    }
172    out.flush()?;
173    eprintln!("wrote {path}");
174    Ok(())
175}
Source

pub fn measure(&mut self, style: TextStyle, text: &str) -> Size

Extent of text, honouring \n.

The height is lines * line_height, not the ink’s bounding box: a label that changes from Ok to Ogg must not change height, or a form would reflow every time a reading gained a descender.

Source

pub fn wrap<'a>( &mut self, style: TextStyle, text: &'a str, max_width: i32, ) -> Vec<&'a str>

The lines text becomes when broken to fit max_width.

Greedy: words are added to a line until the next one would not fit. That is what every text editor does, it is one measuring pass, and the alternative — balancing lines by minimising raggedness — is a dynamic-programming problem this toolkit has no reason to solve.

Explicit \n always breaks, so a caller who has already decided where the lines go keeps that decision.

Slices borrow from text; nothing is copied. Words are separated by ASCII spaces, which is the boundary the built-in font can render and the one the languages this toolkit ships keyboard layouts for use.

§A word wider than the line

Goes on a line of its own and overflows, rather than being broken between characters. Breaking mid-word needs to know where a grapheme ends, and getting that wrong turns æ into two bytes of nothing — so an honest overflow the caller can see beats a corruption they cannot. A max_width of zero or less disables wrapping entirely for the same reason: there is no width that any word fits in.

Source

pub fn wrapped_height( &mut self, style: TextStyle, text: &str, max_width: i32, ) -> i32

Height of text once wrapped to max_width.

Source

pub fn draw_line( &mut self, canvas: &mut Pen<'_>, style: TextStyle, origin: Point, text: &str, color: Color, ) -> i32

Draws one line with its baseline at origin.

Returns the total advance, of the whole line and not of the part drawn: a caller measuring a line to know how far it scrolls needs all of it.

Only the glyphs the canvas would keep are rasterised and handed over. The painter clips the rest away to nothing, so the pixels are the same either way, but handing them over is not free: a line a megabyte long is a million glyphs, and a painter that builds geometry per glyph turns that into hundreds of megabytes for the few hundred that are on screen — more, on some, than a GPU will take in one buffer. Laying the line out is still the whole of it, which is where width comes from; it is cheap beside rasterising, being an advance apiece from the cache.

Source

pub fn draw( &mut self, canvas: &mut Pen<'_>, style: TextStyle, origin: Point, text: &str, color: Color, ) -> Size

Draws text with the top-left corner of its first line at origin, honouring \n. Returns the extent laid out.

Top-left rather than baseline, because a widget positions text in a box and should not have to know where the baseline of a font it did not choose happens to fall.

Examples found in repository?
examples/specimen.rs (lines 92-98)
29fn main() -> std::io::Result<()> {
30    let mut args = std::env::args().skip(1);
31    let path = args.next().unwrap_or_else(|| "specimen.ppm".to_owned());
32    let font_path = args.next();
33    // A third argument replaces the sample, which is how you check whether a
34    // given tier can actually draw the script a panel has to show.
35    let sample = args.next();
36    let sample = sample.as_deref().unwrap_or(SAMPLE);
37
38    let mut engine = TextEngine::new();
39    // `mut` only when a second face can be added, which is feature-dependent.
40    #[allow(unused_mut)]
41    let mut faces = vec![(TextStyle::built_in(16).font, "built-in 5x7".to_owned())];
42
43    #[cfg(feature = "truetype")]
44    if let Some(font_path) = &font_path {
45        let data = std::fs::read(font_path)?;
46        match denise_text::TrueTypeSource::from_bytes(font_path, &data) {
47            Ok(source) => {
48                let name = source.name().to_owned();
49                let id = engine.add_font(Box::new(source));
50                faces.push((id, name));
51            }
52            Err(error) => eprintln!("could not parse {font_path}: {error}"),
53        }
54    }
55    #[cfg(feature = "shaping")]
56    if let Some(font_path) = &font_path {
57        let data = std::fs::read(font_path)?;
58        match denise_text::ShapedSource::from_fonts("shaped", [data]) {
59            Ok(source) => {
60                let id = engine.add_font(Box::new(source));
61                faces.push((id, "shaped (cosmic-text)".to_owned()));
62            }
63            Err(error) => eprintln!("could not build a shaper: {error}"),
64        }
65    }
66    #[cfg(not(any(feature = "truetype", feature = "shaping")))]
67    if font_path.is_some() {
68        eprintln!("built without --features truetype; ignoring the font path");
69    }
70
71    let theme: Theme = theme::DARK;
72    let mut pixels = vec![0u32; (SIZE.width * SIZE.height) as usize];
73    {
74        let mut frame = Frame::new(
75            &mut pixels,
76            SIZE,
77            SIZE.width,
78            PixelFormat::Xrgb8888,
79            BufferAge::Undefined,
80        )
81        .expect("frame");
82        let mut raster = Canvas::new(&mut frame);
83        let mut canvas = raster.pen();
84        canvas.clear(theme.color(Role::Base100));
85
86        let mut y = 16;
87        for (font, name) in &faces {
88            let heading = TextStyle {
89                font: *font,
90                size_px: 16,
91            };
92            engine.draw(
93                &mut canvas,
94                heading,
95                Point::new(16, y),
96                name,
97                theme.color(Role::Accent),
98            );
99            y += engine.line_height(heading) + 6;
100
101            for size in SIZES {
102                let style = TextStyle {
103                    font: *font,
104                    size_px: size,
105                };
106                let snapped = engine.snap_size(style);
107                let label = format!("{size}px");
108                engine.draw(
109                    &mut canvas,
110                    TextStyle::built_in(8),
111                    Point::new(16, y + 4),
112                    &label,
113                    theme.color(Role::Base300),
114                );
115                engine.draw(
116                    &mut canvas,
117                    style,
118                    Point::new(64, y),
119                    sample,
120                    theme.color(Role::BaseContent),
121                );
122                if snapped != size {
123                    engine.draw(
124                        &mut canvas,
125                        TextStyle::built_in(8),
126                        Point::new(16, y + 14),
127                        &format!("→{snapped}"),
128                        theme.color(Role::Warning),
129                    );
130                }
131                y += engine.line_height(style).max(12) + 4;
132            }
133
134            // A pangram at a readable size, to show spacing rather than shapes.
135            let body = TextStyle {
136                font: *font,
137                size_px: 16,
138            };
139            let width = engine.measure_line(body, PANGRAM);
140            engine.draw(
141                &mut canvas,
142                body,
143                Point::new(16, y),
144                PANGRAM,
145                theme.color(Role::BaseContent),
146            );
147            y += engine.line_height(body) + 16;
148            eprintln!("{name}: pangram is {width} px wide at 16 px");
149        }
150
151        // A frame around the last line, to show that measurement and ink agree.
152        let stats = engine.stats();
153        eprintln!(
154            "{} glyphs cached, {} hits, {} misses, {} resets",
155            engine.atlas().len(),
156            stats.hits,
157            stats.misses,
158            stats.resets
159        );
160        canvas.stroke_rect(
161            Rect::new(8, 8, SIZE.width as i32 - 16, y.min(SIZE.height as i32) - 8),
162            1,
163            theme.color(Role::Base300),
164        );
165    }
166
167    let mut out = std::io::BufWriter::new(std::fs::File::create(&path)?);
168    write!(out, "P6\n{} {}\n255\n", SIZE.width, SIZE.height)?;
169    for word in &pixels {
170        out.write_all(&[(word >> 16) as u8, (word >> 8) as u8, *word as u8])?;
171    }
172    out.flush()?;
173    eprintln!("wrote {path}");
174    Ok(())
175}

Trait Implementations§

Source§

impl Debug for TextEngine

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for TextEngine

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.