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(
657 dl: &DisplayList,
658 res: &RendererResources,
659 opts: RenderOptions,
660 glyph_cache: &mut GlyphCache,
661) -> Result<AzulPixmap, String> {
662 let RenderOptions {
663 width,
664 height,
665 dpi_factor,
666 } = opts;
667
668 let mut pixmap = acquire_pixmap(
669 None,
670 (width * dpi_factor) as u32,
671 (height * dpi_factor) as u32,
672 )?;
673 pixmap.fill(255, 255, 255, 255);
674
675 render_display_list(dl, &mut pixmap, dpi_factor, res, None, glyph_cache)?;
676
677 Ok(pixmap)
678}
679
680pub fn render_with_font_manager(
686 dl: &DisplayList,
687 res: &RendererResources,
688 font_manager: &FontManager<FontRef>,
689 opts: RenderOptions,
690 glyph_cache: &mut GlyphCache,
691) -> Result<AzulPixmap, String> {
692 let empty_state = CpuRenderState::new(ScrollOffsetMap::new());
693 render_with_font_manager_and_scroll(dl, res, font_manager, opts, glyph_cache, &empty_state)
694}
695
696pub fn render_with_font_manager_and_scroll(
702 dl: &DisplayList,
703 res: &RendererResources,
704 font_manager: &FontManager<FontRef>,
705 opts: RenderOptions,
706 glyph_cache: &mut GlyphCache,
707 render_state: &CpuRenderState,
708) -> Result<AzulPixmap, String> {
709 render_with_font_manager_and_scroll_retained(
710 dl,
711 res,
712 font_manager,
713 opts,
714 glyph_cache,
715 render_state,
716 None,
717 )
718}
719
720#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub fn render_with_font_manager_and_scroll_retained(
728 dl: &DisplayList,
729 res: &RendererResources,
730 font_manager: &FontManager<FontRef>,
731 opts: RenderOptions,
732 glyph_cache: &mut GlyphCache,
733 render_state: &CpuRenderState,
734 retained: Option<AzulPixmap>,
735) -> Result<AzulPixmap, String> {
736 let RenderOptions {
737 width,
738 height,
739 dpi_factor,
740 } = opts;
741
742 let pw = (width * dpi_factor) as u32;
743 let ph = (height * dpi_factor) as u32;
744 let mut pixmap = acquire_pixmap(retained, pw, ph)?;
745 pixmap.fill(255, 255, 255, 255);
746
747 render_display_list_with_state(
748 dl,
749 &mut pixmap,
750 dpi_factor,
751 res,
752 Some(font_manager),
753 glyph_cache,
754 render_state,
755 )?;
756
757 Ok(pixmap)
758}
759
760pub type ScrollOffsetMap = HashMap<LocalScrollId, (f32, f32)>;
764
765#[derive(Debug)]
771pub struct CpuRenderState {
772 pub scroll_offsets: ScrollOffsetMap,
774 pub transforms: HashMap<usize, azul_core::transform::ComputedTransform3D>,
777 pub opacities: HashMap<usize, f32>,
781 pub system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
785 pub virtual_view_display_lists:
791 std::collections::BTreeMap<azul_core::dom::DomId, std::sync::Arc<DisplayList>>,
792 pub image_callback_results:
799 std::collections::BTreeMap<azul_core::resources::ImageRefHash, ImageRef>,
800}
801
802impl CpuRenderState {
803 #[must_use] pub fn new(scroll_offsets: ScrollOffsetMap) -> Self {
804 Self {
805 scroll_offsets,
806 transforms: HashMap::new(),
807 opacities: HashMap::new(),
808 system_style: None,
809 virtual_view_display_lists: std::collections::BTreeMap::new(),
810 image_callback_results: std::collections::BTreeMap::new(),
811 }
812 }
813
814 #[must_use] pub fn with_image_callback_results(
816 mut self,
817 results: std::collections::BTreeMap<
818 azul_core::resources::ImageRefHash,
819 ImageRef,
820 >,
821 ) -> Self {
822 self.image_callback_results = results;
823 self
824 }
825
826 #[must_use] pub fn with_virtual_view_display_lists(
829 mut self,
830 lists: std::collections::BTreeMap<azul_core::dom::DomId, std::sync::Arc<DisplayList>>,
831 ) -> Self {
832 self.virtual_view_display_lists = lists;
833 self
834 }
835
836 #[must_use] pub fn with_system_style(
839 mut self,
840 system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
841 ) -> Self {
842 self.system_style = system_style;
843 self
844 }
845
846 #[must_use] pub fn from_gpu_cache(
848 gpu_cache: Option<&azul_core::gpu::GpuValueCache>,
849 dom_id: azul_core::dom::DomId,
850 scroll_offsets: &ScrollOffsetMap,
851 ) -> Self {
852 let (transforms, opacities) = extract_gpu_values(gpu_cache, dom_id);
853 Self {
854 scroll_offsets: scroll_offsets.clone(),
855 transforms,
856 opacities,
857 system_style: None,
858 virtual_view_display_lists: std::collections::BTreeMap::new(),
859 image_callback_results: std::collections::BTreeMap::new(),
860 }
861 }
862}
863
864#[must_use] pub fn extract_gpu_values(
872 gpu_cache: Option<&azul_core::gpu::GpuValueCache>,
873 dom_id: azul_core::dom::DomId,
874) -> (
875 HashMap<usize, azul_core::transform::ComputedTransform3D>,
876 HashMap<usize, f32>,
877) {
878 {
879 let mut transforms = HashMap::new();
880 let mut opacities = HashMap::new();
881
882 if let Some(cache) = gpu_cache {
883 for (node_id, key) in &cache.transform_keys {
885 if let Some(value) = cache.current_transform_values.get(node_id) {
886 transforms.insert(key.id, *value);
887 }
888 }
889 for (node_id, key) in &cache.h_transform_keys {
891 if let Some(value) = cache.h_current_transform_values.get(node_id) {
892 transforms.insert(key.id, *value);
893 }
894 }
895 for (node_id, key) in &cache.css_transform_keys {
897 if let Some(value) = cache.css_current_transform_values.get(node_id) {
898 transforms.insert(key.id, *value);
899 }
900 }
901 for ((d, node_id), key) in &cache.scrollbar_v_opacity_keys {
903 if *d == dom_id {
904 if let Some(&value) = cache.scrollbar_v_opacity_values.get(&(*d, *node_id)) {
905 opacities.insert(key.id, value);
906 }
907 }
908 }
909 for ((d, node_id), key) in &cache.scrollbar_h_opacity_keys {
911 if *d == dom_id {
912 if let Some(&value) = cache.scrollbar_h_opacity_values.get(&(*d, *node_id)) {
913 opacities.insert(key.id, value);
914 }
915 }
916 }
917 for (node_id, key) in &cache.opacity_keys {
919 if let Some(&value) = cache.current_opacity_values.get(node_id) {
920 opacities.insert(key.id, value);
921 }
922 }
923 }
924
925 (transforms, opacities)
926 }
927}
928
929fn render_display_list(
930 display_list: &DisplayList,
931 pixmap: &mut AzulPixmap,
932 dpi_factor: f32,
933 renderer_resources: &RendererResources,
934 font_manager: Option<&FontManager<FontRef>>,
935 glyph_cache: &mut GlyphCache,
936) -> Result<(), String> {
937 let empty_state = CpuRenderState::new(ScrollOffsetMap::new());
938 render_display_list_with_state(
939 display_list,
940 pixmap,
941 dpi_factor,
942 renderer_resources,
943 font_manager,
944 glyph_cache,
945 &empty_state,
946 )
947}
948
949fn render_display_list_with_state(
950 display_list: &DisplayList,
951 pixmap: &mut AzulPixmap,
952 dpi_factor: f32,
953 renderer_resources: &RendererResources,
954 font_manager: Option<&FontManager<FontRef>>,
955 glyph_cache: &mut GlyphCache,
956 render_state: &CpuRenderState,
957) -> Result<(), String> {
958 let mut transform_stack = vec![TransAffine::new()]; let mut clip_stack: Vec<Option<AzRect>> = vec![None];
960 let mut mask_stack: Vec<MaskEntry> = Vec::new();
961 let mut scroll_offset_stack: Vec<(f32, f32)> = vec![(0.0, 0.0)];
966 let mut text_shadow_stack: Vec<StyleBoxShadow> = Vec::new();
967
968 let _p_loop = crate::probe::Probe::span("raster_loop");
969 for item in &display_list.items {
970 let _p_item = crate::probe::Probe::span(probe_label_for_item(item));
971 render_single_item(
972 item,
973 pixmap,
974 dpi_factor,
975 renderer_resources,
976 font_manager,
977 glyph_cache,
978 &mut transform_stack,
979 &mut clip_stack,
980 &mut mask_stack,
981 &mut scroll_offset_stack,
982 &mut text_shadow_stack,
983 render_state,
984 )?;
985 }
986
987 Ok(())
988}
989
990#[inline]
994const fn probe_label_for_item(item: &DisplayListItem) -> &'static str {
995 use crate::solver3::display_list::DisplayListItem as I;
996 match item {
997 I::Rect { .. } => "dl:rect",
998 I::SelectionRect { .. } => "dl:sel_rect",
999 I::CursorRect { .. } => "dl:cursor",
1000 I::Border { .. } => "dl:border",
1001 I::Text { .. } => "dl:text",
1002 I::TextLayout { .. } => "dl:text_layout",
1003 I::Image { .. } => "dl:image",
1004 I::ScrollBar { .. } => "dl:scrollbar_raw",
1005 I::ScrollBarStyled { .. } => "dl:scrollbar",
1006 I::PushClip { .. } => "dl:push_clip",
1007 I::PopClip => "dl:pop_clip",
1008 I::PushScrollFrame { .. } => "dl:push_scroll",
1009 I::PopScrollFrame => "dl:pop_scroll",
1010 I::PushStackingContext { .. } => "dl:push_stack",
1011 I::PopStackingContext => "dl:pop_stack",
1012 I::PushReferenceFrame { .. } => "dl:push_ref",
1013 I::PopReferenceFrame => "dl:pop_ref",
1014 I::PushOpacity { .. } => "dl:push_opacity",
1015 I::PopOpacity => "dl:pop_opacity",
1016 I::PushFilter { .. } => "dl:push_filter",
1017 I::PopFilter => "dl:pop_filter",
1018 I::PushBackdropFilter { .. } => "dl:push_bdfilter",
1019 I::PopBackdropFilter => "dl:pop_bdfilter",
1020 I::PushTextShadow { .. } => "dl:push_tshadow",
1021 I::PopTextShadow => "dl:pop_tshadow",
1022 I::PushImageMaskClip { .. } => "dl:push_imask",
1023 I::PopImageMaskClip => "dl:pop_imask",
1024 I::LinearGradient { .. } => "dl:linear_grad",
1025 I::RadialGradient { .. } => "dl:radial_grad",
1026 I::ConicGradient { .. } => "dl:conic_grad",
1027 I::BoxShadow { .. } => "dl:box_shadow",
1028 I::Underline { .. } => "dl:underline",
1029 I::Strikethrough { .. } => "dl:strike",
1030 I::Overline { .. } => "dl:overline",
1031 I::HitTestArea { .. } => "dl:hit",
1032 I::VirtualView { .. } => "dl:vview",
1033 I::VirtualViewPlaceholder { .. } => "dl:vview_ph",
1034 }
1035}
1036
1037#[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(
1056 display_list: &DisplayList,
1057 pixmap: &mut AzulPixmap,
1058 dpi_factor: f32,
1059 renderer_resources: &RendererResources,
1060 font_manager: Option<&FontManager<FontRef>>,
1061 glyph_cache: &mut GlyphCache,
1062 render_state: &CpuRenderState,
1063 damage_rects: &[LogicalRect],
1064) -> Result<(), String> {
1065 struct SnappedRect {
1069 x0: i32,
1070 y0: i32,
1071 x1: i32,
1072 y1: i32,
1073 logical: LogicalRect,
1074 }
1075
1076 if damage_rects.is_empty() {
1077 return Ok(()); }
1079
1080 let pw_i = pixmap.width() as i32;
1088 let ph_i = pixmap.height() as i32;
1089 let snap_out = |dr: &LogicalRect| -> Option<SnappedRect> {
1090 let x0 = ((dr.origin.x * dpi_factor).floor() as i32).clamp(0, pw_i);
1091 let y0 = ((dr.origin.y * dpi_factor).floor() as i32).clamp(0, ph_i);
1092 let x1 = (((dr.origin.x + dr.size.width) * dpi_factor).ceil() as i32).clamp(0, pw_i);
1093 let y1 = (((dr.origin.y + dr.size.height) * dpi_factor).ceil() as i32).clamp(0, ph_i);
1094 if x1 <= x0 || y1 <= y0 {
1095 return None;
1096 }
1097 Some(SnappedRect {
1098 x0,
1099 y0,
1100 x1,
1101 y1,
1102 logical: LogicalRect {
1103 origin: LogicalPosition {
1104 x: x0 as f32 / dpi_factor,
1105 y: y0 as f32 / dpi_factor,
1106 },
1107 size: LogicalSize {
1108 width: (x1 - x0) as f32 / dpi_factor,
1109 height: (y1 - y0) as f32 / dpi_factor,
1110 },
1111 },
1112 })
1113 };
1114 let mut rects: Vec<SnappedRect> = damage_rects.iter().filter_map(snap_out).collect();
1115
1116 let mut i = 0;
1122 while i < rects.len() {
1123 let mut j = i + 1;
1124 let mut merged_any = false;
1125 while j < rects.len() {
1126 let (a, b) = (&rects[i], &rects[j]);
1127 let overlap = a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;
1128 if overlap {
1129 let x0 = a.x0.min(b.x0);
1130 let y0 = a.y0.min(b.y0);
1131 let x1 = a.x1.max(b.x1);
1132 let y1 = a.y1.max(b.y1);
1133 rects[i] = SnappedRect {
1134 x0,
1135 y0,
1136 x1,
1137 y1,
1138 logical: LogicalRect {
1139 origin: LogicalPosition {
1140 x: x0 as f32 / dpi_factor,
1141 y: y0 as f32 / dpi_factor,
1142 },
1143 size: LogicalSize {
1144 width: (x1 - x0) as f32 / dpi_factor,
1145 height: (y1 - y0) as f32 / dpi_factor,
1146 },
1147 },
1148 };
1149 rects.swap_remove(j);
1150 merged_any = true;
1151 } else {
1154 j += 1;
1155 }
1156 }
1157 if merged_any {
1158 if rects.len() > 1 {
1160 continue;
1161 }
1162 }
1163 i += 1;
1164 }
1165
1166 for sr in &rects {
1177 pixmap.fill_rect(
1178 sr.x0,
1179 sr.y0,
1180 sr.x1 - sr.x0,
1181 sr.y1 - sr.y0,
1182 255,
1183 255,
1184 255,
1185 255,
1186 );
1187
1188 let base_clip = AzRect::from_xywh(
1189 sr.x0 as f32,
1190 sr.y0 as f32,
1191 (sr.x1 - sr.x0) as f32,
1192 (sr.y1 - sr.y0) as f32,
1193 );
1194 let mut transform_stack = vec![TransAffine::new()];
1195 let mut clip_stack: Vec<Option<AzRect>> = vec![base_clip];
1196 let mut mask_stack: Vec<MaskEntry> = Vec::new();
1197 let mut scroll_offset_stack: Vec<(f32, f32)> = vec![(0.0, 0.0)];
1198 let mut text_shadow_stack: Vec<StyleBoxShadow> = Vec::new();
1199
1200 for item in &display_list.items {
1201 if !item.is_state_management() {
1204 if let Some(item_bounds) = item.bounds() {
1205 let (sdx, sdy) = *scroll_offset_stack.last().unwrap_or(&(0.0, 0.0));
1212 let test_bounds = if sdx == 0.0 && sdy == 0.0 {
1213 item_bounds
1214 } else {
1215 LogicalRect {
1216 origin: LogicalPosition {
1217 x: item_bounds.origin.x - sdx,
1218 y: item_bounds.origin.y - sdy,
1219 },
1220 size: item_bounds.size,
1221 }
1222 };
1223 if !rects_overlap_or_adjacent(&test_bounds, &sr.logical, 0.0) {
1224 continue;
1225 }
1226 }
1227 }
1228
1229 render_single_item(
1230 item,
1231 pixmap,
1232 dpi_factor,
1233 renderer_resources,
1234 font_manager,
1235 glyph_cache,
1236 &mut transform_stack,
1237 &mut clip_stack,
1238 &mut mask_stack,
1239 &mut scroll_offset_stack,
1240 &mut text_shadow_stack,
1241 render_state,
1242 )?;
1243 }
1244 }
1245
1246 Ok(())
1247}
1248
1249#[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(
1261 item: &DisplayListItem,
1262 pixmap: &mut AzulPixmap,
1263 dpi_factor: f32,
1264 renderer_resources: &RendererResources,
1265 font_manager: Option<&FontManager<FontRef>>,
1266 glyph_cache: &mut GlyphCache,
1267 transform_stack: &mut Vec<TransAffine>,
1268 clip_stack: &mut Vec<Option<AzRect>>,
1269 mask_stack: &mut Vec<MaskEntry>,
1270 scroll_offset_stack: &mut Vec<(f32, f32)>,
1271 text_shadow_stack: &mut Vec<StyleBoxShadow>,
1272 render_state: &CpuRenderState,
1273) -> Result<(), String> {
1274 use azul_css::props::style::border::BorderStyle;
1275 let (scroll_dx, scroll_dy) = *scroll_offset_stack.last().unwrap_or(&(0.0, 0.0));
1278
1279 let scroll_rect = |r: &LogicalRect| -> LogicalRect {
1284 if scroll_dx == 0.0 && scroll_dy == 0.0 {
1285 return *r;
1286 }
1287 LogicalRect {
1288 origin: LogicalPosition {
1289 x: r.origin.x - scroll_dx,
1290 y: r.origin.y - scroll_dy,
1291 },
1292 size: r.size,
1293 }
1294 };
1295
1296 match item {
1297 DisplayListItem::Rect {
1298 bounds,
1299 color,
1300 border_radius,
1301 } => {
1302 let clip = *clip_stack.last().unwrap();
1303 render_rect(
1304 pixmap,
1305 &scroll_rect(bounds.inner()),
1306 *color,
1307 border_radius,
1308 clip,
1309 dpi_factor,
1310 );
1311 }
1312 DisplayListItem::SelectionRect {
1313 bounds,
1314 color,
1315 border_radius,
1316 } => {
1317 let clip = *clip_stack.last().unwrap();
1318 render_rect(
1319 pixmap,
1320 &scroll_rect(bounds.inner()),
1321 *color,
1322 border_radius,
1323 clip,
1324 dpi_factor,
1325 );
1326 }
1327 DisplayListItem::CursorRect { bounds, color } => {
1328 let clip = *clip_stack.last().unwrap();
1329 render_rect(
1330 pixmap,
1331 &scroll_rect(bounds.inner()),
1332 *color,
1333 &BorderRadius::default(),
1334 clip,
1335 dpi_factor,
1336 );
1337 }
1338 DisplayListItem::Border {
1339 bounds,
1340 widths,
1341 colors,
1342 styles,
1343 border_radius,
1344 } => {
1345 let default_color = ColorU {
1346 r: 0,
1347 g: 0,
1348 b: 0,
1349 a: 255,
1350 };
1351
1352 let w_top = widths
1353 .top
1354 .and_then(|w| w.get_property().copied())
1355 .map_or(0.0, |w| {
1356 w.inner
1357 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
1358 });
1359 let w_right = widths
1360 .right
1361 .and_then(|w| w.get_property().copied())
1362 .map_or(0.0, |w| {
1363 w.inner
1364 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
1365 });
1366 let w_bottom = widths
1367 .bottom
1368 .and_then(|w| w.get_property().copied())
1369 .map_or(0.0, |w| {
1370 w.inner
1371 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
1372 });
1373 let w_left = widths
1374 .left
1375 .and_then(|w| w.get_property().copied())
1376 .map_or(0.0, |w| {
1377 w.inner
1378 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
1379 });
1380
1381 let c_top = colors
1382 .top
1383 .and_then(|c| c.get_property().copied())
1384 .map_or(default_color, |c| c.inner);
1385 let c_right = colors
1386 .right
1387 .and_then(|c| c.get_property().copied())
1388 .map_or(default_color, |c| c.inner);
1389 let c_bottom = colors
1390 .bottom
1391 .and_then(|c| c.get_property().copied())
1392 .map_or(default_color, |c| c.inner);
1393 let c_left = colors
1394 .left
1395 .and_then(|c| c.get_property().copied())
1396 .map_or(default_color, |c| c.inner);
1397
1398 let s_top = styles
1399 .top
1400 .and_then(|s| s.get_property().copied())
1401 .map_or(BorderStyle::Solid, |s| s.inner);
1402 let s_right = styles
1403 .right
1404 .and_then(|s| s.get_property().copied())
1405 .map_or(BorderStyle::Solid, |s| s.inner);
1406 let s_bottom = styles
1407 .bottom
1408 .and_then(|s| s.get_property().copied())
1409 .map_or(BorderStyle::Solid, |s| s.inner);
1410 let s_left = styles
1411 .left
1412 .and_then(|s| s.get_property().copied())
1413 .map_or(BorderStyle::Solid, |s| s.inner);
1414
1415 let simple_radius = BorderRadius {
1416 top_left: border_radius.top_left.to_pixels_internal(
1417 bounds.0.size.width,
1418 DEFAULT_FONT_SIZE,
1419 DEFAULT_FONT_SIZE,
1420 ),
1421 top_right: border_radius.top_right.to_pixels_internal(
1422 bounds.0.size.width,
1423 DEFAULT_FONT_SIZE,
1424 DEFAULT_FONT_SIZE,
1425 ),
1426 bottom_left: border_radius.bottom_left.to_pixels_internal(
1427 bounds.0.size.width,
1428 DEFAULT_FONT_SIZE,
1429 DEFAULT_FONT_SIZE,
1430 ),
1431 bottom_right: border_radius.bottom_right.to_pixels_internal(
1432 bounds.0.size.width,
1433 DEFAULT_FONT_SIZE,
1434 DEFAULT_FONT_SIZE,
1435 ),
1436 };
1437
1438 let clip = *clip_stack.last().unwrap();
1439 let b = scroll_rect(bounds.inner());
1440
1441 let all_same = c_top == c_right
1443 && c_top == c_bottom
1444 && c_top == c_left
1445 && w_top == w_right
1446 && w_top == w_bottom
1447 && w_top == w_left
1448 && s_top == s_right
1449 && s_top == s_bottom
1450 && s_top == s_left;
1451
1452 if all_same {
1453 render_border(
1454 pixmap,
1455 &b,
1456 c_top,
1457 w_top,
1458 s_top,
1459 &simple_radius,
1460 clip,
1461 dpi_factor,
1462 );
1463 } else {
1464 render_border_sides(
1466 pixmap,
1467 &b,
1468 [c_top, c_right, c_bottom, c_left],
1469 [w_top, w_right, w_bottom, w_left],
1470 [s_top, s_right, s_bottom, s_left],
1471 &simple_radius,
1472 clip,
1473 dpi_factor,
1474 );
1475 }
1476 }
1477 DisplayListItem::Underline {
1478 bounds,
1479 color,
1480 thickness: _,
1481 } => {
1482 let clip = *clip_stack.last().unwrap();
1483 render_rect(
1484 pixmap,
1485 &scroll_rect(bounds.inner()),
1486 *color,
1487 &BorderRadius::default(),
1488 clip,
1489 dpi_factor,
1490 );
1491 }
1492 DisplayListItem::Strikethrough {
1493 bounds,
1494 color,
1495 thickness: _,
1496 } => {
1497 let clip = *clip_stack.last().unwrap();
1498 render_rect(
1499 pixmap,
1500 &scroll_rect(bounds.inner()),
1501 *color,
1502 &BorderRadius::default(),
1503 clip,
1504 dpi_factor,
1505 );
1506 }
1507 DisplayListItem::Overline {
1508 bounds,
1509 color,
1510 thickness: _,
1511 } => {
1512 let clip = *clip_stack.last().unwrap();
1513 render_rect(
1514 pixmap,
1515 &scroll_rect(bounds.inner()),
1516 *color,
1517 &BorderRadius::default(),
1518 clip,
1519 dpi_factor,
1520 );
1521 }
1522 DisplayListItem::Text {
1523 glyphs,
1524 font_size_px,
1525 font_hash,
1526 color,
1527 clip_rect,
1528 ..
1529 } => {
1530 let clip = *clip_stack.last().unwrap();
1531 let text_clip = scroll_rect(clip_rect.inner());
1532 for shadow in text_shadow_stack.iter() {
1537 render_text_shadow(
1538 shadow,
1539 glyphs,
1540 *font_hash,
1541 *font_size_px,
1542 pixmap,
1543 &text_clip,
1544 clip,
1545 renderer_resources,
1546 font_manager,
1547 dpi_factor,
1548 glyph_cache,
1549 (scroll_dx, scroll_dy),
1550 );
1551 }
1552 render_text(
1553 glyphs,
1554 *font_hash,
1555 *font_size_px,
1556 *color,
1557 pixmap,
1558 &text_clip,
1559 clip,
1560 renderer_resources,
1561 font_manager,
1562 dpi_factor,
1563 glyph_cache,
1564 (scroll_dx, scroll_dy),
1565 false,
1566 );
1567 }
1568 DisplayListItem::TextLayout {
1569 layout,
1570 bounds,
1571 font_hash,
1572 font_size_px,
1573 color,
1574 } => {
1575 }
1577 DisplayListItem::Image { bounds, image, .. } => {
1578 let clip = *clip_stack.last().unwrap();
1579 let resolved = render_state.image_callback_results.get(&image.get_hash());
1585 render_image(
1586 pixmap,
1587 &scroll_rect(bounds.inner()),
1588 resolved.unwrap_or(image),
1589 clip,
1590 dpi_factor,
1591 );
1592 }
1593 DisplayListItem::ScrollBar {
1594 bounds,
1595 color,
1596 orientation,
1597 opacity_key: _,
1598 hit_id: _,
1599 } => {
1600 let clip = *clip_stack.last().unwrap();
1601 render_rect(
1602 pixmap,
1603 &scroll_rect(bounds.inner()),
1604 *color,
1605 &BorderRadius::default(),
1606 clip,
1607 dpi_factor,
1608 );
1609 }
1610 DisplayListItem::ScrollBarStyled { info } => {
1611 let clip = *clip_stack.last().unwrap();
1612
1613 let scrollbar_opacity = info
1619 .opacity_key
1620 .and_then(|key| render_state.opacities.get(&key.id).copied())
1621 .unwrap_or(1.0);
1622
1623 if scrollbar_opacity > 0.001 {
1624 if info.track_color.a > 0 {
1626 render_rect(
1627 pixmap,
1628 &scroll_rect(info.track_bounds.inner()),
1629 info.track_color,
1630 &BorderRadius::default(),
1631 clip,
1632 dpi_factor,
1633 );
1634 }
1635
1636 if let Some(btn_bounds) = &info.button_decrement_bounds {
1638 if info.button_color.a > 0 {
1639 render_rect(
1640 pixmap,
1641 &scroll_rect(btn_bounds.inner()),
1642 info.button_color,
1643 &BorderRadius::default(),
1644 clip,
1645 dpi_factor,
1646 );
1647 }
1648 }
1649
1650 if let Some(btn_bounds) = &info.button_increment_bounds {
1652 if info.button_color.a > 0 {
1653 render_rect(
1654 pixmap,
1655 &scroll_rect(btn_bounds.inner()),
1656 info.button_color,
1657 &BorderRadius::default(),
1658 clip,
1659 dpi_factor,
1660 );
1661 }
1662 }
1663
1664 if info.thumb_color.a > 0 {
1669 let thumb_rect = info.thumb_bounds.inner();
1670 let transform = info
1672 .thumb_transform_key
1673 .and_then(|key| render_state.transforms.get(&key.id))
1674 .unwrap_or(&info.thumb_initial_transform);
1675 let tx = transform.m[3][0];
1676 let ty = transform.m[3][1];
1677 let transformed_thumb = LogicalRect {
1678 origin: LogicalPosition {
1679 x: thumb_rect.origin.x + tx,
1680 y: thumb_rect.origin.y + ty,
1681 },
1682 size: thumb_rect.size,
1683 };
1684 render_rect(
1685 pixmap,
1686 &scroll_rect(&transformed_thumb),
1687 info.thumb_color,
1688 &info.thumb_border_radius,
1689 clip,
1690 dpi_factor,
1691 );
1692 }
1693 } }
1695 DisplayListItem::PushClip {
1696 bounds,
1697 border_radius,
1698 } => {
1699 let new_clip = logical_rect_to_az_rect(&scroll_rect(bounds.inner()), dpi_factor);
1709 let new_clip = Some(new_clip.unwrap_or(AzRect::DENY_ALL));
1714 let merged = intersect_clips(clip_stack.last().copied().flatten(), new_clip);
1715 clip_stack.push(merged);
1716 }
1717 DisplayListItem::PopClip => {
1718 if clip_stack.len() > 1 {
1728 clip_stack.pop();
1729 } else {
1730 #[cfg(feature = "std")]
1731 if std::env::var("AZ_CLIP_DEBUG").is_ok() {
1732 eprintln!(
1733 "[CpuBackend] PopClip with no matching PushClip — clamping to base clip"
1734 );
1735 }
1736 }
1737 }
1738 DisplayListItem::PushScrollFrame { scroll_id, .. } => {
1739 transform_stack.push(
1744 transform_stack
1745 .last()
1746 .copied()
1747 .unwrap_or_else(TransAffine::new),
1748 );
1749 let frame_offset = render_state
1750 .scroll_offsets
1751 .get(scroll_id)
1752 .copied()
1753 .unwrap_or((0.0, 0.0));
1754 let new_scroll = (scroll_dx + frame_offset.0, scroll_dy + frame_offset.1);
1755 scroll_offset_stack.push(new_scroll);
1756 }
1757 DisplayListItem::PopScrollFrame => {
1758 if transform_stack.len() > 1 {
1761 transform_stack.pop();
1762 }
1763 if scroll_offset_stack.len() > 1 {
1764 scroll_offset_stack.pop();
1765 }
1766 }
1767 DisplayListItem::HitTestArea { bounds, tag } => {
1768 }
1770 DisplayListItem::PushStackingContext { z_index, bounds } => {
1771 }
1773 DisplayListItem::PopStackingContext => {}
1774 DisplayListItem::VirtualView {
1775 child_dom_id,
1776 bounds,
1777 clip_rect,
1778 } => {
1779 let _ = clip_rect;
1780 let child_dl = render_state.virtual_view_display_lists.get(child_dom_id).cloned();
1789 #[cfg(feature = "std")]
1790 if std::env::var("AZ_MAP_DEBUG").is_ok() {
1791 eprintln!(
1792 "[cpu-vview] VirtualView item: child_dom_id={} found={} items={} bounds={:?} avail_ids={:?}",
1793 child_dom_id.inner,
1794 child_dl.is_some(),
1795 child_dl.as_ref().map_or(0, |d| d.items.len()),
1796 bounds.inner(),
1797 render_state.virtual_view_display_lists.keys().map(|k| k.inner).collect::<Vec<_>>(),
1798 );
1799 }
1800 if let Some(child_dl) = child_dl {
1801 let vv_origin = bounds.inner().origin;
1802 let vv_clip = intersect_clips(
1805 clip_stack.last().copied().flatten(),
1806 logical_rect_to_az_rect(&scroll_rect(bounds.inner()), dpi_factor),
1807 );
1808 clip_stack.push(vv_clip);
1809 scroll_offset_stack.push((scroll_dx - vv_origin.x, scroll_dy - vv_origin.y));
1810 for child_item in &child_dl.items {
1811 render_single_item(
1812 child_item,
1813 pixmap,
1814 dpi_factor,
1815 renderer_resources,
1816 font_manager,
1817 glyph_cache,
1818 transform_stack,
1819 clip_stack,
1820 mask_stack,
1821 scroll_offset_stack,
1822 text_shadow_stack,
1823 render_state,
1824 )?;
1825 }
1826 scroll_offset_stack.pop();
1827 clip_stack.pop();
1828 }
1829 }
1830 DisplayListItem::VirtualViewPlaceholder { .. } => {
1831 #[cfg(feature = "std")]
1832 if std::env::var("AZ_MAP_DEBUG").is_ok() {
1833 eprintln!("[cpu-vview] VirtualViewPlaceholder hit (NOT swapped to a VirtualView item — nothing composites)");
1834 }
1835 }
1836
1837 DisplayListItem::LinearGradient {
1839 bounds,
1840 gradient,
1841 border_radius,
1842 } => {
1843 let clip = *clip_stack.last().unwrap();
1844 render_linear_gradient(
1845 pixmap,
1846 &scroll_rect(bounds.inner()),
1847 gradient,
1848 border_radius,
1849 clip,
1850 dpi_factor,
1851 render_state.system_style.as_deref().map(|s| &s.colors),
1852 );
1853 }
1854 DisplayListItem::RadialGradient {
1855 bounds,
1856 gradient,
1857 border_radius,
1858 } => {
1859 let clip = *clip_stack.last().unwrap();
1860 render_radial_gradient(
1861 pixmap,
1862 &scroll_rect(bounds.inner()),
1863 gradient,
1864 border_radius,
1865 clip,
1866 dpi_factor,
1867 render_state.system_style.as_deref().map(|s| &s.colors),
1868 );
1869 }
1870 DisplayListItem::ConicGradient {
1871 bounds,
1872 gradient,
1873 border_radius,
1874 } => {
1875 let clip = *clip_stack.last().unwrap();
1876 render_conic_gradient(
1877 pixmap,
1878 &scroll_rect(bounds.inner()),
1879 gradient,
1880 border_radius,
1881 clip,
1882 dpi_factor,
1883 render_state.system_style.as_deref().map(|s| &s.colors),
1884 );
1885 }
1886
1887 DisplayListItem::BoxShadow {
1889 bounds,
1890 shadow,
1891 border_radius,
1892 } => {
1893 render_box_shadow(
1894 pixmap,
1895 &scroll_rect(bounds.inner()),
1896 shadow,
1897 border_radius,
1898 dpi_factor,
1899 )?;
1900 }
1901
1902 DisplayListItem::PushOpacity { bounds, opacity } => {
1904 let rect = logical_rect_to_az_rect(&scroll_rect(bounds.inner()), dpi_factor);
1905 if let Some(r) = rect {
1906 let snap = snapshot_region(
1907 pixmap,
1908 r.x as i32,
1909 r.y as i32,
1910 r.width as u32,
1911 r.height as u32,
1912 );
1913 mask_stack.push(MaskEntry::Opacity {
1914 snapshot: snap,
1915 rect: r,
1916 opacity: *opacity,
1917 });
1918 }
1919 }
1920 DisplayListItem::PopOpacity => {
1921 if let Some(MaskEntry::Opacity {
1922 snapshot,
1923 rect,
1924 opacity,
1925 }) = mask_stack.pop()
1926 {
1927 let x = rect.x as i32;
1928 let y = rect.y as i32;
1929 let w = rect.width as u32;
1930 let h = rect.height as u32;
1931 let pw = pixmap.width as i32;
1932 let ph = pixmap.height as i32;
1933 for py in 0..h as i32 {
1935 let dy = y + py;
1936 if dy < 0 || dy >= ph {
1937 continue;
1938 }
1939 for px in 0..w as i32 {
1940 let dx = x + px;
1941 if dx < 0 || dx >= pw {
1942 continue;
1943 }
1944 let pi = ((dy as u32 * pixmap.width + dx as u32) * 4) as usize;
1945 let si = ((py as u32 * w + px as u32) * 4) as usize;
1946 if pi + 3 >= pixmap.data.len() || si + 3 >= snapshot.len() {
1947 continue;
1948 }
1949 let op = (opacity * 255.0).clamp(0.0, 255.0) as u32;
1950 let inv_op = 255 - op;
1951 for c in 0..4 {
1952 let snap_c = u32::from(snapshot[si + c]);
1953 let cur_c = u32::from(pixmap.data[pi + c]);
1954 pixmap.data[pi + c] = ((cur_c * op + snap_c * inv_op) / 255) as u8;
1955 }
1956 }
1957 }
1958 }
1959 }
1960
1961 DisplayListItem::PushReferenceFrame {
1963 transform_key,
1964 initial_transform,
1965 bounds,
1966 } => {
1967 let live_transform = render_state.transforms.get(&transform_key.id);
1972 let m = live_transform.map_or(&initial_transform.m, |t| &t.m);
1973 let tf = TransAffine::new_custom(
1974 f64::from(m[0][0]),
1975 f64::from(m[0][1]), f64::from(m[1][0]),
1977 f64::from(m[1][1]), f64::from(m[3][0]),
1979 f64::from(m[3][1]), );
1981 let current = transform_stack
1982 .last()
1983 .copied()
1984 .unwrap_or_else(TransAffine::new);
1985 let mut composed = tf;
1986 composed.premultiply(¤t);
1987 transform_stack.push(composed);
1988 }
1989 DisplayListItem::PopReferenceFrame => {
1990 if transform_stack.len() > 1 {
1991 transform_stack.pop();
1992 }
1993 }
1994
1995 DisplayListItem::PushFilter { .. } => {}
2005 DisplayListItem::PopFilter => {}
2006
2007 DisplayListItem::PushBackdropFilter { .. } => {}
2022 DisplayListItem::PopBackdropFilter => {}
2023
2024 DisplayListItem::PushTextShadow { shadow } => {
2031 text_shadow_stack.push(*shadow);
2032 }
2033 DisplayListItem::PopTextShadow => {
2034 text_shadow_stack.pop();
2035 }
2036
2037 DisplayListItem::PushImageMaskClip {
2038 bounds,
2039 mask_image,
2040 mask_rect,
2041 } => {
2042 let mr = &scroll_rect(mask_rect.inner());
2043 let px_x = (mr.origin.x * dpi_factor) as i32;
2044 let px_y = (mr.origin.y * dpi_factor) as i32;
2045 let px_w = (mr.size.width * dpi_factor).ceil() as u32;
2046 let px_h = (mr.size.height * dpi_factor).ceil() as u32;
2047
2048 if px_w > 0 && px_h > 0 {
2049 let snapshot = snapshot_region(pixmap, px_x, px_y, px_w, px_h);
2050 let mask_data = extract_mask_data(mask_image, px_w, px_h)
2051 .unwrap_or_else(|| vec![255u8; (px_w * px_h) as usize]);
2052 mask_stack.push(MaskEntry::ImageMask {
2053 snapshot,
2054 mask_data,
2055 origin_x: px_x,
2056 origin_y: px_y,
2057 width: px_w,
2058 height: px_h,
2059 });
2060 }
2061 }
2062 DisplayListItem::PopImageMaskClip => {
2063 if let Some(entry) = mask_stack.pop() {
2064 apply_mask(pixmap, &entry);
2065 }
2066 }
2067 }
2068
2069 Ok(())
2070}
2071
2072#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] fn render_rect(
2074 pixmap: &mut AzulPixmap,
2075 bounds: &LogicalRect,
2076 color: ColorU,
2077 border_radius: &BorderRadius,
2078 clip: Option<AzRect>,
2079 dpi_factor: f32,
2080) {
2081 if color.a == 0 {
2082 return;
2083 }
2084
2085 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
2086 return;
2087 };
2088
2089 if let Some(ref c) = clip {
2091 if rect.clip(c).is_none() {
2092 return;
2093 }
2094 }
2095
2096 let agg_color = Rgba8::new(
2097 u32::from(color.r),
2098 u32::from(color.g),
2099 u32::from(color.b),
2100 u32::from(color.a),
2101 );
2102
2103 if border_radius.is_zero() {
2104 let w = pixmap.width;
2108 let h = pixmap.height;
2109 let stride = (w * 4) as i32;
2110 let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
2111 let mut pf = PixfmtRgba32::new(&mut ra);
2112 let mut rb = RendererBase::new(pf);
2113 if let Some(c) = clip {
2114 rb.clip_box_i(
2115 c.x as i32,
2116 c.y as i32,
2117 (c.x + c.width) as i32 - 1,
2118 (c.y + c.height) as i32 - 1,
2119 );
2120 }
2121 rb.blend_bar(
2122 rect.x as i32,
2123 rect.y as i32,
2124 (rect.x + rect.width) as i32 - 1,
2125 (rect.y + rect.height) as i32 - 1,
2126 &agg_color,
2127 255, );
2129 } else {
2130 let mut path = build_rounded_rect_path(&rect, border_radius, dpi_factor);
2132 agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::NonZero, clip);
2133 }
2134
2135}
2136
2137pub const TEXT_LCD_DEFAULT: bool = true;
2147
2148fn text_lcd_enabled() -> bool {
2151 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2152 *V.get_or_init(|| {
2153 std::env::var("AZ_TEXT_LCD")
2154 .map(|s| !(s == "0" || s.eq_ignore_ascii_case("false")))
2155 .unwrap_or(TEXT_LCD_DEFAULT)
2156 })
2157}
2158
2159#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] #[allow(clippy::too_many_arguments)] fn render_glyphs_lcd(
2183 pixmap: &mut AzulPixmap,
2184 clip: Option<AzRect>,
2185 glyphs: &[GlyphInstance],
2186 parsed_font: &ParsedFont,
2187 font_hash: FontHash,
2188 ppem: u16,
2189 scale: f32,
2190 hint_correction: f32,
2191 color: ColorU,
2192 dpi_factor: f32,
2193 scroll_offset: (f32, f32),
2194 glyph_cache: &mut GlyphCache,
2195) {
2196 use agg_rust::pixfmt_lcd::{LcdDistributionLut, PixfmtRgba32Lcd};
2197
2198 let agg_color = Rgba8::new(
2199 u32::from(color.r),
2200 u32::from(color.g),
2201 u32::from(color.b),
2202 u32::from(color.a),
2203 );
2204 let subpx = crate::glyph_cache::text_subpixel_enabled();
2205
2206 let mut ras = RasterizerScanlineAa::new();
2209 ras.filling_rule(FillingRule::NonZero);
2210
2211 for glyph in glyphs {
2212 let glyph_index = glyph.index as u16;
2213 let Some(glyph_data) = parsed_font.get_or_decode_glyph(glyph_index) else {
2214 continue;
2215 };
2216 let Some(cached) = glyph_cache.get_or_build(
2217 font_hash.font_hash,
2218 glyph_index,
2219 &glyph_data,
2220 parsed_font,
2221 ppem,
2222 ) else {
2223 continue;
2224 };
2225 let is_hinted = cached.is_hinted;
2226
2227 let glyph_x = (glyph.point.x - scroll_offset.0) * dpi_factor;
2228 let glyph_baseline_y = (glyph.point.y - scroll_offset.1) * dpi_factor;
2229 let px = if subpx { glyph_x } else { glyph_x.round() };
2232 let py = glyph_baseline_y.round();
2233
2234 let rescale_hinted = is_hinted && (hint_correction - 1.0).abs() > 1e-4;
2239 let path_scale = if is_hinted {
2240 if rescale_hinted { f64::from(hint_correction) } else { 1.0 }
2241 } else {
2242 f64::from(scale)
2243 };
2244
2245 let mut transform = TransAffine::new_scaling(3.0 * path_scale, path_scale);
2250 transform.multiply(&TransAffine::new_translation(3.0 * f64::from(px), f64::from(py)));
2251 let mut src = agg_rust::conv_transform::ConvTransform::new(
2254 crate::glyph_cache::SliceVertexSource::new(cached.path.vertices()),
2255 transform,
2256 );
2257 ras.add_path(&mut src, 0);
2258 }
2259
2260 let w = pixmap.width;
2264 let h = pixmap.height;
2265 let stride = (w * 4) as i32;
2266 let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
2267 let lut = LcdDistributionLut::new(f64::from(0x56u32), f64::from(0x4Du32), f64::from(0x08u32));
2270 let pf = PixfmtRgba32Lcd::new(&mut ra, &lut);
2271 let mut rb = RendererBase::new(pf);
2272 if let Some(c) = clip {
2273 rb.clip_box_i(
2274 (c.x as i32) * 3,
2275 c.y as i32,
2276 ((c.x + c.width) as i32) * 3 - 1,
2277 (c.y + c.height) as i32 - 1,
2278 );
2279 }
2280 let mut sl = ScanlineU8::new();
2281 render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &agg_color);
2282}
2283
2284#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines)] fn render_text(
2287 glyphs: &[GlyphInstance],
2288 font_hash: FontHash,
2289 font_size_px: f32,
2290 color: ColorU,
2291 pixmap: &mut AzulPixmap,
2292 clip_rect: &LogicalRect,
2293 clip: Option<AzRect>,
2294 renderer_resources: &RendererResources,
2295 font_manager: Option<&FontManager<FontRef>>,
2296 dpi_factor: f32,
2297 glyph_cache: &mut GlyphCache,
2298 scroll_offset: (f32, f32),
2299 force_grayscale: bool,
2304) {
2305 if color.a == 0 || glyphs.is_empty() {
2306 return;
2307 }
2308
2309 if let Some(ref c) = clip {
2311 let Some(text_rect) = logical_rect_to_az_rect(clip_rect, dpi_factor) else {
2312 return;
2313 };
2314 if text_rect.clip(c).is_none() {
2315 return; }
2317 }
2318
2319 let agg_color = Rgba8::new(
2320 u32::from(color.r),
2321 u32::from(color.g),
2322 u32::from(color.b),
2323 u32::from(color.a),
2324 );
2325
2326 let parsed_font: &ParsedFont = if let Some(fm) = font_manager {
2328 if let Some(font_ref) = fm.get_font_by_hash(font_hash.font_hash) { unsafe { &*font_ref.get_parsed().cast::<ParsedFont>() } } else {
2329 eprintln!(
2330 "[cpurender] Font hash {} not found in FontManager",
2331 font_hash.font_hash
2332 );
2333 return;
2334 }
2335 } else {
2336 let Some(font_key) = renderer_resources.font_hash_map.get(&font_hash.font_hash) else {
2337 eprintln!(
2338 "[cpurender] Font hash {} not found in font_hash_map (available: {:?})",
2339 font_hash.font_hash,
2340 renderer_resources.font_hash_map.keys().collect::<Vec<_>>()
2341 );
2342 return;
2343 };
2344
2345 let Some((font_ref, _instances)) = renderer_resources.currently_registered_fonts.get(font_key) else {
2346 eprintln!(
2347 "[cpurender] FontKey {font_key:?} not found in currently_registered_fonts"
2348 );
2349 return;
2350 };
2351
2352 unsafe { &*font_ref.get_parsed().cast::<ParsedFont>() }
2353 };
2354
2355 let units_per_em = f32::from(parsed_font.font_metrics.units_per_em);
2356 if units_per_em <= 0.0 {
2357 return;
2358 }
2359
2360 let effective_px = font_size_px * dpi_factor;
2361 let scale = effective_px / units_per_em;
2362 let ppem = effective_px.round() as u16;
2363 let hint_correction = if ppem > 0 { effective_px / f32::from(ppem) } else { 1.0 };
2367
2368 if text_lcd_enabled() && !force_grayscale {
2372 render_glyphs_lcd(
2373 pixmap, clip, glyphs, parsed_font, font_hash, ppem, scale,
2374 hint_correction, color, dpi_factor, scroll_offset, glyph_cache,
2375 );
2376 return;
2377 }
2378
2379 let w = pixmap.width;
2381 let h = pixmap.height;
2382 let stride = (w * 4) as i32;
2383
2384 let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
2387 let mut pf = PixfmtRgba32::new(&mut ra);
2388 let mut rb = RendererBase::new(pf);
2389 if let Some(c) = clip {
2390 rb.clip_box_i(
2391 c.x as i32,
2392 c.y as i32,
2393 (c.x + c.width) as i32 - 1,
2394 (c.y + c.height) as i32 - 1,
2395 );
2396 }
2397 let mut ras = RasterizerScanlineAa::new();
2398 ras.filling_rule(FillingRule::NonZero);
2399
2400 for glyph in glyphs {
2403 let glyph_index = glyph.index as u16;
2404
2405 let Some(glyph_data) = parsed_font.get_or_decode_glyph(glyph_index) else {
2409 continue;
2410 };
2411
2412 let is_hinted = glyph_cache
2413 .get_or_build(
2414 font_hash.font_hash,
2415 glyph_index,
2416 &glyph_data,
2417 parsed_font,
2418 ppem,
2419 )
2420 .is_some_and(|c| c.is_hinted);
2421
2422 let glyph_x = (glyph.point.x - scroll_offset.0) * dpi_factor;
2423 let glyph_baseline_y = (glyph.point.y - scroll_offset.1) * dpi_factor;
2424
2425 let Some((cells, int_x, int_y)) = glyph_cache.get_or_build_cells(
2426 font_hash.font_hash,
2427 glyph_index,
2428 ppem,
2429 glyph_x,
2430 glyph_baseline_y,
2431 scale,
2432 is_hinted,
2433 hint_correction,
2434 ) else {
2435 continue;
2436 };
2437
2438 ras.add_cells_offset(cells, int_x, int_y);
2439 }
2440
2441 let mut sl = ScanlineU8::new();
2443 render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &agg_color);
2444
2445}
2446
2447#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)]
2461fn render_text_shadow(
2462 shadow: &StyleBoxShadow,
2463 glyphs: &[GlyphInstance],
2464 font_hash: FontHash,
2465 font_size_px: f32,
2466 pixmap: &mut AzulPixmap,
2467 clip_rect: &LogicalRect,
2468 clip: Option<AzRect>,
2469 renderer_resources: &RendererResources,
2470 font_manager: Option<&FontManager<FontRef>>,
2471 dpi_factor: f32,
2472 glyph_cache: &mut GlyphCache,
2473 scroll_offset: (f32, f32),
2474) {
2475 let color = shadow.color;
2476 if color.a == 0 || glyphs.is_empty() {
2477 return;
2478 }
2479
2480 let off_x = shadow
2482 .offset_x
2483 .inner
2484 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
2485 let off_y = shadow
2486 .offset_y
2487 .inner
2488 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
2489 let blur_logical = shadow
2490 .blur_radius
2491 .inner
2492 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
2493 .max(0.0);
2494
2495 let Some(mut tmp) = AzulPixmap::new(pixmap.width, pixmap.height) else {
2497 return;
2498 };
2499 tmp.fill(0, 0, 0, 0);
2500
2501 let shifted: Vec<GlyphInstance> = glyphs
2503 .iter()
2504 .map(|g| {
2505 let mut g = *g;
2506 g.point.x += off_x;
2507 g.point.y += off_y;
2508 g
2509 })
2510 .collect();
2511
2512 let shadow_clip_rect = LogicalRect {
2514 origin: LogicalPosition {
2515 x: clip_rect.origin.x + off_x,
2516 y: clip_rect.origin.y + off_y,
2517 },
2518 size: clip_rect.size,
2519 };
2520 render_text(
2521 &shifted,
2522 font_hash,
2523 font_size_px,
2524 color,
2525 &mut tmp,
2526 &shadow_clip_rect,
2527 clip,
2528 renderer_resources,
2529 font_manager,
2530 dpi_factor,
2531 glyph_cache,
2532 scroll_offset,
2533 true,
2536 );
2537
2538 let blur_px = blur_logical * dpi_factor;
2540 if blur_px > 0.5 {
2541 let radius = (blur_px.ceil() as u32).min(254);
2542 let w = tmp.width;
2543 let h = tmp.height;
2544 let stride = (w * 4) as i32;
2545 let mut ra = unsafe { RowAccessor::new_with_buf(tmp.data.as_mut_ptr(), w, h, stride) };
2546 stack_blur_rgba32(&mut ra, radius, radius);
2547 }
2548
2549 blit_buffer(pixmap, &tmp.data, tmp.width, tmp.height, 0, 0);
2551}
2552
2553#[allow(clippy::suboptimal_flops)] #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] fn render_border(
2556 pixmap: &mut AzulPixmap,
2557 bounds: &LogicalRect,
2558 color: ColorU,
2559 width: f32,
2560 border_style: azul_css::props::style::border::BorderStyle,
2561 border_radius: &BorderRadius,
2562 clip: Option<AzRect>,
2563 dpi_factor: f32,
2564) {
2565 use azul_css::props::style::border::BorderStyle;
2566
2567 if color.a == 0 || width <= 0.0 {
2568 return;
2569 }
2570
2571 match border_style {
2572 BorderStyle::None | BorderStyle::Hidden => return,
2573 _ => {}
2574 }
2575
2576 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
2577 return;
2578 };
2579
2580 if let Some(ref c) = clip {
2582 if rect.clip(c).is_none() {
2583 return;
2584 }
2585 }
2586
2587 let scaled_width = width * dpi_factor;
2588 let agg_color = Rgba8::new(
2589 u32::from(color.r),
2590 u32::from(color.g),
2591 u32::from(color.b),
2592 u32::from(color.a),
2593 );
2594
2595 let mut path = build_rounded_rect_path(&rect, border_radius, dpi_factor);
2597
2598 let x = f64::from(rect.x);
2599 let y = f64::from(rect.y);
2600 let w = f64::from(rect.width);
2601 let h = f64::from(rect.height);
2602 let sw = f64::from(scaled_width);
2603
2604 let ir = AzRect::from_xywh(
2606 rect.x + scaled_width,
2607 rect.y + scaled_width,
2608 rect.width - 2.0 * scaled_width,
2609 rect.height - 2.0 * scaled_width,
2610 );
2611
2612 if let Some(ir) = ir {
2613 let inner_radius = BorderRadius {
2614 top_left: (border_radius.top_left - width).max(0.0),
2615 top_right: (border_radius.top_right - width).max(0.0),
2616 bottom_right: (border_radius.bottom_right - width).max(0.0),
2617 bottom_left: (border_radius.bottom_left - width).max(0.0),
2618 };
2619 let mut inner = build_rounded_rect_path(&ir, &inner_radius, dpi_factor);
2620 path.concat_path(&mut inner, 0);
2621 }
2622
2623 match border_style {
2625 BorderStyle::Dashed | BorderStyle::Dotted => {
2626 use agg_rust::conv_dash::ConvDash;
2628 use agg_rust::conv_stroke::ConvStroke;
2629
2630 let half = sw / 2.0;
2631 let mut stroke_path = PathStorage::new();
2632 let (cx, cy, cw, ch) = (x + half, y + half, w - sw, h - sw);
2633 stroke_path.move_to(cx, cy);
2634 stroke_path.line_to(cx + cw, cy);
2635 stroke_path.line_to(cx + cw, cy + ch);
2636 stroke_path.line_to(cx, cy + ch);
2637 stroke_path.close_polygon(PATH_FLAGS_NONE);
2638
2639 let mut dashed = ConvDash::new(stroke_path);
2640 if border_style == BorderStyle::Dashed {
2641 dashed.add_dash(sw * 3.0, sw);
2642 } else {
2643 dashed.add_dash(sw, sw);
2644 }
2645
2646 let mut stroked = ConvStroke::new(dashed);
2647 stroked.set_width(sw);
2648
2649 agg_fill_path_clipped(pixmap, &mut stroked, &agg_color, FillingRule::NonZero, clip);
2650 }
2651 _ if border_radius.is_zero() => {
2652 let pw = pixmap.width;
2654 let ph = pixmap.height;
2655 let stride = (pw * 4) as i32;
2656 let mut ra =
2657 unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), pw, ph, stride) };
2658 let mut pf = PixfmtRgba32::new(&mut ra);
2659 let mut rb = RendererBase::new(pf);
2660 if let Some(c) = clip {
2661 rb.clip_box_i(
2662 c.x as i32,
2663 c.y as i32,
2664 (c.x + c.width) as i32 - 1,
2665 (c.y + c.height) as i32 - 1,
2666 );
2667 }
2668 let (xi, yi) = (x as i32, y as i32);
2669 let (x2i, y2i) = ((x + w) as i32 - 1, (y + h) as i32 - 1);
2670 let swi = sw as i32;
2671 rb.blend_bar(xi, yi, x2i, yi + swi - 1, &agg_color, 255);
2673 rb.blend_bar(xi, y2i - swi + 1, x2i, y2i, &agg_color, 255);
2675 rb.blend_bar(xi, yi + swi, xi + swi - 1, y2i - swi, &agg_color, 255);
2677 rb.blend_bar(x2i - swi + 1, yi + swi, x2i, y2i - swi, &agg_color, 255);
2679 }
2680 _ => {
2681 agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::EvenOdd, clip);
2683 }
2684 }
2685
2686}
2687
2688#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] #[allow(clippy::too_many_lines)] fn render_border_sides(
2693 pixmap: &mut AzulPixmap,
2694 bounds: &LogicalRect,
2695 colors: [ColorU; 4], widths: [f32; 4], _styles: [azul_css::props::style::border::BorderStyle; 4],
2698 border_radius: &BorderRadius,
2699 clip: Option<AzRect>,
2700 dpi_factor: f32,
2701) {
2702 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
2703 return;
2704 };
2705
2706 let ox = f64::from(rect.x);
2708 let oy = f64::from(rect.y);
2709 let ow = f64::from(rect.width);
2710 let oh = f64::from(rect.height);
2711
2712 let wt = f64::from(widths[0] * dpi_factor);
2714 let wr = f64::from(widths[1] * dpi_factor);
2715 let wb = f64::from(widths[2] * dpi_factor);
2716 let wl = f64::from(widths[3] * dpi_factor);
2717
2718 let ix = ox + wl;
2719 let iy = oy + wt;
2720 let iw = ow - wl - wr;
2721 let ih = oh - wt - wb;
2722
2723 let sides: [(f64, f64, f64, f64, f64, f64, f64, f64, ColorU, f32); 4] = [
2730 (
2732 ox,
2733 oy,
2734 ox + ow,
2735 oy,
2736 ix + iw,
2737 iy,
2738 ix,
2739 iy,
2740 colors[0],
2741 widths[0],
2742 ),
2743 (
2745 ox + ow,
2746 oy,
2747 ox + ow,
2748 oy + oh,
2749 ix + iw,
2750 iy + ih,
2751 ix + iw,
2752 iy,
2753 colors[1],
2754 widths[1],
2755 ),
2756 (
2758 ox + ow,
2759 oy + oh,
2760 ox,
2761 oy + oh,
2762 ix,
2763 iy + ih,
2764 ix + iw,
2765 iy + ih,
2766 colors[2],
2767 widths[2],
2768 ),
2769 (
2771 ox,
2772 oy + oh,
2773 ox,
2774 oy,
2775 ix,
2776 iy,
2777 ix,
2778 iy + ih,
2779 colors[3],
2780 widths[3],
2781 ),
2782 ];
2783
2784 if border_radius.is_zero() {
2785 let pw = pixmap.width;
2787 let ph = pixmap.height;
2788 let stride = (pw * 4) as i32;
2789 let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), pw, ph, stride) };
2790 let mut pf = PixfmtRgba32::new(&mut ra);
2791 let mut rb = RendererBase::new(pf);
2792 if let Some(c) = clip {
2793 rb.clip_box_i(
2794 c.x as i32,
2795 c.y as i32,
2796 (c.x + c.width) as i32 - 1,
2797 (c.y + c.height) as i32 - 1,
2798 );
2799 }
2800 if widths[0] > 0.0 && colors[0].a > 0 {
2802 let c = colors[0];
2803 let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
2804 rb.blend_bar(
2805 ox as i32,
2806 oy as i32,
2807 (ox + ow) as i32 - 1,
2808 iy as i32 - 1,
2809 &ac,
2810 255,
2811 );
2812 }
2813 if widths[2] > 0.0 && colors[2].a > 0 {
2815 let c = colors[2];
2816 let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
2817 rb.blend_bar(
2818 ox as i32,
2819 (iy + ih) as i32,
2820 (ox + ow) as i32 - 1,
2821 (oy + oh) as i32 - 1,
2822 &ac,
2823 255,
2824 );
2825 }
2826 if widths[3] > 0.0 && colors[3].a > 0 {
2828 let c = colors[3];
2829 let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
2830 rb.blend_bar(
2831 ox as i32,
2832 iy as i32,
2833 ix as i32 - 1,
2834 (iy + ih) as i32 - 1,
2835 &ac,
2836 255,
2837 );
2838 }
2839 if widths[1] > 0.0 && colors[1].a > 0 {
2841 let c = colors[1];
2842 let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
2843 rb.blend_bar(
2844 (ix + iw) as i32,
2845 iy as i32,
2846 (ox + ow) as i32 - 1,
2847 (iy + ih) as i32 - 1,
2848 &ac,
2849 255,
2850 );
2851 }
2852 } else {
2853 for &(x0, y0, x1, y1, x2, y2, x3, y3, color, width) in &sides {
2855 if width <= 0.0 || color.a == 0 {
2856 continue;
2857 }
2858
2859 let mut path = PathStorage::new();
2860 path.move_to(x0, y0);
2861 path.line_to(x1, y1);
2862 path.line_to(x2, y2);
2863 path.line_to(x3, y3);
2864 path.close_polygon(PATH_FLAGS_NONE);
2865
2866 let agg_color = Rgba8::new(
2867 u32::from(color.r),
2868 u32::from(color.g),
2869 u32::from(color.b),
2870 u32::from(color.a),
2871 );
2872 agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::NonZero, clip);
2873 }
2874 }
2875
2876}
2877
2878#[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(
2882 pixmap: &mut AzulPixmap,
2883 bounds: &LogicalRect,
2884 image: &ImageRef,
2885 clip: Option<AzRect>,
2886 dpi_factor: f32,
2887) {
2888 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
2889 return;
2890 };
2891
2892 if let Some(ref c) = clip {
2894 if rect.clip(c).is_none() {
2895 return;
2896 }
2897 }
2898
2899 let image_data = image.get_data();
2900 let (src_rgba, src_w, src_h) = match image_data {
2901 DecodedImage::Raw((descriptor, data)) => {
2902 let w = descriptor.width as u32;
2903 let h = descriptor.height as u32;
2904 if w == 0 || h == 0 {
2905 return;
2906 }
2907 let bytes = match data {
2908 azul_core::resources::ImageData::Raw(shared) => shared.as_ref(),
2909 azul_core::resources::ImageData::External(_) => return,
2910 };
2911
2912 let rgba = match descriptor.format {
2913 azul_core::resources::RawImageFormat::RGBA8 => bytes.to_vec(),
2919 azul_core::resources::RawImageFormat::RGB8 => {
2920 let mut out = Vec::with_capacity(bytes.len() / 3 * 4);
2921 for chunk in bytes.chunks_exact(3) {
2922 out.extend_from_slice(&[chunk[0], chunk[1], chunk[2], 255]);
2923 }
2924 out
2925 }
2926 azul_core::resources::RawImageFormat::BGRA8 => {
2927 let mut out = Vec::with_capacity(bytes.len());
2928 for chunk in bytes.chunks_exact(4) {
2929 let b = chunk[0];
2930 let g = chunk[1];
2931 let r = chunk[2];
2932 let a = chunk[3];
2933 out.push(r);
2934 out.push(g);
2935 out.push(b);
2936 out.push(a);
2937 }
2938 out
2939 }
2940 azul_core::resources::RawImageFormat::R8 => {
2941 let mut out = Vec::with_capacity(bytes.len() * 4);
2942 for &v in bytes {
2943 out.push(v);
2944 out.push(v);
2945 out.push(v);
2946 out.push(v);
2947 }
2948 out
2949 }
2950 _ => {
2951 let gray = Rgba8::new(200, 200, 200, 255);
2953 let mut path = build_rect_path(&rect);
2954 agg_fill_path(pixmap, &mut path, &gray, FillingRule::NonZero);
2955 return;
2956 }
2957 };
2958
2959 (rgba, w, h)
2960 }
2961 DecodedImage::NullImage { .. } | DecodedImage::Callback(_) => {
2962 let gray = Rgba8::new(200, 200, 200, 255);
2963 let mut path = build_rect_path(&rect);
2964 agg_fill_path(pixmap, &mut path, &gray, FillingRule::NonZero);
2965 return;
2966 }
2967 DecodedImage::Gl(_) => return,
2968 };
2969
2970 let dst_x = rect.x as i32;
2972 let dst_y = rect.y as i32;
2973 let dst_w = rect.width as u32;
2974 let dst_h = rect.height as u32;
2975 let pw = pixmap.width;
2976 let ph = pixmap.height;
2977
2978 let sx = src_w as f32 / dst_w.max(1) as f32;
2979 let sy = src_h as f32 / dst_h.max(1) as f32;
2980
2981 let (clip_x1, clip_y1, clip_x2, clip_y2) = clip.as_ref().map_or((0, 0, pw as i32, ph as i32), |c| (
2983 c.x as i32,
2984 c.y as i32,
2985 (c.x + c.width) as i32,
2986 (c.y + c.height) as i32,
2987 ));
2988
2989 for py in 0..dst_h {
2990 for px in 0..dst_w {
2991 let tx = dst_x + px as i32;
2992 let ty = dst_y + py as i32;
2993 if tx < 0 || ty < 0 || tx >= pw as i32 || ty >= ph as i32 {
2994 continue;
2995 }
2996 if tx < clip_x1 || ty < clip_y1 || tx >= clip_x2 || ty >= clip_y2 {
2998 continue;
2999 }
3000
3001 let src_x = ((px as f32 * sx) as u32).min(src_w - 1);
3002 let src_y = ((py as f32 * sy) as u32).min(src_h - 1);
3003 let si = ((src_y * src_w + src_x) * 4) as usize;
3004 let di = ((ty as u32 * pw + tx as u32) * 4) as usize;
3005
3006 if si + 3 < src_rgba.len() && di + 3 < pixmap.data.len() {
3007 let sa = u32::from(src_rgba[si + 3]);
3008 if sa == 255 {
3009 pixmap.data[di] = src_rgba[si];
3010 pixmap.data[di + 1] = src_rgba[si + 1];
3011 pixmap.data[di + 2] = src_rgba[si + 2];
3012 pixmap.data[di + 3] = 255;
3013 } else if sa > 0 {
3014 let da = 255 - sa;
3016 pixmap.data[di] =
3017 ((u32::from(src_rgba[si]) * sa + u32::from(pixmap.data[di]) * da) / 255) as u8;
3018 pixmap.data[di + 1] = ((u32::from(src_rgba[si + 1]) * sa
3019 + u32::from(pixmap.data[di + 1]) * da)
3020 / 255) as u8;
3021 pixmap.data[di + 2] = ((u32::from(src_rgba[si + 2]) * sa
3022 + u32::from(pixmap.data[di + 2]) * da)
3023 / 255) as u8;
3024 pixmap.data[di + 3] =
3025 ((sa + u32::from(pixmap.data[di + 3]) * da / 255).min(255)) as u8;
3026 }
3027 }
3028 }
3029 }
3030
3031}
3032
3033fn build_rect_path(rect: &AzRect) -> PathStorage {
3034 let mut path = PathStorage::new();
3035 let x = f64::from(rect.x);
3036 let y = f64::from(rect.y);
3037 let w = f64::from(rect.width);
3038 let h = f64::from(rect.height);
3039 path.move_to(x, y);
3040 path.line_to(x + w, y);
3041 path.line_to(x + w, y + h);
3042 path.line_to(x, y + h);
3043 path.close_polygon(PATH_FLAGS_NONE);
3044 path
3045}
3046
3047fn build_rounded_rect_path(
3048 rect: &AzRect,
3049 border_radius: &BorderRadius,
3050 dpi_factor: f32,
3051) -> PathStorage {
3052 let mut path = PathStorage::new();
3053
3054 let x = f64::from(rect.x);
3055 let y = f64::from(rect.y);
3056 let w = f64::from(rect.width);
3057 let h = f64::from(rect.height);
3058
3059 let tl = f64::from(border_radius.top_left * dpi_factor);
3060 let tr = f64::from(border_radius.top_right * dpi_factor);
3061 let br = f64::from(border_radius.bottom_right * dpi_factor);
3062 let bl = f64::from(border_radius.bottom_left * dpi_factor);
3063
3064 if tl <= 0.0 && tr <= 0.0 && br <= 0.0 && bl <= 0.0 {
3065 path.move_to(x, y);
3066 path.line_to(x + w, y);
3067 path.line_to(x + w, y + h);
3068 path.line_to(x, y + h);
3069 path.close_polygon(PATH_FLAGS_NONE);
3070 return path;
3071 }
3072
3073 let mut rr = RoundedRect::default_new();
3085 rr.rect(x, y, x + w, y + h);
3086 rr.radius_all(tl, tl, tr, tr, br, br, bl, bl);
3087 rr.normalize_radius();
3088 rr.set_approximation_scale(f64::from(dpi_factor.max(1.0)));
3089
3090 path.concat_path(&mut rr, 0);
3091 path
3092}
3093
3094#[derive(Debug, Clone, Copy)]
3100pub struct ComponentPreviewOptions {
3101 pub width: Option<f32>,
3103 pub height: Option<f32>,
3105 pub dpi_factor: f32,
3107 pub background_color: ColorU,
3109}
3110
3111impl Default for ComponentPreviewOptions {
3112 fn default() -> Self {
3113 Self {
3114 width: None,
3115 height: None,
3116 dpi_factor: 1.0,
3117 background_color: ColorU {
3118 r: 255,
3119 g: 255,
3120 b: 255,
3121 a: 255,
3122 },
3123 }
3124 }
3125}
3126
3127#[derive(Debug)]
3129pub struct ComponentPreviewResult {
3130 pub png_data: Vec<u8>,
3132 pub content_width: f32,
3134 pub content_height: f32,
3136}
3137
3138#[allow(clippy::match_same_arms)] fn compute_content_bounds(dl: &DisplayList) -> Option<(f32, f32, f32, f32)> {
3141 let mut min_x = f32::MAX;
3142 let mut min_y = f32::MAX;
3143 let mut max_x = f32::MIN;
3144 let mut max_y = f32::MIN;
3145 let mut has_items = false;
3146
3147 for item in &dl.items {
3148 let bounds = match item {
3149 DisplayListItem::Rect { bounds, .. } => Some(*bounds),
3150 DisplayListItem::SelectionRect { bounds, .. } => Some(*bounds),
3151 DisplayListItem::Border { bounds, .. } => Some(*bounds),
3152 DisplayListItem::Text { clip_rect, .. } => Some(*clip_rect),
3153 DisplayListItem::Image { bounds, .. } => Some(*bounds),
3154 DisplayListItem::BoxShadow { bounds, .. } => Some(*bounds),
3155 DisplayListItem::PushClip { bounds, .. } => Some(*bounds),
3156 DisplayListItem::LinearGradient { bounds, .. } => Some(*bounds),
3157 DisplayListItem::RadialGradient { bounds, .. } => Some(*bounds),
3158 DisplayListItem::ConicGradient { bounds, .. } => Some(*bounds),
3159 DisplayListItem::VirtualView { bounds, .. } => Some(*bounds),
3160 DisplayListItem::ScrollBar { bounds, .. } => Some(*bounds),
3161 _ => None,
3162 };
3163 if let Some(b) = bounds {
3164 has_items = true;
3165 min_x = min_x.min(b.0.origin.x);
3166 min_y = min_y.min(b.0.origin.y);
3167 max_x = max_x.max(b.0.origin.x + b.0.size.width);
3168 max_y = max_y.max(b.0.origin.y + b.0.size.height);
3169 }
3170 }
3171
3172 if has_items {
3173 Some((min_x, min_y, max_x, max_y))
3174 } else {
3175 None
3176 }
3177}
3178
3179#[cfg(all(feature = "std", feature = "text_layout", feature = "font_loading"))]
3181#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines)] pub fn render_component_preview(
3190 styled_dom: &azul_core::styled_dom::StyledDom,
3191 font_manager: &FontManager<FontRef>,
3192 opts: ComponentPreviewOptions,
3193 system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
3194) -> Result<ComponentPreviewResult, String> {
3195 use crate::{
3196 font_traits::TextLayoutCache,
3197 solver3::{self, cache::LayoutCache, display_list::DisplayList},
3198 };
3199 use azul_core::{
3200 dom::DomId,
3201 geom::{LogicalPosition, LogicalRect, LogicalSize},
3202 resources::{IdNamespace, RendererResources},
3203 selection::{SelectionState, TextSelection},
3204 };
3205 use std::collections::{BTreeMap, HashMap};
3206
3207 const MAX_SIZE: f32 = 4096.0;
3208
3209 let layout_width = opts.width.unwrap_or(MAX_SIZE);
3210 let layout_height = opts.height.unwrap_or(MAX_SIZE);
3211
3212 let viewport = LogicalRect {
3213 origin: LogicalPosition::zero(),
3214 size: LogicalSize {
3215 width: layout_width,
3216 height: layout_height,
3217 },
3218 };
3219
3220 let mut preview_font_manager = FontManager::from_arc_shared(
3221 font_manager.fc_cache.clone(),
3222 font_manager.parsed_fonts.clone(),
3223 )
3224 .map_err(|e| format!("Failed to create preview font manager: {e:?}"))?;
3225
3226 for (family, faces) in &font_manager.memory_families {
3232 preview_font_manager
3233 .memory_families
3234 .entry(family.clone())
3235 .or_insert_with(|| faces.clone());
3236 }
3237
3238 {
3240 use crate::solver3::getters::collect_and_resolve_font_chains_with_registration;
3241 use crate::text3::default::PathLoader;
3242
3243 let platform = azul_css::system::Platform::current();
3244
3245 let chains = collect_and_resolve_font_chains_with_registration(
3246 styled_dom,
3247 &preview_font_manager.fc_cache,
3248 &preview_font_manager,
3249 &platform,
3250 );
3251 let loader = PathLoader::new();
3252 let _failed = preview_font_manager.load_missing_for_chains(&chains, |bytes, index| {
3253 loader.load_font_shared(bytes, index)
3254 });
3255 preview_font_manager.set_font_chain_cache(chains.into_fontconfig_chains());
3256 }
3257
3258 let mut layout_cache = LayoutCache {
3260 tree: None,
3261 calculated_positions: Vec::new(),
3262 viewport: None,
3263 scroll_ids: HashMap::new(),
3264 scroll_id_to_node_id: HashMap::new(),
3265 counters: HashMap::new(),
3266 float_cache: HashMap::new(),
3267 cache_map: solver3::cache::LayoutCacheMap::default(),
3268 previous_positions: Vec::new(),
3269 cached_display_list: None,
3270 prev_dom_ptr: 0,
3271 prev_viewport: LogicalRect::zero(),
3272 };
3273 let mut text_cache = TextLayoutCache::new();
3274 let empty_scroll_offsets = BTreeMap::new();
3275 let empty_text_selections = BTreeMap::new();
3276 let renderer_resources = RendererResources::default();
3277 let id_namespace = IdNamespace(0xFFFF);
3278 let dom_id = DomId::ROOT_ID;
3279 let mut debug_messages = None;
3280 let get_system_time_fn = azul_core::task::GetSystemTimeCallback {
3281 cb: azul_core::task::get_system_time_libstd,
3282 };
3283
3284 let display_list = solver3::layout_document(
3285 &mut layout_cache,
3286 &mut text_cache,
3287 styled_dom,
3288 viewport,
3289 &preview_font_manager,
3290 &empty_scroll_offsets,
3291 &empty_text_selections,
3292 &mut debug_messages,
3293 None,
3294 &renderer_resources,
3295 id_namespace,
3296 dom_id,
3297 false,
3298 Vec::new(),
3299 None, &azul_core::resources::ImageCache::default(),
3301 system_style.clone(),
3302 get_system_time_fn,
3303 )
3304 .map_err(|e| format!("Layout failed: {e:?}"))?;
3305
3306 let (render_width, render_height) = if opts.width.is_some() && opts.height.is_some() {
3308 (opts.width.unwrap(), opts.height.unwrap())
3309 } else {
3310 match compute_content_bounds(&display_list) {
3311 Some((_min_x, _min_y, max_x, max_y)) => {
3312 let w = if opts.width.is_some() {
3313 opts.width.unwrap()
3314 } else {
3315 max_x.max(1.0).ceil()
3316 };
3317 let h = if opts.height.is_some() {
3318 opts.height.unwrap()
3319 } else {
3320 max_y.max(1.0).ceil()
3321 };
3322 (w, h)
3323 }
3324 None => {
3325 return Ok(ComponentPreviewResult {
3326 png_data: Vec::new(),
3327 content_width: 0.0,
3328 content_height: 0.0,
3329 });
3330 }
3331 }
3332 };
3333
3334 let render_width = render_width.min(MAX_SIZE);
3335 let render_height = render_height.min(MAX_SIZE);
3336
3337 let dpi = opts.dpi_factor;
3339 let pixel_w = ((render_width * dpi) as u32).max(1);
3340 let pixel_h = ((render_height * dpi) as u32).max(1);
3341
3342 let mut pixmap = AzulPixmap::new(pixel_w, pixel_h)
3343 .ok_or_else(|| format!("Cannot create pixmap {pixel_w}x{pixel_h}"))?;
3344
3345 let bg = opts.background_color;
3346 pixmap.fill(bg.r, bg.g, bg.b, bg.a);
3347
3348 let mut preview_glyph_cache = GlyphCache::new();
3349 let preview_render_state =
3350 CpuRenderState::new(ScrollOffsetMap::new()).with_system_style(system_style);
3351 render_display_list_with_state(
3352 &display_list,
3353 &mut pixmap,
3354 dpi,
3355 &renderer_resources,
3356 Some(&preview_font_manager),
3357 &mut preview_glyph_cache,
3358 &preview_render_state,
3359 )?;
3360
3361 let png_data = pixmap
3362 .encode_png()
3363 .map_err(|e| format!("PNG encoding failed: {e}"))?;
3364
3365 Ok(ComponentPreviewResult {
3366 png_data,
3367 content_width: render_width,
3368 content_height: render_height,
3369 })
3370}
3371
3372#[cfg(all(feature = "std", feature = "text_layout", feature = "font_loading"))]
3377pub fn render_dom_to_image(
3381 mut dom: azul_core::dom::Dom,
3382 css: azul_css::css::Css,
3383 width: f32,
3384 height: f32,
3385 dpi: f32,
3386) -> Result<Vec<u8>, String> {
3387 use crate::font_traits::FontManager;
3388 use azul_core::styled_dom::StyledDom;
3389
3390 let styled_dom = StyledDom::create(&mut dom, css);
3391
3392 let fc_cache = crate::font::loading::build_font_cache();
3393 let font_manager = FontManager::new(fc_cache)
3394 .map_err(|e| format!("Failed to create font manager: {e:?}"))?;
3395
3396 let opts = ComponentPreviewOptions {
3397 width: Some(width),
3398 height: Some(height),
3399 dpi_factor: dpi,
3400 background_color: ColorU {
3401 r: 255,
3402 g: 255,
3403 b: 255,
3404 a: 255,
3405 },
3406 };
3407
3408 let result = render_component_preview(&styled_dom, &font_manager, opts, None)?;
3409 Ok(result.png_data)
3410}
3411
3412#[cfg(all(feature = "std", feature = "text_layout", feature = "font_loading"))]
3430#[must_use]
3431#[allow(clippy::suboptimal_flops, clippy::cast_possible_truncation, clippy::cast_sign_loss)]
3433pub fn render_text_run_to_pixmap(
3434 fc_cache: &rust_fontconfig::FcFontCache,
3435 text: &str,
3436 font_size_px: f32,
3437 text_color: ColorU,
3438 bg_color: ColorU,
3439 padding_px: f32,
3440 dpi_factor: f32,
3441) -> Option<AzulPixmap> {
3442 use azul_core::resources::{FontKey, IdNamespace};
3443 use rust_fontconfig::{FcPattern, OwnedFontSource};
3444
3445 let mut trace = Vec::new();
3447 let matched = fc_cache
3448 .query(
3449 &FcPattern {
3450 family: Some("sans-serif".to_string()),
3451 ..Default::default()
3452 },
3453 &mut trace,
3454 )
3455 .or_else(|| fc_cache.query(&FcPattern::default(), &mut trace))?;
3456
3457 let bytes = fc_cache.get_font_bytes(&matched.id)?;
3458 let font_index = fc_cache
3459 .get_font_by_id(&matched.id)
3460 .map_or(0, |src| match src {
3461 OwnedFontSource::Disk(path) => path.font_index,
3462 OwnedFontSource::Memory(font) => font.font_index,
3463 });
3464
3465 let parsed = ParsedFont::from_bytes(bytes.as_slice(), font_index, &mut Vec::new())?
3466 .with_source_bytes(bytes.clone());
3467
3468 let upm = f32::from(parsed.font_metrics.units_per_em);
3469 if upm <= 0.0 {
3470 return None;
3471 }
3472 let scale = font_size_px / upm;
3473
3474 let mut rr = RendererResources::default();
3477 let font_ref = crate::parsed_font_to_font_ref(parsed.clone());
3478 let key = FontKey::unique(IdNamespace(0));
3479 let hash = crate::font_ref_to_parsed_font(&font_ref).hash;
3480 rr.font_hash_map.insert(hash, key);
3481 rr.currently_registered_fonts
3482 .insert(key, (font_ref, std::collections::BTreeMap::default()));
3483 let font_hash = FontHash { font_hash: hash };
3484
3485 let ascent = parsed.font_metrics.ascent * scale;
3489 let descent = parsed.font_metrics.descent * scale; let baseline_y = padding_px + ascent;
3491 let mut pen_x = padding_px;
3492 let mut glyphs = Vec::new();
3493 for c in text.chars() {
3494 let gid = parsed.lookup_glyph_index(c as u32).unwrap_or(0);
3495 let advance = f32::from(parsed.get_horizontal_advance(gid)) * scale;
3496 glyphs.push(GlyphInstance {
3497 index: u32::from(gid),
3498 point: LogicalPosition { x: pen_x, y: baseline_y },
3499 size: LogicalSize { width: advance, height: font_size_px },
3500 });
3501 pen_x += advance;
3502 }
3503
3504 let logical_w = (pen_x + padding_px).max(1.0);
3506 let logical_h = (ascent - descent + padding_px * 2.0).max(1.0);
3507 let w = ((logical_w * dpi_factor).ceil() as u32).max(1);
3508 let h = ((logical_h * dpi_factor).ceil() as u32).max(1);
3509
3510 let mut pixmap = AzulPixmap::new(w, h)?;
3511 pixmap.fill(bg_color.r, bg_color.g, bg_color.b, bg_color.a);
3512
3513 let clip_rect: crate::solver3::display_list::WindowLogicalRect = LogicalRect {
3515 origin: LogicalPosition { x: 0.0, y: 0.0 },
3516 size: LogicalSize { width: logical_w, height: logical_h },
3517 }
3518 .into();
3519
3520 let item = DisplayListItem::Text {
3521 glyphs,
3522 font_hash,
3523 font_size_px,
3524 color: text_color,
3525 clip_rect,
3526 source_node_index: None,
3527 };
3528 let dl = DisplayList {
3529 items: vec![item],
3530 ..Default::default()
3531 };
3532 let mut gc = GlyphCache::new();
3533 render_display_list(&dl, &mut pixmap, dpi_factor, &rr, None, &mut gc).ok()?;
3534
3535 Some(pixmap)
3536}
3537
3538#[cfg(all(test, feature = "std"))]
3544mod text_shadow_tests {
3545 use super::*;
3546 use crate::font::parsed::ParsedFont;
3547 use crate::solver3::display_list::{DisplayList, WindowLogicalRect};
3548 use azul_core::resources::{FontKey, IdNamespace};
3549 use azul_css::props::basic::pixel::{PixelValue, PixelValueNoPercent};
3550 use azul_css::props::style::box_shadow::StyleBoxShadow;
3551
3552 fn load_test_font() -> Option<ParsedFont> {
3553 let candidates = [
3554 "/System/Library/Fonts/Supplemental/Times New Roman.ttf",
3555 "/System/Library/Fonts/Helvetica.ttc",
3556 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
3557 "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
3558 "C:/Windows/Fonts/arial.ttf",
3559 ];
3560 for path in candidates {
3561 if let Ok(bytes) = std::fs::read(path) {
3562 let arc = std::sync::Arc::new(rust_fontconfig::FontBytes::Owned(
3563 std::sync::Arc::from(bytes.as_slice()),
3564 ));
3565 if let Some(font) = ParsedFont::from_bytes(&bytes, 0, &mut Vec::new())
3566 .map(|f| f.with_source_bytes(arc))
3567 {
3568 return Some(font);
3569 }
3570 }
3571 }
3572 None
3573 }
3574
3575 fn renderer_resources_with(font: &ParsedFont) -> (RendererResources, FontHash) {
3576 let mut rr = RendererResources::default();
3577 let font_ref = crate::parsed_font_to_font_ref(font.clone());
3578 let key = FontKey::unique(IdNamespace(0));
3579 let hash = crate::font_ref_to_parsed_font(&font_ref).hash;
3580 rr.font_hash_map.insert(hash, key);
3581 rr.currently_registered_fonts
3582 .insert(key, (font_ref, std::collections::BTreeMap::default()));
3583 (rr, FontHash { font_hash: hash })
3584 }
3585
3586 fn shape(parsed: &ParsedFont, text: &str, font_size: f32, x: f32, y: f32) -> Vec<GlyphInstance> {
3588 let upm = f32::from(parsed.font_metrics.units_per_em);
3589 let scale = font_size / upm;
3590 let mut pen_x = x;
3591 let mut out = Vec::new();
3592 for c in text.chars() {
3593 let gid = parsed.lookup_glyph_index(c as u32).unwrap_or(0);
3594 let advance = f32::from(parsed.get_horizontal_advance(gid)) * scale;
3595 out.push(GlyphInstance {
3596 index: u32::from(gid),
3597 point: LogicalPosition { x: pen_x, y },
3598 size: LogicalSize {
3599 width: advance,
3600 height: font_size,
3601 },
3602 });
3603 pen_x += advance;
3604 }
3605 out
3606 }
3607
3608 fn count_red(pixmap: &AzulPixmap) -> usize {
3609 pixmap
3610 .data()
3611 .chunks_exact(4)
3612 .filter(|p| p[0] > 150 && p[1] < 100 && p[2] < 100)
3613 .count()
3614 }
3615
3616 #[test]
3619 fn text_shadow_paints_offset_colored_pixels() {
3620 let Some(font) = load_test_font() else {
3621 eprintln!("[skip] no system font available");
3622 return;
3623 };
3624 let (rr, font_hash) = renderer_resources_with(&font);
3625
3626 let w = 200u32;
3627 let h = 60u32;
3628 let font_size = 32.0;
3629 let glyphs = shape(&font, "Hi", font_size, 10.0, 40.0);
3631 #[allow(clippy::cast_precision_loss)]
3633 let clip_rect: WindowLogicalRect = LogicalRect {
3634 origin: LogicalPosition { x: 0.0, y: 0.0 },
3635 size: LogicalSize { width: w as f32, height: h as f32 },
3636 }
3637 .into();
3638
3639 let text_item = DisplayListItem::Text {
3640 glyphs,
3641 font_hash,
3642 font_size_px: font_size,
3643 color: ColorU { r: 0, g: 0, b: 0, a: 255 },
3644 clip_rect,
3645 source_node_index: None,
3646 };
3647
3648 let mut gc = GlyphCache::new();
3650 let mut no_shadow = AzulPixmap::new(w, h).unwrap();
3651 no_shadow.fill(255, 255, 255, 255);
3652 let dl_plain = DisplayList {
3653 items: vec![text_item.clone()],
3654 ..Default::default()
3655 };
3656 render_display_list(&dl_plain, &mut no_shadow, 1.0, &rr, None, &mut gc).unwrap();
3657 let red_plain = count_red(&no_shadow);
3661
3662 let shadow = StyleBoxShadow {
3664 offset_x: PixelValueNoPercent { inner: PixelValue::px(24.0) },
3665 offset_y: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3666 blur_radius: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3667 spread_radius: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3668 color: ColorU { r: 255, g: 0, b: 0, a: 255 },
3669 clip_mode: azul_css::props::style::box_shadow::BoxShadowClipMode::Outset,
3670 };
3671 let mut with_shadow = AzulPixmap::new(w, h).unwrap();
3672 with_shadow.fill(255, 255, 255, 255);
3673 let dl_shadow = DisplayList {
3674 items: vec![
3675 DisplayListItem::PushTextShadow { shadow },
3676 text_item,
3677 DisplayListItem::PopTextShadow,
3678 ],
3679 ..Default::default()
3680 };
3681 let mut gc2 = GlyphCache::new();
3682 render_display_list(&dl_shadow, &mut with_shadow, 1.0, &rr, None, &mut gc2).unwrap();
3683 let red_shadow = count_red(&with_shadow);
3684
3685 assert!(
3686 red_shadow > red_plain + 20,
3687 "text-shadow must paint red shadow pixels beyond the baseline \
3688 (plain {red_plain}, shadow {red_shadow})"
3689 );
3690
3691 let right_red = with_shadow
3695 .data()
3696 .chunks_exact(4)
3697 .enumerate()
3698 .filter(|(i, p)| {
3699 #[allow(clippy::cast_possible_truncation)] let x = (*i as u32) % w;
3701 x > 30 && p[0] > 150 && p[1] < 100 && p[2] < 100
3702 })
3703 .count();
3704 assert!(
3705 right_red > 0,
3706 "shadow should appear offset to the right of the glyphs"
3707 );
3708 }
3709
3710 #[test]
3713 fn text_shadow_blur_spreads_coverage() {
3714 let Some(font) = load_test_font() else {
3715 eprintln!("[skip] no system font available");
3716 return;
3717 };
3718 let (rr, font_hash) = renderer_resources_with(&font);
3719 let w = 200u32;
3720 let h = 80u32;
3721 let font_size = 32.0;
3722 let glyphs = shape(&font, "Hi", font_size, 40.0, 50.0);
3723 #[allow(clippy::cast_precision_loss)]
3725 let clip_rect: WindowLogicalRect = LogicalRect {
3726 origin: LogicalPosition { x: 0.0, y: 0.0 },
3727 size: LogicalSize { width: w as f32, height: h as f32 },
3728 }
3729 .into();
3730
3731 let make = |blur: f32| -> usize {
3732 let shadow = StyleBoxShadow {
3733 offset_x: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3734 offset_y: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3735 blur_radius: PixelValueNoPercent { inner: PixelValue::px(blur) },
3736 spread_radius: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3737 color: ColorU { r: 255, g: 0, b: 0, a: 255 },
3738 clip_mode: azul_css::props::style::box_shadow::BoxShadowClipMode::Outset,
3739 };
3740 let text_item = DisplayListItem::Text {
3741 glyphs: glyphs.clone(),
3742 font_hash,
3743 font_size_px: font_size,
3744 color: ColorU { r: 0, g: 0, b: 0, a: 0 }, clip_rect,
3746 source_node_index: None,
3747 };
3748 let dl = DisplayList {
3749 items: vec![
3750 DisplayListItem::PushTextShadow { shadow },
3751 text_item,
3752 DisplayListItem::PopTextShadow,
3753 ],
3754 ..Default::default()
3755 };
3756 let mut pm = AzulPixmap::new(w, h).unwrap();
3757 pm.fill(255, 255, 255, 255);
3758 let mut gc = GlyphCache::new();
3759 render_display_list(&dl, &mut pm, 1.0, &rr, None, &mut gc).unwrap();
3760 pm.data()
3762 .chunks_exact(4)
3763 .filter(|p| p[0] != 255 || p[1] != 255 || p[2] != 255)
3764 .count()
3765 };
3766
3767 let hard = make(0.0);
3768 let blurred = make(6.0);
3769 assert!(hard > 0, "hard shadow should paint");
3770 assert!(
3771 blurred > hard,
3772 "blurred shadow ({blurred}) should cover more pixels than hard ({hard})"
3773 );
3774 }
3775}
3776
3777#[cfg(all(test, feature = "std"))]
3778#[allow(clippy::float_cmp)] #[allow(clippy::many_single_char_names)] #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] mod autotest_generated {
3782 use agg_rust::gradient_lut::ColorFunction;
3783 use azul_core::{
3784 dom::{DomId, NodeId},
3785 gpu::GpuValueCache,
3786 resources::{OpacityKey, RawImage, RawImageData, RawImageFormat, TransformKey},
3787 transform::ComputedTransform3D,
3788 };
3789 use azul_css::{
3790 props::{
3791 basic::{
3792 angle::AngleValue,
3793 length::PercentageValue,
3794 pixel::{PixelValue, PixelValueNoPercent},
3795 color::{OptionColorU, SystemColorRef},
3796 },
3797 style::{
3798 background::{
3799 BackgroundPositionHorizontal, BackgroundPositionVertical, ConicGradient,
3800 LinearGradient, NormalizedLinearColorStop, NormalizedLinearColorStopVec,
3801 NormalizedRadialColorStop, NormalizedRadialColorStopVec, RadialGradient,
3802 RadialGradientSize, Shape, StyleBackgroundPosition,
3803 },
3804 border::BorderStyle,
3805 box_shadow::BoxShadowClipMode,
3806 },
3807 },
3808 system::SystemColors,
3809 };
3810
3811 use super::*;
3812 use crate::solver3::display_list::WindowLogicalRect;
3813
3814 const RED: ColorU = ColorU { r: 255, g: 0, b: 0, a: 255 };
3819 const BLACK: ColorU = ColorU { r: 0, g: 0, b: 0, a: 255 };
3820 const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
3821 const BLUE: ColorU = ColorU { r: 0, g: 0, b: 255, a: 255 };
3822 const CLEAR: ColorU = ColorU { r: 255, g: 0, b: 0, a: 0 };
3823
3824 const DEGENERATE: [f32; 7] = [
3829 0.0,
3830 -0.0,
3831 -1.0,
3832 f32::NAN,
3833 f32::INFINITY,
3834 f32::NEG_INFINITY,
3835 f32::MIN,
3836 ];
3837
3838 fn pixmap(w: u32, h: u32) -> AzulPixmap {
3839 let mut p = AzulPixmap::new(w, h).expect("test pixmap must allocate");
3840 p.fill(255, 255, 255, 255);
3841 p
3842 }
3843
3844 fn snap(p: &AzulPixmap) -> Vec<u8> {
3845 p.data().to_vec()
3846 }
3847
3848 fn px_at(p: &AzulPixmap, x: u32, y: u32) -> [u8; 4] {
3849 let i = ((y * p.width + x) * 4) as usize;
3850 [p.data()[i], p.data()[i + 1], p.data()[i + 2], p.data()[i + 3]]
3851 }
3852
3853 fn is_reddish(px: [u8; 4]) -> bool {
3854 px[0] > 200 && px[1] < 60 && px[2] < 60
3855 }
3856
3857 fn lrect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
3858 LogicalRect {
3859 origin: LogicalPosition { x, y },
3860 size: LogicalSize {
3861 width: w,
3862 height: h,
3863 },
3864 }
3865 }
3866
3867 fn wrect(x: f32, y: f32, w: f32, h: f32) -> WindowLogicalRect {
3868 lrect(x, y, w, h).into()
3869 }
3870
3871 fn lin_stops(pairs: &[(f32, ColorU)]) -> NormalizedLinearColorStopVec {
3872 pairs
3873 .iter()
3874 .map(|(offset_percent, color)| NormalizedLinearColorStop {
3875 offset: PercentageValue::new(*offset_percent),
3876 color: ColorOrSystem::Color(*color),
3877 })
3878 .collect::<Vec<_>>()
3879 .into()
3880 }
3881
3882 fn rad_stops(pairs: &[(f32, ColorU)]) -> NormalizedRadialColorStopVec {
3883 pairs
3884 .iter()
3885 .map(|(degrees, color)| NormalizedRadialColorStop {
3886 angle: AngleValue::deg(*degrees),
3887 color: ColorOrSystem::Color(*color),
3888 })
3889 .collect::<Vec<_>>()
3890 .into()
3891 }
3892
3893 fn shadow(offset: f32, blur: f32, spread: f32, color: ColorU) -> StyleBoxShadow {
3894 StyleBoxShadow {
3895 offset_x: PixelValueNoPercent {
3896 inner: PixelValue::px(offset),
3897 },
3898 offset_y: PixelValueNoPercent {
3899 inner: PixelValue::px(offset),
3900 },
3901 blur_radius: PixelValueNoPercent {
3902 inner: PixelValue::px(blur),
3903 },
3904 spread_radius: PixelValueNoPercent {
3905 inner: PixelValue::px(spread),
3906 },
3907 color,
3908 clip_mode: BoxShadowClipMode::Outset,
3909 }
3910 }
3911
3912 fn r8_image(w: usize, h: usize, bytes: Vec<u8>) -> ImageRef {
3913 ImageRef::new_rawimage(RawImage {
3914 pixels: RawImageData::U8(bytes.into()),
3915 width: w,
3916 height: h,
3917 premultiplied_alpha: false,
3918 data_format: RawImageFormat::R8,
3919 tag: Vec::new().into(),
3920 })
3921 .expect("R8 RawImage must build")
3922 }
3923
3924 fn rgba_image(w: usize, h: usize, bytes: Vec<u8>) -> ImageRef {
3925 ImageRef::new_rawimage(RawImage {
3926 pixels: RawImageData::U8(bytes.into()),
3927 width: w,
3928 height: h,
3929 premultiplied_alpha: false,
3930 data_format: RawImageFormat::RGBA8,
3931 tag: Vec::new().into(),
3932 })
3933 .expect("RGBA8 RawImage must build")
3934 }
3935
3936 struct Stacks {
3939 transforms: Vec<TransAffine>,
3940 clips: Vec<Option<AzRect>>,
3941 masks: Vec<MaskEntry>,
3942 scrolls: Vec<(f32, f32)>,
3943 shadows: Vec<StyleBoxShadow>,
3944 }
3945
3946 impl Stacks {
3947 fn new() -> Self {
3948 Self {
3949 transforms: vec![TransAffine::new()],
3950 clips: vec![None],
3951 masks: Vec::new(),
3952 scrolls: vec![(0.0, 0.0)],
3953 shadows: Vec::new(),
3954 }
3955 }
3956 }
3957
3958 fn run_item(
3960 item: &DisplayListItem,
3961 p: &mut AzulPixmap,
3962 st: &mut Stacks,
3963 state: &CpuRenderState,
3964 ) -> Result<(), String> {
3965 let res = RendererResources::default();
3966 let mut gc = GlyphCache::new();
3967 render_single_item(
3968 item,
3969 p,
3970 1.0,
3971 &res,
3972 None,
3973 &mut gc,
3974 &mut st.transforms,
3975 &mut st.clips,
3976 &mut st.masks,
3977 &mut st.scrolls,
3978 &mut st.shadows,
3979 state,
3980 )
3981 }
3982
3983 fn run_list(dl: &DisplayList, p: &mut AzulPixmap, dpi: f32) -> Result<(), String> {
3984 let res = RendererResources::default();
3985 let mut gc = GlyphCache::new();
3986 render_display_list(dl, p, dpi, &res, None, &mut gc)
3987 }
3988
3989 fn run_list_with_state(
3990 dl: &DisplayList,
3991 p: &mut AzulPixmap,
3992 state: &CpuRenderState,
3993 ) -> Result<(), String> {
3994 let res = RendererResources::default();
3995 let mut gc = GlyphCache::new();
3996 render_display_list_with_state(dl, p, 1.0, &res, None, &mut gc, state)
3997 }
3998
3999 #[test]
4004 fn resolve_color_concrete_is_returned_verbatim() {
4005 let c = ColorU { r: 1, g: 2, b: 3, a: 4 };
4006 let palette = SystemColors {
4007 accent: OptionColorU::Some(BLUE),
4008 ..SystemColors::default()
4009 };
4010 assert_eq!(resolve_color(&ColorOrSystem::Color(c), None), c);
4012 assert_eq!(resolve_color(&ColorOrSystem::Color(c), Some(&palette)), c);
4013 }
4014
4015 #[test]
4016 fn resolve_color_system_without_palette_is_transparent_fallback() {
4017 for key in [
4018 SystemColorRef::Text,
4019 SystemColorRef::Accent,
4020 SystemColorRef::SelectionBackground,
4021 ] {
4022 let got = resolve_color(&ColorOrSystem::System(key), None);
4023 assert_eq!(got, SYSTEM_COLOR_FALLBACK);
4024 assert_eq!(got.a, 0, "the fallback must contribute nothing");
4025 }
4026 }
4027
4028 #[test]
4029 fn resolve_color_system_resolves_set_keys_and_falls_back_for_unset_ones() {
4030 let palette = SystemColors {
4031 accent: OptionColorU::Some(BLUE),
4032 ..SystemColors::default()
4033 };
4034 assert_eq!(
4035 resolve_color(&ColorOrSystem::System(SystemColorRef::Accent), Some(&palette)),
4036 BLUE
4037 );
4038 assert_eq!(
4040 resolve_color(&ColorOrSystem::System(SystemColorRef::Text), Some(&palette)),
4041 SYSTEM_COLOR_FALLBACK
4042 );
4043 assert_eq!(
4045 resolve_color(
4046 &ColorOrSystem::System(SystemColorRef::ButtonFace),
4047 Some(&SystemColors::default())
4048 ),
4049 SYSTEM_COLOR_FALLBACK
4050 );
4051 }
4052
4053 #[test]
4058 fn gradient_lut_linear_under_two_stops_is_fully_transparent() {
4059 for stops in [lin_stops(&[]), lin_stops(&[(50.0, RED)])] {
4060 let lut = build_gradient_lut_linear(&stops, None);
4061 assert_eq!(lut.size(), 256);
4062 for i in [0usize, 1, 128, 255] {
4063 assert_eq!(
4064 lut.get(i).a,
4065 0,
4066 "a gradient with <2 stops must not paint anything"
4067 );
4068 }
4069 }
4070 }
4071
4072 #[test]
4073 fn gradient_lut_linear_two_stops_interpolate_end_to_end() {
4074 let lut = build_gradient_lut_linear(&lin_stops(&[(0.0, BLACK), (100.0, WHITE)]), None);
4075 assert_eq!(lut.size(), 256);
4076 assert_eq!(lut.get(0).r, 0);
4077 assert_eq!(lut.get(255).r, 255);
4078 assert!(lut.get(64).r < lut.get(192).r);
4080 assert_eq!(lut.get(0).a, 255);
4081 }
4082
4083 #[test]
4084 fn gradient_lut_linear_out_of_range_offsets_are_clamped_not_panicking() {
4085 let lut = build_gradient_lut_linear(
4087 &lin_stops(&[(-500.0, BLACK), (900.0, WHITE), (1e30, RED)]),
4088 None,
4089 );
4090 assert_eq!(lut.size(), 256);
4091 assert_eq!(lut.get(0).r, 0, "the -500% stop clamps to offset 0");
4092 assert!(lut.get(255).a > 0);
4094 }
4095
4096 #[test]
4097 fn gradient_lut_linear_unsorted_stops_are_sorted_by_offset() {
4098 let lut = build_gradient_lut_linear(&lin_stops(&[(100.0, WHITE), (0.0, BLACK)]), None);
4100 assert_eq!(lut.get(0).r, 0);
4101 assert_eq!(lut.get(255).r, 255);
4102 }
4103
4104 #[test]
4105 fn gradient_lut_linear_duplicate_offsets_degrade_to_transparent_not_panic() {
4106 let lut = build_gradient_lut_linear(&lin_stops(&[(50.0, RED), (50.0, BLUE)]), None);
4110 assert_eq!(lut.size(), 256);
4111 assert_eq!(lut.get(128).a, 0);
4112 }
4113
4114 #[test]
4115 fn gradient_lut_linear_resolves_system_stops_against_the_palette() {
4116 let palette = SystemColors {
4117 accent: OptionColorU::Some(BLUE),
4118 ..SystemColors::default()
4119 };
4120 let stops: NormalizedLinearColorStopVec = vec![
4121 NormalizedLinearColorStop {
4122 offset: PercentageValue::new(0.0),
4123 color: ColorOrSystem::System(SystemColorRef::Accent),
4124 },
4125 NormalizedLinearColorStop {
4126 offset: PercentageValue::new(100.0),
4127 color: ColorOrSystem::Color(WHITE),
4128 },
4129 ]
4130 .into();
4131
4132 let with_palette = build_gradient_lut_linear(&stops, Some(&palette));
4133 assert_eq!(with_palette.get(0).b, 255, "system:accent must resolve to blue");
4134 assert_eq!(with_palette.get(0).a, 255);
4135
4136 let without = build_gradient_lut_linear(&stops, None);
4138 assert_eq!(without.get(0).a, 0);
4139 }
4140
4141 #[test]
4142 fn gradient_lut_radial_distinct_angles_interpolate() {
4143 let lut = build_gradient_lut_radial(&rad_stops(&[(0.0, BLACK), (180.0, WHITE)]), None);
4144 assert_eq!(lut.size(), 256);
4145 assert_eq!(lut.get(0).r, 0);
4146 assert_eq!(lut.get(255).r, 255);
4148 assert!(lut.get(64).r < lut.get(127).r);
4149 }
4150
4151 #[test]
4152 fn gradient_lut_radial_extreme_angles_do_not_panic() {
4153 for angles in [
4155 [-720.0_f32, 90.0],
4156 [1e30, 45.0],
4157 [f32::NAN, 90.0],
4158 [f32::INFINITY, 270.0],
4159 ] {
4160 let lut = build_gradient_lut_radial(
4161 &rad_stops(&[(angles[0], RED), (angles[1], BLUE)]),
4162 None,
4163 );
4164 assert_eq!(lut.size(), 256, "angles {angles:?} must still build a LUT");
4165 }
4166 }
4167
4168 #[test]
4173 fn resolve_background_position_keywords_map_to_fractions() {
4174 let cases = [
4175 (
4176 BackgroundPositionHorizontal::Left,
4177 BackgroundPositionVertical::Top,
4178 (0.0, 0.0),
4179 ),
4180 (
4181 BackgroundPositionHorizontal::Center,
4182 BackgroundPositionVertical::Center,
4183 (0.5, 0.5),
4184 ),
4185 (
4186 BackgroundPositionHorizontal::Right,
4187 BackgroundPositionVertical::Bottom,
4188 (1.0, 1.0),
4189 ),
4190 ];
4191 for (horizontal, vertical, expected) in cases {
4192 let pos = StyleBackgroundPosition {
4193 horizontal,
4194 vertical,
4195 };
4196 assert_eq!(resolve_background_position(&pos, 200.0, 100.0), expected);
4197 }
4198 }
4199
4200 #[test]
4201 fn resolve_background_position_exact_px_is_a_fraction_of_the_box() {
4202 let pos = StyleBackgroundPosition {
4203 horizontal: BackgroundPositionHorizontal::Exact(PixelValue::px(50.0)),
4204 vertical: BackgroundPositionVertical::Exact(PixelValue::px(25.0)),
4205 };
4206 assert_eq!(resolve_background_position(&pos, 200.0, 100.0), (0.25, 0.25));
4207 }
4208
4209 #[test]
4210 fn resolve_background_position_exact_percent_resolves_against_the_box() {
4211 let pos = StyleBackgroundPosition {
4212 horizontal: BackgroundPositionHorizontal::Exact(PixelValue::percent(50.0)),
4213 vertical: BackgroundPositionVertical::Exact(PixelValue::percent(10.0)),
4214 };
4215 let (x, y) = resolve_background_position(&pos, 200.0, 100.0);
4216 assert!((x - 0.5).abs() < 1e-4, "50% of the width is the center, got {x}");
4217 assert!((y - 0.1).abs() < 1e-4, "10% of the height, got {y}");
4218 }
4219
4220 #[test]
4221 fn resolve_background_position_zero_box_falls_back_to_center() {
4222 let pos = StyleBackgroundPosition {
4224 horizontal: BackgroundPositionHorizontal::Exact(PixelValue::px(10.0)),
4225 vertical: BackgroundPositionVertical::Exact(PixelValue::px(10.0)),
4226 };
4227 assert_eq!(resolve_background_position(&pos, 0.0, 0.0), (0.5, 0.5));
4228 }
4229
4230 #[test]
4231 fn resolve_background_position_never_returns_nan_for_degenerate_boxes() {
4232 let pos = StyleBackgroundPosition {
4233 horizontal: BackgroundPositionHorizontal::Exact(PixelValue::px(10.0)),
4234 vertical: BackgroundPositionVertical::Exact(PixelValue::px(-10.0)),
4235 };
4236 for w in DEGENERATE {
4237 for h in DEGENERATE {
4238 let (x, y) = resolve_background_position(&pos, w, h);
4239 assert!(
4240 !x.is_nan() && !y.is_nan(),
4241 "w={w}, h={h} produced NaN ({x}, {y}) — a NaN center poisons the gradient transform"
4242 );
4243 }
4244 }
4245 let (x, y) = resolve_background_position(&pos, f32::MAX, f32::MAX);
4247 assert!(x.is_finite() && y.is_finite());
4248 }
4249
4250 #[test]
4255 fn render_rect_paints_exactly_its_bounds() {
4256 let mut p = pixmap(10, 10);
4257 render_rect(
4258 &mut p,
4259 &lrect(2.0, 2.0, 4.0, 4.0),
4260 RED,
4261 &BorderRadius::default(),
4262 None,
4263 1.0,
4264 );
4265 assert!(is_reddish(px_at(&p, 3, 3)), "inside the rect must be red");
4266 assert_eq!(px_at(&p, 0, 0), [255, 255, 255, 255], "outside stays white");
4267 let red = p.data().chunks_exact(4).filter(|c| c[0] > 200 && c[1] < 60).count();
4268 assert_eq!(red, 16, "a 4x4 rect covers exactly 16 pixels");
4269 }
4270
4271 #[test]
4272 fn render_rect_transparent_color_is_a_noop() {
4273 let mut p = pixmap(8, 8);
4274 let before = snap(&p);
4275 render_rect(
4276 &mut p,
4277 &lrect(0.0, 0.0, 8.0, 8.0),
4278 CLEAR,
4279 &BorderRadius::default(),
4280 None,
4281 1.0,
4282 );
4283 assert_eq!(before, p.data(), "alpha=0 must not touch the buffer");
4284 }
4285
4286 #[test]
4287 fn render_rect_degenerate_bounds_are_noops() {
4288 for bad in DEGENERATE {
4289 let mut p = pixmap(8, 8);
4290 let before = snap(&p);
4291 render_rect(
4292 &mut p,
4293 &lrect(0.0, 0.0, bad, bad),
4294 RED,
4295 &BorderRadius::default(),
4296 None,
4297 1.0,
4298 );
4299 assert_eq!(before, p.data(), "size {bad} must be rejected, not painted");
4300
4301 if bad == f32::MIN {
4305 continue;
4306 }
4307 let mut p = pixmap(8, 8);
4308 let before = snap(&p);
4309 render_rect(
4310 &mut p,
4311 &lrect(bad, bad, 4.0, 4.0),
4312 RED,
4313 &BorderRadius::default(),
4314 None,
4315 1.0,
4316 );
4317 if !bad.is_finite() {
4318 assert_eq!(before, p.data(), "origin {bad} must be rejected");
4319 }
4320 }
4321 }
4322
4323 #[test]
4324 fn render_rect_degenerate_dpi_is_a_noop() {
4325 for dpi in DEGENERATE {
4328 let mut p = pixmap(8, 8);
4329 let before = snap(&p);
4330 render_rect(
4331 &mut p,
4332 &lrect(1.0, 1.0, 4.0, 4.0),
4333 RED,
4334 &BorderRadius::default(),
4335 None,
4336 dpi,
4337 );
4338 assert_eq!(before, p.data(), "dpi {dpi} must be rejected, not painted");
4339 }
4340 let mut p = pixmap(8, 8);
4342 let before = snap(&p);
4343 render_rect(
4344 &mut p,
4345 &lrect(1.0, 1.0, 4.0, 4.0),
4346 RED,
4347 &BorderRadius::default(),
4348 None,
4349 f32::MAX,
4350 );
4351 assert_eq!(before, p.data());
4352 }
4353
4354 #[test]
4355 fn render_rect_saturating_bounds_clamp_to_the_pixmap() {
4356 let mut p = pixmap(8, 8);
4359 render_rect(
4360 &mut p,
4361 &lrect(0.0, 0.0, f32::MAX, f32::MAX),
4362 RED,
4363 &BorderRadius::default(),
4364 None,
4365 1.0,
4366 );
4367 assert!(p.data().chunks_exact(4).all(|c| c[0] > 200 && c[1] < 60));
4368 }
4369
4370 #[test]
4371 fn render_rect_negative_origin_clamps_to_the_pixmap() {
4372 let mut p = pixmap(8, 8);
4373 render_rect(
4374 &mut p,
4375 &lrect(-1e9, -1e9, 2e9, 2e9),
4376 RED,
4377 &BorderRadius::default(),
4378 None,
4379 1.0,
4380 );
4381 assert!(is_reddish(px_at(&p, 0, 0)));
4382 assert!(is_reddish(px_at(&p, 7, 7)));
4383 }
4384
4385 #[test]
4386 fn render_rect_fully_outside_the_clip_is_a_noop() {
4387 let mut p = pixmap(10, 10);
4388 let before = snap(&p);
4389 let clip = AzRect::from_xywh(0.0, 0.0, 2.0, 2.0).unwrap();
4390 render_rect(
4391 &mut p,
4392 &lrect(5.0, 5.0, 3.0, 3.0),
4393 RED,
4394 &BorderRadius::default(),
4395 Some(clip),
4396 1.0,
4397 );
4398 assert_eq!(before, p.data());
4399 }
4400
4401 #[test]
4402 fn render_rect_clip_narrows_the_painted_area() {
4403 let mut p = pixmap(10, 10);
4404 let clip = AzRect::from_xywh(0.0, 0.0, 2.0, 2.0).unwrap();
4405 render_rect(
4406 &mut p,
4407 &lrect(0.0, 0.0, 10.0, 10.0),
4408 RED,
4409 &BorderRadius::default(),
4410 Some(clip),
4411 1.0,
4412 );
4413 let red = p.data().chunks_exact(4).filter(|c| c[0] > 200 && c[1] < 60).count();
4414 assert_eq!(red, 4, "only the 2x2 clip region may be painted");
4415 }
4416
4417 #[test]
4418 fn render_rect_rounded_corners_leave_the_corner_pixel_unpainted() {
4419 let mut p = pixmap(20, 20);
4420 let radius = BorderRadius {
4421 top_left: 6.0,
4422 top_right: 6.0,
4423 bottom_left: 6.0,
4424 bottom_right: 6.0,
4425 };
4426 render_rect(&mut p, &lrect(0.0, 0.0, 20.0, 20.0), RED, &radius, None, 1.0);
4427 assert!(is_reddish(px_at(&p, 10, 10)), "the middle is filled");
4428 assert_eq!(
4429 px_at(&p, 0, 0),
4430 [255, 255, 255, 255],
4431 "the rounded corner must not be filled"
4432 );
4433 }
4434
4435 #[test]
4436 fn render_rect_radius_larger_than_the_rect_does_not_panic() {
4437 let mut p = pixmap(10, 10);
4438 let radius = BorderRadius {
4439 top_left: 1e6,
4440 top_right: 1e6,
4441 bottom_left: 1e6,
4442 bottom_right: 1e6,
4443 };
4444 render_rect(&mut p, &lrect(0.0, 0.0, 10.0, 10.0), RED, &radius, None, 1.0);
4445 assert!(is_reddish(px_at(&p, 5, 5)));
4447 }
4448
4449 fn linear(stops: NormalizedLinearColorStopVec) -> LinearGradient {
4454 LinearGradient {
4455 stops,
4456 ..LinearGradient::default()
4457 }
4458 }
4459
4460 #[test]
4461 fn linear_gradient_paints_a_ramp_top_to_bottom() {
4462 let mut p = pixmap(16, 16);
4463 render_linear_gradient(
4464 &mut p,
4465 &lrect(0.0, 0.0, 16.0, 16.0),
4466 &linear(lin_stops(&[(0.0, BLACK), (100.0, WHITE)])),
4467 &BorderRadius::default(),
4468 None,
4469 1.0,
4470 None,
4471 );
4472 let top = px_at(&p, 8, 0)[0];
4473 let bottom = px_at(&p, 8, 15)[0];
4474 assert!(
4475 top < bottom,
4476 "the default Top->Bottom direction must ramp dark->light (top {top}, bottom {bottom})"
4477 );
4478 }
4479
4480 #[test]
4481 fn linear_gradient_without_stops_is_a_noop() {
4482 let mut p = pixmap(8, 8);
4483 let before = snap(&p);
4484 render_linear_gradient(
4485 &mut p,
4486 &lrect(0.0, 0.0, 8.0, 8.0),
4487 &linear(lin_stops(&[])),
4488 &BorderRadius::default(),
4489 None,
4490 1.0,
4491 None,
4492 );
4493 assert_eq!(before, p.data());
4494 }
4495
4496 #[test]
4497 fn linear_gradient_single_stop_paints_nothing() {
4498 let mut p = pixmap(8, 8);
4500 let before = snap(&p);
4501 render_linear_gradient(
4502 &mut p,
4503 &lrect(0.0, 0.0, 8.0, 8.0),
4504 &linear(lin_stops(&[(50.0, RED)])),
4505 &BorderRadius::default(),
4506 None,
4507 1.0,
4508 None,
4509 );
4510 assert_eq!(before, p.data());
4511 }
4512
4513 #[test]
4514 fn linear_gradient_degenerate_geometry_is_a_noop() {
4515 for bad in DEGENERATE {
4516 let mut p = pixmap(8, 8);
4517 let before = snap(&p);
4518 render_linear_gradient(
4519 &mut p,
4520 &lrect(0.0, 0.0, 8.0, 8.0),
4521 &linear(lin_stops(&[(0.0, BLACK), (100.0, WHITE)])),
4522 &BorderRadius::default(),
4523 None,
4524 bad,
4525 None,
4526 );
4527 assert_eq!(before, p.data(), "dpi {bad} must be rejected");
4528
4529 let mut p = pixmap(8, 8);
4530 let before = snap(&p);
4531 render_linear_gradient(
4532 &mut p,
4533 &lrect(0.0, 0.0, bad, bad),
4534 &linear(lin_stops(&[(0.0, BLACK), (100.0, WHITE)])),
4535 &BorderRadius::default(),
4536 None,
4537 1.0,
4538 None,
4539 );
4540 assert_eq!(before, p.data(), "size {bad} must be rejected");
4541 }
4542 }
4543
4544 #[test]
4545 fn radial_gradient_zero_radius_is_a_noop() {
4546 let gradient = RadialGradient {
4548 shape: Shape::Circle,
4549 size: RadialGradientSize::ClosestSide,
4550 position: StyleBackgroundPosition {
4551 horizontal: BackgroundPositionHorizontal::Left,
4552 vertical: BackgroundPositionVertical::Top,
4553 },
4554 stops: lin_stops(&[(0.0, BLACK), (100.0, WHITE)]),
4555 ..RadialGradient::default()
4556 };
4557 let mut p = pixmap(8, 8);
4558 let before = snap(&p);
4559 render_radial_gradient(
4560 &mut p,
4561 &lrect(0.0, 0.0, 8.0, 8.0),
4562 &gradient,
4563 &BorderRadius::default(),
4564 None,
4565 1.0,
4566 None,
4567 );
4568 assert_eq!(before, p.data(), "a 0-radius gradient must paint nothing");
4569 }
4570
4571 #[test]
4572 fn radial_gradient_paints_from_the_center_outward() {
4573 let gradient = RadialGradient {
4574 shape: Shape::Circle,
4575 size: RadialGradientSize::FarthestCorner,
4576 position: StyleBackgroundPosition {
4577 horizontal: BackgroundPositionHorizontal::Center,
4578 vertical: BackgroundPositionVertical::Center,
4579 },
4580 stops: lin_stops(&[(0.0, BLACK), (100.0, WHITE)]),
4581 ..RadialGradient::default()
4582 };
4583 let mut p = pixmap(16, 16);
4584 render_radial_gradient(
4585 &mut p,
4586 &lrect(0.0, 0.0, 16.0, 16.0),
4587 &gradient,
4588 &BorderRadius::default(),
4589 None,
4590 1.0,
4591 None,
4592 );
4593 let center = px_at(&p, 8, 8)[0];
4594 let corner = px_at(&p, 0, 0)[0];
4595 assert!(
4596 center < corner,
4597 "the center stop is black, the rim white (center {center}, corner {corner})"
4598 );
4599 }
4600
4601 #[test]
4602 fn radial_gradient_empty_stops_and_degenerate_dpi_are_noops() {
4603 let empty = RadialGradient {
4604 stops: lin_stops(&[]),
4605 ..RadialGradient::default()
4606 };
4607 let mut p = pixmap(8, 8);
4608 let before = snap(&p);
4609 render_radial_gradient(
4610 &mut p,
4611 &lrect(0.0, 0.0, 8.0, 8.0),
4612 &empty,
4613 &BorderRadius::default(),
4614 None,
4615 1.0,
4616 None,
4617 );
4618 assert_eq!(before, p.data());
4619
4620 let filled = RadialGradient {
4621 stops: lin_stops(&[(0.0, BLACK), (100.0, WHITE)]),
4622 ..RadialGradient::default()
4623 };
4624 for bad in DEGENERATE {
4625 let mut p = pixmap(8, 8);
4626 let before = snap(&p);
4627 render_radial_gradient(
4628 &mut p,
4629 &lrect(0.0, 0.0, 8.0, 8.0),
4630 &filled,
4631 &BorderRadius::default(),
4632 None,
4633 bad,
4634 None,
4635 );
4636 assert_eq!(before, p.data(), "dpi {bad} must be rejected");
4637 }
4638 }
4639
4640 #[test]
4641 fn conic_gradient_empty_stops_and_degenerate_dpi_are_noops() {
4642 let empty = ConicGradient {
4643 stops: rad_stops(&[]),
4644 ..ConicGradient::default()
4645 };
4646 let mut p = pixmap(8, 8);
4647 let before = snap(&p);
4648 render_conic_gradient(
4649 &mut p,
4650 &lrect(0.0, 0.0, 8.0, 8.0),
4651 &empty,
4652 &BorderRadius::default(),
4653 None,
4654 1.0,
4655 None,
4656 );
4657 assert_eq!(before, p.data());
4658
4659 let filled = ConicGradient {
4660 stops: rad_stops(&[(0.0, BLACK), (180.0, WHITE)]),
4661 ..ConicGradient::default()
4662 };
4663 for bad in DEGENERATE {
4664 let mut p = pixmap(8, 8);
4665 let before = snap(&p);
4666 render_conic_gradient(
4667 &mut p,
4668 &lrect(0.0, 0.0, 8.0, 8.0),
4669 &filled,
4670 &BorderRadius::default(),
4671 None,
4672 bad,
4673 None,
4674 );
4675 assert_eq!(before, p.data(), "dpi {bad} must be rejected");
4676 }
4677 }
4678
4679 #[test]
4680 fn conic_gradient_with_distinct_angle_stops_paints() {
4681 let gradient = ConicGradient {
4682 stops: rad_stops(&[(0.0, BLACK), (180.0, WHITE)]),
4683 ..ConicGradient::default()
4684 };
4685 let mut p = pixmap(16, 16);
4686 let before = snap(&p);
4687 render_conic_gradient(
4688 &mut p,
4689 &lrect(0.0, 0.0, 16.0, 16.0),
4690 &gradient,
4691 &BorderRadius::default(),
4692 None,
4693 1.0,
4694 None,
4695 );
4696 assert_ne!(before, p.data(), "a 2-stop conic gradient must paint");
4697 }
4698
4699 #[test]
4707 fn conic_gradient_full_circle_stops_paint_the_rect() {
4708 let gradient = ConicGradient {
4709 stops: rad_stops(&[(0.0, BLACK), (360.0, WHITE)]),
4710 ..ConicGradient::default()
4711 };
4712 let mut p = pixmap(16, 16);
4713 let before = snap(&p);
4714 render_conic_gradient(
4715 &mut p,
4716 &lrect(0.0, 0.0, 16.0, 16.0),
4717 &gradient,
4718 &BorderRadius::default(),
4719 None,
4720 1.0,
4721 None,
4722 );
4723 assert_ne!(
4724 before,
4725 p.data(),
4726 "conic-gradient(black, white) normalizes to 0deg/360deg and must still paint"
4727 );
4728 }
4729
4730 #[test]
4735 fn box_shadow_paints_under_the_bounds() {
4736 let mut p = pixmap(40, 40);
4737 let res = render_box_shadow(
4738 &mut p,
4739 &lrect(10.0, 10.0, 20.0, 20.0),
4740 &shadow(0.0, 0.0, 0.0, BLACK),
4741 &BorderRadius::default(),
4742 1.0,
4743 );
4744 assert!(res.is_ok());
4745 let dark = p.data().chunks_exact(4).filter(|c| c[0] < 50).count();
4746 assert!(dark > 100, "a hard 20x20 shadow must darken the box, got {dark}");
4747 }
4748
4749 #[test]
4750 fn box_shadow_transparent_color_is_ok_and_a_noop() {
4751 let mut p = pixmap(20, 20);
4752 let before = snap(&p);
4753 let res = render_box_shadow(
4754 &mut p,
4755 &lrect(5.0, 5.0, 10.0, 10.0),
4756 &shadow(0.0, 4.0, 0.0, CLEAR),
4757 &BorderRadius::default(),
4758 1.0,
4759 );
4760 assert_eq!(res, Ok(()));
4761 assert_eq!(before, p.data());
4762 }
4763
4764 #[test]
4765 fn box_shadow_oversized_blur_is_rejected_without_allocating() {
4766 let mut p = pixmap(20, 20);
4769 let before = snap(&p);
4770 let res = render_box_shadow(
4771 &mut p,
4772 &lrect(5.0, 5.0, 10.0, 10.0),
4773 &shadow(0.0, 1e6, 0.0, BLACK),
4774 &BorderRadius::default(),
4775 1.0,
4776 );
4777 assert_eq!(res, Ok(()));
4778 assert_eq!(before, p.data(), "an oversized shadow must be skipped");
4779 }
4780
4781 #[test]
4782 fn box_shadow_huge_negative_spread_collapses_to_a_noop() {
4783 let mut p = pixmap(20, 20);
4784 let before = snap(&p);
4785 let res = render_box_shadow(
4786 &mut p,
4787 &lrect(5.0, 5.0, 10.0, 10.0),
4788 &shadow(0.0, 0.0, -1e6, BLACK),
4789 &BorderRadius::default(),
4790 1.0,
4791 );
4792 assert_eq!(res, Ok(()));
4793 assert_eq!(before, p.data(), "a fully-shrunk shadow paints nothing");
4794 }
4795
4796 #[test]
4797 fn box_shadow_degenerate_geometry_is_ok_and_a_noop() {
4798 for bad in DEGENERATE {
4799 let mut p = pixmap(20, 20);
4800 let before = snap(&p);
4801 let res = render_box_shadow(
4802 &mut p,
4803 &lrect(5.0, 5.0, 10.0, 10.0),
4804 &shadow(0.0, 2.0, 0.0, BLACK),
4805 &BorderRadius::default(),
4806 bad,
4807 );
4808 assert_eq!(res, Ok(()), "dpi {bad} must not error");
4809 assert_eq!(before, p.data(), "dpi {bad} must not paint");
4810
4811 let mut p = pixmap(20, 20);
4812 let before = snap(&p);
4813 let res = render_box_shadow(
4814 &mut p,
4815 &lrect(0.0, 0.0, bad, bad),
4816 &shadow(0.0, 2.0, 0.0, BLACK),
4817 &BorderRadius::default(),
4818 1.0,
4819 );
4820 assert_eq!(res, Ok(()), "size {bad} must not error");
4821 assert_eq!(before, p.data(), "size {bad} must not paint");
4822 }
4823 }
4824
4825 #[test]
4830 fn extract_mask_data_zero_target_is_none() {
4831 let img = r8_image(2, 2, vec![0, 64, 128, 255]);
4832 assert!(extract_mask_data(&img, 0, 4).is_none());
4833 assert!(extract_mask_data(&img, 4, 0).is_none());
4834 assert!(extract_mask_data(&img, 0, 0).is_none());
4835 }
4836
4837 #[test]
4838 fn extract_mask_data_r8_identity_scale_is_a_passthrough() {
4839 let img = r8_image(2, 2, vec![0, 64, 128, 255]);
4840 let mask = extract_mask_data(&img, 2, 2).expect("R8 mask must extract");
4841 assert_eq!(mask, vec![0, 64, 128, 255]);
4842 }
4843
4844 #[test]
4845 fn extract_mask_data_upscales_nearest_neighbour() {
4846 let img = r8_image(2, 2, vec![0, 255, 255, 0]);
4847 let mask = extract_mask_data(&img, 4, 4).expect("mask must extract");
4848 assert_eq!(mask.len(), 16);
4849 assert_eq!(
4851 mask,
4852 vec![
4853 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0,
4857 ]
4858 );
4859 }
4860
4861 #[test]
4862 fn extract_mask_data_downscales_without_reading_out_of_bounds() {
4863 let img = r8_image(4, 4, (0..16).map(|i| i as u8 * 16).collect());
4864 let mask = extract_mask_data(&img, 1, 1).expect("mask must extract");
4865 assert_eq!(mask, vec![0], "1x1 nearest-neighbour samples the first texel");
4866
4867 let mask = extract_mask_data(&img, 8, 2).expect("mask must extract");
4869 assert_eq!(mask.len(), 16);
4870 }
4871
4872 #[test]
4873 fn extract_mask_data_bgra_source_uses_the_alpha_channel() {
4874 let px = vec![
4876 255, 0, 0, 0, 0, 255, 0, 85, 0, 0, 255, 170, 9, 9, 9, 255, ];
4881 let img = rgba_image(2, 2, px);
4882 let mask = extract_mask_data(&img, 2, 2).expect("BGRA mask must extract");
4883 assert_eq!(mask, vec![0, 85, 170, 255]);
4884 }
4885
4886 #[test]
4887 fn extract_mask_data_target_length_always_matches_the_request() {
4888 let img = r8_image(3, 3, vec![7; 9]);
4889 for (w, h) in [(1u32, 1u32), (2, 5), (5, 2), (16, 16), (1, 64)] {
4890 let mask = extract_mask_data(&img, w, h).expect("mask must extract");
4891 assert_eq!(mask.len(), (w * h) as usize, "target {w}x{h}");
4892 assert!(mask.iter().all(|&v| v == 7));
4893 }
4894 }
4895
4896 fn image_mask_entry(
4901 snapshot: Vec<u8>,
4902 mask_data: Vec<u8>,
4903 origin: (i32, i32),
4904 size: (u32, u32),
4905 ) -> MaskEntry {
4906 MaskEntry::ImageMask {
4907 snapshot,
4908 mask_data,
4909 origin_x: origin.0,
4910 origin_y: origin.1,
4911 width: size.0,
4912 height: size.1,
4913 }
4914 }
4915
4916 #[test]
4917 fn apply_mask_zero_mask_restores_the_snapshot() {
4918 let mut p = pixmap(4, 4);
4919 let snapshot = snapshot_region(&p, 0, 0, 4, 4); p.fill(0, 0, 0, 255); apply_mask(
4922 &mut p,
4923 &image_mask_entry(snapshot, vec![0; 16], (0, 0), (4, 4)),
4924 );
4925 assert!(
4926 p.data().chunks_exact(4).all(|c| c[0] == 255 && c[1] == 255),
4927 "mask=0 means fully clipped -> the pre-mask snapshot is restored"
4928 );
4929 }
4930
4931 #[test]
4932 fn apply_mask_opaque_mask_keeps_the_current_pixels() {
4933 let mut p = pixmap(4, 4);
4934 let snapshot = snapshot_region(&p, 0, 0, 4, 4);
4935 p.fill(0, 0, 0, 255);
4936 apply_mask(
4937 &mut p,
4938 &image_mask_entry(snapshot, vec![255; 16], (0, 0), (4, 4)),
4939 );
4940 assert!(
4941 p.data().chunks_exact(4).all(|c| c[0] == 0),
4942 "mask=255 means fully visible -> the drawing survives"
4943 );
4944 }
4945
4946 #[test]
4947 fn apply_mask_opacity_entry_is_ignored() {
4948 let mut p = pixmap(4, 4);
4949 p.fill(0, 0, 0, 255);
4950 let before = snap(&p);
4951 apply_mask(
4952 &mut p,
4953 &MaskEntry::Opacity {
4954 snapshot: vec![255; 64],
4955 rect: AzRect::from_xywh(0.0, 0.0, 4.0, 4.0).unwrap(),
4956 opacity: 0.5,
4957 },
4958 );
4959 assert_eq!(before, p.data(), "apply_mask only handles ImageMask entries");
4960 }
4961
4962 #[test]
4963 fn apply_mask_out_of_bounds_origin_does_not_panic_or_write() {
4964 let mut p = pixmap(4, 4);
4965 p.fill(0, 0, 0, 255);
4966 let before = snap(&p);
4967 for origin in [(-100, -100), (100, 100), (i32::MIN, 0), (0, i32::MIN)] {
4971 apply_mask(
4972 &mut p,
4973 &image_mask_entry(vec![255; 64], vec![0; 16], origin, (4, 4)),
4974 );
4975 }
4976 assert_eq!(before, p.data(), "off-buffer masks must be skipped entirely");
4977 }
4978
4979 #[test]
4980 fn apply_mask_truncated_mask_data_is_treated_as_zero() {
4981 let mut p = pixmap(4, 4);
4982 let snapshot = snapshot_region(&p, 0, 0, 4, 4);
4983 p.fill(0, 0, 0, 255);
4984 apply_mask(
4987 &mut p,
4988 &image_mask_entry(snapshot, vec![255; 4], (0, 0), (4, 4)),
4989 );
4990 assert_eq!(px_at(&p, 0, 0), [0, 0, 0, 255], "the covered texels stay");
4991 assert_eq!(
4992 px_at(&p, 0, 3),
4993 [255, 255, 255, 255],
4994 "missing mask bytes restore the snapshot"
4995 );
4996 }
4997
4998 #[test]
4999 fn apply_mask_partially_offscreen_only_touches_visible_pixels() {
5000 let mut p = pixmap(4, 4);
5001 let snapshot = snapshot_region(&p, -2, -2, 4, 4);
5002 p.fill(0, 0, 0, 255);
5003 apply_mask(
5004 &mut p,
5005 &image_mask_entry(snapshot, vec![0; 16], (-2, -2), (4, 4)),
5006 );
5007 assert_eq!(px_at(&p, 3, 3), [0, 0, 0, 255]);
5009 }
5010
5011 #[test]
5016 fn acquire_pixmap_zero_dimensions_error_instead_of_allocating() {
5017 assert!(acquire_pixmap(None, 0, 0).is_err());
5018 assert!(acquire_pixmap(None, 0, 4).is_err());
5019 assert!(acquire_pixmap(None, 4, 0).is_err());
5020 assert!(acquire_pixmap(Some(pixmap(4, 4)), 0, 4).is_err());
5023 }
5024
5025 #[test]
5026 fn acquire_pixmap_reuses_a_matching_retained_buffer_verbatim() {
5027 let mut retained = pixmap(4, 4);
5028 retained.fill(1, 2, 3, 4);
5029 let got = acquire_pixmap(Some(retained), 4, 4).expect("must reuse");
5030 assert_eq!(got.width, 4);
5031 assert_eq!(got.height, 4);
5032 assert_eq!(
5033 &got.data()[0..4],
5034 &[1, 2, 3, 4],
5035 "reuse must not clear — the caller does that"
5036 );
5037 }
5038
5039 #[test]
5040 fn acquire_pixmap_allocates_fresh_on_a_size_mismatch() {
5041 let mut retained = pixmap(4, 4);
5042 retained.fill(1, 2, 3, 4);
5043 let got = acquire_pixmap(Some(retained), 5, 5).expect("must allocate");
5044 assert_eq!((got.width, got.height), (5, 5));
5045 assert_eq!(&got.data()[0..4], &[255, 255, 255, 255], "fresh = opaque white");
5046 }
5047
5048 fn opts(width: f32, height: f32, dpi_factor: f32) -> RenderOptions {
5053 RenderOptions {
5054 width,
5055 height,
5056 dpi_factor,
5057 }
5058 }
5059
5060 #[test]
5061 fn render_empty_display_list_is_opaque_white() {
5062 let dl = DisplayList::default();
5063 let res = RendererResources::default();
5064 let mut gc = GlyphCache::new();
5065 let p = render(&dl, &res, opts(4.0, 4.0, 1.0), &mut gc).expect("must render");
5066 assert_eq!((p.width, p.height), (4, 4));
5067 assert!(p
5068 .data()
5069 .chunks_exact(4)
5070 .all(|c| c[0] == 255 && c[1] == 255 && c[2] == 255 && c[3] == 255));
5071 }
5072
5073 #[test]
5074 fn render_applies_the_dpi_factor_to_the_pixmap_size() {
5075 let dl = DisplayList::default();
5076 let res = RendererResources::default();
5077 let mut gc = GlyphCache::new();
5078 let p = render(&dl, &res, opts(4.0, 3.0, 2.0), &mut gc).expect("must render");
5079 assert_eq!((p.width, p.height), (8, 6));
5080 }
5081
5082 #[test]
5083 fn render_collapsing_dimensions_error_instead_of_panicking() {
5084 let dl = DisplayList::default();
5085 let res = RendererResources::default();
5086 let mut gc = GlyphCache::new();
5087 for o in [
5089 opts(0.0, 4.0, 1.0),
5090 opts(4.0, 0.0, 1.0),
5091 opts(-4.0, -4.0, 1.0),
5092 opts(f32::NAN, f32::NAN, 1.0),
5093 opts(4.0, 4.0, 0.0),
5094 opts(4.0, 4.0, -1.0),
5095 opts(4.0, 4.0, f32::NAN),
5096 opts(0.4, 0.4, 1.0), ] {
5098 let got = render(&dl, &res, o, &mut gc);
5099 assert!(
5100 got.is_err(),
5101 "{o:?} must return Err, not panic or allocate a 0-sized buffer"
5102 );
5103 }
5104 }
5105
5106 #[test]
5107 fn render_paints_display_list_items() {
5108 let dl = DisplayList {
5109 items: vec![DisplayListItem::Rect {
5110 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5111 color: RED,
5112 border_radius: BorderRadius::default(),
5113 }],
5114 ..Default::default()
5115 };
5116 let res = RendererResources::default();
5117 let mut gc = GlyphCache::new();
5118 let p = render(&dl, &res, opts(8.0, 8.0, 1.0), &mut gc).expect("must render");
5119 assert!(is_reddish(px_at(&p, 1, 1)));
5120 assert_eq!(px_at(&p, 7, 7), [255, 255, 255, 255]);
5121 }
5122
5123 #[test]
5128 fn cpu_render_state_new_keeps_the_scroll_offsets_and_empties_the_rest() {
5129 let mut offsets = ScrollOffsetMap::new();
5130 offsets.insert(7, (1.0, 2.0));
5131 let state = CpuRenderState::new(offsets);
5132 assert_eq!(state.scroll_offsets.get(&7), Some(&(1.0, 2.0)));
5133 assert!(state.transforms.is_empty());
5134 assert!(state.opacities.is_empty());
5135 assert!(state.system_style.is_none());
5136 assert!(state.virtual_view_display_lists.is_empty());
5137 assert!(state.image_callback_results.is_empty());
5138 }
5139
5140 #[test]
5141 fn cpu_render_state_builders_set_their_field_and_preserve_the_others() {
5142 let mut offsets = ScrollOffsetMap::new();
5143 offsets.insert(1, (3.0, 4.0));
5144
5145 let mut lists = std::collections::BTreeMap::new();
5146 lists.insert(DomId { inner: 9 }, std::sync::Arc::new(DisplayList::default()));
5147
5148 let img = r8_image(1, 1, vec![255]);
5149 let hash = img.get_hash();
5150 let mut results = std::collections::BTreeMap::new();
5151 results.insert(hash, img);
5152
5153 let state = CpuRenderState::new(offsets)
5154 .with_virtual_view_display_lists(lists)
5155 .with_image_callback_results(results)
5156 .with_system_style(Some(std::sync::Arc::new(
5157 azul_css::system::SystemStyle::default(),
5158 )));
5159
5160 assert_eq!(state.scroll_offsets.get(&1), Some(&(3.0, 4.0)));
5161 assert_eq!(state.virtual_view_display_lists.len(), 1);
5162 assert!(state.virtual_view_display_lists.contains_key(&DomId { inner: 9 }));
5163 assert_eq!(state.image_callback_results.len(), 1);
5164 assert!(state.image_callback_results.contains_key(&hash));
5165 assert!(state.system_style.is_some());
5166
5167 let cleared = CpuRenderState::new(ScrollOffsetMap::new()).with_system_style(None);
5169 assert!(cleared.system_style.is_none());
5170 }
5171
5172 #[test]
5173 fn cpu_render_state_builders_accept_empty_collections() {
5174 let state = CpuRenderState::new(ScrollOffsetMap::new())
5175 .with_virtual_view_display_lists(std::collections::BTreeMap::new())
5176 .with_image_callback_results(std::collections::BTreeMap::new());
5177 assert!(state.virtual_view_display_lists.is_empty());
5178 assert!(state.image_callback_results.is_empty());
5179 }
5180
5181 #[test]
5182 fn extract_gpu_values_without_a_cache_is_empty() {
5183 let (transforms, opacities) = extract_gpu_values(None, DomId::ROOT_ID);
5184 assert!(transforms.is_empty());
5185 assert!(opacities.is_empty());
5186 }
5187
5188 #[test]
5189 fn extract_gpu_values_flattens_keys_to_ids() {
5190 let mut cache = GpuValueCache::default();
5191 let node = NodeId::new(3);
5192 let tkey = TransformKey { id: 11 };
5193 let okey = OpacityKey { id: 22 };
5194
5195 cache.transform_keys.insert(node, tkey);
5196 cache
5197 .current_transform_values
5198 .insert(node, ComputedTransform3D::IDENTITY);
5199 cache.opacity_keys.insert(node, okey);
5200 cache.current_opacity_values.insert(node, 0.25);
5201
5202 let (transforms, opacities) = extract_gpu_values(Some(&cache), DomId::ROOT_ID);
5203 assert_eq!(transforms.len(), 1);
5204 assert_eq!(transforms.get(&11).map(|t| t.m), Some(ComputedTransform3D::IDENTITY.m));
5205 assert_eq!(opacities.get(&22), Some(&0.25));
5206 }
5207
5208 #[test]
5209 fn extract_gpu_values_drops_keys_without_a_value() {
5210 let mut cache = GpuValueCache::default();
5212 cache.transform_keys.insert(NodeId::new(0), TransformKey { id: 5 });
5213 cache.opacity_keys.insert(NodeId::new(0), OpacityKey { id: 6 });
5214 let (transforms, opacities) = extract_gpu_values(Some(&cache), DomId::ROOT_ID);
5215 assert!(transforms.is_empty());
5216 assert!(opacities.is_empty());
5217 }
5218
5219 #[test]
5220 fn extract_gpu_values_filters_scrollbar_opacity_by_dom_id() {
5221 let mut cache = GpuValueCache::default();
5222 let other_dom = DomId { inner: 42 };
5223 let node = NodeId::new(1);
5224 cache
5225 .scrollbar_v_opacity_keys
5226 .insert((other_dom, node), OpacityKey { id: 77 });
5227 cache
5228 .scrollbar_v_opacity_values
5229 .insert((other_dom, node), 1.0);
5230
5231 let (_, opacities) = extract_gpu_values(Some(&cache), DomId::ROOT_ID);
5233 assert!(opacities.is_empty());
5234
5235 let (_, opacities) = extract_gpu_values(Some(&cache), other_dom);
5237 assert_eq!(opacities.get(&77), Some(&1.0));
5238 }
5239
5240 #[test]
5241 fn cpu_render_state_from_gpu_cache_matches_extract_gpu_values() {
5242 let mut cache = GpuValueCache::default();
5243 cache.css_transform_keys.insert(NodeId::new(2), TransformKey { id: 8 });
5244 cache
5245 .css_current_transform_values
5246 .insert(NodeId::new(2), ComputedTransform3D::IDENTITY);
5247
5248 let mut offsets = ScrollOffsetMap::new();
5249 offsets.insert(5, (10.0, 20.0));
5250
5251 let state = CpuRenderState::from_gpu_cache(Some(&cache), DomId::ROOT_ID, &offsets);
5252 let (transforms, opacities) = extract_gpu_values(Some(&cache), DomId::ROOT_ID);
5253 assert_eq!(state.transforms.len(), transforms.len());
5254 assert!(state.transforms.contains_key(&8));
5255 assert_eq!(state.opacities.len(), opacities.len());
5256 assert_eq!(state.scroll_offsets.get(&5), Some(&(10.0, 20.0)));
5257 assert!(state.system_style.is_none());
5258
5259 let empty = CpuRenderState::from_gpu_cache(None, DomId::ROOT_ID, &ScrollOffsetMap::new());
5260 assert!(empty.transforms.is_empty() && empty.opacities.is_empty());
5261 }
5262
5263 #[test]
5268 fn probe_label_for_item_returns_a_distinct_static_label() {
5269 let cases = [
5270 (
5271 DisplayListItem::Rect {
5272 bounds: wrect(0.0, 0.0, 1.0, 1.0),
5273 color: RED,
5274 border_radius: BorderRadius::default(),
5275 },
5276 "dl:rect",
5277 ),
5278 (DisplayListItem::PopClip, "dl:pop_clip"),
5279 (DisplayListItem::PopScrollFrame, "dl:pop_scroll"),
5280 (DisplayListItem::PopOpacity, "dl:pop_opacity"),
5281 (DisplayListItem::PopTextShadow, "dl:pop_tshadow"),
5282 (DisplayListItem::PopImageMaskClip, "dl:pop_imask"),
5283 (
5284 DisplayListItem::BoxShadow {
5285 bounds: wrect(0.0, 0.0, 1.0, 1.0),
5286 shadow: shadow(0.0, 0.0, 0.0, BLACK),
5287 border_radius: BorderRadius::default(),
5288 },
5289 "dl:box_shadow",
5290 ),
5291 ];
5292 for (item, expected) in cases {
5293 assert_eq!(probe_label_for_item(&item), expected);
5294 }
5295 }
5296
5297 #[test]
5302 fn compute_content_bounds_of_an_empty_list_is_none() {
5303 assert!(compute_content_bounds(&DisplayList::default()).is_none());
5304 }
5305
5306 #[test]
5307 fn compute_content_bounds_ignores_state_management_items() {
5308 let dl = DisplayList {
5309 items: vec![
5310 DisplayListItem::PopClip,
5311 DisplayListItem::PopScrollFrame,
5312 DisplayListItem::PopOpacity,
5313 ],
5314 ..Default::default()
5315 };
5316 assert!(
5317 compute_content_bounds(&dl).is_none(),
5318 "push/pop markers carry no content"
5319 );
5320 }
5321
5322 #[test]
5323 fn compute_content_bounds_unions_every_drawing_item() {
5324 let dl = DisplayList {
5325 items: vec![
5326 DisplayListItem::Rect {
5327 bounds: wrect(10.0, 20.0, 30.0, 40.0),
5328 color: RED,
5329 border_radius: BorderRadius::default(),
5330 },
5331 DisplayListItem::Rect {
5332 bounds: wrect(-5.0, 0.0, 5.0, 5.0),
5333 color: BLUE,
5334 border_radius: BorderRadius::default(),
5335 },
5336 DisplayListItem::PopClip, ],
5338 ..Default::default()
5339 };
5340 let (min_x, min_y, max_x, max_y) = compute_content_bounds(&dl).expect("has items");
5341 assert_eq!((min_x, min_y), (-5.0, 0.0));
5342 assert_eq!((max_x, max_y), (40.0, 60.0));
5343 }
5344
5345 #[test]
5346 fn compute_content_bounds_with_nan_bounds_does_not_produce_nan() {
5347 let dl = DisplayList {
5350 items: vec![
5351 DisplayListItem::Rect {
5352 bounds: wrect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
5353 color: RED,
5354 border_radius: BorderRadius::default(),
5355 },
5356 DisplayListItem::Rect {
5357 bounds: wrect(0.0, 0.0, 10.0, 10.0),
5358 color: BLUE,
5359 border_radius: BorderRadius::default(),
5360 },
5361 ],
5362 ..Default::default()
5363 };
5364 let (min_x, min_y, max_x, max_y) = compute_content_bounds(&dl).expect("has items");
5365 for v in [min_x, min_y, max_x, max_y] {
5366 assert!(!v.is_nan(), "NaN item bounds must not poison the content box");
5367 }
5368 assert_eq!((max_x, max_y), (10.0, 10.0));
5369 }
5370
5371 #[test]
5376 fn build_rect_path_is_a_closed_quad() {
5377 let rect = AzRect::from_xywh(1.0, 2.0, 3.0, 4.0).unwrap();
5378 let path = build_rect_path(&rect);
5379 assert_eq!(path.total_vertices(), 5);
5381 let (mut x, mut y) = (0.0, 0.0);
5382 path.vertex_idx(0, &mut x, &mut y);
5383 assert_eq!((x, y), (1.0, 2.0));
5384 path.vertex_idx(2, &mut x, &mut y);
5385 assert_eq!((x, y), (4.0, 6.0), "the opposite corner is origin + size");
5386 }
5387
5388 #[test]
5389 fn build_rounded_rect_path_falls_back_to_a_quad_for_non_positive_radii() {
5390 let rect = AzRect::from_xywh(0.0, 0.0, 10.0, 10.0).unwrap();
5391 let plain = build_rect_path(&rect).total_vertices();
5392
5393 assert_eq!(
5395 build_rounded_rect_path(&rect, &BorderRadius::default(), 1.0).total_vertices(),
5396 plain
5397 );
5398 let negative = BorderRadius {
5400 top_left: -5.0,
5401 top_right: -5.0,
5402 bottom_left: -5.0,
5403 bottom_right: -5.0,
5404 };
5405 assert_eq!(
5406 build_rounded_rect_path(&rect, &negative, 1.0).total_vertices(),
5407 plain
5408 );
5409 let positive = BorderRadius {
5411 top_left: 4.0,
5412 top_right: 4.0,
5413 bottom_left: 4.0,
5414 bottom_right: 4.0,
5415 };
5416 assert_eq!(
5417 build_rounded_rect_path(&rect, &positive, 0.0).total_vertices(),
5418 plain
5419 );
5420 }
5421
5422 #[test]
5423 fn build_rounded_rect_path_emits_arc_vertices_for_positive_radii() {
5424 let rect = AzRect::from_xywh(0.0, 0.0, 40.0, 40.0).unwrap();
5425 let radius = BorderRadius {
5426 top_left: 8.0,
5427 top_right: 8.0,
5428 bottom_left: 8.0,
5429 bottom_right: 8.0,
5430 };
5431 let rounded = build_rounded_rect_path(&rect, &radius, 1.0).total_vertices();
5432 assert!(
5433 rounded > build_rect_path(&rect).total_vertices(),
5434 "arcs must add vertices (a square-cornered path would be the old bug)"
5435 );
5436 }
5437
5438 #[test]
5439 fn build_rounded_rect_path_normalizes_oversized_radii() {
5440 let rect = AzRect::from_xywh(0.0, 0.0, 10.0, 10.0).unwrap();
5442 let radius = BorderRadius {
5443 top_left: 1e6,
5444 top_right: 1e6,
5445 bottom_left: 1e6,
5446 bottom_right: 1e6,
5447 };
5448 let path = build_rounded_rect_path(&rect, &radius, 1.0);
5449 assert!(path.total_vertices() > 4);
5450 let (mut x, mut y) = (0.0, 0.0);
5451 for i in 0..path.total_vertices() {
5452 path.vertex_idx(i, &mut x, &mut y);
5453 assert!(
5454 x.is_finite() && y.is_finite(),
5455 "vertex {i} is not finite: ({x}, {y})"
5456 );
5457 assert!(
5458 (-1.0..=11.0).contains(&x) && (-1.0..=11.0).contains(&y),
5459 "vertex {i} ({x}, {y}) escaped the 10x10 rect"
5460 );
5461 }
5462 }
5463
5464 #[test]
5469 fn text_lcd_enabled_is_read_once_and_stable() {
5470 let first = text_lcd_enabled();
5471 assert_eq!(first, text_lcd_enabled(), "the OnceLock must not flip");
5472 if std::env::var("AZ_TEXT_LCD").is_err() {
5473 assert_eq!(first, TEXT_LCD_DEFAULT, "unset env -> the documented default");
5474 }
5475 }
5476
5477 #[test]
5482 fn unbalanced_pops_never_underflow_the_stacks() {
5483 let mut p = pixmap(8, 8);
5486 let state = CpuRenderState::new(ScrollOffsetMap::new());
5487 let mut st = Stacks::new();
5488 for item in [
5489 DisplayListItem::PopClip,
5490 DisplayListItem::PopScrollFrame,
5491 DisplayListItem::PopReferenceFrame,
5492 DisplayListItem::PopStackingContext,
5493 DisplayListItem::PopOpacity,
5494 DisplayListItem::PopTextShadow,
5495 DisplayListItem::PopImageMaskClip,
5496 DisplayListItem::PopFilter,
5497 DisplayListItem::PopBackdropFilter,
5498 ] {
5499 let res = run_item(&item, &mut p, &mut st, &state);
5500 assert_eq!(res, Ok(()), "{item:?} must not error");
5501 }
5502 assert_eq!(st.clips.len(), 1, "the base clip must never be popped");
5503 assert_eq!(st.transforms.len(), 1);
5504 assert_eq!(st.scrolls.len(), 1);
5505 assert!(st.masks.is_empty());
5506 assert!(st.shadows.is_empty());
5507 }
5508
5509 #[test]
5510 #[should_panic = "called `Option::unwrap()` on a `None` value"]
5511 fn render_single_item_with_an_empty_clip_stack_panics_as_documented() {
5512 let mut p = pixmap(4, 4);
5515 let state = CpuRenderState::new(ScrollOffsetMap::new());
5516 let mut st = Stacks::new();
5517 st.clips.clear();
5518 let _ = run_item(
5519 &DisplayListItem::Rect {
5520 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5521 color: RED,
5522 border_radius: BorderRadius::default(),
5523 },
5524 &mut p,
5525 &mut st,
5526 &state,
5527 );
5528 }
5529
5530 #[test]
5531 fn push_clip_intersects_with_the_active_clip_and_never_widens_it() {
5532 let mut p = pixmap(16, 16);
5533 let state = CpuRenderState::new(ScrollOffsetMap::new());
5534 let mut st = Stacks::new();
5535
5536 run_item(
5537 &DisplayListItem::PushClip {
5538 bounds: wrect(0.0, 0.0, 10.0, 10.0),
5539 border_radius: BorderRadius::default(),
5540 },
5541 &mut p,
5542 &mut st,
5543 &state,
5544 )
5545 .unwrap();
5546 run_item(
5548 &DisplayListItem::PushClip {
5549 bounds: wrect(5.0, 5.0, 100.0, 100.0),
5550 border_radius: BorderRadius::default(),
5551 },
5552 &mut p,
5553 &mut st,
5554 &state,
5555 )
5556 .unwrap();
5557
5558 let top = st.clips.last().copied().flatten().expect("clip present");
5559 assert_eq!((top.x, top.y), (5.0, 5.0));
5560 assert_eq!((top.width, top.height), (5.0, 5.0), "the child cannot escape the parent");
5561
5562 run_item(&DisplayListItem::PopClip, &mut p, &mut st, &state).unwrap();
5563 run_item(&DisplayListItem::PopClip, &mut p, &mut st, &state).unwrap();
5564 assert_eq!(st.clips.len(), 1);
5565 }
5566
5567 #[test]
5568 fn push_clip_with_degenerate_bounds_pushes_an_unpaintable_clip() {
5569 let mut p = pixmap(8, 8);
5570 let state = CpuRenderState::new(ScrollOffsetMap::new());
5571 let mut st = Stacks::new();
5572 run_item(
5573 &DisplayListItem::PushClip {
5574 bounds: wrect(0.0, 0.0, f32::NAN, f32::NAN),
5575 border_radius: BorderRadius::default(),
5576 },
5577 &mut p,
5578 &mut st,
5579 &state,
5580 )
5581 .unwrap();
5582 assert_eq!(st.clips.len(), 2, "the pop must still find a matching push");
5583
5584 let before = snap(&p);
5585 run_item(
5586 &DisplayListItem::Rect {
5587 bounds: wrect(0.0, 0.0, 8.0, 8.0),
5588 color: RED,
5589 border_radius: BorderRadius::default(),
5590 },
5591 &mut p,
5592 &mut st,
5593 &state,
5594 )
5595 .unwrap();
5596 assert_eq!(before, p.data(), "a NaN clip must not silently become 'no clip'");
5597 }
5598
5599 #[test]
5600 fn scroll_frames_shift_item_bounds_by_the_accumulated_offset() {
5601 let mut offsets = ScrollOffsetMap::new();
5602 offsets.insert(7, (0.0, 5.0));
5603 let state = CpuRenderState::new(offsets);
5604
5605 let dl = DisplayList {
5606 items: vec![
5607 DisplayListItem::PushScrollFrame {
5608 clip_bounds: wrect(0.0, 0.0, 10.0, 10.0),
5609 content_size: LogicalSize {
5610 width: 10.0,
5611 height: 100.0,
5612 },
5613 scroll_id: 7,
5614 },
5615 DisplayListItem::Rect {
5616 bounds: wrect(0.0, 5.0, 10.0, 2.0),
5617 color: RED,
5618 border_radius: BorderRadius::default(),
5619 },
5620 DisplayListItem::PopScrollFrame,
5621 ],
5622 ..Default::default()
5623 };
5624
5625 let mut p = pixmap(10, 10);
5626 run_list_with_state(&dl, &mut p, &state).expect("must render");
5627 assert!(
5628 is_reddish(px_at(&p, 0, 0)),
5629 "content at y=5 scrolled by 5 must land on row 0"
5630 );
5631 assert_eq!(px_at(&p, 0, 5), [255, 255, 255, 255], "row 5 is now empty");
5632 }
5633
5634 #[test]
5635 fn a_missing_scroll_id_defaults_to_a_zero_offset() {
5636 let dl = DisplayList {
5637 items: vec![
5638 DisplayListItem::PushScrollFrame {
5639 clip_bounds: wrect(0.0, 0.0, 10.0, 10.0),
5640 content_size: LogicalSize {
5641 width: 10.0,
5642 height: 10.0,
5643 },
5644 scroll_id: 999, },
5646 DisplayListItem::Rect {
5647 bounds: wrect(0.0, 0.0, 2.0, 2.0),
5648 color: RED,
5649 border_radius: BorderRadius::default(),
5650 },
5651 DisplayListItem::PopScrollFrame,
5652 ],
5653 ..Default::default()
5654 };
5655 let mut p = pixmap(10, 10);
5656 run_list_with_state(&dl, &mut p, &CpuRenderState::new(ScrollOffsetMap::new()))
5657 .expect("must render");
5658 assert!(is_reddish(px_at(&p, 0, 0)), "an unknown scroll id must not shift");
5659 }
5660
5661 fn opacity_layer_result(op: f32) -> u8 {
5668 let dl = DisplayList {
5669 items: vec![
5670 DisplayListItem::PushOpacity {
5671 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5672 opacity: op,
5673 },
5674 DisplayListItem::Rect {
5675 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5676 color: BLACK,
5677 border_radius: BorderRadius::default(),
5678 },
5679 DisplayListItem::PopOpacity,
5680 ],
5681 ..Default::default()
5682 };
5683 let mut p = pixmap(4, 4);
5684 run_list(&dl, &mut p, 1.0).expect("must render");
5685 px_at(&p, 1, 1)[0]
5686 }
5687
5688 #[test]
5689 fn opacity_layer_blends_against_the_pre_push_snapshot() {
5690 assert_eq!(opacity_layer_result(1.0), 0, "opacity 1 keeps the drawing");
5691 assert_eq!(opacity_layer_result(0.0), 255, "opacity 0 restores the snapshot");
5692 let half = opacity_layer_result(0.5);
5693 assert!(
5694 (120..=136).contains(&half),
5695 "opacity 0.5 must land near mid-gray, got {half}"
5696 );
5697 }
5698
5699 #[test]
5700 fn opacity_layer_saturates_out_of_range_and_nan_values() {
5701 assert_eq!(opacity_layer_result(5.0), 0, "opacity > 1 clamps to opaque");
5704 assert_eq!(opacity_layer_result(-5.0), 255, "opacity < 0 clamps to transparent");
5705 assert_eq!(opacity_layer_result(f32::INFINITY), 0);
5706 assert_eq!(opacity_layer_result(f32::NEG_INFINITY), 255);
5707 assert_eq!(opacity_layer_result(f32::NAN), 255);
5708 }
5709
5710 #[test]
5711 fn push_opacity_with_degenerate_bounds_pushes_nothing() {
5712 let mut p = pixmap(8, 8);
5715 let state = CpuRenderState::new(ScrollOffsetMap::new());
5716 let mut st = Stacks::new();
5717 run_item(
5718 &DisplayListItem::PushOpacity {
5719 bounds: wrect(0.0, 0.0, f32::NAN, 0.0),
5720 opacity: 0.5,
5721 },
5722 &mut p,
5723 &mut st,
5724 &state,
5725 )
5726 .unwrap();
5727 assert!(st.masks.is_empty());
5728 assert_eq!(
5729 run_item(&DisplayListItem::PopOpacity, &mut p, &mut st, &state),
5730 Ok(())
5731 );
5732 }
5733
5734 #[test]
5739 fn image_mask_clip_masks_the_drawing_it_wraps() {
5740 let mask = r8_image(2, 2, vec![255, 0, 255, 0]);
5742 let dl = DisplayList {
5743 items: vec![
5744 DisplayListItem::PushImageMaskClip {
5745 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5746 mask_image: mask,
5747 mask_rect: wrect(0.0, 0.0, 4.0, 4.0),
5748 },
5749 DisplayListItem::Rect {
5750 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5751 color: BLACK,
5752 border_radius: BorderRadius::default(),
5753 },
5754 DisplayListItem::PopImageMaskClip,
5755 ],
5756 ..Default::default()
5757 };
5758 let mut p = pixmap(4, 4);
5759 run_list(&dl, &mut p, 1.0).expect("must render");
5760 assert_eq!(px_at(&p, 0, 0), [0, 0, 0, 255], "mask=255 keeps the fill");
5761 assert_eq!(
5762 px_at(&p, 3, 0),
5763 [255, 255, 255, 255],
5764 "mask=0 restores the background"
5765 );
5766 }
5767
5768 #[test]
5769 fn image_mask_clip_with_a_degenerate_rect_is_skipped() {
5770 let mask = r8_image(1, 1, vec![255]);
5771 let mut p = pixmap(8, 8);
5772 let state = CpuRenderState::new(ScrollOffsetMap::new());
5773 let mut st = Stacks::new();
5774 run_item(
5775 &DisplayListItem::PushImageMaskClip {
5776 bounds: wrect(0.0, 0.0, 8.0, 8.0),
5777 mask_image: mask,
5778 mask_rect: wrect(0.0, 0.0, 0.0, 0.0),
5779 },
5780 &mut p,
5781 &mut st,
5782 &state,
5783 )
5784 .unwrap();
5785 assert!(st.masks.is_empty(), "a 0-sized mask rect pushes no entry");
5786 }
5787
5788 #[test]
5793 fn a_text_item_whose_font_is_unknown_paints_nothing() {
5794 let dl = DisplayList {
5795 items: vec![DisplayListItem::Text {
5796 glyphs: vec![GlyphInstance {
5797 index: 1,
5798 point: LogicalPosition { x: 0.0, y: 10.0 },
5799 size: LogicalSize {
5800 width: 8.0,
5801 height: 16.0,
5802 },
5803 }],
5804 font_hash: FontHash { font_hash: 0xdead_beef },
5805 font_size_px: 16.0,
5806 color: BLACK,
5807 clip_rect: wrect(0.0, 0.0, 16.0, 16.0),
5808 source_node_index: None,
5809 }],
5810 ..Default::default()
5811 };
5812 let mut p = pixmap(16, 16);
5813 let before = snap(&p);
5814 run_list(&dl, &mut p, 1.0).expect("a missing font must not fail the frame");
5815 assert_eq!(before, p.data());
5816 }
5817
5818 #[test]
5819 fn a_text_item_with_no_glyphs_or_no_alpha_paints_nothing() {
5820 for (glyphs, color) in [
5821 (Vec::new(), BLACK),
5822 (
5823 vec![GlyphInstance {
5824 index: 1,
5825 point: LogicalPosition { x: 0.0, y: 10.0 },
5826 size: LogicalSize {
5827 width: 8.0,
5828 height: 16.0,
5829 },
5830 }],
5831 CLEAR,
5832 ),
5833 ] {
5834 let dl = DisplayList {
5835 items: vec![DisplayListItem::Text {
5836 glyphs,
5837 font_hash: FontHash { font_hash: 1 },
5838 font_size_px: 16.0,
5839 color,
5840 clip_rect: wrect(0.0, 0.0, 16.0, 16.0),
5841 source_node_index: None,
5842 }],
5843 ..Default::default()
5844 };
5845 let mut p = pixmap(16, 16);
5846 let before = snap(&p);
5847 run_list(&dl, &mut p, 1.0).expect("must render");
5848 assert_eq!(before, p.data());
5849 }
5850 }
5851
5852 #[test]
5857 fn an_rgba_image_is_blitted_with_its_channels_in_order() {
5858 let img = rgba_image(2, 2, [255, 0, 0, 255].repeat(4));
5860 let dl = DisplayList {
5861 items: vec![DisplayListItem::Image {
5862 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5863 image: img,
5864 border_radius: BorderRadius::default(),
5865 }],
5866 ..Default::default()
5867 };
5868 let mut p = pixmap(8, 8);
5869 run_list(&dl, &mut p, 1.0).expect("must render");
5870 assert!(
5871 is_reddish(px_at(&p, 1, 1)),
5872 "an RGBA image must not come out swizzled or gray, got {:?}",
5873 px_at(&p, 1, 1)
5874 );
5875 assert_eq!(px_at(&p, 6, 6), [255, 255, 255, 255], "outside the bounds");
5876 }
5877
5878 #[test]
5879 fn an_image_with_degenerate_bounds_is_skipped() {
5880 for bad in DEGENERATE {
5881 let img = rgba_image(1, 1, vec![255, 0, 0, 255]);
5882 let dl = DisplayList {
5883 items: vec![DisplayListItem::Image {
5884 bounds: wrect(0.0, 0.0, bad, bad),
5885 image: img,
5886 border_radius: BorderRadius::default(),
5887 }],
5888 ..Default::default()
5889 };
5890 let mut p = pixmap(8, 8);
5891 let before = snap(&p);
5892 run_list(&dl, &mut p, 1.0).expect("must render");
5893 assert_eq!(before, p.data(), "image size {bad} must be rejected");
5894 }
5895 }
5896
5897 #[test]
5898 fn a_fully_transparent_image_leaves_the_background_alone() {
5899 let img = rgba_image(2, 2, [255, 0, 0, 0].repeat(4));
5900 let dl = DisplayList {
5901 items: vec![DisplayListItem::Image {
5902 bounds: wrect(0.0, 0.0, 4.0, 4.0),
5903 image: img,
5904 border_radius: BorderRadius::default(),
5905 }],
5906 ..Default::default()
5907 };
5908 let mut p = pixmap(8, 8);
5909 let before = snap(&p);
5910 run_list(&dl, &mut p, 1.0).expect("must render");
5911 assert_eq!(before, p.data(), "alpha=0 source pixels must not blend");
5912 }
5913
5914 #[test]
5919 fn render_border_draws_the_frame_but_not_the_middle() {
5920 let mut p = pixmap(20, 20);
5921 render_border(
5922 &mut p,
5923 &lrect(0.0, 0.0, 20.0, 20.0),
5924 RED,
5925 2.0,
5926 BorderStyle::Solid,
5927 &BorderRadius::default(),
5928 None,
5929 1.0,
5930 );
5931 assert!(is_reddish(px_at(&p, 0, 0)), "the frame is painted");
5932 assert!(is_reddish(px_at(&p, 19, 19)));
5933 assert_eq!(px_at(&p, 10, 10), [255, 255, 255, 255], "the middle stays clear");
5934 }
5935
5936 #[test]
5937 fn render_border_zero_or_negative_width_is_a_noop() {
5938 for width in [0.0, -1.0, -1e30, f32::NEG_INFINITY] {
5939 let mut p = pixmap(10, 10);
5940 let before = snap(&p);
5941 render_border(
5942 &mut p,
5943 &lrect(0.0, 0.0, 10.0, 10.0),
5944 RED,
5945 width,
5946 BorderStyle::Solid,
5947 &BorderRadius::default(),
5948 None,
5949 1.0,
5950 );
5951 assert_eq!(before, p.data(), "border width {width} must not paint");
5952 }
5953 }
5954
5955 #[test]
5956 fn render_border_nan_width_and_hidden_styles_are_noops() {
5957 let mut p = pixmap(10, 10);
5962 render_border(
5963 &mut p,
5964 &lrect(0.0, 0.0, 10.0, 10.0),
5965 RED,
5966 f32::NAN,
5967 BorderStyle::Solid,
5968 &BorderRadius::default(),
5969 None,
5970 1.0,
5971 );
5972 assert_eq!(p.data().len(), 400, "the buffer must be intact");
5973 assert_eq!(
5974 px_at(&p, 5, 5),
5975 [255, 255, 255, 255],
5976 "a NaN border width must not fill the middle of the box"
5977 );
5978
5979 for style in [BorderStyle::None, BorderStyle::Hidden] {
5980 let mut p = pixmap(10, 10);
5981 let before = snap(&p);
5982 render_border(
5983 &mut p,
5984 &lrect(0.0, 0.0, 10.0, 10.0),
5985 RED,
5986 2.0,
5987 style,
5988 &BorderRadius::default(),
5989 None,
5990 1.0,
5991 );
5992 assert_eq!(before, p.data(), "{style:?} must not paint");
5993 }
5994 }
5995
5996 #[test]
5997 fn render_border_transparent_color_and_degenerate_dpi_are_noops() {
5998 let mut p = pixmap(10, 10);
5999 let before = snap(&p);
6000 render_border(
6001 &mut p,
6002 &lrect(0.0, 0.0, 10.0, 10.0),
6003 CLEAR,
6004 2.0,
6005 BorderStyle::Solid,
6006 &BorderRadius::default(),
6007 None,
6008 1.0,
6009 );
6010 assert_eq!(before, p.data());
6011
6012 for dpi in DEGENERATE {
6013 let mut p = pixmap(10, 10);
6014 let before = snap(&p);
6015 render_border(
6016 &mut p,
6017 &lrect(0.0, 0.0, 10.0, 10.0),
6018 RED,
6019 2.0,
6020 BorderStyle::Solid,
6021 &BorderRadius::default(),
6022 None,
6023 dpi,
6024 );
6025 assert_eq!(before, p.data(), "dpi {dpi} must be rejected");
6026 }
6027 }
6028
6029 #[test]
6030 fn render_border_width_larger_than_the_box_does_not_panic() {
6031 let mut p = pixmap(10, 10);
6034 render_border(
6035 &mut p,
6036 &lrect(0.0, 0.0, 10.0, 10.0),
6037 RED,
6038 1000.0,
6039 BorderStyle::Solid,
6040 &BorderRadius::default(),
6041 None,
6042 1.0,
6043 );
6044 assert!(is_reddish(px_at(&p, 5, 5)));
6045 }
6046
6047 #[test]
6048 fn render_border_dashed_and_dotted_styles_paint_without_panicking() {
6049 for style in [BorderStyle::Dashed, BorderStyle::Dotted] {
6050 let mut p = pixmap(20, 20);
6051 let before = snap(&p);
6052 render_border(
6053 &mut p,
6054 &lrect(2.0, 2.0, 16.0, 16.0),
6055 RED,
6056 2.0,
6057 style,
6058 &BorderRadius::default(),
6059 None,
6060 1.0,
6061 );
6062 assert_ne!(before, p.data(), "{style:?} must paint something");
6063 }
6064 }
6065
6066 #[test]
6067 fn render_border_sides_with_mixed_widths_paints_each_side() {
6068 let mut p = pixmap(20, 20);
6069 render_border_sides(
6070 &mut p,
6071 &lrect(0.0, 0.0, 20.0, 20.0),
6072 [RED, BLUE, RED, BLUE],
6073 [3.0, 1.0, 3.0, 1.0],
6074 [
6075 BorderStyle::Solid,
6076 BorderStyle::Solid,
6077 BorderStyle::Solid,
6078 BorderStyle::Solid,
6079 ],
6080 &BorderRadius::default(),
6081 None,
6082 1.0,
6083 );
6084 assert!(is_reddish(px_at(&p, 10, 0)), "the top side is red");
6085 assert_eq!(px_at(&p, 10, 10), [255, 255, 255, 255], "the middle stays clear");
6086 }
6087
6088 #[test]
6089 fn render_border_sides_zero_widths_and_degenerate_values_are_noops() {
6090 let styles = [
6091 BorderStyle::Solid,
6092 BorderStyle::Solid,
6093 BorderStyle::Solid,
6094 BorderStyle::Solid,
6095 ];
6096 let mut p = pixmap(10, 10);
6097 let before = snap(&p);
6098 render_border_sides(
6099 &mut p,
6100 &lrect(0.0, 0.0, 10.0, 10.0),
6101 [RED; 4],
6102 [0.0; 4],
6103 styles,
6104 &BorderRadius::default(),
6105 None,
6106 1.0,
6107 );
6108 assert_eq!(before, p.data(), "0-width sides must not paint");
6109
6110 for bad in DEGENERATE {
6111 let mut p = pixmap(10, 10);
6112 let before = snap(&p);
6113 render_border_sides(
6114 &mut p,
6115 &lrect(0.0, 0.0, 10.0, 10.0),
6116 [RED; 4],
6117 [2.0; 4],
6118 styles,
6119 &BorderRadius::default(),
6120 None,
6121 bad,
6122 );
6123 assert_eq!(before, p.data(), "dpi {bad} must be rejected");
6124 }
6125
6126 for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -5.0] {
6128 let mut p = pixmap(10, 10);
6129 render_border_sides(
6130 &mut p,
6131 &lrect(0.0, 0.0, 10.0, 10.0),
6132 [RED; 4],
6133 [bad; 4],
6134 styles,
6135 &BorderRadius::default(),
6136 None,
6137 1.0,
6138 );
6139 assert_eq!(p.data().len(), 400, "width {bad} must not resize the buffer");
6140 }
6141 }
6142
6143 fn damaged(
6148 dl: &DisplayList,
6149 p: &mut AzulPixmap,
6150 rects: &[LogicalRect],
6151 ) -> Result<(), String> {
6152 let res = RendererResources::default();
6153 let mut gc = GlyphCache::new();
6154 let state = CpuRenderState::new(ScrollOffsetMap::new());
6155 render_display_list_damaged(dl, p, 1.0, &res, None, &mut gc, &state, rects)
6156 }
6157
6158 fn full_red_dl() -> DisplayList {
6159 DisplayList {
6160 items: vec![DisplayListItem::Rect {
6161 bounds: wrect(0.0, 0.0, 8.0, 8.0),
6162 color: RED,
6163 border_radius: BorderRadius::default(),
6164 }],
6165 ..Default::default()
6166 }
6167 }
6168
6169 #[test]
6170 fn damaged_render_without_rects_is_a_noop() {
6171 let mut p = pixmap(8, 8);
6172 p.fill(0, 0, 255, 255);
6173 let before = snap(&p);
6174 damaged(&full_red_dl(), &mut p, &[]).expect("must succeed");
6175 assert_eq!(before, p.data(), "no damage -> no repaint at all");
6176 }
6177
6178 #[test]
6179 fn damaged_render_only_repaints_inside_the_damage_rect() {
6180 let mut p = pixmap(8, 8);
6181 p.fill(0, 0, 255, 255); damaged(&full_red_dl(), &mut p, &[lrect(0.0, 0.0, 4.0, 4.0)]).expect("must succeed");
6183 assert!(is_reddish(px_at(&p, 1, 1)), "the damaged region is repainted");
6184 assert_eq!(
6185 px_at(&p, 6, 6),
6186 [0, 0, 255, 255],
6187 "untouched pixels must survive — a union-clip repaint used to wipe them"
6188 );
6189 }
6190
6191 #[test]
6192 fn damaged_render_with_nan_rects_paints_nothing() {
6193 let mut p = pixmap(8, 8);
6194 p.fill(0, 0, 255, 255);
6195 let before = snap(&p);
6196 damaged(
6197 &full_red_dl(),
6198 &mut p,
6199 &[lrect(f32::NAN, f32::NAN, f32::NAN, f32::NAN)],
6200 )
6201 .expect("must succeed");
6202 assert_eq!(before, p.data(), "a NaN damage rect must collapse to nothing");
6203 }
6204
6205 #[test]
6206 fn damaged_render_clamps_saturating_rects_to_the_pixmap() {
6207 let mut p = pixmap(8, 8);
6208 p.fill(0, 0, 255, 255);
6209 damaged(&full_red_dl(), &mut p, &[lrect(-1e9, -1e9, 3e9, 3e9)]).expect("must succeed");
6210 assert!(
6211 p.data().chunks_exact(4).all(|c| c[0] > 200 && c[1] < 60),
6212 "an oversized damage rect clamps to the buffer and repaints all of it"
6213 );
6214 }
6215
6216 #[test]
6217 fn damaged_render_merges_overlapping_rects_without_double_blending() {
6218 let half_red = ColorU { r: 255, g: 0, b: 0, a: 128 };
6221 let dl = DisplayList {
6222 items: vec![DisplayListItem::Rect {
6223 bounds: wrect(0.0, 0.0, 8.0, 8.0),
6224 color: half_red,
6225 border_radius: BorderRadius::default(),
6226 }],
6227 ..Default::default()
6228 };
6229
6230 let mut once = pixmap(8, 8);
6231 damaged(&dl, &mut once, &[lrect(0.0, 0.0, 8.0, 8.0)]).expect("must succeed");
6232
6233 let mut twice = pixmap(8, 8);
6234 damaged(
6235 &dl,
6236 &mut twice,
6237 &[lrect(0.0, 0.0, 6.0, 6.0), lrect(2.0, 2.0, 6.0, 6.0)],
6238 )
6239 .expect("must succeed");
6240
6241 assert_eq!(
6242 px_at(&once, 3, 3),
6243 px_at(&twice, 3, 3),
6244 "the overlap must be blended exactly once"
6245 );
6246 }
6247
6248 #[test]
6249 fn damaged_render_with_a_zero_area_rect_is_a_noop() {
6250 let mut p = pixmap(8, 8);
6251 p.fill(0, 0, 255, 255);
6252 let before = snap(&p);
6253 damaged(&full_red_dl(), &mut p, &[lrect(4.0, 4.0, 0.0, 0.0)]).expect("must succeed");
6254 assert_eq!(before, p.data());
6255 }
6256
6257 #[cfg(all(feature = "text_layout", feature = "font_loading"))]
6262 #[test]
6263 fn component_preview_of_a_degenerate_size_never_panics() {
6264 use rust_fontconfig::FcFontCache;
6265
6266 let mut dom = azul_core::dom::Dom::create_body();
6267 let styled = azul_core::styled_dom::StyledDom::create(&mut dom, azul_css::css::Css::empty());
6268 let fm = FontManager::<FontRef>::new(FcFontCache::default()).expect("font manager");
6269
6270 for (w, h, dpi) in [
6274 (Some(0.0), Some(0.0), 1.0),
6275 (Some(8.0), Some(8.0), 0.0),
6276 (Some(8.0), Some(8.0), 1.0),
6277 ] {
6278 let o = ComponentPreviewOptions {
6279 width: w,
6280 height: h,
6281 dpi_factor: dpi,
6282 ..ComponentPreviewOptions::default()
6283 };
6284 match render_component_preview(&styled, &fm, o, None) {
6285 Ok(res) => {
6286 assert!(
6287 res.content_width.is_finite() && res.content_height.is_finite(),
6288 "{w:?}x{h:?}@{dpi} produced non-finite content bounds"
6289 );
6290 assert!(
6291 res.content_width <= 4096.0 && res.content_height <= 4096.0,
6292 "the preview must stay bounded by MAX_SIZE"
6293 );
6294 }
6295 Err(e) => assert!(!e.is_empty(), "an error must carry a message"),
6296 }
6297 }
6298 }
6299
6300 #[cfg(all(feature = "text_layout", feature = "font_loading"))]
6301 #[test]
6302 fn text_run_to_pixmap_without_any_font_returns_none_for_every_input() {
6303 use rust_fontconfig::FcFontCache;
6304
6305 let empty = FcFontCache::default();
6309 let long = "A".repeat(1_000_000);
6310 let nested = "[".repeat(10_000);
6311 let inputs = [
6312 "",
6313 " ",
6314 "\t\n\r",
6315 "\0\u{1}\u{7f}",
6316 "0",
6317 "-0",
6318 "9223372036854775807",
6319 "NaN",
6320 "inf",
6321 "-inf",
6322 " valid ",
6323 "valid;garbage",
6324 "\u{1F600}\u{1F1E9}\u{1F1EA}",
6325 "e\u{301}\u{323}\u{489}",
6326 long.as_str(),
6327 nested.as_str(),
6328 ];
6329 for text in inputs {
6330 let got = render_text_run_to_pixmap(&empty, text, 16.0, BLACK, WHITE, 2.0, 1.0);
6331 assert!(
6332 got.is_none(),
6333 "no resolvable font must yield None (input len {})",
6334 text.len()
6335 );
6336 }
6337
6338 for size in [0.0, -16.0, f32::NAN, f32::INFINITY] {
6340 assert!(render_text_run_to_pixmap(&empty, "hi", size, BLACK, WHITE, 0.0, 1.0).is_none());
6341 }
6342 for dpi in [0.0, -1.0, f32::NAN] {
6343 assert!(render_text_run_to_pixmap(&empty, "hi", 16.0, BLACK, WHITE, 2.0, dpi).is_none());
6344 }
6345 }
6346
6347 #[cfg(all(feature = "text_layout", feature = "font_loading"))]
6348 #[test]
6349 fn text_run_to_pixmap_renders_dark_glyphs_on_the_background() {
6350 use rust_fontconfig::{FcFont, FcFontCache, FcPattern};
6351
6352 let candidates = [
6353 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
6354 "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
6355 "/System/Library/Fonts/Supplemental/Times New Roman.ttf",
6356 "C:/Windows/Fonts/arial.ttf",
6357 ];
6358 let Some(bytes) = candidates.iter().find_map(|p| std::fs::read(p).ok()) else {
6359 eprintln!("[skip] no system font file available");
6360 return;
6361 };
6362
6363 let cache = FcFontCache::default();
6364 cache.with_memory_fonts(vec![(
6365 FcPattern {
6366 family: Some("sans-serif".to_string()),
6367 ..Default::default()
6368 },
6369 FcFont {
6370 bytes,
6371 font_index: 0,
6372 id: "autotest-sans".to_string(),
6373 },
6374 )]);
6375
6376 let Some(p) = render_text_run_to_pixmap(&cache, "Hi", 24.0, BLACK, WHITE, 4.0, 1.0) else {
6377 eprintln!("[skip] the memory font did not resolve through fontconfig");
6378 return;
6379 };
6380 assert!(p.width >= 1 && p.height >= 1);
6381 let dark = p.data().chunks_exact(4).filter(|c| c[0] < 128).count();
6382 assert!(dark > 0, "the glyph run must actually rasterize");
6383
6384 let empty = render_text_run_to_pixmap(&cache, "", 24.0, BLACK, WHITE, 4.0, 1.0)
6387 .expect("empty text must still give a pixmap");
6388 assert!(empty.width >= 1 && empty.height >= 1);
6389 assert!(
6390 empty.data().chunks_exact(4).all(|c| c[0] == 255 && c[1] == 255),
6391 "empty text must paint no glyphs"
6392 );
6393
6394 assert!(
6396 render_text_run_to_pixmap(&cache, "\u{1F600}é\u{301}", 24.0, BLACK, WHITE, 4.0, 1.0)
6397 .is_some(),
6398 "unicode input must not panic or bail out"
6399 );
6400 }
6401}