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
impl TextEngine
Sourcepub fn new() -> Self
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?
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}Sourcepub fn with_atlas(atlas: GlyphAtlas) -> Self
pub fn with_atlas(atlas: GlyphAtlas) -> Self
As TextEngine::new, with a cache of a chosen size.
Sourcepub fn add_font(&mut self, source: Box<dyn GlyphSource>) -> FontId
pub fn add_font(&mut self, source: Box<dyn GlyphSource>) -> FontId
Registers a font and returns its id.
Examples found in repository?
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}Sourcepub fn set_default_font(&mut self, font: FontId)
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);Sourcepub const fn default_font(&self) -> FontId
pub const fn default_font(&self) -> FontId
Which face FontId::DEFAULT currently stands for.
Sourcepub fn font_count(&self) -> usize
pub fn font_count(&self) -> usize
Number of registered fonts.
Sourcepub fn font_contains(&self, font: FontId, ch: char) -> bool
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.
Sourcepub const fn atlas(&self) -> &GlyphAtlas
pub const fn atlas(&self) -> &GlyphAtlas
The glyph cache.
Examples found in repository?
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}Sourcepub const fn stats(&self) -> AtlasStats
pub const fn stats(&self) -> AtlasStats
Cache statistics.
Examples found in repository?
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}Sourcepub fn clear_cache(&mut self)
pub fn clear_cache(&mut self)
Empties the glyph cache. Needed after nothing; useful in benches.
Sourcepub fn metrics(&self, style: TextStyle) -> FontMetrics
pub fn metrics(&self, style: TextStyle) -> FontMetrics
Vertical metrics for a style.
Sourcepub fn snap_size(&self, style: TextStyle) -> u16
pub fn snap_size(&self, style: TextStyle) -> u16
The size this style will actually be drawn at.
Examples found in repository?
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}Sourcepub fn line_height(&self, style: TextStyle) -> i32
pub fn line_height(&self, style: TextStyle) -> i32
Baseline-to-baseline distance for a style.
Examples found in repository?
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}Sourcepub fn layout_line(
&mut self,
style: TextStyle,
text: &str,
f: impl FnMut(PositionedGlyph),
) -> i32
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.
Sourcepub fn measure_line(&mut self, style: TextStyle, text: &str) -> i32
pub fn measure_line(&mut self, style: TextStyle, text: &str) -> i32
Width of one line, ignoring \n.
Examples found in repository?
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}Sourcepub fn measure(&mut self, style: TextStyle, text: &str) -> Size
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.
Sourcepub fn wrap<'a>(
&mut self,
style: TextStyle,
text: &'a str,
max_width: i32,
) -> Vec<&'a str>
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.
Sourcepub fn wrapped_height(
&mut self,
style: TextStyle,
text: &str,
max_width: i32,
) -> i32
pub fn wrapped_height( &mut self, style: TextStyle, text: &str, max_width: i32, ) -> i32
Height of text once wrapped to max_width.
Sourcepub fn draw_line(
&mut self,
canvas: &mut Pen<'_>,
style: TextStyle,
origin: Point,
text: &str,
color: Color,
) -> i32
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.
Sourcepub fn draw(
&mut self,
canvas: &mut Pen<'_>,
style: TextStyle,
origin: Point,
text: &str,
color: Color,
) -> Size
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?
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}