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() / 360.0).clamp(0.0, 1.0));
108 let c = resolve_color(&stop.color, system_colors);
109 lut.add_color(
110 offset,
111 Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a)),
112 );
113 }
114 lut.build_lut();
115 lut
116}
117
118fn resolve_background_position(
120 pos: &azul_css::props::style::background::StyleBackgroundPosition,
121 width: f32,
122 height: f32,
123) -> (f32, f32) {
124 use azul_css::props::style::background::{
125 BackgroundPositionHorizontal, BackgroundPositionVertical,
126 };
127
128 let x = match pos.horizontal {
129 BackgroundPositionHorizontal::Left => 0.0,
130 BackgroundPositionHorizontal::Center => 0.5,
131 BackgroundPositionHorizontal::Right => 1.0,
132 BackgroundPositionHorizontal::Exact(px) => {
133 let val = px.to_pixels_internal(width, 16.0, 16.0);
134 if width > 0.0 {
135 val / width
136 } else {
137 0.5
138 }
139 }
140 };
141 let y = match pos.vertical {
142 BackgroundPositionVertical::Top => 0.0,
143 BackgroundPositionVertical::Center => 0.5,
144 BackgroundPositionVertical::Bottom => 1.0,
145 BackgroundPositionVertical::Exact(px) => {
146 let val = px.to_pixels_internal(height, 16.0, 16.0);
147 if height > 0.0 {
148 val / height
149 } else {
150 0.5
151 }
152 }
153 };
154 (x, y)
155}
156
157#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] fn render_linear_gradient(
159 pixmap: &mut AzulPixmap,
160 bounds: &LogicalRect,
161 gradient: &azul_css::props::style::background::LinearGradient,
162 border_radius: &BorderRadius,
163 clip: Option<AzRect>,
164 dpi_factor: f32,
165 system_colors: Option<&azul_css::system::SystemColors>,
166) {
167 use azul_css::props::basic::geometry::{LayoutRect, LayoutSize};
168
169 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
170 return;
171 };
172
173 let stops = gradient.stops.as_ref();
174 if stops.is_empty() {
175 return;
176 }
177
178 let lut = build_gradient_lut_linear(&gradient.stops, system_colors);
179
180 let layout_rect = LayoutRect {
182 origin: azul_css::props::basic::geometry::LayoutPoint::new(0, 0),
183 size: LayoutSize {
184 width: (rect.width as isize),
185 height: (rect.height as isize),
186 },
187 };
188 let (from_pt, to_pt) = gradient.direction.to_points(&layout_rect);
189
190 let x1 = f64::from(rect.x) + from_pt.x as f64;
192 let y1 = f64::from(rect.y) + from_pt.y as f64;
193 let x2 = f64::from(rect.x) + to_pt.x as f64;
194 let y2 = f64::from(rect.y) + to_pt.y as f64;
195
196 let dx = x2 - x1;
197 let dy = y2 - y1;
198 let len = dx.hypot(dy);
199 if len < 0.001 {
200 return;
201 }
202
203 let mut transform = TransAffine::new_line_segment(x1, y1, x2, y2, 100.0);
208 transform.invert();
209
210 let mut path = if border_radius.is_zero() {
211 build_rect_path(&rect)
212 } else {
213 build_rounded_rect_path(&rect, border_radius, dpi_factor)
214 };
215
216 agg_fill_gradient_clipped(
217 pixmap, &mut path, &lut, GradientX, transform, 0.0, 100.0, clip,
218 );
219}
220
221#[allow(clippy::suboptimal_flops)] #[allow(clippy::similar_names)] #[allow(clippy::match_same_arms)] fn render_radial_gradient(
225 pixmap: &mut AzulPixmap,
226 bounds: &LogicalRect,
227 gradient: &azul_css::props::style::background::RadialGradient,
228 border_radius: &BorderRadius,
229 clip: Option<AzRect>,
230 dpi_factor: f32,
231 system_colors: Option<&azul_css::system::SystemColors>,
232) {
233 use azul_css::props::style::background::{RadialGradientSize, Shape};
234
235 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
236 return;
237 };
238
239 let stops = gradient.stops.as_ref();
240 if stops.is_empty() {
241 return;
242 }
243
244 let lut = build_gradient_lut_linear(&gradient.stops, system_colors);
245
246 let w = f64::from(rect.width);
247 let h = f64::from(rect.height);
248
249 let (cx_frac, cy_frac) =
251 resolve_background_position(&gradient.position, rect.width, rect.height);
252 let cx = f64::from(rect.x) + f64::from(cx_frac) * w;
253 let cy = f64::from(rect.y) + f64::from(cy_frac) * h;
254
255 let radius = match gradient.size {
257 RadialGradientSize::ClosestSide => {
258 let dx = (f64::from(cx_frac) * w).min((1.0 - f64::from(cx_frac)) * w);
259 let dy = (f64::from(cy_frac) * h).min((1.0 - f64::from(cy_frac)) * h);
260 match gradient.shape {
261 Shape::Circle => dx.min(dy),
262 Shape::Ellipse => dx.min(dy), }
264 }
265 RadialGradientSize::FarthestSide => {
266 let dx = (f64::from(cx_frac) * w).max((1.0 - f64::from(cx_frac)) * w);
267 let dy = (f64::from(cy_frac) * h).max((1.0 - f64::from(cy_frac)) * h);
268 match gradient.shape {
269 Shape::Circle => dx.max(dy),
270 Shape::Ellipse => dx.max(dy),
271 }
272 }
273 RadialGradientSize::ClosestCorner => {
274 let dx = (f64::from(cx_frac) * w).min((1.0 - f64::from(cx_frac)) * w);
275 let dy = (f64::from(cy_frac) * h).min((1.0 - f64::from(cy_frac)) * h);
276 dx.hypot(dy)
277 }
278 RadialGradientSize::FarthestCorner => {
279 let dx = (f64::from(cx_frac) * w).max((1.0 - f64::from(cx_frac)) * w);
280 let dy = (f64::from(cy_frac) * h).max((1.0 - f64::from(cy_frac)) * h);
281 dx.hypot(dy)
282 }
283 };
284
285 if radius < 0.001 {
286 return;
287 }
288
289 let mut transform = TransAffine::new_scaling_uniform(radius / 100.0);
293 transform.translate(cx, cy);
294 transform.invert();
295
296 let mut path = if border_radius.is_zero() {
297 build_rect_path(&rect)
298 } else {
299 build_rounded_rect_path(&rect, border_radius, dpi_factor)
300 };
301
302 agg_fill_gradient_clipped(
303 pixmap,
304 &mut path,
305 &lut,
306 GradientRadialD,
307 transform,
308 0.0,
309 100.0,
310 clip,
311 );
312}
313
314#[allow(clippy::suboptimal_flops)] #[allow(clippy::similar_names)] fn render_conic_gradient(
317 pixmap: &mut AzulPixmap,
318 bounds: &LogicalRect,
319 gradient: &azul_css::props::style::background::ConicGradient,
320 border_radius: &BorderRadius,
321 clip: Option<AzRect>,
322 dpi_factor: f32,
323 system_colors: Option<&azul_css::system::SystemColors>,
324) {
325 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
326 return;
327 };
328
329 let stops = gradient.stops.as_ref();
330 if stops.is_empty() {
331 return;
332 }
333
334 let lut = build_gradient_lut_radial(&gradient.stops, system_colors);
335
336 let w = f64::from(rect.width);
337 let h = f64::from(rect.height);
338
339 let (cx_frac, cy_frac) = resolve_background_position(&gradient.center, rect.width, rect.height);
341 let cx = f64::from(rect.x) + f64::from(cx_frac) * w;
342 let cy = f64::from(rect.y) + f64::from(cy_frac) * h;
343
344 let start_angle_deg = gradient.angle.to_degrees();
346 let start_angle_rad = f64::from(start_angle_deg - 90.0).to_radians();
347
348 let mut transform = TransAffine::new_rotation(start_angle_rad);
352 transform.translate(cx, cy);
353 transform.invert();
354
355 let d2 = 100.0;
358
359 let mut path = if border_radius.is_zero() {
360 build_rect_path(&rect)
361 } else {
362 build_rounded_rect_path(&rect, border_radius, dpi_factor)
363 };
364
365 agg_fill_gradient_clipped(
366 pixmap,
367 &mut path,
368 &lut,
369 GradientConic,
370 transform,
371 0.0,
372 d2,
373 clip,
374 );
375}
376
377#[allow(clippy::suboptimal_flops)] #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] fn render_box_shadow(
384 pixmap: &mut AzulPixmap,
385 bounds: &LogicalRect,
386 shadow: &StyleBoxShadow,
387 border_radius: &BorderRadius,
388 dpi_factor: f32,
389) -> Result<(), String> {
390 use azul_css::props::style::box_shadow::BoxShadowClipMode;
391
392 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
393 return Ok(());
394 };
395
396 let offset_x =
397 shadow
398 .offset_x
399 .inner
400 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
401 * dpi_factor;
402 let offset_y =
403 shadow
404 .offset_y
405 .inner
406 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
407 * dpi_factor;
408 let blur_r =
409 (shadow
410 .blur_radius
411 .inner
412 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
413 * dpi_factor)
414 .max(0.0);
415 let spread =
416 shadow
417 .spread_radius
418 .inner
419 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
420 * dpi_factor;
421
422 let color = shadow.color;
423 if color.a == 0 {
424 return Ok(());
425 }
426
427 let padding = blur_r.ceil();
429 let shadow_x = rect.x + offset_x - spread - padding;
430 let shadow_y = rect.y + offset_y - spread - padding;
431 let shadow_w = rect.width + 2.0 * spread + 2.0 * padding;
432 let shadow_h = rect.height + 2.0 * spread + 2.0 * padding;
433
434 if shadow_w <= 0.0 || shadow_h <= 0.0 {
435 return Ok(());
436 }
437
438 let sw = shadow_w.ceil() as u32;
439 let sh = shadow_h.ceil() as u32;
440
441 if sw == 0 || sh == 0 || sw > MAX_SHADOW_PIXBUF_SIZE || sh > MAX_SHADOW_PIXBUF_SIZE {
442 return Ok(());
443 }
444
445 let mut tmp = AzulPixmap::new(sw, sh).ok_or("cannot create shadow pixmap")?;
447 tmp.fill(0, 0, 0, 0); let shape_x = padding + spread;
451 let shape_y = padding + spread;
452 let Some(shape_rect) = AzRect::from_xywh(shape_x, shape_y, rect.width, rect.height) else {
453 return Ok(());
454 };
455
456 let agg_color = Rgba8::new(
457 u32::from(color.r),
458 u32::from(color.g),
459 u32::from(color.b),
460 u32::from(color.a),
461 );
462 if border_radius.is_zero() {
463 let mut path = build_rect_path(&shape_rect);
464 agg_fill_path(&mut tmp, &mut path, &agg_color, FillingRule::NonZero);
465 } else {
466 let mut path = build_rounded_rect_path(&shape_rect, border_radius, dpi_factor);
467 agg_fill_path(&mut tmp, &mut path, &agg_color, FillingRule::NonZero);
468 }
469
470 if blur_r > 0.5 {
472 let blur_radius = (blur_r.ceil() as u32).min(254);
473 let stride = (sw * 4) as i32;
474 let mut ra = unsafe { RowAccessor::new_with_buf(tmp.data.as_mut_ptr(), sw, sh, stride) };
475 stack_blur_rgba32(&mut ra, blur_radius, blur_radius);
476 }
477
478 let dst_x = shadow_x as i32;
480 let dst_y = shadow_y as i32;
481 blit_buffer(pixmap, &tmp.data, sw, sh, dst_x, dst_y);
482
483 Ok(())
484}
485
486#[derive(Debug)]
488pub enum MaskEntry {
489 ImageMask {
491 snapshot: Vec<u8>,
492 mask_data: Vec<u8>,
493 origin_x: i32,
494 origin_y: i32,
495 width: u32,
496 height: u32,
497 },
498 Opacity {
500 snapshot: Vec<u8>,
501 rect: AzRect,
502 opacity: f32,
503 },
504}
505
506#[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>> {
509 let image_data = mask_image.get_data();
510 let (mask_bytes, src_w, src_h) = match image_data {
511 DecodedImage::Raw((descriptor, data)) => {
512 let w = descriptor.width as u32;
513 let h = descriptor.height as u32;
514 if w == 0 || h == 0 {
515 return None;
516 }
517 let bytes = match data {
518 azul_core::resources::ImageData::Raw(shared) => shared.as_ref(),
519 azul_core::resources::ImageData::External(_) => return None,
520 };
521 match descriptor.format {
522 azul_core::resources::RawImageFormat::R8 => (bytes.to_vec(), w, h),
523 azul_core::resources::RawImageFormat::BGRA8 => {
524 let mut r8 = Vec::with_capacity((w * h) as usize);
526 for chunk in bytes.chunks_exact(4) {
527 r8.push(chunk[3]); }
529 (r8, w, h)
530 }
531 _ => {
532 let chan_count = bytes.len() / (w * h) as usize;
534 if chan_count == 0 {
535 return None;
536 }
537 let mut r8 = Vec::with_capacity((w * h) as usize);
538 for i in 0..(w * h) as usize {
539 r8.push(bytes[i * chan_count]);
540 }
541 (r8, w, h)
542 }
543 }
544 }
545 _ => return None,
546 };
547
548 if target_w == 0 || target_h == 0 {
549 return None;
550 }
551
552 let mut scaled = vec![0u8; (target_w * target_h) as usize];
554 let sx = src_w as f32 / target_w as f32;
555 let sy = src_h as f32 / target_h as f32;
556 for py in 0..target_h {
557 for px in 0..target_w {
558 let mx = ((px as f32 * sx) as u32).min(src_w - 1);
559 let my = ((py as f32 * sy) as u32).min(src_h - 1);
560 scaled[(py * target_w + px) as usize] = mask_bytes[(my * src_w + mx) as usize];
561 }
562 }
563 Some(scaled)
564}
565
566#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] fn apply_mask(pixmap: &mut AzulPixmap, entry: &MaskEntry) {
570 let (snapshot, mask_data, origin_x, origin_y, width, height) = match entry {
571 MaskEntry::ImageMask {
572 snapshot,
573 mask_data,
574 origin_x,
575 origin_y,
576 width,
577 height,
578 } => (
579 snapshot,
580 mask_data.as_slice(),
581 *origin_x,
582 *origin_y,
583 *width,
584 *height,
585 ),
586 MaskEntry::Opacity{ .. } => return,
587 };
588
589 let pw = pixmap.width as i32;
590 let ph = pixmap.height as i32;
591
592 for py in 0..height as i32 {
593 let dy = origin_y + py;
594 if dy < 0 || dy >= ph {
595 continue;
596 }
597 for px in 0..width as i32 {
598 let dx = origin_x + px;
599 if dx < 0 || dx >= pw {
600 continue;
601 }
602
603 let mi = (py as u32 * width + px as u32) as usize;
604 let mask_val = u32::from(mask_data.get(mi).copied().unwrap_or(0));
605
606 let pi = ((dy as u32 * pixmap.width + dx as u32) * 4) as usize;
607 let si = ((py as u32 * width + px as u32) * 4) as usize;
608
609 if pi + 3 >= pixmap.data.len() || si + 3 >= snapshot.len() {
610 continue;
611 }
612
613 let inv_mask = 255 - mask_val;
616 for c in 0..4 {
617 let snap_c = u32::from(snapshot[si + c]);
618 let cur_c = u32::from(pixmap.data[pi + c]);
619 pixmap.data[pi + c] = ((cur_c * mask_val + snap_c * inv_mask) / 255) as u8;
620 }
621 }
622 }
623}
624
625#[derive(Debug, Clone, Copy)]
630pub struct RenderOptions {
631 pub width: f32,
632 pub height: f32,
633 pub dpi_factor: f32,
634}
635
636fn acquire_pixmap(retained: Option<AzulPixmap>, w: u32, h: u32) -> Result<AzulPixmap, String> {
638 if let Some(p) = retained {
639 if p.width == w && p.height == h {
640 return Ok(p);
641 }
642 }
643 AzulPixmap::new(w, h).ok_or_else(|| "cannot create pixmap".to_string())
644}
645
646#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub fn render(
651 dl: &DisplayList,
652 res: &RendererResources,
653 opts: RenderOptions,
654 glyph_cache: &mut GlyphCache,
655) -> Result<AzulPixmap, String> {
656 let RenderOptions {
657 width,
658 height,
659 dpi_factor,
660 } = opts;
661
662 let mut pixmap = acquire_pixmap(
663 None,
664 (width * dpi_factor) as u32,
665 (height * dpi_factor) as u32,
666 )?;
667 pixmap.fill(255, 255, 255, 255);
668
669 render_display_list(dl, &mut pixmap, dpi_factor, res, None, glyph_cache)?;
670
671 Ok(pixmap)
672}
673
674pub fn render_with_font_manager(
680 dl: &DisplayList,
681 res: &RendererResources,
682 font_manager: &FontManager<FontRef>,
683 opts: RenderOptions,
684 glyph_cache: &mut GlyphCache,
685) -> Result<AzulPixmap, String> {
686 let empty_state = CpuRenderState::new(ScrollOffsetMap::new());
687 render_with_font_manager_and_scroll(dl, res, font_manager, opts, glyph_cache, &empty_state)
688}
689
690pub fn render_with_font_manager_and_scroll(
696 dl: &DisplayList,
697 res: &RendererResources,
698 font_manager: &FontManager<FontRef>,
699 opts: RenderOptions,
700 glyph_cache: &mut GlyphCache,
701 render_state: &CpuRenderState,
702) -> Result<AzulPixmap, String> {
703 render_with_font_manager_and_scroll_retained(
704 dl,
705 res,
706 font_manager,
707 opts,
708 glyph_cache,
709 render_state,
710 None,
711 )
712}
713
714#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub fn render_with_font_manager_and_scroll_retained(
722 dl: &DisplayList,
723 res: &RendererResources,
724 font_manager: &FontManager<FontRef>,
725 opts: RenderOptions,
726 glyph_cache: &mut GlyphCache,
727 render_state: &CpuRenderState,
728 retained: Option<AzulPixmap>,
729) -> Result<AzulPixmap, String> {
730 let RenderOptions {
731 width,
732 height,
733 dpi_factor,
734 } = opts;
735
736 let pw = (width * dpi_factor) as u32;
737 let ph = (height * dpi_factor) as u32;
738 let mut pixmap = acquire_pixmap(retained, pw, ph)?;
739 pixmap.fill(255, 255, 255, 255);
740
741 render_display_list_with_state(
742 dl,
743 &mut pixmap,
744 dpi_factor,
745 res,
746 Some(font_manager),
747 glyph_cache,
748 render_state,
749 )?;
750
751 Ok(pixmap)
752}
753
754pub type ScrollOffsetMap = HashMap<LocalScrollId, (f32, f32)>;
758
759#[derive(Debug)]
765pub struct CpuRenderState {
766 pub scroll_offsets: ScrollOffsetMap,
768 pub transforms: HashMap<usize, azul_core::transform::ComputedTransform3D>,
771 pub opacities: HashMap<usize, f32>,
775 pub system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
779 pub virtual_view_display_lists:
785 std::collections::BTreeMap<azul_core::dom::DomId, std::sync::Arc<DisplayList>>,
786 pub image_callback_results:
793 std::collections::BTreeMap<azul_core::resources::ImageRefHash, ImageRef>,
794}
795
796impl CpuRenderState {
797 #[must_use] pub fn new(scroll_offsets: ScrollOffsetMap) -> Self {
798 Self {
799 scroll_offsets,
800 transforms: HashMap::new(),
801 opacities: HashMap::new(),
802 system_style: None,
803 virtual_view_display_lists: std::collections::BTreeMap::new(),
804 image_callback_results: std::collections::BTreeMap::new(),
805 }
806 }
807
808 #[must_use] pub fn with_image_callback_results(
810 mut self,
811 results: std::collections::BTreeMap<
812 azul_core::resources::ImageRefHash,
813 ImageRef,
814 >,
815 ) -> Self {
816 self.image_callback_results = results;
817 self
818 }
819
820 #[must_use] pub fn with_virtual_view_display_lists(
823 mut self,
824 lists: std::collections::BTreeMap<azul_core::dom::DomId, std::sync::Arc<DisplayList>>,
825 ) -> Self {
826 self.virtual_view_display_lists = lists;
827 self
828 }
829
830 #[must_use] pub fn with_system_style(
833 mut self,
834 system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
835 ) -> Self {
836 self.system_style = system_style;
837 self
838 }
839
840 #[must_use] pub fn from_gpu_cache(
842 gpu_cache: Option<&azul_core::gpu::GpuValueCache>,
843 dom_id: azul_core::dom::DomId,
844 scroll_offsets: &ScrollOffsetMap,
845 ) -> Self {
846 let (transforms, opacities) = extract_gpu_values(gpu_cache, dom_id);
847 Self {
848 scroll_offsets: scroll_offsets.clone(),
849 transforms,
850 opacities,
851 system_style: None,
852 virtual_view_display_lists: std::collections::BTreeMap::new(),
853 image_callback_results: std::collections::BTreeMap::new(),
854 }
855 }
856}
857
858#[must_use] pub fn extract_gpu_values(
866 gpu_cache: Option<&azul_core::gpu::GpuValueCache>,
867 dom_id: azul_core::dom::DomId,
868) -> (
869 HashMap<usize, azul_core::transform::ComputedTransform3D>,
870 HashMap<usize, f32>,
871) {
872 {
873 let mut transforms = HashMap::new();
874 let mut opacities = HashMap::new();
875
876 if let Some(cache) = gpu_cache {
877 for (node_id, key) in &cache.transform_keys {
879 if let Some(value) = cache.current_transform_values.get(node_id) {
880 transforms.insert(key.id, *value);
881 }
882 }
883 for (node_id, key) in &cache.h_transform_keys {
885 if let Some(value) = cache.h_current_transform_values.get(node_id) {
886 transforms.insert(key.id, *value);
887 }
888 }
889 for (node_id, key) in &cache.css_transform_keys {
891 if let Some(value) = cache.css_current_transform_values.get(node_id) {
892 transforms.insert(key.id, *value);
893 }
894 }
895 for ((d, node_id), key) in &cache.scrollbar_v_opacity_keys {
897 if *d == dom_id {
898 if let Some(&value) = cache.scrollbar_v_opacity_values.get(&(*d, *node_id)) {
899 opacities.insert(key.id, value);
900 }
901 }
902 }
903 for ((d, node_id), key) in &cache.scrollbar_h_opacity_keys {
905 if *d == dom_id {
906 if let Some(&value) = cache.scrollbar_h_opacity_values.get(&(*d, *node_id)) {
907 opacities.insert(key.id, value);
908 }
909 }
910 }
911 for (node_id, key) in &cache.opacity_keys {
913 if let Some(&value) = cache.current_opacity_values.get(node_id) {
914 opacities.insert(key.id, value);
915 }
916 }
917 }
918
919 (transforms, opacities)
920 }
921}
922
923fn render_display_list(
924 display_list: &DisplayList,
925 pixmap: &mut AzulPixmap,
926 dpi_factor: f32,
927 renderer_resources: &RendererResources,
928 font_manager: Option<&FontManager<FontRef>>,
929 glyph_cache: &mut GlyphCache,
930) -> Result<(), String> {
931 let empty_state = CpuRenderState::new(ScrollOffsetMap::new());
932 render_display_list_with_state(
933 display_list,
934 pixmap,
935 dpi_factor,
936 renderer_resources,
937 font_manager,
938 glyph_cache,
939 &empty_state,
940 )
941}
942
943fn render_display_list_with_state(
944 display_list: &DisplayList,
945 pixmap: &mut AzulPixmap,
946 dpi_factor: f32,
947 renderer_resources: &RendererResources,
948 font_manager: Option<&FontManager<FontRef>>,
949 glyph_cache: &mut GlyphCache,
950 render_state: &CpuRenderState,
951) -> Result<(), String> {
952 let mut transform_stack = vec![TransAffine::new()]; let mut clip_stack: Vec<Option<AzRect>> = vec![None];
954 let mut mask_stack: Vec<MaskEntry> = Vec::new();
955 let mut scroll_offset_stack: Vec<(f32, f32)> = vec![(0.0, 0.0)];
960 let mut text_shadow_stack: Vec<StyleBoxShadow> = Vec::new();
961
962 let _p_loop = crate::probe::Probe::span("raster_loop");
963 for item in &display_list.items {
964 let _p_item = crate::probe::Probe::span(probe_label_for_item(item));
965 render_single_item(
966 item,
967 pixmap,
968 dpi_factor,
969 renderer_resources,
970 font_manager,
971 glyph_cache,
972 &mut transform_stack,
973 &mut clip_stack,
974 &mut mask_stack,
975 &mut scroll_offset_stack,
976 &mut text_shadow_stack,
977 render_state,
978 )?;
979 }
980
981 Ok(())
982}
983
984#[inline]
988const fn probe_label_for_item(item: &DisplayListItem) -> &'static str {
989 use crate::solver3::display_list::DisplayListItem as I;
990 match item {
991 I::Rect { .. } => "dl:rect",
992 I::SelectionRect { .. } => "dl:sel_rect",
993 I::CursorRect { .. } => "dl:cursor",
994 I::Border { .. } => "dl:border",
995 I::Text { .. } => "dl:text",
996 I::TextLayout { .. } => "dl:text_layout",
997 I::Image { .. } => "dl:image",
998 I::ScrollBar { .. } => "dl:scrollbar_raw",
999 I::ScrollBarStyled { .. } => "dl:scrollbar",
1000 I::PushClip { .. } => "dl:push_clip",
1001 I::PopClip => "dl:pop_clip",
1002 I::PushScrollFrame { .. } => "dl:push_scroll",
1003 I::PopScrollFrame => "dl:pop_scroll",
1004 I::PushStackingContext { .. } => "dl:push_stack",
1005 I::PopStackingContext => "dl:pop_stack",
1006 I::PushReferenceFrame { .. } => "dl:push_ref",
1007 I::PopReferenceFrame => "dl:pop_ref",
1008 I::PushOpacity { .. } => "dl:push_opacity",
1009 I::PopOpacity => "dl:pop_opacity",
1010 I::PushFilter { .. } => "dl:push_filter",
1011 I::PopFilter => "dl:pop_filter",
1012 I::PushBackdropFilter { .. } => "dl:push_bdfilter",
1013 I::PopBackdropFilter => "dl:pop_bdfilter",
1014 I::PushTextShadow { .. } => "dl:push_tshadow",
1015 I::PopTextShadow => "dl:pop_tshadow",
1016 I::PushImageMaskClip { .. } => "dl:push_imask",
1017 I::PopImageMaskClip => "dl:pop_imask",
1018 I::LinearGradient { .. } => "dl:linear_grad",
1019 I::RadialGradient { .. } => "dl:radial_grad",
1020 I::ConicGradient { .. } => "dl:conic_grad",
1021 I::BoxShadow { .. } => "dl:box_shadow",
1022 I::Underline { .. } => "dl:underline",
1023 I::Strikethrough { .. } => "dl:strike",
1024 I::Overline { .. } => "dl:overline",
1025 I::HitTestArea { .. } => "dl:hit",
1026 I::VirtualView { .. } => "dl:vview",
1027 I::VirtualViewPlaceholder { .. } => "dl:vview_ph",
1028 }
1029}
1030
1031#[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(
1050 display_list: &DisplayList,
1051 pixmap: &mut AzulPixmap,
1052 dpi_factor: f32,
1053 renderer_resources: &RendererResources,
1054 font_manager: Option<&FontManager<FontRef>>,
1055 glyph_cache: &mut GlyphCache,
1056 render_state: &CpuRenderState,
1057 damage_rects: &[LogicalRect],
1058) -> Result<(), String> {
1059 struct SnappedRect {
1063 x0: i32,
1064 y0: i32,
1065 x1: i32,
1066 y1: i32,
1067 logical: LogicalRect,
1068 }
1069
1070 if damage_rects.is_empty() {
1071 return Ok(()); }
1073
1074 let pw_i = pixmap.width() as i32;
1082 let ph_i = pixmap.height() as i32;
1083 let snap_out = |dr: &LogicalRect| -> Option<SnappedRect> {
1084 let x0 = ((dr.origin.x * dpi_factor).floor() as i32).clamp(0, pw_i);
1085 let y0 = ((dr.origin.y * dpi_factor).floor() as i32).clamp(0, ph_i);
1086 let x1 = (((dr.origin.x + dr.size.width) * dpi_factor).ceil() as i32).clamp(0, pw_i);
1087 let y1 = (((dr.origin.y + dr.size.height) * dpi_factor).ceil() as i32).clamp(0, ph_i);
1088 if x1 <= x0 || y1 <= y0 {
1089 return None;
1090 }
1091 Some(SnappedRect {
1092 x0,
1093 y0,
1094 x1,
1095 y1,
1096 logical: LogicalRect {
1097 origin: LogicalPosition {
1098 x: x0 as f32 / dpi_factor,
1099 y: y0 as f32 / dpi_factor,
1100 },
1101 size: LogicalSize {
1102 width: (x1 - x0) as f32 / dpi_factor,
1103 height: (y1 - y0) as f32 / dpi_factor,
1104 },
1105 },
1106 })
1107 };
1108 let mut rects: Vec<SnappedRect> = damage_rects.iter().filter_map(snap_out).collect();
1109
1110 let mut i = 0;
1116 while i < rects.len() {
1117 let mut j = i + 1;
1118 let mut merged_any = false;
1119 while j < rects.len() {
1120 let (a, b) = (&rects[i], &rects[j]);
1121 let overlap = a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;
1122 if overlap {
1123 let x0 = a.x0.min(b.x0);
1124 let y0 = a.y0.min(b.y0);
1125 let x1 = a.x1.max(b.x1);
1126 let y1 = a.y1.max(b.y1);
1127 rects[i] = SnappedRect {
1128 x0,
1129 y0,
1130 x1,
1131 y1,
1132 logical: LogicalRect {
1133 origin: LogicalPosition {
1134 x: x0 as f32 / dpi_factor,
1135 y: y0 as f32 / dpi_factor,
1136 },
1137 size: LogicalSize {
1138 width: (x1 - x0) as f32 / dpi_factor,
1139 height: (y1 - y0) as f32 / dpi_factor,
1140 },
1141 },
1142 };
1143 rects.swap_remove(j);
1144 merged_any = true;
1145 } else {
1148 j += 1;
1149 }
1150 }
1151 if merged_any {
1152 if rects.len() > 1 {
1154 continue;
1155 }
1156 }
1157 i += 1;
1158 }
1159
1160 for sr in &rects {
1171 pixmap.fill_rect(
1172 sr.x0,
1173 sr.y0,
1174 sr.x1 - sr.x0,
1175 sr.y1 - sr.y0,
1176 255,
1177 255,
1178 255,
1179 255,
1180 );
1181
1182 let base_clip = AzRect::from_xywh(
1183 sr.x0 as f32,
1184 sr.y0 as f32,
1185 (sr.x1 - sr.x0) as f32,
1186 (sr.y1 - sr.y0) as f32,
1187 );
1188 let mut transform_stack = vec![TransAffine::new()];
1189 let mut clip_stack: Vec<Option<AzRect>> = vec![base_clip];
1190 let mut mask_stack: Vec<MaskEntry> = Vec::new();
1191 let mut scroll_offset_stack: Vec<(f32, f32)> = vec![(0.0, 0.0)];
1192 let mut text_shadow_stack: Vec<StyleBoxShadow> = Vec::new();
1193
1194 for item in &display_list.items {
1195 if !item.is_state_management() {
1198 if let Some(item_bounds) = item.bounds() {
1199 let (sdx, sdy) = *scroll_offset_stack.last().unwrap_or(&(0.0, 0.0));
1206 let test_bounds = if sdx == 0.0 && sdy == 0.0 {
1207 item_bounds
1208 } else {
1209 LogicalRect {
1210 origin: LogicalPosition {
1211 x: item_bounds.origin.x - sdx,
1212 y: item_bounds.origin.y - sdy,
1213 },
1214 size: item_bounds.size,
1215 }
1216 };
1217 if !rects_overlap_or_adjacent(&test_bounds, &sr.logical, 0.0) {
1218 continue;
1219 }
1220 }
1221 }
1222
1223 render_single_item(
1224 item,
1225 pixmap,
1226 dpi_factor,
1227 renderer_resources,
1228 font_manager,
1229 glyph_cache,
1230 &mut transform_stack,
1231 &mut clip_stack,
1232 &mut mask_stack,
1233 &mut scroll_offset_stack,
1234 &mut text_shadow_stack,
1235 render_state,
1236 )?;
1237 }
1238 }
1239
1240 Ok(())
1241}
1242
1243#[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(
1255 item: &DisplayListItem,
1256 pixmap: &mut AzulPixmap,
1257 dpi_factor: f32,
1258 renderer_resources: &RendererResources,
1259 font_manager: Option<&FontManager<FontRef>>,
1260 glyph_cache: &mut GlyphCache,
1261 transform_stack: &mut Vec<TransAffine>,
1262 clip_stack: &mut Vec<Option<AzRect>>,
1263 mask_stack: &mut Vec<MaskEntry>,
1264 scroll_offset_stack: &mut Vec<(f32, f32)>,
1265 text_shadow_stack: &mut Vec<StyleBoxShadow>,
1266 render_state: &CpuRenderState,
1267) -> Result<(), String> {
1268 use azul_css::props::style::border::BorderStyle;
1269 let (scroll_dx, scroll_dy) = *scroll_offset_stack.last().unwrap_or(&(0.0, 0.0));
1272
1273 let scroll_rect = |r: &LogicalRect| -> LogicalRect {
1278 if scroll_dx == 0.0 && scroll_dy == 0.0 {
1279 return *r;
1280 }
1281 LogicalRect {
1282 origin: LogicalPosition {
1283 x: r.origin.x - scroll_dx,
1284 y: r.origin.y - scroll_dy,
1285 },
1286 size: r.size,
1287 }
1288 };
1289
1290 match item {
1291 DisplayListItem::Rect {
1292 bounds,
1293 color,
1294 border_radius,
1295 } => {
1296 let clip = *clip_stack.last().unwrap();
1297 render_rect(
1298 pixmap,
1299 &scroll_rect(bounds.inner()),
1300 *color,
1301 border_radius,
1302 clip,
1303 dpi_factor,
1304 );
1305 }
1306 DisplayListItem::SelectionRect {
1307 bounds,
1308 color,
1309 border_radius,
1310 } => {
1311 let clip = *clip_stack.last().unwrap();
1312 render_rect(
1313 pixmap,
1314 &scroll_rect(bounds.inner()),
1315 *color,
1316 border_radius,
1317 clip,
1318 dpi_factor,
1319 );
1320 }
1321 DisplayListItem::CursorRect { bounds, color } => {
1322 let clip = *clip_stack.last().unwrap();
1323 render_rect(
1324 pixmap,
1325 &scroll_rect(bounds.inner()),
1326 *color,
1327 &BorderRadius::default(),
1328 clip,
1329 dpi_factor,
1330 );
1331 }
1332 DisplayListItem::Border {
1333 bounds,
1334 widths,
1335 colors,
1336 styles,
1337 border_radius,
1338 } => {
1339 let default_color = ColorU {
1340 r: 0,
1341 g: 0,
1342 b: 0,
1343 a: 255,
1344 };
1345
1346 let w_top = widths
1347 .top
1348 .and_then(|w| w.get_property().copied())
1349 .map_or(0.0, |w| {
1350 w.inner
1351 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
1352 });
1353 let w_right = widths
1354 .right
1355 .and_then(|w| w.get_property().copied())
1356 .map_or(0.0, |w| {
1357 w.inner
1358 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
1359 });
1360 let w_bottom = widths
1361 .bottom
1362 .and_then(|w| w.get_property().copied())
1363 .map_or(0.0, |w| {
1364 w.inner
1365 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
1366 });
1367 let w_left = widths
1368 .left
1369 .and_then(|w| w.get_property().copied())
1370 .map_or(0.0, |w| {
1371 w.inner
1372 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
1373 });
1374
1375 let c_top = colors
1376 .top
1377 .and_then(|c| c.get_property().copied())
1378 .map_or(default_color, |c| c.inner);
1379 let c_right = colors
1380 .right
1381 .and_then(|c| c.get_property().copied())
1382 .map_or(default_color, |c| c.inner);
1383 let c_bottom = colors
1384 .bottom
1385 .and_then(|c| c.get_property().copied())
1386 .map_or(default_color, |c| c.inner);
1387 let c_left = colors
1388 .left
1389 .and_then(|c| c.get_property().copied())
1390 .map_or(default_color, |c| c.inner);
1391
1392 let s_top = styles
1393 .top
1394 .and_then(|s| s.get_property().copied())
1395 .map_or(BorderStyle::Solid, |s| s.inner);
1396 let s_right = styles
1397 .right
1398 .and_then(|s| s.get_property().copied())
1399 .map_or(BorderStyle::Solid, |s| s.inner);
1400 let s_bottom = styles
1401 .bottom
1402 .and_then(|s| s.get_property().copied())
1403 .map_or(BorderStyle::Solid, |s| s.inner);
1404 let s_left = styles
1405 .left
1406 .and_then(|s| s.get_property().copied())
1407 .map_or(BorderStyle::Solid, |s| s.inner);
1408
1409 let simple_radius = BorderRadius {
1410 top_left: border_radius.top_left.to_pixels_internal(
1411 bounds.0.size.width,
1412 DEFAULT_FONT_SIZE,
1413 DEFAULT_FONT_SIZE,
1414 ),
1415 top_right: border_radius.top_right.to_pixels_internal(
1416 bounds.0.size.width,
1417 DEFAULT_FONT_SIZE,
1418 DEFAULT_FONT_SIZE,
1419 ),
1420 bottom_left: border_radius.bottom_left.to_pixels_internal(
1421 bounds.0.size.width,
1422 DEFAULT_FONT_SIZE,
1423 DEFAULT_FONT_SIZE,
1424 ),
1425 bottom_right: border_radius.bottom_right.to_pixels_internal(
1426 bounds.0.size.width,
1427 DEFAULT_FONT_SIZE,
1428 DEFAULT_FONT_SIZE,
1429 ),
1430 };
1431
1432 let clip = *clip_stack.last().unwrap();
1433 let b = scroll_rect(bounds.inner());
1434
1435 let all_same = c_top == c_right
1437 && c_top == c_bottom
1438 && c_top == c_left
1439 && w_top == w_right
1440 && w_top == w_bottom
1441 && w_top == w_left
1442 && s_top == s_right
1443 && s_top == s_bottom
1444 && s_top == s_left;
1445
1446 if all_same {
1447 render_border(
1448 pixmap,
1449 &b,
1450 c_top,
1451 w_top,
1452 s_top,
1453 &simple_radius,
1454 clip,
1455 dpi_factor,
1456 );
1457 } else {
1458 render_border_sides(
1460 pixmap,
1461 &b,
1462 [c_top, c_right, c_bottom, c_left],
1463 [w_top, w_right, w_bottom, w_left],
1464 [s_top, s_right, s_bottom, s_left],
1465 &simple_radius,
1466 clip,
1467 dpi_factor,
1468 );
1469 }
1470 }
1471 DisplayListItem::Underline {
1472 bounds,
1473 color,
1474 thickness: _,
1475 } => {
1476 let clip = *clip_stack.last().unwrap();
1477 render_rect(
1478 pixmap,
1479 &scroll_rect(bounds.inner()),
1480 *color,
1481 &BorderRadius::default(),
1482 clip,
1483 dpi_factor,
1484 );
1485 }
1486 DisplayListItem::Strikethrough {
1487 bounds,
1488 color,
1489 thickness: _,
1490 } => {
1491 let clip = *clip_stack.last().unwrap();
1492 render_rect(
1493 pixmap,
1494 &scroll_rect(bounds.inner()),
1495 *color,
1496 &BorderRadius::default(),
1497 clip,
1498 dpi_factor,
1499 );
1500 }
1501 DisplayListItem::Overline {
1502 bounds,
1503 color,
1504 thickness: _,
1505 } => {
1506 let clip = *clip_stack.last().unwrap();
1507 render_rect(
1508 pixmap,
1509 &scroll_rect(bounds.inner()),
1510 *color,
1511 &BorderRadius::default(),
1512 clip,
1513 dpi_factor,
1514 );
1515 }
1516 DisplayListItem::Text {
1517 glyphs,
1518 font_size_px,
1519 font_hash,
1520 color,
1521 clip_rect,
1522 ..
1523 } => {
1524 let clip = *clip_stack.last().unwrap();
1525 let text_clip = scroll_rect(clip_rect.inner());
1526 for shadow in text_shadow_stack.iter() {
1531 render_text_shadow(
1532 shadow,
1533 glyphs,
1534 *font_hash,
1535 *font_size_px,
1536 pixmap,
1537 &text_clip,
1538 clip,
1539 renderer_resources,
1540 font_manager,
1541 dpi_factor,
1542 glyph_cache,
1543 (scroll_dx, scroll_dy),
1544 );
1545 }
1546 render_text(
1547 glyphs,
1548 *font_hash,
1549 *font_size_px,
1550 *color,
1551 pixmap,
1552 &text_clip,
1553 clip,
1554 renderer_resources,
1555 font_manager,
1556 dpi_factor,
1557 glyph_cache,
1558 (scroll_dx, scroll_dy),
1559 false,
1560 );
1561 }
1562 DisplayListItem::TextLayout {
1563 layout,
1564 bounds,
1565 font_hash,
1566 font_size_px,
1567 color,
1568 } => {
1569 }
1571 DisplayListItem::Image { bounds, image, .. } => {
1572 let clip = *clip_stack.last().unwrap();
1573 let resolved = render_state.image_callback_results.get(&image.get_hash());
1579 render_image(
1580 pixmap,
1581 &scroll_rect(bounds.inner()),
1582 resolved.unwrap_or(image),
1583 clip,
1584 dpi_factor,
1585 );
1586 }
1587 DisplayListItem::ScrollBar {
1588 bounds,
1589 color,
1590 orientation,
1591 opacity_key: _,
1592 hit_id: _,
1593 } => {
1594 let clip = *clip_stack.last().unwrap();
1595 render_rect(
1596 pixmap,
1597 &scroll_rect(bounds.inner()),
1598 *color,
1599 &BorderRadius::default(),
1600 clip,
1601 dpi_factor,
1602 );
1603 }
1604 DisplayListItem::ScrollBarStyled { info } => {
1605 let clip = *clip_stack.last().unwrap();
1606
1607 let scrollbar_opacity = info
1613 .opacity_key
1614 .and_then(|key| render_state.opacities.get(&key.id).copied())
1615 .unwrap_or(1.0);
1616
1617 if scrollbar_opacity > 0.001 {
1618 if info.track_color.a > 0 {
1620 render_rect(
1621 pixmap,
1622 &scroll_rect(info.track_bounds.inner()),
1623 info.track_color,
1624 &BorderRadius::default(),
1625 clip,
1626 dpi_factor,
1627 );
1628 }
1629
1630 if let Some(btn_bounds) = &info.button_decrement_bounds {
1632 if info.button_color.a > 0 {
1633 render_rect(
1634 pixmap,
1635 &scroll_rect(btn_bounds.inner()),
1636 info.button_color,
1637 &BorderRadius::default(),
1638 clip,
1639 dpi_factor,
1640 );
1641 }
1642 }
1643
1644 if let Some(btn_bounds) = &info.button_increment_bounds {
1646 if info.button_color.a > 0 {
1647 render_rect(
1648 pixmap,
1649 &scroll_rect(btn_bounds.inner()),
1650 info.button_color,
1651 &BorderRadius::default(),
1652 clip,
1653 dpi_factor,
1654 );
1655 }
1656 }
1657
1658 if info.thumb_color.a > 0 {
1663 let thumb_rect = info.thumb_bounds.inner();
1664 let transform = info
1666 .thumb_transform_key
1667 .and_then(|key| render_state.transforms.get(&key.id))
1668 .unwrap_or(&info.thumb_initial_transform);
1669 let tx = transform.m[3][0];
1670 let ty = transform.m[3][1];
1671 let transformed_thumb = LogicalRect {
1672 origin: LogicalPosition {
1673 x: thumb_rect.origin.x + tx,
1674 y: thumb_rect.origin.y + ty,
1675 },
1676 size: thumb_rect.size,
1677 };
1678 render_rect(
1679 pixmap,
1680 &scroll_rect(&transformed_thumb),
1681 info.thumb_color,
1682 &info.thumb_border_radius,
1683 clip,
1684 dpi_factor,
1685 );
1686 }
1687 } }
1689 DisplayListItem::PushClip {
1690 bounds,
1691 border_radius,
1692 } => {
1693 let new_clip = logical_rect_to_az_rect(&scroll_rect(bounds.inner()), dpi_factor);
1703 let merged = intersect_clips(clip_stack.last().copied().flatten(), new_clip);
1704 clip_stack.push(merged);
1705 }
1706 DisplayListItem::PopClip => {
1707 if clip_stack.len() > 1 {
1717 clip_stack.pop();
1718 } else {
1719 #[cfg(feature = "std")]
1720 if std::env::var("AZ_CLIP_DEBUG").is_ok() {
1721 eprintln!(
1722 "[CpuBackend] PopClip with no matching PushClip — clamping to base clip"
1723 );
1724 }
1725 }
1726 }
1727 DisplayListItem::PushScrollFrame { scroll_id, .. } => {
1728 transform_stack.push(
1733 transform_stack
1734 .last()
1735 .copied()
1736 .unwrap_or_else(TransAffine::new),
1737 );
1738 let frame_offset = render_state
1739 .scroll_offsets
1740 .get(scroll_id)
1741 .copied()
1742 .unwrap_or((0.0, 0.0));
1743 let new_scroll = (scroll_dx + frame_offset.0, scroll_dy + frame_offset.1);
1744 scroll_offset_stack.push(new_scroll);
1745 }
1746 DisplayListItem::PopScrollFrame => {
1747 if transform_stack.len() > 1 {
1750 transform_stack.pop();
1751 }
1752 if scroll_offset_stack.len() > 1 {
1753 scroll_offset_stack.pop();
1754 }
1755 }
1756 DisplayListItem::HitTestArea { bounds, tag } => {
1757 }
1759 DisplayListItem::PushStackingContext { z_index, bounds } => {
1760 }
1762 DisplayListItem::PopStackingContext => {}
1763 DisplayListItem::VirtualView {
1764 child_dom_id,
1765 bounds,
1766 clip_rect,
1767 } => {
1768 let _ = clip_rect;
1769 let child_dl = render_state.virtual_view_display_lists.get(child_dom_id).cloned();
1778 #[cfg(feature = "std")]
1779 if std::env::var("AZ_MAP_DEBUG").is_ok() {
1780 eprintln!(
1781 "[cpu-vview] VirtualView item: child_dom_id={} found={} items={} bounds={:?} avail_ids={:?}",
1782 child_dom_id.inner,
1783 child_dl.is_some(),
1784 child_dl.as_ref().map_or(0, |d| d.items.len()),
1785 bounds.inner(),
1786 render_state.virtual_view_display_lists.keys().map(|k| k.inner).collect::<Vec<_>>(),
1787 );
1788 }
1789 if let Some(child_dl) = child_dl {
1790 let vv_origin = bounds.inner().origin;
1791 let vv_clip = intersect_clips(
1794 clip_stack.last().copied().flatten(),
1795 logical_rect_to_az_rect(&scroll_rect(bounds.inner()), dpi_factor),
1796 );
1797 clip_stack.push(vv_clip);
1798 scroll_offset_stack.push((scroll_dx - vv_origin.x, scroll_dy - vv_origin.y));
1799 for child_item in &child_dl.items {
1800 render_single_item(
1801 child_item,
1802 pixmap,
1803 dpi_factor,
1804 renderer_resources,
1805 font_manager,
1806 glyph_cache,
1807 transform_stack,
1808 clip_stack,
1809 mask_stack,
1810 scroll_offset_stack,
1811 text_shadow_stack,
1812 render_state,
1813 )?;
1814 }
1815 scroll_offset_stack.pop();
1816 clip_stack.pop();
1817 }
1818 }
1819 DisplayListItem::VirtualViewPlaceholder { .. } => {
1820 #[cfg(feature = "std")]
1821 if std::env::var("AZ_MAP_DEBUG").is_ok() {
1822 eprintln!("[cpu-vview] VirtualViewPlaceholder hit (NOT swapped to a VirtualView item — nothing composites)");
1823 }
1824 }
1825
1826 DisplayListItem::LinearGradient {
1828 bounds,
1829 gradient,
1830 border_radius,
1831 } => {
1832 let clip = *clip_stack.last().unwrap();
1833 render_linear_gradient(
1834 pixmap,
1835 &scroll_rect(bounds.inner()),
1836 gradient,
1837 border_radius,
1838 clip,
1839 dpi_factor,
1840 render_state.system_style.as_deref().map(|s| &s.colors),
1841 );
1842 }
1843 DisplayListItem::RadialGradient {
1844 bounds,
1845 gradient,
1846 border_radius,
1847 } => {
1848 let clip = *clip_stack.last().unwrap();
1849 render_radial_gradient(
1850 pixmap,
1851 &scroll_rect(bounds.inner()),
1852 gradient,
1853 border_radius,
1854 clip,
1855 dpi_factor,
1856 render_state.system_style.as_deref().map(|s| &s.colors),
1857 );
1858 }
1859 DisplayListItem::ConicGradient {
1860 bounds,
1861 gradient,
1862 border_radius,
1863 } => {
1864 let clip = *clip_stack.last().unwrap();
1865 render_conic_gradient(
1866 pixmap,
1867 &scroll_rect(bounds.inner()),
1868 gradient,
1869 border_radius,
1870 clip,
1871 dpi_factor,
1872 render_state.system_style.as_deref().map(|s| &s.colors),
1873 );
1874 }
1875
1876 DisplayListItem::BoxShadow {
1878 bounds,
1879 shadow,
1880 border_radius,
1881 } => {
1882 render_box_shadow(
1883 pixmap,
1884 &scroll_rect(bounds.inner()),
1885 shadow,
1886 border_radius,
1887 dpi_factor,
1888 )?;
1889 }
1890
1891 DisplayListItem::PushOpacity { bounds, opacity } => {
1893 let rect = logical_rect_to_az_rect(&scroll_rect(bounds.inner()), dpi_factor);
1894 if let Some(r) = rect {
1895 let snap = snapshot_region(
1896 pixmap,
1897 r.x as i32,
1898 r.y as i32,
1899 r.width as u32,
1900 r.height as u32,
1901 );
1902 mask_stack.push(MaskEntry::Opacity {
1903 snapshot: snap,
1904 rect: r,
1905 opacity: *opacity,
1906 });
1907 }
1908 }
1909 DisplayListItem::PopOpacity => {
1910 if let Some(MaskEntry::Opacity {
1911 snapshot,
1912 rect,
1913 opacity,
1914 }) = mask_stack.pop()
1915 {
1916 let x = rect.x as i32;
1917 let y = rect.y as i32;
1918 let w = rect.width as u32;
1919 let h = rect.height as u32;
1920 let pw = pixmap.width as i32;
1921 let ph = pixmap.height as i32;
1922 for py in 0..h as i32 {
1924 let dy = y + py;
1925 if dy < 0 || dy >= ph {
1926 continue;
1927 }
1928 for px in 0..w as i32 {
1929 let dx = x + px;
1930 if dx < 0 || dx >= pw {
1931 continue;
1932 }
1933 let pi = ((dy as u32 * pixmap.width + dx as u32) * 4) as usize;
1934 let si = ((py as u32 * w + px as u32) * 4) as usize;
1935 if pi + 3 >= pixmap.data.len() || si + 3 >= snapshot.len() {
1936 continue;
1937 }
1938 let op = (opacity * 255.0).clamp(0.0, 255.0) as u32;
1939 let inv_op = 255 - op;
1940 for c in 0..4 {
1941 let snap_c = u32::from(snapshot[si + c]);
1942 let cur_c = u32::from(pixmap.data[pi + c]);
1943 pixmap.data[pi + c] = ((cur_c * op + snap_c * inv_op) / 255) as u8;
1944 }
1945 }
1946 }
1947 }
1948 }
1949
1950 DisplayListItem::PushReferenceFrame {
1952 transform_key,
1953 initial_transform,
1954 bounds,
1955 } => {
1956 let live_transform = render_state.transforms.get(&transform_key.id);
1961 let m = live_transform.map_or(&initial_transform.m, |t| &t.m);
1962 let tf = TransAffine::new_custom(
1963 f64::from(m[0][0]),
1964 f64::from(m[0][1]), f64::from(m[1][0]),
1966 f64::from(m[1][1]), f64::from(m[3][0]),
1968 f64::from(m[3][1]), );
1970 let current = transform_stack
1971 .last()
1972 .copied()
1973 .unwrap_or_else(TransAffine::new);
1974 let mut composed = tf;
1975 composed.premultiply(¤t);
1976 transform_stack.push(composed);
1977 }
1978 DisplayListItem::PopReferenceFrame => {
1979 if transform_stack.len() > 1 {
1980 transform_stack.pop();
1981 }
1982 }
1983
1984 DisplayListItem::PushFilter { .. } => {}
1994 DisplayListItem::PopFilter => {}
1995
1996 DisplayListItem::PushBackdropFilter { .. } => {}
2011 DisplayListItem::PopBackdropFilter => {}
2012
2013 DisplayListItem::PushTextShadow { shadow } => {
2020 text_shadow_stack.push(*shadow);
2021 }
2022 DisplayListItem::PopTextShadow => {
2023 text_shadow_stack.pop();
2024 }
2025
2026 DisplayListItem::PushImageMaskClip {
2027 bounds,
2028 mask_image,
2029 mask_rect,
2030 } => {
2031 let mr = &scroll_rect(mask_rect.inner());
2032 let px_x = (mr.origin.x * dpi_factor) as i32;
2033 let px_y = (mr.origin.y * dpi_factor) as i32;
2034 let px_w = (mr.size.width * dpi_factor).ceil() as u32;
2035 let px_h = (mr.size.height * dpi_factor).ceil() as u32;
2036
2037 if px_w > 0 && px_h > 0 {
2038 let snapshot = snapshot_region(pixmap, px_x, px_y, px_w, px_h);
2039 let mask_data = extract_mask_data(mask_image, px_w, px_h)
2040 .unwrap_or_else(|| vec![255u8; (px_w * px_h) as usize]);
2041 mask_stack.push(MaskEntry::ImageMask {
2042 snapshot,
2043 mask_data,
2044 origin_x: px_x,
2045 origin_y: px_y,
2046 width: px_w,
2047 height: px_h,
2048 });
2049 }
2050 }
2051 DisplayListItem::PopImageMaskClip => {
2052 if let Some(entry) = mask_stack.pop() {
2053 apply_mask(pixmap, &entry);
2054 }
2055 }
2056 }
2057
2058 Ok(())
2059}
2060
2061#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] fn render_rect(
2063 pixmap: &mut AzulPixmap,
2064 bounds: &LogicalRect,
2065 color: ColorU,
2066 border_radius: &BorderRadius,
2067 clip: Option<AzRect>,
2068 dpi_factor: f32,
2069) {
2070 if color.a == 0 {
2071 return;
2072 }
2073
2074 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
2075 return;
2076 };
2077
2078 if let Some(ref c) = clip {
2080 if rect.clip(c).is_none() {
2081 return;
2082 }
2083 }
2084
2085 let agg_color = Rgba8::new(
2086 u32::from(color.r),
2087 u32::from(color.g),
2088 u32::from(color.b),
2089 u32::from(color.a),
2090 );
2091
2092 if border_radius.is_zero() {
2093 let w = pixmap.width;
2097 let h = pixmap.height;
2098 let stride = (w * 4) as i32;
2099 let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
2100 let mut pf = PixfmtRgba32::new(&mut ra);
2101 let mut rb = RendererBase::new(pf);
2102 if let Some(c) = clip {
2103 rb.clip_box_i(
2104 c.x as i32,
2105 c.y as i32,
2106 (c.x + c.width) as i32 - 1,
2107 (c.y + c.height) as i32 - 1,
2108 );
2109 }
2110 rb.blend_bar(
2111 rect.x as i32,
2112 rect.y as i32,
2113 (rect.x + rect.width) as i32 - 1,
2114 (rect.y + rect.height) as i32 - 1,
2115 &agg_color,
2116 255, );
2118 } else {
2119 let mut path = build_rounded_rect_path(&rect, border_radius, dpi_factor);
2121 agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::NonZero, clip);
2122 }
2123
2124}
2125
2126pub const TEXT_LCD_DEFAULT: bool = true;
2136
2137fn text_lcd_enabled() -> bool {
2140 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2141 *V.get_or_init(|| {
2142 std::env::var("AZ_TEXT_LCD")
2143 .map(|s| !(s == "0" || s.eq_ignore_ascii_case("false")))
2144 .unwrap_or(TEXT_LCD_DEFAULT)
2145 })
2146}
2147
2148#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] #[allow(clippy::too_many_arguments)] fn render_glyphs_lcd(
2172 pixmap: &mut AzulPixmap,
2173 clip: Option<AzRect>,
2174 glyphs: &[GlyphInstance],
2175 parsed_font: &ParsedFont,
2176 font_hash: FontHash,
2177 ppem: u16,
2178 scale: f32,
2179 hint_correction: f32,
2180 color: ColorU,
2181 dpi_factor: f32,
2182 scroll_offset: (f32, f32),
2183 glyph_cache: &mut GlyphCache,
2184) {
2185 use agg_rust::pixfmt_lcd::{LcdDistributionLut, PixfmtRgba32Lcd};
2186
2187 let agg_color = Rgba8::new(
2188 u32::from(color.r),
2189 u32::from(color.g),
2190 u32::from(color.b),
2191 u32::from(color.a),
2192 );
2193 let subpx = crate::glyph_cache::text_subpixel_enabled();
2194
2195 let mut ras = RasterizerScanlineAa::new();
2198 ras.filling_rule(FillingRule::NonZero);
2199
2200 for glyph in glyphs {
2201 let glyph_index = glyph.index as u16;
2202 let Some(glyph_data) = parsed_font.get_or_decode_glyph(glyph_index) else {
2203 continue;
2204 };
2205 let Some(cached) = glyph_cache.get_or_build(
2206 font_hash.font_hash,
2207 glyph_index,
2208 &glyph_data,
2209 parsed_font,
2210 ppem,
2211 ) else {
2212 continue;
2213 };
2214 let is_hinted = cached.is_hinted;
2215
2216 let glyph_x = (glyph.point.x - scroll_offset.0) * dpi_factor;
2217 let glyph_baseline_y = (glyph.point.y - scroll_offset.1) * dpi_factor;
2218 let px = if subpx { glyph_x } else { glyph_x.round() };
2221 let py = glyph_baseline_y.round();
2222
2223 let rescale_hinted = is_hinted && (hint_correction - 1.0).abs() > 1e-4;
2228 let path_scale = if is_hinted {
2229 if rescale_hinted { f64::from(hint_correction) } else { 1.0 }
2230 } else {
2231 f64::from(scale)
2232 };
2233
2234 let mut transform = TransAffine::new_scaling(3.0 * path_scale, path_scale);
2239 transform.multiply(&TransAffine::new_translation(3.0 * f64::from(px), f64::from(py)));
2240 ras.add_path_vertices_transformed(cached.path.vertices(), &transform);
2241 }
2242
2243 let w = pixmap.width;
2247 let h = pixmap.height;
2248 let stride = (w * 4) as i32;
2249 let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
2250 let lut = LcdDistributionLut::new(f64::from(0x56u32), f64::from(0x4Du32), f64::from(0x08u32));
2253 let pf = PixfmtRgba32Lcd::new(&mut ra, &lut);
2254 let mut rb = RendererBase::new(pf);
2255 if let Some(c) = clip {
2256 rb.clip_box_i(
2257 (c.x as i32) * 3,
2258 c.y as i32,
2259 ((c.x + c.width) as i32) * 3 - 1,
2260 (c.y + c.height) as i32 - 1,
2261 );
2262 }
2263 let mut sl = ScanlineU8::new();
2264 render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &agg_color);
2265}
2266
2267#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines)] fn render_text(
2270 glyphs: &[GlyphInstance],
2271 font_hash: FontHash,
2272 font_size_px: f32,
2273 color: ColorU,
2274 pixmap: &mut AzulPixmap,
2275 clip_rect: &LogicalRect,
2276 clip: Option<AzRect>,
2277 renderer_resources: &RendererResources,
2278 font_manager: Option<&FontManager<FontRef>>,
2279 dpi_factor: f32,
2280 glyph_cache: &mut GlyphCache,
2281 scroll_offset: (f32, f32),
2282 force_grayscale: bool,
2287) {
2288 if color.a == 0 || glyphs.is_empty() {
2289 return;
2290 }
2291
2292 if let Some(ref c) = clip {
2294 let Some(text_rect) = logical_rect_to_az_rect(clip_rect, dpi_factor) else {
2295 return;
2296 };
2297 if text_rect.clip(c).is_none() {
2298 return; }
2300 }
2301
2302 let agg_color = Rgba8::new(
2303 u32::from(color.r),
2304 u32::from(color.g),
2305 u32::from(color.b),
2306 u32::from(color.a),
2307 );
2308
2309 let parsed_font: &ParsedFont = if let Some(fm) = font_manager {
2311 if let Some(font_ref) = fm.get_font_by_hash(font_hash.font_hash) { unsafe { &*font_ref.get_parsed().cast::<ParsedFont>() } } else {
2312 eprintln!(
2313 "[cpurender] Font hash {} not found in FontManager",
2314 font_hash.font_hash
2315 );
2316 return;
2317 }
2318 } else {
2319 let Some(font_key) = renderer_resources.font_hash_map.get(&font_hash.font_hash) else {
2320 eprintln!(
2321 "[cpurender] Font hash {} not found in font_hash_map (available: {:?})",
2322 font_hash.font_hash,
2323 renderer_resources.font_hash_map.keys().collect::<Vec<_>>()
2324 );
2325 return;
2326 };
2327
2328 let Some((font_ref, _instances)) = renderer_resources.currently_registered_fonts.get(font_key) else {
2329 eprintln!(
2330 "[cpurender] FontKey {font_key:?} not found in currently_registered_fonts"
2331 );
2332 return;
2333 };
2334
2335 unsafe { &*font_ref.get_parsed().cast::<ParsedFont>() }
2336 };
2337
2338 let units_per_em = f32::from(parsed_font.font_metrics.units_per_em);
2339 if units_per_em <= 0.0 {
2340 return;
2341 }
2342
2343 let effective_px = font_size_px * dpi_factor;
2344 let scale = effective_px / units_per_em;
2345 let ppem = effective_px.round() as u16;
2346 let hint_correction = if ppem > 0 { effective_px / f32::from(ppem) } else { 1.0 };
2350
2351 if text_lcd_enabled() && !force_grayscale {
2355 render_glyphs_lcd(
2356 pixmap, clip, glyphs, parsed_font, font_hash, ppem, scale,
2357 hint_correction, color, dpi_factor, scroll_offset, glyph_cache,
2358 );
2359 return;
2360 }
2361
2362 let w = pixmap.width;
2364 let h = pixmap.height;
2365 let stride = (w * 4) as i32;
2366
2367 let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
2370 let mut pf = PixfmtRgba32::new(&mut ra);
2371 let mut rb = RendererBase::new(pf);
2372 if let Some(c) = clip {
2373 rb.clip_box_i(
2374 c.x as i32,
2375 c.y as i32,
2376 (c.x + c.width) as i32 - 1,
2377 (c.y + c.height) as i32 - 1,
2378 );
2379 }
2380 let mut ras = RasterizerScanlineAa::new();
2381 ras.filling_rule(FillingRule::NonZero);
2382
2383 for glyph in glyphs {
2386 let glyph_index = glyph.index as u16;
2387
2388 let Some(glyph_data) = parsed_font.get_or_decode_glyph(glyph_index) else {
2392 continue;
2393 };
2394
2395 let is_hinted = glyph_cache
2396 .get_or_build(
2397 font_hash.font_hash,
2398 glyph_index,
2399 &glyph_data,
2400 parsed_font,
2401 ppem,
2402 )
2403 .is_some_and(|c| c.is_hinted);
2404
2405 let glyph_x = (glyph.point.x - scroll_offset.0) * dpi_factor;
2406 let glyph_baseline_y = (glyph.point.y - scroll_offset.1) * dpi_factor;
2407
2408 let Some((cells, int_x, int_y)) = glyph_cache.get_or_build_cells(
2409 font_hash.font_hash,
2410 glyph_index,
2411 ppem,
2412 glyph_x,
2413 glyph_baseline_y,
2414 scale,
2415 is_hinted,
2416 hint_correction,
2417 ) else {
2418 continue;
2419 };
2420
2421 ras.add_cells_offset(cells, int_x, int_y);
2422 }
2423
2424 let mut sl = ScanlineU8::new();
2426 render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &agg_color);
2427
2428}
2429
2430#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)]
2444fn render_text_shadow(
2445 shadow: &StyleBoxShadow,
2446 glyphs: &[GlyphInstance],
2447 font_hash: FontHash,
2448 font_size_px: f32,
2449 pixmap: &mut AzulPixmap,
2450 clip_rect: &LogicalRect,
2451 clip: Option<AzRect>,
2452 renderer_resources: &RendererResources,
2453 font_manager: Option<&FontManager<FontRef>>,
2454 dpi_factor: f32,
2455 glyph_cache: &mut GlyphCache,
2456 scroll_offset: (f32, f32),
2457) {
2458 let color = shadow.color;
2459 if color.a == 0 || glyphs.is_empty() {
2460 return;
2461 }
2462
2463 let off_x = shadow
2465 .offset_x
2466 .inner
2467 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
2468 let off_y = shadow
2469 .offset_y
2470 .inner
2471 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
2472 let blur_logical = shadow
2473 .blur_radius
2474 .inner
2475 .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
2476 .max(0.0);
2477
2478 let Some(mut tmp) = AzulPixmap::new(pixmap.width, pixmap.height) else {
2480 return;
2481 };
2482 tmp.fill(0, 0, 0, 0);
2483
2484 let shifted: Vec<GlyphInstance> = glyphs
2486 .iter()
2487 .map(|g| {
2488 let mut g = *g;
2489 g.point.x += off_x;
2490 g.point.y += off_y;
2491 g
2492 })
2493 .collect();
2494
2495 let shadow_clip_rect = LogicalRect {
2497 origin: LogicalPosition {
2498 x: clip_rect.origin.x + off_x,
2499 y: clip_rect.origin.y + off_y,
2500 },
2501 size: clip_rect.size,
2502 };
2503 render_text(
2504 &shifted,
2505 font_hash,
2506 font_size_px,
2507 color,
2508 &mut tmp,
2509 &shadow_clip_rect,
2510 clip,
2511 renderer_resources,
2512 font_manager,
2513 dpi_factor,
2514 glyph_cache,
2515 scroll_offset,
2516 true,
2519 );
2520
2521 let blur_px = blur_logical * dpi_factor;
2523 if blur_px > 0.5 {
2524 let radius = (blur_px.ceil() as u32).min(254);
2525 let w = tmp.width;
2526 let h = tmp.height;
2527 let stride = (w * 4) as i32;
2528 let mut ra = unsafe { RowAccessor::new_with_buf(tmp.data.as_mut_ptr(), w, h, stride) };
2529 stack_blur_rgba32(&mut ra, radius, radius);
2530 }
2531
2532 blit_buffer(pixmap, &tmp.data, tmp.width, tmp.height, 0, 0);
2534}
2535
2536#[allow(clippy::suboptimal_flops)] #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] fn render_border(
2539 pixmap: &mut AzulPixmap,
2540 bounds: &LogicalRect,
2541 color: ColorU,
2542 width: f32,
2543 border_style: azul_css::props::style::border::BorderStyle,
2544 border_radius: &BorderRadius,
2545 clip: Option<AzRect>,
2546 dpi_factor: f32,
2547) {
2548 use azul_css::props::style::border::BorderStyle;
2549
2550 if color.a == 0 || width <= 0.0 {
2551 return;
2552 }
2553
2554 match border_style {
2555 BorderStyle::None | BorderStyle::Hidden => return,
2556 _ => {}
2557 }
2558
2559 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
2560 return;
2561 };
2562
2563 if let Some(ref c) = clip {
2565 if rect.clip(c).is_none() {
2566 return;
2567 }
2568 }
2569
2570 let scaled_width = width * dpi_factor;
2571 let agg_color = Rgba8::new(
2572 u32::from(color.r),
2573 u32::from(color.g),
2574 u32::from(color.b),
2575 u32::from(color.a),
2576 );
2577
2578 let mut path = build_rounded_rect_path(&rect, border_radius, dpi_factor);
2580
2581 let x = f64::from(rect.x);
2582 let y = f64::from(rect.y);
2583 let w = f64::from(rect.width);
2584 let h = f64::from(rect.height);
2585 let sw = f64::from(scaled_width);
2586
2587 let ir = AzRect::from_xywh(
2589 rect.x + scaled_width,
2590 rect.y + scaled_width,
2591 rect.width - 2.0 * scaled_width,
2592 rect.height - 2.0 * scaled_width,
2593 );
2594
2595 if let Some(ir) = ir {
2596 let inner_radius = BorderRadius {
2597 top_left: (border_radius.top_left - width).max(0.0),
2598 top_right: (border_radius.top_right - width).max(0.0),
2599 bottom_right: (border_radius.bottom_right - width).max(0.0),
2600 bottom_left: (border_radius.bottom_left - width).max(0.0),
2601 };
2602 let mut inner = build_rounded_rect_path(&ir, &inner_radius, dpi_factor);
2603 path.concat_path(&mut inner, 0);
2604 }
2605
2606 match border_style {
2608 BorderStyle::Dashed | BorderStyle::Dotted => {
2609 use agg_rust::conv_dash::ConvDash;
2611 use agg_rust::conv_stroke::ConvStroke;
2612
2613 let half = sw / 2.0;
2614 let mut stroke_path = PathStorage::new();
2615 let (cx, cy, cw, ch) = (x + half, y + half, w - sw, h - sw);
2616 stroke_path.move_to(cx, cy);
2617 stroke_path.line_to(cx + cw, cy);
2618 stroke_path.line_to(cx + cw, cy + ch);
2619 stroke_path.line_to(cx, cy + ch);
2620 stroke_path.close_polygon(PATH_FLAGS_NONE);
2621
2622 let mut dashed = ConvDash::new(stroke_path);
2623 if border_style == BorderStyle::Dashed {
2624 dashed.add_dash(sw * 3.0, sw);
2625 } else {
2626 dashed.add_dash(sw, sw);
2627 }
2628
2629 let mut stroked = ConvStroke::new(dashed);
2630 stroked.set_width(sw);
2631
2632 agg_fill_path_clipped(pixmap, &mut stroked, &agg_color, FillingRule::NonZero, clip);
2633 }
2634 _ if border_radius.is_zero() => {
2635 let pw = pixmap.width;
2637 let ph = pixmap.height;
2638 let stride = (pw * 4) as i32;
2639 let mut ra =
2640 unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), pw, ph, stride) };
2641 let mut pf = PixfmtRgba32::new(&mut ra);
2642 let mut rb = RendererBase::new(pf);
2643 if let Some(c) = clip {
2644 rb.clip_box_i(
2645 c.x as i32,
2646 c.y as i32,
2647 (c.x + c.width) as i32 - 1,
2648 (c.y + c.height) as i32 - 1,
2649 );
2650 }
2651 let (xi, yi) = (x as i32, y as i32);
2652 let (x2i, y2i) = ((x + w) as i32 - 1, (y + h) as i32 - 1);
2653 let swi = sw as i32;
2654 rb.blend_bar(xi, yi, x2i, yi + swi - 1, &agg_color, 255);
2656 rb.blend_bar(xi, y2i - swi + 1, x2i, y2i, &agg_color, 255);
2658 rb.blend_bar(xi, yi + swi, xi + swi - 1, y2i - swi, &agg_color, 255);
2660 rb.blend_bar(x2i - swi + 1, yi + swi, x2i, y2i - swi, &agg_color, 255);
2662 }
2663 _ => {
2664 agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::EvenOdd, clip);
2666 }
2667 }
2668
2669}
2670
2671#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] #[allow(clippy::too_many_lines)] fn render_border_sides(
2676 pixmap: &mut AzulPixmap,
2677 bounds: &LogicalRect,
2678 colors: [ColorU; 4], widths: [f32; 4], _styles: [azul_css::props::style::border::BorderStyle; 4],
2681 border_radius: &BorderRadius,
2682 clip: Option<AzRect>,
2683 dpi_factor: f32,
2684) {
2685 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
2686 return;
2687 };
2688
2689 let ox = f64::from(rect.x);
2691 let oy = f64::from(rect.y);
2692 let ow = f64::from(rect.width);
2693 let oh = f64::from(rect.height);
2694
2695 let wt = f64::from(widths[0] * dpi_factor);
2697 let wr = f64::from(widths[1] * dpi_factor);
2698 let wb = f64::from(widths[2] * dpi_factor);
2699 let wl = f64::from(widths[3] * dpi_factor);
2700
2701 let ix = ox + wl;
2702 let iy = oy + wt;
2703 let iw = ow - wl - wr;
2704 let ih = oh - wt - wb;
2705
2706 let sides: [(f64, f64, f64, f64, f64, f64, f64, f64, ColorU, f32); 4] = [
2713 (
2715 ox,
2716 oy,
2717 ox + ow,
2718 oy,
2719 ix + iw,
2720 iy,
2721 ix,
2722 iy,
2723 colors[0],
2724 widths[0],
2725 ),
2726 (
2728 ox + ow,
2729 oy,
2730 ox + ow,
2731 oy + oh,
2732 ix + iw,
2733 iy + ih,
2734 ix + iw,
2735 iy,
2736 colors[1],
2737 widths[1],
2738 ),
2739 (
2741 ox + ow,
2742 oy + oh,
2743 ox,
2744 oy + oh,
2745 ix,
2746 iy + ih,
2747 ix + iw,
2748 iy + ih,
2749 colors[2],
2750 widths[2],
2751 ),
2752 (
2754 ox,
2755 oy + oh,
2756 ox,
2757 oy,
2758 ix,
2759 iy,
2760 ix,
2761 iy + ih,
2762 colors[3],
2763 widths[3],
2764 ),
2765 ];
2766
2767 if border_radius.is_zero() {
2768 let pw = pixmap.width;
2770 let ph = pixmap.height;
2771 let stride = (pw * 4) as i32;
2772 let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), pw, ph, stride) };
2773 let mut pf = PixfmtRgba32::new(&mut ra);
2774 let mut rb = RendererBase::new(pf);
2775 if let Some(c) = clip {
2776 rb.clip_box_i(
2777 c.x as i32,
2778 c.y as i32,
2779 (c.x + c.width) as i32 - 1,
2780 (c.y + c.height) as i32 - 1,
2781 );
2782 }
2783 if widths[0] > 0.0 && colors[0].a > 0 {
2785 let c = colors[0];
2786 let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
2787 rb.blend_bar(
2788 ox as i32,
2789 oy as i32,
2790 (ox + ow) as i32 - 1,
2791 iy as i32 - 1,
2792 &ac,
2793 255,
2794 );
2795 }
2796 if widths[2] > 0.0 && colors[2].a > 0 {
2798 let c = colors[2];
2799 let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
2800 rb.blend_bar(
2801 ox as i32,
2802 (iy + ih) as i32,
2803 (ox + ow) as i32 - 1,
2804 (oy + oh) as i32 - 1,
2805 &ac,
2806 255,
2807 );
2808 }
2809 if widths[3] > 0.0 && colors[3].a > 0 {
2811 let c = colors[3];
2812 let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
2813 rb.blend_bar(
2814 ox as i32,
2815 iy as i32,
2816 ix as i32 - 1,
2817 (iy + ih) as i32 - 1,
2818 &ac,
2819 255,
2820 );
2821 }
2822 if widths[1] > 0.0 && colors[1].a > 0 {
2824 let c = colors[1];
2825 let ac = Rgba8::new(u32::from(c.r), u32::from(c.g), u32::from(c.b), u32::from(c.a));
2826 rb.blend_bar(
2827 (ix + iw) as i32,
2828 iy as i32,
2829 (ox + ow) as i32 - 1,
2830 (iy + ih) as i32 - 1,
2831 &ac,
2832 255,
2833 );
2834 }
2835 } else {
2836 for &(x0, y0, x1, y1, x2, y2, x3, y3, color, width) in &sides {
2838 if width <= 0.0 || color.a == 0 {
2839 continue;
2840 }
2841
2842 let mut path = PathStorage::new();
2843 path.move_to(x0, y0);
2844 path.line_to(x1, y1);
2845 path.line_to(x2, y2);
2846 path.line_to(x3, y3);
2847 path.close_polygon(PATH_FLAGS_NONE);
2848
2849 let agg_color = Rgba8::new(
2850 u32::from(color.r),
2851 u32::from(color.g),
2852 u32::from(color.b),
2853 u32::from(color.a),
2854 );
2855 agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::NonZero, clip);
2856 }
2857 }
2858
2859}
2860
2861#[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(
2865 pixmap: &mut AzulPixmap,
2866 bounds: &LogicalRect,
2867 image: &ImageRef,
2868 clip: Option<AzRect>,
2869 dpi_factor: f32,
2870) {
2871 let Some(rect) = logical_rect_to_az_rect(bounds, dpi_factor) else {
2872 return;
2873 };
2874
2875 if let Some(ref c) = clip {
2877 if rect.clip(c).is_none() {
2878 return;
2879 }
2880 }
2881
2882 let image_data = image.get_data();
2883 let (src_rgba, src_w, src_h) = match image_data {
2884 DecodedImage::Raw((descriptor, data)) => {
2885 let w = descriptor.width as u32;
2886 let h = descriptor.height as u32;
2887 if w == 0 || h == 0 {
2888 return;
2889 }
2890 let bytes = match data {
2891 azul_core::resources::ImageData::Raw(shared) => shared.as_ref(),
2892 azul_core::resources::ImageData::External(_) => return,
2893 };
2894
2895 let rgba = match descriptor.format {
2896 azul_core::resources::RawImageFormat::RGBA8 => bytes.to_vec(),
2902 azul_core::resources::RawImageFormat::RGB8 => {
2903 let mut out = Vec::with_capacity(bytes.len() / 3 * 4);
2904 for chunk in bytes.chunks_exact(3) {
2905 out.extend_from_slice(&[chunk[0], chunk[1], chunk[2], 255]);
2906 }
2907 out
2908 }
2909 azul_core::resources::RawImageFormat::BGRA8 => {
2910 let mut out = Vec::with_capacity(bytes.len());
2911 for chunk in bytes.chunks_exact(4) {
2912 let b = chunk[0];
2913 let g = chunk[1];
2914 let r = chunk[2];
2915 let a = chunk[3];
2916 out.push(r);
2917 out.push(g);
2918 out.push(b);
2919 out.push(a);
2920 }
2921 out
2922 }
2923 azul_core::resources::RawImageFormat::R8 => {
2924 let mut out = Vec::with_capacity(bytes.len() * 4);
2925 for &v in bytes {
2926 out.push(v);
2927 out.push(v);
2928 out.push(v);
2929 out.push(v);
2930 }
2931 out
2932 }
2933 _ => {
2934 let gray = Rgba8::new(200, 200, 200, 255);
2936 let mut path = build_rect_path(&rect);
2937 agg_fill_path(pixmap, &mut path, &gray, FillingRule::NonZero);
2938 return;
2939 }
2940 };
2941
2942 (rgba, w, h)
2943 }
2944 DecodedImage::NullImage { .. } | DecodedImage::Callback(_) => {
2945 let gray = Rgba8::new(200, 200, 200, 255);
2946 let mut path = build_rect_path(&rect);
2947 agg_fill_path(pixmap, &mut path, &gray, FillingRule::NonZero);
2948 return;
2949 }
2950 DecodedImage::Gl(_) => return,
2951 };
2952
2953 let dst_x = rect.x as i32;
2955 let dst_y = rect.y as i32;
2956 let dst_w = rect.width as u32;
2957 let dst_h = rect.height as u32;
2958 let pw = pixmap.width;
2959 let ph = pixmap.height;
2960
2961 let sx = src_w as f32 / dst_w.max(1) as f32;
2962 let sy = src_h as f32 / dst_h.max(1) as f32;
2963
2964 let (clip_x1, clip_y1, clip_x2, clip_y2) = clip.as_ref().map_or((0, 0, pw as i32, ph as i32), |c| (
2966 c.x as i32,
2967 c.y as i32,
2968 (c.x + c.width) as i32,
2969 (c.y + c.height) as i32,
2970 ));
2971
2972 for py in 0..dst_h {
2973 for px in 0..dst_w {
2974 let tx = dst_x + px as i32;
2975 let ty = dst_y + py as i32;
2976 if tx < 0 || ty < 0 || tx >= pw as i32 || ty >= ph as i32 {
2977 continue;
2978 }
2979 if tx < clip_x1 || ty < clip_y1 || tx >= clip_x2 || ty >= clip_y2 {
2981 continue;
2982 }
2983
2984 let src_x = ((px as f32 * sx) as u32).min(src_w - 1);
2985 let src_y = ((py as f32 * sy) as u32).min(src_h - 1);
2986 let si = ((src_y * src_w + src_x) * 4) as usize;
2987 let di = ((ty as u32 * pw + tx as u32) * 4) as usize;
2988
2989 if si + 3 < src_rgba.len() && di + 3 < pixmap.data.len() {
2990 let sa = u32::from(src_rgba[si + 3]);
2991 if sa == 255 {
2992 pixmap.data[di] = src_rgba[si];
2993 pixmap.data[di + 1] = src_rgba[si + 1];
2994 pixmap.data[di + 2] = src_rgba[si + 2];
2995 pixmap.data[di + 3] = 255;
2996 } else if sa > 0 {
2997 let da = 255 - sa;
2999 pixmap.data[di] =
3000 ((u32::from(src_rgba[si]) * sa + u32::from(pixmap.data[di]) * da) / 255) as u8;
3001 pixmap.data[di + 1] = ((u32::from(src_rgba[si + 1]) * sa
3002 + u32::from(pixmap.data[di + 1]) * da)
3003 / 255) as u8;
3004 pixmap.data[di + 2] = ((u32::from(src_rgba[si + 2]) * sa
3005 + u32::from(pixmap.data[di + 2]) * da)
3006 / 255) as u8;
3007 pixmap.data[di + 3] =
3008 ((sa + u32::from(pixmap.data[di + 3]) * da / 255).min(255)) as u8;
3009 }
3010 }
3011 }
3012 }
3013
3014}
3015
3016fn build_rect_path(rect: &AzRect) -> PathStorage {
3017 let mut path = PathStorage::new();
3018 let x = f64::from(rect.x);
3019 let y = f64::from(rect.y);
3020 let w = f64::from(rect.width);
3021 let h = f64::from(rect.height);
3022 path.move_to(x, y);
3023 path.line_to(x + w, y);
3024 path.line_to(x + w, y + h);
3025 path.line_to(x, y + h);
3026 path.close_polygon(PATH_FLAGS_NONE);
3027 path
3028}
3029
3030fn build_rounded_rect_path(
3031 rect: &AzRect,
3032 border_radius: &BorderRadius,
3033 dpi_factor: f32,
3034) -> PathStorage {
3035 let mut path = PathStorage::new();
3036
3037 let x = f64::from(rect.x);
3038 let y = f64::from(rect.y);
3039 let w = f64::from(rect.width);
3040 let h = f64::from(rect.height);
3041
3042 let tl = f64::from(border_radius.top_left * dpi_factor);
3043 let tr = f64::from(border_radius.top_right * dpi_factor);
3044 let br = f64::from(border_radius.bottom_right * dpi_factor);
3045 let bl = f64::from(border_radius.bottom_left * dpi_factor);
3046
3047 if tl <= 0.0 && tr <= 0.0 && br <= 0.0 && bl <= 0.0 {
3048 path.move_to(x, y);
3049 path.line_to(x + w, y);
3050 path.line_to(x + w, y + h);
3051 path.line_to(x, y + h);
3052 path.close_polygon(PATH_FLAGS_NONE);
3053 return path;
3054 }
3055
3056 let mut rr = RoundedRect::default_new();
3068 rr.rect(x, y, x + w, y + h);
3069 rr.radius_all(tl, tl, tr, tr, br, br, bl, bl);
3070 rr.normalize_radius();
3071 rr.set_approximation_scale(f64::from(dpi_factor.max(1.0)));
3072
3073 path.concat_path(&mut rr, 0);
3074 path
3075}
3076
3077#[derive(Debug, Clone, Copy)]
3083pub struct ComponentPreviewOptions {
3084 pub width: Option<f32>,
3086 pub height: Option<f32>,
3088 pub dpi_factor: f32,
3090 pub background_color: ColorU,
3092}
3093
3094impl Default for ComponentPreviewOptions {
3095 fn default() -> Self {
3096 Self {
3097 width: None,
3098 height: None,
3099 dpi_factor: 1.0,
3100 background_color: ColorU {
3101 r: 255,
3102 g: 255,
3103 b: 255,
3104 a: 255,
3105 },
3106 }
3107 }
3108}
3109
3110#[derive(Debug)]
3112pub struct ComponentPreviewResult {
3113 pub png_data: Vec<u8>,
3115 pub content_width: f32,
3117 pub content_height: f32,
3119}
3120
3121#[allow(clippy::match_same_arms)] fn compute_content_bounds(dl: &DisplayList) -> Option<(f32, f32, f32, f32)> {
3124 let mut min_x = f32::MAX;
3125 let mut min_y = f32::MAX;
3126 let mut max_x = f32::MIN;
3127 let mut max_y = f32::MIN;
3128 let mut has_items = false;
3129
3130 for item in &dl.items {
3131 let bounds = match item {
3132 DisplayListItem::Rect { bounds, .. } => Some(*bounds),
3133 DisplayListItem::SelectionRect { bounds, .. } => Some(*bounds),
3134 DisplayListItem::Border { bounds, .. } => Some(*bounds),
3135 DisplayListItem::Text { clip_rect, .. } => Some(*clip_rect),
3136 DisplayListItem::Image { bounds, .. } => Some(*bounds),
3137 DisplayListItem::BoxShadow { bounds, .. } => Some(*bounds),
3138 DisplayListItem::PushClip { bounds, .. } => Some(*bounds),
3139 DisplayListItem::LinearGradient { bounds, .. } => Some(*bounds),
3140 DisplayListItem::RadialGradient { bounds, .. } => Some(*bounds),
3141 DisplayListItem::ConicGradient { bounds, .. } => Some(*bounds),
3142 DisplayListItem::VirtualView { bounds, .. } => Some(*bounds),
3143 DisplayListItem::ScrollBar { bounds, .. } => Some(*bounds),
3144 _ => None,
3145 };
3146 if let Some(b) = bounds {
3147 has_items = true;
3148 min_x = min_x.min(b.0.origin.x);
3149 min_y = min_y.min(b.0.origin.y);
3150 max_x = max_x.max(b.0.origin.x + b.0.size.width);
3151 max_y = max_y.max(b.0.origin.y + b.0.size.height);
3152 }
3153 }
3154
3155 if has_items {
3156 Some((min_x, min_y, max_x, max_y))
3157 } else {
3158 None
3159 }
3160}
3161
3162#[cfg(all(feature = "std", feature = "text_layout", feature = "font_loading"))]
3164#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines)] pub fn render_component_preview(
3173 styled_dom: &azul_core::styled_dom::StyledDom,
3174 font_manager: &FontManager<FontRef>,
3175 opts: ComponentPreviewOptions,
3176 system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
3177) -> Result<ComponentPreviewResult, String> {
3178 use crate::{
3179 font_traits::TextLayoutCache,
3180 solver3::{self, cache::LayoutCache, display_list::DisplayList},
3181 };
3182 use azul_core::{
3183 dom::DomId,
3184 geom::{LogicalPosition, LogicalRect, LogicalSize},
3185 resources::{IdNamespace, RendererResources},
3186 selection::{SelectionState, TextSelection},
3187 };
3188 use std::collections::{BTreeMap, HashMap};
3189
3190 const MAX_SIZE: f32 = 4096.0;
3191
3192 let layout_width = opts.width.unwrap_or(MAX_SIZE);
3193 let layout_height = opts.height.unwrap_or(MAX_SIZE);
3194
3195 let viewport = LogicalRect {
3196 origin: LogicalPosition::zero(),
3197 size: LogicalSize {
3198 width: layout_width,
3199 height: layout_height,
3200 },
3201 };
3202
3203 let mut preview_font_manager = FontManager::from_arc_shared(
3204 font_manager.fc_cache.clone(),
3205 font_manager.parsed_fonts.clone(),
3206 )
3207 .map_err(|e| format!("Failed to create preview font manager: {e:?}"))?;
3208
3209 {
3211 use crate::solver3::getters::collect_and_resolve_font_chains_with_registration;
3212 use crate::text3::default::PathLoader;
3213
3214 let platform = azul_css::system::Platform::current();
3215
3216 let chains = collect_and_resolve_font_chains_with_registration(
3217 styled_dom,
3218 &preview_font_manager.fc_cache,
3219 &preview_font_manager,
3220 &platform,
3221 );
3222 let loader = PathLoader::new();
3223 let _failed = preview_font_manager.load_missing_for_chains(&chains, |bytes, index| {
3224 loader.load_font_shared(bytes, index)
3225 });
3226 preview_font_manager.set_font_chain_cache(chains.into_fontconfig_chains());
3227 }
3228
3229 let mut layout_cache = LayoutCache {
3231 tree: None,
3232 calculated_positions: Vec::new(),
3233 viewport: None,
3234 scroll_ids: HashMap::new(),
3235 scroll_id_to_node_id: HashMap::new(),
3236 counters: HashMap::new(),
3237 float_cache: HashMap::new(),
3238 cache_map: solver3::cache::LayoutCacheMap::default(),
3239 previous_positions: Vec::new(),
3240 cached_display_list: None,
3241 prev_dom_ptr: 0,
3242 prev_viewport: LogicalRect::zero(),
3243 };
3244 let mut text_cache = TextLayoutCache::new();
3245 let empty_scroll_offsets = BTreeMap::new();
3246 let empty_text_selections = BTreeMap::new();
3247 let renderer_resources = RendererResources::default();
3248 let id_namespace = IdNamespace(0xFFFF);
3249 let dom_id = DomId::ROOT_ID;
3250 let mut debug_messages = None;
3251 let get_system_time_fn = azul_core::task::GetSystemTimeCallback {
3252 cb: azul_core::task::get_system_time_libstd,
3253 };
3254
3255 let display_list = solver3::layout_document(
3256 &mut layout_cache,
3257 &mut text_cache,
3258 styled_dom,
3259 viewport,
3260 &preview_font_manager,
3261 &empty_scroll_offsets,
3262 &empty_text_selections,
3263 &mut debug_messages,
3264 None,
3265 &renderer_resources,
3266 id_namespace,
3267 dom_id,
3268 false,
3269 Vec::new(),
3270 None, &azul_core::resources::ImageCache::default(),
3272 system_style.clone(),
3273 get_system_time_fn,
3274 )
3275 .map_err(|e| format!("Layout failed: {e:?}"))?;
3276
3277 let (render_width, render_height) = if opts.width.is_some() && opts.height.is_some() {
3279 (opts.width.unwrap(), opts.height.unwrap())
3280 } else {
3281 match compute_content_bounds(&display_list) {
3282 Some((_min_x, _min_y, max_x, max_y)) => {
3283 let w = if opts.width.is_some() {
3284 opts.width.unwrap()
3285 } else {
3286 max_x.max(1.0).ceil()
3287 };
3288 let h = if opts.height.is_some() {
3289 opts.height.unwrap()
3290 } else {
3291 max_y.max(1.0).ceil()
3292 };
3293 (w, h)
3294 }
3295 None => {
3296 return Ok(ComponentPreviewResult {
3297 png_data: Vec::new(),
3298 content_width: 0.0,
3299 content_height: 0.0,
3300 });
3301 }
3302 }
3303 };
3304
3305 let render_width = render_width.min(MAX_SIZE);
3306 let render_height = render_height.min(MAX_SIZE);
3307
3308 let dpi = opts.dpi_factor;
3310 let pixel_w = ((render_width * dpi) as u32).max(1);
3311 let pixel_h = ((render_height * dpi) as u32).max(1);
3312
3313 let mut pixmap = AzulPixmap::new(pixel_w, pixel_h)
3314 .ok_or_else(|| format!("Cannot create pixmap {pixel_w}x{pixel_h}"))?;
3315
3316 let bg = opts.background_color;
3317 pixmap.fill(bg.r, bg.g, bg.b, bg.a);
3318
3319 let mut preview_glyph_cache = GlyphCache::new();
3320 let preview_render_state =
3321 CpuRenderState::new(ScrollOffsetMap::new()).with_system_style(system_style);
3322 render_display_list_with_state(
3323 &display_list,
3324 &mut pixmap,
3325 dpi,
3326 &renderer_resources,
3327 Some(&preview_font_manager),
3328 &mut preview_glyph_cache,
3329 &preview_render_state,
3330 )?;
3331
3332 let png_data = pixmap
3333 .encode_png()
3334 .map_err(|e| format!("PNG encoding failed: {e}"))?;
3335
3336 Ok(ComponentPreviewResult {
3337 png_data,
3338 content_width: render_width,
3339 content_height: render_height,
3340 })
3341}
3342
3343#[cfg(all(feature = "std", feature = "text_layout", feature = "font_loading"))]
3348pub fn render_dom_to_image(
3352 mut dom: azul_core::dom::Dom,
3353 css: azul_css::css::Css,
3354 width: f32,
3355 height: f32,
3356 dpi: f32,
3357) -> Result<Vec<u8>, String> {
3358 use crate::font_traits::FontManager;
3359 use azul_core::styled_dom::StyledDom;
3360
3361 let styled_dom = StyledDom::create(&mut dom, css);
3362
3363 let fc_cache = crate::font::loading::build_font_cache();
3364 let font_manager = FontManager::new(fc_cache)
3365 .map_err(|e| format!("Failed to create font manager: {e:?}"))?;
3366
3367 let opts = ComponentPreviewOptions {
3368 width: Some(width),
3369 height: Some(height),
3370 dpi_factor: dpi,
3371 background_color: ColorU {
3372 r: 255,
3373 g: 255,
3374 b: 255,
3375 a: 255,
3376 },
3377 };
3378
3379 let result = render_component_preview(&styled_dom, &font_manager, opts, None)?;
3380 Ok(result.png_data)
3381}
3382
3383#[cfg(all(feature = "std", feature = "text_layout", feature = "font_loading"))]
3401#[must_use]
3402#[allow(clippy::suboptimal_flops, clippy::cast_possible_truncation, clippy::cast_sign_loss)]
3404pub fn render_text_run_to_pixmap(
3405 fc_cache: &rust_fontconfig::FcFontCache,
3406 text: &str,
3407 font_size_px: f32,
3408 text_color: ColorU,
3409 bg_color: ColorU,
3410 padding_px: f32,
3411 dpi_factor: f32,
3412) -> Option<AzulPixmap> {
3413 use azul_core::resources::{FontKey, IdNamespace};
3414 use rust_fontconfig::{FcPattern, OwnedFontSource};
3415
3416 let mut trace = Vec::new();
3418 let matched = fc_cache
3419 .query(
3420 &FcPattern {
3421 family: Some("sans-serif".to_string()),
3422 ..Default::default()
3423 },
3424 &mut trace,
3425 )
3426 .or_else(|| fc_cache.query(&FcPattern::default(), &mut trace))?;
3427
3428 let bytes = fc_cache.get_font_bytes(&matched.id)?;
3429 let font_index = fc_cache
3430 .get_font_by_id(&matched.id)
3431 .map_or(0, |src| match src {
3432 OwnedFontSource::Disk(path) => path.font_index,
3433 OwnedFontSource::Memory(font) => font.font_index,
3434 });
3435
3436 let parsed = ParsedFont::from_bytes(bytes.as_slice(), font_index, &mut Vec::new())?
3437 .with_source_bytes(bytes.clone());
3438
3439 let upm = f32::from(parsed.font_metrics.units_per_em);
3440 if upm <= 0.0 {
3441 return None;
3442 }
3443 let scale = font_size_px / upm;
3444
3445 let mut rr = RendererResources::default();
3448 let font_ref = crate::parsed_font_to_font_ref(parsed.clone());
3449 let key = FontKey::unique(IdNamespace(0));
3450 let hash = crate::font_ref_to_parsed_font(&font_ref).hash;
3451 rr.font_hash_map.insert(hash, key);
3452 rr.currently_registered_fonts
3453 .insert(key, (font_ref, std::collections::BTreeMap::default()));
3454 let font_hash = FontHash { font_hash: hash };
3455
3456 let ascent = parsed.font_metrics.ascent * scale;
3460 let descent = parsed.font_metrics.descent * scale; let baseline_y = padding_px + ascent;
3462 let mut pen_x = padding_px;
3463 let mut glyphs = Vec::new();
3464 for c in text.chars() {
3465 let gid = parsed.lookup_glyph_index(c as u32).unwrap_or(0);
3466 let advance = f32::from(parsed.get_horizontal_advance(gid)) * scale;
3467 glyphs.push(GlyphInstance {
3468 index: u32::from(gid),
3469 point: LogicalPosition { x: pen_x, y: baseline_y },
3470 size: LogicalSize { width: advance, height: font_size_px },
3471 });
3472 pen_x += advance;
3473 }
3474
3475 let logical_w = (pen_x + padding_px).max(1.0);
3477 let logical_h = (ascent - descent + padding_px * 2.0).max(1.0);
3478 let w = ((logical_w * dpi_factor).ceil() as u32).max(1);
3479 let h = ((logical_h * dpi_factor).ceil() as u32).max(1);
3480
3481 let mut pixmap = AzulPixmap::new(w, h)?;
3482 pixmap.fill(bg_color.r, bg_color.g, bg_color.b, bg_color.a);
3483
3484 let clip_rect: crate::solver3::display_list::WindowLogicalRect = LogicalRect {
3486 origin: LogicalPosition { x: 0.0, y: 0.0 },
3487 size: LogicalSize { width: logical_w, height: logical_h },
3488 }
3489 .into();
3490
3491 let item = DisplayListItem::Text {
3492 glyphs,
3493 font_hash,
3494 font_size_px,
3495 color: text_color,
3496 clip_rect,
3497 source_node_index: None,
3498 };
3499 let dl = DisplayList {
3500 items: vec![item],
3501 ..Default::default()
3502 };
3503 let mut gc = GlyphCache::new();
3504 render_display_list(&dl, &mut pixmap, dpi_factor, &rr, None, &mut gc).ok()?;
3505
3506 Some(pixmap)
3507}
3508
3509#[cfg(all(test, feature = "std"))]
3515mod text_shadow_tests {
3516 use super::*;
3517 use crate::font::parsed::ParsedFont;
3518 use crate::solver3::display_list::{DisplayList, WindowLogicalRect};
3519 use azul_core::resources::{FontKey, IdNamespace};
3520 use azul_css::props::basic::pixel::{PixelValue, PixelValueNoPercent};
3521 use azul_css::props::style::box_shadow::StyleBoxShadow;
3522
3523 fn load_test_font() -> Option<ParsedFont> {
3524 let candidates = [
3525 "/System/Library/Fonts/Supplemental/Times New Roman.ttf",
3526 "/System/Library/Fonts/Helvetica.ttc",
3527 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
3528 "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
3529 "C:/Windows/Fonts/arial.ttf",
3530 ];
3531 for path in candidates {
3532 if let Ok(bytes) = std::fs::read(path) {
3533 let arc = std::sync::Arc::new(rust_fontconfig::FontBytes::Owned(
3534 std::sync::Arc::from(bytes.as_slice()),
3535 ));
3536 if let Some(font) = ParsedFont::from_bytes(&bytes, 0, &mut Vec::new())
3537 .map(|f| f.with_source_bytes(arc))
3538 {
3539 return Some(font);
3540 }
3541 }
3542 }
3543 None
3544 }
3545
3546 fn renderer_resources_with(font: &ParsedFont) -> (RendererResources, FontHash) {
3547 let mut rr = RendererResources::default();
3548 let font_ref = crate::parsed_font_to_font_ref(font.clone());
3549 let key = FontKey::unique(IdNamespace(0));
3550 let hash = crate::font_ref_to_parsed_font(&font_ref).hash;
3551 rr.font_hash_map.insert(hash, key);
3552 rr.currently_registered_fonts
3553 .insert(key, (font_ref, std::collections::BTreeMap::default()));
3554 (rr, FontHash { font_hash: hash })
3555 }
3556
3557 fn shape(parsed: &ParsedFont, text: &str, font_size: f32, x: f32, y: f32) -> Vec<GlyphInstance> {
3559 let upm = f32::from(parsed.font_metrics.units_per_em);
3560 let scale = font_size / upm;
3561 let mut pen_x = x;
3562 let mut out = Vec::new();
3563 for c in text.chars() {
3564 let gid = parsed.lookup_glyph_index(c as u32).unwrap_or(0);
3565 let advance = f32::from(parsed.get_horizontal_advance(gid)) * scale;
3566 out.push(GlyphInstance {
3567 index: u32::from(gid),
3568 point: LogicalPosition { x: pen_x, y },
3569 size: LogicalSize {
3570 width: advance,
3571 height: font_size,
3572 },
3573 });
3574 pen_x += advance;
3575 }
3576 out
3577 }
3578
3579 fn count_red(pixmap: &AzulPixmap) -> usize {
3580 pixmap
3581 .data()
3582 .chunks_exact(4)
3583 .filter(|p| p[0] > 150 && p[1] < 100 && p[2] < 100)
3584 .count()
3585 }
3586
3587 #[test]
3590 fn text_shadow_paints_offset_colored_pixels() {
3591 let Some(font) = load_test_font() else {
3592 eprintln!("[skip] no system font available");
3593 return;
3594 };
3595 let (rr, font_hash) = renderer_resources_with(&font);
3596
3597 let w = 200u32;
3598 let h = 60u32;
3599 let font_size = 32.0;
3600 let glyphs = shape(&font, "Hi", font_size, 10.0, 40.0);
3602 #[allow(clippy::cast_precision_loss)]
3604 let clip_rect: WindowLogicalRect = LogicalRect {
3605 origin: LogicalPosition { x: 0.0, y: 0.0 },
3606 size: LogicalSize { width: w as f32, height: h as f32 },
3607 }
3608 .into();
3609
3610 let text_item = DisplayListItem::Text {
3611 glyphs,
3612 font_hash,
3613 font_size_px: font_size,
3614 color: ColorU { r: 0, g: 0, b: 0, a: 255 },
3615 clip_rect,
3616 source_node_index: None,
3617 };
3618
3619 let mut gc = GlyphCache::new();
3621 let mut no_shadow = AzulPixmap::new(w, h).unwrap();
3622 no_shadow.fill(255, 255, 255, 255);
3623 let dl_plain = DisplayList {
3624 items: vec![text_item.clone()],
3625 ..Default::default()
3626 };
3627 render_display_list(&dl_plain, &mut no_shadow, 1.0, &rr, None, &mut gc).unwrap();
3628 let red_plain = count_red(&no_shadow);
3632
3633 let shadow = StyleBoxShadow {
3635 offset_x: PixelValueNoPercent { inner: PixelValue::px(24.0) },
3636 offset_y: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3637 blur_radius: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3638 spread_radius: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3639 color: ColorU { r: 255, g: 0, b: 0, a: 255 },
3640 clip_mode: azul_css::props::style::box_shadow::BoxShadowClipMode::Outset,
3641 };
3642 let mut with_shadow = AzulPixmap::new(w, h).unwrap();
3643 with_shadow.fill(255, 255, 255, 255);
3644 let dl_shadow = DisplayList {
3645 items: vec![
3646 DisplayListItem::PushTextShadow { shadow },
3647 text_item,
3648 DisplayListItem::PopTextShadow,
3649 ],
3650 ..Default::default()
3651 };
3652 let mut gc2 = GlyphCache::new();
3653 render_display_list(&dl_shadow, &mut with_shadow, 1.0, &rr, None, &mut gc2).unwrap();
3654 let red_shadow = count_red(&with_shadow);
3655
3656 assert!(
3657 red_shadow > red_plain + 20,
3658 "text-shadow must paint red shadow pixels beyond the baseline \
3659 (plain {red_plain}, shadow {red_shadow})"
3660 );
3661
3662 let right_red = with_shadow
3666 .data()
3667 .chunks_exact(4)
3668 .enumerate()
3669 .filter(|(i, p)| {
3670 #[allow(clippy::cast_possible_truncation)] let x = (*i as u32) % w;
3672 x > 30 && p[0] > 150 && p[1] < 100 && p[2] < 100
3673 })
3674 .count();
3675 assert!(
3676 right_red > 0,
3677 "shadow should appear offset to the right of the glyphs"
3678 );
3679 }
3680
3681 #[test]
3684 fn text_shadow_blur_spreads_coverage() {
3685 let Some(font) = load_test_font() else {
3686 eprintln!("[skip] no system font available");
3687 return;
3688 };
3689 let (rr, font_hash) = renderer_resources_with(&font);
3690 let w = 200u32;
3691 let h = 80u32;
3692 let font_size = 32.0;
3693 let glyphs = shape(&font, "Hi", font_size, 40.0, 50.0);
3694 #[allow(clippy::cast_precision_loss)]
3696 let clip_rect: WindowLogicalRect = LogicalRect {
3697 origin: LogicalPosition { x: 0.0, y: 0.0 },
3698 size: LogicalSize { width: w as f32, height: h as f32 },
3699 }
3700 .into();
3701
3702 let make = |blur: f32| -> usize {
3703 let shadow = StyleBoxShadow {
3704 offset_x: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3705 offset_y: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3706 blur_radius: PixelValueNoPercent { inner: PixelValue::px(blur) },
3707 spread_radius: PixelValueNoPercent { inner: PixelValue::px(0.0) },
3708 color: ColorU { r: 255, g: 0, b: 0, a: 255 },
3709 clip_mode: azul_css::props::style::box_shadow::BoxShadowClipMode::Outset,
3710 };
3711 let text_item = DisplayListItem::Text {
3712 glyphs: glyphs.clone(),
3713 font_hash,
3714 font_size_px: font_size,
3715 color: ColorU { r: 0, g: 0, b: 0, a: 0 }, clip_rect,
3717 source_node_index: None,
3718 };
3719 let dl = DisplayList {
3720 items: vec![
3721 DisplayListItem::PushTextShadow { shadow },
3722 text_item,
3723 DisplayListItem::PopTextShadow,
3724 ],
3725 ..Default::default()
3726 };
3727 let mut pm = AzulPixmap::new(w, h).unwrap();
3728 pm.fill(255, 255, 255, 255);
3729 let mut gc = GlyphCache::new();
3730 render_display_list(&dl, &mut pm, 1.0, &rr, None, &mut gc).unwrap();
3731 pm.data()
3733 .chunks_exact(4)
3734 .filter(|p| p[0] != 255 || p[1] != 255 || p[2] != 255)
3735 .count()
3736 };
3737
3738 let hard = make(0.0);
3739 let blurred = make(6.0);
3740 assert!(hard > 0, "hard shadow should paint");
3741 assert!(
3742 blurred > hard,
3743 "blurred shadow ({blurred}) should cover more pixels than hard ({hard})"
3744 );
3745 }
3746}