1pub mod page;
2#[cfg(test)]
3mod profile;
4mod tag_art;
5pub use tag_art::{TagArt, art_sources, import as import_tag_art};
6
7use crate::{
8 layout::{TextBrush, TextLayout},
9 outline::TagIcon,
10};
11use draw::{GlyphRun, Glyphs, Layer, Primitive, RenderError};
12use parley::PositionedLayoutItem;
13
14#[derive(Clone, Copy)]
15pub struct Viewport {
16 pub size: [u32; 2],
17 /// Physical pixels per document point.
18 pub scale: f32,
19 /// Physical pixel position of the document origin.
20 pub origin: [f32; 2],
21}
22
23impl Viewport {
24 pub fn document_point(&self, point: [f32; 2]) -> [f32; 2] {
25 [
26 (point[0] - self.origin[0]) / self.scale,
27 (point[1] - self.origin[1]) / self.scale,
28 ]
29 }
30
31 /// Document-point primitives drawn through this viewport.
32 pub fn layer<'a>(&self, primitives: &'a [Primitive<'a>]) -> Layer<'a> {
33 Layer {
34 scale: self.scale,
35 origin: self.origin,
36 clip: None,
37 backdrop: None,
38 round: None,
39 motion: None,
40 primitives,
41 }
42 }
43}
44
45/// The page's paper and the ink that content in OneNote's automatic colour draws in,
46/// linear RGBA.
47#[derive(Clone, Copy, Debug)]
48pub struct Paper {
49 pub color: [f32; 4],
50 pub ink: [f32; 4],
51}
52
53impl Paper {
54 pub const WHITE: Self = Self {
55 color: [1.0; 4],
56 ink: [0.0, 0.0, 0.0, 1.0],
57 };
58
59 /// This paper under a page coloured `color` (COLORREF, OneNote's View, Page Color): the
60 /// colour as OneNote paints it on white paper, moved onto this paper as a fill is, so
61 /// dark paper takes the colour's hue at its own depth. Text keeps its ink; template art
62 /// lies over the colour.
63 pub fn colored(self, color: Option<u32>) -> Self {
64 match color {
65 Some(color) => Self {
66 color: self.tint(colorref(color)),
67 ..self
68 },
69 None => self,
70 }
71 }
72
73 /// A fill OneNote draws on white paper, moved onto this paper: as far from it in
74 /// OKLab lightness as from white, in the same hue and chroma, so text keeps its
75 /// contrast on it.
76 pub fn tint(&self, light: [f32; 4]) -> [f32; 4] {
77 let [lightness, a, b] = draw::oklab(light);
78 let [paper, ..] = draw::oklab(self.color);
79 let distance = 1.0 - lightness;
80 let lightness = if paper < 0.5 {
81 paper + distance
82 } else {
83 paper - distance
84 };
85 let [red, green, blue] = draw::from_oklab([lightness, a, b]);
86 [red, green, blue, light[3]]
87 }
88
89 /// A near-neutral colour OneNote draws on white paper, moved onto this paper: its
90 /// darkness becomes ink and its tint tints the paper.
91 pub(crate) fn shade(&self, light: [f32; 4]) -> [f32; 4] {
92 let ink = 1.0 - (light[0] + light[1] + light[2]) / 3.0;
93 let mut shade = light;
94 for channel in 0..3 {
95 shade[channel] = self.color[channel] * light[channel] + self.ink[channel] * ink;
96 }
97 shade
98 }
99}
100
101impl Glyphs for TextLayout {
102 fn runs(
103 &self,
104 paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>,
105 ) -> Result<(), RenderError> {
106 for (line, bounds) in self.lines() {
107 for item in line.items() {
108 let PositionedLayoutItem::GlyphRun(run) = item else {
109 continue;
110 };
111 draw::paint_parley_run(
112 &run,
113 &self.text,
114 bounds.baseline,
115 run.style().brush.rise,
116 [bounds.top, bounds.height],
117 // Automatic text on a highlight stays black, but on a black highlight, which
118 // paints in the ink, it stays ink.
119 |brush: &TextBrush| {
120 brush
121 .color
122 .or(brush.highlight.filter(|&color| color != 0).map(|_| 0))
123 .map(colorref)
124 },
125 run.style()
126 .brush
127 .highlight
128 .filter(|&color| color != 0)
129 .map(colorref),
130 paint,
131 )?;
132 }
133 }
134 Ok(())
135 }
136}
137
138/// The layout a text primitive paints.
139#[cfg(test)]
140pub(crate) fn painted_layout(text: &dyn Glyphs) -> &TextLayout {
141 (text as &dyn std::any::Any)
142 .downcast_ref()
143 .expect("Page text is a TextLayout")
144}
145
146const CHECKMARK: &str = include_str!("../../assets/tags/checkmark.svg");
147const BOX_SMALL: &str = include_str!("../../assets/tags/box-small.svg");
148const CHECK_SMALL: &str = include_str!("../../assets/tags/check-small.svg");
149const TAG: &str = include_str!("../../assets/tags/tag.svg");
150
151/// How a tag symbol's artwork takes a check: a large box under it, a small box with the
152/// check between the box and its badge, or art that shows none.
153enum Art {
154 Box(&'static str),
155 Badged(&'static str),
156 Plain(&'static str),
157}
158
159/// The artwork of symbol `shape` of MS-ONE's NoteTagShape, as OneNote 2010 draws it on the
160/// page. A shape MS-ONE does not list shows a plain tag.
161fn art(shape: u16) -> Art {
162 use Art::{Badged, Box, Plain};
163 macro_rules! tag {
164 ($name:literal) => {
165 include_str!(concat!("../../assets/tags/", $name, ".svg"))
166 };
167 }
168 match shape {
169 1 => Box(tag!("checkbox-green")),
170 2 => Box(tag!("checkbox-yellow")),
171 3 => Box(tag!("checkbox")),
172 4 => Badged(tag!("mark-star-green")),
173 5 => Badged(tag!("mark-star-yellow")),
174 6 => Badged(tag!("mark-star-blue")),
175 7 => Badged(tag!("mark-exclamation-green")),
176 8 => Badged(tag!("mark-exclamation-yellow")),
177 9 => Badged(tag!("mark-exclamation-blue")),
178 10 => Badged(tag!("mark-arrow-green")),
179 11 => Badged(tag!("mark-arrow-yellow")),
180 12 => Badged(tag!("mark-arrow-blue")),
181 13 => Plain(tag!("star")),
182 14 => Plain(tag!("follow-up")),
183 15 => Plain(tag!("question")),
184 16 => Plain(tag!("arrow-right-blue")),
185 17 => Plain(tag!("exclamation")),
186 18 => Plain(tag!("phone")),
187 19 => Plain(tag!("calendar")),
188 20 => Plain(tag!("clock")),
189 21 => Plain(tag!("idea")),
190 22 => Plain(tag!("pushpin")),
191 23 => Plain(tag!("address")),
192 24 => Plain(tag!("blog")),
193 25 => Plain(tag!("smiley")),
194 26 => Plain(tag!("ribbon")),
195 27 => Plain(tag!("key")),
196 28 => Badged(tag!("mark-one-blue")),
197 29 => Plain(tag!("circle-1-blue")),
198 30 => Badged(tag!("mark-two-blue")),
199 31 => Plain(tag!("circle-2-blue")),
200 32 => Badged(tag!("mark-three-blue")),
201 33 => Plain(tag!("circle-3-blue")),
202 34 => Plain(tag!("star8-blue")),
203 35 => Plain(tag!("tick-blue")),
204 36 => Plain(tag!("circle-blue")),
205 37 => Plain(tag!("arrow-down-blue")),
206 38 => Plain(tag!("arrow-left-blue")),
207 39 => Plain(tag!("solid-target-blue")),
208 40 => Plain(tag!("star-blue")),
209 41 => Plain(tag!("sun-blue")),
210 42 => Plain(tag!("target-blue")),
211 43 => Plain(tag!("triangle-blue")),
212 44 => Plain(tag!("umbrella-blue")),
213 45 => Plain(tag!("arrow-up-blue")),
214 46 => Plain(tag!("x-dots-blue")),
215 47 => Plain(tag!("x-blue")),
216 48 => Badged(tag!("mark-one-green")),
217 49 => Plain(tag!("circle-1-green")),
218 50 => Badged(tag!("mark-two-green")),
219 51 => Plain(tag!("circle-2-green")),
220 52 => Badged(tag!("mark-three-green")),
221 53 => Plain(tag!("circle-3-green")),
222 54 => Plain(tag!("star8-green")),
223 55 => Plain(tag!("tick-green")),
224 56 => Plain(tag!("circle-green")),
225 57 => Plain(tag!("arrow-down-green")),
226 58 => Plain(tag!("arrow-left-green")),
227 59 => Plain(tag!("arrow-right-green")),
228 60 => Plain(tag!("solid-target-green")),
229 61 => Plain(tag!("star-green")),
230 62 => Plain(tag!("sun-green")),
231 63 => Plain(tag!("target-green")),
232 64 => Plain(tag!("triangle-green")),
233 65 => Plain(tag!("umbrella-green")),
234 66 => Plain(tag!("arrow-up-green")),
235 67 => Plain(tag!("x-dots-green")),
236 68 => Plain(tag!("x-green")),
237 69 => Badged(tag!("mark-one-yellow")),
238 70 => Plain(tag!("circle-1-yellow")),
239 71 => Badged(tag!("mark-two-yellow")),
240 72 => Plain(tag!("circle-2-yellow")),
241 73 => Badged(tag!("mark-three-yellow")),
242 74 => Plain(tag!("circle-3-yellow")),
243 75 => Plain(tag!("star8-yellow")),
244 76 => Plain(tag!("tick-yellow")),
245 77 => Plain(tag!("circle-yellow")),
246 78 => Plain(tag!("arrow-down-yellow")),
247 79 => Plain(tag!("arrow-left-yellow")),
248 80 => Plain(tag!("arrow-right-yellow")),
249 81 => Plain(tag!("solid-target-yellow")),
250 82 => Plain(tag!("sun-yellow")),
251 83 => Plain(tag!("target-yellow")),
252 84 => Plain(tag!("triangle-yellow")),
253 85 => Plain(tag!("umbrella-yellow")),
254 86 => Plain(tag!("arrow-up-yellow")),
255 87 => Plain(tag!("x-dots-yellow")),
256 88 => Plain(tag!("x-yellow")),
257 89 => Plain(tag!("flag-today")),
258 90 => Plain(tag!("flag-tomorrow")),
259 91 => Plain(tag!("flag-this-week")),
260 92 => Plain(tag!("flag-next-week")),
261 93 => Plain(tag!("flag-no-date")),
262 94 => Badged(tag!("mark-person-blue")),
263 95 => Badged(tag!("mark-person-yellow")),
264 96 => Badged(tag!("mark-person-green")),
265 97 => Badged(tag!("mark-flag-blue")),
266 98 => Badged(tag!("mark-flag-yellow")),
267 99 => Badged(tag!("mark-flag-green")),
268 100 => Plain(tag!("red-square")),
269 101 => Plain(tag!("yellow-square")),
270 102 => Plain(tag!("blue-square")),
271 103 => Plain(tag!("green-square")),
272 104 => Plain(tag!("orange-square")),
273 105 => Plain(tag!("pink-square")),
274 106 => Plain(tag!("email")),
275 107 => Plain(tag!("envelope")),
276 108 => Plain(tag!("envelope-open")),
277 109 => Plain(tag!("mobile")),
278 110 => Plain(tag!("phone-clock")),
279 111 => Plain(tag!("question-balloon")),
280 112 => Plain(tag!("paperclip")),
281 113 => Plain(tag!("frown")),
282 114 => Plain(tag!("im-contact")),
283 115 => Plain(tag!("person")),
284 116 => Plain(tag!("people")),
285 117 => Plain(tag!("bell")),
286 118 => Plain(tag!("contact")),
287 119 => Plain(tag!("rose")),
288 120 => Plain(tag!("date")),
289 121 => Plain(tag!("music")),
290 122 => Plain(tag!("movie")),
291 123 => Plain(tag!("quote")),
292 124 => Plain(tag!("globe")),
293 125 => Plain(tag!("web")),
294 126 => Plain(tag!("laptop")),
295 127 => Plain(tag!("plane")),
296 128 => Plain(tag!("car")),
297 129 => Plain(tag!("binoculars")),
298 130 => Plain(tag!("presentation")),
299 131 => Plain(tag!("password")),
300 132 => Plain(tag!("book")),
301 133 => Plain(tag!("notebook")),
302 134 => Plain(tag!("paper")),
303 135 => Plain(tag!("research")),
304 136 => Plain(tag!("highlight")),
305 137 => Plain(tag!("dollar")),
306 138 => Plain(tag!("coins")),
307 139 => Plain(tag!("schedule")),
308 140 => Plain(tag!("lightning")),
309 141 => Plain(tag!("cloud")),
310 142 => Plain(tag!("heart")),
311 143 => Plain(tag!("sunflower")),
312 _ => Plain(TAG),
313 }
314}
315
316/// Symbols MS-ONE lists.
317const SYMBOLS: u16 = 143;
318
319/// The tag's artwork, drawn in order.
320pub fn tag_sources(icon: TagIcon) -> &'static [&'static str] {
321 static SOURCES: std::sync::OnceLock<Vec<[Vec<&'static str>; 2]>> = std::sync::OnceLock::new();
322 let (shape, checked) = match icon {
323 TagIcon::Symbol { shape, checked } => (shape, checked),
324 TagIcon::Task { shape } => (shape, false),
325 };
326 let sources = SOURCES.get_or_init(|| {
327 (0..=SYMBOLS)
328 .map(|shape| match art(shape) {
329 Art::Box(art) => [vec![art], vec![art, CHECKMARK]],
330 Art::Badged(badge) => [vec![BOX_SMALL, badge], vec![BOX_SMALL, CHECK_SMALL, badge]],
331 Art::Plain(art) => [vec![art], vec![art]],
332 })
333 .collect()
334 });
335 match sources.get(usize::from(shape)) {
336 Some(sources) => &sources[usize::from(checked)],
337 None => &[TAG],
338 }
339}
340
341/// A COLORREF (0x00BBGGRR) as linear RGBA.
342pub fn colorref(color: u32) -> [f32; 4] {
343 let [red, green, blue, _] = color.to_le_bytes();
344 draw::srgb(red, green, blue)
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::layout::TextEngine;
351 use onestore::document::Format;
352 use onestore::page::text::Paragraph;
353 use std::time::Duration;
354
355 /// OneNote 2010 draws paragraph `shape N` of `corpus/custom-tags/native/shapes.one` with
356 /// the art `DRAWN`'s Nth word names (its render: `shapes-1-72.png`, `shapes-73-143.png`);
357 /// each paragraph's tag definition stores the NoteTagShape.
358 #[test]
359 fn symbols_draw_the_art_onenote_draws_for_their_number() {
360 use onestore::page::{PageObject, ParagraphContent};
361 use onestore::{RevisionIndex, Store, document::Document, document::Kind, page::Page};
362 const DRAWN: &str = "\
363 checkbox-green checkbox-yellow checkbox mark-star-green mark-star-yellow \
364 mark-star-blue mark-exclamation-green mark-exclamation-yellow mark-exclamation-blue \
365 mark-arrow-green mark-arrow-yellow mark-arrow-blue star follow-up question \
366 arrow-right-blue exclamation phone calendar clock idea pushpin address blog smiley \
367 ribbon key mark-one-blue circle-1-blue mark-two-blue circle-2-blue mark-three-blue \
368 circle-3-blue star8-blue tick-blue circle-blue arrow-down-blue arrow-left-blue \
369 solid-target-blue star-blue sun-blue target-blue triangle-blue umbrella-blue \
370 arrow-up-blue x-dots-blue x-blue mark-one-green circle-1-green mark-two-green \
371 circle-2-green mark-three-green circle-3-green star8-green tick-green circle-green \
372 arrow-down-green arrow-left-green arrow-right-green solid-target-green star-green \
373 sun-green target-green triangle-green umbrella-green arrow-up-green x-dots-green \
374 x-green mark-one-yellow circle-1-yellow mark-two-yellow circle-2-yellow \
375 mark-three-yellow circle-3-yellow star8-yellow tick-yellow circle-yellow \
376 arrow-down-yellow arrow-left-yellow arrow-right-yellow solid-target-yellow \
377 sun-yellow target-yellow triangle-yellow umbrella-yellow arrow-up-yellow \
378 x-dots-yellow x-yellow flag-today flag-tomorrow flag-this-week flag-next-week \
379 flag-no-date mark-person-blue mark-person-yellow mark-person-green mark-flag-blue \
380 mark-flag-yellow mark-flag-green red-square yellow-square blue-square green-square \
381 orange-square pink-square email envelope envelope-open mobile phone-clock \
382 question-balloon paperclip frown im-contact person people bell contact rose date \
383 music movie quote globe web laptop plane car binoculars presentation password book \
384 notebook paper research highlight dollar coins schedule lightning cloud heart \
385 sunflower";
386 let bytes = include_bytes!("../../../../corpus/custom-tags/native/shapes.one");
387 let store = Store::parse(bytes).unwrap();
388 let index = RevisionIndex::parse(&store).unwrap();
389 let document = Document::parse(&index).unwrap();
390 let files: Vec<&str> = DRAWN.split_whitespace().collect();
391 let mut seen = Vec::new();
392 for (space, _) in document.pages().unwrap() {
393 let page = Page::from_space(&document, space).unwrap();
394 if !page.title.starts_with("Shapes") {
395 continue;
396 }
397 for object in &page.objects {
398 let PageObject::Outline(outline) = object else {
399 continue;
400 };
401 for paragraph in &outline.paragraphs {
402 let ParagraphContent::Text(text) = &paragraph.content else {
403 continue;
404 };
405 let label: usize = text.text.text()["shape ".len()..].parse().unwrap();
406 let definition = text.tags[0].definition.unwrap();
407 let Kind::TagDefinition {
408 shape: Some(shape), ..
409 } = page.definitions[&definition].kind
410 else {
411 panic!("shape {label} stores no symbol");
412 };
413 let (Art::Box(drawn) | Art::Badged(drawn) | Art::Plain(drawn)) = art(shape);
414 let file = files[label - 1];
415 let expected = std::fs::read_to_string(format!(
416 "{}/assets/tags/{file}.svg",
417 env!("CARGO_MANIFEST_DIR")
418 ))
419 .unwrap();
420 assert_eq!(
421 drawn, expected,
422 "shape {label}, stored {shape}, draws {file}"
423 );
424 seen.push(label);
425 }
426 }
427 }
428 seen.sort_unstable();
429 assert_eq!(seen, (1..=143).collect::<Vec<_>>());
430 }
431
432 /// A page colour paints as OneNote paints it on white paper, and on dark paper as a
433 /// dark paper of its hue; no colour leaves the paper as it was.
434 #[test]
435 fn page_colours_colour_the_paper() {
436 let teal = 0x00f2f9d4;
437 assert_eq!(
438 Paper::WHITE
439 .colored(Some(teal))
440 .color
441 .map(|c| (c * 1000.0).round()),
442 colorref(teal).map(|c| (c * 1000.0).round())
443 );
444 let dark = Paper {
445 color: draw::srgb(0x1f, 0x20, 0x22),
446 ink: draw::srgb(0xe6, 0xe6, 0xe6),
447 };
448 let colored = dark.colored(Some(teal));
449 let [lightness, a, b] = draw::oklab(colored.color);
450 let [paper, ..] = draw::oklab(dark.color);
451 let [_, ta, tb] = draw::oklab(colorref(teal));
452 assert!(lightness > paper && lightness < 0.5);
453 assert!((a - ta).abs() < 1e-3 && (b - tb).abs() < 1e-3);
454 assert_eq!(colored.ink, dark.ink);
455 assert_eq!(dark.colored(None).color, dark.color);
456 }
457
458 #[test]
459 fn runs_carry_their_highlight_as_what_lies_behind_them() {
460 let run = |highlight, color| {
461 (
462 "text".into(),
463 Format {
464 highlight,
465 color,
466 ..Format::default()
467 },
468 )
469 };
470 let text = TextEngine::default()
471 .layout(
472 &Paragraph::from_runs([
473 run(None, None),
474 run(Some(0x0000ffff), None),
475 run(Some(0x0000ffff), Some(0x000000ff)),
476 run(Some(0), None),
477 ]),
478 400.0,
479 )
480 .unwrap();
481 let mut painted = Vec::new();
482 text.runs(&mut |run| {
483 painted.push((run.color, run.backdrop));
484 Ok(())
485 })
486 .unwrap();
487 let [black, red, yellow] = [0, 0x000000ff, 0x0000ffff].map(colorref);
488 assert_eq!(
489 painted,
490 [
491 (None, None),
492 (Some(black), Some(yellow)),
493 (Some(red), Some(yellow)),
494 // A black highlight paints in the ink, as its automatic text does.
495 (None, None),
496 ]
497 );
498 }
499
500 #[test]
501 #[ignore = "requires a native GPU adapter"]
502 fn tags_and_page_text_paint_through_the_renderer() {
503 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
504 let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
505 let (device, queue) =
506 pollster::block_on(adapter.request_device(&Default::default())).unwrap();
507 let size = [448, 64];
508 let target = device.create_texture(&wgpu::TextureDescriptor {
509 label: Some("Tag readback test"),
510 size: wgpu::Extent3d {
511 width: size[0],
512 height: size[1],
513 depth_or_array_layers: 1,
514 },
515 mip_level_count: 1,
516 sample_count: 1,
517 dimension: wgpu::TextureDimension::D2,
518 format: wgpu::TextureFormat::Rgba8UnormSrgb,
519 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
520 view_formats: &[],
521 });
522 let readback = device.create_buffer(&wgpu::BufferDescriptor {
523 label: Some("Tag readback"),
524 size: u64::from(size[0] * size[1] * 4),
525 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
526 mapped_at_creation: false,
527 });
528 let mut renderer = draw::Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb);
529 let icons: Vec<_> = [
530 (3, false),
531 (3, true),
532 (13, false),
533 (15, false),
534 (136, false),
535 (118, false),
536 (23, false),
537 (18, false),
538 (121, false),
539 (17, false),
540 (100, false),
541 (101, false),
542 (102, false),
543 (999, false),
544 ]
545 .into_iter()
546 .map(|(shape, checked)| TagIcon::of(shape, checked).unwrap())
547 .chain([TagIcon::Task { shape: 91 }])
548 .collect();
549 let text = TextEngine::default()
550 .layout(
551 &Paragraph::from_runs([
552 (
553 "Tagged".into(),
554 Format {
555 color: Some(0x000000ff),
556 underline: Some(true),
557 ..Format::default()
558 },
559 ),
560 (" automatic".into(), Format::default()),
561 (
562 " lit".into(),
563 Format {
564 highlight: Some(0x0000ffff),
565 ..Format::default()
566 },
567 ),
568 ]),
569 100.0,
570 )
571 .unwrap();
572 let mut primitives: Vec<_> = icons
573 .iter()
574 .enumerate()
575 .map(|(index, icon)| Primitive::Icon {
576 sources: tag_sources(*icon),
577 origin: [2.0 + 16.0 * index as f32, 2.0],
578 size: crate::outline::ParagraphTag::SIZE,
579 tint: [1.0; 4],
580 palette: draw::Palette::default(),
581 })
582 .collect();
583 primitives.push(Primitive::Text {
584 text: &text,
585 origin: [2.0, 20.0],
586 clip: None,
587 ink: [0.0, 1.0, 0.0, 1.0],
588 });
589 let viewport = Viewport {
590 size,
591 scale: 2.0,
592 origin: [0.0; 2],
593 };
594 renderer
595 .draw(
596 &target.create_view(&Default::default()).into(),
597 size,
598 [1.0; 4],
599 &[viewport.layer(&primitives)],
600 )
601 .unwrap();
602 let mut encoder = renderer
603 .device()
604 .create_command_encoder(&Default::default());
605 encoder.copy_texture_to_buffer(
606 target.as_image_copy(),
607 wgpu::TexelCopyBufferInfo {
608 buffer: &readback,
609 layout: wgpu::TexelCopyBufferLayout {
610 offset: 0,
611 bytes_per_row: Some(size[0] * 4),
612 rows_per_image: Some(size[1]),
613 },
614 },
615 wgpu::Extent3d {
616 width: size[0],
617 height: size[1],
618 depth_or_array_layers: 1,
619 },
620 );
621 renderer.queue().submit([encoder.finish()]);
622 readback.map_async(wgpu::MapMode::Read, .., |result| result.unwrap());
623 renderer
624 .device()
625 .poll(wgpu::PollType::Wait {
626 submission_index: None,
627 timeout: Some(Duration::from_secs(5)),
628 })
629 .unwrap();
630 let pixels = readback.get_mapped_range(..).unwrap().to_vec();
631 let pixel = |x: u32, y: u32| {
632 let offset = ((y * size[0] + x) * 4) as usize;
633 [pixels[offset], pixels[offset + 1], pixels[offset + 2]]
634 };
635 for index in 0..icons.len() as u32 {
636 let painted = (4..28)
637 .flat_map(|y| (4 + 32 * index..28 + 32 * index).map(move |x| (x, y)))
638 .filter(|(x, y)| pixel(*x, *y).iter().any(|v| *v < 200))
639 .count();
640 assert!(painted > 20, "tag {index} was not painted");
641 }
642 let count = |color: [u8; 3]| {
643 (40..64)
644 .flat_map(|y| (4..size[0]).map(move |x| (x, y)))
645 .filter(|(x, y)| {
646 pixel(*x, *y)
647 .iter()
648 .zip(color)
649 .all(|(v, c)| v.abs_diff(c) < 80)
650 })
651 .count()
652 };
653 assert!(
654 count([255, 0, 0]) > 50,
655 "text colour or underline was not painted"
656 );
657 assert!(
658 count([0, 255, 0]) > 50,
659 "automatic text did not paint in the primitive's ink"
660 );
661 assert!(
662 count([0, 0, 0]) > 10,
663 "automatic text on a highlight did not stay black"
664 );
665 }
666}