1#[allow(clippy::wildcard_imports)] use super::*;
3
4use std::collections::HashMap;
5use azul_core::geom::{LogicalPosition, LogicalRect, LogicalSize};
6use azul_core::resources::{DecodedImage, ImageRef, RendererResources};
7use azul_core::ui_solver::GlyphInstance;
8use azul_css::props::basic::{ColorOrSystem, ColorU, FontRef};
9use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
10use azul_css::props::style::filter::StyleFilter;
11use azul_css::props::style::box_shadow::StyleBoxShadow;
12use agg_rust::basics::{FillingRule, PATH_FLAGS_NONE};
13use agg_rust::blur::stack_blur_rgba32;
14use agg_rust::color::Rgba8;
15use agg_rust::conv_stroke::ConvStroke;
16use agg_rust::gradient_lut::GradientLut;
17use agg_rust::path_storage::PathStorage;
18use agg_rust::pixfmt_rgba::PixfmtRgba32;
19use agg_rust::rasterizer_scanline_aa::RasterizerScanlineAa;
20use agg_rust::renderer_base::RendererBase;
21use agg_rust::renderer_scanline::render_scanlines_aa_solid;
22use agg_rust::rendering_buffer::RowAccessor;
23use agg_rust::rounded_rect::RoundedRect;
24use agg_rust::scanline_u::ScanlineU8;
25use agg_rust::span_gradient::{GradientConic, GradientRadialD, GradientX};
26use agg_rust::trans_affine::TransAffine;
27use crate::font::parsed::ParsedFont;
28use crate::glyph_cache::GlyphCache;
29use crate::solver3::display_list::{BorderRadius, DisplayList, DisplayListItem, LocalScrollId};
30use crate::text3::cache::{FontHash, FontManager};
31
32const MAX_SHADOW_PIXBUF_SIZE: u32 = 4096;
33
34const SYSTEM_COLOR_FALLBACK: ColorU = ColorU {
43 r: 0,
44 g: 0,
45 b: 0,
46 a: 0,
47};
48
49#[allow(clippy::trivially_copy_pass_by_ref)] fn resolve_color(
56 color: &ColorOrSystem,
57 system_colors: Option<&azul_css::system::SystemColors>,
58) -> ColorU {
59 match (color, system_colors) {
60 (ColorOrSystem::Color(c), _) => *c,
61 (ColorOrSystem::System(_), Some(sc)) => color.resolve(sc, SYSTEM_COLOR_FALLBACK),
62 (ColorOrSystem::System(_), None) => SYSTEM_COLOR_FALLBACK,
63 }
64}
65
66fn build_gradient_lut_linear(
68 stops: &azul_css::props::style::background::NormalizedLinearColorStopVec,
69 system_colors: Option<&azul_css::system::SystemColors>,
70) -> GradientLut {
71 let mut lut = GradientLut::new_default();
72 let stops_slice = stops.as_ref();
73 if stops_slice.len() < 2 {
74 lut.add_color(0.0, Rgba8::new(0, 0, 0, 0));
76 lut.add_color(1.0, Rgba8::new(0, 0, 0, 0));
77 lut.build_lut();
78 return lut;
79 }
80 for stop in stops_slice {
81 let offset = f64::from(stop.offset.normalized()); let c = resolve_color(&stop.color, system_colors);
83 lut.add_color(
84 offset,
85 Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a)),
86 );
87 }
88 lut.build_lut();
89 lut
90}
91
92fn build_gradient_lut_radial(
94 stops: &azul_css::props::style::background::NormalizedRadialColorStopVec,
95 system_colors: Option<&azul_css::system::SystemColors>,
96) -> GradientLut {
97 let mut lut = GradientLut::new_default();
98 let stops_slice = stops.as_ref();
99 if stops_slice.len() < 2 {
100 lut.add_color(0.0, Rgba8::new(0, 0, 0, 0));
101 lut.add_color(1.0, Rgba8::new(0, 0, 0, 0));
102 lut.build_lut();
103 return lut;
104 }
105 for stop in stops_slice {
106 let offset = f64::from((stop.angle.to_degrees_raw() / 360.0).clamp(0.0, 1.0));
114 let c = resolve_color(&stop.color, system_colors);
115 lut.add_color(
116 offset,
117 Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a)),
118 );
119 }
120 lut.build_lut();
121 lut
122}
123
124fn resolve_background_position(
126 pos: &azul_css::props::style::background::StyleBackgroundPosition,
127 width: f32,
128 height: f32,
129) -> (f32, f32) {
130 use azul_css::props::style::background::{
131 BackgroundPositionHorizontal, BackgroundPositionVertical,
132 };
133
134 let x = match pos.horizontal {
135 BackgroundPositionHorizontal::Left => 0.0,
136 BackgroundPositionHorizontal::Center => 0.5,
137 BackgroundPositionHorizontal::Right => 1.0,
138 BackgroundPositionHorizontal::Exact(px) => {
139 let val = px.to_pixels_internal(width, 16.0, 16.0);
140 if width > 0.0 {
141 val / width
142 } else {
143 0.5
144 }
145 }
146 };
147 let y = match pos.vertical {
148 BackgroundPositionVertical::Top => 0.0,
149 BackgroundPositionVertical::Center => 0.5,
150 BackgroundPositionVertical::Bottom => 1.0,
151 BackgroundPositionVertical::Exact(px) => {
152 let val = px.to_pixels_internal(height, 16.0, 16.0);
153 if height > 0.0 {
154 val / height
155 } else {
156 0.5
157 }
158 }
159 };
160 (x, y)
161}
162
163#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] fn render_linear_gradient(
165 pixmap: &mut AzulPixmap,
166 bounds: &LogicalRect,
167 gradient: &azul_css::props::style::background::LinearGradient,
168 border_radius: &BorderRadius,
169 clip: Option<AzRect>,
170 dpi_factor: f32,
171 system_colors: Option<&azul_css::system::SystemColors>,
172) {
173 use azul_css::props::basic::geometry::{LayoutRect, LayoutSize};
174
175 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
176 return;
177 };
178
179 let stops = gradient.stops.as_ref();
180 if stops.is_empty() {
181 return;
182 }
183
184 let lut = build_gradient_lut_linear(&gradient.stops, system_colors);
185
186 let layout_rect = LayoutRect {
188 origin: azul_css::props::basic::geometry::LayoutPoint::new(0, 0),
189 size: LayoutSize {
190 width: (rect.width as isize),
191 height: (rect.height as isize),
192 },
193 };
194 let (from_pt, to_pt) = gradient.direction.to_points(&layout_rect);
195
196 let x1 = f64::from(rect.x) + from_pt.x as f64;
198 let y1 = f64::from(rect.y) + from_pt.y as f64;
199 let x2 = f64::from(rect.x) + to_pt.x as f64;
200 let y2 = f64::from(rect.y) + to_pt.y as f64;
201
202 let dx = x2 - x1;
203 let dy = y2 - y1;
204 let len = dx.hypot(dy);
205 if len < 0.001 {
206 return;
207 }
208
209 let mut transform = TransAffine::new_line_segment(x1, y1, x2, y2, 100.0);
214 transform.invert();
215
216 let mut path = if border_radius.is_zero() {
217 build_rect_path(&rect)
218 } else {
219 build_rounded_rect_path(&rect, border_radius, dpi_factor)
220 };
221
222 agg_fill_gradient_clipped(
223 pixmap, &mut path, &lut, GradientX, transform, 0.0, 100.0, clip,
224 );
225}
226
227#[allow(clippy::suboptimal_flops)] #[allow(clippy::similar_names)] #[allow(clippy::match_same_arms)] fn render_radial_gradient(
231 pixmap: &mut AzulPixmap,
232 bounds: &LogicalRect,
233 gradient: &azul_css::props::style::background::RadialGradient,
234 border_radius: &BorderRadius,
235 clip: Option<AzRect>,
236 dpi_factor: f32,
237 system_colors: Option<&azul_css::system::SystemColors>,
238) {
239 use azul_css::props::style::background::{RadialGradientSize, Shape};
240
241 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
242 return;
243 };
244
245 let stops = gradient.stops.as_ref();
246 if stops.is_empty() {
247 return;
248 }
249
250 let lut = build_gradient_lut_linear(&gradient.stops, system_colors);
251
252 let w = f64::from(rect.width);
253 let h = f64::from(rect.height);
254
255 let (cx_frac, cy_frac) =
257 resolve_background_position(&gradient.position, rect.width, rect.height);
258 let cx = f64::from(rect.x) + f64::from(cx_frac) * w;
259 let cy = f64::from(rect.y) + f64::from(cy_frac) * h;
260
261 let radius = match gradient.size {
263 RadialGradientSize::ClosestSide => {
264 let dx = (f64::from(cx_frac) * w).min((1.0 - f64::from(cx_frac)) * w);
265 let dy = (f64::from(cy_frac) * h).min((1.0 - f64::from(cy_frac)) * h);
266 match gradient.shape {
267 Shape::Circle => dx.min(dy),
268 Shape::Ellipse => dx.min(dy), }
270 }
271 RadialGradientSize::FarthestSide => {
272 let dx = (f64::from(cx_frac) * w).max((1.0 - f64::from(cx_frac)) * w);
273 let dy = (f64::from(cy_frac) * h).max((1.0 - f64::from(cy_frac)) * h);
274 match gradient.shape {
275 Shape::Circle => dx.max(dy),
276 Shape::Ellipse => dx.max(dy),
277 }
278 }
279 RadialGradientSize::ClosestCorner => {
280 let dx = (f64::from(cx_frac) * w).min((1.0 - f64::from(cx_frac)) * w);
281 let dy = (f64::from(cy_frac) * h).min((1.0 - f64::from(cy_frac)) * h);
282 dx.hypot(dy)
283 }
284 RadialGradientSize::FarthestCorner => {
285 let dx = (f64::from(cx_frac) * w).max((1.0 - f64::from(cx_frac)) * w);
286 let dy = (f64::from(cy_frac) * h).max((1.0 - f64::from(cy_frac)) * h);
287 dx.hypot(dy)
288 }
289 };
290
291 if radius < 0.001 {
292 return;
293 }
294
295 let mut transform = TransAffine::new_scaling_uniform(radius / 100.0);
299 transform.translate(cx, cy);
300 transform.invert();
301
302 let mut path = if border_radius.is_zero() {
303 build_rect_path(&rect)
304 } else {
305 build_rounded_rect_path(&rect, border_radius, dpi_factor)
306 };
307
308 agg_fill_gradient_clipped(
309 pixmap,
310 &mut path,
311 &lut,
312 GradientRadialD,
313 transform,
314 0.0,
315 100.0,
316 clip,
317 );
318}
319
320#[allow(clippy::suboptimal_flops)] #[allow(clippy::similar_names)] fn render_conic_gradient(
323 pixmap: &mut AzulPixmap,
324 bounds: &LogicalRect,
325 gradient: &azul_css::props::style::background::ConicGradient,
326 border_radius: &BorderRadius,
327 clip: Option<AzRect>,
328 dpi_factor: f32,
329 system_colors: Option<&azul_css::system::SystemColors>,
330) {
331 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
332 return;
333 };
334
335 let stops = gradient.stops.as_ref();
336 if stops.is_empty() {
337 return;
338 }
339
340 let lut = build_gradient_lut_radial(&gradient.stops, system_colors);
341
342 let w = f64::from(rect.width);
343 let h = f64::from(rect.height);
344
345 let (cx_frac, cy_frac) = resolve_background_position(&gradient.center, rect.width, rect.height);
347 let cx = f64::from(rect.x) + f64::from(cx_frac) * w;
348 let cy = f64::from(rect.y) + f64::from(cy_frac) * h;
349
350 let start_angle_deg = gradient.angle.to_degrees();
352 let start_angle_rad = f64::from(start_angle_deg - 90.0).to_radians();
353
354 let mut transform = TransAffine::new_rotation(start_angle_rad);
358 transform.translate(cx, cy);
359 transform.invert();
360
361 let d2 = 100.0;
364
365 let mut path = if border_radius.is_zero() {
366 build_rect_path(&rect)
367 } else {
368 build_rounded_rect_path(&rect, border_radius, dpi_factor)
369 };
370
371 agg_fill_gradient_clipped(
372 pixmap,
373 &mut path,
374 &lut,
375 GradientConic,
376 transform,
377 0.0,
378 d2,
379 clip,
380 );
381}
382
383#[allow(clippy::suboptimal_flops)] #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] fn render_box_shadow(
390 pixmap: &mut AzulPixmap,
391 bounds: &LogicalRect,
392 shadow: &StyleBoxShadow,
393 border_radius: &BorderRadius,
394 dpi_factor: f32,
395) -> Result<(), String> {
396 use azul_css::props::style::box_shadow::BoxShadowClipMode;
397
398 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
399 return Ok(());
400 };
401
402 let offset_x =
403 shadow
404 .offset_x
405 .inner
406 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
407 * dpi_factor;
408 let offset_y =
409 shadow
410 .offset_y
411 .inner
412 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
413 * dpi_factor;
414 let blur_r =
415 (shadow
416 .blur_radius
417 .inner
418 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
419 * dpi_factor)
420 .max(0.0);
421 let spread =
422 shadow
423 .spread_radius
424 .inner
425 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
426 * dpi_factor;
427
428 let color = shadow.color;
429 if color.a == 0 {
430 return Ok(());
431 }
432
433 let padding = blur_r.ceil();
435 let shadow_x = rect.x + offset_x - spread - padding;
436 let shadow_y = rect.y + offset_y - spread - padding;
437 let shadow_w = rect.width + 2.0 * spread + 2.0 * padding;
438 let shadow_h = rect.height + 2.0 * spread + 2.0 * padding;
439
440 if shadow_w <= 0.0 || shadow_h <= 0.0 {
441 return Ok(());
442 }
443
444 let sw = shadow_w.ceil() as u32;
445 let sh = shadow_h.ceil() as u32;
446
447 if sw == 0 || sh == 0 || sw > MAX_SHADOW_PIXBUF_SIZE || sh > MAX_SHADOW_PIXBUF_SIZE {
448 return Ok(());
449 }
450
451 let mut tmp = AzulPixmap::new(sw, sh).ok_or("cannot create shadow pixmap")?;
453 tmp.fill(0, 0, 0, 0); let shape_x = padding + spread;
457 let shape_y = padding + spread;
458 let Some(shape_rect) = AzRect::from_xywh(shape_x, shape_y, rect.width, rect.height) else {
459 return Ok(());
460 };
461
462 let agg_color = Rgba8::new(
463 u32::from(color.r),
464 u32::from(color.g),
465 u32::from(color.b),
466 u32::from(color.a),
467 );
468 if border_radius.is_zero() {
469 let mut path = build_rect_path(&shape_rect);
470 agg_fill_path(&mut tmp, &mut path, &agg_color, FillingRule::NonZero);
471 } else {
472 let mut path = build_rounded_rect_path(&shape_rect, border_radius, dpi_factor);
473 agg_fill_path(&mut tmp, &mut path, &agg_color, FillingRule::NonZero);
474 }
475
476 if blur_r > 0.5 {
478 let blur_radius = (blur_r.ceil() as u32).min(254);
479 let stride = (sw * 4) as i32;
480 let mut ra = unsafe { RowAccessor::new_with_buf(tmp.data.as_mut_ptr(), sw, sh, stride) };
481 stack_blur_rgba32(&mut ra, blur_radius, blur_radius);
482 }
483
484 let dst_x = shadow_x as i32;
486 let dst_y = shadow_y as i32;
487 blit_buffer(pixmap, &tmp.data, sw, sh, dst_x, dst_y);
488
489 Ok(())
490}
491
492#[derive(Debug)]
494pub enum MaskEntry {
495 ImageMask {
497 snapshot: Vec<u8>,
498 mask_data: Vec<u8>,
499 origin_x: i32,
500 origin_y: i32,
501 width: u32,
502 height: u32,
503 },
504 Opacity {
506 snapshot: Vec<u8>,
507 rect: AzRect,
508 opacity: f32,
509 },
510}
511
512#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::cast_sign_loss)] fn extract_mask_data(mask_image: &ImageRef, target_w: u32, target_h: u32) -> Option<Vec<u8>> {
515 let image_data = mask_image.get_data();
516 let (mask_bytes, src_w, src_h) = match image_data {
517 DecodedImage::Raw((descriptor, data)) => {
518 let w = descriptor.width as u32;
519 let h = descriptor.height as u32;
520 if w == 0 || h == 0 {
521 return None;
522 }
523 let bytes = match data {
524 azul_core::resources::ImageData::Raw(shared) => shared.as_ref(),
525 azul_core::resources::ImageData::External(_) => return None,
526 };
527 match descriptor.format {
528 azul_core::resources::RawImageFormat::R8 => (bytes.to_vec(), w, h),
529 azul_core::resources::RawImageFormat::BGRA8 => {
530 let mut r8 = Vec::with_capacity((w * h) as usize);
532 for chunk in bytes.chunks_exact(4) {
533 r8.push(chunk[3]); }
535 (r8, w, h)
536 }
537 _ => {
538 let chan_count = bytes.len() / (w * h) as usize;
540 if chan_count == 0 {
541 return None;
542 }
543 let mut r8 = Vec::with_capacity((w * h) as usize);
544 for i in 0..(w * h) as usize {
545 r8.push(bytes[i * chan_count]);
546 }
547 (r8, w, h)
548 }
549 }
550 }
551 _ => return None,
552 };
553
554 if target_w == 0 || target_h == 0 {
555 return None;
556 }
557
558 let mut scaled = vec![0u8; (target_w * target_h) as usize];
560 let sx = src_w as f32 / target_w as f32;
561 let sy = src_h as f32 / target_h as f32;
562 for py in 0..target_h {
563 for px in 0..target_w {
564 let mx = ((px as f32 * sx) as u32).min(src_w - 1);
565 let my = ((py as f32 * sy) as u32).min(src_h - 1);
566 scaled[(py * target_w + px) as usize] = mask_bytes[(my * src_w + mx) as usize];
567 }
568 }
569 Some(scaled)
570}
571
572#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] fn apply_mask(pixmap: &mut AzulPixmap, entry: &MaskEntry) {
576 let (snapshot, mask_data, origin_x, origin_y, width, height) = match entry {
577 MaskEntry::ImageMask {
578 snapshot,
579 mask_data,
580 origin_x,
581 origin_y,
582 width,
583 height,
584 } => (
585 snapshot,
586 mask_data.as_slice(),
587 *origin_x,
588 *origin_y,
589 *width,
590 *height,
591 ),
592 MaskEntry::Opacity{ .. } => return,
593 };
594
595 let pw = pixmap.width as i32;
596 let ph = pixmap.height as i32;
597
598 for py in 0..height as i32 {
599 let dy = origin_y + py;
600 if dy < 0 || dy >= ph {
601 continue;
602 }
603 for px in 0..width as i32 {
604 let dx = origin_x + px;
605 if dx < 0 || dx >= pw {
606 continue;
607 }
608
609 let mi = (py as u32 * width + px as u32) as usize;
610 let mask_val = u32::from(mask_data.get(mi).copied().unwrap_or(0));
611
612 let pi = ((dy as u32 * pixmap.width + dx as u32) * 4) as usize;
613 let si = ((py as u32 * width + px as u32) * 4) as usize;
614
615 if pi + 3 >= pixmap.data.len() || si + 3 >= snapshot.len() {
616 continue;
617 }
618
619 let inv_mask = 255 - mask_val;
622 for c in 0..4 {
623 let snap_c = u32::from(snapshot[si + c]);
624 let cur_c = u32::from(pixmap.data[pi + c]);
625 pixmap.data[pi + c] = ((cur_c * mask_val + snap_c * inv_mask) / 255) as u8;
626 }
627 }
628 }
629}
630
631#[derive(Debug, Clone, Copy)]
636pub struct RenderOptions {
637 pub width: f32,
638 pub height: f32,
639 pub dpi_factor: f32,
640}
641
642fn acquire_pixmap(retained: Option<AzulPixmap>, w: u32, h: u32) -> Result<AzulPixmap, String> {
644 if let Some(p) = retained {
645 if p.width == w && p.height == h {
646 return Ok(p);
647 }
648 }
649 AzulPixmap::new(w, h).ok_or_else(|| "cannot create pixmap".to_string())
650}
651
652#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub fn render(
661 dl: &DisplayList,
662 res: &RendererResources,
663 font_manager: &FontManager<FontRef>,
664 opts: RenderOptions,
665 glyph_cache: &mut GlyphCache,
666) -> Result<AzulPixmap, String> {
667 let RenderOptions {
668 width,
669 height,
670 dpi_factor,
671 } = opts;
672
673 let mut pixmap = acquire_pixmap(
674 None,
675 (width * dpi_factor) as u32,
676 (height * dpi_factor) as u32,
677 )?;
678 pixmap.fill(255, 255, 255, 255);
679
680 render_display_list(dl, &mut pixmap, dpi_factor, res, font_manager, glyph_cache)?;
681
682 Ok(pixmap)
683}
684
685pub fn render_with_font_manager(
691 dl: &DisplayList,
692 res: &RendererResources,
693 font_manager: &FontManager<FontRef>,
694 opts: RenderOptions,
695 glyph_cache: &mut GlyphCache,
696) -> Result<AzulPixmap, String> {
697 let empty_state = CpuRenderState::new(ScrollOffsetMap::new());
698 render_with_font_manager_and_scroll(dl, res, font_manager, opts, glyph_cache, &empty_state)
699}
700
701pub fn render_with_font_manager_and_scroll(
707 dl: &DisplayList,
708 res: &RendererResources,
709 font_manager: &FontManager<FontRef>,
710 opts: RenderOptions,
711 glyph_cache: &mut GlyphCache,
712 render_state: &CpuRenderState,
713) -> Result<AzulPixmap, String> {
714 render_with_font_manager_and_scroll_retained(
715 dl,
716 res,
717 font_manager,
718 opts,
719 glyph_cache,
720 render_state,
721 None,
722 )
723}
724
725#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub fn render_with_font_manager_and_scroll_retained(
733 dl: &DisplayList,
734 res: &RendererResources,
735 font_manager: &FontManager<FontRef>,
736 opts: RenderOptions,
737 glyph_cache: &mut GlyphCache,
738 render_state: &CpuRenderState,
739 retained: Option<AzulPixmap>,
740) -> Result<AzulPixmap, String> {
741 let RenderOptions {
742 width,
743 height,
744 dpi_factor,
745 } = opts;
746
747 let pw = (width * dpi_factor) as u32;
748 let ph = (height * dpi_factor) as u32;
749 let mut pixmap = acquire_pixmap(retained, pw, ph)?;
750 pixmap.fill(255, 255, 255, 255);
751
752 render_display_list_with_state(
753 dl,
754 &mut pixmap,
755 dpi_factor,
756 res,
757 font_manager,
758 glyph_cache,
759 render_state,
760 )?;
761
762 Ok(pixmap)
763}
764
765pub type ScrollOffsetMap = HashMap<LocalScrollId, (f32, f32)>;
769
770#[derive(Debug)]
776pub struct CpuRenderState {
777 pub scroll_offsets: ScrollOffsetMap,
779 pub transforms: HashMap<usize, azul_core::transform::ComputedTransform3D>,
782 pub opacities: HashMap<usize, f32>,
786 pub system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
790 pub virtual_view_display_lists:
796 std::collections::BTreeMap<azul_core::dom::DomId, std::sync::Arc<DisplayList>>,
797 pub image_callback_results:
804 std::collections::BTreeMap<azul_core::resources::ImageRefHash, ImageRef>,
805}
806
807impl CpuRenderState {
808 #[must_use] pub fn new(scroll_offsets: ScrollOffsetMap) -> Self {
809 Self {
810 scroll_offsets,
811 transforms: HashMap::new(),
812 opacities: HashMap::new(),
813 system_style: None,
814 virtual_view_display_lists: std::collections::BTreeMap::new(),
815 image_callback_results: std::collections::BTreeMap::new(),
816 }
817 }
818
819 #[must_use] pub fn with_image_callback_results(
821 mut self,
822 results: std::collections::BTreeMap<
823 azul_core::resources::ImageRefHash,
824 ImageRef,
825 >,
826 ) -> Self {
827 self.image_callback_results = results;
828 self
829 }
830
831 #[must_use] pub fn with_virtual_view_display_lists(
834 mut self,
835 lists: std::collections::BTreeMap<azul_core::dom::DomId, std::sync::Arc<DisplayList>>,
836 ) -> Self {
837 self.virtual_view_display_lists = lists;
838 self
839 }
840
841 #[must_use] pub fn with_system_style(
844 mut self,
845 system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
846 ) -> Self {
847 self.system_style = system_style;
848 self
849 }
850
851 #[must_use] pub fn from_gpu_cache(
853 gpu_cache: Option<&azul_core::gpu::GpuValueCache>,
854 dom_id: azul_core::dom::DomId,
855 scroll_offsets: &ScrollOffsetMap,
856 ) -> Self {
857 let (transforms, opacities) = extract_gpu_values(gpu_cache, dom_id);
858 Self {
859 scroll_offsets: scroll_offsets.clone(),
860 transforms,
861 opacities,
862 system_style: None,
863 virtual_view_display_lists: std::collections::BTreeMap::new(),
864 image_callback_results: std::collections::BTreeMap::new(),
865 }
866 }
867}
868
869#[must_use] pub fn extract_gpu_values(
877 gpu_cache: Option<&azul_core::gpu::GpuValueCache>,
878 dom_id: azul_core::dom::DomId,
879) -> (
880 HashMap<usize, azul_core::transform::ComputedTransform3D>,
881 HashMap<usize, f32>,
882) {
883 {
884 let mut transforms = HashMap::new();
885 let mut opacities = HashMap::new();
886
887 if let Some(cache) = gpu_cache {
888 for (node_id, key) in &cache.transform_keys {
890 if let Some(value) = cache.current_transform_values.get(node_id) {
891 transforms.insert(key.id, *value);
892 }
893 }
894 for (node_id, key) in &cache.h_transform_keys {
896 if let Some(value) = cache.h_current_transform_values.get(node_id) {
897 transforms.insert(key.id, *value);
898 }
899 }
900 for (node_id, key) in &cache.css_transform_keys {
902 if let Some(value) = cache.css_current_transform_values.get(node_id) {
903 transforms.insert(key.id, *value);
904 }
905 }
906 for ((d, node_id), key) in &cache.scrollbar_v_opacity_keys {
908 if *d == dom_id {
909 if let Some(&value) = cache.scrollbar_v_opacity_values.get(&(*d, *node_id)) {
910 opacities.insert(key.id, value);
911 }
912 }
913 }
914 for ((d, node_id), key) in &cache.scrollbar_h_opacity_keys {
916 if *d == dom_id {
917 if let Some(&value) = cache.scrollbar_h_opacity_values.get(&(*d, *node_id)) {
918 opacities.insert(key.id, value);
919 }
920 }
921 }
922 for (node_id, key) in &cache.opacity_keys {
924 if let Some(&value) = cache.current_opacity_values.get(node_id) {
925 opacities.insert(key.id, value);
926 }
927 }
928 }
929
930 (transforms, opacities)
931 }
932}
933
934fn render_display_list(
935 display_list: &DisplayList,
936 pixmap: &mut AzulPixmap,
937 dpi_factor: f32,
938 renderer_resources: &RendererResources,
939 font_manager: &FontManager<FontRef>,
940 glyph_cache: &mut GlyphCache,
941) -> Result<(), String> {
942 let empty_state = CpuRenderState::new(ScrollOffsetMap::new());
943 render_display_list_with_state(
944 display_list,
945 pixmap,
946 dpi_factor,
947 renderer_resources,
948 font_manager,
949 glyph_cache,
950 &empty_state,
951 )
952}
953
954fn render_display_list_with_state(
955 display_list: &DisplayList,
956 pixmap: &mut AzulPixmap,
957 dpi_factor: f32,
958 renderer_resources: &RendererResources,
959 font_manager: &FontManager<FontRef>,
960 glyph_cache: &mut GlyphCache,
961 render_state: &CpuRenderState,
962) -> Result<(), String> {
963 let mut transform_stack = vec![TransAffine::new()]; let mut clip_stack: Vec<Option<AzRect>> = vec![None];
965 let mut mask_stack: Vec<MaskEntry> = Vec::new();
966 let mut scroll_offset_stack: Vec<(f32, f32)> = vec![(0.0, 0.0)];
971 let mut text_shadow_stack: Vec<StyleBoxShadow> = Vec::new();
972
973 let _p_loop = crate::probe::Probe::span("raster_loop");
974 for item in &display_list.items {
975 let _p_item = crate::probe::Probe::span(probe_label_for_item(item));
976 render_single_item(
977 item,
978 pixmap,
979 dpi_factor,
980 renderer_resources,
981 font_manager,
982 glyph_cache,
983 &mut transform_stack,
984 &mut clip_stack,
985 &mut mask_stack,
986 &mut scroll_offset_stack,
987 &mut text_shadow_stack,
988 render_state,
989 )?;
990 }
991
992 Ok(())
993}
994
995#[inline]
999const fn probe_label_for_item(item: &DisplayListItem) -> &'static str {
1000 use crate::solver3::display_list::DisplayListItem as I;
1001 match item {
1002 I::Rect { .. } => "dl:rect",
1003 I::SelectionRect { .. } => "dl:sel_rect",
1004 I::CursorRect { .. } => "dl:cursor",
1005 I::Border { .. } => "dl:border",
1006 I::Text { .. } => "dl:text",
1007 I::TextLayout { .. } => "dl:text_layout",
1008 I::Image { .. } => "dl:image",
1009 I::ScrollBar { .. } => "dl:scrollbar_raw",
1010 I::ScrollBarStyled { .. } => "dl:scrollbar",
1011 I::PushClip { .. } => "dl:push_clip",
1012 I::PopClip => "dl:pop_clip",
1013 I::PushScrollFrame { .. } => "dl:push_scroll",
1014 I::PopScrollFrame => "dl:pop_scroll",
1015 I::PushStackingContext { .. } => "dl:push_stack",
1016 I::PopStackingContext => "dl:pop_stack",
1017 I::PushReferenceFrame { .. } => "dl:push_ref",
1018 I::PopReferenceFrame => "dl:pop_ref",
1019 I::PushOpacity { .. } => "dl:push_opacity",
1020 I::PopOpacity => "dl:pop_opacity",
1021 I::PushFilter { .. } => "dl:push_filter",
1022 I::PopFilter => "dl:pop_filter",
1023 I::PushBackdropFilter { .. } => "dl:push_bdfilter",
1024 I::PopBackdropFilter => "dl:pop_bdfilter",
1025 I::PushTextShadow { .. } => "dl:push_tshadow",
1026 I::PopTextShadow => "dl:pop_tshadow",
1027 I::PushImageMaskClip { .. } => "dl:push_imask",
1028 I::PopImageMaskClip => "dl:pop_imask",
1029 I::LinearGradient { .. } => "dl:linear_grad",
1030 I::RadialGradient { .. } => "dl:radial_grad",
1031 I::ConicGradient { .. } => "dl:conic_grad",
1032 I::BoxShadow { .. } => "dl:box_shadow",
1033 I::Underline { .. } => "dl:underline",
1034 I::Strikethrough { .. } => "dl:strike",
1035 I::Overline { .. } => "dl:overline",
1036 I::HitTestArea { .. } => "dl:hit",
1037 I::VirtualView { .. } => "dl:vview",
1038 I::VirtualViewPlaceholder { .. } => "dl:vview_ph",
1039 }
1040}
1041
1042#[allow(clippy::cast_possible_truncation)] #[allow(clippy::similar_names)] #[allow(clippy::cast_possible_wrap, clippy::cast_precision_loss)] #[allow(clippy::too_many_lines)] pub fn render_display_list_damaged(
1061 display_list: &DisplayList,
1062 pixmap: &mut AzulPixmap,
1063 dpi_factor: f32,
1064 renderer_resources: &RendererResources,
1065 font_manager: &FontManager<FontRef>,
1066 glyph_cache: &mut GlyphCache,
1067 render_state: &CpuRenderState,
1068 damage_rects: &[LogicalRect],
1069) -> Result<(), String> {
1070 struct SnappedRect {
1074 x0: i32,
1075 y0: i32,
1076 x1: i32,
1077 y1: i32,
1078 logical: LogicalRect,
1079 }
1080
1081 if damage_rects.is_empty() {
1082 return Ok(()); }
1084
1085 let pw_i = pixmap.width() as i32;
1093 let ph_i = pixmap.height() as i32;
1094 let snap_out = |dr: &LogicalRect| -> Option<SnappedRect> {
1095 let x0 = ((dr.origin.x * dpi_factor).floor() as i32).clamp(0, pw_i);
1096 let y0 = ((dr.origin.y * dpi_factor).floor() as i32).clamp(0, ph_i);
1097 let x1 = (((dr.origin.x + dr.size.width) * dpi_factor).ceil() as i32).clamp(0, pw_i);
1098 let y1 = (((dr.origin.y + dr.size.height) * dpi_factor).ceil() as i32).clamp(0, ph_i);
1099 if x1 <= x0 || y1 <= y0 {
1100 return None;
1101 }
1102 Some(SnappedRect {
1103 x0,
1104 y0,
1105 x1,
1106 y1,
1107 logical: LogicalRect {
1108 origin: LogicalPosition {
1109 x: x0 as f32 / dpi_factor,
1110 y: y0 as f32 / dpi_factor,
1111 },
1112 size: LogicalSize {
1113 width: (x1 - x0) as f32 / dpi_factor,
1114 height: (y1 - y0) as f32 / dpi_factor,
1115 },
1116 },
1117 })
1118 };
1119 let mut rects: Vec<SnappedRect> = damage_rects.iter().filter_map(snap_out).collect();
1120
1121 let mut i = 0;
1127 while i < rects.len() {
1128 let mut j = i + 1;
1129 let mut merged_any = false;
1130 while j < rects.len() {
1131 let (a, b) = (&rects[i], &rects[j]);
1132 let overlap = a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;
1133 if overlap {
1134 let x0 = a.x0.min(b.x0);
1135 let y0 = a.y0.min(b.y0);
1136 let x1 = a.x1.max(b.x1);
1137 let y1 = a.y1.max(b.y1);
1138 rects[i] = SnappedRect {
1139 x0,
1140 y0,
1141 x1,
1142 y1,
1143 logical: LogicalRect {
1144 origin: LogicalPosition {
1145 x: x0 as f32 / dpi_factor,
1146 y: y0 as f32 / dpi_factor,
1147 },
1148 size: LogicalSize {
1149 width: (x1 - x0) as f32 / dpi_factor,
1150 height: (y1 - y0) as f32 / dpi_factor,
1151 },
1152 },
1153 };
1154 rects.swap_remove(j);
1155 merged_any = true;
1156 } else {
1159 j += 1;
1160 }
1161 }
1162 if merged_any {
1163 if rects.len() > 1 {
1165 continue;
1166 }
1167 }
1168 i += 1;
1169 }
1170
1171 for sr in &rects {
1182 pixmap.fill_rect(
1183 sr.x0,
1184 sr.y0,
1185 sr.x1 - sr.x0,
1186 sr.y1 - sr.y0,
1187 255,
1188 255,
1189 255,
1190 255,
1191 );
1192
1193 let base_clip = AzRect::from_xywh(
1194 sr.x0 as f32,
1195 sr.y0 as f32,
1196 (sr.x1 - sr.x0) as f32,
1197 (sr.y1 - sr.y0) as f32,
1198 );
1199 let mut transform_stack = vec![TransAffine::new()];
1200 let mut clip_stack: Vec<Option<AzRect>> = vec![base_clip];
1201 let mut mask_stack: Vec<MaskEntry> = Vec::new();
1202 let mut scroll_offset_stack: Vec<(f32, f32)> = vec![(0.0, 0.0)];
1203 let mut text_shadow_stack: Vec<StyleBoxShadow> = Vec::new();
1204
1205 for item in &display_list.items {
1206 if !item.is_state_management() {
1209 if let Some(item_bounds) = item.bounds() {
1210 let (sdx, sdy) = *scroll_offset_stack.last().unwrap_or(&(0.0, 0.0));
1217 let test_bounds = if sdx == 0.0 && sdy == 0.0 {
1218 item_bounds
1219 } else {
1220 LogicalRect {
1221 origin: LogicalPosition {
1222 x: item_bounds.origin.x - sdx,
1223 y: item_bounds.origin.y - sdy,
1224 },
1225 size: item_bounds.size,
1226 }
1227 };
1228 if !rects_overlap_or_adjacent(&test_bounds, &sr.logical, 0.0) {
1229 continue;
1230 }
1231 }
1232 }
1233
1234 render_single_item(
1235 item,
1236 pixmap,
1237 dpi_factor,
1238 renderer_resources,
1239 font_manager,
1240 glyph_cache,
1241 &mut transform_stack,
1242 &mut clip_stack,
1243 &mut mask_stack,
1244 &mut scroll_offset_stack,
1245 &mut text_shadow_stack,
1246 render_state,
1247 )?;
1248 }
1249 }
1250
1251 Ok(())
1252}
1253
1254#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] #[allow(clippy::similar_names)] #[allow(clippy::float_cmp)] #[allow(clippy::match_same_arms)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn render_single_item(
1266 item: &DisplayListItem,
1267 pixmap: &mut AzulPixmap,
1268 dpi_factor: f32,
1269 renderer_resources: &RendererResources,
1270 font_manager: &FontManager<FontRef>,
1271 glyph_cache: &mut GlyphCache,
1272 transform_stack: &mut Vec<TransAffine>,
1273 clip_stack: &mut Vec<Option<AzRect>>,
1274 mask_stack: &mut Vec<MaskEntry>,
1275 scroll_offset_stack: &mut Vec<(f32, f32)>,
1276 text_shadow_stack: &mut Vec<StyleBoxShadow>,
1277 render_state: &CpuRenderState,
1278) -> Result<(), String> {
1279 use azul_css::props::style::border::BorderStyle;
1280 let (scroll_dx, scroll_dy) = *scroll_offset_stack.last().unwrap_or(&(0.0, 0.0));
1283
1284 let scroll_rect = |r: &LogicalRect| -> LogicalRect {
1289 if scroll_dx == 0.0 && scroll_dy == 0.0 {
1290 return *r;
1291 }
1292 LogicalRect {
1293 origin: LogicalPosition {
1294 x: r.origin.x - scroll_dx,
1295 y: r.origin.y - scroll_dy,
1296 },
1297 size: r.size,
1298 }
1299 };
1300
1301 match item {
1302 DisplayListItem::Rect {
1303 bounds,
1304 color,
1305 border_radius,
1306 } => {
1307 let clip = *clip_stack.last().unwrap();
1308 render_rect(
1309 pixmap,
1310 &scroll_rect(bounds.inner()),
1311 *color,
1312 border_radius,
1313 clip,
1314 dpi_factor,
1315 );
1316 }
1317 DisplayListItem::SelectionRect {
1318 bounds,
1319 color,
1320 border_radius,
1321 } => {
1322 let clip = *clip_stack.last().unwrap();
1323 render_rect(
1324 pixmap,
1325 &scroll_rect(bounds.inner()),
1326 *color,
1327 border_radius,
1328 clip,
1329 dpi_factor,
1330 );
1331 }
1332 DisplayListItem::CursorRect { bounds, color } => {
1333 let clip = *clip_stack.last().unwrap();
1334 render_rect(
1335 pixmap,
1336 &scroll_rect(bounds.inner()),
1337 *color,
1338 &BorderRadius::default(),
1339 clip,
1340 dpi_factor,
1341 );
1342 }
1343 DisplayListItem::Border {
1344 bounds,
1345 widths,
1346 colors,
1347 styles,
1348 border_radius,
1349 } => {
1350 let default_color = ColorU {
1351 r: 0,
1352 g: 0,
1353 b: 0,
1354 a: 255,
1355 };
1356
1357 let w_top = widths
1358 .top
1359 .and_then(|w| w.get_property().copied())
1360 .map_or(0.0, |w| {
1361 w.inner
1362 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
1363 });
1364 let w_right = widths
1365 .right
1366 .and_then(|w| w.get_property().copied())
1367 .map_or(0.0, |w| {
1368 w.inner
1369 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
1370 });
1371 let w_bottom = widths
1372 .bottom
1373 .and_then(|w| w.get_property().copied())
1374 .map_or(0.0, |w| {
1375 w.inner
1376 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
1377 });
1378 let w_left = widths
1379 .left
1380 .and_then(|w| w.get_property().copied())
1381 .map_or(0.0, |w| {
1382 w.inner
1383 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
1384 });
1385
1386 let c_top = colors
1387 .top
1388 .and_then(|c| c.get_property().copied())
1389 .map_or(default_color, |c| c.inner);
1390 let c_right = colors
1391 .right
1392 .and_then(|c| c.get_property().copied())
1393 .map_or(default_color, |c| c.inner);
1394 let c_bottom = colors
1395 .bottom
1396 .and_then(|c| c.get_property().copied())
1397 .map_or(default_color, |c| c.inner);
1398 let c_left = colors
1399 .left
1400 .and_then(|c| c.get_property().copied())
1401 .map_or(default_color, |c| c.inner);
1402
1403 let s_top = styles
1404 .top
1405 .and_then(|s| s.get_property().copied())
1406 .map_or(BorderStyle::Solid, |s| s.inner);
1407 let s_right = styles
1408 .right
1409 .and_then(|s| s.get_property().copied())
1410 .map_or(BorderStyle::Solid, |s| s.inner);
1411 let s_bottom = styles
1412 .bottom
1413 .and_then(|s| s.get_property().copied())
1414 .map_or(BorderStyle::Solid, |s| s.inner);
1415 let s_left = styles
1416 .left
1417 .and_then(|s| s.get_property().copied())
1418 .map_or(BorderStyle::Solid, |s| s.inner);
1419
1420 let simple_radius = BorderRadius {
1421 top_left: border_radius.top_left.to_pixels_internal(
1422 bounds.0.size.width,
1423 DEFAULT_FONT_SIZE,
1424 DEFAULT_FONT_SIZE,
1425 ),
1426 top_right: border_radius.top_right.to_pixels_internal(
1427 bounds.0.size.width,
1428 DEFAULT_FONT_SIZE,
1429 DEFAULT_FONT_SIZE,
1430 ),
1431 bottom_left: border_radius.bottom_left.to_pixels_internal(
1432 bounds.0.size.width,
1433 DEFAULT_FONT_SIZE,
1434 DEFAULT_FONT_SIZE,
1435 ),
1436 bottom_right: border_radius.bottom_right.to_pixels_internal(
1437 bounds.0.size.width,
1438 DEFAULT_FONT_SIZE,
1439 DEFAULT_FONT_SIZE,
1440 ),
1441 };
1442
1443 let clip = *clip_stack.last().unwrap();
1444 let b = scroll_rect(bounds.inner());
1445
1446 let all_same = c_top == c_right
1448 && c_top == c_bottom
1449 && c_top == c_left
1450 && w_top == w_right
1451 && w_top == w_bottom
1452 && w_top == w_left
1453 && s_top == s_right
1454 && s_top == s_bottom
1455 && s_top == s_left;
1456
1457 if all_same {
1458 render_border(
1459 pixmap,
1460 &b,
1461 c_top,
1462 w_top,
1463 s_top,
1464 &simple_radius,
1465 clip,
1466 dpi_factor,
1467 );
1468 } else {
1469 render_border_sides(
1471 pixmap,
1472 &b,
1473 [c_top, c_right, c_bottom, c_left],
1474 [w_top, w_right, w_bottom, w_left],
1475 [s_top, s_right, s_bottom, s_left],
1476 &simple_radius,
1477 clip,
1478 dpi_factor,
1479 );
1480 }
1481 }
1482 DisplayListItem::Underline {
1483 bounds,
1484 color,
1485 thickness: _,
1486 } => {
1487 let clip = *clip_stack.last().unwrap();
1488 render_rect(
1489 pixmap,
1490 &scroll_rect(bounds.inner()),
1491 *color,
1492 &BorderRadius::default(),
1493 clip,
1494 dpi_factor,
1495 );
1496 }
1497 DisplayListItem::Strikethrough {
1498 bounds,
1499 color,
1500 thickness: _,
1501 } => {
1502 let clip = *clip_stack.last().unwrap();
1503 render_rect(
1504 pixmap,
1505 &scroll_rect(bounds.inner()),
1506 *color,
1507 &BorderRadius::default(),
1508 clip,
1509 dpi_factor,
1510 );
1511 }
1512 DisplayListItem::Overline {
1513 bounds,
1514 color,
1515 thickness: _,
1516 } => {
1517 let clip = *clip_stack.last().unwrap();
1518 render_rect(
1519 pixmap,
1520 &scroll_rect(bounds.inner()),
1521 *color,
1522 &BorderRadius::default(),
1523 clip,
1524 dpi_factor,
1525 );
1526 }
1527 DisplayListItem::Text {
1528 glyphs,
1529 font_size_px,
1530 font_hash,
1531 color,
1532 clip_rect,
1533 ..
1534 } => {
1535 let clip = *clip_stack.last().unwrap();
1536 let text_clip = scroll_rect(clip_rect.inner());
1537 for shadow in text_shadow_stack.iter() {
1542 render_text_shadow(
1543 shadow,
1544 glyphs,
1545 *font_hash,
1546 *font_size_px,
1547 pixmap,
1548 &text_clip,
1549 clip,
1550 renderer_resources,
1551 font_manager,
1552 dpi_factor,
1553 glyph_cache,
1554 (scroll_dx, scroll_dy),
1555 );
1556 }
1557 render_text(
1558 glyphs,
1559 *font_hash,
1560 *font_size_px,
1561 *color,
1562 pixmap,
1563 &text_clip,
1564 clip,
1565 renderer_resources,
1566 font_manager,
1567 dpi_factor,
1568 glyph_cache,
1569 (scroll_dx, scroll_dy),
1570 false,
1571 );
1572 }
1573 DisplayListItem::TextLayout {
1574 layout,
1575 bounds,
1576 font_hash,
1577 font_size_px,
1578 color,
1579 } => {
1580 }
1582 DisplayListItem::Image { bounds, image, .. } => {
1583 let clip = *clip_stack.last().unwrap();
1584 let resolved = render_state.image_callback_results.get(&image.get_hash());
1590 render_image(
1591 pixmap,
1592 &scroll_rect(bounds.inner()),
1593 resolved.unwrap_or(image),
1594 clip,
1595 dpi_factor,
1596 );
1597 }
1598 DisplayListItem::ScrollBar {
1599 bounds,
1600 color,
1601 orientation,
1602 opacity_key: _,
1603 hit_id: _,
1604 } => {
1605 let clip = *clip_stack.last().unwrap();
1606 render_rect(
1607 pixmap,
1608 &scroll_rect(bounds.inner()),
1609 *color,
1610 &BorderRadius::default(),
1611 clip,
1612 dpi_factor,
1613 );
1614 }
1615 DisplayListItem::ScrollBarStyled { info } => {
1616 let clip = *clip_stack.last().unwrap();
1617
1618 let scrollbar_opacity = info
1624 .opacity_key
1625 .and_then(|key| render_state.opacities.get(&key.id).copied())
1626 .unwrap_or(1.0);
1627
1628 if scrollbar_opacity > 0.001 {
1629 if info.track_color.a > 0 {
1631 render_rect(
1632 pixmap,
1633 &scroll_rect(info.track_bounds.inner()),
1634 info.track_color,
1635 &BorderRadius::default(),
1636 clip,
1637 dpi_factor,
1638 );
1639 }
1640
1641 if let Some(btn_bounds) = &info.button_decrement_bounds {
1643 if info.button_color.a > 0 {
1644 render_rect(
1645 pixmap,
1646 &scroll_rect(btn_bounds.inner()),
1647 info.button_color,
1648 &BorderRadius::default(),
1649 clip,
1650 dpi_factor,
1651 );
1652 }
1653 }
1654
1655 if let Some(btn_bounds) = &info.button_increment_bounds {
1657 if info.button_color.a > 0 {
1658 render_rect(
1659 pixmap,
1660 &scroll_rect(btn_bounds.inner()),
1661 info.button_color,
1662 &BorderRadius::default(),
1663 clip,
1664 dpi_factor,
1665 );
1666 }
1667 }
1668
1669 if info.thumb_color.a > 0 {
1674 let thumb_rect = info.thumb_bounds.inner();
1675 let transform = info
1677 .thumb_transform_key
1678 .and_then(|key| render_state.transforms.get(&key.id))
1679 .unwrap_or(&info.thumb_initial_transform);
1680 let tx = transform.m[3][0];
1681 let ty = transform.m[3][1];
1682 let transformed_thumb = LogicalRect {
1683 origin: LogicalPosition {
1684 x: thumb_rect.origin.x + tx,
1685 y: thumb_rect.origin.y + ty,
1686 },
1687 size: thumb_rect.size,
1688 };
1689 render_rect(
1690 pixmap,
1691 &scroll_rect(&transformed_thumb),
1692 info.thumb_color,
1693 &info.thumb_border_radius,
1694 clip,
1695 dpi_factor,
1696 );
1697 }
1698 } }
1700 DisplayListItem::PushClip {
1701 bounds,
1702 border_radius,
1703 } => {
1704 let new_clip = logical_rect_to_az_rect(&scroll_rect(bounds.inner()), dpi_factor);
1714 let new_clip = Some(new_clip.unwrap_or(AzRect::DENY_ALL));
1719 let merged = intersect_clips(clip_stack.last().copied().flatten(), new_clip);
1720 clip_stack.push(merged);
1721 }
1722 DisplayListItem::PopClip => {
1723 if clip_stack.len() > 1 {
1733 clip_stack.pop();
1734 } else {
1735 #[cfg(feature = "std")]
1736 if std::env::var("AZ_CLIP_DEBUG").is_ok() {
1737 eprintln!(
1738 "[CpuBackend] PopClip with no matching PushClip — clamping to base clip"
1739 );
1740 }
1741 }
1742 }
1743 DisplayListItem::PushScrollFrame { scroll_id, .. } => {
1744 transform_stack.push(
1749 transform_stack
1750 .last()
1751 .copied()
1752 .unwrap_or_else(TransAffine::new),
1753 );
1754 let frame_offset = render_state
1755 .scroll_offsets
1756 .get(scroll_id)
1757 .copied()
1758 .unwrap_or((0.0, 0.0));
1759 let new_scroll = (scroll_dx + frame_offset.0, scroll_dy + frame_offset.1);
1760 scroll_offset_stack.push(new_scroll);
1761 }
1762 DisplayListItem::PopScrollFrame => {
1763 if transform_stack.len() > 1 {
1766 transform_stack.pop();
1767 }
1768 if scroll_offset_stack.len() > 1 {
1769 scroll_offset_stack.pop();
1770 }
1771 }
1772 DisplayListItem::HitTestArea { bounds, tag } => {
1773 }
1775 DisplayListItem::PushStackingContext { z_index, bounds } => {
1776 }
1778 DisplayListItem::PopStackingContext => {}
1779 DisplayListItem::VirtualView {
1780 child_dom_id,
1781 bounds,
1782 clip_rect,
1783 } => {
1784 let _ = clip_rect;
1785 let child_dl = render_state.virtual_view_display_lists.get(child_dom_id).cloned();
1794 #[cfg(feature = "std")]
1795 if std::env::var("AZ_MAP_DEBUG").is_ok() {
1796 eprintln!(
1797 "[cpu-vview] VirtualView item: child_dom_id={} found={} items={} bounds={:?} avail_ids={:?}",
1798 child_dom_id.inner,
1799 child_dl.is_some(),
1800 child_dl.as_ref().map_or(0, |d| d.items.len()),
1801 bounds.inner(),
1802 render_state.virtual_view_display_lists.keys().map(|k| k.inner).collect::<Vec<_>>(),
1803 );
1804 }
1805 if let Some(child_dl) = child_dl {
1806 let vv_origin = bounds.inner().origin;
1807 let vv_clip = intersect_clips(
1810 clip_stack.last().copied().flatten(),
1811 logical_rect_to_az_rect(&scroll_rect(bounds.inner()), dpi_factor),
1812 );
1813 clip_stack.push(vv_clip);
1814 scroll_offset_stack.push((scroll_dx - vv_origin.x, scroll_dy - vv_origin.y));
1815 for child_item in &child_dl.items {
1816 render_single_item(
1817 child_item,
1818 pixmap,
1819 dpi_factor,
1820 renderer_resources,
1821 font_manager,
1822 glyph_cache,
1823 transform_stack,
1824 clip_stack,
1825 mask_stack,
1826 scroll_offset_stack,
1827 text_shadow_stack,
1828 render_state,
1829 )?;
1830 }
1831 scroll_offset_stack.pop();
1832 clip_stack.pop();
1833 }
1834 }
1835 DisplayListItem::VirtualViewPlaceholder { .. } => {
1836 #[cfg(feature = "std")]
1837 if std::env::var("AZ_MAP_DEBUG").is_ok() {
1838 eprintln!("[cpu-vview] VirtualViewPlaceholder hit (NOT swapped to a VirtualView item — nothing composites)");
1839 }
1840 }
1841
1842 DisplayListItem::LinearGradient {
1844 bounds,
1845 gradient,
1846 border_radius,
1847 } => {
1848 let clip = *clip_stack.last().unwrap();
1849 render_linear_gradient(
1850 pixmap,
1851 &scroll_rect(bounds.inner()),
1852 gradient,
1853 border_radius,
1854 clip,
1855 dpi_factor,
1856 render_state.system_style.as_deref().map(|s| &s.colors),
1857 );
1858 }
1859 DisplayListItem::RadialGradient {
1860 bounds,
1861 gradient,
1862 border_radius,
1863 } => {
1864 let clip = *clip_stack.last().unwrap();
1865 render_radial_gradient(
1866 pixmap,
1867 &scroll_rect(bounds.inner()),
1868 gradient,
1869 border_radius,
1870 clip,
1871 dpi_factor,
1872 render_state.system_style.as_deref().map(|s| &s.colors),
1873 );
1874 }
1875 DisplayListItem::ConicGradient {
1876 bounds,
1877 gradient,
1878 border_radius,
1879 } => {
1880 let clip = *clip_stack.last().unwrap();
1881 render_conic_gradient(
1882 pixmap,
1883 &scroll_rect(bounds.inner()),
1884 gradient,
1885 border_radius,
1886 clip,
1887 dpi_factor,
1888 render_state.system_style.as_deref().map(|s| &s.colors),
1889 );
1890 }
1891
1892 DisplayListItem::BoxShadow {
1894 bounds,
1895 shadow,
1896 border_radius,
1897 } => {
1898 render_box_shadow(
1899 pixmap,
1900 &scroll_rect(bounds.inner()),
1901 shadow,
1902 border_radius,
1903 dpi_factor,
1904 )?;
1905 }
1906
1907 DisplayListItem::PushOpacity { bounds, opacity } => {
1909 let rect = logical_rect_to_az_rect(&scroll_rect(bounds.inner()), dpi_factor);
1910 if let Some(r) = rect {
1911 let snap = snapshot_region(
1912 pixmap,
1913 r.x as i32,
1914 r.y as i32,
1915 r.width as u32,
1916 r.height as u32,
1917 );
1918 mask_stack.push(MaskEntry::Opacity {
1919 snapshot: snap,
1920 rect: r,
1921 opacity: *opacity,
1922 });
1923 }
1924 }
1925 DisplayListItem::PopOpacity => {
1926 if let Some(MaskEntry::Opacity {
1927 snapshot,
1928 rect,
1929 opacity,
1930 }) = mask_stack.pop()
1931 {
1932 let x = rect.x as i32;
1933 let y = rect.y as i32;
1934 let w = rect.width as u32;
1935 let h = rect.height as u32;
1936 let pw = pixmap.width as i32;
1937 let ph = pixmap.height as i32;
1938 for py in 0..h as i32 {
1940 let dy = y + py;
1941 if dy < 0 || dy >= ph {
1942 continue;
1943 }
1944 for px in 0..w as i32 {
1945 let dx = x + px;
1946 if dx < 0 || dx >= pw {
1947 continue;
1948 }
1949 let pi = ((dy as u32 * pixmap.width + dx as u32) * 4) as usize;
1950 let si = ((py as u32 * w + px as u32) * 4) as usize;
1951 if pi + 3 >= pixmap.data.len() || si + 3 >= snapshot.len() {
1952 continue;
1953 }
1954 let op = (opacity * 255.0).clamp(0.0, 255.0) as u32;
1955 let inv_op = 255 - op;
1956 for c in 0..4 {
1957 let snap_c = u32::from(snapshot[si + c]);
1958 let cur_c = u32::from(pixmap.data[pi + c]);
1959 pixmap.data[pi + c] = ((cur_c * op + snap_c * inv_op) / 255) as u8;
1960 }
1961 }
1962 }
1963 }
1964 }
1965
1966 DisplayListItem::PushReferenceFrame {
1968 transform_key,
1969 initial_transform,
1970 bounds,
1971 } => {
1972 let live_transform = render_state.transforms.get(&transform_key.id);
1977 let m = live_transform.map_or(&initial_transform.m, |t| &t.m);
1978 let tf = TransAffine::new_custom(
1979 f64::from(m[0][0]),
1980 f64::from(m[0][1]), f64::from(m[1][0]),
1982 f64::from(m[1][1]), f64::from(m[3][0]),
1984 f64::from(m[3][1]), );
1986 let current = transform_stack
1987 .last()
1988 .copied()
1989 .unwrap_or_else(TransAffine::new);
1990 let mut composed = tf;
1991 composed.premultiply(¤t);
1992 transform_stack.push(composed);
1993 }
1994 DisplayListItem::PopReferenceFrame => {
1995 if transform_stack.len() > 1 {
1996 transform_stack.pop();
1997 }
1998 }
1999
2000 DisplayListItem::PushFilter { .. } => {}
2010 DisplayListItem::PopFilter => {}
2011
2012 DisplayListItem::PushBackdropFilter { .. } => {}
2027 DisplayListItem::PopBackdropFilter => {}
2028
2029 DisplayListItem::PushTextShadow { shadow } => {
2036 text_shadow_stack.push(*shadow);
2037 }
2038 DisplayListItem::PopTextShadow => {
2039 text_shadow_stack.pop();
2040 }
2041
2042 DisplayListItem::PushImageMaskClip {
2043 bounds,
2044 mask_image,
2045 mask_rect,
2046 } => {
2047 let mr = &scroll_rect(mask_rect.inner());
2048 let px_x = (mr.origin.x * dpi_factor) as i32;
2049 let px_y = (mr.origin.y * dpi_factor) as i32;
2050 let px_w = (mr.size.width * dpi_factor).ceil() as u32;
2051 let px_h = (mr.size.height * dpi_factor).ceil() as u32;
2052
2053 if px_w > 0 && px_h > 0 {
2054 let snapshot = snapshot_region(pixmap, px_x, px_y, px_w, px_h);
2055 let mask_data = extract_mask_data(mask_image, px_w, px_h)
2056 .unwrap_or_else(|| vec![255u8; (px_w * px_h) as usize]);
2057 mask_stack.push(MaskEntry::ImageMask {
2058 snapshot,
2059 mask_data,
2060 origin_x: px_x,
2061 origin_y: px_y,
2062 width: px_w,
2063 height: px_h,
2064 });
2065 }
2066 }
2067 DisplayListItem::PopImageMaskClip => {
2068 if let Some(entry) = mask_stack.pop() {
2069 apply_mask(pixmap, &entry);
2070 }
2071 }
2072 }
2073
2074 Ok(())
2075}
2076
2077#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] fn render_rect(
2079 pixmap: &mut AzulPixmap,
2080 bounds: &LogicalRect,
2081 color: ColorU,
2082 border_radius: &BorderRadius,
2083 clip: Option<AzRect>,
2084 dpi_factor: f32,
2085) {
2086 if color.a == 0 {
2087 return;
2088 }
2089
2090 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
2091 return;
2092 };
2093
2094 if let Some(ref c) = clip {
2096 if rect.clip(c).is_none() {
2097 return;
2098 }
2099 }
2100
2101 let agg_color = Rgba8::new(
2102 u32::from(color.r),
2103 u32::from(color.g),
2104 u32::from(color.b),
2105 u32::from(color.a),
2106 );
2107
2108 if border_radius.is_zero() {
2109 let w = pixmap.width;
2113 let h = pixmap.height;
2114 let stride = (w * 4) as i32;
2115 let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
2116 let mut pf = PixfmtRgba32::new(&mut ra);
2117 let mut rb = RendererBase::new(pf);
2118 if let Some(c) = clip {
2119 rb.clip_box_i(
2120 c.x as i32,
2121 c.y as i32,
2122 (c.x + c.width) as i32 - 1,
2123 (c.y + c.height) as i32 - 1,
2124 );
2125 }
2126 rb.blend_bar(
2127 rect.x as i32,
2128 rect.y as i32,
2129 (rect.x + rect.width) as i32 - 1,
2130 (rect.y + rect.height) as i32 - 1,
2131 &agg_color,
2132 255, );
2134 } else {
2135 let mut path = build_rounded_rect_path(&rect, border_radius, dpi_factor);
2137 agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::NonZero, clip);
2138 }
2139
2140}
2141
2142pub const TEXT_LCD_DEFAULT: bool = true;
2152
2153fn text_lcd_enabled() -> bool {
2156 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2157 *V.get_or_init(|| {
2158 std::env::var("AZ_TEXT_LCD")
2159 .map(|s| !(s == "0" || s.eq_ignore_ascii_case("false")))
2160 .unwrap_or(TEXT_LCD_DEFAULT)
2161 })
2162}
2163
2164#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] #[allow(clippy::too_many_arguments)] fn render_glyphs_lcd(
2188 pixmap: &mut AzulPixmap,
2189 clip: Option<AzRect>,
2190 glyphs: &[GlyphInstance],
2191 parsed_font: &ParsedFont,
2192 font_hash: FontHash,
2193 ppem: u16,
2194 scale: f32,
2195 hint_correction: f32,
2196 color: ColorU,
2197 dpi_factor: f32,
2198 scroll_offset: (f32, f32),
2199 glyph_cache: &mut GlyphCache,
2200) {
2201 use agg_rust::pixfmt_lcd::{LcdDistributionLut, PixfmtRgba32Lcd};
2202
2203 let agg_color = Rgba8::new(
2204 u32::from(color.r),
2205 u32::from(color.g),
2206 u32::from(color.b),
2207 u32::from(color.a),
2208 );
2209 let subpx = crate::glyph_cache::text_subpixel_enabled();
2210
2211 let mut ras = RasterizerScanlineAa::new();
2214 ras.filling_rule(FillingRule::NonZero);
2215
2216 for glyph in glyphs {
2217 let glyph_index = glyph.index as u16;
2218 let Some(glyph_data) = parsed_font.get_or_decode_glyph(glyph_index) else {
2219 continue;
2220 };
2221 let Some(cached) = glyph_cache.get_or_build(
2222 font_hash.font_hash,
2223 glyph_index,
2224 &glyph_data,
2225 parsed_font,
2226 ppem,
2227 ) else {
2228 continue;
2229 };
2230 let is_hinted = cached.is_hinted;
2231
2232 let glyph_x = (glyph.point.x - scroll_offset.0) * dpi_factor;
2233 let glyph_baseline_y = (glyph.point.y - scroll_offset.1) * dpi_factor;
2234 let px = if subpx { glyph_x } else { glyph_x.round() };
2237 let py = glyph_baseline_y.round();
2238
2239 let rescale_hinted = is_hinted && (hint_correction - 1.0).abs() > 1e-4;
2244 let path_scale = if is_hinted {
2245 if rescale_hinted { f64::from(hint_correction) } else { 1.0 }
2246 } else {
2247 f64::from(scale)
2248 };
2249
2250 let mut transform = TransAffine::new_scaling(3.0 * path_scale, path_scale);
2255 transform.multiply(&TransAffine::new_translation(3.0 * f64::from(px), f64::from(py)));
2256 let mut src = agg_rust::conv_transform::ConvTransform::new(
2259 crate::glyph_cache::SliceVertexSource::new(cached.path.vertices()),
2260 transform,
2261 );
2262 ras.add_path(&mut src, 0);
2263 }
2264
2265 let w = pixmap.width;
2269 let h = pixmap.height;
2270 let stride = (w * 4) as i32;
2271 let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
2272 let lut = LcdDistributionLut::new(f64::from(0x56u32), f64::from(0x4Du32), f64::from(0x08u32));
2275 let pf = PixfmtRgba32Lcd::new(&mut ra, &lut);
2276 let mut rb = RendererBase::new(pf);
2277 if let Some(c) = clip {
2278 rb.clip_box_i(
2279 (c.x as i32) * 3,
2280 c.y as i32,
2281 ((c.x + c.width) as i32) * 3 - 1,
2282 (c.y + c.height) as i32 - 1,
2283 );
2284 }
2285 let mut sl = ScanlineU8::new();
2286 render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &agg_color);
2287}
2288
2289#[cfg(feature = "std")]
2297fn font_resolution_failed(font_hash: u64) {
2298 use std::sync::{Mutex, OnceLock};
2299 static SEEN: OnceLock<Mutex<std::collections::BTreeSet<u64>>> = OnceLock::new();
2300 debug_assert!(
2301 false,
2302 "[cpurender] BUG: layout emitted font hash {font_hash} that its own FontManager \
2303 cannot resolve — the display list and the font state are out of sync"
2304 );
2305 let seen = SEEN.get_or_init(|| Mutex::new(std::collections::BTreeSet::new()));
2306 if let Ok(mut seen) = seen.lock() {
2307 if seen.insert(font_hash) {
2308 eprintln!(
2309 "[azul][font] BUG: layout emitted font hash {font_hash} that its own \
2310 FontManager cannot resolve (neither a loaded face nor a registered \
2311 embedded font). The text using it CANNOT be drawn. This is an azul \
2312 bug — please report it."
2313 );
2314 }
2315 }
2316}
2317
2318#[cfg(not(feature = "std"))]
2319const fn font_resolution_failed(_font_hash: u64) {}
2320
2321#[cfg(test)]
2325pub(crate) fn empty_font_manager() -> FontManager<FontRef> {
2326 FontManager::new(rust_fontconfig::FcFontCache::default()).expect("FontManager::new")
2327}
2328
2329#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines)] fn render_text(
2332 glyphs: &[GlyphInstance],
2333 font_hash: FontHash,
2334 font_size_px: f32,
2335 color: ColorU,
2336 pixmap: &mut AzulPixmap,
2337 clip_rect: &LogicalRect,
2338 clip: Option<AzRect>,
2339 renderer_resources: &RendererResources,
2340 font_manager: &FontManager<FontRef>,
2341 dpi_factor: f32,
2342 glyph_cache: &mut GlyphCache,
2343 scroll_offset: (f32, f32),
2344 force_grayscale: bool,
2349) {
2350 if color.a == 0 || glyphs.is_empty() {
2351 return;
2352 }
2353
2354 if let Some(ref c) = clip {
2356 let Some(text_rect) = logical_rect_to_az_rect(clip_rect, dpi_factor) else {
2357 return;
2358 };
2359 if text_rect.clip(c).is_none() {
2360 return; }
2362 }
2363
2364 let agg_color = Rgba8::new(
2365 u32::from(color.r),
2366 u32::from(color.g),
2367 u32::from(color.b),
2368 u32::from(color.a),
2369 );
2370
2371 let Some(font_ref) = font_manager.resolve_font_by_hash(font_hash.font_hash) else {
2378 font_resolution_failed(font_hash.font_hash);
2382 return;
2383 };
2384 let parsed_font: &ParsedFont = crate::font_ref_to_parsed_font(&font_ref);
2387
2388 let units_per_em = f32::from(parsed_font.font_metrics.units_per_em);
2389 if units_per_em <= 0.0 {
2390 return;
2391 }
2392
2393 let effective_px = font_size_px * dpi_factor;
2394 let scale = effective_px / units_per_em;
2395 let ppem = effective_px.round() as u16;
2396 let hint_correction = if ppem > 0 { effective_px / f32::from(ppem) } else { 1.0 };
2400
2401 if text_lcd_enabled() && !force_grayscale {
2405 render_glyphs_lcd(
2406 pixmap, clip, glyphs, parsed_font, font_hash, ppem, scale,
2407 hint_correction, color, dpi_factor, scroll_offset, glyph_cache,
2408 );
2409 return;
2410 }
2411
2412 let w = pixmap.width;
2414 let h = pixmap.height;
2415 let stride = (w * 4) as i32;
2416
2417 let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
2420 let mut pf = PixfmtRgba32::new(&mut ra);
2421 let mut rb = RendererBase::new(pf);
2422 if let Some(c) = clip {
2423 rb.clip_box_i(
2424 c.x as i32,
2425 c.y as i32,
2426 (c.x + c.width) as i32 - 1,
2427 (c.y + c.height) as i32 - 1,
2428 );
2429 }
2430 let mut ras = RasterizerScanlineAa::new();
2431 ras.filling_rule(FillingRule::NonZero);
2432
2433 for glyph in glyphs {
2436 let glyph_index = glyph.index as u16;
2437
2438 let Some(glyph_data) = parsed_font.get_or_decode_glyph(glyph_index) else {
2442 continue;
2443 };
2444
2445 let is_hinted = glyph_cache
2446 .get_or_build(
2447 font_hash.font_hash,
2448 glyph_index,
2449 &glyph_data,
2450 parsed_font,
2451 ppem,
2452 )
2453 .is_some_and(|c| c.is_hinted);
2454
2455 let glyph_x = (glyph.point.x - scroll_offset.0) * dpi_factor;
2456 let glyph_baseline_y = (glyph.point.y - scroll_offset.1) * dpi_factor;
2457
2458 let Some((cells, int_x, int_y)) = glyph_cache.get_or_build_cells(
2459 font_hash.font_hash,
2460 glyph_index,
2461 ppem,
2462 glyph_x,
2463 glyph_baseline_y,
2464 scale,
2465 is_hinted,
2466 hint_correction,
2467 ) else {
2468 continue;
2469 };
2470
2471 ras.add_cells_offset(cells, int_x, int_y);
2472 }
2473
2474 let mut sl = ScanlineU8::new();
2476 render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &agg_color);
2477
2478}
2479
2480#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)]
2494fn render_text_shadow(
2495 shadow: &StyleBoxShadow,
2496 glyphs: &[GlyphInstance],
2497 font_hash: FontHash,
2498 font_size_px: f32,
2499 pixmap: &mut AzulPixmap,
2500 clip_rect: &LogicalRect,
2501 clip: Option<AzRect>,
2502 renderer_resources: &RendererResources,
2503 font_manager: &FontManager<FontRef>,
2504 dpi_factor: f32,
2505 glyph_cache: &mut GlyphCache,
2506 scroll_offset: (f32, f32),
2507) {
2508 let color = shadow.color;
2509 if color.a == 0 || glyphs.is_empty() {
2510 return;
2511 }
2512
2513 let off_x = shadow
2515 .offset_x
2516 .inner
2517 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
2518 let off_y = shadow
2519 .offset_y
2520 .inner
2521 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
2522 let blur_logical = shadow
2523 .blur_radius
2524 .inner
2525 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
2526 .max(0.0);
2527
2528 let Some(mut tmp) = AzulPixmap::new(pixmap.width, pixmap.height) else {
2530 return;
2531 };
2532 tmp.fill(0, 0, 0, 0);
2533
2534 let shifted: Vec<GlyphInstance> = glyphs
2536 .iter()
2537 .map(|g| {
2538 let mut g = *g;
2539 g.point.x += off_x;
2540 g.point.y += off_y;
2541 g
2542 })
2543 .collect();
2544
2545 let shadow_clip_rect = LogicalRect {
2547 origin: LogicalPosition {
2548 x: clip_rect.origin.x + off_x,
2549 y: clip_rect.origin.y + off_y,
2550 },
2551 size: clip_rect.size,
2552 };
2553 render_text(
2554 &shifted,
2555 font_hash,
2556 font_size_px,
2557 color,
2558 &mut tmp,
2559 &shadow_clip_rect,
2560 clip,
2561 renderer_resources,
2562 font_manager,
2563 dpi_factor,
2564 glyph_cache,
2565 scroll_offset,
2566 true,
2569 );
2570
2571 let blur_px = blur_logical * dpi_factor;
2573 if blur_px > 0.5 {
2574 let radius = (blur_px.ceil() as u32).min(254);
2575 let w = tmp.width;
2576 let h = tmp.height;
2577 let stride = (w * 4) as i32;
2578 let mut ra = unsafe { RowAccessor::new_with_buf(tmp.data.as_mut_ptr(), w, h, stride) };
2579 stack_blur_rgba32(&mut ra, radius, radius);
2580 }
2581
2582 blit_buffer(pixmap, &tmp.data, tmp.width, tmp.height, 0, 0);
2584}
2585
2586#[allow(clippy::suboptimal_flops)] #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] fn render_border(
2589 pixmap: &mut AzulPixmap,
2590 bounds: &LogicalRect,
2591 color: ColorU,
2592 width: f32,
2593 border_style: azul_css::props::style::border::BorderStyle,
2594 border_radius: &BorderRadius,
2595 clip: Option<AzRect>,
2596 dpi_factor: f32,
2597) {
2598 use azul_css::props::style::border::BorderStyle;
2599
2600 if color.a == 0 || width <= 0.0 {
2601 return;
2602 }
2603
2604 match border_style {
2605 BorderStyle::None | BorderStyle::Hidden => return,
2606 _ => {}
2607 }
2608
2609 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
2610 return;
2611 };
2612
2613 if let Some(ref c) = clip {
2615 if rect.clip(c).is_none() {
2616 return;
2617 }
2618 }
2619
2620 let scaled_width = width * dpi_factor;
2621 let agg_color = Rgba8::new(
2622 u32::from(color.r),
2623 u32::from(color.g),
2624 u32::from(color.b),
2625 u32::from(color.a),
2626 );
2627
2628 let mut path = build_rounded_rect_path(&rect, border_radius, dpi_factor);
2630
2631 let x = f64::from(rect.x);
2632 let y = f64::from(rect.y);
2633 let w = f64::from(rect.width);
2634 let h = f64::from(rect.height);
2635 let sw = f64::from(scaled_width);
2636
2637 let ir = AzRect::from_xywh(
2639 rect.x + scaled_width,
2640 rect.y + scaled_width,
2641 rect.width - 2.0 * scaled_width,
2642 rect.height - 2.0 * scaled_width,
2643 );
2644
2645 if let Some(ir) = ir {
2646 let inner_radius = BorderRadius {
2647 top_left: (border_radius.top_left - width).max(0.0),
2648 top_right: (border_radius.top_right - width).max(0.0),
2649 bottom_right: (border_radius.bottom_right - width).max(0.0),
2650 bottom_left: (border_radius.bottom_left - width).max(0.0),
2651 };
2652 let mut inner = build_rounded_rect_path(&ir, &inner_radius, dpi_factor);
2653 path.concat_path(&mut inner, 0);
2654 }
2655
2656 match border_style {
2658 BorderStyle::Dashed | BorderStyle::Dotted => {
2659 use agg_rust::conv_dash::ConvDash;
2661 use agg_rust::conv_stroke::ConvStroke;
2662
2663 let half = sw / 2.0;
2664 let mut stroke_path = PathStorage::new();
2665 let (cx, cy, cw, ch) = (x + half, y + half, w - sw, h - sw);
2666 stroke_path.move_to(cx, cy);
2667 stroke_path.line_to(cx + cw, cy);
2668 stroke_path.line_to(cx + cw, cy + ch);
2669 stroke_path.line_to(cx, cy + ch);
2670 stroke_path.close_polygon(PATH_FLAGS_NONE);
2671
2672 let mut dashed = ConvDash::new(stroke_path);
2673 if border_style == BorderStyle::Dashed {
2674 dashed.add_dash(sw * 3.0, sw);
2675 } else {
2676 dashed.add_dash(sw, sw);
2677 }
2678
2679 let mut stroked = ConvStroke::new(dashed);
2680 stroked.set_width(sw);
2681
2682 agg_fill_path_clipped(pixmap, &mut stroked, &agg_color, FillingRule::NonZero, clip);
2683 }
2684 _ if border_radius.is_zero() => {
2685 let pw = pixmap.width;
2687 let ph = pixmap.height;
2688 let stride = (pw * 4) as i32;
2689 let mut ra =
2690 unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), pw, ph, stride) };
2691 let mut pf = PixfmtRgba32::new(&mut ra);
2692 let mut rb = RendererBase::new(pf);
2693 if let Some(c) = clip {
2694 rb.clip_box_i(
2695 c.x as i32,
2696 c.y as i32,
2697 (c.x + c.width) as i32 - 1,
2698 (c.y + c.height) as i32 - 1,
2699 );
2700 }
2701 let (xi, yi) = (x as i32, y as i32);
2702 let (x2i, y2i) = ((x + w) as i32 - 1, (y + h) as i32 - 1);
2703 let swi = sw as i32;
2704 rb.blend_bar(xi, yi, x2i, yi + swi - 1, &agg_color, 255);
2706 rb.blend_bar(xi, y2i - swi + 1, x2i, y2i, &agg_color, 255);
2708 rb.blend_bar(xi, yi + swi, xi + swi - 1, y2i - swi, &agg_color, 255);
2710 rb.blend_bar(x2i - swi + 1, yi + swi, x2i, y2i - swi, &agg_color, 255);
2712 }
2713 _ => {
2714 agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::EvenOdd, clip);
2716 }
2717 }
2718
2719}
2720
2721#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] #[allow(clippy::too_many_lines)] fn render_border_sides(
2726 pixmap: &mut AzulPixmap,
2727 bounds: &LogicalRect,
2728 colors: [ColorU; 4], widths: [f32; 4], _styles: [azul_css::props::style::border::BorderStyle; 4],
2731 border_radius: &BorderRadius,
2732 clip: Option<AzRect>,
2733 dpi_factor: f32,
2734) {
2735 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
2736 return;
2737 };
2738
2739 let ox = f64::from(rect.x);
2741 let oy = f64::from(rect.y);
2742 let ow = f64::from(rect.width);
2743 let oh = f64::from(rect.height);
2744
2745 let wt = f64::from(widths[0] * dpi_factor);
2747 let wr = f64::from(widths[1] * dpi_factor);
2748 let wb = f64::from(widths[2] * dpi_factor);
2749 let wl = f64::from(widths[3] * dpi_factor);
2750
2751 let ix = ox + wl;
2752 let iy = oy + wt;
2753 let iw = ow - wl - wr;
2754 let ih = oh - wt - wb;
2755
2756 let sides: [(f64, f64, f64, f64, f64, f64, f64, f64, ColorU, f32); 4] = [
2763 (
2765 ox,
2766 oy,
2767 ox + ow,
2768 oy,
2769 ix + iw,
2770 iy,
2771 ix,
2772 iy,
2773 colors[0],
2774 widths[0],
2775 ),
2776 (
2778 ox + ow,
2779 oy,
2780 ox + ow,
2781 oy + oh,
2782 ix + iw,
2783 iy + ih,
2784 ix + iw,
2785 iy,
2786 colors[1],
2787 widths[1],
2788 ),
2789 (
2791 ox + ow,
2792 oy + oh,
2793 ox,
2794 oy + oh,
2795 ix,
2796 iy + ih,
2797 ix + iw,
2798 iy + ih,
2799 colors[2],
2800 widths[2],
2801 ),
2802 (
2804 ox,
2805 oy + oh,
2806 ox,
2807 oy,
2808 ix,
2809 iy,
2810 ix,
2811 iy + ih,
2812 colors[3],
2813 widths[3],
2814 ),
2815 ];
2816
2817 if border_radius.is_zero() {
2818 let pw = pixmap.width;
2820 let ph = pixmap.height;
2821 let stride = (pw * 4) as i32;
2822 let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), pw, ph, stride) };
2823 let mut pf = PixfmtRgba32::new(&mut ra);
2824 let mut rb = RendererBase::new(pf);
2825 if let Some(c) = clip {
2826 rb.clip_box_i(
2827 c.x as i32,
2828 c.y as i32,
2829 (c.x + c.width) as i32 - 1,
2830 (c.y + c.height) as i32 - 1,
2831 );
2832 }
2833 if widths[0] > 0.0 && colors[0].a > 0 {
2835 let c = colors[0];
2836 let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
2837 rb.blend_bar(
2838 ox as i32,
2839 oy as i32,
2840 (ox + ow) as i32 - 1,
2841 iy as i32 - 1,
2842 &ac,
2843 255,
2844 );
2845 }
2846 if widths[2] > 0.0 && colors[2].a > 0 {
2848 let c = colors[2];
2849 let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
2850 rb.blend_bar(
2851 ox as i32,
2852 (iy + ih) as i32,
2853 (ox + ow) as i32 - 1,
2854 (oy + oh) as i32 - 1,
2855 &ac,
2856 255,
2857 );
2858 }
2859 if widths[3] > 0.0 && colors[3].a > 0 {
2861 let c = colors[3];
2862 let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
2863 rb.blend_bar(
2864 ox as i32,
2865 iy as i32,
2866 ix as i32 - 1,
2867 (iy + ih) as i32 - 1,
2868 &ac,
2869 255,
2870 );
2871 }
2872 if widths[1] > 0.0 && colors[1].a > 0 {
2874 let c = colors[1];
2875 let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
2876 rb.blend_bar(
2877 (ix + iw) as i32,
2878 iy as i32,
2879 (ox + ow) as i32 - 1,
2880 (iy + ih) as i32 - 1,
2881 &ac,
2882 255,
2883 );
2884 }
2885 } else {
2886 for &(x0, y0, x1, y1, x2, y2, x3, y3, color, width) in &sides {
2888 if width <= 0.0 || color.a == 0 {
2889 continue;
2890 }
2891
2892 let mut path = PathStorage::new();
2893 path.move_to(x0, y0);
2894 path.line_to(x1, y1);
2895 path.line_to(x2, y2);
2896 path.line_to(x3, y3);
2897 path.close_polygon(PATH_FLAGS_NONE);
2898
2899 let agg_color = Rgba8::new(
2900 u32::from(color.r),
2901 u32::from(color.g),
2902 u32::from(color.b),
2903 u32::from(color.a),
2904 );
2905 agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::NonZero, clip);
2906 }
2907 }
2908
2909}
2910
2911#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_precision_loss, clippy::cast_sign_loss)] #[allow(clippy::many_single_char_names, clippy::similar_names)] #[allow(clippy::too_many_lines)] fn render_image(
2915 pixmap: &mut AzulPixmap,
2916 bounds: &LogicalRect,
2917 image: &ImageRef,
2918 clip: Option<AzRect>,
2919 dpi_factor: f32,
2920) {
2921 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
2922 return;
2923 };
2924
2925 if let Some(ref c) = clip {
2927 if rect.clip(c).is_none() {
2928 return;
2929 }
2930 }
2931
2932 let image_data = image.get_data();
2933 let (src_rgba, src_w, src_h) = match image_data {
2934 DecodedImage::Raw((descriptor, data)) => {
2935 let w = descriptor.width as u32;
2936 let h = descriptor.height as u32;
2937 if w == 0 || h == 0 {
2938 return;
2939 }
2940 let bytes = match data {
2941 azul_core::resources::ImageData::Raw(shared) => shared.as_ref(),
2942 azul_core::resources::ImageData::External(_) => return,
2943 };
2944
2945 let rgba = match descriptor.format {
2946 azul_core::resources::RawImageFormat::RGBA8 => bytes.to_vec(),
2952 azul_core::resources::RawImageFormat::RGB8 => {
2953 let mut out = Vec::with_capacity(bytes.len() / 3 * 4);
2954 for chunk in bytes.chunks_exact(3) {
2955 out.extend_from_slice(&[chunk[0], chunk[1], chunk[2], 255]);
2956 }
2957 out
2958 }
2959 azul_core::resources::RawImageFormat::BGRA8 => {
2960 let mut out = Vec::with_capacity(bytes.len());
2961 for chunk in bytes.chunks_exact(4) {
2962 let b = chunk[0];
2963 let g = chunk[1];
2964 let r = chunk[2];
2965 let a = chunk[3];
2966 out.push(r);
2967 out.push(g);
2968 out.push(b);
2969 out.push(a);
2970 }
2971 out
2972 }
2973 azul_core::resources::RawImageFormat::R8 => {
2974 let mut out = Vec::with_capacity(bytes.len() * 4);
2975 for &v in bytes {
2976 out.push(v);
2977 out.push(v);
2978 out.push(v);
2979 out.push(v);
2980 }
2981 out
2982 }
2983 _ => {
2984 let gray = Rgba8::new(200, 200, 200, 255);
2986 let mut path = build_rect_path(&rect);
2987 agg_fill_path(pixmap, &mut path, &gray, FillingRule::NonZero);
2988 return;
2989 }
2990 };
2991
2992 (rgba, w, h)
2993 }
2994 DecodedImage::NullImage { .. } | DecodedImage::Callback(_) => {
2995 let gray = Rgba8::new(200, 200, 200, 255);
2996 let mut path = build_rect_path(&rect);
2997 agg_fill_path(pixmap, &mut path, &gray, FillingRule::NonZero);
2998 return;
2999 }
3000 DecodedImage::Gl(_) => return,
3001 };
3002
3003 let dst_x = rect.x as i32;
3005 let dst_y = rect.y as i32;
3006 let dst_w = rect.width as u32;
3007 let dst_h = rect.height as u32;
3008 let pw = pixmap.width;
3009 let ph = pixmap.height;
3010
3011 let sx = src_w as f32 / dst_w.max(1) as f32;
3012 let sy = src_h as f32 / dst_h.max(1) as f32;
3013
3014 let (clip_x1, clip_y1, clip_x2, clip_y2) = clip.as_ref().map_or((0, 0, pw as i32, ph as i32), |c| (
3016 c.x as i32,
3017 c.y as i32,
3018 (c.x + c.width) as i32,
3019 (c.y + c.height) as i32,
3020 ));
3021
3022 for py in 0..dst_h {
3023 for px in 0..dst_w {
3024 let tx = dst_x + px as i32;
3025 let ty = dst_y + py as i32;
3026 if tx < 0 || ty < 0 || tx >= pw as i32 || ty >= ph as i32 {
3027 continue;
3028 }
3029 if tx < clip_x1 || ty < clip_y1 || tx >= clip_x2 || ty >= clip_y2 {
3031 continue;
3032 }
3033
3034 let src_x = ((px as f32 * sx) as u32).min(src_w - 1);
3035 let src_y = ((py as f32 * sy) as u32).min(src_h - 1);
3036 let si = ((src_y * src_w + src_x) * 4) as usize;
3037 let di = ((ty as u32 * pw + tx as u32) * 4) as usize;
3038
3039 if si + 3 < src_rgba.len() && di + 3 < pixmap.data.len() {
3040 let sa = u32::from(src_rgba[si + 3]);
3041 if sa == 255 {
3042 pixmap.data[di] = src_rgba[si];
3043 pixmap.data[di + 1] = src_rgba[si + 1];
3044 pixmap.data[di + 2] = src_rgba[si + 2];
3045 pixmap.data[di + 3] = 255;
3046 } else if sa > 0 {
3047 let da = 255 - sa;
3049 pixmap.data[di] =
3050 ((u32::from(src_rgba[si]) * sa + u32::from(pixmap.data[di]) * da) / 255) as u8;
3051 pixmap.data[di + 1] = ((u32::from(src_rgba[si + 1]) * sa
3052 + u32::from(pixmap.data[di + 1]) * da)
3053 / 255) as u8;
3054 pixmap.data[di + 2] = ((u32::from(src_rgba[si + 2]) * sa
3055 + u32::from(pixmap.data[di + 2]) * da)
3056 / 255) as u8;
3057 pixmap.data[di + 3] =
3058 ((sa + u32::from(pixmap.data[di + 3]) * da / 255).min(255)) as u8;
3059 }
3060 }
3061 }
3062 }
3063
3064}
3065
3066fn build_rect_path(rect: &AzRect) -> PathStorage {
3067 let mut path = PathStorage::new();
3068 let x = f64::from(rect.x);
3069 let y = f64::from(rect.y);
3070 let w = f64::from(rect.width);
3071 let h = f64::from(rect.height);
3072 path.move_to(x, y);
3073 path.line_to(x + w, y);
3074 path.line_to(x + w, y + h);
3075 path.line_to(x, y + h);
3076 path.close_polygon(PATH_FLAGS_NONE);
3077 path
3078}
3079
3080fn build_rounded_rect_path(
3081 rect: &AzRect,
3082 border_radius: &BorderRadius,
3083 dpi_factor: f32,
3084) -> PathStorage {
3085 let mut path = PathStorage::new();
3086
3087 let x = f64::from(rect.x);
3088 let y = f64::from(rect.y);
3089 let w = f64::from(rect.width);
3090 let h = f64::from(rect.height);
3091
3092 let tl = f64::from(border_radius.top_left * dpi_factor);
3093 let tr = f64::from(border_radius.top_right * dpi_factor);
3094 let br = f64::from(border_radius.bottom_right * dpi_factor);
3095 let bl = f64::from(border_radius.bottom_left * dpi_factor);
3096
3097 if tl <= 0.0 && tr <= 0.0 && br <= 0.0 && bl <= 0.0 {
3098 path.move_to(x, y);
3099 path.line_to(x + w, y);
3100 path.line_to(x + w, y + h);
3101 path.line_to(x, y + h);
3102 path.close_polygon(PATH_FLAGS_NONE);
3103 return path;
3104 }
3105
3106 let mut rr = RoundedRect::default_new();
3118 rr.rect(x, y, x + w, y + h);
3119 rr.radius_all(tl, tl, tr, tr, br, br, bl, bl);
3120 rr.normalize_radius();
3121 rr.set_approximation_scale(f64::from(dpi_factor.max(1.0)));
3122
3123 path.concat_path(&mut rr, 0);
3124 path
3125}
3126
3127#[derive(Debug, Clone, Copy)]
3133pub struct ComponentPreviewOptions {
3134 pub width: Option<f32>,
3136 pub height: Option<f32>,
3138 pub dpi_factor: f32,
3140 pub background_color: ColorU,
3142}
3143
3144impl Default for ComponentPreviewOptions {
3145 fn default() -> Self {
3146 Self {
3147 width: None,
3148 height: None,
3149 dpi_factor: 1.0,
3150 background_color: ColorU {
3151 r: 255,
3152 g: 255,
3153 b: 255,
3154 a: 255,
3155 },
3156 }
3157 }
3158}
3159
3160#[derive(Debug)]
3162pub struct ComponentPreviewResult {
3163 pub png_data: Vec<u8>,
3165 pub content_width: f32,
3167 pub content_height: f32,
3169}
3170
3171#[allow(clippy::match_same_arms)] fn compute_content_bounds(dl: &DisplayList) -> Option<(f32, f32, f32, f32)> {
3174 let mut min_x = f32::MAX;
3175 let mut min_y = f32::MAX;
3176 let mut max_x = f32::MIN;
3177 let mut max_y = f32::MIN;
3178 let mut has_items = false;
3179
3180 for item in &dl.items {
3181 let bounds = match item {
3182 DisplayListItem::Rect { bounds, .. } => Some(*bounds),
3183 DisplayListItem::SelectionRect { bounds, .. } => Some(*bounds),
3184 DisplayListItem::Border { bounds, .. } => Some(*bounds),
3185 DisplayListItem::Text { clip_rect, .. } => Some(*clip_rect),
3186 DisplayListItem::Image { bounds, .. } => Some(*bounds),
3187 DisplayListItem::BoxShadow { bounds, .. } => Some(*bounds),
3188 DisplayListItem::PushClip { bounds, .. } => Some(*bounds),
3189 DisplayListItem::LinearGradient { bounds, .. } => Some(*bounds),
3190 DisplayListItem::RadialGradient { bounds, .. } => Some(*bounds),
3191 DisplayListItem::ConicGradient { bounds, .. } => Some(*bounds),
3192 DisplayListItem::VirtualView { bounds, .. } => Some(*bounds),
3193 DisplayListItem::ScrollBar { bounds, .. } => Some(*bounds),
3194 _ => None,
3195 };
3196 if let Some(b) = bounds {
3197 has_items = true;
3198 min_x = min_x.min(b.0.origin.x);
3199 min_y = min_y.min(b.0.origin.y);
3200 max_x = max_x.max(b.0.origin.x + b.0.size.width);
3201 max_y = max_y.max(b.0.origin.y + b.0.size.height);
3202 }
3203 }
3204
3205 if has_items {
3206 Some((min_x, min_y, max_x, max_y))
3207 } else {
3208 None
3209 }
3210}
3211
3212#[cfg(all(feature = "std", feature = "text_layout", feature = "font_loading"))]
3214#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines)] pub fn render_component_preview(
3223 styled_dom: &azul_core::styled_dom::StyledDom,
3224 font_manager: &FontManager<FontRef>,
3225 opts: ComponentPreviewOptions,
3226 system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
3227) -> Result<ComponentPreviewResult, String> {
3228 use crate::{
3229 font_traits::TextLayoutCache,
3230 solver3::{self, cache::LayoutCache, display_list::DisplayList},
3231 };
3232 use azul_core::{
3233 dom::DomId,
3234 geom::{LogicalPosition, LogicalRect, LogicalSize},
3235 resources::{IdNamespace, RendererResources},
3236 selection::{SelectionState, TextSelection},
3237 };
3238 use std::collections::{BTreeMap, HashMap};
3239
3240 const MAX_SIZE: f32 = 4096.0;
3241
3242 let layout_width = opts.width.unwrap_or(MAX_SIZE);
3243 let layout_height = opts.height.unwrap_or(MAX_SIZE);
3244
3245 let viewport = LogicalRect {
3246 origin: LogicalPosition::zero(),
3247 size: LogicalSize {
3248 width: layout_width,
3249 height: layout_height,
3250 },
3251 };
3252
3253 let mut preview_font_manager = FontManager::from_arc_shared(
3254 font_manager.fc_cache.clone(),
3255 font_manager.parsed_fonts.clone(),
3256 )
3257 .map_err(|e| format!("Failed to create preview font manager: {e:?}"))?;
3258
3259 for (family, faces) in &font_manager.memory_families {
3265 preview_font_manager
3266 .memory_families
3267 .entry(family.clone())
3268 .or_insert_with(|| faces.clone());
3269 }
3270
3271 {
3273 use crate::solver3::getters::collect_and_resolve_font_chains_with_registration;
3274 use crate::text3::default::PathLoader;
3275
3276 let platform = azul_css::system::Platform::current();
3277
3278 let chains = collect_and_resolve_font_chains_with_registration(
3279 styled_dom,
3280 &preview_font_manager.fc_cache,
3281 &preview_font_manager,
3282 &platform,
3283 );
3284 let loader = PathLoader::new();
3285 let _failed = preview_font_manager.load_missing_for_chains(&chains, |bytes, index| {
3286 loader.load_font_shared(bytes, index)
3287 });
3288 preview_font_manager.set_font_chain_cache(chains.into_fontconfig_chains());
3289 }
3290
3291 let mut layout_cache = LayoutCache {
3293 tree: None,
3294 calculated_positions: Vec::new(),
3295 viewport: None,
3296 scroll_ids: HashMap::new(),
3297 scroll_id_to_node_id: HashMap::new(),
3298 counters: HashMap::new(),
3299 float_cache: HashMap::new(),
3300 cache_map: solver3::cache::LayoutCacheMap::default(),
3301 previous_positions: Vec::new(),
3302 cached_display_list: None,
3303 prev_dom_ptr: 0,
3304 prev_viewport: LogicalRect::zero(),
3305 };
3306 let mut text_cache = TextLayoutCache::new();
3307 let empty_scroll_offsets = BTreeMap::new();
3308 let empty_text_selections = BTreeMap::new();
3309 let renderer_resources = RendererResources::default();
3310 let id_namespace = IdNamespace(0xFFFF);
3311 let dom_id = DomId::ROOT_ID;
3312 let mut debug_messages = None;
3313 let get_system_time_fn = azul_core::task::GetSystemTimeCallback {
3314 cb: azul_core::task::get_system_time_libstd,
3315 };
3316
3317 let display_list = solver3::layout_document(
3318 &mut layout_cache,
3319 &mut text_cache,
3320 styled_dom,
3321 viewport,
3322 &preview_font_manager,
3323 &empty_scroll_offsets,
3324 &empty_text_selections,
3325 &mut debug_messages,
3326 None,
3327 &renderer_resources,
3328 id_namespace,
3329 dom_id,
3330 false,
3331 Vec::new(),
3332 None, &azul_core::resources::ImageCache::default(),
3334 system_style.clone(),
3335 get_system_time_fn,
3336 )
3337 .map_err(|e| format!("Layout failed: {e:?}"))?;
3338
3339 let (render_width, render_height) = if opts.width.is_some() && opts.height.is_some() {
3341 (opts.width.unwrap(), opts.height.unwrap())
3342 } else {
3343 match compute_content_bounds(&display_list) {
3344 Some((_min_x, _min_y, max_x, max_y)) => {
3345 let w = if opts.width.is_some() {
3346 opts.width.unwrap()
3347 } else {
3348 max_x.max(1.0).ceil()
3349 };
3350 let h = if opts.height.is_some() {
3351 opts.height.unwrap()
3352 } else {
3353 max_y.max(1.0).ceil()
3354 };
3355 (w, h)
3356 }
3357 None => {
3358 return Ok(ComponentPreviewResult {
3359 png_data: Vec::new(),
3360 content_width: 0.0,
3361 content_height: 0.0,
3362 });
3363 }
3364 }
3365 };
3366
3367 let render_width = render_width.min(MAX_SIZE);
3368 let render_height = render_height.min(MAX_SIZE);
3369
3370 let dpi = opts.dpi_factor;
3372 let pixel_w = ((render_width * dpi) as u32).max(1);
3373 let pixel_h = ((render_height * dpi) as u32).max(1);
3374
3375 let mut pixmap = AzulPixmap::new(pixel_w, pixel_h)
3376 .ok_or_else(|| format!("Cannot create pixmap {pixel_w}x{pixel_h}"))?;
3377
3378 let bg = opts.background_color;
3379 pixmap.fill(bg.r, bg.g, bg.b, bg.a);
3380
3381 let mut preview_glyph_cache = GlyphCache::new();
3382 let preview_render_state =
3383 CpuRenderState::new(ScrollOffsetMap::new()).with_system_style(system_style);
3384 render_display_list_with_state(
3385 &display_list,
3386 &mut pixmap,
3387 dpi,
3388 &renderer_resources,
3389 &preview_font_manager,
3390 &mut preview_glyph_cache,
3391 &preview_render_state,
3392 )?;
3393
3394 let png_data = pixmap
3395 .encode_png()
3396 .map_err(|e| format!("PNG encoding failed: {e}"))?;
3397
3398 Ok(ComponentPreviewResult {
3399 png_data,
3400 content_width: render_width,
3401 content_height: render_height,
3402 })
3403}
3404
3405#[cfg(all(feature = "std", feature = "text_layout", feature = "font_loading"))]
3410pub fn render_dom_to_image(
3414 mut dom: azul_core::dom::Dom,
3415 css: azul_css::css::Css,
3416 width: f32,
3417 height: f32,
3418 dpi: f32,
3419) -> Result<Vec<u8>, String> {
3420 use crate::font_traits::FontManager;
3421 use azul_core::styled_dom::StyledDom;
3422
3423 let styled_dom = StyledDom::create(&mut dom, css);
3424
3425 let fc_cache = crate::font::loading::build_font_cache();
3426 let font_manager = FontManager::new(fc_cache)
3427 .map_err(|e| format!("Failed to create font manager: {e:?}"))?;
3428
3429 let opts = ComponentPreviewOptions {
3430 width: Some(width),
3431 height: Some(height),
3432 dpi_factor: dpi,
3433 background_color: ColorU {
3434 r: 255,
3435 g: 255,
3436 b: 255,
3437 a: 255,
3438 },
3439 };
3440
3441 let result = render_component_preview(&styled_dom, &font_manager, opts, None)?;
3442 Ok(result.png_data)
3443}
3444
3445#[cfg(all(feature = "std", feature = "text_layout", feature = "font_loading"))]
3463#[must_use]
3464#[allow(clippy::suboptimal_flops, clippy::cast_possible_truncation, clippy::cast_sign_loss)]
3466pub fn render_text_run_to_pixmap(
3467 fc_cache: &rust_fontconfig::FcFontCache,
3468 text: &str,
3469 font_size_px: f32,
3470 text_color: ColorU,
3471 bg_color: ColorU,
3472 padding_px: f32,
3473 dpi_factor: f32,
3474) -> Option<AzulPixmap> {
3475 use azul_core::resources::{FontKey, IdNamespace};
3476 use rust_fontconfig::{FcPattern, OwnedFontSource};
3477
3478 let mut trace = Vec::new();
3483 let matched = fc_cache.query_with_fallback(
3484 &FcPattern {
3485 family: Some("sans-serif".to_string()),
3486 ..Default::default()
3487 },
3488 &mut trace,
3489 )?;
3490
3491 let bytes = fc_cache.get_font_bytes(&matched.id)?;
3492 let font_index = fc_cache
3493 .get_font_by_id(&matched.id)
3494 .map_or(0, |src| match src {
3495 OwnedFontSource::Disk(path) => path.font_index,
3496 OwnedFontSource::Memory(font) => font.font_index,
3497 });
3498
3499 let parsed = ParsedFont::from_bytes(bytes.as_slice(), font_index, &mut Vec::new())?
3500 .with_source_bytes(bytes.clone());
3501
3502 let upm = f32::from(parsed.font_metrics.units_per_em);
3503 if upm <= 0.0 {
3504 return None;
3505 }
3506 let scale = font_size_px / upm;
3507
3508 let rr = RendererResources::default();
3513 let font_ref = crate::parsed_font_to_font_ref(parsed.clone());
3514 let hash = crate::font_ref_to_parsed_font(&font_ref).hash;
3515 let fm: FontManager<FontRef> =
3516 FontManager::new(rust_fontconfig::FcFontCache::default()).ok()?;
3517 fm.insert_font(rust_fontconfig::FontId::new(), font_ref);
3518 let font_hash = FontHash { font_hash: hash };
3519
3520 let ascent = parsed.font_metrics.ascent * scale;
3524 let descent = parsed.font_metrics.descent * scale; let baseline_y = padding_px + ascent;
3526 let mut pen_x = padding_px;
3527 let mut glyphs = Vec::new();
3528 for c in text.chars() {
3529 let gid = parsed.lookup_glyph_index(c as u32).unwrap_or(0);
3530 let advance = f32::from(parsed.get_horizontal_advance(gid)) * scale;
3531 glyphs.push(GlyphInstance {
3532 index: u32::from(gid),
3533 point: LogicalPosition { x: pen_x, y: baseline_y },
3534 size: LogicalSize { width: advance, height: font_size_px },
3535 });
3536 pen_x += advance;
3537 }
3538
3539 let logical_w = (pen_x + padding_px).max(1.0);
3541 let logical_h = (ascent - descent + padding_px * 2.0).max(1.0);
3542 let w = ((logical_w * dpi_factor).ceil() as u32).max(1);
3543 let h = ((logical_h * dpi_factor).ceil() as u32).max(1);
3544
3545 let mut pixmap = AzulPixmap::new(w, h)?;
3546 pixmap.fill(bg_color.r, bg_color.g, bg_color.b, bg_color.a);
3547
3548 let clip_rect: crate::solver3::display_list::WindowLogicalRect = LogicalRect {
3550 origin: LogicalPosition { x: 0.0, y: 0.0 },
3551 size: LogicalSize { width: logical_w, height: logical_h },
3552 }
3553 .into();
3554
3555 let item = DisplayListItem::Text {
3556 glyphs,
3557 font_hash,
3558 font_size_px,
3559 color: text_color,
3560 clip_rect,
3561 source_node_index: None,
3562 };
3563 let dl = DisplayList {
3564 items: vec![item],
3565 ..Default::default()
3566 };
3567 let mut gc = GlyphCache::new();
3568 render_display_list(&dl, &mut pixmap, dpi_factor, &rr, &fm, &mut gc).ok()?;
3569
3570 Some(pixmap)
3571}
3572
3573#[cfg(all(test, feature = "std"))]
3579mod text_shadow_tests {
3580 use super::*;
3581 use crate::font::parsed::ParsedFont;
3582 use crate::solver3::display_list::{DisplayList, WindowLogicalRect};
3583 use azul_core::resources::{FontKey, IdNamespace};
3584 use azul_css::props::basic::pixel::{PixelValue, PixelValueNoPercent};
3585 use azul_css::props::style::box_shadow::StyleBoxShadow;
3586
3587 fn load_test_font() -> Option<ParsedFont> {
3588 let candidates = [
3589 "/System/Library/Fonts/Supplemental/Times New Roman.ttf",
3590 "/System/Library/Fonts/Helvetica.ttc",
3591 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
3592 "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
3593 "C:/Windows/Fonts/arial.ttf",
3594 ];
3595 for path in candidates {
3596 if let Ok(bytes) = std::fs::read(path) {
3597 let arc = std::sync::Arc::new(rust_fontconfig::FontBytes::Owned(
3598 std::sync::Arc::from(bytes.as_slice()),
3599 ));
3600 if let Some(font) = ParsedFont::from_bytes(&bytes, 0, &mut Vec::new())
3601 .map(|f| f.with_source_bytes(arc))
3602 {
3603 return Some(font);
3604 }
3605 }
3606 }
3607 None
3608 }
3609
3610 fn renderer_resources_with(
3611 font: &ParsedFont,
3612 ) -> (RendererResources, FontManager<FontRef>, FontHash) {
3613 let rr = RendererResources::default();
3614 let font_ref = crate::parsed_font_to_font_ref(font.clone());
3615 let hash = crate::font_ref_to_parsed_font(&font_ref).hash;
3616 let fm: FontManager<FontRef> =
3617 FontManager::new(rust_fontconfig::FcFontCache::default()).expect("FontManager::new");
3618 fm.insert_font(rust_fontconfig::FontId::new(), font_ref);
3619 (rr, fm, FontHash { font_hash: hash })
3620 }
3621
3622
3623
3624 fn shape(parsed: &ParsedFont, text: &str, font_size: f32, x: f32, y: f32) -> Vec<GlyphInstance> {
3626 let upm = f32::from(parsed.font_metrics.units_per_em);
3627 let scale = font_size / upm;
3628 let mut pen_x = x;
3629 let mut out = Vec::new();
3630 for c in text.chars() {
3631 let gid = parsed.lookup_glyph_index(c as u32).unwrap_or(0);
3632 let advance = f32::from(parsed.get_horizontal_advance(gid)) * scale;
3633 out.push(GlyphInstance {
3634 index: u32::from(gid),
3635 point: LogicalPosition { x: pen_x, y },
3636 size: LogicalSize {
3637 width: advance,
3638 height: font_size,
3639 },
3640 });
3641 pen_x += advance;
3642 }
3643 out
3644 }
3645
3646 fn count_red(pixmap: &AzulPixmap) -> usize {
3647 pixmap
3648 .data()
3649 .chunks_exact(4)
3650 .filter(|p| p[0] > 150 && p[1] < 100 && p[2] < 100)
3651 .count()
3652 }
3653
3654 #[test]
3657 fn text_shadow_paints_offset_colored_pixels() {
3658 let Some(font) = load_test_font() else {
3659 eprintln!("[skip] no system font available");
3660 return;
3661 };
3662 let (rr, fm, font_hash) = renderer_resources_with(&font);
3663
3664 let w = 200u32;
3665 let h = 60u32;
3666 let font_size = 32.0;
3667 let glyphs = shape(&font, "Hi", font_size, 10.0, 40.0);
3669 #[allow(clippy::cast_precision_loss)]
3671 let clip_rect: WindowLogicalRect = LogicalRect {
3672 origin: LogicalPosition { x: 0.0, y: 0.0 },
3673 size: LogicalSize { width: w as f32, height: h as f32 },
3674 }
3675 .into();
3676
3677 let text_item = DisplayListItem::Text {
3678 glyphs,
3679 font_hash,
3680 font_size_px: font_size,
3681 color: ColorU { r: 0, g: 0, b: 0, a: 255 },
3682 clip_rect,
3683 source_node_index: None,
3684 };
3685
3686 let mut gc = GlyphCache::new();
3688 let mut no_shadow = AzulPixmap::new(w, h).unwrap();
3689 no_shadow.fill(255, 255, 255, 255);
3690 let dl_plain = DisplayList {
3691 items: vec![text_item.clone()],
3692 ..Default::default()
3693 };
3694 render_display_list(&dl_plain, &mut no_shadow, 1.0, &rr, &fm, &mut gc).unwrap();
3695 let red_plain = count_red(&no_shadow);
3699
3700 let shadow = StyleBoxShadow {
3702 offset_x: PixelValueNoPercent { inner: PixelValue::px(24.0) },
3703 offset_y: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3704 blur_radius: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3705 spread_radius: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3706 color: ColorU { r: 255, g: 0, b: 0, a: 255 },
3707 clip_mode: azul_css::props::style::box_shadow::BoxShadowClipMode::Outset,
3708 };
3709 let mut with_shadow = AzulPixmap::new(w, h).unwrap();
3710 with_shadow.fill(255, 255, 255, 255);
3711 let dl_shadow = DisplayList {
3712 items: vec![
3713 DisplayListItem::PushTextShadow { shadow },
3714 text_item,
3715 DisplayListItem::PopTextShadow,
3716 ],
3717 ..Default::default()
3718 };
3719 let mut gc2 = GlyphCache::new();
3720 render_display_list(&dl_shadow, &mut with_shadow, 1.0, &rr, &fm, &mut gc2).unwrap();
3721 let red_shadow = count_red(&with_shadow);
3722
3723 assert!(
3724 red_shadow > red_plain + 20,
3725 "text-shadow must paint red shadow pixels beyond the baseline \
3726 (plain {red_plain}, shadow {red_shadow})"
3727 );
3728
3729 let right_red = with_shadow
3733 .data()
3734 .chunks_exact(4)
3735 .enumerate()
3736 .filter(|(i, p)| {
3737 #[allow(clippy::cast_possible_truncation)] let x = (*i as u32) % w;
3739 x > 30 && p[0] > 150 && p[1] < 100 && p[2] < 100
3740 })
3741 .count();
3742 assert!(
3743 right_red > 0,
3744 "shadow should appear offset to the right of the glyphs"
3745 );
3746 }
3747
3748 #[test]
3751 fn text_shadow_blur_spreads_coverage() {
3752 let Some(font) = load_test_font() else {
3753 eprintln!("[skip] no system font available");
3754 return;
3755 };
3756 let (rr, fm, font_hash) = renderer_resources_with(&font);
3757 let w = 200u32;
3758 let h = 80u32;
3759 let font_size = 32.0;
3760 let glyphs = shape(&font, "Hi", font_size, 40.0, 50.0);
3761 #[allow(clippy::cast_precision_loss)]
3763 let clip_rect: WindowLogicalRect = LogicalRect {
3764 origin: LogicalPosition { x: 0.0, y: 0.0 },
3765 size: LogicalSize { width: w as f32, height: h as f32 },
3766 }
3767 .into();
3768
3769 let make = |blur: f32| -> usize {
3770 let shadow = StyleBoxShadow {
3771 offset_x: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3772 offset_y: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3773 blur_radius: PixelValueNoPercent { inner: PixelValue::px(blur) },
3774 spread_radius: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3775 color: ColorU { r: 255, g: 0, b: 0, a: 255 },
3776 clip_mode: azul_css::props::style::box_shadow::BoxShadowClipMode::Outset,
3777 };
3778 let text_item = DisplayListItem::Text {
3779 glyphs: glyphs.clone(),
3780 font_hash,
3781 font_size_px: font_size,
3782 color: ColorU { r: 0, g: 0, b: 0, a: 0 }, clip_rect,
3784 source_node_index: None,
3785 };
3786 let dl = DisplayList {
3787 items: vec![
3788 DisplayListItem::PushTextShadow { shadow },
3789 text_item,
3790 DisplayListItem::PopTextShadow,
3791 ],
3792 ..Default::default()
3793 };
3794 let mut pm = AzulPixmap::new(w, h).unwrap();
3795 pm.fill(255, 255, 255, 255);
3796 let mut gc = GlyphCache::new();
3797 render_display_list(&dl, &mut pm, 1.0, &rr, &fm, &mut gc).unwrap();
3798 pm.data()
3800 .chunks_exact(4)
3801 .filter(|p| p[0] != 255 || p[1] != 255 || p[2] != 255)
3802 .count()
3803 };
3804
3805 let hard = make(0.0);
3806 let blurred = make(6.0);
3807 assert!(hard > 0, "hard shadow should paint");
3808 assert!(
3809 blurred > hard,
3810 "blurred shadow ({blurred}) should cover more pixels than hard ({hard})"
3811 );
3812 }
3813}
3814
3815#[cfg(all(test, feature = "std"))]
3816#[allow(clippy::float_cmp)] #[allow(clippy::many_single_char_names)] #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] mod autotest_generated {
3820 use agg_rust::gradient_lut::ColorFunction;
3821 use azul_core::{
3822 dom::{DomId, NodeId},
3823 gpu::GpuValueCache,
3824 resources::{OpacityKey, RawImage, RawImageData, RawImageFormat, TransformKey},
3825 transform::ComputedTransform3D,
3826 };
3827 use azul_css::{
3828 props::{
3829 basic::{
3830 angle::AngleValue,
3831 length::PercentageValue,
3832 pixel::{PixelValue, PixelValueNoPercent},
3833 color::{OptionColorU, SystemColorRef},
3834 },
3835 style::{
3836 background::{
3837 BackgroundPositionHorizontal, BackgroundPositionVertical, ConicGradient,
3838 LinearGradient, NormalizedLinearColorStop, NormalizedLinearColorStopVec,
3839 NormalizedRadialColorStop, NormalizedRadialColorStopVec, RadialGradient,
3840 RadialGradientSize, Shape, StyleBackgroundPosition,
3841 },
3842 border::BorderStyle,
3843 box_shadow::BoxShadowClipMode,
3844 },
3845 },
3846 system::SystemColors,
3847 };
3848
3849 use super::*;
3850 use crate::solver3::display_list::WindowLogicalRect;
3851
3852 const RED: ColorU = ColorU { r: 255, g: 0, b: 0, a: 255 };
3857 const BLACK: ColorU = ColorU { r: 0, g: 0, b: 0, a: 255 };
3858 const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
3859 const BLUE: ColorU = ColorU { r: 0, g: 0, b: 255, a: 255 };
3860 const CLEAR: ColorU = ColorU { r: 255, g: 0, b: 0, a: 0 };
3861
3862 const DEGENERATE: [f32; 7] = [
3867 0.0,
3868 -0.0,
3869 -1.0,
3870 f32::NAN,
3871 f32::INFINITY,
3872 f32::NEG_INFINITY,
3873 f32::MIN,
3874 ];
3875
3876 fn pixmap(w: u32, h: u32) -> AzulPixmap {
3877 let mut p = AzulPixmap::new(w, h).expect("test pixmap must allocate");
3878 p.fill(255, 255, 255, 255);
3879 p
3880 }
3881
3882 fn snap(p: &AzulPixmap) -> Vec<u8> {
3883 p.data().to_vec()
3884 }
3885
3886 fn px_at(p: &AzulPixmap, x: u32, y: u32) -> [u8; 4] {
3887 let i = ((y * p.width + x) * 4) as usize;
3888 [p.data()[i], p.data()[i + 1], p.data()[i + 2], p.data()[i + 3]]
3889 }
3890
3891 fn is_reddish(px: [u8; 4]) -> bool {
3892 px[0] > 200 && px[1] < 60 && px[2] < 60
3893 }
3894
3895 fn lrect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
3896 LogicalRect {
3897 origin: LogicalPosition { x, y },
3898 size: LogicalSize {
3899 width: w,
3900 height: h,
3901 },
3902 }
3903 }
3904
3905 fn wrect(x: f32, y: f32, w: f32, h: f32) -> WindowLogicalRect {
3906 lrect(x, y, w, h).into()
3907 }
3908
3909 fn lin_stops(pairs: &[(f32, ColorU)]) -> NormalizedLinearColorStopVec {
3910 pairs
3911 .iter()
3912 .map(|(offset_percent, color)| NormalizedLinearColorStop {
3913 offset: PercentageValue::new(*offset_percent),
3914 color: ColorOrSystem::Color(*color),
3915 })
3916 .collect::<Vec<_>>()
3917 .into()
3918 }
3919
3920 fn rad_stops(pairs: &[(f32, ColorU)]) -> NormalizedRadialColorStopVec {
3921 pairs
3922 .iter()
3923 .map(|(degrees, color)| NormalizedRadialColorStop {
3924 angle: AngleValue::deg(*degrees),
3925 color: ColorOrSystem::Color(*color),
3926 })
3927 .collect::<Vec<_>>()
3928 .into()
3929 }
3930
3931 fn shadow(offset: f32, blur: f32, spread: f32, color: ColorU) -> StyleBoxShadow {
3932 StyleBoxShadow {
3933 offset_x: PixelValueNoPercent {
3934 inner: PixelValue::px(offset),
3935 },
3936 offset_y: PixelValueNoPercent {
3937 inner: PixelValue::px(offset),
3938 },
3939 blur_radius: PixelValueNoPercent {
3940 inner: PixelValue::px(blur),
3941 },
3942 spread_radius: PixelValueNoPercent {
3943 inner: PixelValue::px(spread),
3944 },
3945 color,
3946 clip_mode: BoxShadowClipMode::Outset,
3947 }
3948 }
3949
3950 fn r8_image(w: usize, h: usize, bytes: Vec<u8>) -> ImageRef {
3951 ImageRef::new_rawimage(RawImage {
3952 pixels: RawImageData::U8(bytes.into()),
3953 width: w,
3954 height: h,
3955 premultiplied_alpha: false,
3956 data_format: RawImageFormat::R8,
3957 tag: Vec::new().into(),
3958 })
3959 .expect("R8 RawImage must build")
3960 }
3961
3962 fn rgba_image(w: usize, h: usize, bytes: Vec<u8>) -> ImageRef {
3963 ImageRef::new_rawimage(RawImage {
3964 pixels: RawImageData::U8(bytes.into()),
3965 width: w,
3966 height: h,
3967 premultiplied_alpha: false,
3968 data_format: RawImageFormat::RGBA8,
3969 tag: Vec::new().into(),
3970 })
3971 .expect("RGBA8 RawImage must build")
3972 }
3973
3974 struct Stacks {
3977 transforms: Vec<TransAffine>,
3978 clips: Vec<Option<AzRect>>,
3979 masks: Vec<MaskEntry>,
3980 scrolls: Vec<(f32, f32)>,
3981 shadows: Vec<StyleBoxShadow>,
3982 }
3983
3984 impl Stacks {
3985 fn new() -> Self {
3986 Self {
3987 transforms: vec![TransAffine::new()],
3988 clips: vec![None],
3989 masks: Vec::new(),
3990 scrolls: vec![(0.0, 0.0)],
3991 shadows: Vec::new(),
3992 }
3993 }
3994 }
3995
3996 fn run_item(
3998 item: &DisplayListItem,
3999 p: &mut AzulPixmap,
4000 st: &mut Stacks,
4001 state: &CpuRenderState,
4002 ) -> Result<(), String> {
4003 let res = RendererResources::default();
4004 let mut gc = GlyphCache::new();
4005 render_single_item(
4006 item,
4007 p,
4008 1.0,
4009 &res,
4010 &empty_font_manager(),
4011 &mut gc,
4012 &mut st.transforms,
4013 &mut st.clips,
4014 &mut st.masks,
4015 &mut st.scrolls,
4016 &mut st.shadows,
4017 state,
4018 )
4019 }
4020
4021 fn run_list(dl: &DisplayList, p: &mut AzulPixmap, dpi: f32) -> Result<(), String> {
4022 let res = RendererResources::default();
4023 let mut gc = GlyphCache::new();
4024 render_display_list(dl, p, dpi, &res, &empty_font_manager(), &mut gc)
4025 }
4026
4027 fn run_list_with_state(
4028 dl: &DisplayList,
4029 p: &mut AzulPixmap,
4030 state: &CpuRenderState,
4031 ) -> Result<(), String> {
4032 let res = RendererResources::default();
4033 let mut gc = GlyphCache::new();
4034 render_display_list_with_state(dl, p, 1.0, &res, &empty_font_manager(), &mut gc, state)
4035 }
4036
4037 #[test]
4042 fn resolve_color_concrete_is_returned_verbatim() {
4043 let c = ColorU { r: 1, g: 2, b: 3, a: 4 };
4044 let palette = SystemColors {
4045 accent: OptionColorU::Some(BLUE),
4046 ..SystemColors::default()
4047 };
4048 assert_eq!(resolve_color(&ColorOrSystem::Color(c), None), c);
4050 assert_eq!(resolve_color(&ColorOrSystem::Color(c), Some(&palette)), c);
4051 }
4052
4053 #[test]
4054 fn resolve_color_system_without_palette_is_transparent_fallback() {
4055 for key in [
4056 SystemColorRef::Text,
4057 SystemColorRef::Accent,
4058 SystemColorRef::SelectionBackground,
4059 ] {
4060 let got = resolve_color(&ColorOrSystem::System(key), None);
4061 assert_eq!(got, SYSTEM_COLOR_FALLBACK);
4062 assert_eq!(got.a, 0, "the fallback must contribute nothing");
4063 }
4064 }
4065
4066 #[test]
4067 fn resolve_color_system_resolves_set_keys_and_falls_back_for_unset_ones() {
4068 let palette = SystemColors {
4069 accent: OptionColorU::Some(BLUE),
4070 ..SystemColors::default()
4071 };
4072 assert_eq!(
4073 resolve_color(&ColorOrSystem::System(SystemColorRef::Accent), Some(&palette)),
4074 BLUE
4075 );
4076 assert_eq!(
4078 resolve_color(&ColorOrSystem::System(SystemColorRef::Text), Some(&palette)),
4079 SYSTEM_COLOR_FALLBACK
4080 );
4081 assert_eq!(
4083 resolve_color(
4084 &ColorOrSystem::System(SystemColorRef::ButtonFace),
4085 Some(&SystemColors::default())
4086 ),
4087 SYSTEM_COLOR_FALLBACK
4088 );
4089 }
4090
4091 #[test]
4096 fn gradient_lut_linear_under_two_stops_is_fully_transparent() {
4097 for stops in [lin_stops(&[]), lin_stops(&[(50.0, RED)])] {
4098 let lut = build_gradient_lut_linear(&stops, None);
4099 assert_eq!(lut.size(), 256);
4100 for i in [0usize, 1, 128, 255] {
4101 assert_eq!(
4102 lut.get(i).a,
4103 0,
4104 "a gradient with <2 stops must not paint anything"
4105 );
4106 }
4107 }
4108 }
4109
4110 #[test]
4111 fn gradient_lut_linear_two_stops_interpolate_end_to_end() {
4112 let lut = build_gradient_lut_linear(&lin_stops(&[(0.0, BLACK), (100.0, WHITE)]), None);
4113 assert_eq!(lut.size(), 256);
4114 assert_eq!(lut.get(0).r, 0);
4115 assert_eq!(lut.get(255).r, 255);
4116 assert!(lut.get(64).r < lut.get(192).r);
4118 assert_eq!(lut.get(0).a, 255);
4119 }
4120
4121 #[test]
4122 fn gradient_lut_linear_out_of_range_offsets_are_clamped_not_panicking() {
4123 let lut = build_gradient_lut_linear(
4125 &lin_stops(&[(-500.0, BLACK), (900.0, WHITE), (1e30, RED)]),
4126 None,
4127 );
4128 assert_eq!(lut.size(), 256);
4129 assert_eq!(lut.get(0).r, 0, "the -500% stop clamps to offset 0");
4130 assert!(lut.get(255).a > 0);
4132 }
4133
4134 #[test]
4135 fn gradient_lut_linear_unsorted_stops_are_sorted_by_offset() {
4136 let lut = build_gradient_lut_linear(&lin_stops(&[(100.0, WHITE), (0.0, BLACK)]), None);
4138 assert_eq!(lut.get(0).r, 0);
4139 assert_eq!(lut.get(255).r, 255);
4140 }
4141
4142 #[test]
4143 fn gradient_lut_linear_duplicate_offsets_degrade_to_transparent_not_panic() {
4144 let lut = build_gradient_lut_linear(&lin_stops(&[(50.0, RED), (50.0, BLUE)]), None);
4148 assert_eq!(lut.size(), 256);
4149 assert_eq!(lut.get(128).a, 0);
4150 }
4151
4152 #[test]
4153 fn gradient_lut_linear_resolves_system_stops_against_the_palette() {
4154 let palette = SystemColors {
4155 accent: OptionColorU::Some(BLUE),
4156 ..SystemColors::default()
4157 };
4158 let stops: NormalizedLinearColorStopVec = vec![
4159 NormalizedLinearColorStop {
4160 offset: PercentageValue::new(0.0),
4161 color: ColorOrSystem::System(SystemColorRef::Accent),
4162 },
4163 NormalizedLinearColorStop {
4164 offset: PercentageValue::new(100.0),
4165 color: ColorOrSystem::Color(WHITE),
4166 },
4167 ]
4168 .into();
4169
4170 let with_palette = build_gradient_lut_linear(&stops, Some(&palette));
4171 assert_eq!(with_palette.get(0).b, 255, "system:accent must resolve to blue");
4172 assert_eq!(with_palette.get(0).a, 255);
4173
4174 let without = build_gradient_lut_linear(&stops, None);
4176 assert_eq!(without.get(0).a, 0);
4177 }
4178
4179 #[test]
4180 fn gradient_lut_radial_distinct_angles_interpolate() {
4181 let lut = build_gradient_lut_radial(&rad_stops(&[(0.0, BLACK), (180.0, WHITE)]), None);
4182 assert_eq!(lut.size(), 256);
4183 assert_eq!(lut.get(0).r, 0);
4184 assert_eq!(lut.get(255).r, 255);
4186 assert!(lut.get(64).r < lut.get(127).r);
4187 }
4188
4189 #[test]
4190 fn gradient_lut_radial_extreme_angles_do_not_panic() {
4191 for angles in [
4193 [-720.0_f32, 90.0],
4194 [1e30, 45.0],
4195 [f32::NAN, 90.0],
4196 [f32::INFINITY, 270.0],
4197 ] {
4198 let lut = build_gradient_lut_radial(
4199 &rad_stops(&[(angles[0], RED), (angles[1], BLUE)]),
4200 None,
4201 );
4202 assert_eq!(lut.size(), 256, "angles {angles:?} must still build a LUT");
4203 }
4204 }
4205
4206 #[test]
4211 fn resolve_background_position_keywords_map_to_fractions() {
4212 let cases = [
4213 (
4214 BackgroundPositionHorizontal::Left,
4215 BackgroundPositionVertical::Top,
4216 (0.0, 0.0),
4217 ),
4218 (
4219 BackgroundPositionHorizontal::Center,
4220 BackgroundPositionVertical::Center,
4221 (0.5, 0.5),
4222 ),
4223 (
4224 BackgroundPositionHorizontal::Right,
4225 BackgroundPositionVertical::Bottom,
4226 (1.0, 1.0),
4227 ),
4228 ];
4229 for (horizontal, vertical, expected) in cases {
4230 let pos = StyleBackgroundPosition {
4231 horizontal,
4232 vertical,
4233 };
4234 assert_eq!(resolve_background_position(&pos, 200.0, 100.0), expected);
4235 }
4236 }
4237
4238 #[test]
4239 fn resolve_background_position_exact_px_is_a_fraction_of_the_box() {
4240 let pos = StyleBackgroundPosition {
4241 horizontal: BackgroundPositionHorizontal::Exact(PixelValue::px(50.0)),
4242 vertical: BackgroundPositionVertical::Exact(PixelValue::px(25.0)),
4243 };
4244 assert_eq!(resolve_background_position(&pos, 200.0, 100.0), (0.25, 0.25));
4245 }
4246
4247 #[test]
4248 fn resolve_background_position_exact_percent_resolves_against_the_box() {
4249 let pos = StyleBackgroundPosition {
4250 horizontal: BackgroundPositionHorizontal::Exact(PixelValue::percent(50.0)),
4251 vertical: BackgroundPositionVertical::Exact(PixelValue::percent(10.0)),
4252 };
4253 let (x, y) = resolve_background_position(&pos, 200.0, 100.0);
4254 assert!((x - 0.5).abs() < 1e-4, "50% of the width is the center, got {x}");
4255 assert!((y - 0.1).abs() < 1e-4, "10% of the height, got {y}");
4256 }
4257
4258 #[test]
4259 fn resolve_background_position_zero_box_falls_back_to_center() {
4260 let pos = StyleBackgroundPosition {
4262 horizontal: BackgroundPositionHorizontal::Exact(PixelValue::px(10.0)),
4263 vertical: BackgroundPositionVertical::Exact(PixelValue::px(10.0)),
4264 };
4265 assert_eq!(resolve_background_position(&pos, 0.0, 0.0), (0.5, 0.5));
4266 }
4267
4268 #[test]
4269 fn resolve_background_position_never_returns_nan_for_degenerate_boxes() {
4270 let pos = StyleBackgroundPosition {
4271 horizontal: BackgroundPositionHorizontal::Exact(PixelValue::px(10.0)),
4272 vertical: BackgroundPositionVertical::Exact(PixelValue::px(-10.0)),
4273 };
4274 for w in DEGENERATE {
4275 for h in DEGENERATE {
4276 let (x, y) = resolve_background_position(&pos, w, h);
4277 assert!(
4278 !x.is_nan() && !y.is_nan(),
4279 "w={w}, h={h} produced NaN ({x}, {y}) — a NaN center poisons the gradient transform"
4280 );
4281 }
4282 }
4283 let (x, y) = resolve_background_position(&pos, f32::MAX, f32::MAX);
4285 assert!(x.is_finite() && y.is_finite());
4286 }
4287
4288 #[test]
4293 fn render_rect_paints_exactly_its_bounds() {
4294 let mut p = pixmap(10, 10);
4295 render_rect(
4296 &mut p,
4297 &lrect(2.0, 2.0, 4.0, 4.0),
4298 RED,
4299 &BorderRadius::default(),
4300 None,
4301 1.0,
4302 );
4303 assert!(is_reddish(px_at(&p, 3, 3)), "inside the rect must be red");
4304 assert_eq!(px_at(&p, 0, 0), [255, 255, 255, 255], "outside stays white");
4305 let red = p.data().chunks_exact(4).filter(|c| c[0] > 200 && c[1] < 60).count();
4306 assert_eq!(red, 16, "a 4x4 rect covers exactly 16 pixels");
4307 }
4308
4309 #[test]
4310 fn render_rect_transparent_color_is_a_noop() {
4311 let mut p = pixmap(8, 8);
4312 let before = snap(&p);
4313 render_rect(
4314 &mut p,
4315 &lrect(0.0, 0.0, 8.0, 8.0),
4316 CLEAR,
4317 &BorderRadius::default(),
4318 None,
4319 1.0,
4320 );
4321 assert_eq!(before, p.data(), "alpha=0 must not touch the buffer");
4322 }
4323
4324 #[test]
4325 fn render_rect_degenerate_bounds_are_noops() {
4326 for bad in DEGENERATE {
4327 let mut p = pixmap(8, 8);
4328 let before = snap(&p);
4329 render_rect(
4330 &mut p,
4331 &lrect(0.0, 0.0, bad, bad),
4332 RED,
4333 &BorderRadius::default(),
4334 None,
4335 1.0,
4336 );
4337 assert_eq!(before, p.data(), "size {bad} must be rejected, not painted");
4338
4339 if bad == f32::MIN {
4343 continue;
4344 }
4345 let mut p = pixmap(8, 8);
4346 let before = snap(&p);
4347 render_rect(
4348 &mut p,
4349 &lrect(bad, bad, 4.0, 4.0),
4350 RED,
4351 &BorderRadius::default(),
4352 None,
4353 1.0,
4354 );
4355 if !bad.is_finite() {
4356 assert_eq!(before, p.data(), "origin {bad} must be rejected");
4357 }
4358 }
4359 }
4360
4361 #[test]
4362 fn render_rect_degenerate_dpi_is_a_noop() {
4363 for dpi in DEGENERATE {
4366 let mut p = pixmap(8, 8);
4367 let before = snap(&p);
4368 render_rect(
4369 &mut p,
4370 &lrect(1.0, 1.0, 4.0, 4.0),
4371 RED,
4372 &BorderRadius::default(),
4373 None,
4374 dpi,
4375 );
4376 assert_eq!(before, p.data(), "dpi {dpi} must be rejected, not painted");
4377 }
4378 let mut p = pixmap(8, 8);
4380 let before = snap(&p);
4381 render_rect(
4382 &mut p,
4383 &lrect(1.0, 1.0, 4.0, 4.0),
4384 RED,
4385 &BorderRadius::default(),
4386 None,
4387 f32::MAX,
4388 );
4389 assert_eq!(before, p.data());
4390 }
4391
4392 #[test]
4393 fn render_rect_saturating_bounds_clamp_to_the_pixmap() {
4394 let mut p = pixmap(8, 8);
4397 render_rect(
4398 &mut p,
4399 &lrect(0.0, 0.0, f32::MAX, f32::MAX),
4400 RED,
4401 &BorderRadius::default(),
4402 None,
4403 1.0,
4404 );
4405 assert!(p.data().chunks_exact(4).all(|c| c[0] > 200 && c[1] < 60));
4406 }
4407
4408 #[test]
4409 fn render_rect_negative_origin_clamps_to_the_pixmap() {
4410 let mut p = pixmap(8, 8);
4411 render_rect(
4412 &mut p,
4413 &lrect(-1e9, -1e9, 2e9, 2e9),
4414 RED,
4415 &BorderRadius::default(),
4416 None,
4417 1.0,
4418 );
4419 assert!(is_reddish(px_at(&p, 0, 0)));
4420 assert!(is_reddish(px_at(&p, 7, 7)));
4421 }
4422
4423 #[test]
4424 fn render_rect_fully_outside_the_clip_is_a_noop() {
4425 let mut p = pixmap(10, 10);
4426 let before = snap(&p);
4427 let clip = AzRect::from_xywh(0.0, 0.0, 2.0, 2.0).unwrap();
4428 render_rect(
4429 &mut p,
4430 &lrect(5.0, 5.0, 3.0, 3.0),
4431 RED,
4432 &BorderRadius::default(),
4433 Some(clip),
4434 1.0,
4435 );
4436 assert_eq!(before, p.data());
4437 }
4438
4439 #[test]
4440 fn render_rect_clip_narrows_the_painted_area() {
4441 let mut p = pixmap(10, 10);
4442 let clip = AzRect::from_xywh(0.0, 0.0, 2.0, 2.0).unwrap();
4443 render_rect(
4444 &mut p,
4445 &lrect(0.0, 0.0, 10.0, 10.0),
4446 RED,
4447 &BorderRadius::default(),
4448 Some(clip),
4449 1.0,
4450 );
4451 let red = p.data().chunks_exact(4).filter(|c| c[0] > 200 && c[1] < 60).count();
4452 assert_eq!(red, 4, "only the 2x2 clip region may be painted");
4453 }
4454
4455 #[test]
4456 fn render_rect_rounded_corners_leave_the_corner_pixel_unpainted() {
4457 let mut p = pixmap(20, 20);
4458 let radius = BorderRadius {
4459 top_left: 6.0,
4460 top_right: 6.0,
4461 bottom_left: 6.0,
4462 bottom_right: 6.0,
4463 };
4464 render_rect(&mut p, &lrect(0.0, 0.0, 20.0, 20.0), RED, &radius, None, 1.0);
4465 assert!(is_reddish(px_at(&p, 10, 10)), "the middle is filled");
4466 assert_eq!(
4467 px_at(&p, 0, 0),
4468 [255, 255, 255, 255],
4469 "the rounded corner must not be filled"
4470 );
4471 }
4472
4473 #[test]
4474 fn render_rect_radius_larger_than_the_rect_does_not_panic() {
4475 let mut p = pixmap(10, 10);
4476 let radius = BorderRadius {
4477 top_left: 1e6,
4478 top_right: 1e6,
4479 bottom_left: 1e6,
4480 bottom_right: 1e6,
4481 };
4482 render_rect(&mut p, &lrect(0.0, 0.0, 10.0, 10.0), RED, &radius, None, 1.0);
4483 assert!(is_reddish(px_at(&p, 5, 5)));
4485 }
4486
4487 fn linear(stops: NormalizedLinearColorStopVec) -> LinearGradient {
4492 LinearGradient {
4493 stops,
4494 ..LinearGradient::default()
4495 }
4496 }
4497
4498 #[test]
4499 fn linear_gradient_paints_a_ramp_top_to_bottom() {
4500 let mut p = pixmap(16, 16);
4501 render_linear_gradient(
4502 &mut p,
4503 &lrect(0.0, 0.0, 16.0, 16.0),
4504 &linear(lin_stops(&[(0.0, BLACK), (100.0, WHITE)])),
4505 &BorderRadius::default(),
4506 None,
4507 1.0,
4508 None,
4509 );
4510 let top = px_at(&p, 8, 0)[0];
4511 let bottom = px_at(&p, 8, 15)[0];
4512 assert!(
4513 top < bottom,
4514 "the default Top->Bottom direction must ramp dark->light (top {top}, bottom {bottom})"
4515 );
4516 }
4517
4518 #[test]
4519 fn linear_gradient_without_stops_is_a_noop() {
4520 let mut p = pixmap(8, 8);
4521 let before = snap(&p);
4522 render_linear_gradient(
4523 &mut p,
4524 &lrect(0.0, 0.0, 8.0, 8.0),
4525 &linear(lin_stops(&[])),
4526 &BorderRadius::default(),
4527 None,
4528 1.0,
4529 None,
4530 );
4531 assert_eq!(before, p.data());
4532 }
4533
4534 #[test]
4535 fn linear_gradient_single_stop_paints_nothing() {
4536 let mut p = pixmap(8, 8);
4538 let before = snap(&p);
4539 render_linear_gradient(
4540 &mut p,
4541 &lrect(0.0, 0.0, 8.0, 8.0),
4542 &linear(lin_stops(&[(50.0, RED)])),
4543 &BorderRadius::default(),
4544 None,
4545 1.0,
4546 None,
4547 );
4548 assert_eq!(before, p.data());
4549 }
4550
4551 #[test]
4552 fn linear_gradient_degenerate_geometry_is_a_noop() {
4553 for bad in DEGENERATE {
4554 let mut p = pixmap(8, 8);
4555 let before = snap(&p);
4556 render_linear_gradient(
4557 &mut p,
4558 &lrect(0.0, 0.0, 8.0, 8.0),
4559 &linear(lin_stops(&[(0.0, BLACK), (100.0, WHITE)])),
4560 &BorderRadius::default(),
4561 None,
4562 bad,
4563 None,
4564 );
4565 assert_eq!(before, p.data(), "dpi {bad} must be rejected");
4566
4567 let mut p = pixmap(8, 8);
4568 let before = snap(&p);
4569 render_linear_gradient(
4570 &mut p,
4571 &lrect(0.0, 0.0, bad, bad),
4572 &linear(lin_stops(&[(0.0, BLACK), (100.0, WHITE)])),
4573 &BorderRadius::default(),
4574 None,
4575 1.0,
4576 None,
4577 );
4578 assert_eq!(before, p.data(), "size {bad} must be rejected");
4579 }
4580 }
4581
4582 #[test]
4583 fn radial_gradient_zero_radius_is_a_noop() {
4584 let gradient = RadialGradient {
4586 shape: Shape::Circle,
4587 size: RadialGradientSize::ClosestSide,
4588 position: StyleBackgroundPosition {
4589 horizontal: BackgroundPositionHorizontal::Left,
4590 vertical: BackgroundPositionVertical::Top,
4591 },
4592 stops: lin_stops(&[(0.0, BLACK), (100.0, WHITE)]),
4593 ..RadialGradient::default()
4594 };
4595 let mut p = pixmap(8, 8);
4596 let before = snap(&p);
4597 render_radial_gradient(
4598 &mut p,
4599 &lrect(0.0, 0.0, 8.0, 8.0),
4600 &gradient,
4601 &BorderRadius::default(),
4602 None,
4603 1.0,
4604 None,
4605 );
4606 assert_eq!(before, p.data(), "a 0-radius gradient must paint nothing");
4607 }
4608
4609 #[test]
4610 fn radial_gradient_paints_from_the_center_outward() {
4611 let gradient = RadialGradient {
4612 shape: Shape::Circle,
4613 size: RadialGradientSize::FarthestCorner,
4614 position: StyleBackgroundPosition {
4615 horizontal: BackgroundPositionHorizontal::Center,
4616 vertical: BackgroundPositionVertical::Center,
4617 },
4618 stops: lin_stops(&[(0.0, BLACK), (100.0, WHITE)]),
4619 ..RadialGradient::default()
4620 };
4621 let mut p = pixmap(16, 16);
4622 render_radial_gradient(
4623 &mut p,
4624 &lrect(0.0, 0.0, 16.0, 16.0),
4625 &gradient,
4626 &BorderRadius::default(),
4627 None,
4628 1.0,
4629 None,
4630 );
4631 let center = px_at(&p, 8, 8)[0];
4632 let corner = px_at(&p, 0, 0)[0];
4633 assert!(
4634 center < corner,
4635 "the center stop is black, the rim white (center {center}, corner {corner})"
4636 );
4637 }
4638
4639 #[test]
4640 fn radial_gradient_empty_stops_and_degenerate_dpi_are_noops() {
4641 let empty = RadialGradient {
4642 stops: lin_stops(&[]),
4643 ..RadialGradient::default()
4644 };
4645 let mut p = pixmap(8, 8);
4646 let before = snap(&p);
4647 render_radial_gradient(
4648 &mut p,
4649 &lrect(0.0, 0.0, 8.0, 8.0),
4650 &empty,
4651 &BorderRadius::default(),
4652 None,
4653 1.0,
4654 None,
4655 );
4656 assert_eq!(before, p.data());
4657
4658 let filled = RadialGradient {
4659 stops: lin_stops(&[(0.0, BLACK), (100.0, WHITE)]),
4660 ..RadialGradient::default()
4661 };
4662 for bad in DEGENERATE {
4663 let mut p = pixmap(8, 8);
4664 let before = snap(&p);
4665 render_radial_gradient(
4666 &mut p,
4667 &lrect(0.0, 0.0, 8.0, 8.0),
4668 &filled,
4669 &BorderRadius::default(),
4670 None,
4671 bad,
4672 None,
4673 );
4674 assert_eq!(before, p.data(), "dpi {bad} must be rejected");
4675 }
4676 }
4677
4678 #[test]
4679 fn conic_gradient_empty_stops_and_degenerate_dpi_are_noops() {
4680 let empty = ConicGradient {
4681 stops: rad_stops(&[]),
4682 ..ConicGradient::default()
4683 };
4684 let mut p = pixmap(8, 8);
4685 let before = snap(&p);
4686 render_conic_gradient(
4687 &mut p,
4688 &lrect(0.0, 0.0, 8.0, 8.0),
4689 &empty,
4690 &BorderRadius::default(),
4691 None,
4692 1.0,
4693 None,
4694 );
4695 assert_eq!(before, p.data());
4696
4697 let filled = ConicGradient {
4698 stops: rad_stops(&[(0.0, BLACK), (180.0, WHITE)]),
4699 ..ConicGradient::default()
4700 };
4701 for bad in DEGENERATE {
4702 let mut p = pixmap(8, 8);
4703 let before = snap(&p);
4704 render_conic_gradient(
4705 &mut p,
4706 &lrect(0.0, 0.0, 8.0, 8.0),
4707 &filled,
4708 &BorderRadius::default(),
4709 None,
4710 bad,
4711 None,
4712 );
4713 assert_eq!(before, p.data(), "dpi {bad} must be rejected");
4714 }
4715 }
4716
4717 #[test]
4718 fn conic_gradient_with_distinct_angle_stops_paints() {
4719 let gradient = ConicGradient {
4720 stops: rad_stops(&[(0.0, BLACK), (180.0, WHITE)]),
4721 ..ConicGradient::default()
4722 };
4723 let mut p = pixmap(16, 16);
4724 let before = snap(&p);
4725 render_conic_gradient(
4726 &mut p,
4727 &lrect(0.0, 0.0, 16.0, 16.0),
4728 &gradient,
4729 &BorderRadius::default(),
4730 None,
4731 1.0,
4732 None,
4733 );
4734 assert_ne!(before, p.data(), "a 2-stop conic gradient must paint");
4735 }
4736
4737 #[test]
4745 fn conic_gradient_full_circle_stops_paint_the_rect() {
4746 let gradient = ConicGradient {
4747 stops: rad_stops(&[(0.0, BLACK), (360.0, WHITE)]),
4748 ..ConicGradient::default()
4749 };
4750 let mut p = pixmap(16, 16);
4751 let before = snap(&p);
4752 render_conic_gradient(
4753 &mut p,
4754 &lrect(0.0, 0.0, 16.0, 16.0),
4755 &gradient,
4756 &BorderRadius::default(),
4757 None,
4758 1.0,
4759 None,
4760 );
4761 assert_ne!(
4762 before,
4763 p.data(),
4764 "conic-gradient(black, white) normalizes to 0deg/360deg and must still paint"
4765 );
4766 }
4767
4768 #[test]
4773 fn box_shadow_paints_under_the_bounds() {
4774 let mut p = pixmap(40, 40);
4775 let res = render_box_shadow(
4776 &mut p,
4777 &lrect(10.0, 10.0, 20.0, 20.0),
4778 &shadow(0.0, 0.0, 0.0, BLACK),
4779 &BorderRadius::default(),
4780 1.0,
4781 );
4782 assert!(res.is_ok());
4783 let dark = p.data().chunks_exact(4).filter(|c| c[0] < 50).count();
4784 assert!(dark > 100, "a hard 20x20 shadow must darken the box, got {dark}");
4785 }
4786
4787 #[test]
4788 fn box_shadow_transparent_color_is_ok_and_a_noop() {
4789 let mut p = pixmap(20, 20);
4790 let before = snap(&p);
4791 let res = render_box_shadow(
4792 &mut p,
4793 &lrect(5.0, 5.0, 10.0, 10.0),
4794 &shadow(0.0, 4.0, 0.0, CLEAR),
4795 &BorderRadius::default(),
4796 1.0,
4797 );
4798 assert_eq!(res, Ok(()));
4799 assert_eq!(before, p.data());
4800 }
4801
4802 #[test]
4803 fn box_shadow_oversized_blur_is_rejected_without_allocating() {
4804 let mut p = pixmap(20, 20);
4807 let before = snap(&p);
4808 let res = render_box_shadow(
4809 &mut p,
4810 &lrect(5.0, 5.0, 10.0, 10.0),
4811 &shadow(0.0, 1e6, 0.0, BLACK),
4812 &BorderRadius::default(),
4813 1.0,
4814 );
4815 assert_eq!(res, Ok(()));
4816 assert_eq!(before, p.data(), "an oversized shadow must be skipped");
4817 }
4818
4819 #[test]
4820 fn box_shadow_huge_negative_spread_collapses_to_a_noop() {
4821 let mut p = pixmap(20, 20);
4822 let before = snap(&p);
4823 let res = render_box_shadow(
4824 &mut p,
4825 &lrect(5.0, 5.0, 10.0, 10.0),
4826 &shadow(0.0, 0.0, -1e6, BLACK),
4827 &BorderRadius::default(),
4828 1.0,
4829 );
4830 assert_eq!(res, Ok(()));
4831 assert_eq!(before, p.data(), "a fully-shrunk shadow paints nothing");
4832 }
4833
4834 #[test]
4835 fn box_shadow_degenerate_geometry_is_ok_and_a_noop() {
4836 for bad in DEGENERATE {
4837 let mut p = pixmap(20, 20);
4838 let before = snap(&p);
4839 let res = render_box_shadow(
4840 &mut p,
4841 &lrect(5.0, 5.0, 10.0, 10.0),
4842 &shadow(0.0, 2.0, 0.0, BLACK),
4843 &BorderRadius::default(),
4844 bad,
4845 );
4846 assert_eq!(res, Ok(()), "dpi {bad} must not error");
4847 assert_eq!(before, p.data(), "dpi {bad} must not paint");
4848
4849 let mut p = pixmap(20, 20);
4850 let before = snap(&p);
4851 let res = render_box_shadow(
4852 &mut p,
4853 &lrect(0.0, 0.0, bad, bad),
4854 &shadow(0.0, 2.0, 0.0, BLACK),
4855 &BorderRadius::default(),
4856 1.0,
4857 );
4858 assert_eq!(res, Ok(()), "size {bad} must not error");
4859 assert_eq!(before, p.data(), "size {bad} must not paint");
4860 }
4861 }
4862
4863 #[test]
4868 fn extract_mask_data_zero_target_is_none() {
4869 let img = r8_image(2, 2, vec![0, 64, 128, 255]);
4870 assert!(extract_mask_data(&img, 0, 4).is_none());
4871 assert!(extract_mask_data(&img, 4, 0).is_none());
4872 assert!(extract_mask_data(&img, 0, 0).is_none());
4873 }
4874
4875 #[test]
4876 fn extract_mask_data_r8_identity_scale_is_a_passthrough() {
4877 let img = r8_image(2, 2, vec![0, 64, 128, 255]);
4878 let mask = extract_mask_data(&img, 2, 2).expect("R8 mask must extract");
4879 assert_eq!(mask, vec![0, 64, 128, 255]);
4880 }
4881
4882 #[test]
4883 fn extract_mask_data_upscales_nearest_neighbour() {
4884 let img = r8_image(2, 2, vec![0, 255, 255, 0]);
4885 let mask = extract_mask_data(&img, 4, 4).expect("mask must extract");
4886 assert_eq!(mask.len(), 16);
4887 assert_eq!(
4889 mask,
4890 vec![
4891 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0,
4895 ]
4896 );
4897 }
4898
4899 #[test]
4900 fn extract_mask_data_downscales_without_reading_out_of_bounds() {
4901 let img = r8_image(4, 4, (0..16).map(|i| i as u8 * 16).collect());
4902 let mask = extract_mask_data(&img, 1, 1).expect("mask must extract");
4903 assert_eq!(mask, vec![0], "1x1 nearest-neighbour samples the first texel");
4904
4905 let mask = extract_mask_data(&img, 8, 2).expect("mask must extract");
4907 assert_eq!(mask.len(), 16);
4908 }
4909
4910 #[test]
4911 fn extract_mask_data_bgra_source_uses_the_alpha_channel() {
4912 let px = vec![
4914 255, 0, 0, 0, 0, 255, 0, 85, 0, 0, 255, 170, 9, 9, 9, 255, ];
4919 let img = rgba_image(2, 2, px);
4920 let mask = extract_mask_data(&img, 2, 2).expect("BGRA mask must extract");
4921 assert_eq!(mask, vec![0, 85, 170, 255]);
4922 }
4923
4924 #[test]
4925 fn extract_mask_data_target_length_always_matches_the_request() {
4926 let img = r8_image(3, 3, vec![7; 9]);
4927 for (w, h) in [(1u32, 1u32), (2, 5), (5, 2), (16, 16), (1, 64)] {
4928 let mask = extract_mask_data(&img, w, h).expect("mask must extract");
4929 assert_eq!(mask.len(), (w * h) as usize, "target {w}x{h}");
4930 assert!(mask.iter().all(|&v| v == 7));
4931 }
4932 }
4933
4934 fn image_mask_entry(
4939 snapshot: Vec<u8>,
4940 mask_data: Vec<u8>,
4941 origin: (i32, i32),
4942 size: (u32, u32),
4943 ) -> MaskEntry {
4944 MaskEntry::ImageMask {
4945 snapshot,
4946 mask_data,
4947 origin_x: origin.0,
4948 origin_y: origin.1,
4949 width: size.0,
4950 height: size.1,
4951 }
4952 }
4953
4954 #[test]
4955 fn apply_mask_zero_mask_restores_the_snapshot() {
4956 let mut p = pixmap(4, 4);
4957 let snapshot = snapshot_region(&p, 0, 0, 4, 4); p.fill(0, 0, 0, 255); apply_mask(
4960 &mut p,
4961 &image_mask_entry(snapshot, vec![0; 16], (0, 0), (4, 4)),
4962 );
4963 assert!(
4964 p.data().chunks_exact(4).all(|c| c[0] == 255 && c[1] == 255),
4965 "mask=0 means fully clipped -> the pre-mask snapshot is restored"
4966 );
4967 }
4968
4969 #[test]
4970 fn apply_mask_opaque_mask_keeps_the_current_pixels() {
4971 let mut p = pixmap(4, 4);
4972 let snapshot = snapshot_region(&p, 0, 0, 4, 4);
4973 p.fill(0, 0, 0, 255);
4974 apply_mask(
4975 &mut p,
4976 &image_mask_entry(snapshot, vec![255; 16], (0, 0), (4, 4)),
4977 );
4978 assert!(
4979 p.data().chunks_exact(4).all(|c| c[0] == 0),
4980 "mask=255 means fully visible -> the drawing survives"
4981 );
4982 }
4983
4984 #[test]
4985 fn apply_mask_opacity_entry_is_ignored() {
4986 let mut p = pixmap(4, 4);
4987 p.fill(0, 0, 0, 255);
4988 let before = snap(&p);
4989 apply_mask(
4990 &mut p,
4991 &MaskEntry::Opacity {
4992 snapshot: vec![255; 64],
4993 rect: AzRect::from_xywh(0.0, 0.0, 4.0, 4.0).unwrap(),
4994 opacity: 0.5,
4995 },
4996 );
4997 assert_eq!(before, p.data(), "apply_mask only handles ImageMask entries");
4998 }
4999
5000 #[test]
5001 fn apply_mask_out_of_bounds_origin_does_not_panic_or_write() {
5002 let mut p = pixmap(4, 4);
5003 p.fill(0, 0, 0, 255);
5004 let before = snap(&p);
5005 for origin in [(-100, -100), (100, 100), (i32::MIN, 0), (0, i32::MIN)] {
5009 apply_mask(
5010 &mut p,
5011 &image_mask_entry(vec![255; 64], vec![0; 16], origin, (4, 4)),
5012 );
5013 }
5014 assert_eq!(before, p.data(), "off-buffer masks must be skipped entirely");
5015 }
5016
5017 #[test]
5018 fn apply_mask_truncated_mask_data_is_treated_as_zero() {
5019 let mut p = pixmap(4, 4);
5020 let snapshot = snapshot_region(&p, 0, 0, 4, 4);
5021 p.fill(0, 0, 0, 255);
5022 apply_mask(
5025 &mut p,
5026 &image_mask_entry(snapshot, vec![255; 4], (0, 0), (4, 4)),
5027 );
5028 assert_eq!(px_at(&p, 0, 0), [0, 0, 0, 255], "the covered texels stay");
5029 assert_eq!(
5030 px_at(&p, 0, 3),
5031 [255, 255, 255, 255],
5032 "missing mask bytes restore the snapshot"
5033 );
5034 }
5035
5036 #[test]
5037 fn apply_mask_partially_offscreen_only_touches_visible_pixels() {
5038 let mut p = pixmap(4, 4);
5039 let snapshot = snapshot_region(&p, -2, -2, 4, 4);
5040 p.fill(0, 0, 0, 255);
5041 apply_mask(
5042 &mut p,
5043 &image_mask_entry(snapshot, vec![0; 16], (-2, -2), (4, 4)),
5044 );
5045 assert_eq!(px_at(&p, 3, 3), [0, 0, 0, 255]);
5047 }
5048
5049 #[test]
5054 fn acquire_pixmap_zero_dimensions_error_instead_of_allocating() {
5055 assert!(acquire_pixmap(None, 0, 0).is_err());
5056 assert!(acquire_pixmap(None, 0, 4).is_err());
5057 assert!(acquire_pixmap(None, 4, 0).is_err());
5058 assert!(acquire_pixmap(Some(pixmap(4, 4)), 0, 4).is_err());
5061 }
5062
5063 #[test]
5064 fn acquire_pixmap_reuses_a_matching_retained_buffer_verbatim() {
5065 let mut retained = pixmap(4, 4);
5066 retained.fill(1, 2, 3, 4);
5067 let got = acquire_pixmap(Some(retained), 4, 4).expect("must reuse");
5068 assert_eq!(got.width, 4);
5069 assert_eq!(got.height, 4);
5070 assert_eq!(
5071 &got.data()[0..4],
5072 &[1, 2, 3, 4],
5073 "reuse must not clear — the caller does that"
5074 );
5075 }
5076
5077 #[test]
5078 fn acquire_pixmap_allocates_fresh_on_a_size_mismatch() {
5079 let mut retained = pixmap(4, 4);
5080 retained.fill(1, 2, 3, 4);
5081 let got = acquire_pixmap(Some(retained), 5, 5).expect("must allocate");
5082 assert_eq!((got.width, got.height), (5, 5));
5083 assert_eq!(&got.data()[0..4], &[255, 255, 255, 255], "fresh = opaque white");
5084 }
5085
5086 fn opts(width: f32, height: f32, dpi_factor: f32) -> RenderOptions {
5091 RenderOptions {
5092 width,
5093 height,
5094 dpi_factor,
5095 }
5096 }
5097
5098 #[test]
5099 fn render_empty_display_list_is_opaque_white() {
5100 let dl = DisplayList::default();
5101 let res = RendererResources::default();
5102 let mut gc = GlyphCache::new();
5103 let p = render(&dl, &res, &empty_font_manager(), opts(4.0, 4.0, 1.0), &mut gc).expect("must render");
5104 assert_eq!((p.width, p.height), (4, 4));
5105 assert!(p
5106 .data()
5107 .chunks_exact(4)
5108 .all(|c| c[0] == 255 && c[1] == 255 && c[2] == 255 && c[3] == 255));
5109 }
5110
5111 #[test]
5112 fn render_applies_the_dpi_factor_to_the_pixmap_size() {
5113 let dl = DisplayList::default();
5114 let res = RendererResources::default();
5115 let mut gc = GlyphCache::new();
5116 let p = render(&dl, &res, &empty_font_manager(), opts(4.0, 3.0, 2.0), &mut gc).expect("must render");
5117 assert_eq!((p.width, p.height), (8, 6));
5118 }
5119
5120 #[test]
5121 fn render_collapsing_dimensions_error_instead_of_panicking() {
5122 let dl = DisplayList::default();
5123 let res = RendererResources::default();
5124 let mut gc = GlyphCache::new();
5125 for o in [
5127 opts(0.0, 4.0, 1.0),
5128 opts(4.0, 0.0, 1.0),
5129 opts(-4.0, -4.0, 1.0),
5130 opts(f32::NAN, f32::NAN, 1.0),
5131 opts(4.0, 4.0, 0.0),
5132 opts(4.0, 4.0, -1.0),
5133 opts(4.0, 4.0, f32::NAN),
5134 opts(0.4, 0.4, 1.0), ] {
5136 let got = render(&dl, &res, &empty_font_manager(), o, &mut gc);
5137 assert!(
5138 got.is_err(),
5139 "{o:?} must return Err, not panic or allocate a 0-sized buffer"
5140 );
5141 }
5142 }
5143
5144 #[test]
5145 fn render_paints_display_list_items() {
5146 let dl = DisplayList {
5147 items: vec![DisplayListItem::Rect {
5148 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5149 color: RED,
5150 border_radius: BorderRadius::default(),
5151 }],
5152 ..Default::default()
5153 };
5154 let res = RendererResources::default();
5155 let mut gc = GlyphCache::new();
5156 let p = render(&dl, &res, &empty_font_manager(), opts(8.0, 8.0, 1.0), &mut gc).expect("must render");
5157 assert!(is_reddish(px_at(&p, 1, 1)));
5158 assert_eq!(px_at(&p, 7, 7), [255, 255, 255, 255]);
5159 }
5160
5161 #[test]
5166 fn cpu_render_state_new_keeps_the_scroll_offsets_and_empties_the_rest() {
5167 let mut offsets = ScrollOffsetMap::new();
5168 offsets.insert(7, (1.0, 2.0));
5169 let state = CpuRenderState::new(offsets);
5170 assert_eq!(state.scroll_offsets.get(&7), Some(&(1.0, 2.0)));
5171 assert!(state.transforms.is_empty());
5172 assert!(state.opacities.is_empty());
5173 assert!(state.system_style.is_none());
5174 assert!(state.virtual_view_display_lists.is_empty());
5175 assert!(state.image_callback_results.is_empty());
5176 }
5177
5178 #[test]
5179 fn cpu_render_state_builders_set_their_field_and_preserve_the_others() {
5180 let mut offsets = ScrollOffsetMap::new();
5181 offsets.insert(1, (3.0, 4.0));
5182
5183 let mut lists = std::collections::BTreeMap::new();
5184 lists.insert(DomId { inner: 9 }, std::sync::Arc::new(DisplayList::default()));
5185
5186 let img = r8_image(1, 1, vec![255]);
5187 let hash = img.get_hash();
5188 let mut results = std::collections::BTreeMap::new();
5189 results.insert(hash, img);
5190
5191 let state = CpuRenderState::new(offsets)
5192 .with_virtual_view_display_lists(lists)
5193 .with_image_callback_results(results)
5194 .with_system_style(Some(std::sync::Arc::new(
5195 azul_css::system::SystemStyle::default(),
5196 )));
5197
5198 assert_eq!(state.scroll_offsets.get(&1), Some(&(3.0, 4.0)));
5199 assert_eq!(state.virtual_view_display_lists.len(), 1);
5200 assert!(state.virtual_view_display_lists.contains_key(&DomId { inner: 9 }));
5201 assert_eq!(state.image_callback_results.len(), 1);
5202 assert!(state.image_callback_results.contains_key(&hash));
5203 assert!(state.system_style.is_some());
5204
5205 let cleared = CpuRenderState::new(ScrollOffsetMap::new()).with_system_style(None);
5207 assert!(cleared.system_style.is_none());
5208 }
5209
5210 #[test]
5211 fn cpu_render_state_builders_accept_empty_collections() {
5212 let state = CpuRenderState::new(ScrollOffsetMap::new())
5213 .with_virtual_view_display_lists(std::collections::BTreeMap::new())
5214 .with_image_callback_results(std::collections::BTreeMap::new());
5215 assert!(state.virtual_view_display_lists.is_empty());
5216 assert!(state.image_callback_results.is_empty());
5217 }
5218
5219 #[test]
5220 fn extract_gpu_values_without_a_cache_is_empty() {
5221 let (transforms, opacities) = extract_gpu_values(None, DomId::ROOT_ID);
5222 assert!(transforms.is_empty());
5223 assert!(opacities.is_empty());
5224 }
5225
5226 #[test]
5227 fn extract_gpu_values_flattens_keys_to_ids() {
5228 let mut cache = GpuValueCache::default();
5229 let node = NodeId::new(3);
5230 let tkey = TransformKey { id: 11 };
5231 let okey = OpacityKey { id: 22 };
5232
5233 cache.transform_keys.insert(node, tkey);
5234 cache
5235 .current_transform_values
5236 .insert(node, ComputedTransform3D::IDENTITY);
5237 cache.opacity_keys.insert(node, okey);
5238 cache.current_opacity_values.insert(node, 0.25);
5239
5240 let (transforms, opacities) = extract_gpu_values(Some(&cache), DomId::ROOT_ID);
5241 assert_eq!(transforms.len(), 1);
5242 assert_eq!(transforms.get(&11).map(|t| t.m), Some(ComputedTransform3D::IDENTITY.m));
5243 assert_eq!(opacities.get(&22), Some(&0.25));
5244 }
5245
5246 #[test]
5247 fn extract_gpu_values_drops_keys_without_a_value() {
5248 let mut cache = GpuValueCache::default();
5250 cache.transform_keys.insert(NodeId::new(0), TransformKey { id: 5 });
5251 cache.opacity_keys.insert(NodeId::new(0), OpacityKey { id: 6 });
5252 let (transforms, opacities) = extract_gpu_values(Some(&cache), DomId::ROOT_ID);
5253 assert!(transforms.is_empty());
5254 assert!(opacities.is_empty());
5255 }
5256
5257 #[test]
5258 fn extract_gpu_values_filters_scrollbar_opacity_by_dom_id() {
5259 let mut cache = GpuValueCache::default();
5260 let other_dom = DomId { inner: 42 };
5261 let node = NodeId::new(1);
5262 cache
5263 .scrollbar_v_opacity_keys
5264 .insert((other_dom, node), OpacityKey { id: 77 });
5265 cache
5266 .scrollbar_v_opacity_values
5267 .insert((other_dom, node), 1.0);
5268
5269 let (_, opacities) = extract_gpu_values(Some(&cache), DomId::ROOT_ID);
5271 assert!(opacities.is_empty());
5272
5273 let (_, opacities) = extract_gpu_values(Some(&cache), other_dom);
5275 assert_eq!(opacities.get(&77), Some(&1.0));
5276 }
5277
5278 #[test]
5279 fn cpu_render_state_from_gpu_cache_matches_extract_gpu_values() {
5280 let mut cache = GpuValueCache::default();
5281 cache.css_transform_keys.insert(NodeId::new(2), TransformKey { id: 8 });
5282 cache
5283 .css_current_transform_values
5284 .insert(NodeId::new(2), ComputedTransform3D::IDENTITY);
5285
5286 let mut offsets = ScrollOffsetMap::new();
5287 offsets.insert(5, (10.0, 20.0));
5288
5289 let state = CpuRenderState::from_gpu_cache(Some(&cache), DomId::ROOT_ID, &offsets);
5290 let (transforms, opacities) = extract_gpu_values(Some(&cache), DomId::ROOT_ID);
5291 assert_eq!(state.transforms.len(), transforms.len());
5292 assert!(state.transforms.contains_key(&8));
5293 assert_eq!(state.opacities.len(), opacities.len());
5294 assert_eq!(state.scroll_offsets.get(&5), Some(&(10.0, 20.0)));
5295 assert!(state.system_style.is_none());
5296
5297 let empty = CpuRenderState::from_gpu_cache(None, DomId::ROOT_ID, &ScrollOffsetMap::new());
5298 assert!(empty.transforms.is_empty() && empty.opacities.is_empty());
5299 }
5300
5301 #[test]
5306 fn probe_label_for_item_returns_a_distinct_static_label() {
5307 let cases = [
5308 (
5309 DisplayListItem::Rect {
5310 bounds: wrect(0.0, 0.0, 1.0, 1.0),
5311 color: RED,
5312 border_radius: BorderRadius::default(),
5313 },
5314 "dl:rect",
5315 ),
5316 (DisplayListItem::PopClip, "dl:pop_clip"),
5317 (DisplayListItem::PopScrollFrame, "dl:pop_scroll"),
5318 (DisplayListItem::PopOpacity, "dl:pop_opacity"),
5319 (DisplayListItem::PopTextShadow, "dl:pop_tshadow"),
5320 (DisplayListItem::PopImageMaskClip, "dl:pop_imask"),
5321 (
5322 DisplayListItem::BoxShadow {
5323 bounds: wrect(0.0, 0.0, 1.0, 1.0),
5324 shadow: shadow(0.0, 0.0, 0.0, BLACK),
5325 border_radius: BorderRadius::default(),
5326 },
5327 "dl:box_shadow",
5328 ),
5329 ];
5330 for (item, expected) in cases {
5331 assert_eq!(probe_label_for_item(&item), expected);
5332 }
5333 }
5334
5335 #[test]
5340 fn compute_content_bounds_of_an_empty_list_is_none() {
5341 assert!(compute_content_bounds(&DisplayList::default()).is_none());
5342 }
5343
5344 #[test]
5345 fn compute_content_bounds_ignores_state_management_items() {
5346 let dl = DisplayList {
5347 items: vec![
5348 DisplayListItem::PopClip,
5349 DisplayListItem::PopScrollFrame,
5350 DisplayListItem::PopOpacity,
5351 ],
5352 ..Default::default()
5353 };
5354 assert!(
5355 compute_content_bounds(&dl).is_none(),
5356 "push/pop markers carry no content"
5357 );
5358 }
5359
5360 #[test]
5361 fn compute_content_bounds_unions_every_drawing_item() {
5362 let dl = DisplayList {
5363 items: vec![
5364 DisplayListItem::Rect {
5365 bounds: wrect(10.0, 20.0, 30.0, 40.0),
5366 color: RED,
5367 border_radius: BorderRadius::default(),
5368 },
5369 DisplayListItem::Rect {
5370 bounds: wrect(-5.0, 0.0, 5.0, 5.0),
5371 color: BLUE,
5372 border_radius: BorderRadius::default(),
5373 },
5374 DisplayListItem::PopClip, ],
5376 ..Default::default()
5377 };
5378 let (min_x, min_y, max_x, max_y) = compute_content_bounds(&dl).expect("has items");
5379 assert_eq!((min_x, min_y), (-5.0, 0.0));
5380 assert_eq!((max_x, max_y), (40.0, 60.0));
5381 }
5382
5383 #[test]
5384 fn compute_content_bounds_with_nan_bounds_does_not_produce_nan() {
5385 let dl = DisplayList {
5388 items: vec![
5389 DisplayListItem::Rect {
5390 bounds: wrect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
5391 color: RED,
5392 border_radius: BorderRadius::default(),
5393 },
5394 DisplayListItem::Rect {
5395 bounds: wrect(0.0, 0.0, 10.0, 10.0),
5396 color: BLUE,
5397 border_radius: BorderRadius::default(),
5398 },
5399 ],
5400 ..Default::default()
5401 };
5402 let (min_x, min_y, max_x, max_y) = compute_content_bounds(&dl).expect("has items");
5403 for v in [min_x, min_y, max_x, max_y] {
5404 assert!(!v.is_nan(), "NaN item bounds must not poison the content box");
5405 }
5406 assert_eq!((max_x, max_y), (10.0, 10.0));
5407 }
5408
5409 #[test]
5414 fn build_rect_path_is_a_closed_quad() {
5415 let rect = AzRect::from_xywh(1.0, 2.0, 3.0, 4.0).unwrap();
5416 let path = build_rect_path(&rect);
5417 assert_eq!(path.total_vertices(), 5);
5419 let (mut x, mut y) = (0.0, 0.0);
5420 path.vertex_idx(0, &mut x, &mut y);
5421 assert_eq!((x, y), (1.0, 2.0));
5422 path.vertex_idx(2, &mut x, &mut y);
5423 assert_eq!((x, y), (4.0, 6.0), "the opposite corner is origin + size");
5424 }
5425
5426 #[test]
5427 fn build_rounded_rect_path_falls_back_to_a_quad_for_non_positive_radii() {
5428 let rect = AzRect::from_xywh(0.0, 0.0, 10.0, 10.0).unwrap();
5429 let plain = build_rect_path(&rect).total_vertices();
5430
5431 assert_eq!(
5433 build_rounded_rect_path(&rect, &BorderRadius::default(), 1.0).total_vertices(),
5434 plain
5435 );
5436 let negative = BorderRadius {
5438 top_left: -5.0,
5439 top_right: -5.0,
5440 bottom_left: -5.0,
5441 bottom_right: -5.0,
5442 };
5443 assert_eq!(
5444 build_rounded_rect_path(&rect, &negative, 1.0).total_vertices(),
5445 plain
5446 );
5447 let positive = BorderRadius {
5449 top_left: 4.0,
5450 top_right: 4.0,
5451 bottom_left: 4.0,
5452 bottom_right: 4.0,
5453 };
5454 assert_eq!(
5455 build_rounded_rect_path(&rect, &positive, 0.0).total_vertices(),
5456 plain
5457 );
5458 }
5459
5460 #[test]
5461 fn build_rounded_rect_path_emits_arc_vertices_for_positive_radii() {
5462 let rect = AzRect::from_xywh(0.0, 0.0, 40.0, 40.0).unwrap();
5463 let radius = BorderRadius {
5464 top_left: 8.0,
5465 top_right: 8.0,
5466 bottom_left: 8.0,
5467 bottom_right: 8.0,
5468 };
5469 let rounded = build_rounded_rect_path(&rect, &radius, 1.0).total_vertices();
5470 assert!(
5471 rounded > build_rect_path(&rect).total_vertices(),
5472 "arcs must add vertices (a square-cornered path would be the old bug)"
5473 );
5474 }
5475
5476 #[test]
5477 fn build_rounded_rect_path_normalizes_oversized_radii() {
5478 let rect = AzRect::from_xywh(0.0, 0.0, 10.0, 10.0).unwrap();
5480 let radius = BorderRadius {
5481 top_left: 1e6,
5482 top_right: 1e6,
5483 bottom_left: 1e6,
5484 bottom_right: 1e6,
5485 };
5486 let path = build_rounded_rect_path(&rect, &radius, 1.0);
5487 assert!(path.total_vertices() > 4);
5488 let (mut x, mut y) = (0.0, 0.0);
5489 for i in 0..path.total_vertices() {
5490 path.vertex_idx(i, &mut x, &mut y);
5491 assert!(
5492 x.is_finite() && y.is_finite(),
5493 "vertex {i} is not finite: ({x}, {y})"
5494 );
5495 assert!(
5496 (-1.0..=11.0).contains(&x) && (-1.0..=11.0).contains(&y),
5497 "vertex {i} ({x}, {y}) escaped the 10x10 rect"
5498 );
5499 }
5500 }
5501
5502 #[test]
5507 fn text_lcd_enabled_is_read_once_and_stable() {
5508 let first = text_lcd_enabled();
5509 assert_eq!(first, text_lcd_enabled(), "the OnceLock must not flip");
5510 if std::env::var("AZ_TEXT_LCD").is_err() {
5511 assert_eq!(first, TEXT_LCD_DEFAULT, "unset env -> the documented default");
5512 }
5513 }
5514
5515 #[test]
5520 fn unbalanced_pops_never_underflow_the_stacks() {
5521 let mut p = pixmap(8, 8);
5524 let state = CpuRenderState::new(ScrollOffsetMap::new());
5525 let mut st = Stacks::new();
5526 for item in [
5527 DisplayListItem::PopClip,
5528 DisplayListItem::PopScrollFrame,
5529 DisplayListItem::PopReferenceFrame,
5530 DisplayListItem::PopStackingContext,
5531 DisplayListItem::PopOpacity,
5532 DisplayListItem::PopTextShadow,
5533 DisplayListItem::PopImageMaskClip,
5534 DisplayListItem::PopFilter,
5535 DisplayListItem::PopBackdropFilter,
5536 ] {
5537 let res = run_item(&item, &mut p, &mut st, &state);
5538 assert_eq!(res, Ok(()), "{item:?} must not error");
5539 }
5540 assert_eq!(st.clips.len(), 1, "the base clip must never be popped");
5541 assert_eq!(st.transforms.len(), 1);
5542 assert_eq!(st.scrolls.len(), 1);
5543 assert!(st.masks.is_empty());
5544 assert!(st.shadows.is_empty());
5545 }
5546
5547 #[test]
5548 #[should_panic = "called `Option::unwrap()` on a `None` value"]
5549 fn render_single_item_with_an_empty_clip_stack_panics_as_documented() {
5550 let mut p = pixmap(4, 4);
5553 let state = CpuRenderState::new(ScrollOffsetMap::new());
5554 let mut st = Stacks::new();
5555 st.clips.clear();
5556 let _ = run_item(
5557 &DisplayListItem::Rect {
5558 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5559 color: RED,
5560 border_radius: BorderRadius::default(),
5561 },
5562 &mut p,
5563 &mut st,
5564 &state,
5565 );
5566 }
5567
5568 #[test]
5569 fn push_clip_intersects_with_the_active_clip_and_never_widens_it() {
5570 let mut p = pixmap(16, 16);
5571 let state = CpuRenderState::new(ScrollOffsetMap::new());
5572 let mut st = Stacks::new();
5573
5574 run_item(
5575 &DisplayListItem::PushClip {
5576 bounds: wrect(0.0, 0.0, 10.0, 10.0),
5577 border_radius: BorderRadius::default(),
5578 },
5579 &mut p,
5580 &mut st,
5581 &state,
5582 )
5583 .unwrap();
5584 run_item(
5586 &DisplayListItem::PushClip {
5587 bounds: wrect(5.0, 5.0, 100.0, 100.0),
5588 border_radius: BorderRadius::default(),
5589 },
5590 &mut p,
5591 &mut st,
5592 &state,
5593 )
5594 .unwrap();
5595
5596 let top = st.clips.last().copied().flatten().expect("clip present");
5597 assert_eq!((top.x, top.y), (5.0, 5.0));
5598 assert_eq!((top.width, top.height), (5.0, 5.0), "the child cannot escape the parent");
5599
5600 run_item(&DisplayListItem::PopClip, &mut p, &mut st, &state).unwrap();
5601 run_item(&DisplayListItem::PopClip, &mut p, &mut st, &state).unwrap();
5602 assert_eq!(st.clips.len(), 1);
5603 }
5604
5605 #[test]
5606 fn push_clip_with_degenerate_bounds_pushes_an_unpaintable_clip() {
5607 let mut p = pixmap(8, 8);
5608 let state = CpuRenderState::new(ScrollOffsetMap::new());
5609 let mut st = Stacks::new();
5610 run_item(
5611 &DisplayListItem::PushClip {
5612 bounds: wrect(0.0, 0.0, f32::NAN, f32::NAN),
5613 border_radius: BorderRadius::default(),
5614 },
5615 &mut p,
5616 &mut st,
5617 &state,
5618 )
5619 .unwrap();
5620 assert_eq!(st.clips.len(), 2, "the pop must still find a matching push");
5621
5622 let before = snap(&p);
5623 run_item(
5624 &DisplayListItem::Rect {
5625 bounds: wrect(0.0, 0.0, 8.0, 8.0),
5626 color: RED,
5627 border_radius: BorderRadius::default(),
5628 },
5629 &mut p,
5630 &mut st,
5631 &state,
5632 )
5633 .unwrap();
5634 assert_eq!(before, p.data(), "a NaN clip must not silently become 'no clip'");
5635 }
5636
5637 #[test]
5638 fn scroll_frames_shift_item_bounds_by_the_accumulated_offset() {
5639 let mut offsets = ScrollOffsetMap::new();
5640 offsets.insert(7, (0.0, 5.0));
5641 let state = CpuRenderState::new(offsets);
5642
5643 let dl = DisplayList {
5644 items: vec![
5645 DisplayListItem::PushScrollFrame {
5646 clip_bounds: wrect(0.0, 0.0, 10.0, 10.0),
5647 content_size: LogicalSize {
5648 width: 10.0,
5649 height: 100.0,
5650 },
5651 scroll_id: 7,
5652 },
5653 DisplayListItem::Rect {
5654 bounds: wrect(0.0, 5.0, 10.0, 2.0),
5655 color: RED,
5656 border_radius: BorderRadius::default(),
5657 },
5658 DisplayListItem::PopScrollFrame,
5659 ],
5660 ..Default::default()
5661 };
5662
5663 let mut p = pixmap(10, 10);
5664 run_list_with_state(&dl, &mut p, &state).expect("must render");
5665 assert!(
5666 is_reddish(px_at(&p, 0, 0)),
5667 "content at y=5 scrolled by 5 must land on row 0"
5668 );
5669 assert_eq!(px_at(&p, 0, 5), [255, 255, 255, 255], "row 5 is now empty");
5670 }
5671
5672 #[test]
5673 fn a_missing_scroll_id_defaults_to_a_zero_offset() {
5674 let dl = DisplayList {
5675 items: vec![
5676 DisplayListItem::PushScrollFrame {
5677 clip_bounds: wrect(0.0, 0.0, 10.0, 10.0),
5678 content_size: LogicalSize {
5679 width: 10.0,
5680 height: 10.0,
5681 },
5682 scroll_id: 999, },
5684 DisplayListItem::Rect {
5685 bounds: wrect(0.0, 0.0, 2.0, 2.0),
5686 color: RED,
5687 border_radius: BorderRadius::default(),
5688 },
5689 DisplayListItem::PopScrollFrame,
5690 ],
5691 ..Default::default()
5692 };
5693 let mut p = pixmap(10, 10);
5694 run_list_with_state(&dl, &mut p, &CpuRenderState::new(ScrollOffsetMap::new()))
5695 .expect("must render");
5696 assert!(is_reddish(px_at(&p, 0, 0)), "an unknown scroll id must not shift");
5697 }
5698
5699 fn opacity_layer_result(op: f32) -> u8 {
5706 let dl = DisplayList {
5707 items: vec![
5708 DisplayListItem::PushOpacity {
5709 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5710 opacity: op,
5711 },
5712 DisplayListItem::Rect {
5713 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5714 color: BLACK,
5715 border_radius: BorderRadius::default(),
5716 },
5717 DisplayListItem::PopOpacity,
5718 ],
5719 ..Default::default()
5720 };
5721 let mut p = pixmap(4, 4);
5722 run_list(&dl, &mut p, 1.0).expect("must render");
5723 px_at(&p, 1, 1)[0]
5724 }
5725
5726 #[test]
5727 fn opacity_layer_blends_against_the_pre_push_snapshot() {
5728 assert_eq!(opacity_layer_result(1.0), 0, "opacity 1 keeps the drawing");
5729 assert_eq!(opacity_layer_result(0.0), 255, "opacity 0 restores the snapshot");
5730 let half = opacity_layer_result(0.5);
5731 assert!(
5732 (120..=136).contains(&half),
5733 "opacity 0.5 must land near mid-gray, got {half}"
5734 );
5735 }
5736
5737 #[test]
5738 fn opacity_layer_saturates_out_of_range_and_nan_values() {
5739 assert_eq!(opacity_layer_result(5.0), 0, "opacity > 1 clamps to opaque");
5742 assert_eq!(opacity_layer_result(-5.0), 255, "opacity < 0 clamps to transparent");
5743 assert_eq!(opacity_layer_result(f32::INFINITY), 0);
5744 assert_eq!(opacity_layer_result(f32::NEG_INFINITY), 255);
5745 assert_eq!(opacity_layer_result(f32::NAN), 255);
5746 }
5747
5748 #[test]
5749 fn push_opacity_with_degenerate_bounds_pushes_nothing() {
5750 let mut p = pixmap(8, 8);
5753 let state = CpuRenderState::new(ScrollOffsetMap::new());
5754 let mut st = Stacks::new();
5755 run_item(
5756 &DisplayListItem::PushOpacity {
5757 bounds: wrect(0.0, 0.0, f32::NAN, 0.0),
5758 opacity: 0.5,
5759 },
5760 &mut p,
5761 &mut st,
5762 &state,
5763 )
5764 .unwrap();
5765 assert!(st.masks.is_empty());
5766 assert_eq!(
5767 run_item(&DisplayListItem::PopOpacity, &mut p, &mut st, &state),
5768 Ok(())
5769 );
5770 }
5771
5772 #[test]
5777 fn image_mask_clip_masks_the_drawing_it_wraps() {
5778 let mask = r8_image(2, 2, vec![255, 0, 255, 0]);
5780 let dl = DisplayList {
5781 items: vec![
5782 DisplayListItem::PushImageMaskClip {
5783 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5784 mask_image: mask,
5785 mask_rect: wrect(0.0, 0.0, 4.0, 4.0),
5786 },
5787 DisplayListItem::Rect {
5788 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5789 color: BLACK,
5790 border_radius: BorderRadius::default(),
5791 },
5792 DisplayListItem::PopImageMaskClip,
5793 ],
5794 ..Default::default()
5795 };
5796 let mut p = pixmap(4, 4);
5797 run_list(&dl, &mut p, 1.0).expect("must render");
5798 assert_eq!(px_at(&p, 0, 0), [0, 0, 0, 255], "mask=255 keeps the fill");
5799 assert_eq!(
5800 px_at(&p, 3, 0),
5801 [255, 255, 255, 255],
5802 "mask=0 restores the background"
5803 );
5804 }
5805
5806 #[test]
5807 fn image_mask_clip_with_a_degenerate_rect_is_skipped() {
5808 let mask = r8_image(1, 1, vec![255]);
5809 let mut p = pixmap(8, 8);
5810 let state = CpuRenderState::new(ScrollOffsetMap::new());
5811 let mut st = Stacks::new();
5812 run_item(
5813 &DisplayListItem::PushImageMaskClip {
5814 bounds: wrect(0.0, 0.0, 8.0, 8.0),
5815 mask_image: mask,
5816 mask_rect: wrect(0.0, 0.0, 0.0, 0.0),
5817 },
5818 &mut p,
5819 &mut st,
5820 &state,
5821 )
5822 .unwrap();
5823 assert!(st.masks.is_empty(), "a 0-sized mask rect pushes no entry");
5824 }
5825
5826 #[test]
5844 #[cfg_attr(debug_assertions, should_panic(expected = "cannot resolve"))]
5845 fn a_text_item_whose_font_is_unknown_paints_nothing() {
5846 let dl = DisplayList {
5847 items: vec![DisplayListItem::Text {
5848 glyphs: vec![GlyphInstance {
5849 index: 1,
5850 point: LogicalPosition { x: 0.0, y: 10.0 },
5851 size: LogicalSize {
5852 width: 8.0,
5853 height: 16.0,
5854 },
5855 }],
5856 font_hash: FontHash { font_hash: 0xdead_beef },
5857 font_size_px: 16.0,
5858 color: BLACK,
5859 clip_rect: wrect(0.0, 0.0, 16.0, 16.0),
5860 source_node_index: None,
5861 }],
5862 ..Default::default()
5863 };
5864 let mut p = pixmap(16, 16);
5865 let before = snap(&p);
5866 run_list(&dl, &mut p, 1.0).expect("a missing font must not fail the frame");
5867 assert_eq!(before, p.data());
5868 }
5869
5870 #[test]
5871 fn a_text_item_with_no_glyphs_or_no_alpha_paints_nothing() {
5872 for (glyphs, color) in [
5873 (Vec::new(), BLACK),
5874 (
5875 vec![GlyphInstance {
5876 index: 1,
5877 point: LogicalPosition { x: 0.0, y: 10.0 },
5878 size: LogicalSize {
5879 width: 8.0,
5880 height: 16.0,
5881 },
5882 }],
5883 CLEAR,
5884 ),
5885 ] {
5886 let dl = DisplayList {
5887 items: vec![DisplayListItem::Text {
5888 glyphs,
5889 font_hash: FontHash { font_hash: 1 },
5890 font_size_px: 16.0,
5891 color,
5892 clip_rect: wrect(0.0, 0.0, 16.0, 16.0),
5893 source_node_index: None,
5894 }],
5895 ..Default::default()
5896 };
5897 let mut p = pixmap(16, 16);
5898 let before = snap(&p);
5899 run_list(&dl, &mut p, 1.0).expect("must render");
5900 assert_eq!(before, p.data());
5901 }
5902 }
5903
5904 #[test]
5909 fn an_rgba_image_is_blitted_with_its_channels_in_order() {
5910 let img = rgba_image(2, 2, [255, 0, 0, 255].repeat(4));
5912 let dl = DisplayList {
5913 items: vec![DisplayListItem::Image {
5914 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5915 image: img,
5916 border_radius: BorderRadius::default(),
5917 }],
5918 ..Default::default()
5919 };
5920 let mut p = pixmap(8, 8);
5921 run_list(&dl, &mut p, 1.0).expect("must render");
5922 assert!(
5923 is_reddish(px_at(&p, 1, 1)),
5924 "an RGBA image must not come out swizzled or gray, got {:?}",
5925 px_at(&p, 1, 1)
5926 );
5927 assert_eq!(px_at(&p, 6, 6), [255, 255, 255, 255], "outside the bounds");
5928 }
5929
5930 #[test]
5931 fn an_image_with_degenerate_bounds_is_skipped() {
5932 for bad in DEGENERATE {
5933 let img = rgba_image(1, 1, vec![255, 0, 0, 255]);
5934 let dl = DisplayList {
5935 items: vec![DisplayListItem::Image {
5936 bounds: wrect(0.0, 0.0, bad, bad),
5937 image: img,
5938 border_radius: BorderRadius::default(),
5939 }],
5940 ..Default::default()
5941 };
5942 let mut p = pixmap(8, 8);
5943 let before = snap(&p);
5944 run_list(&dl, &mut p, 1.0).expect("must render");
5945 assert_eq!(before, p.data(), "image size {bad} must be rejected");
5946 }
5947 }
5948
5949 #[test]
5950 fn a_fully_transparent_image_leaves_the_background_alone() {
5951 let img = rgba_image(2, 2, [255, 0, 0, 0].repeat(4));
5952 let dl = DisplayList {
5953 items: vec![DisplayListItem::Image {
5954 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5955 image: img,
5956 border_radius: BorderRadius::default(),
5957 }],
5958 ..Default::default()
5959 };
5960 let mut p = pixmap(8, 8);
5961 let before = snap(&p);
5962 run_list(&dl, &mut p, 1.0).expect("must render");
5963 assert_eq!(before, p.data(), "alpha=0 source pixels must not blend");
5964 }
5965
5966 #[test]
5971 fn render_border_draws_the_frame_but_not_the_middle() {
5972 let mut p = pixmap(20, 20);
5973 render_border(
5974 &mut p,
5975 &lrect(0.0, 0.0, 20.0, 20.0),
5976 RED,
5977 2.0,
5978 BorderStyle::Solid,
5979 &BorderRadius::default(),
5980 None,
5981 1.0,
5982 );
5983 assert!(is_reddish(px_at(&p, 0, 0)), "the frame is painted");
5984 assert!(is_reddish(px_at(&p, 19, 19)));
5985 assert_eq!(px_at(&p, 10, 10), [255, 255, 255, 255], "the middle stays clear");
5986 }
5987
5988 #[test]
5989 fn render_border_zero_or_negative_width_is_a_noop() {
5990 for width in [0.0, -1.0, -1e30, f32::NEG_INFINITY] {
5991 let mut p = pixmap(10, 10);
5992 let before = snap(&p);
5993 render_border(
5994 &mut p,
5995 &lrect(0.0, 0.0, 10.0, 10.0),
5996 RED,
5997 width,
5998 BorderStyle::Solid,
5999 &BorderRadius::default(),
6000 None,
6001 1.0,
6002 );
6003 assert_eq!(before, p.data(), "border width {width} must not paint");
6004 }
6005 }
6006
6007 #[test]
6008 fn render_border_nan_width_and_hidden_styles_are_noops() {
6009 let mut p = pixmap(10, 10);
6014 render_border(
6015 &mut p,
6016 &lrect(0.0, 0.0, 10.0, 10.0),
6017 RED,
6018 f32::NAN,
6019 BorderStyle::Solid,
6020 &BorderRadius::default(),
6021 None,
6022 1.0,
6023 );
6024 assert_eq!(p.data().len(), 400, "the buffer must be intact");
6025 assert_eq!(
6026 px_at(&p, 5, 5),
6027 [255, 255, 255, 255],
6028 "a NaN border width must not fill the middle of the box"
6029 );
6030
6031 for style in [BorderStyle::None, BorderStyle::Hidden] {
6032 let mut p = pixmap(10, 10);
6033 let before = snap(&p);
6034 render_border(
6035 &mut p,
6036 &lrect(0.0, 0.0, 10.0, 10.0),
6037 RED,
6038 2.0,
6039 style,
6040 &BorderRadius::default(),
6041 None,
6042 1.0,
6043 );
6044 assert_eq!(before, p.data(), "{style:?} must not paint");
6045 }
6046 }
6047
6048 #[test]
6049 fn render_border_transparent_color_and_degenerate_dpi_are_noops() {
6050 let mut p = pixmap(10, 10);
6051 let before = snap(&p);
6052 render_border(
6053 &mut p,
6054 &lrect(0.0, 0.0, 10.0, 10.0),
6055 CLEAR,
6056 2.0,
6057 BorderStyle::Solid,
6058 &BorderRadius::default(),
6059 None,
6060 1.0,
6061 );
6062 assert_eq!(before, p.data());
6063
6064 for dpi in DEGENERATE {
6065 let mut p = pixmap(10, 10);
6066 let before = snap(&p);
6067 render_border(
6068 &mut p,
6069 &lrect(0.0, 0.0, 10.0, 10.0),
6070 RED,
6071 2.0,
6072 BorderStyle::Solid,
6073 &BorderRadius::default(),
6074 None,
6075 dpi,
6076 );
6077 assert_eq!(before, p.data(), "dpi {dpi} must be rejected");
6078 }
6079 }
6080
6081 #[test]
6082 fn render_border_width_larger_than_the_box_does_not_panic() {
6083 let mut p = pixmap(10, 10);
6086 render_border(
6087 &mut p,
6088 &lrect(0.0, 0.0, 10.0, 10.0),
6089 RED,
6090 1000.0,
6091 BorderStyle::Solid,
6092 &BorderRadius::default(),
6093 None,
6094 1.0,
6095 );
6096 assert!(is_reddish(px_at(&p, 5, 5)));
6097 }
6098
6099 #[test]
6100 fn render_border_dashed_and_dotted_styles_paint_without_panicking() {
6101 for style in [BorderStyle::Dashed, BorderStyle::Dotted] {
6102 let mut p = pixmap(20, 20);
6103 let before = snap(&p);
6104 render_border(
6105 &mut p,
6106 &lrect(2.0, 2.0, 16.0, 16.0),
6107 RED,
6108 2.0,
6109 style,
6110 &BorderRadius::default(),
6111 None,
6112 1.0,
6113 );
6114 assert_ne!(before, p.data(), "{style:?} must paint something");
6115 }
6116 }
6117
6118 #[test]
6119 fn render_border_sides_with_mixed_widths_paints_each_side() {
6120 let mut p = pixmap(20, 20);
6121 render_border_sides(
6122 &mut p,
6123 &lrect(0.0, 0.0, 20.0, 20.0),
6124 [RED, BLUE, RED, BLUE],
6125 [3.0, 1.0, 3.0, 1.0],
6126 [
6127 BorderStyle::Solid,
6128 BorderStyle::Solid,
6129 BorderStyle::Solid,
6130 BorderStyle::Solid,
6131 ],
6132 &BorderRadius::default(),
6133 None,
6134 1.0,
6135 );
6136 assert!(is_reddish(px_at(&p, 10, 0)), "the top side is red");
6137 assert_eq!(px_at(&p, 10, 10), [255, 255, 255, 255], "the middle stays clear");
6138 }
6139
6140 #[test]
6141 fn render_border_sides_zero_widths_and_degenerate_values_are_noops() {
6142 let styles = [
6143 BorderStyle::Solid,
6144 BorderStyle::Solid,
6145 BorderStyle::Solid,
6146 BorderStyle::Solid,
6147 ];
6148 let mut p = pixmap(10, 10);
6149 let before = snap(&p);
6150 render_border_sides(
6151 &mut p,
6152 &lrect(0.0, 0.0, 10.0, 10.0),
6153 [RED; 4],
6154 [0.0; 4],
6155 styles,
6156 &BorderRadius::default(),
6157 None,
6158 1.0,
6159 );
6160 assert_eq!(before, p.data(), "0-width sides must not paint");
6161
6162 for bad in DEGENERATE {
6163 let mut p = pixmap(10, 10);
6164 let before = snap(&p);
6165 render_border_sides(
6166 &mut p,
6167 &lrect(0.0, 0.0, 10.0, 10.0),
6168 [RED; 4],
6169 [2.0; 4],
6170 styles,
6171 &BorderRadius::default(),
6172 None,
6173 bad,
6174 );
6175 assert_eq!(before, p.data(), "dpi {bad} must be rejected");
6176 }
6177
6178 for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -5.0] {
6180 let mut p = pixmap(10, 10);
6181 render_border_sides(
6182 &mut p,
6183 &lrect(0.0, 0.0, 10.0, 10.0),
6184 [RED; 4],
6185 [bad; 4],
6186 styles,
6187 &BorderRadius::default(),
6188 None,
6189 1.0,
6190 );
6191 assert_eq!(p.data().len(), 400, "width {bad} must not resize the buffer");
6192 }
6193 }
6194
6195 fn damaged(
6200 dl: &DisplayList,
6201 p: &mut AzulPixmap,
6202 rects: &[LogicalRect],
6203 ) -> Result<(), String> {
6204 let res = RendererResources::default();
6205 let mut gc = GlyphCache::new();
6206 let state = CpuRenderState::new(ScrollOffsetMap::new());
6207 render_display_list_damaged(dl, p, 1.0, &res, &empty_font_manager(), &mut gc, &state, rects)
6208 }
6209
6210 fn full_red_dl() -> DisplayList {
6211 DisplayList {
6212 items: vec![DisplayListItem::Rect {
6213 bounds: wrect(0.0, 0.0, 8.0, 8.0),
6214 color: RED,
6215 border_radius: BorderRadius::default(),
6216 }],
6217 ..Default::default()
6218 }
6219 }
6220
6221 #[test]
6222 fn damaged_render_without_rects_is_a_noop() {
6223 let mut p = pixmap(8, 8);
6224 p.fill(0, 0, 255, 255);
6225 let before = snap(&p);
6226 damaged(&full_red_dl(), &mut p, &[]).expect("must succeed");
6227 assert_eq!(before, p.data(), "no damage -> no repaint at all");
6228 }
6229
6230 #[test]
6231 fn damaged_render_only_repaints_inside_the_damage_rect() {
6232 let mut p = pixmap(8, 8);
6233 p.fill(0, 0, 255, 255); damaged(&full_red_dl(), &mut p, &[lrect(0.0, 0.0, 4.0, 4.0)]).expect("must succeed");
6235 assert!(is_reddish(px_at(&p, 1, 1)), "the damaged region is repainted");
6236 assert_eq!(
6237 px_at(&p, 6, 6),
6238 [0, 0, 255, 255],
6239 "untouched pixels must survive — a union-clip repaint used to wipe them"
6240 );
6241 }
6242
6243 #[test]
6244 fn damaged_render_with_nan_rects_paints_nothing() {
6245 let mut p = pixmap(8, 8);
6246 p.fill(0, 0, 255, 255);
6247 let before = snap(&p);
6248 damaged(
6249 &full_red_dl(),
6250 &mut p,
6251 &[lrect(f32::NAN, f32::NAN, f32::NAN, f32::NAN)],
6252 )
6253 .expect("must succeed");
6254 assert_eq!(before, p.data(), "a NaN damage rect must collapse to nothing");
6255 }
6256
6257 #[test]
6258 fn damaged_render_clamps_saturating_rects_to_the_pixmap() {
6259 let mut p = pixmap(8, 8);
6260 p.fill(0, 0, 255, 255);
6261 damaged(&full_red_dl(), &mut p, &[lrect(-1e9, -1e9, 3e9, 3e9)]).expect("must succeed");
6262 assert!(
6263 p.data().chunks_exact(4).all(|c| c[0] > 200 && c[1] < 60),
6264 "an oversized damage rect clamps to the buffer and repaints all of it"
6265 );
6266 }
6267
6268 #[test]
6269 fn damaged_render_merges_overlapping_rects_without_double_blending() {
6270 let half_red = ColorU { r: 255, g: 0, b: 0, a: 128 };
6273 let dl = DisplayList {
6274 items: vec![DisplayListItem::Rect {
6275 bounds: wrect(0.0, 0.0, 8.0, 8.0),
6276 color: half_red,
6277 border_radius: BorderRadius::default(),
6278 }],
6279 ..Default::default()
6280 };
6281
6282 let mut once = pixmap(8, 8);
6283 damaged(&dl, &mut once, &[lrect(0.0, 0.0, 8.0, 8.0)]).expect("must succeed");
6284
6285 let mut twice = pixmap(8, 8);
6286 damaged(
6287 &dl,
6288 &mut twice,
6289 &[lrect(0.0, 0.0, 6.0, 6.0), lrect(2.0, 2.0, 6.0, 6.0)],
6290 )
6291 .expect("must succeed");
6292
6293 assert_eq!(
6294 px_at(&once, 3, 3),
6295 px_at(&twice, 3, 3),
6296 "the overlap must be blended exactly once"
6297 );
6298 }
6299
6300 #[test]
6301 fn damaged_render_with_a_zero_area_rect_is_a_noop() {
6302 let mut p = pixmap(8, 8);
6303 p.fill(0, 0, 255, 255);
6304 let before = snap(&p);
6305 damaged(&full_red_dl(), &mut p, &[lrect(4.0, 4.0, 0.0, 0.0)]).expect("must succeed");
6306 assert_eq!(before, p.data());
6307 }
6308
6309 #[cfg(all(feature = "text_layout", feature = "font_loading"))]
6314 #[test]
6315 fn component_preview_of_a_degenerate_size_never_panics() {
6316 use rust_fontconfig::FcFontCache;
6317
6318 let mut dom = azul_core::dom::Dom::create_body();
6319 let styled = azul_core::styled_dom::StyledDom::create(&mut dom, azul_css::css::Css::empty());
6320 let fm = FontManager::<FontRef>::new(FcFontCache::default()).expect("font manager");
6321
6322 for (w, h, dpi) in [
6326 (Some(0.0), Some(0.0), 1.0),
6327 (Some(8.0), Some(8.0), 0.0),
6328 (Some(8.0), Some(8.0), 1.0),
6329 ] {
6330 let o = ComponentPreviewOptions {
6331 width: w,
6332 height: h,
6333 dpi_factor: dpi,
6334 ..ComponentPreviewOptions::default()
6335 };
6336 match render_component_preview(&styled, &fm, o, None) {
6337 Ok(res) => {
6338 assert!(
6339 res.content_width.is_finite() && res.content_height.is_finite(),
6340 "{w:?}x{h:?}@{dpi} produced non-finite content bounds"
6341 );
6342 assert!(
6343 res.content_width <= 4096.0 && res.content_height <= 4096.0,
6344 "the preview must stay bounded by MAX_SIZE"
6345 );
6346 }
6347 Err(e) => assert!(!e.is_empty(), "an error must carry a message"),
6348 }
6349 }
6350 }
6351
6352 #[cfg(all(feature = "text_layout", feature = "font_loading"))]
6353 #[test]
6354 fn text_run_to_pixmap_without_any_font_returns_none_for_every_input() {
6355 use rust_fontconfig::FcFontCache;
6356
6357 let empty = FcFontCache::default();
6361 let long = "A".repeat(1_000_000);
6362 let nested = "[".repeat(10_000);
6363 let inputs = [
6364 "",
6365 " ",
6366 "\t\n\r",
6367 "\0\u{1}\u{7f}",
6368 "0",
6369 "-0",
6370 "9223372036854775807",
6371 "NaN",
6372 "inf",
6373 "-inf",
6374 " valid ",
6375 "valid;garbage",
6376 "\u{1F600}\u{1F1E9}\u{1F1EA}",
6377 "e\u{301}\u{323}\u{489}",
6378 long.as_str(),
6379 nested.as_str(),
6380 ];
6381 for text in inputs {
6382 let got = render_text_run_to_pixmap(&empty, text, 16.0, BLACK, WHITE, 2.0, 1.0);
6383 assert!(
6384 got.is_none(),
6385 "no resolvable font must yield None (input len {})",
6386 text.len()
6387 );
6388 }
6389
6390 for size in [0.0, -16.0, f32::NAN, f32::INFINITY] {
6392 assert!(render_text_run_to_pixmap(&empty, "hi", size, BLACK, WHITE, 0.0, 1.0).is_none());
6393 }
6394 for dpi in [0.0, -1.0, f32::NAN] {
6395 assert!(render_text_run_to_pixmap(&empty, "hi", 16.0, BLACK, WHITE, 2.0, dpi).is_none());
6396 }
6397 }
6398
6399 #[cfg(all(feature = "text_layout", feature = "font_loading"))]
6400 #[test]
6401 fn text_run_to_pixmap_renders_dark_glyphs_on_the_background() {
6402 use rust_fontconfig::{FcFont, FcFontCache, FcPattern};
6403
6404 let candidates = [
6405 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
6406 "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
6407 "/System/Library/Fonts/Supplemental/Times New Roman.ttf",
6408 "C:/Windows/Fonts/arial.ttf",
6409 ];
6410 let Some(bytes) = candidates.iter().find_map(|p| std::fs::read(p).ok()) else {
6411 eprintln!("[skip] no system font file available");
6412 return;
6413 };
6414
6415 let cache = FcFontCache::default();
6416 cache.with_memory_fonts(vec![(
6417 FcPattern {
6418 family: Some("sans-serif".to_string()),
6419 ..Default::default()
6420 },
6421 FcFont {
6422 bytes,
6423 font_index: 0,
6424 id: "autotest-sans".to_string(),
6425 },
6426 )]);
6427
6428 let Some(p) = render_text_run_to_pixmap(&cache, "Hi", 24.0, BLACK, WHITE, 4.0, 1.0) else {
6429 eprintln!("[skip] the memory font did not resolve through fontconfig");
6430 return;
6431 };
6432 assert!(p.width >= 1 && p.height >= 1);
6433 let dark = p.data().chunks_exact(4).filter(|c| c[0] < 128).count();
6434 assert!(dark > 0, "the glyph run must actually rasterize");
6435
6436 let empty = render_text_run_to_pixmap(&cache, "", 24.0, BLACK, WHITE, 4.0, 1.0)
6439 .expect("empty text must still give a pixmap");
6440 assert!(empty.width >= 1 && empty.height >= 1);
6441 assert!(
6442 empty.data().chunks_exact(4).all(|c| c[0] == 255 && c[1] == 255),
6443 "empty text must paint no glyphs"
6444 );
6445
6446 assert!(
6448 render_text_run_to_pixmap(&cache, "\u{1F600}é\u{301}", 24.0, BLACK, WHITE, 4.0, 1.0)
6449 .is_some(),
6450 "unicode input must not panic or bail out"
6451 );
6452 }
6453}