1use crate::{Anchor, AtlasPage, Error, MipLevel, Representation, Span, TextEngine, caret, marks};
2
3const FULL_CLIP: [f32; 4] = [-f32::MAX, -f32::MAX, f32::MAX, f32::MAX];
4const FILL_GRID: u32 = 8;
5const ICON_GRID: u32 = 8;
6const MAX_TEXELS: u64 = 1 << 28;
7
8#[derive(Clone, Debug, PartialEq)]
9pub struct PrintImage {
10 pub width: u32,
11 pub height: u32,
12 pub density: f32,
13 pub pixels: Vec<u8>,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct Selection {
18 pub from: usize,
19 pub to: usize,
20 pub color: [f32; 4],
21}
22
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct Caret {
25 pub offset: usize,
26 pub color: [f32; 4],
27 pub width: f32,
28 pub height: f32,
29 pub shift: f32,
30}
31
32#[derive(Clone, Debug)]
33pub struct PrintBlock<'a> {
34 pub spans: &'a [Span],
35 pub wrap: Option<f32>,
36 pub anchor: Anchor,
37 pub at: [f32; 2],
38 pub alpha: f32,
39 pub clip: [f32; 4],
40 pub turn: [f32; 3],
41 pub selection: Option<Selection>,
42 pub caret: Option<Caret>,
43}
44
45impl<'a> PrintBlock<'a> {
46 pub fn new(spans: &'a [Span], at: [f32; 2]) -> Self {
47 Self {
48 spans,
49 wrap: None,
50 anchor: Anchor::Start,
51 at,
52 alpha: 1.0,
53 clip: FULL_CLIP,
54 turn: [0.0; 3],
55 selection: None,
56 caret: None,
57 }
58 }
59}
60
61#[derive(Clone, Copy, Debug)]
62pub struct Icon<'a> {
63 pub rect: [f32; 4],
64 pub width: u32,
65 pub height: u32,
66 pub rgba: &'a [u8],
67 pub premultiplied: bool,
68 pub tint: [f32; 4],
69 pub clip: [f32; 4],
70 pub turn: [f32; 3],
71}
72
73impl<'a> Icon<'a> {
74 pub fn new(rect: [f32; 4], width: u32, height: u32, rgba: &'a [u8]) -> Self {
75 Self {
76 rect,
77 width,
78 height,
79 rgba,
80 premultiplied: false,
81 tint: [1.0; 4],
82 clip: FULL_CLIP,
83 turn: [0.0; 3],
84 }
85 }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq)]
89pub struct Printed {
90 pub lines: usize,
91 pub width: f32,
92 pub height: f32,
93 pub baseline: f32,
94 pub origin: [f32; 2],
95 pub caret: Option<[f32; 4]>,
96}
97
98#[derive(Clone, Copy)]
99struct Frame {
100 clip: [f32; 4],
101 pivot: [f32; 2],
102 cos: f32,
103 sin: f32,
104 turned: bool,
105}
106
107impl Frame {
108 fn new(density: f32, clip: [f32; 4], turn: [f32; 3]) -> Self {
109 Self {
110 clip: clip.map(|value| value * density),
111 pivot: [turn[0] * density, turn[1] * density],
112 cos: turn[2].cos(),
113 sin: turn[2].sin(),
114 turned: turn[2] != 0.0,
115 }
116 }
117
118 fn bounds(&self, rect: [f32; 4]) -> [i32; 4] {
119 let corners = [
120 [rect[0], rect[1]],
121 [rect[0] + rect[2], rect[1]],
122 [rect[0], rect[1] + rect[3]],
123 [rect[0] + rect[2], rect[1] + rect[3]],
124 ];
125 let mut low = [f32::MAX; 2];
126 let mut high = [f32::MIN; 2];
127 for corner in corners {
128 let point = if self.turned {
129 let dx = corner[0] - self.pivot[0];
130 let dy = corner[1] - self.pivot[1];
131 [
132 self.pivot[0] + self.cos * dx - self.sin * dy,
133 self.pivot[1] + self.sin * dx + self.cos * dy,
134 ]
135 } else {
136 corner
137 };
138 for axis in 0..2 {
139 low[axis] = low[axis].min(point[axis]);
140 high[axis] = high[axis].max(point[axis]);
141 }
142 }
143 [
144 low[0].floor() as i32,
145 low[1].floor() as i32,
146 high[0].ceil() as i32,
147 high[1].ceil() as i32,
148 ]
149 }
150
151 fn source(&self, x: f32, y: f32) -> Option<[f32; 2]> {
152 let point = if self.turned {
153 let dx = x - self.pivot[0];
154 let dy = y - self.pivot[1];
155 [
156 self.pivot[0] + self.cos * dx + self.sin * dy,
157 self.pivot[1] - self.sin * dx + self.cos * dy,
158 ]
159 } else {
160 [x, y]
161 };
162 (point[0] >= self.clip[0]
163 && point[0] <= self.clip[2]
164 && point[1] >= self.clip[1]
165 && point[1] <= self.clip[3])
166 .then_some(point)
167 }
168}
169
170fn valid(density: f32, values: &[f32]) -> Result<(), Error> {
171 if !density.is_finite() || density <= 0.0 || values.iter().any(|value| !value.is_finite()) {
172 return Err(Error::InvalidStyle);
173 }
174 Ok(())
175}
176
177fn finite_clip(clip: [f32; 4]) -> [f32; 4] {
178 clip.map(|value| value.clamp(-f32::MAX, f32::MAX))
179}
180
181impl PrintImage {
182 pub fn new(width: f32, height: f32, density: f32) -> Result<Self, Error> {
183 valid(density, &[width, height])?;
184 if width <= 0.0 || height <= 0.0 {
185 return Err(Error::InvalidStyle);
186 }
187 let columns = (width * density).ceil();
188 let rows = (height * density).ceil();
189 if columns > u32::MAX as f32 || rows > u32::MAX as f32 {
190 return Err(Error::AtlasTooLarge);
191 }
192 Self::from_texels(columns as u32, rows as u32, density)
193 }
194
195 pub fn from_texels(width: u32, height: u32, density: f32) -> Result<Self, Error> {
196 valid(density, &[])?;
197 if width == 0 || height == 0 {
198 return Err(Error::InvalidStyle);
199 }
200 if u64::from(width) * u64::from(height) > MAX_TEXELS {
201 return Err(Error::AtlasTooLarge);
202 }
203 Ok(Self {
204 width,
205 height,
206 density,
207 pixels: vec![0; width as usize * height as usize * 4],
208 })
209 }
210
211 pub fn pixel(&self, x: u32, y: u32) -> [u8; 4] {
212 let at = ((y * self.width + x) * 4) as usize;
213 [
214 self.pixels[at],
215 self.pixels[at + 1],
216 self.pixels[at + 2],
217 self.pixels[at + 3],
218 ]
219 }
220
221 fn blend(&mut self, x: i32, y: i32, source: [f32; 4]) {
222 if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 || source[3] <= 0.0 {
223 return;
224 }
225 let at = ((y as u32 * self.width + x as u32) * 4) as usize;
226 let keep = 1.0 - source[3].min(1.0);
227 for (byte, value) in self.pixels[at..at + 4].iter_mut().zip(source) {
228 let below = f32::from(*byte) / 255.0;
229 let value = value.clamp(0.0, 1.0) + below * keep;
230 *byte = (value * 255.0).round().min(255.0) as u8;
231 }
232 }
233
234 pub fn fill(&mut self, rect: [f32; 4], color: [f32; 4], clip: [f32; 4], turn: [f32; 3]) {
235 let density = self.density;
236 if !rect.iter().chain(&color).all(|value| value.is_finite()) || color[3] <= 0.0 {
237 return;
238 }
239 let frame = Frame::new(density, finite_clip(clip), turn);
240 let rect = rect.map(|value| value * density);
241 let bounds = frame.bounds(rect);
242 let alpha = color[3].min(1.0);
243 let source = [color[0] * alpha, color[1] * alpha, color[2] * alpha, alpha];
244 for y in bounds[1]..bounds[3] {
245 for x in bounds[0]..bounds[2] {
246 let mut inside = 0u32;
247 for j in 0..FILL_GRID {
248 for i in 0..FILL_GRID {
249 let sx = x as f32 + (i as f32 + 0.5) / FILL_GRID as f32;
250 let sy = y as f32 + (j as f32 + 0.5) / FILL_GRID as f32;
251 if let Some(p) = frame.source(sx, sy)
252 && p[0] >= rect[0]
253 && p[0] < rect[0] + rect[2]
254 && p[1] >= rect[1]
255 && p[1] < rect[1] + rect[3]
256 {
257 inside += 1;
258 }
259 }
260 }
261 if inside > 0 {
262 let cover = inside as f32 / (FILL_GRID * FILL_GRID) as f32;
263 self.blend(x, y, source.map(|value| value * cover));
264 }
265 }
266 }
267 }
268
269 pub fn icon(&mut self, icon: &Icon<'_>) -> Result<(), Error> {
270 let density = self.density;
271 if icon.width == 0
272 || icon.height == 0
273 || icon.rgba.len() != icon.width as usize * icon.height as usize * 4
274 || !icon
275 .rect
276 .iter()
277 .chain(&icon.tint)
278 .all(|value| value.is_finite())
279 || icon.rect[2] <= 0.0
280 || icon.rect[3] <= 0.0
281 {
282 return Err(Error::InvalidStyle);
283 }
284 let frame = Frame::new(density, finite_clip(icon.clip), icon.turn);
285 let rect = icon.rect.map(|value| value * density);
286 let reach = (icon.width as f32 / rect[2])
287 .max(icon.height as f32 / rect[3])
288 .ceil()
289 .clamp(1.0, ICON_GRID as f32) as u32;
290 let bounds = frame.bounds(rect);
291 let texel = |x: i32, y: i32| -> [f32; 4] {
292 let at = ((y as u32 * icon.width + x as u32) * 4) as usize;
293 let mut rgba = [0.0; 4];
294 for (value, byte) in rgba.iter_mut().zip(&icon.rgba[at..at + 4]) {
295 *value = f32::from(*byte) / 255.0;
296 }
297 if !icon.premultiplied {
298 let alpha = rgba[3];
299 for value in &mut rgba[..3] {
300 *value *= alpha;
301 }
302 }
303 rgba
304 };
305 for y in bounds[1]..bounds[3] {
306 for x in bounds[0]..bounds[2] {
307 let mut sum = [0.0f32; 4];
308 for j in 0..reach {
309 for i in 0..reach {
310 let sx = x as f32 + (i as f32 + 0.5) / reach as f32;
311 let sy = y as f32 + (j as f32 + 0.5) / reach as f32;
312 let Some(p) = frame.source(sx, sy) else {
313 continue;
314 };
315 let u = (p[0] - rect[0]) / rect[2];
316 let v = (p[1] - rect[1]) / rect[3];
317 if !(0.0..1.0).contains(&u) || !(0.0..1.0).contains(&v) {
318 continue;
319 }
320 let ax = u * icon.width as f32 - 0.5;
321 let ay = v * icon.height as f32 - 0.5;
322 let x0 = ax.floor();
323 let y0 = ay.floor();
324 let (tx, ty) = (ax - x0, ay - y0);
325 let clamp_x = |value: f32| value.clamp(0.0, icon.width as f32 - 1.0) as i32;
326 let clamp_y =
327 |value: f32| value.clamp(0.0, icon.height as f32 - 1.0) as i32;
328 let (x0i, x1i) = (clamp_x(x0), clamp_x(x0 + 1.0));
329 let (y0i, y1i) = (clamp_y(y0), clamp_y(y0 + 1.0));
330 let corners = [
331 (texel(x0i, y0i), (1.0 - tx) * (1.0 - ty)),
332 (texel(x1i, y0i), tx * (1.0 - ty)),
333 (texel(x0i, y1i), (1.0 - tx) * ty),
334 (texel(x1i, y1i), tx * ty),
335 ];
336 for (rgba, weight) in corners {
337 for channel in 0..4 {
338 sum[channel] += rgba[channel] * weight;
339 }
340 }
341 }
342 }
343 let count = (reach * reach) as f32;
344 let alpha = icon.tint[3];
345 self.blend(
346 x,
347 y,
348 [
349 sum[0] / count * icon.tint[0] * alpha,
350 sum[1] / count * icon.tint[1] * alpha,
351 sum[2] / count * icon.tint[2] * alpha,
352 sum[3] / count * alpha,
353 ],
354 );
355 }
356 }
357 Ok(())
358 }
359}
360
361fn fetch(level: &MipLevel, channels: usize, cell: [i32; 4], x: f32, y: f32) -> [f32; 4] {
362 let x0 = x.floor();
363 let y0 = y.floor();
364 let (tx, ty) = (x - x0, y - y0);
365 let clamp_x = |value: f32| (value as i32).clamp(cell[0], cell[0] + cell[2] - 1) as usize;
366 let clamp_y = |value: f32| (value as i32).clamp(cell[1], cell[1] + cell[3] - 1) as usize;
367 let (x0i, x1i) = (clamp_x(x0), clamp_x(x0 + 1.0));
368 let (y0i, y1i) = (clamp_y(y0), clamp_y(y0 + 1.0));
369 let at = |px: usize, py: usize| -> [f32; 4] {
370 let base = (py * level.width as usize + px) * channels;
371 let mut out = [0.0; 4];
372 for (value, byte) in out.iter_mut().zip(&level.bytes[base..base + channels]) {
373 *value = f32::from(*byte) / 255.0;
374 }
375 out
376 };
377 let corners = [
378 (at(x0i, y0i), (1.0 - tx) * (1.0 - ty)),
379 (at(x1i, y0i), tx * (1.0 - ty)),
380 (at(x0i, y1i), (1.0 - tx) * ty),
381 (at(x1i, y1i), tx * ty),
382 ];
383 let mut out = [0.0; 4];
384 for (rgba, weight) in corners {
385 for channel in 0..4 {
386 out[channel] += rgba[channel] * weight;
387 }
388 }
389 out
390}
391
392impl TextEngine {
393 pub fn print_spans(
394 &mut self,
395 image: &mut PrintImage,
396 block: &PrintBlock<'_>,
397 ) -> Result<Printed, Error> {
398 let density = image.density;
399 valid(
400 density,
401 &[
402 block.at[0],
403 block.at[1],
404 block.alpha,
405 block.turn[0],
406 block.turn[1],
407 block.turn[2],
408 ],
409 )?;
410 let clip = finite_clip(block.clip);
411 let laid = self.layout_spans(block.spans, block.wrap, density, block.anchor)?;
412 let (width, height, baseline) = (laid.width, laid.height, laid.baseline);
413 let lines = laid.buffer.layout_runs().count().max(1);
414 let selection = block
415 .selection
416 .map(|selection| (marks(laid, selection.from, selection.to), selection.color));
417 let mark = block.caret.and_then(|caret_style| {
418 caret(laid, caret_style.offset).map(|rect| (caret_style, rect))
419 });
420 let origin = [block.at[0], block.at[1] - baseline];
421 let rich = self.render_spans(
422 block.spans,
423 block.wrap,
424 density,
425 block.anchor,
426 Representation::Coverage,
427 origin,
428 block.alpha,
429 clip,
430 block.turn,
431 )?;
432 if let Some((rects, color)) = selection {
433 for rect in rects {
434 image.fill(
435 [origin[0] + rect[0], origin[1] + rect[1], rect[2], rect[3]],
436 color,
437 clip,
438 block.turn,
439 );
440 }
441 }
442 let frame = Frame::new(density, clip, block.turn);
443 for quad in &rich.quads {
444 let (level, channels) = match quad.atlas_page {
445 AtlasPage::Monochrome => (&rich.atlas.levels[0], 1usize),
446 AtlasPage::Color => match &rich.color_atlas {
447 Some(atlas) => (&atlas.levels[0], 4usize),
448 None => continue,
449 },
450 };
451 let rect = quad.rect.map(|value| (value * density).round());
452 if rect[2] <= 0.0 || rect[3] <= 0.0 {
453 continue;
454 }
455 let cell = [
456 (quad.uv[0] * level.width as f32).round() as i32,
457 (quad.uv[1] * level.height as f32).round() as i32,
458 (quad.uv[2] * level.width as f32).round() as i32,
459 (quad.uv[3] * level.height as f32).round() as i32,
460 ];
461 let cell = [cell[0], cell[1], cell[2] - cell[0], cell[3] - cell[1]];
462 if cell[2] <= 0 || cell[3] <= 0 {
463 continue;
464 }
465 let tint = quad.color;
466 let bounds = frame.bounds(rect);
467 for y in bounds[1]..bounds[3] {
468 for x in bounds[0]..bounds[2] {
469 let Some(p) = frame.source(x as f32 + 0.5, y as f32 + 0.5) else {
470 continue;
471 };
472 let fx = (p[0] - rect[0]) / rect[2];
473 let fy = (p[1] - rect[1]) / rect[3];
474 if !(0.0..=1.0).contains(&fx) || !(0.0..=1.0).contains(&fy) {
475 continue;
476 }
477 let ax = cell[0] as f32 + fx * cell[2] as f32 - 0.5;
478 let ay = cell[1] as f32 + fy * cell[3] as f32 - 0.5;
479 let sampled = fetch(level, channels, cell, ax, ay);
480 let source = if channels == 1 {
481 let opacity = sampled[0] * tint[3];
482 [
483 tint[0] * opacity,
484 tint[1] * opacity,
485 tint[2] * opacity,
486 opacity,
487 ]
488 } else {
489 [
490 sampled[0] * tint[0] * tint[3],
491 sampled[1] * tint[1] * tint[3],
492 sampled[2] * tint[2] * tint[3],
493 sampled[3] * tint[3],
494 ]
495 };
496 image.blend(x, y, source);
497 }
498 }
499 }
500 let caret_rect = mark.map(|(style, rect)| {
501 let bar = [
502 origin[0] + rect[0] + style.shift,
503 origin[1] + rect[1],
504 style.width,
505 style.height,
506 ];
507 image.fill(bar, style.color, clip, block.turn);
508 bar
509 });
510 Ok(Printed {
511 lines,
512 width,
513 height,
514 baseline,
515 origin,
516 caret: caret_rect,
517 })
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524 use crate::Face;
525
526 const FONT: &[u8] = include_bytes!("../fonts/EBGaramond[wght].ttf");
527 const COLOR_FONT: &[u8] = include_bytes!("../tests/fonts/BungeeColor-Regular.ttf");
528
529 fn spans(text: &str, size: f32, color: [f32; 4]) -> Vec<Span> {
530 vec![Span {
531 text: text.into(),
532 face: Face {
533 family: "EB Garamond".into(),
534 size,
535 line: size * 1.3,
536 weight: 400,
537 italic: false,
538 spacing: 0.0,
539 },
540 color,
541 }]
542 }
543
544 fn reference_alpha(
545 engine: &mut TextEngine,
546 spans: &[Span],
547 wrap: Option<f32>,
548 density: f32,
549 origin: [f32; 2],
550 size: [u32; 2],
551 ) -> Vec<f32> {
552 let rich = engine
553 .render_spans(
554 spans,
555 wrap,
556 density,
557 Anchor::Start,
558 Representation::Coverage,
559 origin,
560 1.0,
561 FULL_CLIP,
562 [0.0; 3],
563 )
564 .unwrap();
565 let level = &rich.atlas.levels[0];
566 let mut alpha = vec![0.0f32; (size[0] * size[1]) as usize];
567 for quad in &rich.quads {
568 if quad.atlas_page != AtlasPage::Monochrome {
569 continue;
570 }
571 let rect = quad.rect.map(|value| (value * density).round() as i32);
572 let cell = [
573 (quad.uv[0] * level.width as f32).round() as i32,
574 (quad.uv[1] * level.height as f32).round() as i32,
575 ];
576 for dy in 0..rect[3] {
577 for dx in 0..rect[2] {
578 let (x, y) = (rect[0] + dx, rect[1] + dy);
579 if x < 0 || y < 0 || x >= size[0] as i32 || y >= size[1] as i32 {
580 continue;
581 }
582 let byte = level.bytes
583 [((cell[1] + dy) as u32 * level.width + (cell[0] + dx) as u32) as usize];
584 let cover = f32::from(byte) / 255.0 * quad.color[3];
585 let slot = &mut alpha[(y as u32 * size[0] + x as u32) as usize];
586 *slot = 1.0 - (1.0 - *slot) * (1.0 - cover);
587 }
588 }
589 }
590 alpha
591 }
592
593 fn print(
594 engine: &mut TextEngine,
595 spans: &[Span],
596 edit: impl FnOnce(&mut PrintBlock<'_>),
597 ) -> (PrintImage, Printed) {
598 let mut image = PrintImage::new(60.0, 40.0, 2.0).unwrap();
599 let mut block = PrintBlock::new(spans, [4.0, 24.0]);
600 edit(&mut block);
601 let printed = engine.print_spans(&mut image, &block).unwrap();
602 (image, printed)
603 }
604
605 fn close(a: u8, b: u8, tolerance: i32) -> bool {
606 (i32::from(a) - i32::from(b)).abs() <= tolerance
607 }
608
609 #[test]
610 fn plain_text_matches_render_spans_coverage() {
611 let mut engine = TextEngine::new(FONT).unwrap();
612 let text = spans("Quiet paper, fine ink", 9.0, [0.0, 0.0, 0.0, 1.0]);
613 let (image, printed) = print(&mut engine, &text, |_| {});
614 let expected = reference_alpha(
615 &mut engine,
616 &text,
617 None,
618 2.0,
619 printed.origin,
620 [image.width, image.height],
621 );
622 let mut inked = 0;
623 for y in 0..image.height {
624 for x in 0..image.width {
625 let want = (expected[(y * image.width + x) as usize] * 255.0).round() as u8;
626 let got = image.pixel(x, y);
627 assert!(close(got[3], want, 1), "({x}, {y}): {} vs {want}", got[3]);
628 assert_eq!(got[..3], [0, 0, 0]);
629 inked += usize::from(got[3] > 0);
630 }
631 }
632 assert!(inked > 100);
633 assert_eq!(printed.lines, 1);
634 assert!(printed.width > 20.0);
635 }
636
637 #[test]
638 fn wrapped_text_counts_its_lines_and_matches_coverage() {
639 let mut engine = TextEngine::new(FONT).unwrap();
640 let text = spans(
641 "Quiet paper holds fine ink and a long line that must wrap",
642 5.0,
643 [0.0, 0.0, 0.0, 1.0],
644 );
645 let (image, printed) = print(&mut engine, &text, |block| block.wrap = Some(30.0));
646 let expected = reference_alpha(
647 &mut engine,
648 &text,
649 Some(30.0),
650 2.0,
651 printed.origin,
652 [image.width, image.height],
653 );
654 assert!(printed.lines >= 2);
655 for y in 0..image.height {
656 for x in 0..image.width {
657 let want = (expected[(y * image.width + x) as usize] * 255.0).round() as u8;
658 assert!(close(image.pixel(x, y)[3], want, 1));
659 }
660 }
661 }
662
663 #[test]
664 fn colour_is_premultiplied_by_the_span_density() {
665 let mut engine = TextEngine::new(FONT).unwrap();
666 let text = spans("Ink", 12.0, [0.8, 0.2, 0.1, 0.5]);
667 let (image, _) = print(&mut engine, &text, |_| {});
668 let mut peak = 0;
669 for y in 0..image.height {
670 for x in 0..image.width {
671 let [r, g, b, a] = image.pixel(x, y);
672 peak = peak.max(a);
673 let alpha = f32::from(a);
674 assert!(close(r, (alpha * 0.8).round() as u8, 1));
675 assert!(close(g, (alpha * 0.2).round() as u8, 1));
676 assert!(close(b, (alpha * 0.1).round() as u8, 1));
677 }
678 }
679 assert!((126..=129).contains(&peak), "{peak}");
680 }
681
682 #[test]
683 fn block_alpha_scales_the_print() {
684 let mut engine = TextEngine::new(FONT).unwrap();
685 let text = spans("Ink", 12.0, [0.0, 0.0, 0.0, 1.0]);
686 let (full, _) = print(&mut engine, &text, |_| {});
687 let (half, _) = print(&mut engine, &text, |block| block.alpha = 0.5);
688 for y in 0..full.height {
689 for x in 0..full.width {
690 let want = (f32::from(full.pixel(x, y)[3]) * 0.5).round() as u8;
691 assert!(close(half.pixel(x, y)[3], want, 1));
692 }
693 }
694 }
695
696 #[test]
697 fn clip_keeps_only_the_inside() {
698 let mut engine = TextEngine::new(FONT).unwrap();
699 let text = spans("Quiet paper, fine ink", 9.0, [0.0, 0.0, 0.0, 1.0]);
700 let (whole, _) = print(&mut engine, &text, |_| {});
701 let (clipped, _) = print(&mut engine, &text, |block| {
702 block.clip = [0.0, 0.0, 20.0, 40.0]
703 });
704 let mut kept = 0;
705 for y in 0..whole.height {
706 for x in 0..whole.width {
707 let got = clipped.pixel(x, y);
708 if x < 40 {
709 assert_eq!(got, whole.pixel(x, y));
710 kept += usize::from(got[3] > 0);
711 } else {
712 assert_eq!(got, [0; 4], "({x}, {y})");
713 }
714 }
715 }
716 assert!(kept > 50);
717 assert!(whole.pixels.iter().skip(40 * 4).any(|&byte| byte > 0));
718 }
719
720 #[test]
721 fn a_quarter_turn_moves_every_texel() {
722 let mut engine = TextEngine::new(FONT).unwrap();
723 let text = spans("Quiet paper", 9.0, [0.0, 0.0, 0.0, 1.0]);
724 let mut upright = PrintImage::new(40.0, 40.0, 2.0).unwrap();
725 let mut turned = PrintImage::new(40.0, 40.0, 2.0).unwrap();
726 let block = PrintBlock::new(&text, [4.0, 24.0]);
727 engine.print_spans(&mut upright, &block).unwrap();
728 let mut spun = block.clone();
729 spun.turn = [20.0, 20.0, std::f32::consts::FRAC_PI_2];
730 engine.print_spans(&mut turned, &spun).unwrap();
731 let mut inked = 0;
732 for y in 0..80 {
733 for x in 0..80 {
734 let want = upright.pixel(x, y);
735 let got = turned.pixel(79 - y, x);
736 for channel in 0..4 {
737 assert!(
738 close(got[channel], want[channel], 1),
739 "({x}, {y}) channel {channel}"
740 );
741 }
742 inked += usize::from(want[3] > 0);
743 }
744 }
745 assert!(inked > 100);
746 }
747
748 #[test]
749 fn a_slight_turn_keeps_the_ink() {
750 let mut engine = TextEngine::new(FONT).unwrap();
751 let text = spans("Quiet paper", 9.0, [0.0, 0.0, 0.0, 1.0]);
752 let (upright, _) = print(&mut engine, &text, |_| {});
753 let (tilted, _) = print(&mut engine, &text, |block| block.turn = [4.0, 24.0, 0.1]);
754 let sum = |image: &PrintImage| {
755 image
756 .pixels
757 .chunks_exact(4)
758 .map(|texel| f32::from(texel[3]))
759 .sum::<f32>()
760 };
761 let ratio = sum(&tilted) / sum(&upright);
762 assert!((0.9..1.1).contains(&ratio), "{ratio}");
763 assert_ne!(tilted.pixels, upright.pixels);
764 }
765
766 #[test]
767 fn the_caret_sits_after_the_letter_and_the_selection_behind_the_words() {
768 let mut engine = TextEngine::new(FONT).unwrap();
769 let text = spans("hello world", 9.0, [0.0, 0.0, 0.0, 1.0]);
770 let (image, printed) = print(&mut engine, &text, |block| {
771 block.selection = Some(Selection {
772 from: 0,
773 to: 5,
774 color: [0.0, 0.0, 1.0, 1.0],
775 });
776 block.caret = Some(Caret {
777 offset: 11,
778 color: [1.0, 0.0, 0.0, 1.0],
779 width: 1.0,
780 height: 9.0,
781 shift: 0.5,
782 });
783 });
784 let laid = engine
785 .layout_spans(&text, None, 2.0, Anchor::Start)
786 .unwrap();
787 let last = marks(laid, 10, 11)[0];
788 let edge = last[0] + last[2];
789 let bar = printed.caret.unwrap();
790 assert!((bar[0] - (printed.origin[0] + edge + 0.5)).abs() < 1e-3);
791 let column = ((bar[0] + 0.5) * 2.0) as u32;
792 let row = ((bar[1] + 4.0) * 2.0) as u32;
793 assert_eq!(image.pixel(column, row), [255, 0, 0, 255]);
794 let selected = marks(laid, 0, 5)[0];
795 let top = ((printed.origin[1] + selected[1] + 0.5) * 2.0) as u32;
796 let left = ((printed.origin[0] + selected[0] + selected[2] * 0.5) * 2.0) as u32;
797 assert_eq!(image.pixel(left, top), [0, 0, 255, 255]);
798 let after = ((printed.origin[0] + selected[0] + selected[2] + 1.0) * 2.0) as u32;
799 assert_eq!(image.pixel(after, top)[3], 0);
800 let mut line = marks(laid, 0, 11);
801 assert_eq!(line.len(), 1);
802 let whole = line.remove(0);
803 assert!((caret(laid, 0).unwrap()[0] - whole[0]).abs() < 1e-3);
804 assert!((caret(laid, 11).unwrap()[0] - (whole[0] + whole[2])).abs() < 1e-3);
805 }
806
807 #[test]
808 fn carets_follow_wrapped_lines() {
809 let mut engine = TextEngine::new(FONT).unwrap();
810 let text = spans(
811 "Quiet paper holds fine ink and a long line that must wrap",
812 9.0,
813 [0.0, 0.0, 0.0, 1.0],
814 );
815 let laid = engine
816 .layout_spans(&text, Some(40.0), 1.0, Anchor::Start)
817 .unwrap();
818 let first = caret(laid, 0).unwrap();
819 let last = caret(laid, text[0].text.len()).unwrap();
820 assert!(last[1] > first[1]);
821 assert!(first[0] < 0.01);
822 }
823
824 #[test]
825 fn colour_glyphs_keep_their_colours() {
826 let mut engine = TextEngine::new(COLOR_FONT).unwrap();
827 let text = vec![Span {
828 text: "A".into(),
829 face: Face {
830 family: "Bungee Color".into(),
831 size: 20.0,
832 line: 24.0,
833 weight: 400,
834 italic: false,
835 spacing: 0.0,
836 },
837 color: [1.0; 4],
838 }];
839 let (image, _) = print(&mut engine, &text, |_| {});
840 let mut coloured = 0;
841 for texel in image.pixels.chunks_exact(4) {
842 assert!(texel[..3].iter().all(|&c| c <= texel[3] + 1), "{texel:?}");
843 coloured += usize::from(texel[3] > 0 && texel[0] != texel[1]);
844 }
845 assert!(coloured > 20);
846 }
847
848 #[test]
849 fn icons_land_at_their_texels_with_tint() {
850 let mut image = PrintImage::from_texels(6, 6, 1.0).unwrap();
851 let rgba = [
852 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 128,
853 ];
854 let mut icon = Icon::new([2.0, 2.0, 2.0, 2.0], 2, 2, &rgba);
855 image.icon(&icon).unwrap();
856 assert_eq!(image.pixel(2, 2), [255, 0, 0, 255]);
857 assert_eq!(image.pixel(3, 2), [0, 255, 0, 255]);
858 assert_eq!(image.pixel(2, 3), [0, 0, 255, 255]);
859 assert_eq!(image.pixel(3, 3), [128, 128, 128, 128]);
860 assert_eq!(image.pixel(1, 2), [0; 4]);
861 assert_eq!(image.pixel(4, 3), [0; 4]);
862 let mut tinted = PrintImage::from_texels(6, 6, 1.0).unwrap();
863 icon.tint = [1.0, 1.0, 1.0, 0.5];
864 tinted.icon(&icon).unwrap();
865 assert_eq!(tinted.pixel(2, 2), [128, 0, 0, 128]);
866 assert!(image.icon(&Icon::new([0.0; 4], 2, 2, &rgba)).is_err());
867 assert!(
868 image
869 .icon(&Icon::new([0.0, 0.0, 1.0, 1.0], 2, 2, &rgba[..8]))
870 .is_err()
871 );
872 }
873
874 #[test]
875 fn fills_cover_fractions_of_texels() {
876 let mut image = PrintImage::from_texels(4, 4, 2.0).unwrap();
877 image.fill(
878 [0.25, 0.0, 1.0, 1.0],
879 [0.0, 0.0, 0.0, 1.0],
880 FULL_CLIP,
881 [0.0; 3],
882 );
883 assert_eq!(image.pixel(0, 0)[3], 128);
884 assert_eq!(image.pixel(1, 0)[3], 255);
885 assert_eq!(image.pixel(2, 0)[3], 128);
886 assert_eq!(image.pixel(3, 0)[3], 0);
887 }
888
889 #[test]
890 fn bad_inputs_are_refused() {
891 assert!(PrintImage::new(10.0, 10.0, 0.0).is_err());
892 assert!(PrintImage::new(0.0, 10.0, 1.0).is_err());
893 assert!(PrintImage::new(f32::NAN, 10.0, 1.0).is_err());
894 assert!(PrintImage::from_texels(0, 4, 1.0).is_err());
895 assert!(PrintImage::from_texels(100_000, 100_000, 1.0).is_err());
896 let mut engine = TextEngine::new(FONT).unwrap();
897 let text = spans("x", 9.0, [0.0, 0.0, 0.0, 1.0]);
898 let mut image = PrintImage::new(10.0, 10.0, 1.0).unwrap();
899 let mut block = PrintBlock::new(&text, [f32::NAN, 0.0]);
900 assert!(engine.print_spans(&mut image, &block).is_err());
901 block.at = [0.0, 5.0];
902 assert!(engine.print_spans(&mut image, &block).is_ok());
903 }
904}