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