1#[cfg(all(feature = "wgpu", target_arch = "wasm32"))]
2mod canvas2d;
3mod colr;
4#[cfg(all(feature = "wgpu", windows))]
5mod d3d11;
6#[cfg(all(
7 feature = "wgpu",
8 any(windows, target_os = "macos", target_arch = "wasm32")
9))]
10mod dual;
11#[cfg(any(windows, target_os = "macos", not(feature = "wgpu")))]
12mod gl;
13mod icon;
14#[cfg(feature = "pdf")]
15mod pdf;
16mod text;
17#[cfg(feature = "wgpu")]
18mod translucent;
19#[cfg(feature = "wgpu")]
20mod webgpu;
21
22#[cfg(all(
23 feature = "wgpu",
24 any(windows, target_os = "macos", target_arch = "wasm32")
25))]
26use dual as backend;
27#[cfg(not(feature = "wgpu"))]
28use gl as backend;
29#[cfg(all(
30 feature = "wgpu",
31 not(any(windows, target_os = "macos", target_arch = "wasm32"))
32))]
33use webgpu as backend;
34
35pub use backend::Target;
36pub use icon::{Palette, picture_icon};
37#[cfg(feature = "pdf")]
38pub use pdf::{Sheet, pdf};
39pub use text::{Decoration, Glyph, GlyphRun, Glyphs, paint_parley_run};
40#[cfg(feature = "wgpu")]
41pub use translucent::Translucent;
42
43use bytemuck::{Pod, Zeroable};
44use image::ImageDecoder;
45use linebender_resource_handle::WeakBlob;
46use parley::fontique::Blob;
47use std::{
48 collections::{HashMap, HashSet},
49 ffi::CStr,
50 mem::offset_of,
51 ops::Range,
52};
53use swash::{
54 FontRef,
55 scale::{Render, ScaleContext, Source, StrikeWith, image::Content},
56 zeno::{Angle, Cap, Format, Join, Mask, Transform, Vector},
57};
58
59/// The atlas's side at first; it doubles, up to `MAX_ATLAS_SIZE`, when one frame needs
60/// more than half of it.
61const ATLAS_SIZE: u32 = 2048;
62const MAX_ATLAS_SIZE: u32 = 8192;
63/// Atlas entries per `ATLAS_SIZE` square of atlas.
64const MAX_GLYPHS: usize = 8192;
65const MAX_VERTICES: usize = 65_536;
66const VERTEX_BUFFER_BYTES: u64 = (MAX_VERTICES * size_of::<Vertex>()) as u64;
67/// Decoded bytes of all images one frame may paint.
68pub const MAX_IMAGE_BYTES: u64 = 64 * 1024 * 1024;
69const MAX_IMAGES: usize = 256;
70/// Decoded bytes one picture may take before `RasterImage::decode` shrinks it.
71const MAX_DECODE_BYTES: u64 = 256 * 1024 * 1024;
72
73#[derive(Clone)]
74pub struct RasterImage {
75 size: [u32; 2],
76 pixels: Blob<u8>,
77}
78
79/// A PNG, JPEG, GIF or TIFF decoder for `encoded`, refusing pictures past the decode limits.
80fn decoder(encoded: &[u8]) -> Result<impl ImageDecoder + '_, RenderError> {
81 if encoded.len() as u64 > MAX_DECODE_BYTES {
82 return Err(RenderError::ImageBudget);
83 }
84 let format = image::guess_format(encoded).map_err(RenderError::ImageDecode)?;
85 let mut reader = image::ImageReader::with_format(std::io::Cursor::new(encoded), format);
86 let mut limits = image::Limits::default();
87 limits.max_image_width = Some(16_384);
88 limits.max_image_height = Some(16_384);
89 limits.max_alloc = Some(MAX_DECODE_BYTES);
90 reader.limits(limits.clone());
91 let mut decoder = reader.into_decoder().map_err(RenderError::ImageDecode)?;
92 let (width, height) = decoder.dimensions();
93 if u64::from(width) * u64::from(height) * 4 > MAX_DECODE_BYTES
94 || decoder.total_bytes() > MAX_DECODE_BYTES
95 {
96 return Err(RenderError::ImageBudget);
97 }
98 limits
99 .reserve(decoder.total_bytes())
100 .map_err(RenderError::ImageDecode)?;
101 decoder
102 .set_limits(limits)
103 .map_err(RenderError::ImageDecode)?;
104 Ok(decoder)
105}
106
107/// Premultiplies straight-alpha sRGB RGBA in linear light, so filtering never bleeds the
108/// colour of transparent pixels.
109fn premultiply(pixels: &mut [u8]) {
110 for pixel in pixels.chunks_exact_mut(4) {
111 let alpha = pixel[3];
112 if alpha == 255 {
113 continue;
114 }
115 let linear = srgb(pixel[0], pixel[1], pixel[2]);
116 for (byte, value) in pixel[..3].iter_mut().zip(linear) {
117 *byte = srgb_byte(value * f32::from(alpha) / 255.0);
118 }
119 }
120}
121
122impl RasterImage {
123 /// The pixel size of an encoded picture, read from its header, or why `decode` would
124 /// refuse it.
125 pub fn measure(encoded: &[u8]) -> Result<[u32; 2], RenderError> {
126 let (width, height) = decoder(encoded)?.dimensions();
127 Ok([width, height])
128 }
129
130 /// Decodes a picture, shrinking it to at most `within` pixels along each axis.
131 pub fn decode(encoded: &[u8], within: [u32; 2]) -> Result<Self, RenderError> {
132 let decoder = decoder(encoded)?;
133 let mut pixels = image::DynamicImage::from_decoder(decoder)
134 .map_err(RenderError::ImageDecode)?
135 .into_rgba8();
136 premultiply(&mut pixels);
137 let size = [pixels.width(), pixels.height()];
138 let shown = [0, 1].map(|axis| size[axis].min(within[axis]).max(1));
139 if shown != size {
140 pixels = image::imageops::resize(
141 &pixels,
142 shown[0],
143 shown[1],
144 image::imageops::FilterType::Triangle,
145 );
146 }
147 Self::premultiplied(shown, pixels.into_raw())
148 }
149
150 /// Immutable, straight-alpha sRGB RGBA pixels retain one cache identity across clones.
151 pub fn new(size: [u32; 2], mut pixels: Vec<u8>) -> Result<Self, RenderError> {
152 premultiply(&mut pixels);
153 Self::premultiplied(size, pixels)
154 }
155
156 fn premultiplied(size: [u32; 2], pixels: Vec<u8>) -> Result<Self, RenderError> {
157 let bytes = u64::from(size[0])
158 .checked_mul(u64::from(size[1]))
159 .and_then(|v| v.checked_mul(4))
160 .ok_or(RenderError::InvalidImage)?;
161 if size.contains(&0) || bytes != pixels.len() as u64 {
162 return Err(RenderError::InvalidImage);
163 }
164 if bytes > MAX_IMAGE_BYTES {
165 return Err(RenderError::ImageBudget);
166 }
167 Ok(Self {
168 size,
169 pixels: pixels.into(),
170 })
171 }
172
173 pub fn size(&self) -> [u32; 2] {
174 self.size
175 }
176
177 /// The identity caches and uploads share across clones.
178 pub fn id(&self) -> u64 {
179 self.pixels.id()
180 }
181
182 /// Premultiplied sRGB RGBA rows.
183 pub fn pixels(&self) -> &[u8] {
184 self.pixels.as_ref()
185 }
186}
187
188/// What a backend submits: each group's batches, painted into an offscreen picture of its
189/// own, then the batches painting the target, and the images they show.
190struct Frame<'a> {
191 vertices: &'a [Vertex],
192 groups: &'a [Group],
193 batches: &'a [Batch],
194 images: &'a HashMap<u64, CachedImage>,
195}
196
197impl Frame<'_> {
198 /// The texture image `id` uploaded as, as the backend drawing it holds it.
199 fn image<T>(&self, id: u64) -> &T
200 where
201 backend::Image: AsRef<T>,
202 {
203 self.images[&id].texture.as_ref()
204 }
205}
206
207struct CachedImage {
208 texture: backend::Image,
209 bytes: u64,
210 /// The texture goes when every copy of its image has.
211 pixels: WeakBlob<u8>,
212}
213
214struct Batch {
215 vertices: Range<u32>,
216 blend: Blend,
217 scissor: [u32; 4],
218}
219
220/// Layers appearing as one, as `Blend::Group` paints their picture.
221struct Group {
222 batches: Vec<Batch>,
223 /// What the 2D canvas, which can't draw the group's strips in perspective, leans it by.
224 #[cfg_attr(not(target_arch = "wasm32"), expect(dead_code))]
225 motion: Motion,
226}
227
228/// How a batch meets what lies beneath it, and what it samples: the atlas unless it paints
229/// a picture.
230#[derive(Clone, Copy, PartialEq)]
231enum Blend {
232 Over,
233 /// A premultiplied picture over it.
234 Image(u64),
235 /// That group's offscreen picture over it.
236 Group(usize),
237 /// Clearing it by the batch's coverage.
238 Erase,
239 /// Multiplying it by the batch's colour where covered.
240 Multiply,
241}
242
243#[derive(Clone, Copy)]
244enum Factor {
245 Zero,
246 One,
247 SourceAlpha,
248 OneMinusSourceAlpha,
249 Destination,
250}
251
252impl Blend {
253 /// Each blend state a backend makes: colour's source and destination factors, then
254 /// alpha's, every one adding.
255 const STATES: [[Factor; 4]; 4] = {
256 use Factor::*;
257 [
258 // Coverage accumulates in alpha, leaving premultiplied colour over a transparent
259 // clear.
260 [SourceAlpha, OneMinusSourceAlpha, One, OneMinusSourceAlpha],
261 [One, OneMinusSourceAlpha, One, OneMinusSourceAlpha],
262 [Zero, OneMinusSourceAlpha, Zero, OneMinusSourceAlpha],
263 // The fragment's colour is scaled by its coverage, so this leaves the target times
264 // the colour where covered and the target elsewhere; alpha stays.
265 [Destination, OneMinusSourceAlpha, Zero, One],
266 ]
267 };
268
269 /// This blend's index in `STATES`.
270 fn state(self) -> usize {
271 match self {
272 Blend::Over => 0,
273 Blend::Image(_) | Blend::Group(_) => 1,
274 Blend::Erase => 2,
275 Blend::Multiply => 3,
276 }
277 }
278}
279
280#[repr(C)]
281#[derive(Clone, Copy, Pod, Zeroable)]
282struct Vertex {
283 position: [f32; 2],
284 uv: [f32; 2],
285 color: [f32; 4],
286 local: [f32; 2],
287 shape: [f32; 4],
288 stroke: f32,
289 /// The position from the middle of the rounded rectangle the vertex paints within, and
290 /// that rectangle's half size and corner radius; a zero half size paints everywhere.
291 clip_local: [f32; 2],
292 clip: [f32; 3],
293 /// The standard deviation, in device pixels, of the blur that makes `shape` a shadow;
294 /// negative, `shape` is a tapered capsule's half length and end radii.
295 blur: f32,
296}
297
298/// Each `Vertex` field's name in the shaders, float count and offset, in shader input order.
299const ATTRIBUTES: [(&CStr, usize, usize); 9] = [
300 (c"position", 2, offset_of!(Vertex, position)),
301 (c"uv", 2, offset_of!(Vertex, uv)),
302 (c"color", 4, offset_of!(Vertex, color)),
303 (c"local", 2, offset_of!(Vertex, local)),
304 (c"shape", 4, offset_of!(Vertex, shape)),
305 (c"stroke", 1, offset_of!(Vertex, stroke)),
306 (c"clip_local", 2, offset_of!(Vertex, clip_local)),
307 (c"clip", 3, offset_of!(Vertex, clip)),
308 (c"blur", 1, offset_of!(Vertex, blur)),
309];
310
311#[derive(Hash, PartialEq, Eq)]
312enum AtlasKey {
313 Text {
314 font: u64,
315 index: u32,
316 glyph: u16,
317 size: u32,
318 coords: Vec<i16>,
319 phase: [u8; 2],
320 embolden: bool,
321 skew: u32,
322 /// The ink's sRGB luminance in sixteenths, which `text_coverage` weights by.
323 tone: u8,
324 },
325 Icon {
326 sources: &'static [&'static str],
327 size: u32,
328 /// The bits of the colour `currentColor` paints.
329 ink: [u32; 3],
330 palette: [Option<[u32; 3]>; 3],
331 },
332 Path {
333 data: String,
334 scale: u32,
335 /// The style's kind and width bits.
336 style: (u8, u32),
337 phase: [u8; 2],
338 },
339}
340
341#[derive(Clone, Copy)]
342struct AtlasGlyph {
343 x: u32,
344 y: u32,
345 width: u32,
346 height: u32,
347 left: i32,
348 top: i32,
349 color: bool,
350}
351
352/// Primitives sharing one transform and clip.
353pub struct Layer<'a> {
354 /// Device pixels per layer unit.
355 pub scale: f32,
356 /// Device position of the layer origin.
357 pub origin: [f32; 2],
358 /// Device bounds `[left, top, right, bottom]` the layer paints within.
359 pub clip: Option<[f32; 4]>,
360 /// The colour behind the layer: on a dark one, text in dark colours of its own is
361 /// lifted to stay legible against what lies behind it, as OneNote's dark page does.
362 pub backdrop: Option<[f32; 4]>,
363 /// Device bounds and corner radius of a rounded rectangle the layer paints only inside.
364 pub round: Option<([f32; 4], f32)>,
365 /// Consecutive layers with the same motion appear as one: painted together offscreen,
366 /// then faded and leaned back as a whole, so none shows through another.
367 pub motion: Option<Motion>,
368 pub primitives: &'a [Primitive<'a>],
369}
370
371/// A layer part of the way through appearing: faded, and leaned back in perspective.
372#[derive(Clone, Copy, Debug, PartialEq)]
373pub struct Motion {
374 pub opacity: f32,
375 /// Radians the layer turns about the horizontal line through `pivot`, its part below
376 /// the line leaning away from the viewer.
377 pub tilt: f32,
378 /// Device point the layer turns about.
379 pub pivot: [f32; 2],
380}
381
382impl Motion {
383 /// Target heights away the viewer sees a turn from, so a small one reads as depth.
384 const DISTANCE: f32 = 2.0;
385
386 /// Where device point `[x, y]` on a target `height` tall shows, turned.
387 fn project(&self, [x, y]: [f32; 2], height: f32) -> [f32; 2] {
388 let distance = Self::DISTANCE * height;
389 let below = y - self.pivot[1];
390 let near = distance / (distance + below * self.tilt.sin());
391 [
392 self.pivot[0] + (x - self.pivot[0]) * near,
393 self.pivot[1] + below * self.tilt.cos() * near,
394 ]
395 }
396}
397
398impl Layer<'_> {
399 fn space(&self, size: [u32; 2]) -> Space {
400 Space {
401 size,
402 scale: self.scale,
403 origin: self.origin,
404 backdrop: self.backdrop,
405 round: self.round,
406 }
407 }
408}
409
410/// A layer's transform onto the target.
411#[derive(Clone, Copy)]
412struct Space {
413 size: [u32; 2],
414 scale: f32,
415 origin: [f32; 2],
416 backdrop: Option<[f32; 4]>,
417 round: Option<([f32; 4], f32)>,
418}
419
420impl Space {
421 /// The device point at layer point `point`.
422 fn point(&self, point: [f32; 2]) -> [f32; 2] {
423 [0, 1].map(|axis| point[axis] * self.scale + self.origin[axis])
424 }
425
426 /// The device bounds of layer bounds `rect`.
427 fn rect(&self, rect: [f32; 4]) -> [f32; 4] {
428 [0, 1, 2, 3].map(|edge| rect[edge] * self.scale + self.origin[edge % 2])
429 }
430
431 fn visible_rect(&self, rect: [f32; 4]) -> Result<Option<[f32; 4]>, RenderError> {
432 let rect = self.rect(rect);
433 if rect.iter().any(|v| !v.is_finite()) || rect[2] < rect[0] || rect[3] < rect[1] {
434 return Err(RenderError::InvalidPrimitive);
435 }
436 Ok((rect[0] < self.size[0] as f32
437 && rect[1] < self.size[1] as f32
438 && rect[2] > 0.0
439 && rect[3] > 0.0
440 && rect[0] < rect[2]
441 && rect[1] < rect[3])
442 .then_some(rect))
443 }
444
445 /// Whole device pixels covering `rect` within the target, as a scissor rectangle.
446 fn scissor(&self, rect: [f32; 4]) -> [u32; 4] {
447 let left = rect[0].max(0.0).floor() as u32;
448 let top = rect[1].max(0.0).floor() as u32;
449 let right = (rect[2].min(self.size[0] as f32).ceil() as u32).max(left);
450 let bottom = (rect[3].min(self.size[1] as f32).ceil() as u32).max(top);
451 [left, top, right - left, bottom - top]
452 }
453}
454
455fn intersect(a: [u32; 4], b: [u32; 4]) -> [u32; 4] {
456 let left = a[0].max(b[0]);
457 let top = a[1].max(b[1]);
458 let right = (a[0] + a[2]).min(b[0] + b[2]).max(left);
459 let bottom = (a[1] + a[3]).min(b[1] + b[3]).max(top);
460 [left, top, right - left, bottom - top]
461}
462
463/// How a path paints; widths are layer units.
464#[derive(Clone, Copy, Debug, PartialEq)]
465pub enum PathStyle {
466 Fill,
467 /// Round caps and joins.
468 Stroke(f32),
469 /// Filled and blurred this far, as a soft shadow.
470 Shadow(f32),
471 /// Filled, clearing what lies beneath towards transparency by the colours' opacity, so
472 /// whatever the system shows behind the window shows through.
473 Erase,
474}
475
476#[derive(Clone, Copy, Debug, PartialEq)]
477pub enum Stroke {
478 Solid(f32),
479 /// Dash and gap lengths are twice the stroke width.
480 Dashed(f32),
481}
482
483/// Coordinates are layer units; colors are linear RGBA.
484pub enum Primitive<'a> {
485 Text {
486 text: &'a dyn Glyphs,
487 origin: [f32; 2],
488 /// Layer bounds of the painted glyphs, which keep their full shaping and advances.
489 clip: Option<[f32; 4]>,
490 /// The colour of runs and decorations without their own.
491 ink: [f32; 4],
492 },
493 /// 16×16 SVG artwork painted `size` units square, later sources over earlier ones.
494 /// `currentColor` paints in `tint`'s colour, slots take `palette`'s, other colours keep
495 /// theirs, and `tint`'s opacity fades the whole icon.
496 Icon {
497 sources: &'static [&'static str],
498 origin: [f32; 2],
499 size: f32,
500 tint: [f32; 4],
501 palette: Palette,
502 },
503 /// SVG path data in layer units from `origin`, shaded from `colors[0]` at the top of
504 /// what it paints to `colors[1]` at the bottom. Each distinct path is rasterized once
505 /// per scale and style.
506 Path {
507 data: &'a str,
508 origin: [f32; 2],
509 style: PathStyle,
510 colors: [[f32; 4]; 2],
511 },
512 Rect {
513 rect: [f32; 4],
514 color: [f32; 4],
515 },
516 RoundedRect {
517 rect: [f32; 4],
518 radius: [f32; 2],
519 /// None fills the shape; stroke widths are positive layer units.
520 stroke: Option<Stroke>,
521 color: [f32; 4],
522 },
523 /// A rounded rectangle filled from `colors[0]` at its top to `colors[1]` at its bottom.
524 Gradient {
525 rect: [f32; 4],
526 radius: [f32; 2],
527 colors: [[f32; 4]; 2],
528 },
529 /// The soft shadow a rounded rectangle casts, blurred `blur` units wide as CSS's
530 /// `box-shadow` takes it.
531 Shadow {
532 rect: [f32; 4],
533 radius: f32,
534 blur: f32,
535 color: [f32; 4],
536 },
537 Image {
538 image: &'a RasterImage,
539 rect: [f32; 4],
540 },
541 /// A pen stroke between two points, `width` units across.
542 Segment {
543 from: [f32; 2],
544 to: [f32; 2],
545 width: f32,
546 /// A round pen tip caps the ends; otherwise they are square.
547 round: bool,
548 color: [f32; 4],
549 },
550 /// A pen's stroke between two points, `widths` units across at each, joined by the
551 /// lines touching both ends, as a pressure pen's width changes along a stroke. A square
552 /// pen tip (not `round`) squares the ends in a PDF; the screen rounds them either way.
553 Taper {
554 from: [f32; 2],
555 to: [f32; 2],
556 widths: [f32; 2],
557 round: bool,
558 color: [f32; 4],
559 },
560 /// A highlighter's stroke between two points, `width` units across with square ends:
561 /// its opaque `color` multiplies what lies beneath, so text under it stays dark.
562 Highlight {
563 from: [f32; 2],
564 to: [f32; 2],
565 width: f32,
566 color: [f32; 4],
567 },
568}
569
570#[derive(Debug)]
571pub enum RenderError {
572 AtlasFull,
573 FrameTooLarge,
574 UnreadableFont,
575 UnsupportedGlyph,
576 InvalidLayer,
577 InvalidPrimitive,
578 InvalidImage,
579 ImageBudget,
580 ImageTooLarge,
581 ImageDecode(image::ImageError),
582}
583
584/// Cache and buffer use after the last frame, for resource probes. Texture and buffer
585/// sizes are as requested, without driver overhead.
586#[derive(Debug)]
587pub struct Occupancy {
588 /// Glyph and icon atlas entries, including glyphs with no pixels.
589 pub glyphs: usize,
590 pub atlas_texels: u64,
591 pub images: usize,
592 pub image_bytes: u64,
593 pub vertices: usize,
594 pub batches: usize,
595 pub vertex_capacity_bytes: usize,
596 pub batch_capacity_bytes: usize,
597 pub glyph_capacity: usize,
598 pub image_capacity: usize,
599 pub atlas_bytes: u64,
600 pub vertex_buffer_bytes: u64,
601 pub within_budget: bool,
602}
603
604pub struct Renderer {
605 gpu: backend::Gpu,
606 vertices: Vec<Vertex>,
607 glyphs: HashMap<AtlasKey, Option<AtlasGlyph>>,
608 images: HashMap<u64, CachedImage>,
609 batches: Vec<Batch>,
610 groups: Vec<Group>,
611 /// Batches before this one take no more primitives.
612 barrier: usize,
613 scaler: ScaleContext,
614 pen: [u32; 2],
615 row_height: u32,
616}
617
618#[cfg(any(windows, target_os = "macos", not(feature = "wgpu")))]
619impl Renderer {
620 /// Draws with the OpenGL context current on this thread, which must stay current
621 /// whenever the renderer or a `Target` is used.
622 pub fn opengl() -> Result<Self, String> {
623 gl::Gpu::new().map(Self::with_gpu)
624 }
625
626 /// An offscreen frame `size` pixels large, for Direct3D 11 or OpenGL.
627 pub fn target(&self, size: [u32; 2]) -> Result<Target, String> {
628 self.gpu.target(size)
629 }
630
631 /// Shows `target` in the window, premultiplying each pixel again in sRGB if
632 /// `translucent`, as the desktop compositing the window over its backdrop needs.
633 /// OpenGL's frame waits for its context's buffers to swap.
634 pub fn present(&self, target: &Target, translucent: bool) {
635 self.gpu.present(target, translucent);
636 }
637}
638
639#[cfg(any(
640 windows,
641 target_os = "macos",
642 target_arch = "wasm32",
643 not(feature = "wgpu")
644))]
645impl Renderer {
646 /// The sRGB RGBA rows of `target`, from `Renderer::target` or a 2D canvas, top first.
647 pub fn read_pixels(&self, target: &Target) -> Result<Vec<u8>, String> {
648 self.gpu.read_pixels(target)
649 }
650}
651
652impl Renderer {
653 fn with_gpu(gpu: impl Into<backend::Gpu>) -> Self {
654 let mut renderer = Self {
655 gpu: gpu.into(),
656 vertices: Vec::new(),
657 glyphs: HashMap::new(),
658 images: HashMap::new(),
659 batches: Vec::new(),
660 groups: Vec::new(),
661 barrier: 0,
662 scaler: ScaleContext::with_max_entries(32),
663 pen: [0; 2],
664 row_height: 0,
665 };
666 renderer.clear_glyph_cache();
667 renderer
668 }
669
670 /// The widest and tallest texture the device takes, in pixels.
671 pub fn max_texture_dimension(&self) -> u32 {
672 self.gpu.max_texture_dimension()
673 }
674
675 fn glyph_limit(&self) -> usize {
676 MAX_GLYPHS * (self.gpu.atlas_side() / ATLAS_SIZE).pow(2) as usize
677 }
678
679 fn uv(&self, glyph: AtlasGlyph) -> [f32; 4] {
680 let side = self.gpu.atlas_side() as f32;
681 [
682 glyph.x as f32 / side,
683 glyph.y as f32 / side,
684 (glyph.x + glyph.width) as f32 / side,
685 (glyph.y + glyph.height) as f32 / side,
686 ]
687 }
688
689 /// The atlas's white texel.
690 fn white(&self) -> [f32; 4] {
691 [0.5 / self.gpu.atlas_side() as f32; 4]
692 }
693
694 /// Empties the atlas but for its first texel, the white that solid fills sample.
695 pub fn clear_glyph_cache(&mut self) {
696 self.glyphs.clear();
697 self.gpu.write_atlas([0, 0], [1, 1], &[255; 4]);
698 self.pen = [1, 0];
699 self.row_height = 1;
700 }
701
702 /// Forgets uploaded images; they upload again when next painted.
703 pub fn clear_images(&mut self) {
704 self.images.clear();
705 }
706
707 pub fn occupancy(&self) -> Occupancy {
708 let image_bytes = self.images.values().map(|image| image.bytes).sum();
709 Occupancy {
710 glyphs: self.glyphs.len(),
711 atlas_texels: self
712 .glyphs
713 .values()
714 .flatten()
715 .map(|glyph| u64::from(glyph.width) * u64::from(glyph.height))
716 .sum(),
717 images: self.images.len(),
718 image_bytes,
719 vertices: self.vertices.len(),
720 batches: self.batches.len()
721 + self
722 .groups
723 .iter()
724 .map(|group| group.batches.len())
725 .sum::<usize>(),
726 vertex_capacity_bytes: self.vertices.capacity() * size_of::<Vertex>(),
727 batch_capacity_bytes: self.batches.capacity() * size_of::<Batch>(),
728 glyph_capacity: self.glyphs.capacity(),
729 image_capacity: self.images.capacity(),
730 atlas_bytes: u64::from(self.gpu.atlas_side()).pow(2) * 4,
731 vertex_buffer_bytes: VERTEX_BUFFER_BYTES,
732 within_budget: self.glyphs.len() <= self.glyph_limit()
733 && self.images.len() <= MAX_IMAGES
734 && image_bytes <= MAX_IMAGE_BYTES
735 && self.vertices.len() <= MAX_VERTICES,
736 }
737 }
738
739 /// Clears `target`, `size` device pixels, to linear `clear` and paints the layers in order.
740 pub fn draw(
741 &mut self,
742 target: &Target,
743 size: [u32; 2],
744 clear: [f32; 4],
745 layers: &[Layer<'_>],
746 ) -> Result<(), RenderError> {
747 if size.contains(&0)
748 || clear.iter().any(|v| !v.is_finite())
749 || layers.iter().any(|layer| {
750 !layer.scale.is_finite()
751 || layer.scale <= 0.0
752 || layer.origin.iter().any(|v| !v.is_finite())
753 || layer
754 .clip
755 .is_some_and(|clip| clip.iter().any(|v| !v.is_finite()))
756 })
757 {
758 return Err(RenderError::InvalidLayer);
759 }
760 self.images
761 .retain(|_, image| image.pixels.upgrade().is_some());
762 let mut active_images = HashSet::new();
763 let mut image_bytes = 0;
764 for layer in layers {
765 for primitive in layer.primitives {
766 if let Primitive::Image { image, rect } = primitive
767 && layer.space(size).visible_rect(*rect)?.is_some()
768 && active_images.insert(image.id())
769 {
770 image_bytes += image.pixels().len() as u64;
771 if image_bytes > MAX_IMAGE_BYTES || active_images.len() > MAX_IMAGES {
772 return Err(RenderError::ImageBudget);
773 }
774 }
775 }
776 }
777 let limit = self.max_texture_dimension().min(MAX_ATLAS_SIZE);
778 let mut cleared = false;
779 loop {
780 self.vertices.clear();
781 self.batches.clear();
782 self.groups.clear();
783 self.barrier = 0;
784 let full = match self.prepare(size, layers, &active_images) {
785 Err(RenderError::AtlasFull) => true,
786 result => {
787 result?;
788 false
789 }
790 };
791 // Once cleared, the atlas holds only this frame's entries; past half full, the
792 // next frames would clear it again and again.
793 let crowded = full || (cleared && 2 * self.pen[1] > self.gpu.atlas_side());
794 if !crowded {
795 break;
796 }
797 if !cleared {
798 self.clear_glyph_cache();
799 cleared = true;
800 } else if self.gpu.atlas_side() * 2 <= limit {
801 self.gpu.new_atlas(self.gpu.atlas_side() * 2);
802 self.clear_glyph_cache();
803 } else if full {
804 return Err(RenderError::AtlasFull);
805 } else {
806 break;
807 }
808 }
809 let frame = Frame {
810 vertices: &self.vertices,
811 groups: &self.groups,
812 batches: &self.batches,
813 images: &self.images,
814 };
815 self.gpu.submit(&frame, target, size, clear);
816 Ok(())
817 }
818
819 fn prepare(
820 &mut self,
821 size: [u32; 2],
822 layers: &[Layer<'_>],
823 active_images: &HashSet<u64>,
824 ) -> Result<(), RenderError> {
825 // The motion the layers painted since `start` share, where they paint as a group.
826 let mut group: Option<(Motion, usize)> = None;
827 for layer in layers {
828 let motion = layer
829 .motion
830 .filter(|motion| motion.opacity < 1.0 || motion.tilt != 0.0);
831 if motion != group.map(|(motion, _)| motion) {
832 if let Some((motion, start)) = group {
833 self.group(motion, start, size)?;
834 }
835 // A group's batches never merge with those outside it.
836 self.barrier = self.batches.len();
837 group = motion.map(|motion| (motion, self.batches.len()));
838 }
839 let space = layer.space(size);
840 let bounds = match layer.clip {
841 Some(clip) => space.scissor(clip),
842 None => [0, 0, size[0], size[1]],
843 };
844 if bounds[2] == 0 || bounds[3] == 0 {
845 continue;
846 }
847 for primitive in layer.primitives {
848 self.primitive(space, bounds, primitive, active_images)?;
849 }
850 }
851 if let Some((motion, start)) = group {
852 self.group(motion, start, size)?;
853 }
854 Ok(())
855 }
856
857 /// Makes the batches from `start` a group appearing by `motion`, leaving in their place
858 /// the batch that paints its offscreen picture over the target, in strips so the turn
859 /// stays in perspective across the picture.
860 fn group(&mut self, motion: Motion, start: usize, size: [u32; 2]) -> Result<(), RenderError> {
861 if start == self.batches.len() {
862 return Ok(());
863 }
864 let [width, height] = size.map(|side| side as f32);
865 let [mut left, mut top, mut right, mut bottom] = [width, height, 0.0, 0.0];
866 let first = self.batches[start].vertices.start as usize;
867 for vertex in &self.vertices[first..] {
868 let [x, y] = [
869 (vertex.position[0] + 1.0) * width / 2.0,
870 (1.0 - vertex.position[1]) * height / 2.0,
871 ];
872 [left, top] = [left.min(x), top.min(y)];
873 [right, bottom] = [right.max(x), bottom.max(y)];
874 }
875 let [left, top] = [left.max(0.0).floor(), top.max(0.0).floor()];
876 let [right, bottom] = [right.min(width).ceil(), bottom.min(height).ceil()];
877 let strips = if motion.tilt == 0.0 { 1 } else { 32 };
878 if self.vertices.len() + 6 * strips > MAX_VERTICES {
879 return Err(RenderError::FrameTooLarge);
880 }
881 let from = self.vertices.len() as u32;
882 let flipped = self.gpu.flipped();
883 let corner = |x: f32, y: f32| {
884 let [shown_x, shown_y] = motion.project([x, y], height);
885 let v = y / height;
886 Vertex {
887 position: [shown_x * 2.0 / width - 1.0, 1.0 - shown_y * 2.0 / height],
888 uv: [x / width, if flipped { 1.0 - v } else { v }],
889 color: [motion.opacity; 4],
890 ..Vertex::zeroed()
891 }
892 };
893 for strip in 0..strips {
894 let [y0, y1] =
895 [strip, strip + 1].map(|edge| top + (bottom - top) * edge as f32 / strips as f32);
896 let [a, b, c, d] = [
897 corner(left, y0),
898 corner(left, y1),
899 corner(right, y1),
900 corner(right, y0),
901 ];
902 self.vertices.extend([a, b, c, a, c, d]);
903 }
904 let batches = self.batches.split_off(start);
905 self.batches.push(Batch {
906 vertices: from..self.vertices.len() as u32,
907 blend: Blend::Group(self.groups.len()),
908 scissor: [0, 0, size[0], size[1]],
909 });
910 self.groups.push(Group { batches, motion });
911 Ok(())
912 }
913
914 fn primitive(
915 &mut self,
916 space: Space,
917 bounds: [u32; 4],
918 primitive: &Primitive<'_>,
919 active_images: &HashSet<u64>,
920 ) -> Result<(), RenderError> {
921 let start = self.vertices.len() as u32;
922 let mut blend = Blend::Over;
923 let mut scissor = bounds;
924 match primitive {
925 Primitive::Icon {
926 sources,
927 origin,
928 size,
929 tint,
930 palette,
931 } => {
932 let origin = space.point(*origin);
933 self.icon(Space { origin, ..space }, sources, *size, *tint, palette)?;
934 }
935 Primitive::Path {
936 data,
937 origin,
938 style,
939 colors,
940 } => {
941 if *style == PathStyle::Erase {
942 blend = Blend::Erase;
943 }
944 self.path(space, data, *origin, *style, *colors)?;
945 }
946 Primitive::Text {
947 text,
948 origin,
949 clip,
950 ink,
951 } => {
952 let origin = space.point(*origin);
953 if origin.iter().any(|v| !v.is_finite()) {
954 return Err(RenderError::InvalidPrimitive);
955 }
956 if let Some(clip) = clip {
957 let Some(rect) = space.visible_rect(*clip)? else {
958 return Ok(());
959 };
960 scissor = intersect(bounds, space.scissor(rect));
961 if scissor[2] == 0 || scissor[3] == 0 {
962 return Ok(());
963 }
964 }
965 let space = Space { origin, ..space };
966 text.runs(&mut |run| self.glyph_run(space, run, *ink))?;
967 }
968 Primitive::Rect { rect, color } => {
969 self.quad(space, space.rect(*rect), self.white(), *color)?;
970 }
971 Primitive::RoundedRect {
972 rect,
973 radius,
974 stroke,
975 color,
976 } => self.rounded_rect(space, *rect, *radius, *stroke, [*color; 2])?,
977 Primitive::Gradient {
978 rect,
979 radius,
980 colors,
981 } => self.rounded_rect(space, *rect, *radius, None, *colors)?,
982 Primitive::Shadow {
983 rect,
984 radius,
985 blur,
986 color,
987 } => self.shadow(space, *rect, *radius, *blur, *color)?,
988 Primitive::Segment {
989 from,
990 to,
991 width,
992 round,
993 color,
994 } => self.segment(space, *from, *to, [*width; 2], *round, *color)?,
995 Primitive::Taper {
996 from,
997 to,
998 widths,
999 color,
1000 ..
1001 } => self.segment(space, *from, *to, *widths, true, *color)?,
1002 Primitive::Highlight {
1003 from,
1004 to,
1005 width,
1006 color,
1007 } => {
1008 blend = Blend::Multiply;
1009 self.segment(space, *from, *to, [*width; 2], false, *color)?;
1010 }
1011 Primitive::Image { image, rect } => {
1012 if let Some(rect) = space.visible_rect(*rect)? {
1013 self.image(image, active_images)?;
1014 blend = Blend::Image(image.id());
1015 self.quad(space, rect, [0.0, 0.0, 1.0, 1.0], [1.0; 4])?;
1016 }
1017 }
1018 }
1019 let end = self.vertices.len() as u32;
1020 if let Some((rect, radius)) = space.round {
1021 let [width, height] = space.size.map(|side| side as f32);
1022 let half = [(rect[2] - rect[0]) / 2.0, (rect[3] - rect[1]) / 2.0];
1023 let middle = [rect[0] + half[0], rect[1] + half[1]];
1024 for vertex in &mut self.vertices[start as usize..] {
1025 vertex.clip_local = [
1026 (vertex.position[0] + 1.0) * width / 2.0 - middle[0],
1027 (1.0 - vertex.position[1]) * height / 2.0 - middle[1],
1028 ];
1029 vertex.clip = [half[0], half[1], radius.min(half[0]).min(half[1])];
1030 }
1031 }
1032 if start != end {
1033 if self.batches.len() > self.barrier
1034 && let Some(last) = self.batches.last_mut()
1035 && last.blend == blend
1036 && last.scissor == scissor
1037 {
1038 last.vertices.end = end;
1039 } else {
1040 self.batches.push(Batch {
1041 vertices: start..end,
1042 blend,
1043 scissor,
1044 });
1045 }
1046 }
1047 Ok(())
1048 }
1049
1050 fn image(&mut self, image: &RasterImage, active: &HashSet<u64>) -> Result<(), RenderError> {
1051 if self.images.contains_key(&image.id()) {
1052 return Ok(());
1053 }
1054 if image.size.iter().any(|v| *v > self.max_texture_dimension()) {
1055 return Err(RenderError::ImageTooLarge);
1056 }
1057 let bytes = image.pixels().len() as u64;
1058 while self.images.values().map(|i| i.bytes).sum::<u64>() + bytes > MAX_IMAGE_BYTES
1059 || self.images.len() >= MAX_IMAGES
1060 {
1061 let id = self
1062 .images
1063 .keys()
1064 .find(|id| !active.contains(id))
1065 .copied()
1066 .ok_or(RenderError::ImageBudget)?;
1067 self.images.remove(&id);
1068 }
1069 self.images.insert(
1070 image.id(),
1071 CachedImage {
1072 texture: self.gpu.upload_image(image),
1073 bytes,
1074 pixels: image.pixels.downgrade(),
1075 },
1076 );
1077 Ok(())
1078 }
1079
1080 fn glyph_run(
1081 &mut self,
1082 space: Space,
1083 run: GlyphRun<'_>,
1084 ink: [f32; 4],
1085 ) -> Result<(), RenderError> {
1086 let scale = space.scale;
1087 let origin = space.origin;
1088 let line_top = run.line[0] * scale + origin[1];
1089 let line_height = run.line[1] * scale;
1090 if line_top + line_height < 0.0 || line_top > space.size[1] as f32 {
1091 return Ok(());
1092 }
1093 let size = run.size * scale;
1094 if !size.is_finite() || size > self.gpu.atlas_side() as f32 {
1095 return Err(RenderError::AtlasFull);
1096 }
1097 let color = run
1098 .color
1099 .map_or(ink, |color| legible(color, space.backdrop, run.backdrop));
1100 let [red, green, blue, _] = color;
1101 let luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
1102 let tone = (f32::from(srgb_byte(luminance)) / 255.0 * 16.0).round() as u8;
1103 for glyph in run.glyphs {
1104 let x = glyph.x * scale + origin[0];
1105 let y = glyph.y * scale + origin[1];
1106 // Quantization affects raster coverage only, never advances or line breaks.
1107 let x = (x * 4.0).round() * 0.25;
1108 let y = (y * 4.0).round() * 0.25;
1109 let glyph_id = glyph
1110 .id
1111 .try_into()
1112 .map_err(|_| RenderError::UnsupportedGlyph)?;
1113 let phase = [((x - x.floor()) * 4.0) as u8, ((y - y.floor()) * 4.0) as u8];
1114 let key = AtlasKey::Text {
1115 font: run.font.id(),
1116 index: run.index,
1117 glyph: glyph_id,
1118 size: size.to_bits(),
1119 coords: run.coords.to_vec(),
1120 phase,
1121 embolden: run.embolden,
1122 skew: run.skew.unwrap_or(0.0).to_bits(),
1123 tone,
1124 };
1125 let cached = if let Some(cached) = self.glyphs.get(&key) {
1126 *cached
1127 } else {
1128 if self.glyphs.len() >= self.glyph_limit() {
1129 return Err(RenderError::AtlasFull);
1130 }
1131 let font = FontRef::from_index(run.font.as_ref(), run.index as usize)
1132 .ok_or(RenderError::UnreadableFont)?;
1133 let mut scaler = self
1134 .scaler
1135 .builder_with_id(font, [run.font.id(), u64::from(run.index)])
1136 .size(size)
1137 .hint(false)
1138 .normalized_coords(run.coords.iter().copied())
1139 .build();
1140 let mut render = Render::new(&[
1141 Source::ColorOutline(0),
1142 Source::ColorBitmap(StrikeWith::BestFit),
1143 Source::Outline,
1144 ]);
1145 // Swash rasterizes outlines in Y-up coordinates.
1146 let offset = [f32::from(phase[0]) * 0.25, -f32::from(phase[1]) * 0.25];
1147 render
1148 .format(Format::Alpha)
1149 .offset(Vector::new(offset[0], offset[1]));
1150 if run.embolden {
1151 render.embolden(size / 24.0);
1152 }
1153 if let Some(skew) = run.skew {
1154 render.transform(Some(Transform::skew(
1155 Angle::from_degrees(skew),
1156 Angle::from_degrees(0.0),
1157 )));
1158 }
1159 let image = colr::paint(
1160 run.font.as_ref(),
1161 run.index,
1162 glyph_id,
1163 size,
1164 run.coords,
1165 offset,
1166 )
1167 .or_else(|| render.render(&mut scaler, glyph_id))
1168 .map(|mut image| {
1169 if image.content == Content::Mask && self.gpu.blends_linear() {
1170 let coverage = text_coverage(tone);
1171 for alpha in &mut image.data {
1172 *alpha = coverage[usize::from(*alpha)];
1173 }
1174 }
1175 image
1176 });
1177 let cached = if let Some(image) =
1178 image.filter(|i| i.placement.width > 0 && i.placement.height > 0)
1179 {
1180 Some(self.upload(image)?)
1181 } else {
1182 None
1183 };
1184 self.glyphs.insert(key, cached);
1185 cached
1186 };
1187 if let Some(glyph) = cached {
1188 let mut left = x.floor() + glyph.left as f32;
1189 let mut top = y.floor() - glyph.top as f32;
1190 let mut width = glyph.width as f32;
1191 let mut height = glyph.height as f32;
1192 if glyph.color {
1193 let ratio = (line_height / height).min(1.0);
1194 left += width * (1.0 - ratio) * 0.5;
1195 width *= ratio;
1196 height *= ratio;
1197 top = top.clamp(line_top, (line_top + line_height - height).max(line_top));
1198 }
1199 self.quad(
1200 space,
1201 [left, top, left + width, top + height],
1202 self.uv(glyph),
1203 if glyph.color { [1.0; 4] } else { color },
1204 )?;
1205 }
1206 }
1207 for decoration in run.decorations {
1208 let x = decoration.x * scale + origin[0];
1209 let y = decoration.y * scale + origin[1];
1210 self.quad(
1211 space,
1212 [
1213 x,
1214 y,
1215 x + decoration.width * scale,
1216 y + (decoration.thickness * scale).max(1.0),
1217 ],
1218 self.white(),
1219 decoration
1220 .color
1221 .map_or(ink, |color| legible(color, space.backdrop, run.backdrop)),
1222 )?;
1223 }
1224 Ok(())
1225 }
1226
1227 fn upload(&mut self, image: swash::scale::image::Image) -> Result<AtlasGlyph, RenderError> {
1228 let p = image.placement;
1229 let side = self.gpu.atlas_side();
1230 if p.width + 2 > side || p.height + 2 > side {
1231 return Err(RenderError::AtlasFull);
1232 }
1233 if self.pen[0] + p.width + 1 > side {
1234 self.pen[0] = 0;
1235 self.pen[1] += self.row_height + 1;
1236 self.row_height = 0;
1237 }
1238 if self.pen[1] + p.height + 1 > side {
1239 return Err(RenderError::AtlasFull);
1240 }
1241 let cached = AtlasGlyph {
1242 x: self.pen[0],
1243 y: self.pen[1],
1244 width: p.width,
1245 height: p.height,
1246 left: p.left,
1247 top: p.top,
1248 color: image.content == Content::Color,
1249 };
1250 let rgba = match image.content {
1251 Content::Mask => image
1252 .data
1253 .iter()
1254 .flat_map(|a| [255, 255, 255, *a])
1255 .collect(),
1256 _ => image.data,
1257 };
1258 self.gpu
1259 .write_atlas([cached.x, cached.y], [p.width, p.height], &rgba);
1260 self.pen[0] += p.width + 1;
1261 self.row_height = self.row_height.max(p.height);
1262 Ok(cached)
1263 }
1264
1265 fn icon(
1266 &mut self,
1267 space: Space,
1268 sources: &'static [&'static str],
1269 size: f32,
1270 tint: [f32; 4],
1271 palette: &Palette,
1272 ) -> Result<(), RenderError> {
1273 // Whole device pixels keep a sprite's pixel-art edges crisp at every scale.
1274 let size = (size * space.scale).round().max(1.0);
1275 let [x, y] = space.origin.map(f32::round);
1276 if !size.is_finite() || [x, y].iter().any(|v| !v.is_finite()) {
1277 return Err(RenderError::InvalidPrimitive);
1278 }
1279 if x + size < 0.0 || y + size < 0.0 || x > space.size[0] as f32 || y > space.size[1] as f32
1280 {
1281 return Ok(());
1282 }
1283 if size + 2.0 > self.gpu.atlas_side() as f32 {
1284 return Err(RenderError::AtlasFull);
1285 }
1286 let size = size as u32;
1287 let ink = [tint[0], tint[1], tint[2]];
1288 let key = AtlasKey::Icon {
1289 sources,
1290 size,
1291 ink: ink.map(f32::to_bits),
1292 palette: palette.bits(),
1293 };
1294 let glyph = if let Some(Some(glyph)) = self.glyphs.get(&key) {
1295 *glyph
1296 } else {
1297 if self.glyphs.len() >= self.glyph_limit() {
1298 return Err(RenderError::AtlasFull);
1299 }
1300 let glyph = self.upload(icon::rasterize(sources, size, ink, palette))?;
1301 self.glyphs.insert(key, Some(glyph));
1302 glyph
1303 };
1304 self.quad(
1305 space,
1306 [x, y, x + glyph.width as f32, y + glyph.height as f32],
1307 self.uv(glyph),
1308 [1.0, 1.0, 1.0, tint[3]],
1309 )
1310 }
1311
1312 /// `colors` are the top and bottom edges' colours.
1313 fn rounded_rect(
1314 &mut self,
1315 space: Space,
1316 rect: [f32; 4],
1317 radius: [f32; 2],
1318 stroke: Option<Stroke>,
1319 colors: [[f32; 4]; 2],
1320 ) -> Result<(), RenderError> {
1321 if radius
1322 .iter()
1323 .any(|value| !value.is_finite() || *value < 0.0)
1324 {
1325 return Err(RenderError::InvalidPrimitive);
1326 }
1327 let width = match stroke {
1328 None => 0.0,
1329 Some(Stroke::Solid(width) | Stroke::Dashed(width)) => {
1330 if !width.is_finite() || width <= 0.0 || width * space.scale == 0.0 {
1331 return Err(RenderError::InvalidPrimitive);
1332 }
1333 width
1334 }
1335 };
1336 let rect = space.rect(rect);
1337 let start = self.vertices.len();
1338 // A pixel of margin holds the antialiased fringe outside the edge.
1339 let fringe = [rect[0] - 1.0, rect[1] - 1.0, rect[2] + 1.0, rect[3] + 1.0];
1340 self.quad(space, fringe, self.white(), colors[0])?;
1341 let half = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5];
1342 if !(4.0 * (half[0] + half[1])).is_finite() || !colors[1].iter().all(|v| v.is_finite()) {
1343 return Err(RenderError::InvalidPrimitive);
1344 }
1345 let radius = if radius.contains(&0.0) {
1346 [0.0; 2]
1347 } else {
1348 std::array::from_fn(|axis| (radius[axis] * space.scale).min(half[axis]))
1349 };
1350 let width = (width * space.scale).min(half[0].min(half[1]));
1351 // The stroke sign selects its dash pattern at the shader boundary.
1352 let width = if matches!(stroke, Some(Stroke::Dashed(_))) {
1353 -width
1354 } else {
1355 width
1356 };
1357 for vertex in &mut self.vertices[start..] {
1358 vertex.shape = [half[0], half[1], radius[0], radius[1]];
1359 vertex.stroke = width;
1360 if vertex.local[1] > 0.0 {
1361 vertex.color = colors[1];
1362 }
1363 }
1364 Ok(())
1365 }
1366
1367 fn shadow(
1368 &mut self,
1369 space: Space,
1370 rect: [f32; 4],
1371 radius: f32,
1372 blur: f32,
1373 color: [f32; 4],
1374 ) -> Result<(), RenderError> {
1375 if !radius.is_finite() || radius < 0.0 || !blur.is_finite() || blur <= 0.0 {
1376 return Err(RenderError::InvalidPrimitive);
1377 }
1378 let rect = space.rect(rect);
1379 let sigma = blur * space.scale / 2.0;
1380 let reach = 3.0 * sigma;
1381 let start = self.vertices.len();
1382 self.quad(
1383 space,
1384 [
1385 rect[0] - reach,
1386 rect[1] - reach,
1387 rect[2] + reach,
1388 rect[3] + reach,
1389 ],
1390 self.white(),
1391 color,
1392 )?;
1393 let half = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5];
1394 let radius = (radius * space.scale).min(half[0]).min(half[1]).max(0.0);
1395 for vertex in &mut self.vertices[start..] {
1396 vertex.shape = [half[0], half[1], radius, radius];
1397 vertex.blur = sigma;
1398 }
1399 Ok(())
1400 }
1401
1402 fn path(
1403 &mut self,
1404 space: Space,
1405 data: &str,
1406 origin: [f32; 2],
1407 style: PathStyle,
1408 colors: [[f32; 4]; 2],
1409 ) -> Result<(), RenderError> {
1410 let [x, y] = space
1411 .point(origin)
1412 .map(|value| (value * 4.0).round() * 0.25);
1413 let (kind, width) = match style {
1414 PathStyle::Fill | PathStyle::Erase => (0, 0.0),
1415 PathStyle::Stroke(width) => (1, width),
1416 PathStyle::Shadow(width) => (2, width),
1417 };
1418 if [x, y]
1419 .iter()
1420 .chain(colors.as_flattened())
1421 .any(|v| !v.is_finite())
1422 || !width.is_finite()
1423 || width < 0.0
1424 || (width == 0.0 && kind != 0)
1425 {
1426 return Err(RenderError::InvalidPrimitive);
1427 }
1428 let phase = [((x - x.floor()) * 4.0) as u8, ((y - y.floor()) * 4.0) as u8];
1429 let key = AtlasKey::Path {
1430 data: data.to_owned(),
1431 scale: space.scale.to_bits(),
1432 style: (kind, width.to_bits()),
1433 phase,
1434 };
1435 let glyph = match self.glyphs.get(&key) {
1436 Some(glyph) => *glyph,
1437 None => {
1438 if self.glyphs.len() >= self.glyph_limit() {
1439 return Err(RenderError::AtlasFull);
1440 }
1441 let mut mask = Mask::new(data);
1442 mask.transform(Some(Transform::scale(space.scale, space.scale)))
1443 .offset(phase.map(|quarter| f32::from(quarter) * 0.25));
1444 if let PathStyle::Stroke(width) = style {
1445 mask.style(swash::zeno::Stroke {
1446 start_cap: Cap::Round,
1447 end_cap: Cap::Round,
1448 join: Join::Round,
1449 ..swash::zeno::Stroke::new(width)
1450 });
1451 }
1452 let (mut pixels, mut placement) = mask.render();
1453 if let PathStyle::Shadow(blur) = style {
1454 (pixels, placement) = soften(&pixels, placement, blur * space.scale);
1455 }
1456 let glyph = (placement.width > 0 && placement.height > 0)
1457 .then(|| {
1458 self.upload(swash::scale::image::Image {
1459 content: Content::Mask,
1460 placement,
1461 data: pixels,
1462 ..Default::default()
1463 })
1464 })
1465 .transpose()?;
1466 self.glyphs.insert(key, glyph);
1467 glyph
1468 }
1469 };
1470 let Some(glyph) = glyph else {
1471 return Ok(());
1472 };
1473 let left = x.floor() + glyph.left as f32;
1474 let top = y.floor() + glyph.top as f32;
1475 let start = self.vertices.len();
1476 self.quad(
1477 space,
1478 [
1479 left,
1480 top,
1481 left + glyph.width as f32,
1482 top + glyph.height as f32,
1483 ],
1484 self.uv(glyph),
1485 colors[0],
1486 )?;
1487 for vertex in &mut self.vertices[start..] {
1488 if vertex.local[1] > 0.0 {
1489 vertex.color = colors[1];
1490 }
1491 }
1492 Ok(())
1493 }
1494
1495 /// Draws the segment as a capsule in its own frame, reusing the rounded-rectangle distance;
1496 /// ends of two widths make it a tapered capsule, which the shader marks by a negative blur.
1497 fn segment(
1498 &mut self,
1499 space: Space,
1500 from: [f32; 2],
1501 to: [f32; 2],
1502 widths: [f32; 2],
1503 round: bool,
1504 color: [f32; 4],
1505 ) -> Result<(), RenderError> {
1506 let [from, to] = [space.point(from), space.point(to)];
1507 // Hairlines stay one device pixel wide.
1508 let radii = widths.map(|width| (width * space.scale).max(1.0) * 0.5);
1509 let radius = radii[0].max(radii[1]);
1510 if from
1511 .iter()
1512 .chain(&to)
1513 .chain(&color)
1514 .chain(&radii)
1515 .any(|v| !v.is_finite())
1516 {
1517 return Err(RenderError::InvalidPrimitive);
1518 }
1519 let pad = radius + 1.0;
1520 if from[0].max(to[0]) + pad < 0.0
1521 || from[1].max(to[1]) + pad < 0.0
1522 || from[0].min(to[0]) - pad > space.size[0] as f32
1523 || from[1].min(to[1]) - pad > space.size[1] as f32
1524 {
1525 return Ok(());
1526 }
1527 if self.vertices.len() + 6 > MAX_VERTICES {
1528 return Err(RenderError::FrameTooLarge);
1529 }
1530 let delta = [to[0] - from[0], to[1] - from[1]];
1531 let length = delta[0].hypot(delta[1]);
1532 let along = if length > 0.0 {
1533 [delta[0] / length, delta[1] / length]
1534 } else {
1535 [1.0, 0.0]
1536 };
1537 let across = [-along[1], along[0]];
1538 let center = [(from[0] + to[0]) * 0.5, (from[1] + to[1]) * 0.5];
1539 let half = [length * 0.5 + radius, radius];
1540 let corner = if round { radius } else { 0.0 };
1541 let tapered = radii[0] != radii[1];
1542 let (shape, blur) = if tapered {
1543 ([length * 0.5, radii[0], radii[1], 0.0], -1.0)
1544 } else {
1545 ([half[0], half[1], corner, corner], 0.0)
1546 };
1547 let [hx, hy] = [half[0] + 1.0, half[1] + 1.0];
1548 for local in [
1549 [-hx, -hy],
1550 [-hx, hy],
1551 [hx, hy],
1552 [-hx, -hy],
1553 [hx, hy],
1554 [hx, -hy],
1555 ] {
1556 let x = center[0] + along[0] * local[0] + across[0] * local[1];
1557 let y = center[1] + along[1] * local[0] + across[1] * local[1];
1558 self.vertices.push(Vertex {
1559 position: [
1560 x * 2.0 / space.size[0] as f32 - 1.0,
1561 1.0 - y * 2.0 / space.size[1] as f32,
1562 ],
1563 uv: [self.white()[0]; 2],
1564 color,
1565 local,
1566 shape,
1567 blur,
1568 ..Vertex::zeroed()
1569 });
1570 }
1571 Ok(())
1572 }
1573
1574 fn quad(
1575 &mut self,
1576 space: Space,
1577 rect: [f32; 4],
1578 uv: [f32; 4],
1579 color: [f32; 4],
1580 ) -> Result<(), RenderError> {
1581 if rect.iter().chain(&color).any(|v| !v.is_finite())
1582 || rect[2] < rect[0]
1583 || rect[3] < rect[1]
1584 {
1585 return Err(RenderError::InvalidPrimitive);
1586 }
1587 if rect[2] < 0.0
1588 || rect[3] < 0.0
1589 || rect[0] > space.size[0] as f32
1590 || rect[1] > space.size[1] as f32
1591 {
1592 return Ok(());
1593 }
1594 if self.vertices.len() + 6 > MAX_VERTICES {
1595 return Err(RenderError::FrameTooLarge);
1596 }
1597 let x0 = rect[0] * 2.0 / space.size[0] as f32 - 1.0;
1598 let x1 = rect[2] * 2.0 / space.size[0] as f32 - 1.0;
1599 let y0 = 1.0 - rect[1] * 2.0 / space.size[1] as f32;
1600 let y1 = 1.0 - rect[3] * 2.0 / space.size[1] as f32;
1601 if [x0, x1, y0, y1].iter().any(|v| !v.is_finite()) {
1602 return Err(RenderError::InvalidPrimitive);
1603 }
1604 let [hx, hy] = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5];
1605 for (position, uv, local) in [
1606 ([x0, y0], [uv[0], uv[1]], [-hx, -hy]),
1607 ([x0, y1], [uv[0], uv[3]], [-hx, hy]),
1608 ([x1, y1], [uv[2], uv[3]], [hx, hy]),
1609 ([x0, y0], [uv[0], uv[1]], [-hx, -hy]),
1610 ([x1, y1], [uv[2], uv[3]], [hx, hy]),
1611 ([x1, y0], [uv[2], uv[1]], [hx, -hy]),
1612 ] {
1613 self.vertices.push(Vertex {
1614 position,
1615 uv,
1616 color,
1617 local,
1618 ..Vertex::zeroed()
1619 });
1620 }
1621 Ok(())
1622 }
1623}
1624
1625/// Blurs a coverage mask about `radius` device pixels with three box blurs on each axis,
1626/// growing it to hold the spread.
1627fn soften(
1628 pixels: &[u8],
1629 placement: swash::zeno::Placement,
1630 radius: f32,
1631) -> (Vec<u8>, swash::zeno::Placement) {
1632 let reach = (radius / 2.0).ceil().max(1.0) as usize;
1633 let pad = 3 * reach;
1634 let [width, height] = [placement.width as usize, placement.height as usize];
1635 let [wide, tall] = [width + 2 * pad, height + 2 * pad];
1636 let mut values = vec![0.0_f32; wide * tall];
1637 for row in 0..height {
1638 for column in 0..width {
1639 values[(row + pad) * wide + column + pad] = f32::from(pixels[row * width + column]);
1640 }
1641 }
1642 let window = (2 * reach + 1) as f32;
1643 let mut line = Vec::new();
1644 for _ in 0..3 {
1645 // Rows, then columns.
1646 for (count, step, lines, stride) in [(wide, 1, tall, wide), (tall, wide, wide, 1)] {
1647 for index in 0..lines {
1648 let at = |position: usize| index * stride + position * step;
1649 line.clear();
1650 line.extend((0..count).map(|position| values[at(position)]));
1651 let mut sum: f32 = line[..reach.min(count)].iter().sum();
1652 for position in 0..count {
1653 if position + reach < count {
1654 sum += line[position + reach];
1655 }
1656 values[at(position)] = sum / window;
1657 if position >= reach {
1658 sum -= line[position - reach];
1659 }
1660 }
1661 }
1662 }
1663 }
1664 (
1665 values
1666 .into_iter()
1667 .map(|value| value.round().clamp(0.0, 255.0) as u8)
1668 .collect(),
1669 swash::zeno::Placement {
1670 left: placement.left - pad as i32,
1671 top: placement.top - pad as i32,
1672 width: wide as u32,
1673 height: tall as u32,
1674 },
1675 )
1676}
1677
1678fn srgb_byte(value: f32) -> u8 {
1679 (crate::encode(value) * 255.0).round() as u8
1680}
1681
1682/// Coverage for text of sRGB luminance `tone` sixteenths that, blended in linear light,
1683/// darkens a white backdrop as the rasterized coverage would blended sRGB-encoded, as
1684/// systems blend text: dark text keeps its weight instead of thinning, light text is
1685/// nearly untouched.
1686fn text_coverage(tone: u8) -> [u8; 256] {
1687 let encoded = f32::from(tone) / 16.0;
1688 let ink = crate::linear(encoded);
1689 std::array::from_fn(|coverage| {
1690 let coverage = coverage as f32 / 255.0;
1691 let weighted = if ink < 0.999 {
1692 (1.0 - crate::linear(1.0 - coverage * (1.0 - encoded))) / (1.0 - ink)
1693 } else {
1694 coverage
1695 };
1696 (weighted * 255.0).round() as u8
1697 })
1698}
1699
1700/// Least OKLab lightness difference between text and the backdrop it stays legible on.
1701const LEGIBLE: f32 = 0.4;
1702
1703/// On a dark layer `backdrop`, `color` lifted clear of the lightness of what lies `behind`
1704/// it, or of the backdrop, keeping hue and chroma.
1705fn legible(color: [f32; 4], backdrop: Option<[f32; 4]>, behind: Option<[f32; 4]>) -> [f32; 4] {
1706 let Some(backdrop) = backdrop.filter(|backdrop| oklab(*backdrop)[0] < 0.5) else {
1707 return color;
1708 };
1709 let [under, ..] = oklab(behind.unwrap_or(backdrop));
1710 let [lightness, a, b] = oklab(color);
1711 if under >= 0.5 || lightness >= under + LEGIBLE {
1712 return color;
1713 }
1714 let [red, green, blue] = from_oklab([under + LEGIBLE, a, b]);
1715 [red, green, blue, color[3]]
1716}
1717
1718/// OKLab lightness and opponent axes of a linear RGB colour.
1719pub fn oklab([red, green, blue, _]: [f32; 4]) -> [f32; 3] {
1720 let [l, m, s] = [
1721 [0.412_221_46, 0.536_332_55, 0.051_445_995],
1722 [0.211_903_5, 0.680_699_5, 0.107_396_96],
1723 [0.088_302_46, 0.281_718_85, 0.629_978_7],
1724 ]
1725 .map(|[r, g, b]| (r * red + g * green + b * blue).cbrt());
1726 [
1727 0.210_454_26 * l + 0.793_617_8 * m - 0.004_072_047 * s,
1728 1.977_998_5 * l - 2.428_592_2 * m + 0.450_593_7 * s,
1729 0.025_904_037 * l + 0.782_771_77 * m - 0.808_675_77 * s,
1730 ]
1731}
1732
1733/// Linear RGB of an OKLab colour, clipped to the sRGB gamut.
1734pub fn from_oklab([lightness, a, b]: [f32; 3]) -> [f32; 3] {
1735 let [l, m, s] = [
1736 lightness + 0.396_337_78 * a + 0.215_803_76 * b,
1737 lightness - 0.105_561_346 * a - 0.063_854_17 * b,
1738 lightness - 0.089_484_18 * a - 1.291_485_5 * b,
1739 ]
1740 .map(|value| value.powi(3));
1741 [
1742 [4.076_741_7, -3.307_711_6, 0.230_969_94],
1743 [-1.268_438, 2.609_757_4, -0.341_319_38],
1744 [-0.004_196_086_3, -0.703_418_6, 1.707_614_7],
1745 ]
1746 .map(|[x, y, z]| (x * l + y * m + z * s).clamp(0.0, 1.0))
1747}
1748
1749/// Linear RGBA of an opaque sRGB colour.
1750pub fn srgb(red: u8, green: u8, blue: u8) -> [f32; 4] {
1751 let [red, green, blue] = [red, green, blue].map(|byte| crate::linear(f32::from(byte) / 255.0));
1752 [red, green, blue, 1.0]
1753}
1754
1755/// The sRGB bytes of linear RGBA `color`, as `srgb` takes them.
1756pub fn srgb_bytes([red, green, blue, _]: [f32; 4]) -> [u8; 3] {
1757 [red, green, blue].map(srgb_byte)
1758}
1759
1760/// The hue in degrees of a linear colour, as it looks in sRGB.
1761pub fn hue(color: [f32; 4]) -> f32 {
1762 to_hsl([color[0], color[1], color[2]].map(crate::encode))[0]
1763}
1764
1765/// How much of a shade's saturation a linear colour's hue takes, from 0 to 1: all of it from
1766/// a saturation of `VIVID` up, as OneNote 2010's bright section colours have, less as the
1767/// colour greys, so its Silver and mists stay grey rather than turn their faint hue vivid.
1768pub fn vividness(color: [f32; 4]) -> f32 {
1769 const VIVID: f32 = 0.35;
1770 let [_, saturation, _] = to_hsl([color[0], color[1], color[2]].map(crate::encode));
1771 (saturation / VIVID).min(1.0)
1772}
1773
1774/// Hue in degrees, saturation and lightness of an encoded sRGB colour.
1775fn to_hsl([r, g, b]: [f32; 3]) -> [f32; 3] {
1776 let [max, min] = [r.max(g).max(b), r.min(g).min(b)];
1777 let lightness = (max + min) / 2.0;
1778 let range = max - min;
1779 if range == 0.0 {
1780 return [0.0, 0.0, lightness];
1781 }
1782 let saturation = range / (1.0 - (2.0 * lightness - 1.0).abs());
1783 let sector = if max == r {
1784 (g - b) / range
1785 } else if max == g {
1786 (b - r) / range + 2.0
1787 } else {
1788 (r - g) / range + 4.0
1789 };
1790 [(sector * 60.0).rem_euclid(360.0), saturation, lightness]
1791}
1792
1793/// The encoded sRGB colour of a hue in degrees, saturation and lightness.
1794fn from_hsl([hue, saturation, lightness]: [f32; 3]) -> [f32; 3] {
1795 let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation;
1796 [0.0, 8.0, 4.0].map(|offset: f32| {
1797 let k = (offset + hue / 30.0).rem_euclid(12.0);
1798 lightness - chroma / 2.0 * (k - 3.0).min(9.0 - k).clamp(-1.0, 1.0)
1799 })
1800}
1801
1802/// The light theme's accent `[saturation, lightness]`, also the default ink of a section's pen.
1803pub const LIGHT_ACCENT: [f32; 2] = [0.60, 0.45];
1804
1805/// An sRGB hue, saturation and lightness as linear RGBA.
1806pub fn hsl(hue: f32, saturation: f32, lightness: f32) -> [f32; 4] {
1807 let [red, green, blue] = from_hsl([hue, saturation, lightness])
1808 .map(|value| (value * 255.0).round().clamp(0.0, 255.0) as u8);
1809 srgb(red, green, blue)
1810}
1811
1812#[cfg(all(test, feature = "wgpu"))]
1813mod tests {
1814 use super::*;
1815 use parley::{
1816 FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, LayoutContext,
1817 PositionedLayoutItem, StyleProperty,
1818 };
1819 use std::time::Duration;
1820
1821 const CHECKBOX: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="a" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#7090b0"/></linearGradient></defs><path fill="url(#a)" d="M2 2h12v12H2zM4 4v8h8V4z"/></svg>"##;
1822 const MARK: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="b" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#20c040"/><stop offset="1" stop-color="#107020"/></linearGradient></defs><path fill="url(#b)" d="M4 8l3 3 5-7-1-1-4 5-2-2z"/></svg>"##;
1823 const DOT: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="c" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffd000"/><stop offset="1" stop-color="#c08000"/></linearGradient></defs><path fill="url(#c)" d="M8 2a6 6 0 1 0 0.01 0z"/></svg>"##;
1824
1825 struct Text(Layout<[u8; 4]>);
1826
1827 impl Glyphs for Text {
1828 fn runs(
1829 &self,
1830 paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>,
1831 ) -> Result<(), RenderError> {
1832 for line in self.0.lines() {
1833 let metrics = line.metrics();
1834 for item in line.items() {
1835 if let PositionedLayoutItem::GlyphRun(run) = item {
1836 paint_parley_run(
1837 &run,
1838 "",
1839 metrics.baseline,
1840 0.0,
1841 [metrics.block_min_coord, metrics.line_height],
1842 |_| None,
1843 None,
1844 paint,
1845 )?;
1846 }
1847 }
1848 }
1849 Ok(())
1850 }
1851 }
1852
1853 fn shape(text: &str, family: &str, size: f32, underline: bool, width: f32) -> Text {
1854 let mut fonts = FontContext::default();
1855 let mut context = LayoutContext::default();
1856 let mut builder = context.ranged_builder(&mut fonts, text, 1.0, false);
1857 builder.push_default(StyleProperty::FontFamily(FontFamily::List(
1858 vec![
1859 FontFamilyName::named(family),
1860 GenericFamily::SansSerif.into(),
1861 ]
1862 .into(),
1863 )));
1864 builder.push_default(StyleProperty::FontSize(size));
1865 builder.push_default(StyleProperty::Underline(underline));
1866 let mut layout = builder.build(text);
1867 layout.break_all_lines(Some(width));
1868 Text(layout)
1869 }
1870
1871 #[test]
1872 fn text_is_lifted_against_what_lies_behind_it_on_a_dark_layer() {
1873 let [black, dark_red] = [[0.0, 0.0, 0.0, 1.0], [0.2, 0.0, 0.0, 1.0]];
1874 let [dark, yellow, navy] = [srgb(0x1f, 0x20, 0x22), srgb(255, 255, 0), srgb(0, 0, 128)];
1875 let lightness = |color| oklab(color)[0];
1876 assert_eq!(legible(black, None, Some(navy)), black, "an unset layer");
1877 assert_eq!(
1878 legible(black, Some([1.0; 4]), Some(navy)),
1879 black,
1880 "a light layer keeps its colours, as OneNote's light page does"
1881 );
1882 assert!(lightness(legible(black, Some(dark), None)) >= lightness(dark) + LEGIBLE - 1e-3);
1883 assert_eq!(
1884 legible(black, Some(dark), Some(yellow)),
1885 black,
1886 "a light highlight on a dark layer"
1887 );
1888 assert_eq!(legible(dark_red, Some(dark), Some(yellow)), dark_red);
1889 let lifted = legible(black, Some(dark), Some(navy));
1890 assert!(lightness(lifted) >= lightness(navy) + LEGIBLE - 1e-3);
1891 }
1892
1893 #[test]
1894 fn decoding_preserves_pixels_and_rejects_large_or_truncated_images() {
1895 let mut encoded = Vec::new();
1896 let mut encoder = png::Encoder::new(&mut encoded, 2, 1);
1897 encoder.set_color(png::ColorType::Rgba);
1898 encoder.set_depth(png::BitDepth::Eight);
1899 let rgba = [255, 64, 0, 255, 0, 128, 255, 128];
1900 encoder
1901 .write_header()
1902 .unwrap()
1903 .write_image_data(&rgba)
1904 .unwrap();
1905 let decoded = RasterImage::decode(&encoded, [2, 1]).unwrap();
1906 assert_eq!(decoded.size, [2, 1]);
1907 assert_eq!(decoded.pixels(), [255, 64, 0, 255, 0, 93, 188, 128]);
1908 for end in 0..encoded.len() - 12 {
1909 assert!(
1910 RasterImage::decode(&encoded[..end], [2, 1]).is_err(),
1911 "{end}"
1912 );
1913 }
1914
1915 let mut jpeg = Vec::new();
1916 image::codecs::jpeg::JpegEncoder::new(&mut jpeg)
1917 .encode(&[255; 4 * 4 * 3], 4, 4, image::ExtendedColorType::Rgb8)
1918 .unwrap();
1919 let decoded = RasterImage::decode(&jpeg, [4, 4]).unwrap();
1920 assert_eq!(decoded.size, [4, 4]);
1921 assert_eq!(decoded.pixels(), [255; 4 * 4 * 4]);
1922
1923 let mut gif = Vec::new();
1924 image::codecs::gif::GifEncoder::new(&mut gif)
1925 .encode(&[255; 2 * 2 * 4], 2, 2, image::ExtendedColorType::Rgba8)
1926 .unwrap();
1927 let decoded = RasterImage::decode(&gif, [2, 2]).unwrap();
1928 assert_eq!(decoded.size, [2, 2]);
1929 assert_eq!(decoded.pixels(), [255; 2 * 2 * 4]);
1930
1931 for size in [[8193, 8192], [16_385, 1]] {
1932 let mut header = Vec::new();
1933 let mut encoder = png::Encoder::new(&mut header, size[0], size[1]);
1934 encoder.set_color(png::ColorType::Rgba);
1935 encoder.set_depth(png::BitDepth::Eight);
1936 let mut writer = encoder.write_header().unwrap();
1937 writer.write_chunk(png::chunk::IDAT, &[]).unwrap();
1938 drop(writer);
1939 let error = RasterImage::measure(&header).err().unwrap();
1940 if size[0] == 8193 {
1941 assert!(matches!(error, RenderError::ImageBudget), "{error:?}");
1942 } else {
1943 assert!(
1944 matches!(
1945 error,
1946 RenderError::ImageDecode(image::ImageError::Limits(_))
1947 ),
1948 "{error:?}"
1949 );
1950 }
1951 }
1952 }
1953
1954 #[test]
1955 fn decoding_shrinks_to_the_size_shown_without_bleeding_transparent_colour() {
1956 let mut encoded = Vec::new();
1957 let mut encoder = png::Encoder::new(&mut encoded, 4, 2);
1958 encoder.set_color(png::ColorType::Rgba);
1959 encoder.set_depth(png::BitDepth::Eight);
1960 let row = [
1961 [255, 0, 0, 255],
1962 [255, 0, 0, 255],
1963 [0, 255, 0, 0],
1964 [0, 255, 0, 0],
1965 ];
1966 encoder
1967 .write_header()
1968 .unwrap()
1969 .write_image_data(&[row, row].as_flattened().concat())
1970 .unwrap();
1971 assert_eq!(RasterImage::measure(&encoded).unwrap(), [4, 2]);
1972 let shown = RasterImage::decode(&encoded, [2, 1]).unwrap();
1973 assert_eq!(shown.size(), [2, 1]);
1974 let [left, right] = [&shown.pixels()[..4], &shown.pixels()[4..]];
1975 assert!(left[0] > 200 && left[3] > 200, "{left:?}");
1976 assert!(right[1] == 0 && right[0] <= right[3], "{right:?}");
1977 assert_eq!(
1978 RasterImage::decode(&encoded, [8, 8]).unwrap().size(),
1979 [4, 2]
1980 );
1981 assert_eq!(
1982 RasterImage::decode(&encoded, [0, 0]).unwrap().size(),
1983 [1, 1]
1984 );
1985 }
1986
1987 #[test]
1988 fn image_pixels_have_valid_dimensions_and_immutable_clone_identity() {
1989 assert!(matches!(
1990 RasterImage::new([0, 1], Vec::new()),
1991 Err(RenderError::InvalidImage)
1992 ));
1993 assert!(matches!(
1994 RasterImage::new([2, 2], vec![255; 15]),
1995 Err(RenderError::InvalidImage)
1996 ));
1997 assert!(matches!(
1998 RasterImage::new([u32::MAX; 2], Vec::new()),
1999 Err(RenderError::InvalidImage)
2000 ));
2001 let image = RasterImage::new([1, 1], vec![1, 2, 3, 255]).unwrap();
2002 assert_eq!(image.id(), image.clone().id());
2003 }
2004
2005 struct Target {
2006 texture: wgpu::Texture,
2007 readback: wgpu::Buffer,
2008 }
2009
2010 impl Target {
2011 fn new(device: &wgpu::Device) -> Self {
2012 Self::with_format(device, wgpu::TextureFormat::Rgba8UnormSrgb)
2013 }
2014
2015 fn with_format(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
2016 Self {
2017 texture: device.create_texture(&wgpu::TextureDescriptor {
2018 label: Some("Draw readback test"),
2019 size: wgpu::Extent3d {
2020 width: 512,
2021 height: 256,
2022 depth_or_array_layers: 1,
2023 },
2024 mip_level_count: 1,
2025 sample_count: 1,
2026 dimension: wgpu::TextureDimension::D2,
2027 format,
2028 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2029 view_formats: &[],
2030 }),
2031 readback: device.create_buffer(&wgpu::BufferDescriptor {
2032 label: Some("Draw readback"),
2033 size: 512 * 256 * 4,
2034 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
2035 mapped_at_creation: false,
2036 }),
2037 }
2038 }
2039
2040 fn draw(&self, renderer: &mut Renderer, layers: &[Layer<'_>]) -> Result<(), RenderError> {
2041 renderer.draw(
2042 &self.texture.create_view(&Default::default()).into(),
2043 [512, 256],
2044 [1.0; 4],
2045 layers,
2046 )
2047 }
2048
2049 fn capture(&self, renderer: &Renderer) -> Vec<u8> {
2050 let mut encoder = renderer
2051 .device()
2052 .create_command_encoder(&Default::default());
2053 encoder.copy_texture_to_buffer(
2054 self.texture.as_image_copy(),
2055 wgpu::TexelCopyBufferInfo {
2056 buffer: &self.readback,
2057 layout: wgpu::TexelCopyBufferLayout {
2058 offset: 0,
2059 bytes_per_row: Some(512 * 4),
2060 rows_per_image: Some(256),
2061 },
2062 },
2063 wgpu::Extent3d {
2064 width: 512,
2065 height: 256,
2066 depth_or_array_layers: 1,
2067 },
2068 );
2069 renderer.queue().submit([encoder.finish()]);
2070 let (sender, receiver) = std::sync::mpsc::channel();
2071 self.readback
2072 .map_async(wgpu::MapMode::Read, .., move |result| {
2073 sender.send(result).unwrap();
2074 });
2075 renderer
2076 .device()
2077 .poll(wgpu::PollType::Wait {
2078 submission_index: None,
2079 timeout: Some(Duration::from_secs(5)),
2080 })
2081 .unwrap();
2082 receiver
2083 .recv_timeout(Duration::from_secs(5))
2084 .unwrap()
2085 .unwrap();
2086 let capture = self.readback.get_mapped_range(..).unwrap().to_vec();
2087 self.readback.unmap();
2088 capture
2089 }
2090 }
2091
2092 fn page<'a>(primitives: &'a [Primitive<'a>]) -> [Layer<'a>; 1] {
2093 [Layer {
2094 scale: 2.0,
2095 origin: [24.0; 2],
2096 clip: None,
2097 backdrop: None,
2098 round: None,
2099 motion: None,
2100 primitives,
2101 }]
2102 }
2103
2104 #[test]
2105 #[ignore = "requires a native GPU adapter"]
2106 fn layers_sharing_a_motion_fade_as_one_inside_their_rounded_clip() {
2107 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
2108 let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
2109 let (device, queue) =
2110 pollster::block_on(adapter.request_device(&Default::default())).unwrap();
2111 let target = Target::new(&device);
2112 let mut renderer = Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb);
2113 let rect = |rect, color| [Primitive::Rect { rect, color }];
2114 let [blue, red, black] = [
2115 rect([0.0, 0.0, 100.0, 100.0], [0.0, 0.0, 1.0, 1.0]),
2116 rect([50.0, 0.0, 150.0, 100.0], [1.0, 0.0, 0.0, 1.0]),
2117 rect([200.0, 0.0, 300.0, 100.0], [0.0, 0.0, 0.0, 1.0]),
2118 ];
2119 let half = Motion {
2120 opacity: 0.5,
2121 tilt: 0.0,
2122 pivot: [0.0; 2],
2123 };
2124 let layer = |primitives, motion, round| Layer {
2125 scale: 1.0,
2126 origin: [0.0; 2],
2127 clip: None,
2128 backdrop: None,
2129 round,
2130 motion,
2131 primitives,
2132 };
2133 target
2134 .draw(
2135 &mut renderer,
2136 &[
2137 layer(&blue, Some(half), None),
2138 layer(&red, Some(half), None),
2139 layer(&black, None, Some(([200.0, 0.0, 300.0, 100.0], 30.0))),
2140 ],
2141 )
2142 .unwrap();
2143 let capture = target.capture(&renderer);
2144 let pixel = |x: usize, y: usize| &capture[(y * 512 + x) * 4..(y * 512 + x) * 4 + 4];
2145 let [overlap, alone] = [pixel(75, 50), pixel(25, 50)];
2146 assert!(
2147 overlap[0] == 255 && overlap[1] == overlap[2] && overlap[1] < 255,
2148 "the red covers the blue before both fade: {overlap:?}"
2149 );
2150 assert!(
2151 alone[2] == 255 && alone[0] == alone[1] && alone[0] < 255,
2152 "{alone:?}"
2153 );
2154 assert_eq!(pixel(202, 2), [255; 4], "outside the rounded corner");
2155 assert_eq!(pixel(250, 50), [0, 0, 0, 255]);
2156 }
2157
2158 #[test]
2159 #[ignore = "requires a native GPU adapter"]
2160 fn eviction_and_repaint_reproduce_pixels() {
2161 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
2162 let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
2163 let (device, queue) =
2164 pollster::block_on(adapter.request_device(&Default::default())).unwrap();
2165 let format = wgpu::TextureFormat::Rgba8UnormSrgb;
2166 let target = Target::new(&device);
2167 let mut renderer = Renderer::new(device, queue, format);
2168 let layout = shape("Hello 🌳\nCafé e\u{301} שלום", "Arial", 11.0, true, 220.0);
2169 let mut captures = Vec::new();
2170 let image = RasterImage::new(
2171 [2, 2],
2172 vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0, 0, 0, 0],
2173 )
2174 .unwrap();
2175 let transparent_edge =
2176 RasterImage::new([2, 1], vec![255, 0, 0, 255, 0, 0, 255, 0]).unwrap();
2177 let icons: [&'static [&'static str]; 4] = [&[CHECKBOX], &[CHECKBOX, MARK], &[DOT], &[MARK]];
2178 let phase_layout = shape("H", "Arial", 11.0, false, 20.0);
2179 let mut primitives = vec![
2180 Primitive::Rect {
2181 rect: [0.0, 0.0, 70.0, 14.0],
2182 color: [0.55, 0.73, 1.0, 0.5],
2183 },
2184 Primitive::Text {
2185 clip: None,
2186 text: &layout,
2187 ink: [0.0, 0.0, 0.0, 1.0],
2188 origin: [0.0; 2],
2189 },
2190 Primitive::Text {
2191 clip: None,
2192 text: &layout,
2193 ink: [0.0, 0.0, 0.0, 1.0],
2194 origin: [8.0, 64.0],
2195 },
2196 Primitive::Image {
2197 image: &image,
2198 rect: [128.0, 8.0, 160.0, 40.0],
2199 },
2200 Primitive::Rect {
2201 rect: [128.0, 8.0, 132.0, 12.0],
2202 color: [0.0, 0.0, 1.0, 1.0],
2203 },
2204 Primitive::Image {
2205 image: &transparent_edge,
2206 rect: [176.0, 8.0, 208.0, 40.0],
2207 },
2208 Primitive::RoundedRect {
2209 rect: [128.0, 48.0, 148.0, 68.0],
2210 radius: [5.0, 10.0],
2211 stroke: None,
2212 color: [1.0, 0.0, 0.0, 1.0],
2213 },
2214 Primitive::RoundedRect {
2215 rect: [164.0, 48.0, 184.0, 68.0],
2216 radius: [5.0; 2],
2217 stroke: Some(Stroke::Solid(1.0)),
2218 color: [0.0, 0.0, 1.0, 1.0],
2219 },
2220 Primitive::RoundedRect {
2221 rect: [200.0, 48.0, 240.0, 68.0],
2222 radius: [3.0, 10.0],
2223 stroke: Some(Stroke::Dashed(1.0)),
2224 color: [0.0, 0.0, 1.0, 1.0],
2225 },
2226 Primitive::Segment {
2227 from: [226.0, 104.0],
2228 to: [240.0, 104.0],
2229 width: 4.0,
2230 round: true,
2231 color: [1.0, 0.0, 0.0, 1.0],
2232 },
2233 ];
2234 for (index, sources) in icons.iter().enumerate() {
2235 primitives.push(Primitive::Icon {
2236 sources,
2237 origin: [128.125 + 20.0 * index as f32, 80.25],
2238 size: 12.0,
2239 tint: [1.0; 4],
2240 palette: Palette::default(),
2241 });
2242 }
2243 primitives.extend((0..9).map(|step| Primitive::Text {
2244 clip: None,
2245 text: &phase_layout,
2246 ink: [0.0, 0.0, 0.0, 1.0],
2247 origin: [8.0 + 24.0 * step as f32, 96.0 + 0.125 * step as f32],
2248 }));
2249 for pass in 0..6 {
2250 if pass == 2 {
2251 renderer.clear_glyph_cache();
2252 renderer.images.clear();
2253 } else if pass == 3 {
2254 renderer =
2255 Renderer::new(renderer.device().clone(), renderer.queue().clone(), format);
2256 }
2257 let clipping = [
2258 Primitive::Text {
2259 text: &layout,
2260 ink: [0.0, 0.0, 0.0, 1.0],
2261 origin: [0.0; 2],
2262 clip: (pass == 5).then_some([2.5, 1.25, 30.75, 20.5]),
2263 },
2264 Primitive::Rect {
2265 rect: [40.0, 40.0, 70.0, 50.0],
2266 color: [0.0, 0.0, 1.0, 1.0],
2267 },
2268 ];
2269 target
2270 .draw(
2271 &mut renderer,
2272 &page(if pass < 4 { &primitives } else { &clipping }),
2273 )
2274 .unwrap();
2275 captures.push(target.capture(&renderer));
2276 }
2277 let centroids: Vec<_> = (0..9)
2278 .map(|step| {
2279 let mut weight = 0.0_f64;
2280 let mut moment = 0.0_f64;
2281 for y in 212..252 {
2282 for x in (40 + 48 * step)..(72 + 48 * step) {
2283 // Black text over white leaves the rasterized coverage sRGB-encoded.
2284 let coverage = 1.0 - f64::from(captures[0][(y * 512 + x) * 4]) / 255.0;
2285 weight += coverage;
2286 moment += coverage * y as f64;
2287 }
2288 }
2289 assert!(weight > 10.0, "phase sample {step} was not painted");
2290 moment / weight
2291 })
2292 .collect();
2293 let pixel = |x: usize, y: usize| &captures[0][(y * 512 + x) * 4..(y * 512 + x) * 4 + 4];
2294 assert_eq!(pixel(490, 232), [255, 0, 0, 255]);
2295 assert_eq!(pixel(473, 232), [255, 0, 0, 255]);
2296 assert_eq!(pixel(490, 238), [255; 4]);
2297 assert_eq!(pixel(472, 228), [255; 4]);
2298 assert_eq!(pixel(300, 140), [255, 0, 0, 255]);
2299 assert_eq!(pixel(280, 120), [255; 4]);
2300 assert_eq!(pixel(282, 124), [255; 4]);
2301 assert_eq!(pixel(281, 140), [255, 0, 0, 255]);
2302 assert_eq!(pixel(372, 140), [255; 4]);
2303 assert_eq!(pixel(372, 120), [0, 0, 255, 255]);
2304 assert_eq!(pixel(352, 120), [255; 4]);
2305 assert_eq!(pixel(424, 120), [255; 4]);
2306 assert_eq!(pixel(464, 140), [255; 4]);
2307 assert!((440..480).any(|x| pixel(x, 120)[0] < 40));
2308 assert!((440..480).any(|x| pixel(x, 120)[0] > 250));
2309 for x in 440..472 {
2310 assert!(pixel(x, 120)[0].abs_diff(pixel(x + 8, 120)[0]) <= 1);
2311 }
2312 for (step, centroid) in centroids.iter().enumerate() {
2313 let movement = centroid - centroids[0];
2314 assert!(
2315 (movement - step as f64 * 0.25).abs() < 0.06,
2316 "quarter-pixel step {step} moved the glyph by {movement} px"
2317 );
2318 }
2319 assert!(!renderer.glyphs.is_empty());
2320 assert!(renderer.glyphs.len() <= renderer.glyph_limit());
2321 assert_eq!(
2322 renderer
2323 .glyphs
2324 .keys()
2325 .filter(|key| matches!(key, AtlasKey::Icon { .. }))
2326 .count(),
2327 4
2328 );
2329 for x in [280, 320, 360, 400] {
2330 let colored = (184..210)
2331 .flat_map(|y| (x..x + 26).map(move |x| (y * 512 + x) * 4))
2332 .filter(|offset| captures[0][*offset..*offset + 3].iter().any(|v| *v < 180))
2333 .count();
2334 assert!(colored > 10, "icon at {x} was not painted");
2335 }
2336 let count = renderer.glyphs.len();
2337 target
2338 .draw(
2339 &mut renderer,
2340 &page(&[Primitive::Icon {
2341 sources: icons[0],
2342 origin: [10000.0; 2],
2343 size: 12.0,
2344 tint: [1.0; 4],
2345 palette: Palette::default(),
2346 }]),
2347 )
2348 .unwrap();
2349 assert_eq!(renderer.glyphs.len(), count);
2350 assert!(
2351 captures[0]
2352 .chunks_exact(4)
2353 .filter(|pixel| pixel[0] < 200)
2354 .count()
2355 > 100
2356 );
2357 assert_eq!(captures[0], captures[1]);
2358 assert_eq!(captures[0], captures[2]);
2359 assert_eq!(captures[0], captures[3]);
2360 assert_ne!(captures[4], captures[5]);
2361 for y in 0..256 {
2362 for x in 0..512 {
2363 let offset = (y * 512 + x) * 4;
2364 let expected = if ((29..86).contains(&x) && (26..65).contains(&y))
2365 || ((104..164).contains(&x) && (104..124).contains(&y))
2366 {
2367 &captures[4][offset..offset + 4]
2368 } else {
2369 &[255; 4]
2370 };
2371 assert_eq!(
2372 &captures[5][offset..offset + 4],
2373 expected,
2374 "clipping at {x},{y}"
2375 );
2376 }
2377 }
2378 for (x, y, color) in [
2379 (288, 48, [255, 0, 0, 255]),
2380 (336, 96, [255; 4]),
2381 (284, 44, [0, 0, 255, 255]),
2382 ] {
2383 let offset = (y * 512 + x) * 4;
2384 assert_eq!(captures[0][offset..offset + 4], color);
2385 }
2386 let edge = &captures[0][(48 * 512 + 408) * 4..(48 * 512 + 408) * 4 + 4];
2387 assert_eq!(edge[0], 255, "{edge:?}");
2388 assert_eq!(edge[1], edge[2], "{edge:?}");
2389 assert!((180..=200).contains(&edge[1]), "{edge:?}");
2390 assert_eq!(renderer.images.len(), 2);
2391 for primitive in [
2392 Primitive::RoundedRect {
2393 rect: [-1e38, 0.0, 1e38, 1e38],
2394 radius: [3.0; 2],
2395 stroke: Some(Stroke::Dashed(1.0)),
2396 color: [1.0; 4],
2397 },
2398 Primitive::RoundedRect {
2399 rect: [0.0, 0.0, 10.0, 10.0],
2400 radius: [f32::NAN, 2.0],
2401 stroke: None,
2402 color: [1.0; 4],
2403 },
2404 Primitive::RoundedRect {
2405 rect: [0.0, 0.0, 10.0, 10.0],
2406 radius: [2.0; 2],
2407 stroke: Some(Stroke::Solid(0.0)),
2408 color: [1.0; 4],
2409 },
2410 Primitive::RoundedRect {
2411 rect: [0.0, 0.0, 10.0, 10.0],
2412 radius: [2.0; 2],
2413 stroke: Some(Stroke::Dashed(f32::NAN)),
2414 color: [1.0; 4],
2415 },
2416 Primitive::Rect {
2417 rect: [0.0, 0.0, f32::NAN, 1.0],
2418 color: [1.0; 4],
2419 },
2420 Primitive::Rect {
2421 rect: [2.0, 0.0, 1.0, 1.0],
2422 color: [1.0; 4],
2423 },
2424 Primitive::Text {
2425 clip: None,
2426 text: &layout,
2427 ink: [0.0, 0.0, 0.0, 1.0],
2428 origin: [f32::MAX; 2],
2429 },
2430 Primitive::Text {
2431 clip: Some([0.0, 0.0, f32::NAN, 1.0]),
2432 text: &layout,
2433 ink: [0.0, 0.0, 0.0, 1.0],
2434 origin: [0.0; 2],
2435 },
2436 Primitive::Text {
2437 clip: Some([2.0, 0.0, 1.0, 1.0]),
2438 text: &layout,
2439 ink: [0.0, 0.0, 0.0, 1.0],
2440 origin: [0.0; 2],
2441 },
2442 ] {
2443 assert!(matches!(
2444 target.draw(&mut renderer, &page(&[primitive])),
2445 Err(RenderError::InvalidPrimitive)
2446 ));
2447 }
2448 let too_many: Vec<_> = (0..=MAX_IMAGES)
2449 .map(|_| RasterImage::new([1, 1], vec![255; 4]).unwrap())
2450 .collect();
2451 let primitives: Vec<_> = too_many
2452 .iter()
2453 .map(|image| Primitive::Image {
2454 image,
2455 rect: [0.0, 0.0, 1.0, 1.0],
2456 })
2457 .collect();
2458 assert!(matches!(
2459 target.draw(&mut renderer, &page(&primitives)),
2460 Err(RenderError::ImageBudget)
2461 ));
2462 let primitives: Vec<_> = too_many
2463 .iter()
2464 .map(|image| Primitive::Image {
2465 image,
2466 rect: [-20.0, -20.0, -19.0, -19.0],
2467 })
2468 .collect();
2469 target.draw(&mut renderer, &page(&primitives)).unwrap();
2470 assert_eq!(renderer.images.len(), 2);
2471 let mut seen = HashSet::from([image.id(), transparent_edge.id()]);
2472 let mut previous = None;
2473 for _ in 0..5 {
2474 let large = RasterImage::new([2048; 2], vec![255; 2048 * 2048 * 4]).unwrap();
2475 seen.insert(large.id());
2476 let mut primitives = vec![Primitive::Image {
2477 image: &large,
2478 rect: [0.0, 0.0, 16.0, 16.0],
2479 }];
2480 if let Some(image) = &previous {
2481 primitives.push(Primitive::Image {
2482 image,
2483 rect: [16.0, 0.0, 32.0, 16.0],
2484 });
2485 }
2486 target.draw(&mut renderer, &page(&primitives)).unwrap();
2487 assert!(renderer.images.contains_key(&large.id()));
2488 if let Some(image) = &previous {
2489 assert!(renderer.images.contains_key(&image.id()));
2490 }
2491 assert!(renderer.occupancy().within_budget);
2492 previous = Some(large);
2493 }
2494 assert!(renderer.images.len() <= MAX_IMAGES);
2495 assert!(seen.iter().any(|id| !renderer.images.contains_key(id)));
2496 let last = previous.take().unwrap().id();
2497 target.draw(&mut renderer, &page(&[])).unwrap();
2498 assert!(
2499 !renderer.images.contains_key(&last),
2500 "a texture outlived its image"
2501 );
2502 renderer.images.clear();
2503 target
2504 .draw(
2505 &mut renderer,
2506 &page(&[Primitive::Image {
2507 image: &image,
2508 rect: [128.0, 8.0, 160.0, 40.0],
2509 }]),
2510 )
2511 .unwrap();
2512 assert_eq!(renderer.images.len(), 1);
2513 assert!(renderer.images.contains_key(&image.id()));
2514 }
2515
2516 #[test]
2517 #[ignore = "requires a native GPU adapter"]
2518 fn paths_fill_and_stroke_in_layer_units() {
2519 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
2520 let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
2521 let (device, queue) =
2522 pollster::block_on(adapter.request_device(&Default::default())).unwrap();
2523 let target = Target::new(&device);
2524 let mut renderer = Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb);
2525 let primitives = [
2526 Primitive::Path {
2527 data: "M0 0H20L30 20H0Z",
2528 origin: [10.0, 10.0],
2529 style: PathStyle::Fill,
2530 colors: [[0.0, 0.0, 1.0, 1.0]; 2],
2531 },
2532 Primitive::Path {
2533 data: "M0 0H40",
2534 origin: [10.0, 60.0],
2535 style: PathStyle::Stroke(4.0),
2536 colors: [[1.0, 0.0, 0.0, 1.0]; 2],
2537 },
2538 Primitive::Path {
2539 data: "M0 0H20V20H0Z",
2540 origin: [150.0, 20.0],
2541 style: PathStyle::Shadow(6.0),
2542 colors: [[0.0, 0.0, 0.0, 1.0]; 2],
2543 },
2544 Primitive::Shadow {
2545 rect: [170.0, 90.0, 190.0, 110.0],
2546 radius: 4.0,
2547 blur: 6.0,
2548 color: [0.0, 0.0, 0.0, 1.0],
2549 },
2550 Primitive::Gradient {
2551 rect: [100.0, 10.0, 120.0, 110.0],
2552 radius: [0.0; 2],
2553 colors: [[0.0, 0.0, 1.0, 1.0], [0.0, 1.0, 0.0, 1.0]],
2554 },
2555 ];
2556 target
2557 .draw(
2558 &mut renderer,
2559 &[Layer {
2560 scale: 2.0,
2561 origin: [0.0; 2],
2562 clip: None,
2563 backdrop: None,
2564 round: None,
2565 motion: None,
2566 primitives: &primitives,
2567 }],
2568 )
2569 .unwrap();
2570 let capture = target.capture(&renderer);
2571 let pixel = |x: usize, y: usize| &capture[(y * 512 + x) * 4..(y * 512 + x) * 4 + 4];
2572 assert_eq!(pixel(40, 40), [0, 0, 255, 255]);
2573 assert_eq!(pixel(70, 55), [0, 0, 255, 255], "inside the slanted edge");
2574 assert_eq!(pixel(75, 30), [255; 4], "outside the slanted edge");
2575 let edge = pixel(65, 30);
2576 assert!(
2577 edge[0] > 0 && edge[0] < 255,
2578 "the slant is antialiased: {edge:?}"
2579 );
2580 assert_eq!(pixel(60, 120), [255, 0, 0, 255]);
2581 assert_eq!(pixel(60, 112), [255; 4], "strokes are their width across");
2582 assert_eq!(pixel(17, 120)[1], 0, "round caps reach past the ends");
2583 assert_eq!(pixel(12, 120), [255; 4]);
2584 let [top, bottom] = [pixel(220, 21), pixel(220, 218)];
2585 assert!(
2586 top[2] > 250 && top[1] < 60,
2587 "gradients start at the top colour: {top:?}"
2588 );
2589 assert!(
2590 bottom[1] > 250 && bottom[2] < 60,
2591 "and end at the bottom's: {bottom:?}"
2592 );
2593 let [inside, fading, beyond] = [pixel(320, 60), pixel(298, 60), pixel(270, 60)];
2594 assert!(inside[0] < 40, "a shadow is dark inside: {inside:?}");
2595 assert!(
2596 fading[0] > 40 && fading[0] < 230,
2597 "and fades at its edge: {fading:?}"
2598 );
2599 assert_eq!(beyond, [255; 4]);
2600 let [inside, fading, beyond] = [pixel(360, 200), pixel(340, 200), pixel(310, 200)];
2601 assert!(
2602 inside[0] < 40,
2603 "a rectangle's shadow is dark inside: {inside:?}"
2604 );
2605 assert!(
2606 fading[0] > 40 && fading[0] < 230,
2607 "and fades at its edge: {fading:?}"
2608 );
2609 assert_eq!(beyond, [255; 4]);
2610 assert_eq!(renderer.glyphs.len(), 3, "and takes no room in the atlas");
2611 }
2612
2613 #[test]
2614 #[ignore = "requires a native GPU adapter"]
2615 fn icons_past_one_atlas_never_fail_a_frame() {
2616 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
2617 let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
2618 let (device, queue) =
2619 pollster::block_on(adapter.request_device(&Default::default())).unwrap();
2620 let target = Target::new(&device);
2621 let format = wgpu::TextureFormat::Rgba8UnormSrgb;
2622 let mut renderer = Renderer::new(device, queue, format);
2623 // Each tint is an entry of its own, 64 device pixels square.
2624 let icons = |tints: Range<usize>| -> Vec<Primitive<'static>> {
2625 tints
2626 .map(|tint| Primitive::Icon {
2627 sources: &[CHECKBOX],
2628 origin: [0.0; 2],
2629 size: 32.0,
2630 tint: [tint as f32 / 65_536.0, 0.0, 0.0, 1.0],
2631 palette: Palette::default(),
2632 })
2633 .collect()
2634 };
2635 let one_atlas = (ATLAS_SIZE as usize / 65).pow(2);
2636 for frame in 0..8 {
2637 let start = frame * one_atlas / 3;
2638 let primitives = icons(start..start + one_atlas / 3);
2639 target.draw(&mut renderer, &page(&primitives)).unwrap();
2640 }
2641 assert_eq!(
2642 renderer.gpu.atlas_side(),
2643 ATLAS_SIZE,
2644 "frames each needing a third of the atlas evict, never grow it"
2645 );
2646 let primitives = icons(0..3 * one_atlas);
2647 target.draw(&mut renderer, &page(&primitives)).unwrap();
2648 assert!(renderer.gpu.atlas_side() > ATLAS_SIZE);
2649 assert!(renderer.glyphs.len() >= 3 * one_atlas);
2650 let capture = target.capture(&renderer);
2651 let pixel = |x: usize, y: usize| &capture[(y * 512 + x) * 4..(y * 512 + x) * 4 + 4];
2652 assert_ne!(pixel(24 + 12, 24 + 32), [255; 4], "the icons paint");
2653 }
2654
2655 #[test]
2656 #[ignore = "requires a native GPU adapter"]
2657 fn transparent_targets_hold_premultiplied_coverage_and_erase() {
2658 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
2659 let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
2660 let (device, queue) =
2661 pollster::block_on(adapter.request_device(&Default::default())).unwrap();
2662 let target = Target::new(&device);
2663 let mut renderer = Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb);
2664 let primitives = [
2665 Primitive::Rect {
2666 rect: [0.0, 0.0, 10.0, 10.0],
2667 color: [1.0, 1.0, 1.0, 0.5],
2668 },
2669 Primitive::Rect {
2670 rect: [20.0, 0.0, 40.0, 10.0],
2671 color: [0.0, 0.0, 1.0, 1.0],
2672 },
2673 Primitive::Path {
2674 data: "M0 0H10V10H0Z",
2675 origin: [30.0, 0.0],
2676 style: PathStyle::Erase,
2677 colors: [[0.0, 0.0, 0.0, 1.0]; 2],
2678 },
2679 ];
2680 renderer
2681 .draw(
2682 &target.texture.create_view(&Default::default()).into(),
2683 [512, 256],
2684 [0.0; 4],
2685 &[Layer {
2686 scale: 1.0,
2687 origin: [0.0; 2],
2688 clip: None,
2689 backdrop: None,
2690 round: None,
2691 motion: None,
2692 primitives: &primitives,
2693 }],
2694 )
2695 .unwrap();
2696 let capture = target.capture(&renderer);
2697 let pixel = |x: usize, y: usize| &capture[(y * 512 + x) * 4..(y * 512 + x) * 4 + 4];
2698 assert_eq!(pixel(5, 5)[3], 128, "half coverage is half opaque");
2699 assert_eq!(pixel(5, 5)[0], 188, "over premultiplied colour");
2700 assert_eq!(pixel(25, 5), [0, 0, 255, 255]);
2701 assert_eq!(pixel(35, 5), [0; 4], "erased back to transparency");
2702 assert_eq!(pixel(50, 5), [0; 4]);
2703 }
2704
2705 #[test]
2706 #[ignore = "requires a native GPU adapter"]
2707 fn translucent_windows_take_colour_premultiplied_in_srgb() {
2708 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
2709 let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
2710 let (device, queue) =
2711 pollster::block_on(adapter.request_device(&Default::default())).unwrap();
2712 let format = wgpu::TextureFormat::Rgba8UnormSrgb;
2713 let mut renderer = Renderer::new(device.clone(), queue.clone(), format);
2714 let mut translucent = Translucent::new(&device, format, wgpu::TextureFormat::Rgba8Unorm);
2715 let primitives = [
2716 Primitive::Rect {
2717 rect: [0.0, 0.0, 10.0, 10.0],
2718 color: [1.0, 1.0, 1.0, 0.2],
2719 },
2720 Primitive::Rect {
2721 rect: [20.0, 0.0, 30.0, 10.0],
2722 color: [0.2, 0.2, 0.2, 1.0],
2723 },
2724 ];
2725 let frame = translucent.target(&device, [512, 256]).into();
2726 renderer
2727 .draw(
2728 &frame,
2729 [512, 256],
2730 [0.0; 4],
2731 &[Layer {
2732 scale: 1.0,
2733 origin: [0.0; 2],
2734 clip: None,
2735 backdrop: None,
2736 round: None,
2737 motion: None,
2738 primitives: &primitives,
2739 }],
2740 )
2741 .unwrap();
2742 let window = Target::with_format(&device, wgpu::TextureFormat::Rgba8Unorm);
2743 translucent.present(
2744 &device,
2745 &queue,
2746 &window.texture.create_view(&Default::default()),
2747 );
2748 let capture = window.capture(&renderer);
2749 let pixel = |x: usize, y: usize| &capture[(y * 512 + x) * 4..(y * 512 + x) * 4 + 4];
2750 assert_eq!(
2751 pixel(5, 5),
2752 [51, 51, 51, 51],
2753 "white at a fifth, premultiplied in sRGB"
2754 );
2755 assert_eq!(
2756 pixel(25, 5),
2757 [124, 124, 124, 255],
2758 "opaque colours encode as ever"
2759 );
2760 assert_eq!(pixel(50, 5), [0; 4]);
2761 }
2762
2763 #[test]
2764 #[ignore = "requires a native GPU adapter"]
2765 fn layers_transform_and_clip_independently() {
2766 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
2767 let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
2768 let (device, queue) =
2769 pollster::block_on(adapter.request_device(&Default::default())).unwrap();
2770 let target = Target::new(&device);
2771 let mut renderer = Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb);
2772 let fill = [Primitive::Rect {
2773 rect: [0.0, 0.0, 300.0, 100.0],
2774 color: [1.0, 0.0, 0.0, 1.0],
2775 }];
2776 let chrome = [Primitive::Rect {
2777 rect: [0.0, 0.0, 10.0, 10.0],
2778 color: [0.0, 0.0, 1.0, 1.0],
2779 }];
2780 target
2781 .draw(
2782 &mut renderer,
2783 &[
2784 Layer {
2785 scale: 1.0,
2786 origin: [0.0; 2],
2787 clip: Some([100.0, 50.0, 200.5, 80.0]),
2788 backdrop: None,
2789 round: None,
2790 motion: None,
2791 primitives: &fill,
2792 },
2793 Layer {
2794 scale: 2.0,
2795 origin: [300.0, 10.0],
2796 clip: None,
2797 backdrop: None,
2798 round: None,
2799 motion: None,
2800 primitives: &chrome,
2801 },
2802 ],
2803 )
2804 .unwrap();
2805 let capture = target.capture(&renderer);
2806 let pixel = |x: usize, y: usize| &capture[(y * 512 + x) * 4..(y * 512 + x) * 4 + 4];
2807 assert_eq!(pixel(10, 10), [255; 4], "clipped away");
2808 assert_eq!(pixel(100, 50), [255, 0, 0, 255]);
2809 assert_eq!(
2810 pixel(200, 79),
2811 [255, 0, 0, 255],
2812 "partial pixels round outward"
2813 );
2814 assert_eq!(pixel(99, 60), [255; 4]);
2815 assert_eq!(pixel(201, 60), [255; 4]);
2816 assert_eq!(pixel(150, 80), [255; 4]);
2817 assert_eq!(pixel(319, 29), [0, 0, 255, 255]);
2818 assert_eq!(pixel(320, 30), [255; 4]);
2819 assert!(matches!(
2820 target.draw(
2821 &mut renderer,
2822 &[Layer {
2823 scale: 0.0,
2824 origin: [0.0; 2],
2825 clip: None,
2826 backdrop: None,
2827 round: None,
2828 motion: None,
2829 primitives: &fill,
2830 }]
2831 ),
2832 Err(RenderError::InvalidLayer)
2833 ));
2834 }
2835}