1use std::collections::HashMap;
8
9use azul_core::{
10 dom::ScrollbarOrientation,
11 geom::{LogicalPosition, LogicalRect, LogicalSize},
12 resources::{
13 DecodedImage, FontInstanceKey, ImageRef,
14 RendererResources,
15 },
16 ui_solver::GlyphInstance,
17};
18use azul_css::props::basic::{ColorU, ColorOrSystem, FontRef, pixel::DEFAULT_FONT_SIZE};
19use azul_css::props::style::filter::StyleFilter;
20
21use agg_rust::{
22 basics::{FillingRule, VertexSource, PATH_FLAGS_NONE},
23 blur::stack_blur_rgba32,
24 path_storage::PathStorage,
25 color::Rgba8,
26 conv_stroke::ConvStroke,
27 conv_transform::ConvTransform,
28 gradient_lut::GradientLut,
29 pixfmt_rgba::{PixfmtRgba32, PixelFormat},
30 rasterizer_scanline_aa::RasterizerScanlineAa,
31 renderer_base::RendererBase,
32 renderer_scanline::{render_scanlines_aa, render_scanlines_aa_solid},
33 rendering_buffer::RowAccessor,
34 rounded_rect::RoundedRect,
35 scanline_u::ScanlineU8,
36 span_allocator::SpanAllocator,
37 span_gradient::{GradientConic, GradientFunction, GradientRadialD, GradientX, SpanGradient},
38 span_interpolator_linear::SpanInterpolatorLinear,
39 trans_affine::TransAffine,
40};
41
42use crate::{
43 font::parsed::ParsedFont,
44 glyph_cache::GlyphCache,
45 solver3::display_list::{BorderRadius, DisplayList, DisplayListItem, LocalScrollId},
46 text3::cache::{FontHash, FontManager},
47};
48
49const IDENTITY_EPSILON: f32 = 0.0001;
50const IDENTITY_EPSILON_F64: f64 = 0.0001;
51const MAX_SHADOW_PIXBUF_SIZE: u32 = 4096;
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub struct LayerId(pub u64);
60
61pub struct CompositorState {
66 pub layers: HashMap<LayerId, Layer>,
68 pub root_layer: LayerId,
70 next_layer_id: u64,
72 pub previous_positions: Vec<LogicalPosition>,
74}
75
76pub struct Layer {
78 pub id: LayerId,
79 pub pixbuf: AzulPixmap,
81 pub bounds: LogicalRect,
83 pub damage: Vec<LogicalRect>,
85 pub children: Vec<LayerId>,
87 pub scroll_offset: (f32, f32),
89 pub opacity: f32,
91 pub filters: Vec<StyleFilter>,
93 pub transform: TransAffine,
95 pub display_list_range: (usize, usize),
97 pub scroll_id: Option<LocalScrollId>,
99 pub composite_dirty: bool,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum LayerReason {
106 Root,
108 ScrollFrame,
110 BlurFilter,
112 Opacity,
114 Transform,
116}
117
118impl CompositorState {
119 pub fn new(width: u32, height: u32) -> Self {
121 let root_id = LayerId(0);
122 let root_layer = Layer::new(
123 root_id,
124 LogicalRect {
125 origin: LogicalPosition::zero(),
126 size: LogicalSize { width: width as f32, height: height as f32 },
127 },
128 width,
129 height,
130 );
131 let mut layers = HashMap::new();
132 layers.insert(root_id, root_layer);
133 CompositorState {
134 layers,
135 root_layer: root_id,
136 next_layer_id: 1,
137 previous_positions: Vec::new(),
138 }
139 }
140
141 pub fn alloc_layer_id(&mut self) -> LayerId {
143 let id = LayerId(self.next_layer_id);
144 self.next_layer_id += 1;
145 id
146 }
147
148 pub fn next_layer_id_peek(&self) -> u64 {
150 self.next_layer_id
151 }
152
153 pub fn allocate_layers_from_display_list(
156 &mut self,
157 display_list: &DisplayList,
158 dpi_factor: f32,
159 ) {
160 let root_id = self.root_layer;
162 self.layers.retain(|id, _| *id == root_id);
163 if let Some(root) = self.layers.get_mut(&root_id) {
164 root.children.clear();
165 root.damage.clear();
166 root.display_list_range = (0, display_list.items.len());
167 root.composite_dirty = true;
168 }
169
170 let mut layer_stack: Vec<LayerId> = vec![root_id];
171 let mut i = 0;
172
173 while i < display_list.items.len() {
174 match &display_list.items[i] {
175 DisplayListItem::PushScrollFrame { clip_bounds, content_size, scroll_id, .. } => {
176 let bounds = *clip_bounds.inner();
177 let pw = (bounds.size.width * dpi_factor).ceil() as u32;
178 let ph = (bounds.size.height * dpi_factor).ceil() as u32;
179 if pw > 0 && ph > 0 {
180 let new_id = self.alloc_layer_id();
181 let mut layer = Layer::new(new_id, bounds, pw, ph);
182 layer.scroll_id = Some(*scroll_id);
183 let end = find_matching_pop(&display_list.items, i, MatchKind::ScrollFrame);
185 layer.display_list_range = (i + 1, end);
186 self.layers.insert(new_id, layer);
187 let parent_id = *layer_stack.last().unwrap();
189 if let Some(parent) = self.layers.get_mut(&parent_id) {
190 parent.children.push(new_id);
191 }
192 layer_stack.push(new_id);
193 }
194 }
195 DisplayListItem::PopScrollFrame => {
196 if layer_stack.len() > 1 {
197 layer_stack.pop();
198 }
199 }
200 DisplayListItem::PushOpacity { bounds, opacity } => {
201 if *opacity < 1.0 {
202 let b = *bounds.inner();
203 let pw = (b.size.width * dpi_factor).ceil() as u32;
204 let ph = (b.size.height * dpi_factor).ceil() as u32;
205 if pw > 0 && ph > 0 {
206 let new_id = self.alloc_layer_id();
207 let mut layer = Layer::new(new_id, b, pw, ph);
208 layer.opacity = *opacity;
209 let end = find_matching_pop(&display_list.items, i, MatchKind::Opacity);
210 layer.display_list_range = (i + 1, end);
211 self.layers.insert(new_id, layer);
212 let parent_id = *layer_stack.last().unwrap();
213 if let Some(parent) = self.layers.get_mut(&parent_id) {
214 parent.children.push(new_id);
215 }
216 layer_stack.push(new_id);
217 }
218 }
219 }
220 DisplayListItem::PopOpacity => {
221 if layer_stack.len() > 1 {
223 let top_id = *layer_stack.last().unwrap();
224 if let Some(layer) = self.layers.get(&top_id) {
225 if layer.opacity < 1.0 && layer.scroll_id.is_none() {
226 layer_stack.pop();
227 }
228 }
229 }
230 }
231 DisplayListItem::PushFilter { bounds, filters } => {
232 let has_blur = filters.iter().any(|f| matches!(f, StyleFilter::Blur(_)));
233 if has_blur {
234 let b = *bounds.inner();
235 let pw = (b.size.width * dpi_factor).ceil() as u32;
236 let ph = (b.size.height * dpi_factor).ceil() as u32;
237 if pw > 0 && ph > 0 {
238 let new_id = self.alloc_layer_id();
239 let mut layer = Layer::new(new_id, b, pw, ph);
240 layer.filters = filters.clone();
241 let end = find_matching_pop(&display_list.items, i, MatchKind::Filter);
242 layer.display_list_range = (i + 1, end);
243 self.layers.insert(new_id, layer);
244 let parent_id = *layer_stack.last().unwrap();
245 if let Some(parent) = self.layers.get_mut(&parent_id) {
246 parent.children.push(new_id);
247 }
248 layer_stack.push(new_id);
249 }
250 }
251 }
252 DisplayListItem::PopFilter => {
253 if layer_stack.len() > 1 {
254 let top_id = *layer_stack.last().unwrap();
255 if let Some(layer) = self.layers.get(&top_id) {
256 if !layer.filters.is_empty() {
257 layer_stack.pop();
258 }
259 }
260 }
261 }
262 DisplayListItem::PushReferenceFrame { initial_transform, bounds, .. } => {
263 let m = &initial_transform.m;
264 let is_identity =
265 (m[0][0] - 1.0).abs() < IDENTITY_EPSILON &&
266 m[0][1].abs() < IDENTITY_EPSILON &&
267 m[1][0].abs() < IDENTITY_EPSILON &&
268 (m[1][1] - 1.0).abs() < IDENTITY_EPSILON &&
269 m[3][0].abs() < IDENTITY_EPSILON &&
270 m[3][1].abs() < IDENTITY_EPSILON;
271 if !is_identity {
272 let b = *bounds.inner();
273 let pw = (b.size.width * dpi_factor).ceil().max(1.0) as u32;
274 let ph = (b.size.height * dpi_factor).ceil().max(1.0) as u32;
275 let new_id = self.alloc_layer_id();
276 let mut layer = Layer::new(new_id, b, pw, ph);
277 layer.transform = TransAffine::new_custom(
278 m[0][0] as f64, m[0][1] as f64,
279 m[1][0] as f64, m[1][1] as f64,
280 m[3][0] as f64, m[3][1] as f64,
281 );
282 let end = find_matching_pop(&display_list.items, i, MatchKind::ReferenceFrame);
283 layer.display_list_range = (i + 1, end);
284 self.layers.insert(new_id, layer);
285 let parent_id = *layer_stack.last().unwrap();
286 if let Some(parent) = self.layers.get_mut(&parent_id) {
287 parent.children.push(new_id);
288 }
289 layer_stack.push(new_id);
290 }
291 }
292 DisplayListItem::PopReferenceFrame => {
293 if layer_stack.len() > 1 {
294 let top_id = *layer_stack.last().unwrap();
295 if let Some(layer) = self.layers.get(&top_id) {
296 if !layer.transform.is_identity(IDENTITY_EPSILON_F64) {
297 layer_stack.pop();
298 }
299 }
300 }
301 }
302 _ => {}
303 }
304 i += 1;
305 }
306 }
307
308 pub fn compute_damage(
310 &mut self,
311 dirty_nodes: &std::collections::BTreeSet<usize>,
312 old_positions: &[LogicalPosition],
313 new_positions: &[LogicalPosition],
314 calculated_rects: &[LogicalRect],
315 ) {
316 if dirty_nodes.is_empty() {
317 return;
318 }
319
320 let mut damage_rects = Vec::new();
321 for &node_idx in dirty_nodes {
322 if node_idx < old_positions.len() && node_idx < calculated_rects.len() {
324 let old_rect = LogicalRect {
325 origin: old_positions[node_idx],
326 size: calculated_rects[node_idx].size,
327 };
328 damage_rects.push(old_rect);
329 }
330 if node_idx < new_positions.len() && node_idx < calculated_rects.len() {
332 let new_rect = LogicalRect {
333 origin: new_positions[node_idx],
334 size: calculated_rects[node_idx].size,
335 };
336 damage_rects.push(new_rect);
337 }
338 }
339
340 for (_, layer) in self.layers.iter_mut() {
342 for damage in &damage_rects {
343 if let Some(intersection) = rect_intersection(&layer.bounds, damage) {
344 layer.damage.push(intersection);
345 layer.composite_dirty = true;
346 }
347 }
348 }
349 }
350
351 pub fn render_layers(
353 &mut self,
354 display_list: &DisplayList,
355 dpi_factor: f32,
356 renderer_resources: &RendererResources,
357 font_manager: Option<&FontManager<FontRef>>,
358 glyph_cache: &mut GlyphCache,
359 ) -> Result<(), String> {
360 let layer_ranges: Vec<(LayerId, (usize, usize), LogicalRect)> = self.layers
362 .iter()
363 .map(|(id, layer)| (*id, layer.display_list_range, layer.bounds))
364 .collect();
365
366 for (layer_id, range, layer_bounds) in &layer_ranges {
367 let (start, end) = *range;
368 if start >= end || start >= display_list.items.len() {
369 continue;
370 }
371
372 let layer = self.layers.get_mut(layer_id).unwrap();
373
374 if *layer_id == self.root_layer {
376 layer.pixbuf.fill(255, 255, 255, 255);
377 } else {
378 layer.pixbuf.fill(0, 0, 0, 0);
379 }
380
381 let offset_x = layer_bounds.origin.x;
383 let offset_y = layer_bounds.origin.y;
384 render_display_list_range(
385 display_list,
386 &mut layer.pixbuf,
387 start,
388 end.min(display_list.items.len()),
389 offset_x,
390 offset_y,
391 dpi_factor,
392 renderer_resources,
393 font_manager,
394 glyph_cache,
395 )?;
396 }
397
398 Ok(())
399 }
400
401 pub fn composite_frame(&self, output: &mut AzulPixmap, dpi_factor: f32) {
403 self.composite_layer_recursive(self.root_layer, output, 0.0, 0.0, dpi_factor);
405 }
406
407 fn composite_layer_recursive(
408 &self,
409 layer_id: LayerId,
410 output: &mut AzulPixmap,
411 parent_offset_x: f32,
412 parent_offset_y: f32,
413 dpi_factor: f32,
414 ) {
415 let layer = match self.layers.get(&layer_id) {
416 Some(l) => l,
417 None => return,
418 };
419
420 let abs_x = parent_offset_x + layer.bounds.origin.x;
421 let abs_y = parent_offset_y + layer.bounds.origin.y;
422
423 if layer_id == self.root_layer {
425 blit_pixmap(&layer.pixbuf, output, 0, 0, 1.0);
426 } else {
427 let src = if !layer.filters.is_empty() {
429 let mut filtered = layer.pixbuf.clone_pixmap();
430 apply_layer_filters(&mut filtered, &layer.filters, dpi_factor);
431 Some(filtered)
432 } else {
433 None
434 };
435
436 let src_pixbuf = src.as_ref().unwrap_or(&layer.pixbuf);
437 let px_x = (abs_x * dpi_factor) as i32;
438 let px_y = (abs_y * dpi_factor) as i32;
439 blit_pixmap(src_pixbuf, output, px_x, px_y, layer.opacity);
440 }
441
442 let children: Vec<LayerId> = layer.children.clone();
444 for child_id in &children {
445 self.composite_layer_recursive(
446 *child_id,
447 output,
448 if layer_id == self.root_layer { 0.0 } else { abs_x },
449 if layer_id == self.root_layer { 0.0 } else { abs_y },
450 dpi_factor,
451 );
452 }
453 }
454
455 pub fn scroll_layer(
457 &mut self,
458 scroll_id: LocalScrollId,
459 new_offset: (f32, f32),
460 display_list: &DisplayList,
461 dpi_factor: f32,
462 renderer_resources: &RendererResources,
463 font_manager: Option<&FontManager<FontRef>>,
464 glyph_cache: &mut GlyphCache,
465 ) -> Result<(), String> {
466 let layer_id = self.layers.iter()
468 .find(|(_, l)| l.scroll_id == Some(scroll_id))
469 .map(|(id, _)| *id);
470
471 let layer_id = match layer_id {
472 Some(id) => id,
473 None => return Ok(()), };
475
476 let layer = self.layers.get_mut(&layer_id).unwrap();
477 let old_offset = layer.scroll_offset;
478 let dx = new_offset.0 - old_offset.0;
479 let dy = new_offset.1 - old_offset.1;
480
481 if dx.abs() < 0.5 && dy.abs() < 0.5 {
482 return Ok(());
483 }
484
485 let px_dx = (dx * dpi_factor).round() as i32;
487 let px_dy = (dy * dpi_factor).round() as i32;
488 shift_pixbuf(&mut layer.pixbuf, px_dx, px_dy);
489
490 let exposed = compute_exposed_rects(&layer.bounds, dx, dy);
493 for exposed_rect in exposed {
494 layer.damage.push(exposed_rect);
495 }
496
497 layer.scroll_offset = new_offset;
498 layer.composite_dirty = true;
499
500 let range = layer.display_list_range;
502 let bounds = layer.bounds;
503 let offset_x = bounds.origin.x;
504 let offset_y = bounds.origin.y;
505 render_display_list_range(
506 display_list,
507 &mut self.layers.get_mut(&layer_id).unwrap().pixbuf,
508 range.0,
509 range.1.min(display_list.items.len()),
510 offset_x,
511 offset_y,
512 dpi_factor,
513 renderer_resources,
514 font_manager,
515 glyph_cache,
516 )?;
517
518 Ok(())
519 }
520}
521
522impl Layer {
523 fn new(id: LayerId, bounds: LogicalRect, pixel_width: u32, pixel_height: u32) -> Self {
524 Layer {
525 id,
526 pixbuf: AzulPixmap::new(pixel_width.max(1), pixel_height.max(1))
527 .unwrap_or_else(|| AzulPixmap { data: vec![0; 4], width: 1, height: 1 }),
528 bounds,
529 damage: Vec::new(),
530 children: Vec::new(),
531 scroll_offset: (0.0, 0.0),
532 opacity: 1.0,
533 filters: Vec::new(),
534 transform: TransAffine::new(),
535 display_list_range: (0, 0),
536 scroll_id: None,
537 composite_dirty: true,
538 }
539 }
540}
541
542#[derive(Clone, Copy)]
548enum MatchKind {
549 ScrollFrame,
550 Opacity,
551 Filter,
552 ReferenceFrame,
553}
554
555fn find_matching_pop(items: &[DisplayListItem], start: usize, kind: MatchKind) -> usize {
557 let mut depth = 1u32;
558 for i in (start + 1)..items.len() {
559 match (&items[i], kind) {
560 (DisplayListItem::PushScrollFrame { .. }, MatchKind::ScrollFrame) => depth += 1,
561 (DisplayListItem::PopScrollFrame, MatchKind::ScrollFrame) => {
562 depth -= 1;
563 if depth == 0 { return i; }
564 }
565 (DisplayListItem::PushOpacity { .. }, MatchKind::Opacity) => depth += 1,
566 (DisplayListItem::PopOpacity, MatchKind::Opacity) => {
567 depth -= 1;
568 if depth == 0 { return i; }
569 }
570 (DisplayListItem::PushFilter { .. }, MatchKind::Filter) => depth += 1,
571 (DisplayListItem::PopFilter, MatchKind::Filter) => {
572 depth -= 1;
573 if depth == 0 { return i; }
574 }
575 (DisplayListItem::PushReferenceFrame { .. }, MatchKind::ReferenceFrame) => depth += 1,
576 (DisplayListItem::PopReferenceFrame, MatchKind::ReferenceFrame) => {
577 depth -= 1;
578 if depth == 0 { return i; }
579 }
580 _ => {}
581 }
582 }
583 items.len()
584}
585
586fn rect_intersection(a: &LogicalRect, b: &LogicalRect) -> Option<LogicalRect> {
588 let x1 = a.origin.x.max(b.origin.x);
589 let y1 = a.origin.y.max(b.origin.y);
590 let x2 = (a.origin.x + a.size.width).min(b.origin.x + b.size.width);
591 let y2 = (a.origin.y + a.size.height).min(b.origin.y + b.size.height);
592 if x2 > x1 && y2 > y1 {
593 Some(LogicalRect {
594 origin: LogicalPosition { x: x1, y: y1 },
595 size: LogicalSize { width: x2 - x1, height: y2 - y1 },
596 })
597 } else {
598 None
599 }
600}
601
602fn blit_pixmap(src: &AzulPixmap, dst: &mut AzulPixmap, px_x: i32, px_y: i32, opacity: f32) {
604 let sw = src.width as i32;
605 let sh = src.height as i32;
606 let dw = dst.width as i32;
607 let dh = dst.height as i32;
608 let op = (opacity * 255.0).clamp(0.0, 255.0) as u32;
609
610 for sy in 0..sh {
611 let dy = px_y + sy;
612 if dy < 0 || dy >= dh { continue; }
613 for sx in 0..sw {
614 let dx = px_x + sx;
615 if dx < 0 || dx >= dw { continue; }
616 let si = ((sy * sw + sx) * 4) as usize;
617 let di = ((dy * dw + dx) * 4) as usize;
618 if si + 3 >= src.data.len() || di + 3 >= dst.data.len() { continue; }
619
620 let sr = src.data[si] as u32;
621 let sg = src.data[si + 1] as u32;
622 let sb = src.data[si + 2] as u32;
623 let sa = (src.data[si + 3] as u32 * op) / 255;
624
625 if sa == 0 { continue; }
626 if sa == 255 {
627 dst.data[di] = sr as u8;
628 dst.data[di + 1] = sg as u8;
629 dst.data[di + 2] = sb as u8;
630 dst.data[di + 3] = 255;
631 } else {
632 let inv_sa = 255 - sa;
633 dst.data[di] = ((sr * sa + dst.data[di] as u32 * inv_sa) / 255) as u8;
634 dst.data[di + 1] = ((sg * sa + dst.data[di + 1] as u32 * inv_sa) / 255) as u8;
635 dst.data[di + 2] = ((sb * sa + dst.data[di + 2] as u32 * inv_sa) / 255) as u8;
636 dst.data[di + 3] = ((sa + dst.data[di + 3] as u32 * inv_sa / 255).min(255)) as u8;
637 }
638 }
639 }
640}
641
642fn shift_pixbuf(pixmap: &mut AzulPixmap, dx: i32, dy: i32) {
644 let w = pixmap.width as i32;
645 let h = pixmap.height as i32;
646 if dx.abs() >= w || dy.abs() >= h {
647 pixmap.fill(0, 0, 0, 0);
649 return;
650 }
651
652 let stride = (w * 4) as usize;
653 let data = &mut pixmap.data;
654
655 if dy > 0 {
657 for row in (0..h - dy).rev() {
659 let src_start = (row * w * 4) as usize;
660 let dst_start = ((row + dy) * w * 4) as usize;
661 data.copy_within(src_start..src_start + stride, dst_start);
662 }
663 for row in 0..dy {
665 let start = (row * w * 4) as usize;
666 data[start..start + stride].fill(0);
667 }
668 } else if dy < 0 {
669 let ady = (-dy) as i32;
670 for row in ady..h {
672 let src_start = (row * w * 4) as usize;
673 let dst_start = ((row - ady) * w * 4) as usize;
674 data.copy_within(src_start..src_start + stride, dst_start);
675 }
676 for row in (h - ady)..h {
678 let start = (row * w * 4) as usize;
679 data[start..start + stride].fill(0);
680 }
681 }
682
683 if dx > 0 {
685 for row in 0..h {
686 let row_start = (row * w * 4) as usize;
687 let shift = (dx * 4) as usize;
688 data.copy_within(row_start..row_start + stride - shift, row_start + shift);
690 data[row_start..row_start + shift].fill(0);
692 }
693 } else if dx < 0 {
694 let adx = (-dx * 4) as usize;
695 for row in 0..h {
696 let row_start = (row * w * 4) as usize;
697 data.copy_within(row_start + adx..row_start + stride, row_start);
698 data[row_start + stride - adx..row_start + stride].fill(0);
700 }
701 }
702}
703
704fn compute_exposed_rects(bounds: &LogicalRect, dx: f32, dy: f32) -> Vec<LogicalRect> {
708 let w = bounds.size.width;
709 let h = bounds.size.height;
710 let mut rects = Vec::new();
711
712 if dy.abs() > 0.5 {
714 let strip = if dy > 0.0 {
715 LogicalRect {
717 origin: LogicalPosition { x: bounds.origin.x, y: bounds.origin.y },
718 size: LogicalSize { width: w, height: dy.min(h) },
719 }
720 } else {
721 LogicalRect {
723 origin: LogicalPosition { x: bounds.origin.x, y: bounds.origin.y + h + dy },
724 size: LogicalSize { width: w, height: (-dy).min(h) },
725 }
726 };
727 rects.push(strip);
728 }
729
730 if dx.abs() > 0.5 {
732 let strip = if dx > 0.0 {
733 LogicalRect {
734 origin: LogicalPosition { x: bounds.origin.x, y: bounds.origin.y },
735 size: LogicalSize { width: dx.min(w), height: h },
736 }
737 } else {
738 LogicalRect {
739 origin: LogicalPosition { x: bounds.origin.x + w + dx, y: bounds.origin.y },
740 size: LogicalSize { width: (-dx).min(w), height: h },
741 }
742 };
743 rects.push(strip);
744 }
745
746 rects
747}
748
749fn apply_layer_filters(pixmap: &mut AzulPixmap, filters: &[StyleFilter], dpi_factor: f32) {
751 for filter in filters {
752 match filter {
753 StyleFilter::Blur(blur) => {
754 let rx = blur.width.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE) * dpi_factor;
755 let ry = blur.height.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE) * dpi_factor;
756 let radius = ((rx + ry) / 2.0).ceil() as u32;
757 if radius > 0 {
758 let w = pixmap.width;
759 let h = pixmap.height;
760 let stride = (w * 4) as i32;
761 let mut ra = unsafe {
762 RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride)
763 };
764 stack_blur_rgba32(&mut ra, radius, radius);
765 }
766 }
767 StyleFilter::Opacity(pct) => {
768 let op = (pct.normalized() * 255.0).clamp(0.0, 255.0) as u32;
769 for chunk in pixmap.data.chunks_exact_mut(4) {
770 chunk[3] = ((chunk[3] as u32 * op) / 255) as u8;
771 }
772 }
773 StyleFilter::Grayscale(pct) => {
774 let amount = pct.normalized().clamp(0.0, 1.0);
775 for chunk in pixmap.data.chunks_exact_mut(4) {
776 let r = chunk[0] as f32;
777 let g = chunk[1] as f32;
778 let b = chunk[2] as f32;
779 let gray = 0.2126 * r + 0.7152 * g + 0.0722 * b;
780 chunk[0] = (r + (gray - r) * amount).clamp(0.0, 255.0) as u8;
781 chunk[1] = (g + (gray - g) * amount).clamp(0.0, 255.0) as u8;
782 chunk[2] = (b + (gray - b) * amount).clamp(0.0, 255.0) as u8;
783 }
784 }
785 StyleFilter::Brightness(pct) => {
786 let factor = pct.normalized().max(0.0);
787 for chunk in pixmap.data.chunks_exact_mut(4) {
788 chunk[0] = (chunk[0] as f32 * factor).clamp(0.0, 255.0) as u8;
789 chunk[1] = (chunk[1] as f32 * factor).clamp(0.0, 255.0) as u8;
790 chunk[2] = (chunk[2] as f32 * factor).clamp(0.0, 255.0) as u8;
791 }
792 }
793 StyleFilter::Contrast(pct) => {
794 let factor = pct.normalized().max(0.0);
795 for chunk in pixmap.data.chunks_exact_mut(4) {
796 chunk[0] = ((((chunk[0] as f32 / 255.0) - 0.5) * factor + 0.5) * 255.0).clamp(0.0, 255.0) as u8;
797 chunk[1] = ((((chunk[1] as f32 / 255.0) - 0.5) * factor + 0.5) * 255.0).clamp(0.0, 255.0) as u8;
798 chunk[2] = ((((chunk[2] as f32 / 255.0) - 0.5) * factor + 0.5) * 255.0).clamp(0.0, 255.0) as u8;
799 }
800 }
801 StyleFilter::Invert(pct) => {
802 let amount = pct.normalized().clamp(0.0, 1.0);
803 for chunk in pixmap.data.chunks_exact_mut(4) {
804 chunk[0] = (chunk[0] as f32 + (255.0 - 2.0 * chunk[0] as f32) * amount).clamp(0.0, 255.0) as u8;
805 chunk[1] = (chunk[1] as f32 + (255.0 - 2.0 * chunk[1] as f32) * amount).clamp(0.0, 255.0) as u8;
806 chunk[2] = (chunk[2] as f32 + (255.0 - 2.0 * chunk[2] as f32) * amount).clamp(0.0, 255.0) as u8;
807 }
808 }
809 StyleFilter::Sepia(pct) => {
810 let amount = pct.normalized().clamp(0.0, 1.0);
811 for chunk in pixmap.data.chunks_exact_mut(4) {
812 let r = chunk[0] as f32;
813 let g = chunk[1] as f32;
814 let b = chunk[2] as f32;
815 let sr = (0.393 * r + 0.769 * g + 0.189 * b).min(255.0);
816 let sg = (0.349 * r + 0.686 * g + 0.168 * b).min(255.0);
817 let sb = (0.272 * r + 0.534 * g + 0.131 * b).min(255.0);
818 chunk[0] = (r + (sr - r) * amount).clamp(0.0, 255.0) as u8;
819 chunk[1] = (g + (sg - g) * amount).clamp(0.0, 255.0) as u8;
820 chunk[2] = (b + (sb - b) * amount).clamp(0.0, 255.0) as u8;
821 }
822 }
823 StyleFilter::Saturate(pct) => {
824 let s = pct.normalized().max(0.0);
825 for chunk in pixmap.data.chunks_exact_mut(4) {
826 let r = chunk[0] as f32;
827 let g = chunk[1] as f32;
828 let b = chunk[2] as f32;
829 let gray = 0.2126 * r + 0.7152 * g + 0.0722 * b;
830 chunk[0] = (gray + (r - gray) * s).clamp(0.0, 255.0) as u8;
831 chunk[1] = (gray + (g - gray) * s).clamp(0.0, 255.0) as u8;
832 chunk[2] = (gray + (b - gray) * s).clamp(0.0, 255.0) as u8;
833 }
834 }
835 StyleFilter::HueRotate(angle) => {
836 let rad = angle.to_degrees().to_radians();
837 let cos_a = rad.cos();
838 let sin_a = rad.sin();
839 for chunk in pixmap.data.chunks_exact_mut(4) {
840 let r = chunk[0] as f32;
841 let g = chunk[1] as f32;
842 let b = chunk[2] as f32;
843 let nr = (0.213 + 0.787 * cos_a - 0.213 * sin_a) * r
844 + (0.715 - 0.715 * cos_a - 0.715 * sin_a) * g
845 + (0.072 - 0.072 * cos_a + 0.928 * sin_a) * b;
846 let ng = (0.213 - 0.213 * cos_a + 0.143 * sin_a) * r
847 + (0.715 + 0.285 * cos_a + 0.140 * sin_a) * g
848 + (0.072 - 0.072 * cos_a - 0.283 * sin_a) * b;
849 let nb = (0.213 - 0.213 * cos_a - 0.787 * sin_a) * r
850 + (0.715 - 0.715 * cos_a + 0.715 * sin_a) * g
851 + (0.072 + 0.928 * cos_a + 0.072 * sin_a) * b;
852 chunk[0] = nr.clamp(0.0, 255.0) as u8;
853 chunk[1] = ng.clamp(0.0, 255.0) as u8;
854 chunk[2] = nb.clamp(0.0, 255.0) as u8;
855 }
856 }
857 _ => {} }
859 }
860}
861
862fn render_display_list_range(
865 display_list: &DisplayList,
866 pixmap: &mut AzulPixmap,
867 start: usize,
868 end: usize,
869 offset_x: f32,
870 offset_y: f32,
871 dpi_factor: f32,
872 renderer_resources: &RendererResources,
873 font_manager: Option<&FontManager<FontRef>>,
874 glyph_cache: &mut GlyphCache,
875) -> Result<(), String> {
876 let empty_state = CpuRenderState::new(ScrollOffsetMap::new());
877 let render_state = &empty_state;
878 let mut transform_stack = vec![TransAffine::new()];
879 let mut clip_stack: Vec<Option<AzRect>> = vec![None];
880 let mut mask_stack: Vec<MaskEntry> = Vec::new();
881 let mut scroll_offset_stack: Vec<(f32, f32)> = vec![(0.0, 0.0)];
882
883 for i in start..end {
884 let item = &display_list.items[i];
885 render_single_item(
886 item,
887 pixmap,
888 dpi_factor,
889 renderer_resources,
890 font_manager,
891 glyph_cache,
892 &mut transform_stack,
893 &mut clip_stack,
894 &mut mask_stack,
895 &mut scroll_offset_stack,
896 render_state,
897 )?;
898 }
899
900 Ok(())
901}
902
903pub struct AzulPixmap {
909 data: Vec<u8>,
910 width: u32,
911 height: u32,
912}
913
914impl AzulPixmap {
915 pub fn new(width: u32, height: u32) -> Option<Self> {
917 if width == 0 || height == 0 {
918 return None;
919 }
920 let len = (width as usize) * (height as usize) * 4;
921 let data = vec![255u8; len]; Some(Self { data, width, height })
923 }
924
925 pub fn fill(&mut self, r: u8, g: u8, b: u8, a: u8) {
927 for chunk in self.data.chunks_exact_mut(4) {
928 chunk[0] = r;
929 chunk[1] = g;
930 chunk[2] = b;
931 chunk[3] = a;
932 }
933 }
934
935 pub fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, r: u8, g: u8, b: u8, a: u8) {
937 let pw = self.width as i32;
938 let ph = self.height as i32;
939 let x0 = x.max(0).min(pw);
940 let y0 = y.max(0).min(ph);
941 let x1 = (x + w).max(0).min(pw);
942 let y1 = (y + h).max(0).min(ph);
943 for row in y0..y1 {
944 let start = (row * pw + x0) as usize * 4;
945 let end = (row * pw + x1) as usize * 4;
946 if end <= self.data.len() {
947 for chunk in self.data[start..end].chunks_exact_mut(4) {
948 chunk[0] = r;
949 chunk[1] = g;
950 chunk[2] = b;
951 chunk[3] = a;
952 }
953 }
954 }
955 }
956
957 pub fn data(&self) -> &[u8] {
959 &self.data
960 }
961
962 pub fn data_mut(&mut self) -> &mut [u8] {
964 &mut self.data
965 }
966
967 pub fn width(&self) -> u32 {
969 self.width
970 }
971
972 pub fn height(&self) -> u32 {
974 self.height
975 }
976
977 pub fn clone_pixmap(&self) -> Self {
979 Self {
980 data: self.data.clone(),
981 width: self.width,
982 height: self.height,
983 }
984 }
985
986 pub fn resize_grow_only(
990 &mut self,
991 new_width: u32,
992 new_height: u32,
993 fill_r: u8, fill_g: u8, fill_b: u8, fill_a: u8,
994 ) -> Option<()> {
995 if new_width < self.width || new_height < self.height {
996 return None;
997 }
998 if new_width == self.width && new_height == self.height {
999 return Some(());
1000 }
1001
1002 let old_w = self.width as usize;
1003 let old_h = self.height as usize;
1004 let new_w = new_width as usize;
1005 let new_h = new_height as usize;
1006 let mut new_data = vec![fill_a; new_w * new_h * 4];
1007
1008 for chunk in new_data.chunks_exact_mut(4) {
1010 chunk[0] = fill_r;
1011 chunk[1] = fill_g;
1012 chunk[2] = fill_b;
1013 chunk[3] = fill_a;
1014 }
1015
1016 let old_stride = old_w * 4;
1018 let new_stride = new_w * 4;
1019 for row in 0..old_h {
1020 let src = row * old_stride;
1021 let dst = row * new_stride;
1022 new_data[dst..dst + old_stride]
1023 .copy_from_slice(&self.data[src..src + old_stride]);
1024 }
1025
1026 self.data = new_data;
1027 self.width = new_width;
1028 self.height = new_height;
1029 Some(())
1030 }
1031
1032 pub fn resize_reuse(
1035 &mut self,
1036 new_width: u32,
1037 new_height: u32,
1038 fill_r: u8, fill_g: u8, fill_b: u8, fill_a: u8,
1039 ) {
1040 if new_width == self.width && new_height == self.height {
1041 return;
1042 }
1043
1044 let old_w = self.width as usize;
1045 let old_h = self.height as usize;
1046 let new_w = new_width as usize;
1047 let new_h = new_height as usize;
1048 let new_stride = new_w * 4;
1049 let old_stride = old_w * 4;
1050
1051 let mut new_data = vec![0u8; new_w * new_h * 4];
1052
1053 for chunk in new_data.chunks_exact_mut(4) {
1055 chunk[0] = fill_r;
1056 chunk[1] = fill_g;
1057 chunk[2] = fill_b;
1058 chunk[3] = fill_a;
1059 }
1060
1061 let copy_rows = old_h.min(new_h);
1063 let copy_cols_bytes = old_stride.min(new_stride);
1064 for row in 0..copy_rows {
1065 let src = row * old_stride;
1066 let dst = row * new_stride;
1067 new_data[dst..dst + copy_cols_bytes]
1068 .copy_from_slice(&self.data[src..src + copy_cols_bytes]);
1069 }
1070
1071 self.data = new_data;
1072 self.width = new_width;
1073 self.height = new_height;
1074 }
1075
1076 pub fn encode_png(&self) -> Result<Vec<u8>, String> {
1078 let mut buf = Vec::new();
1079 {
1080 let mut encoder = png::Encoder::new(&mut buf, self.width, self.height);
1081 encoder.set_color(png::ColorType::Rgba);
1082 encoder.set_depth(png::BitDepth::Eight);
1083 let mut writer = encoder.write_header()
1084 .map_err(|e| format!("PNG header error: {}", e))?;
1085 writer.write_image_data(&self.data)
1086 .map_err(|e| format!("PNG write error: {}", e))?;
1087 }
1088 Ok(buf)
1089 }
1090
1091 pub fn decode_png(png_bytes: &[u8]) -> Result<Self, String> {
1093 let decoder = png::Decoder::new(std::io::Cursor::new(png_bytes));
1094 let mut reader = decoder.read_info()
1095 .map_err(|e| format!("PNG decode error: {}", e))?;
1096 let buf_size = reader.output_buffer_size()
1097 .ok_or_else(|| "PNG: unknown output buffer size".to_string())?;
1098 let mut buf = vec![0u8; buf_size];
1099 let info = reader.next_frame(&mut buf)
1100 .map_err(|e| format!("PNG frame error: {}", e))?;
1101 let width = info.width;
1102 let height = info.height;
1103
1104 let data = match info.color_type {
1106 png::ColorType::Rgba => buf[..info.buffer_size()].to_vec(),
1107 png::ColorType::Rgb => {
1108 let mut rgba = Vec::with_capacity((width * height * 4) as usize);
1109 for chunk in buf[..info.buffer_size()].chunks_exact(3) {
1110 rgba.push(chunk[0]);
1111 rgba.push(chunk[1]);
1112 rgba.push(chunk[2]);
1113 rgba.push(255);
1114 }
1115 rgba
1116 }
1117 png::ColorType::Grayscale => {
1118 let mut rgba = Vec::with_capacity((width * height * 4) as usize);
1119 for &v in &buf[..info.buffer_size()] {
1120 rgba.push(v);
1121 rgba.push(v);
1122 rgba.push(v);
1123 rgba.push(255);
1124 }
1125 rgba
1126 }
1127 other => return Err(format!("Unsupported PNG color type: {:?}", other)),
1128 };
1129
1130 Ok(Self { data, width, height })
1131 }
1132}
1133
1134#[derive(Debug, Clone)]
1140pub struct PixelDiffResult {
1141 pub diff_count: u64,
1143 pub total_pixels: u64,
1145 pub max_delta: u8,
1147 pub dimensions_match: bool,
1149 pub ref_width: u32,
1151 pub ref_height: u32,
1153 pub test_width: u32,
1155 pub test_height: u32,
1157}
1158
1159impl PixelDiffResult {
1160 pub fn is_match(&self) -> bool {
1162 self.dimensions_match && self.diff_count == 0
1163 }
1164
1165 pub fn diff_ratio(&self) -> f64 {
1167 if self.total_pixels == 0 { 0.0 }
1168 else { self.diff_count as f64 / self.total_pixels as f64 }
1169 }
1170}
1171
1172pub fn pixel_diff(reference: &AzulPixmap, test: &AzulPixmap, threshold: u8) -> PixelDiffResult {
1177 let dimensions_match = reference.width == test.width && reference.height == test.height;
1178 if !dimensions_match {
1179 return PixelDiffResult {
1180 diff_count: 0,
1181 total_pixels: 0,
1182 max_delta: 0,
1183 dimensions_match: false,
1184 ref_width: reference.width,
1185 ref_height: reference.height,
1186 test_width: test.width,
1187 test_height: test.height,
1188 };
1189 }
1190
1191 let total_pixels = (reference.width as u64) * (reference.height as u64);
1192 let mut diff_count = 0u64;
1193 let mut max_delta = 0u8;
1194
1195 for (ref_chunk, test_chunk) in reference.data.chunks_exact(4).zip(test.data.chunks_exact(4)) {
1196 let mut pixel_differs = false;
1197 for c in 0..4 {
1198 let delta = (ref_chunk[c] as i16 - test_chunk[c] as i16).unsigned_abs() as u8;
1199 if delta > threshold {
1200 pixel_differs = true;
1201 }
1202 if delta > max_delta {
1203 max_delta = delta;
1204 }
1205 }
1206 if pixel_differs {
1207 diff_count += 1;
1208 }
1209 }
1210
1211 PixelDiffResult {
1212 diff_count,
1213 total_pixels,
1214 max_delta,
1215 dimensions_match: true,
1216 ref_width: reference.width,
1217 ref_height: reference.height,
1218 test_width: test.width,
1219 test_height: test.height,
1220 }
1221}
1222
1223pub fn compare_against_reference(
1228 rendered: &AzulPixmap,
1229 reference_png_path: &str,
1230 threshold: u8,
1231) -> Result<PixelDiffResult, String> {
1232 let ref_bytes = std::fs::read(reference_png_path)
1233 .map_err(|e| format!("Cannot read reference image {}: {}", reference_png_path, e))?;
1234 let reference = AzulPixmap::decode_png(&ref_bytes)?;
1235 Ok(pixel_diff(&reference, rendered, threshold))
1236}
1237
1238#[derive(Debug, Clone, Copy)]
1243struct AzRect {
1244 x: f32,
1245 y: f32,
1246 width: f32,
1247 height: f32,
1248}
1249
1250impl AzRect {
1251 fn from_xywh(x: f32, y: f32, w: f32, h: f32) -> Option<Self> {
1252 if w <= 0.0 || h <= 0.0 || !x.is_finite() || !y.is_finite() || !w.is_finite() || !h.is_finite() {
1253 return None;
1254 }
1255 Some(Self { x, y, width: w, height: h })
1256 }
1257
1258 fn clip(&self, clip: &AzRect) -> Option<AzRect> {
1260 let x1 = self.x.max(clip.x);
1261 let y1 = self.y.max(clip.y);
1262 let x2 = (self.x + self.width).min(clip.x + clip.width);
1263 let y2 = (self.y + self.height).min(clip.y + clip.height);
1264 if x2 > x1 && y2 > y1 {
1265 Some(AzRect { x: x1, y: y1, width: x2 - x1, height: y2 - y1 })
1266 } else {
1267 None
1268 }
1269 }
1270}
1271
1272fn agg_fill_path(
1277 pixmap: &mut AzulPixmap,
1278 path: &mut dyn VertexSource,
1279 color: &Rgba8,
1280 rule: FillingRule,
1281) {
1282 agg_fill_path_clipped(pixmap, path, color, rule, None);
1283}
1284
1285fn agg_fill_path_clipped(
1292 pixmap: &mut AzulPixmap,
1293 path: &mut dyn VertexSource,
1294 color: &Rgba8,
1295 rule: FillingRule,
1296 clip: Option<AzRect>,
1297) {
1298 let w = pixmap.width;
1299 let h = pixmap.height;
1300 let stride = (w * 4) as i32;
1301 let mut ra = unsafe {
1302 RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride)
1303 };
1304 let mut pf = PixfmtRgba32::new(&mut ra);
1305 let mut rb = RendererBase::new(pf);
1306 if let Some(c) = clip {
1307 rb.clip_box_i(
1308 c.x as i32,
1309 c.y as i32,
1310 (c.x + c.width) as i32 - 1,
1311 (c.y + c.height) as i32 - 1,
1312 );
1313 }
1314 let mut ras = RasterizerScanlineAa::new();
1315 ras.filling_rule(rule);
1316 ras.add_path(path, 0);
1317 let mut sl = ScanlineU8::new();
1318 render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, color);
1319}
1320
1321fn agg_fill_transformed_path(
1322 pixmap: &mut AzulPixmap,
1323 path: &mut PathStorage,
1324 color: &Rgba8,
1325 rule: FillingRule,
1326 transform: &TransAffine,
1327) {
1328 agg_fill_transformed_path_clipped(pixmap, path, color, rule, transform, None);
1329}
1330
1331fn agg_fill_transformed_path_clipped(
1332 pixmap: &mut AzulPixmap,
1333 path: &mut PathStorage,
1334 color: &Rgba8,
1335 rule: FillingRule,
1336 transform: &TransAffine,
1337 clip: Option<AzRect>,
1338) {
1339 if transform.is_identity(IDENTITY_EPSILON_F64) {
1340 agg_fill_path_clipped(pixmap, path, color, rule, clip);
1341 } else {
1342 let mut transformed = ConvTransform::new(path, transform.clone());
1343 agg_fill_path_clipped(pixmap, &mut transformed, color, rule, clip);
1344 }
1345}
1346
1347fn agg_fill_gradient<G: GradientFunction>(
1352 pixmap: &mut AzulPixmap,
1353 path: &mut dyn VertexSource,
1354 lut: &GradientLut,
1355 gradient_fn: G,
1356 transform: TransAffine,
1357 d1: f64,
1358 d2: f64,
1359) {
1360 agg_fill_gradient_clipped(pixmap, path, lut, gradient_fn, transform, d1, d2, None);
1361}
1362
1363fn agg_fill_gradient_clipped<G: GradientFunction>(
1364 pixmap: &mut AzulPixmap,
1365 path: &mut dyn VertexSource,
1366 lut: &GradientLut,
1367 gradient_fn: G,
1368 transform: TransAffine,
1369 d1: f64,
1370 d2: f64,
1371 clip: Option<AzRect>,
1372) {
1373 let w = pixmap.width;
1374 let h = pixmap.height;
1375 let stride = (w * 4) as i32;
1376 let mut ra = unsafe {
1377 RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride)
1378 };
1379 let mut pf = PixfmtRgba32::new(&mut ra);
1380 let mut rb = RendererBase::new(pf);
1381 if let Some(c) = clip {
1382 rb.clip_box_i(
1383 c.x as i32,
1384 c.y as i32,
1385 (c.x + c.width) as i32 - 1,
1386 (c.y + c.height) as i32 - 1,
1387 );
1388 }
1389 let mut ras = RasterizerScanlineAa::new();
1390 ras.filling_rule(FillingRule::NonZero);
1391 ras.add_path(path, 0);
1392 let mut sl = ScanlineU8::new();
1393
1394 let interp = SpanInterpolatorLinear::new(transform);
1395 let mut sg = SpanGradient::new(interp, gradient_fn, lut, d1, d2);
1396 let mut alloc = SpanAllocator::<Rgba8>::new();
1397 render_scanlines_aa(&mut ras, &mut sl, &mut rb, &mut alloc, &mut sg);
1398}
1399
1400const SYSTEM_COLOR_FALLBACK: ColorU = ColorU { r: 0, g: 0, b: 0, a: 0 };
1413
1414fn resolve_color(
1420 color: &ColorOrSystem,
1421 system_colors: Option<&azul_css::system::SystemColors>,
1422) -> ColorU {
1423 match (color, system_colors) {
1424 (ColorOrSystem::Color(c), _) => *c,
1425 (ColorOrSystem::System(_), Some(sc)) => color.resolve(sc, SYSTEM_COLOR_FALLBACK),
1426 (ColorOrSystem::System(_), None) => SYSTEM_COLOR_FALLBACK,
1427 }
1428}
1429
1430fn build_gradient_lut_linear(
1432 stops: &azul_css::props::style::background::NormalizedLinearColorStopVec,
1433 system_colors: Option<&azul_css::system::SystemColors>,
1434) -> GradientLut {
1435 let mut lut = GradientLut::new_default();
1436 let stops_slice = stops.as_ref();
1437 if stops_slice.len() < 2 {
1438 lut.add_color(0.0, Rgba8::new(0, 0, 0, 0));
1440 lut.add_color(1.0, Rgba8::new(0, 0, 0, 0));
1441 lut.build_lut();
1442 return lut;
1443 }
1444 for stop in stops_slice {
1445 let offset = stop.offset.normalized() as f64; let c = resolve_color(&stop.color, system_colors);
1447 lut.add_color(offset, Rgba8::new(c.r as u32, c.g as u32, c.b as u32, c.a as u32));
1448 }
1449 lut.build_lut();
1450 lut
1451}
1452
1453fn build_gradient_lut_radial(
1455 stops: &azul_css::props::style::background::NormalizedRadialColorStopVec,
1456 system_colors: Option<&azul_css::system::SystemColors>,
1457) -> GradientLut {
1458 let mut lut = GradientLut::new_default();
1459 let stops_slice = stops.as_ref();
1460 if stops_slice.len() < 2 {
1461 lut.add_color(0.0, Rgba8::new(0, 0, 0, 0));
1462 lut.add_color(1.0, Rgba8::new(0, 0, 0, 0));
1463 lut.build_lut();
1464 return lut;
1465 }
1466 for stop in stops_slice {
1467 let offset = (stop.angle.to_degrees() / 360.0).clamp(0.0, 1.0) as f64;
1469 let c = resolve_color(&stop.color, system_colors);
1470 lut.add_color(offset, Rgba8::new(c.r as u32, c.g as u32, c.b as u32, c.a as u32));
1471 }
1472 lut.build_lut();
1473 lut
1474}
1475
1476fn resolve_background_position(
1478 pos: &azul_css::props::style::background::StyleBackgroundPosition,
1479 width: f32,
1480 height: f32,
1481) -> (f32, f32) {
1482 use azul_css::props::style::background::{BackgroundPositionHorizontal, BackgroundPositionVertical};
1483
1484 let x = match pos.horizontal {
1485 BackgroundPositionHorizontal::Left => 0.0,
1486 BackgroundPositionHorizontal::Center => 0.5,
1487 BackgroundPositionHorizontal::Right => 1.0,
1488 BackgroundPositionHorizontal::Exact(px) => {
1489 let val = px.to_pixels_internal(width, 16.0, 16.0);
1490 if width > 0.0 { val / width } else { 0.5 }
1491 }
1492 };
1493 let y = match pos.vertical {
1494 BackgroundPositionVertical::Top => 0.0,
1495 BackgroundPositionVertical::Center => 0.5,
1496 BackgroundPositionVertical::Bottom => 1.0,
1497 BackgroundPositionVertical::Exact(px) => {
1498 let val = px.to_pixels_internal(height, 16.0, 16.0);
1499 if height > 0.0 { val / height } else { 0.5 }
1500 }
1501 };
1502 (x, y)
1503}
1504
1505fn render_linear_gradient(
1506 pixmap: &mut AzulPixmap,
1507 bounds: &LogicalRect,
1508 gradient: &azul_css::props::style::background::LinearGradient,
1509 border_radius: &BorderRadius,
1510 clip: Option<AzRect>,
1511 dpi_factor: f32,
1512 system_colors: Option<&azul_css::system::SystemColors>,
1513) -> Result<(), String> {
1514 use azul_css::props::basic::geometry::{LayoutRect, LayoutSize};
1515
1516 let rect = match logical_rect_to_az_rect(bounds, dpi_factor) {
1517 Some(r) => r,
1518 None => return Ok(()),
1519 };
1520
1521 let stops = gradient.stops.as_ref();
1522 if stops.is_empty() {
1523 return Ok(());
1524 }
1525
1526
1527 let lut = build_gradient_lut_linear(&gradient.stops, system_colors);
1528
1529 let layout_rect = LayoutRect {
1531 origin: azul_css::props::basic::geometry::LayoutPoint::new(0, 0),
1532 size: LayoutSize {
1533 width: (rect.width as isize),
1534 height: (rect.height as isize),
1535 },
1536 };
1537 let (from_pt, to_pt) = gradient.direction.to_points(&layout_rect);
1538
1539 let x1 = rect.x as f64 + from_pt.x as f64;
1541 let y1 = rect.y as f64 + from_pt.y as f64;
1542 let x2 = rect.x as f64 + to_pt.x as f64;
1543 let y2 = rect.y as f64 + to_pt.y as f64;
1544
1545 let dx = x2 - x1;
1546 let dy = y2 - y1;
1547 let len = (dx * dx + dy * dy).sqrt();
1548 if len < 0.001 {
1549 return Ok(());
1550 }
1551
1552 let mut transform = TransAffine::new_line_segment(x1, y1, x2, y2, 100.0);
1557 transform.invert();
1558
1559 let mut path = if border_radius.is_zero() {
1560 build_rect_path(&rect)
1561 } else {
1562 build_rounded_rect_path(&rect, border_radius, dpi_factor)
1563 };
1564
1565 agg_fill_gradient_clipped(pixmap, &mut path, &lut, GradientX, transform, 0.0, 100.0, clip);
1566 Ok(())
1567}
1568
1569fn render_radial_gradient(
1570 pixmap: &mut AzulPixmap,
1571 bounds: &LogicalRect,
1572 gradient: &azul_css::props::style::background::RadialGradient,
1573 border_radius: &BorderRadius,
1574 clip: Option<AzRect>,
1575 dpi_factor: f32,
1576 system_colors: Option<&azul_css::system::SystemColors>,
1577) -> Result<(), String> {
1578 use azul_css::props::style::background::{RadialGradientSize, Shape};
1579
1580 let rect = match logical_rect_to_az_rect(bounds, dpi_factor) {
1581 Some(r) => r,
1582 None => return Ok(()),
1583 };
1584
1585 let stops = gradient.stops.as_ref();
1586 if stops.is_empty() {
1587 return Ok(());
1588 }
1589
1590 let lut = build_gradient_lut_linear(&gradient.stops, system_colors);
1591
1592 let w = rect.width as f64;
1593 let h = rect.height as f64;
1594
1595 let (cx_frac, cy_frac) = resolve_background_position(&gradient.position, rect.width, rect.height);
1597 let cx = rect.x as f64 + cx_frac as f64 * w;
1598 let cy = rect.y as f64 + cy_frac as f64 * h;
1599
1600 let radius = match gradient.size {
1602 RadialGradientSize::ClosestSide => {
1603 let dx = (cx_frac as f64 * w).min((1.0 - cx_frac as f64) * w);
1604 let dy = (cy_frac as f64 * h).min((1.0 - cy_frac as f64) * h);
1605 match gradient.shape {
1606 Shape::Circle => dx.min(dy),
1607 Shape::Ellipse => dx.min(dy), }
1609 }
1610 RadialGradientSize::FarthestSide => {
1611 let dx = (cx_frac as f64 * w).max((1.0 - cx_frac as f64) * w);
1612 let dy = (cy_frac as f64 * h).max((1.0 - cy_frac as f64) * h);
1613 match gradient.shape {
1614 Shape::Circle => dx.max(dy),
1615 Shape::Ellipse => dx.max(dy),
1616 }
1617 }
1618 RadialGradientSize::ClosestCorner => {
1619 let dx = (cx_frac as f64 * w).min((1.0 - cx_frac as f64) * w);
1620 let dy = (cy_frac as f64 * h).min((1.0 - cy_frac as f64) * h);
1621 (dx * dx + dy * dy).sqrt()
1622 }
1623 RadialGradientSize::FarthestCorner => {
1624 let dx = (cx_frac as f64 * w).max((1.0 - cx_frac as f64) * w);
1625 let dy = (cy_frac as f64 * h).max((1.0 - cy_frac as f64) * h);
1626 (dx * dx + dy * dy).sqrt()
1627 }
1628 };
1629
1630 if radius < 0.001 {
1631 return Ok(());
1632 }
1633
1634 let mut transform = TransAffine::new_scaling_uniform(radius / 100.0);
1638 transform.translate(cx, cy);
1639 transform.invert();
1640
1641 let mut path = if border_radius.is_zero() {
1642 build_rect_path(&rect)
1643 } else {
1644 build_rounded_rect_path(&rect, border_radius, dpi_factor)
1645 };
1646
1647 agg_fill_gradient_clipped(pixmap, &mut path, &lut, GradientRadialD, transform, 0.0, 100.0, clip);
1648 Ok(())
1649}
1650
1651fn render_conic_gradient(
1652 pixmap: &mut AzulPixmap,
1653 bounds: &LogicalRect,
1654 gradient: &azul_css::props::style::background::ConicGradient,
1655 border_radius: &BorderRadius,
1656 clip: Option<AzRect>,
1657 dpi_factor: f32,
1658 system_colors: Option<&azul_css::system::SystemColors>,
1659) -> Result<(), String> {
1660 let rect = match logical_rect_to_az_rect(bounds, dpi_factor) {
1661 Some(r) => r,
1662 None => return Ok(()),
1663 };
1664
1665 let stops = gradient.stops.as_ref();
1666 if stops.is_empty() {
1667 return Ok(());
1668 }
1669
1670 let lut = build_gradient_lut_radial(&gradient.stops, system_colors);
1671
1672 let w = rect.width as f64;
1673 let h = rect.height as f64;
1674
1675 let (cx_frac, cy_frac) = resolve_background_position(&gradient.center, rect.width, rect.height);
1677 let cx = rect.x as f64 + cx_frac as f64 * w;
1678 let cy = rect.y as f64 + cy_frac as f64 * h;
1679
1680 let start_angle_deg = gradient.angle.to_degrees();
1682 let start_angle_rad = ((start_angle_deg - 90.0) as f64).to_radians();
1683
1684 let mut transform = TransAffine::new_rotation(start_angle_rad);
1688 transform.translate(cx, cy);
1689 transform.invert();
1690
1691 let d2 = 100.0;
1694
1695 let mut path = if border_radius.is_zero() {
1696 build_rect_path(&rect)
1697 } else {
1698 build_rounded_rect_path(&rect, border_radius, dpi_factor)
1699 };
1700
1701 agg_fill_gradient_clipped(pixmap, &mut path, &lut, GradientConic, transform, 0.0, d2, clip);
1702 Ok(())
1703}
1704
1705fn render_box_shadow(
1710 pixmap: &mut AzulPixmap,
1711 bounds: &LogicalRect,
1712 shadow: &azul_css::props::style::box_shadow::StyleBoxShadow,
1713 border_radius: &BorderRadius,
1714 dpi_factor: f32,
1715) -> Result<(), String> {
1716 use azul_css::props::style::box_shadow::BoxShadowClipMode;
1717
1718 let rect = match logical_rect_to_az_rect(bounds, dpi_factor) {
1719 Some(r) => r,
1720 None => return Ok(()),
1721 };
1722
1723 let offset_x = shadow.offset_x.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE) * dpi_factor;
1724 let offset_y = shadow.offset_y.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE) * dpi_factor;
1725 let blur_r = (shadow.blur_radius.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE) * dpi_factor).max(0.0);
1726 let spread = shadow.spread_radius.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE) * dpi_factor;
1727
1728 let color = shadow.color;
1729 if color.a == 0 {
1730 return Ok(());
1731 }
1732
1733 let padding = blur_r.ceil();
1735 let shadow_x = rect.x + offset_x - spread - padding;
1736 let shadow_y = rect.y + offset_y - spread - padding;
1737 let shadow_w = rect.width + 2.0 * spread + 2.0 * padding;
1738 let shadow_h = rect.height + 2.0 * spread + 2.0 * padding;
1739
1740 if shadow_w <= 0.0 || shadow_h <= 0.0 {
1741 return Ok(());
1742 }
1743
1744 let sw = shadow_w.ceil() as u32;
1745 let sh = shadow_h.ceil() as u32;
1746
1747 if sw == 0 || sh == 0 || sw > MAX_SHADOW_PIXBUF_SIZE || sh > MAX_SHADOW_PIXBUF_SIZE {
1748 return Ok(());
1749 }
1750
1751 let mut tmp = AzulPixmap::new(sw, sh).ok_or("cannot create shadow pixmap")?;
1753 tmp.fill(0, 0, 0, 0); let shape_x = padding + spread;
1757 let shape_y = padding + spread;
1758 let shape_rect = match AzRect::from_xywh(shape_x, shape_y, rect.width, rect.height) {
1759 Some(r) => r,
1760 None => return Ok(()),
1761 };
1762
1763 let agg_color = Rgba8::new(color.r as u32, color.g as u32, color.b as u32, color.a as u32);
1764 if border_radius.is_zero() {
1765 let mut path = build_rect_path(&shape_rect);
1766 agg_fill_path(&mut tmp, &mut path, &agg_color, FillingRule::NonZero);
1767 } else {
1768 let mut path = build_rounded_rect_path(&shape_rect, border_radius, dpi_factor);
1769 agg_fill_path(&mut tmp, &mut path, &agg_color, FillingRule::NonZero);
1770 }
1771
1772 if blur_r > 0.5 {
1774 let blur_radius = (blur_r.ceil() as u32).min(254);
1775 let stride = (sw * 4) as i32;
1776 let mut ra = unsafe {
1777 RowAccessor::new_with_buf(tmp.data.as_mut_ptr(), sw, sh, stride)
1778 };
1779 stack_blur_rgba32(&mut ra, blur_radius, blur_radius);
1780 }
1781
1782 let dst_x = shadow_x as i32;
1784 let dst_y = shadow_y as i32;
1785 blit_buffer(pixmap, &tmp.data, sw, sh, dst_x, dst_y);
1786
1787 Ok(())
1788}
1789
1790fn blit_buffer(dst: &mut AzulPixmap, src: &[u8], src_w: u32, src_h: u32, dx: i32, dy: i32) {
1792 let dw = dst.width as i32;
1793 let dh = dst.height as i32;
1794
1795 for py in 0..src_h as i32 {
1796 let ty = dy + py;
1797 if ty < 0 || ty >= dh {
1798 continue;
1799 }
1800 for px in 0..src_w as i32 {
1801 let tx = dx + px;
1802 if tx < 0 || tx >= dw {
1803 continue;
1804 }
1805
1806 let si = ((py as u32 * src_w + px as u32) * 4) as usize;
1807 let di = ((ty as u32 * dst.width + tx as u32) * 4) as usize;
1808
1809 if si + 3 >= src.len() || di + 3 >= dst.data.len() {
1810 continue;
1811 }
1812
1813 let sa = src[si + 3] as u32;
1814 if sa == 0 {
1815 continue;
1816 }
1817 if sa == 255 {
1818 dst.data[di] = src[si];
1819 dst.data[di + 1] = src[si + 1];
1820 dst.data[di + 2] = src[si + 2];
1821 dst.data[di + 3] = 255;
1822 } else {
1823 let inv_sa = 255 - sa;
1825 dst.data[di] = ((src[si] as u32 + dst.data[di] as u32 * inv_sa / 255).min(255)) as u8;
1826 dst.data[di + 1] = ((src[si + 1] as u32 + dst.data[di + 1] as u32 * inv_sa / 255).min(255)) as u8;
1827 dst.data[di + 2] = ((src[si + 2] as u32 + dst.data[di + 2] as u32 * inv_sa / 255).min(255)) as u8;
1828 dst.data[di + 3] = ((sa + dst.data[di + 3] as u32 * inv_sa / 255).min(255)) as u8;
1829 }
1830 }
1831 }
1832}
1833
1834enum MaskEntry {
1840 ImageMask {
1842 snapshot: Vec<u8>,
1843 mask_data: Vec<u8>,
1844 origin_x: i32,
1845 origin_y: i32,
1846 width: u32,
1847 height: u32,
1848 },
1849 Opacity {
1851 snapshot: Vec<u8>,
1852 rect: AzRect,
1853 opacity: f32,
1854 },
1855}
1856
1857fn snapshot_region(pixmap: &AzulPixmap, x: i32, y: i32, w: u32, h: u32) -> Vec<u8> {
1859 let pw = pixmap.width as i32;
1860 let ph = pixmap.height as i32;
1861 let mut snap = vec![0u8; (w as usize) * (h as usize) * 4];
1862
1863 for py in 0..h as i32 {
1864 let sy = y + py;
1865 if sy < 0 || sy >= ph {
1866 continue;
1867 }
1868 for px in 0..w as i32 {
1869 let sx = x + px;
1870 if sx < 0 || sx >= pw {
1871 continue;
1872 }
1873 let si = ((sy as u32 * pixmap.width + sx as u32) * 4) as usize;
1874 let di = ((py as u32 * w + px as u32) * 4) as usize;
1875 if si + 3 < pixmap.data.len() && di + 3 < snap.len() {
1876 snap[di] = pixmap.data[si];
1877 snap[di + 1] = pixmap.data[si + 1];
1878 snap[di + 2] = pixmap.data[si + 2];
1879 snap[di + 3] = pixmap.data[si + 3];
1880 }
1881 }
1882 }
1883 snap
1884}
1885
1886fn extract_mask_data(mask_image: &ImageRef, target_w: u32, target_h: u32) -> Option<Vec<u8>> {
1888 let image_data = mask_image.get_data();
1889 let (mask_bytes, src_w, src_h) = match &*image_data {
1890 DecodedImage::Raw((descriptor, data)) => {
1891 let w = descriptor.width as u32;
1892 let h = descriptor.height as u32;
1893 if w == 0 || h == 0 {
1894 return None;
1895 }
1896 let bytes = match data {
1897 azul_core::resources::ImageData::Raw(shared) => shared.as_ref(),
1898 _ => return None,
1899 };
1900 match descriptor.format {
1901 azul_core::resources::RawImageFormat::R8 => {
1902 (bytes.to_vec(), w, h)
1903 }
1904 azul_core::resources::RawImageFormat::BGRA8 => {
1905 let mut r8 = Vec::with_capacity((w * h) as usize);
1907 for chunk in bytes.chunks_exact(4) {
1908 r8.push(chunk[3]); }
1910 (r8, w, h)
1911 }
1912 _ => {
1913 let chan_count = bytes.len() / (w * h) as usize;
1915 if chan_count == 0 {
1916 return None;
1917 }
1918 let mut r8 = Vec::with_capacity((w * h) as usize);
1919 for i in 0..(w * h) as usize {
1920 r8.push(bytes[i * chan_count]);
1921 }
1922 (r8, w, h)
1923 }
1924 }
1925 }
1926 _ => return None,
1927 };
1928
1929 if target_w == 0 || target_h == 0 {
1930 return None;
1931 }
1932
1933 let mut scaled = vec![0u8; (target_w * target_h) as usize];
1935 let sx = src_w as f32 / target_w as f32;
1936 let sy = src_h as f32 / target_h as f32;
1937 for py in 0..target_h {
1938 for px in 0..target_w {
1939 let mx = ((px as f32 * sx) as u32).min(src_w - 1);
1940 let my = ((py as f32 * sy) as u32).min(src_h - 1);
1941 scaled[(py * target_w + px) as usize] = mask_bytes[(my * src_w + mx) as usize];
1942 }
1943 }
1944 Some(scaled)
1945}
1946
1947fn apply_mask(pixmap: &mut AzulPixmap, entry: &MaskEntry) {
1950 let (snapshot, mask_data, origin_x, origin_y, width, height) = match entry {
1951 MaskEntry::ImageMask { snapshot, mask_data, origin_x, origin_y, width, height } => {
1952 (snapshot, mask_data.as_slice(), *origin_x, *origin_y, *width, *height)
1953 }
1954 _ => return,
1955 };
1956
1957 let pw = pixmap.width as i32;
1958 let ph = pixmap.height as i32;
1959
1960 for py in 0..height as i32 {
1961 let dy = origin_y + py;
1962 if dy < 0 || dy >= ph {
1963 continue;
1964 }
1965 for px in 0..width as i32 {
1966 let dx = origin_x + px;
1967 if dx < 0 || dx >= pw {
1968 continue;
1969 }
1970
1971 let mi = (py as u32 * width + px as u32) as usize;
1972 let mask_val = mask_data.get(mi).copied().unwrap_or(0) as u32;
1973
1974 let pi = ((dy as u32 * pixmap.width + dx as u32) * 4) as usize;
1975 let si = ((py as u32 * width + px as u32) * 4) as usize;
1976
1977 if pi + 3 >= pixmap.data.len() || si + 3 >= snapshot.len() {
1978 continue;
1979 }
1980
1981 let inv_mask = 255 - mask_val;
1984 for c in 0..4 {
1985 let snap_c = snapshot[si + c] as u32;
1986 let cur_c = pixmap.data[pi + c] as u32;
1987 pixmap.data[pi + c] = ((cur_c * mask_val + snap_c * inv_mask) / 255) as u8;
1988 }
1989 }
1990 }
1991}
1992
1993pub struct RenderOptions {
1998 pub width: f32,
1999 pub height: f32,
2000 pub dpi_factor: f32,
2001}
2002
2003fn acquire_pixmap(retained: Option<AzulPixmap>, w: u32, h: u32) -> Result<AzulPixmap, String> {
2005 if let Some(p) = retained {
2006 if p.width == w && p.height == h {
2007 return Ok(p);
2008 }
2009 }
2010 AzulPixmap::new(w, h).ok_or_else(|| "cannot create pixmap".to_string())
2011}
2012
2013pub fn render(
2014 dl: &DisplayList,
2015 res: &RendererResources,
2016 opts: RenderOptions,
2017 glyph_cache: &mut GlyphCache,
2018) -> Result<AzulPixmap, String> {
2019 let RenderOptions {
2020 width,
2021 height,
2022 dpi_factor,
2023 } = opts;
2024
2025 let mut pixmap = acquire_pixmap(None, (width * dpi_factor) as u32, (height * dpi_factor) as u32)?;
2026 pixmap.fill(255, 255, 255, 255);
2027
2028 render_display_list(dl, &mut pixmap, dpi_factor, res, None, glyph_cache)?;
2029
2030 Ok(pixmap)
2031}
2032
2033pub fn render_with_font_manager(
2036 dl: &DisplayList,
2037 res: &RendererResources,
2038 font_manager: &FontManager<FontRef>,
2039 opts: RenderOptions,
2040 glyph_cache: &mut GlyphCache,
2041) -> Result<AzulPixmap, String> {
2042 let empty_state = CpuRenderState::new(ScrollOffsetMap::new());
2043 render_with_font_manager_and_scroll(dl, res, font_manager, opts, glyph_cache, &empty_state)
2044}
2045
2046pub fn render_with_font_manager_and_scroll(
2049 dl: &DisplayList,
2050 res: &RendererResources,
2051 font_manager: &FontManager<FontRef>,
2052 opts: RenderOptions,
2053 glyph_cache: &mut GlyphCache,
2054 render_state: &CpuRenderState,
2055) -> Result<AzulPixmap, String> {
2056 render_with_font_manager_and_scroll_retained(dl, res, font_manager, opts, glyph_cache, render_state, None)
2057}
2058
2059pub fn render_with_font_manager_and_scroll_retained(
2063 dl: &DisplayList,
2064 res: &RendererResources,
2065 font_manager: &FontManager<FontRef>,
2066 opts: RenderOptions,
2067 glyph_cache: &mut GlyphCache,
2068 render_state: &CpuRenderState,
2069 retained: Option<AzulPixmap>,
2070) -> Result<AzulPixmap, String> {
2071 let RenderOptions {
2072 width,
2073 height,
2074 dpi_factor,
2075 } = opts;
2076
2077 let pw = (width * dpi_factor) as u32;
2078 let ph = (height * dpi_factor) as u32;
2079 let mut pixmap = acquire_pixmap(retained, pw, ph)?;
2080 pixmap.fill(255, 255, 255, 255);
2081
2082 render_display_list_with_state(dl, &mut pixmap, dpi_factor, res, Some(font_manager), glyph_cache, render_state)?;
2083
2084 Ok(pixmap)
2085}
2086
2087
2088pub type ScrollOffsetMap = HashMap<LocalScrollId, (f32, f32)>;
2092
2093pub fn compute_display_list_damage(
2101 old: &DisplayList,
2102 new: &DisplayList,
2103) -> Option<Vec<LogicalRect>> {
2104 if old.items.len() != new.items.len() {
2106 return None;
2107 }
2108
2109 let mut damage = Vec::new();
2110
2111 for (old_item, new_item) in old.items.iter().zip(new.items.iter()) {
2112 if std::mem::discriminant(old_item) != std::mem::discriminant(new_item) {
2114 return None; }
2116
2117 if !old_item.is_visually_equal(new_item) {
2121 let old_bounds = old_item.visual_bounds();
2122 let new_bounds = new_item.visual_bounds();
2123 if let Some(ob) = old_bounds { damage.push(ob); }
2124 if let Some(nb) = new_bounds { damage.push(nb); }
2125 }
2126 }
2127
2128 coalesce_damage_rects(&mut damage);
2130 Some(damage)
2131}
2132
2133fn coalesce_damage_rects(rects: &mut Vec<LogicalRect>) {
2135 if rects.len() <= 1 { return; }
2136
2137 let mut changed = true;
2139 while changed {
2140 changed = false;
2141 let mut i = 0;
2142 while i < rects.len() {
2143 let mut j = i + 1;
2144 while j < rects.len() {
2145 if rects_overlap_or_adjacent(&rects[i], &rects[j], 8.0) {
2148 rects[i] = union_rect(&rects[i], &rects[j]);
2149 rects.swap_remove(j);
2150 changed = true;
2151 } else {
2152 j += 1;
2153 }
2154 }
2155 i += 1;
2156 }
2157 }
2158}
2159
2160fn rects_overlap_or_adjacent(a: &LogicalRect, b: &LogicalRect, gap: f32) -> bool {
2161 a.origin.x - gap <= b.origin.x + b.size.width
2162 && b.origin.x - gap <= a.origin.x + a.size.width
2163 && a.origin.y - gap <= b.origin.y + b.size.height
2164 && b.origin.y - gap <= a.origin.y + a.size.height
2165}
2166
2167pub fn union_rect(a: &LogicalRect, b: &LogicalRect) -> LogicalRect {
2168 let x = a.origin.x.min(b.origin.x);
2169 let y = a.origin.y.min(b.origin.y);
2170 let right = (a.origin.x + a.size.width).max(b.origin.x + b.size.width);
2171 let bottom = (a.origin.y + a.size.height).max(b.origin.y + b.size.height);
2172 LogicalRect {
2173 origin: LogicalPosition { x, y },
2174 size: LogicalSize { width: right - x, height: bottom - y },
2175 }
2176}
2177
2178pub fn compute_resize_damage(
2181 old_width: f32, old_height: f32,
2182 new_width: f32, new_height: f32,
2183) -> Vec<LogicalRect> {
2184 let mut rects = Vec::new();
2185 if new_width > old_width {
2186 rects.push(LogicalRect {
2187 origin: LogicalPosition { x: old_width, y: 0.0 },
2188 size: LogicalSize { width: new_width - old_width, height: new_height },
2189 });
2190 }
2191 if new_height > old_height {
2192 rects.push(LogicalRect {
2193 origin: LogicalPosition { x: 0.0, y: old_height },
2194 size: LogicalSize {
2195 width: old_width.min(new_width),
2196 height: new_height - old_height,
2197 },
2198 });
2199 }
2200 rects
2201}
2202
2203pub fn compare_region(
2206 a: &AzulPixmap, b: &AzulPixmap,
2207 x: u32, y: u32, w: u32, h: u32,
2208 threshold: u8,
2209) -> usize {
2210 let mut diff_count = 0;
2211 for row in y..(y + h).min(a.height).min(b.height) {
2212 for col in x..(x + w).min(a.width).min(b.width) {
2213 let ai = (row * a.width + col) as usize * 4;
2214 let bi = (row * b.width + col) as usize * 4;
2215 if ai + 3 >= a.data.len() || bi + 3 >= b.data.len() { continue; }
2216 let dr = (a.data[ai] as i16 - b.data[bi] as i16).unsigned_abs() as u8;
2217 let dg = (a.data[ai+1] as i16 - b.data[bi+1] as i16).unsigned_abs() as u8;
2218 let db = (a.data[ai+2] as i16 - b.data[bi+2] as i16).unsigned_abs() as u8;
2219 if dr > threshold || dg > threshold || db > threshold {
2220 diff_count += 1;
2221 }
2222 }
2223 }
2224 diff_count
2225}
2226
2227pub struct CpuRenderState {
2233 pub scroll_offsets: ScrollOffsetMap,
2235 pub transforms: HashMap<usize, azul_core::transform::ComputedTransform3D>,
2238 pub opacities: HashMap<usize, f32>,
2242 pub system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
2246}
2247
2248impl CpuRenderState {
2249 pub fn new(scroll_offsets: ScrollOffsetMap) -> Self {
2250 Self {
2251 scroll_offsets,
2252 transforms: HashMap::new(),
2253 opacities: HashMap::new(),
2254 system_style: None,
2255 }
2256 }
2257
2258 pub fn with_system_style(
2261 mut self,
2262 system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
2263 ) -> Self {
2264 self.system_style = system_style;
2265 self
2266 }
2267
2268 pub fn from_gpu_cache(
2270 gpu_cache: Option<&azul_core::gpu::GpuValueCache>,
2271 dom_id: azul_core::dom::DomId,
2272 scroll_offsets: &ScrollOffsetMap,
2273 ) -> Self {
2274 let mut transforms = HashMap::new();
2275 let mut opacities = HashMap::new();
2276
2277 if let Some(cache) = gpu_cache {
2278 for (node_id, key) in &cache.transform_keys {
2280 if let Some(value) = cache.current_transform_values.get(node_id) {
2281 transforms.insert(key.id, value.clone());
2282 }
2283 }
2284 for (node_id, key) in &cache.h_transform_keys {
2286 if let Some(value) = cache.h_current_transform_values.get(node_id) {
2287 transforms.insert(key.id, value.clone());
2288 }
2289 }
2290 for (node_id, key) in &cache.css_transform_keys {
2292 if let Some(value) = cache.css_current_transform_values.get(node_id) {
2293 transforms.insert(key.id, value.clone());
2294 }
2295 }
2296 for ((d, node_id), key) in &cache.scrollbar_v_opacity_keys {
2298 if *d == dom_id {
2299 if let Some(&value) = cache.scrollbar_v_opacity_values.get(&(*d, *node_id)) {
2300 opacities.insert(key.id, value);
2301 }
2302 }
2303 }
2304 for ((d, node_id), key) in &cache.scrollbar_h_opacity_keys {
2306 if *d == dom_id {
2307 if let Some(&value) = cache.scrollbar_h_opacity_values.get(&(*d, *node_id)) {
2308 opacities.insert(key.id, value);
2309 }
2310 }
2311 }
2312 for (node_id, key) in &cache.opacity_keys {
2314 if let Some(&value) = cache.current_opacity_values.get(node_id) {
2315 opacities.insert(key.id, value);
2316 }
2317 }
2318 }
2319
2320 Self {
2321 scroll_offsets: scroll_offsets.clone(),
2322 transforms,
2323 opacities,
2324 system_style: None,
2325 }
2326 }
2327}
2328
2329fn render_display_list(
2330 display_list: &DisplayList,
2331 pixmap: &mut AzulPixmap,
2332 dpi_factor: f32,
2333 renderer_resources: &RendererResources,
2334 font_manager: Option<&FontManager<FontRef>>,
2335 glyph_cache: &mut GlyphCache,
2336) -> Result<(), String> {
2337 let empty_state = CpuRenderState::new(ScrollOffsetMap::new());
2338 render_display_list_with_state(display_list, pixmap, dpi_factor, renderer_resources, font_manager, glyph_cache, &empty_state)
2339}
2340
2341fn render_display_list_with_state(
2342 display_list: &DisplayList,
2343 pixmap: &mut AzulPixmap,
2344 dpi_factor: f32,
2345 renderer_resources: &RendererResources,
2346 font_manager: Option<&FontManager<FontRef>>,
2347 glyph_cache: &mut GlyphCache,
2348 render_state: &CpuRenderState,
2349) -> Result<(), String> {
2350 let mut transform_stack = vec![TransAffine::new()]; let mut clip_stack: Vec<Option<AzRect>> = vec![None];
2352 let mut mask_stack: Vec<MaskEntry> = Vec::new();
2353 let mut scroll_offset_stack: Vec<(f32, f32)> = vec![(0.0, 0.0)];
2358
2359 let _p_loop = crate::probe::Probe::span("raster_loop");
2360 for item in &display_list.items {
2361 let _p_item = crate::probe::Probe::span(probe_label_for_item(item));
2362 render_single_item(
2363 item,
2364 pixmap,
2365 dpi_factor,
2366 renderer_resources,
2367 font_manager,
2368 glyph_cache,
2369 &mut transform_stack,
2370 &mut clip_stack,
2371 &mut mask_stack,
2372 &mut scroll_offset_stack,
2373 render_state,
2374 )?;
2375 }
2376
2377 Ok(())
2378}
2379
2380#[inline]
2384fn probe_label_for_item(item: &DisplayListItem) -> &'static str {
2385 use crate::solver3::display_list::DisplayListItem as I;
2386 match item {
2387 I::Rect { .. } => "dl:rect",
2388 I::SelectionRect { .. } => "dl:sel_rect",
2389 I::CursorRect { .. } => "dl:cursor",
2390 I::Border { .. } => "dl:border",
2391 I::Text { .. } => "dl:text",
2392 I::TextLayout { .. } => "dl:text_layout",
2393 I::Image { .. } => "dl:image",
2394 I::ScrollBar { .. } => "dl:scrollbar_raw",
2395 I::ScrollBarStyled { .. } => "dl:scrollbar",
2396 I::PushClip { .. } => "dl:push_clip",
2397 I::PopClip => "dl:pop_clip",
2398 I::PushScrollFrame { .. } => "dl:push_scroll",
2399 I::PopScrollFrame => "dl:pop_scroll",
2400 I::PushStackingContext { .. } => "dl:push_stack",
2401 I::PopStackingContext => "dl:pop_stack",
2402 I::PushReferenceFrame { .. } => "dl:push_ref",
2403 I::PopReferenceFrame => "dl:pop_ref",
2404 I::PushOpacity { .. } => "dl:push_opacity",
2405 I::PopOpacity => "dl:pop_opacity",
2406 I::PushFilter { .. } => "dl:push_filter",
2407 I::PopFilter => "dl:pop_filter",
2408 I::PushBackdropFilter { .. } => "dl:push_bdfilter",
2409 I::PopBackdropFilter => "dl:pop_bdfilter",
2410 I::PushTextShadow { .. } => "dl:push_tshadow",
2411 I::PopTextShadow => "dl:pop_tshadow",
2412 I::PushImageMaskClip { .. } => "dl:push_imask",
2413 I::PopImageMaskClip => "dl:pop_imask",
2414 I::LinearGradient { .. } => "dl:linear_grad",
2415 I::RadialGradient { .. } => "dl:radial_grad",
2416 I::ConicGradient { .. } => "dl:conic_grad",
2417 I::BoxShadow { .. } => "dl:box_shadow",
2418 I::Underline { .. } => "dl:underline",
2419 I::Strikethrough { .. } => "dl:strike",
2420 I::Overline { .. } => "dl:overline",
2421 I::HitTestArea { .. } => "dl:hit",
2422 I::VirtualView { .. } => "dl:vview",
2423 I::VirtualViewPlaceholder { .. } => "dl:vview_ph",
2424 }
2425}
2426
2427pub fn render_display_list_damaged(
2436 display_list: &DisplayList,
2437 pixmap: &mut AzulPixmap,
2438 dpi_factor: f32,
2439 renderer_resources: &RendererResources,
2440 font_manager: Option<&FontManager<FontRef>>,
2441 glyph_cache: &mut GlyphCache,
2442 render_state: &CpuRenderState,
2443 damage_rects: &[LogicalRect],
2444) -> Result<(), String> {
2445 if damage_rects.is_empty() {
2446 return Ok(()); }
2448
2449 for dr in damage_rects {
2451 let px = (dr.origin.x * dpi_factor) as i32;
2452 let py = (dr.origin.y * dpi_factor) as i32;
2453 let pw = (dr.size.width * dpi_factor) as i32;
2454 let ph = (dr.size.height * dpi_factor) as i32;
2455 pixmap.fill_rect(px, py, pw, ph, 255, 255, 255, 255);
2456 }
2457
2458 let mut transform_stack = vec![TransAffine::new()];
2462 let mut clip_stack: Vec<Option<AzRect>> = vec![None]; let mut mask_stack: Vec<MaskEntry> = Vec::new();
2464 let mut scroll_offset_stack: Vec<(f32, f32)> = vec![(0.0, 0.0)];
2465
2466 for item in display_list.items.iter() {
2467 if !item.is_state_management() {
2470 if let Some(item_bounds) = item.bounds() {
2471 let hits_damage = damage_rects.iter().any(|dr| {
2473 rects_overlap_or_adjacent(&item_bounds, dr, 0.0)
2474 });
2475 if !hits_damage {
2476 continue;
2477 }
2478 }
2479 }
2480
2481 render_single_item(
2482 item,
2483 pixmap,
2484 dpi_factor,
2485 renderer_resources,
2486 font_manager,
2487 glyph_cache,
2488 &mut transform_stack,
2489 &mut clip_stack,
2490 &mut mask_stack,
2491 &mut scroll_offset_stack,
2492 render_state,
2493 )?;
2494 }
2495
2496 Ok(())
2497}
2498
2499fn render_single_item(
2500 item: &DisplayListItem,
2501 pixmap: &mut AzulPixmap,
2502 dpi_factor: f32,
2503 renderer_resources: &RendererResources,
2504 font_manager: Option<&FontManager<FontRef>>,
2505 glyph_cache: &mut GlyphCache,
2506 transform_stack: &mut Vec<TransAffine>,
2507 clip_stack: &mut Vec<Option<AzRect>>,
2508 mask_stack: &mut Vec<MaskEntry>,
2509 scroll_offset_stack: &mut Vec<(f32, f32)>,
2510 render_state: &CpuRenderState,
2511) -> Result<(), String> {
2512 let (scroll_dx, scroll_dy) = *scroll_offset_stack.last().unwrap_or(&(0.0, 0.0));
2515
2516 let scroll_rect = |r: &LogicalRect| -> LogicalRect {
2521 if scroll_dx == 0.0 && scroll_dy == 0.0 { return *r; }
2522 LogicalRect {
2523 origin: LogicalPosition {
2524 x: r.origin.x - scroll_dx,
2525 y: r.origin.y - scroll_dy,
2526 },
2527 size: r.size,
2528 }
2529 };
2530
2531 match item {
2532 DisplayListItem::Rect {
2533 bounds,
2534 color,
2535 border_radius,
2536 } => {
2537 let clip = *clip_stack.last().unwrap();
2538 render_rect(
2539 pixmap,
2540 &scroll_rect(bounds.inner()),
2541 *color,
2542 border_radius,
2543 clip,
2544 dpi_factor,
2545 )?;
2546 }
2547 DisplayListItem::SelectionRect {
2548 bounds,
2549 color,
2550 border_radius,
2551 } => {
2552 let clip = *clip_stack.last().unwrap();
2553 render_rect(
2554 pixmap,
2555 &scroll_rect(bounds.inner()),
2556 *color,
2557 border_radius,
2558 clip,
2559 dpi_factor,
2560 )?;
2561 }
2562 DisplayListItem::CursorRect { bounds, color } => {
2563 let clip = *clip_stack.last().unwrap();
2564 render_rect(
2565 pixmap,
2566 &scroll_rect(bounds.inner()),
2567 *color,
2568 &BorderRadius::default(),
2569 clip,
2570 dpi_factor,
2571 )?;
2572 }
2573 DisplayListItem::Border {
2574 bounds,
2575 widths,
2576 colors,
2577 styles,
2578 border_radius,
2579 } => {
2580 let default_color = ColorU { r: 0, g: 0, b: 0, a: 255 };
2581
2582 let w_top = widths.top.and_then(|w| w.get_property().cloned())
2583 .map(|w| w.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)).unwrap_or(0.0);
2584 let w_right = widths.right.and_then(|w| w.get_property().cloned())
2585 .map(|w| w.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)).unwrap_or(0.0);
2586 let w_bottom = widths.bottom.and_then(|w| w.get_property().cloned())
2587 .map(|w| w.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)).unwrap_or(0.0);
2588 let w_left = widths.left.and_then(|w| w.get_property().cloned())
2589 .map(|w| w.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)).unwrap_or(0.0);
2590
2591 let c_top = colors.top.and_then(|c| c.get_property().cloned())
2592 .map(|c| c.inner).unwrap_or(default_color);
2593 let c_right = colors.right.and_then(|c| c.get_property().cloned())
2594 .map(|c| c.inner).unwrap_or(default_color);
2595 let c_bottom = colors.bottom.and_then(|c| c.get_property().cloned())
2596 .map(|c| c.inner).unwrap_or(default_color);
2597 let c_left = colors.left.and_then(|c| c.get_property().cloned())
2598 .map(|c| c.inner).unwrap_or(default_color);
2599
2600 use azul_css::props::style::border::BorderStyle;
2601 let s_top = styles.top.and_then(|s| s.get_property().cloned())
2602 .map(|s| s.inner).unwrap_or(BorderStyle::Solid);
2603 let s_right = styles.right.and_then(|s| s.get_property().cloned())
2604 .map(|s| s.inner).unwrap_or(BorderStyle::Solid);
2605 let s_bottom = styles.bottom.and_then(|s| s.get_property().cloned())
2606 .map(|s| s.inner).unwrap_or(BorderStyle::Solid);
2607 let s_left = styles.left.and_then(|s| s.get_property().cloned())
2608 .map(|s| s.inner).unwrap_or(BorderStyle::Solid);
2609
2610 let simple_radius = BorderRadius {
2611 top_left: border_radius.top_left
2612 .to_pixels_internal(bounds.0.size.width, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
2613 top_right: border_radius.top_right
2614 .to_pixels_internal(bounds.0.size.width, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
2615 bottom_left: border_radius.bottom_left
2616 .to_pixels_internal(bounds.0.size.width, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
2617 bottom_right: border_radius.bottom_right
2618 .to_pixels_internal(bounds.0.size.width, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
2619 };
2620
2621 let clip = *clip_stack.last().unwrap();
2622 let b = scroll_rect(bounds.inner());
2623
2624 let all_same = c_top == c_right && c_top == c_bottom && c_top == c_left
2626 && w_top == w_right && w_top == w_bottom && w_top == w_left
2627 && s_top == s_right && s_top == s_bottom && s_top == s_left;
2628
2629 if all_same {
2630 render_border(pixmap, &b, c_top, w_top, s_top, &simple_radius, clip, dpi_factor)?;
2631 } else {
2632 render_border_sides(
2634 pixmap, &b,
2635 [c_top, c_right, c_bottom, c_left],
2636 [w_top, w_right, w_bottom, w_left],
2637 [s_top, s_right, s_bottom, s_left],
2638 &simple_radius, clip, dpi_factor,
2639 )?;
2640 }
2641 }
2642 DisplayListItem::Underline {
2643 bounds,
2644 color,
2645 thickness: _,
2646 } => {
2647 let clip = *clip_stack.last().unwrap();
2648 render_rect(
2649 pixmap,
2650 &scroll_rect(bounds.inner()),
2651 *color,
2652 &BorderRadius::default(),
2653 clip,
2654 dpi_factor,
2655 )?;
2656 }
2657 DisplayListItem::Strikethrough {
2658 bounds,
2659 color,
2660 thickness: _,
2661 } => {
2662 let clip = *clip_stack.last().unwrap();
2663 render_rect(
2664 pixmap,
2665 &scroll_rect(bounds.inner()),
2666 *color,
2667 &BorderRadius::default(),
2668 clip,
2669 dpi_factor,
2670 )?;
2671 }
2672 DisplayListItem::Overline {
2673 bounds,
2674 color,
2675 thickness: _,
2676 } => {
2677 let clip = *clip_stack.last().unwrap();
2678 render_rect(
2679 pixmap,
2680 &scroll_rect(bounds.inner()),
2681 *color,
2682 &BorderRadius::default(),
2683 clip,
2684 dpi_factor,
2685 )?;
2686 }
2687 DisplayListItem::Text {
2688 glyphs,
2689 font_size_px,
2690 font_hash,
2691 color,
2692 clip_rect,
2693 ..
2694 } => {
2695 let clip = *clip_stack.last().unwrap();
2696 render_text(
2697 glyphs,
2698 *font_hash,
2699 *font_size_px,
2700 *color,
2701 pixmap,
2702 &scroll_rect(clip_rect.inner()),
2703 clip,
2704 renderer_resources,
2705 font_manager,
2706 dpi_factor,
2707 glyph_cache,
2708 (scroll_dx, scroll_dy),
2709 )?;
2710 }
2711 DisplayListItem::TextLayout {
2712 layout,
2713 bounds,
2714 font_hash,
2715 font_size_px,
2716 color,
2717 } => {
2718 }
2720 DisplayListItem::Image { bounds, image, .. } => {
2721 let clip = *clip_stack.last().unwrap();
2722 render_image(
2723 pixmap,
2724 &scroll_rect(bounds.inner()),
2725 image,
2726 clip,
2727 dpi_factor,
2728 )?;
2729 }
2730 DisplayListItem::ScrollBar {
2731 bounds,
2732 color,
2733 orientation,
2734 opacity_key: _,
2735 hit_id: _,
2736 } => {
2737 let clip = *clip_stack.last().unwrap();
2738 render_rect(
2739 pixmap,
2740 &scroll_rect(bounds.inner()),
2741 *color,
2742 &BorderRadius::default(),
2743 clip,
2744 dpi_factor,
2745 )?;
2746 }
2747 DisplayListItem::ScrollBarStyled { info } => {
2748 let clip = *clip_stack.last().unwrap();
2749
2750 let scrollbar_opacity = info.opacity_key
2756 .and_then(|key| render_state.opacities.get(&key.id).copied())
2757 .unwrap_or(1.0);
2758
2759 if scrollbar_opacity > 0.001 {
2760
2761 if info.track_color.a > 0 {
2763 render_rect(
2764 pixmap,
2765 &scroll_rect(info.track_bounds.inner()),
2766 info.track_color,
2767 &BorderRadius::default(),
2768 clip,
2769 dpi_factor,
2770 )?;
2771 }
2772
2773 if let Some(btn_bounds) = &info.button_decrement_bounds {
2775 if info.button_color.a > 0 {
2776 render_rect(
2777 pixmap,
2778 &scroll_rect(btn_bounds.inner()),
2779 info.button_color,
2780 &BorderRadius::default(),
2781 clip,
2782 dpi_factor,
2783 )?;
2784 }
2785 }
2786
2787 if let Some(btn_bounds) = &info.button_increment_bounds {
2789 if info.button_color.a > 0 {
2790 render_rect(
2791 pixmap,
2792 &scroll_rect(btn_bounds.inner()),
2793 info.button_color,
2794 &BorderRadius::default(),
2795 clip,
2796 dpi_factor,
2797 )?;
2798 }
2799 }
2800
2801 if info.thumb_color.a > 0 {
2806 let thumb_rect = info.thumb_bounds.inner();
2807 let transform = info.thumb_transform_key
2809 .and_then(|key| render_state.transforms.get(&key.id))
2810 .unwrap_or(&info.thumb_initial_transform);
2811 let tx = transform.m[3][0];
2812 let ty = transform.m[3][1];
2813 let transformed_thumb = LogicalRect {
2814 origin: LogicalPosition {
2815 x: thumb_rect.origin.x + tx,
2816 y: thumb_rect.origin.y + ty,
2817 },
2818 size: thumb_rect.size,
2819 };
2820 render_rect(
2821 pixmap,
2822 &scroll_rect(&transformed_thumb),
2823 info.thumb_color,
2824 &info.thumb_border_radius,
2825 clip,
2826 dpi_factor,
2827 )?;
2828 }
2829
2830 } }
2832 DisplayListItem::PushClip {
2833 bounds,
2834 border_radius,
2835 } => {
2836 let new_clip = logical_rect_to_az_rect(bounds.inner(), dpi_factor);
2837 clip_stack.push(new_clip);
2838 }
2839 DisplayListItem::PopClip => {
2840 clip_stack.pop();
2841 if clip_stack.is_empty() {
2842 return Err("Clip stack underflow".to_string());
2843 }
2844 }
2845 DisplayListItem::PushScrollFrame {
2846 scroll_id,
2847 ..
2848 } => {
2849 transform_stack.push(transform_stack.last().cloned().unwrap_or_else(TransAffine::new));
2854 let frame_offset = render_state.scroll_offsets.get(scroll_id).copied().unwrap_or((0.0, 0.0));
2855 let new_scroll = (
2856 scroll_dx + frame_offset.0,
2857 scroll_dy + frame_offset.1,
2858 );
2859 scroll_offset_stack.push(new_scroll);
2860 }
2861 DisplayListItem::PopScrollFrame => {
2862 if transform_stack.len() > 1 {
2865 transform_stack.pop();
2866 }
2867 if scroll_offset_stack.len() > 1 {
2868 scroll_offset_stack.pop();
2869 }
2870 }
2871 DisplayListItem::HitTestArea { bounds, tag } => {
2872 }
2874 DisplayListItem::PushStackingContext { z_index, bounds } => {
2875 }
2877 DisplayListItem::PopStackingContext => {}
2878 DisplayListItem::VirtualView {
2879 child_dom_id,
2880 bounds,
2881 clip_rect,
2882 } => {
2883 let clip = *clip_stack.last().unwrap();
2884 render_rect(
2886 pixmap,
2887 &scroll_rect(bounds.inner()),
2888 ColorU {
2889 r: 200,
2890 g: 200,
2891 b: 255,
2892 a: 128,
2893 },
2894 &BorderRadius::default(),
2895 clip,
2896 dpi_factor,
2897 )?;
2898 }
2899 DisplayListItem::VirtualViewPlaceholder { .. } => {}
2900
2901 DisplayListItem::LinearGradient {
2903 bounds,
2904 gradient,
2905 border_radius,
2906 } => {
2907 let clip = *clip_stack.last().unwrap();
2908 render_linear_gradient(
2909 pixmap,
2910 &scroll_rect(bounds.inner()),
2911 gradient,
2912 border_radius,
2913 clip,
2914 dpi_factor,
2915 render_state.system_style.as_deref().map(|s| &s.colors),
2916 )?;
2917 }
2918 DisplayListItem::RadialGradient {
2919 bounds,
2920 gradient,
2921 border_radius,
2922 } => {
2923 let clip = *clip_stack.last().unwrap();
2924 render_radial_gradient(
2925 pixmap,
2926 &scroll_rect(bounds.inner()),
2927 gradient,
2928 border_radius,
2929 clip,
2930 dpi_factor,
2931 render_state.system_style.as_deref().map(|s| &s.colors),
2932 )?;
2933 }
2934 DisplayListItem::ConicGradient {
2935 bounds,
2936 gradient,
2937 border_radius,
2938 } => {
2939 let clip = *clip_stack.last().unwrap();
2940 render_conic_gradient(
2941 pixmap,
2942 &scroll_rect(bounds.inner()),
2943 gradient,
2944 border_radius,
2945 clip,
2946 dpi_factor,
2947 render_state.system_style.as_deref().map(|s| &s.colors),
2948 )?;
2949 }
2950
2951 DisplayListItem::BoxShadow {
2953 bounds,
2954 shadow,
2955 border_radius,
2956 } => {
2957 render_box_shadow(
2958 pixmap,
2959 &scroll_rect(bounds.inner()),
2960 shadow,
2961 border_radius,
2962 dpi_factor,
2963 )?;
2964 }
2965
2966 DisplayListItem::PushOpacity { bounds, opacity } => {
2968 let rect = logical_rect_to_az_rect(&scroll_rect(bounds.inner()), dpi_factor);
2969 if let Some(r) = rect {
2970 let snap = snapshot_region(pixmap, r.x as i32, r.y as i32, r.width as u32, r.height as u32);
2971 mask_stack.push(MaskEntry::Opacity {
2972 snapshot: snap,
2973 rect: r,
2974 opacity: *opacity,
2975 });
2976 }
2977 }
2978 DisplayListItem::PopOpacity => {
2979 if let Some(MaskEntry::Opacity { snapshot, rect, opacity }) = mask_stack.pop() {
2980 let x = rect.x as i32;
2981 let y = rect.y as i32;
2982 let w = rect.width as u32;
2983 let h = rect.height as u32;
2984 let pw = pixmap.width as i32;
2985 let ph = pixmap.height as i32;
2986 for py in 0..h as i32 {
2988 let dy = y + py;
2989 if dy < 0 || dy >= ph { continue; }
2990 for px in 0..w as i32 {
2991 let dx = x + px;
2992 if dx < 0 || dx >= pw { continue; }
2993 let pi = ((dy as u32 * pixmap.width + dx as u32) * 4) as usize;
2994 let si = ((py as u32 * w + px as u32) * 4) as usize;
2995 if pi + 3 >= pixmap.data.len() || si + 3 >= snapshot.len() { continue; }
2996 let op = (opacity * 255.0).clamp(0.0, 255.0) as u32;
2997 let inv_op = 255 - op;
2998 for c in 0..4 {
2999 let snap_c = snapshot[si + c] as u32;
3000 let cur_c = pixmap.data[pi + c] as u32;
3001 pixmap.data[pi + c] = ((cur_c * op + snap_c * inv_op) / 255) as u8;
3002 }
3003 }
3004 }
3005 }
3006 }
3007
3008 DisplayListItem::PushReferenceFrame {
3010 transform_key,
3011 initial_transform,
3012 bounds,
3013 } => {
3014 let live_transform = render_state.transforms.get(&transform_key.id);
3019 let m = match live_transform {
3020 Some(t) => &t.m,
3021 None => &initial_transform.m,
3022 };
3023 let tf = TransAffine::new_custom(
3024 m[0][0] as f64, m[0][1] as f64, m[1][0] as f64, m[1][1] as f64, m[3][0] as f64, m[3][1] as f64, );
3028 let current = transform_stack.last().cloned().unwrap_or_else(TransAffine::new);
3029 let mut composed = tf;
3030 composed.premultiply(¤t);
3031 transform_stack.push(composed);
3032 }
3033 DisplayListItem::PopReferenceFrame => {
3034 if transform_stack.len() > 1 {
3035 transform_stack.pop();
3036 }
3037 }
3038
3039 DisplayListItem::PushFilter { .. } => {}
3042 DisplayListItem::PopFilter => {}
3043 DisplayListItem::PushBackdropFilter { .. } => {}
3044 DisplayListItem::PopBackdropFilter => {}
3045 DisplayListItem::PushTextShadow { .. } => {}
3046 DisplayListItem::PopTextShadow => {}
3047
3048 DisplayListItem::PushImageMaskClip {
3049 bounds,
3050 mask_image,
3051 mask_rect,
3052 } => {
3053 let mr = &scroll_rect(mask_rect.inner());
3054 let px_x = (mr.origin.x * dpi_factor) as i32;
3055 let px_y = (mr.origin.y * dpi_factor) as i32;
3056 let px_w = (mr.size.width * dpi_factor).ceil() as u32;
3057 let px_h = (mr.size.height * dpi_factor).ceil() as u32;
3058
3059 if px_w > 0 && px_h > 0 {
3060 let snapshot = snapshot_region(pixmap, px_x, px_y, px_w, px_h);
3061 let mask_data = extract_mask_data(mask_image, px_w, px_h)
3062 .unwrap_or_else(|| vec![255u8; (px_w * px_h) as usize]);
3063 mask_stack.push(MaskEntry::ImageMask {
3064 snapshot,
3065 mask_data,
3066 origin_x: px_x,
3067 origin_y: px_y,
3068 width: px_w,
3069 height: px_h,
3070 });
3071 }
3072 }
3073 DisplayListItem::PopImageMaskClip => {
3074 if let Some(entry) = mask_stack.pop() {
3075 apply_mask(pixmap, &entry);
3076 }
3077 }
3078 }
3079
3080 Ok(())
3081}
3082
3083fn render_rect(
3084 pixmap: &mut AzulPixmap,
3085 bounds: &LogicalRect,
3086 color: ColorU,
3087 border_radius: &BorderRadius,
3088 clip: Option<AzRect>,
3089 dpi_factor: f32,
3090) -> Result<(), String> {
3091 if color.a == 0 {
3092 return Ok(());
3093 }
3094
3095 let rect = match logical_rect_to_az_rect(bounds, dpi_factor) {
3096 Some(r) => r,
3097 None => return Ok(()),
3098 };
3099
3100 if let Some(ref c) = clip {
3102 if rect.clip(c).is_none() {
3103 return Ok(());
3104 }
3105 }
3106
3107 let agg_color = Rgba8::new(color.r as u32, color.g as u32, color.b as u32, color.a as u32);
3108
3109 if border_radius.is_zero() {
3110 let w = pixmap.width;
3114 let h = pixmap.height;
3115 let stride = (w * 4) as i32;
3116 let mut ra = unsafe {
3117 RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride)
3118 };
3119 let mut pf = PixfmtRgba32::new(&mut ra);
3120 let mut rb = RendererBase::new(pf);
3121 if let Some(c) = clip {
3122 rb.clip_box_i(
3123 c.x as i32,
3124 c.y as i32,
3125 (c.x + c.width) as i32 - 1,
3126 (c.y + c.height) as i32 - 1,
3127 );
3128 }
3129 rb.blend_bar(
3130 rect.x as i32,
3131 rect.y as i32,
3132 (rect.x + rect.width) as i32 - 1,
3133 (rect.y + rect.height) as i32 - 1,
3134 &agg_color,
3135 255, );
3137 } else {
3138 let mut path = build_rounded_rect_path(&rect, border_radius, dpi_factor);
3140 agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::NonZero, clip);
3141 }
3142
3143 Ok(())
3144}
3145
3146fn render_text(
3147 glyphs: &[GlyphInstance],
3148 font_hash: FontHash,
3149 font_size_px: f32,
3150 color: ColorU,
3151 pixmap: &mut AzulPixmap,
3152 clip_rect: &LogicalRect,
3153 clip: Option<AzRect>,
3154 renderer_resources: &RendererResources,
3155 font_manager: Option<&FontManager<FontRef>>,
3156 dpi_factor: f32,
3157 glyph_cache: &mut GlyphCache,
3158 scroll_offset: (f32, f32),
3159) -> Result<(), String> {
3160 if color.a == 0 || glyphs.is_empty() {
3161 return Ok(());
3162 }
3163
3164 if let Some(ref c) = clip {
3166 let text_rect = match logical_rect_to_az_rect(clip_rect, dpi_factor) {
3167 Some(r) => r,
3168 None => return Ok(()),
3169 };
3170 if text_rect.clip(c).is_none() {
3171 return Ok(()); }
3173 }
3174
3175 let agg_color = Rgba8::new(color.r as u32, color.g as u32, color.b as u32, color.a as u32);
3176
3177 let parsed_font: &ParsedFont = if let Some(fm) = font_manager {
3179 match fm.get_font_by_hash(font_hash.font_hash) {
3180 Some(font_ref) => unsafe { &*(font_ref.get_parsed() as *const ParsedFont) },
3181 None => {
3182 eprintln!(
3183 "[cpurender] Font hash {} not found in FontManager",
3184 font_hash.font_hash
3185 );
3186 return Ok(());
3187 }
3188 }
3189 } else {
3190 let font_key = match renderer_resources.font_hash_map.get(&font_hash.font_hash) {
3191 Some(k) => k,
3192 None => {
3193 eprintln!(
3194 "[cpurender] Font hash {} not found in font_hash_map (available: {:?})",
3195 font_hash.font_hash,
3196 renderer_resources.font_hash_map.keys().collect::<Vec<_>>()
3197 );
3198 return Ok(());
3199 }
3200 };
3201
3202 let font_ref = match renderer_resources.currently_registered_fonts.get(font_key) {
3203 Some((font_ref, _instances)) => font_ref,
3204 None => {
3205 eprintln!(
3206 "[cpurender] FontKey {:?} not found in currently_registered_fonts",
3207 font_key
3208 );
3209 return Ok(());
3210 }
3211 };
3212
3213 unsafe { &*(font_ref.get_parsed() as *const ParsedFont) }
3214 };
3215
3216 let units_per_em = parsed_font.font_metrics.units_per_em as f32;
3217 if units_per_em <= 0.0 {
3218 return Ok(());
3219 }
3220
3221 let scale = (font_size_px * dpi_factor) / units_per_em;
3222 let ppem = (font_size_px * dpi_factor).round() as u16;
3223
3224 let w = pixmap.width;
3226 let h = pixmap.height;
3227 let stride = (w * 4) as i32;
3228
3229 let mut ra = unsafe {
3232 RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride)
3233 };
3234 let mut pf = PixfmtRgba32::new(&mut ra);
3235 let mut rb = RendererBase::new(pf);
3236 if let Some(c) = clip {
3237 rb.clip_box_i(
3238 c.x as i32,
3239 c.y as i32,
3240 (c.x + c.width) as i32 - 1,
3241 (c.y + c.height) as i32 - 1,
3242 );
3243 }
3244 let mut ras = RasterizerScanlineAa::new();
3245 ras.filling_rule(FillingRule::NonZero);
3246
3247 for glyph in glyphs {
3250 let glyph_index = glyph.index as u16;
3251
3252 let glyph_data = match parsed_font.get_or_decode_glyph(glyph_index) {
3256 Some(d) => d,
3257 None => continue,
3258 };
3259
3260 let is_hinted = glyph_cache.get_or_build(
3261 font_hash.font_hash, glyph_index, &glyph_data, parsed_font, ppem,
3262 ).map(|c| c.is_hinted).unwrap_or(false);
3263
3264 let glyph_x = (glyph.point.x - scroll_offset.0) * dpi_factor;
3265 let glyph_baseline_y = (glyph.point.y - scroll_offset.1) * dpi_factor;
3266
3267 let (cells, int_x, int_y) = match glyph_cache.get_or_build_cells(
3268 font_hash.font_hash, glyph_index, ppem,
3269 glyph_x, glyph_baseline_y, scale, is_hinted,
3270 ) {
3271 Some(c) => c,
3272 None => continue,
3273 };
3274
3275 ras.add_cells_offset(cells, int_x, int_y);
3276 }
3277
3278 let mut sl = ScanlineU8::new();
3280 render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &agg_color);
3281
3282 Ok(())
3283}
3284
3285fn render_border(
3286 pixmap: &mut AzulPixmap,
3287 bounds: &LogicalRect,
3288 color: ColorU,
3289 width: f32,
3290 border_style: azul_css::props::style::border::BorderStyle,
3291 border_radius: &BorderRadius,
3292 clip: Option<AzRect>,
3293 dpi_factor: f32,
3294) -> Result<(), String> {
3295 use azul_css::props::style::border::BorderStyle;
3296
3297 if color.a == 0 || width <= 0.0 {
3298 return Ok(());
3299 }
3300
3301 match border_style {
3302 BorderStyle::None | BorderStyle::Hidden => return Ok(()),
3303 _ => {}
3304 }
3305
3306 let rect = match logical_rect_to_az_rect(bounds, dpi_factor) {
3307 Some(r) => r,
3308 None => return Ok(()),
3309 };
3310
3311 if let Some(ref c) = clip {
3313 if rect.clip(c).is_none() {
3314 return Ok(());
3315 }
3316 }
3317
3318 let scaled_width = width * dpi_factor;
3319 let agg_color = Rgba8::new(color.r as u32, color.g as u32, color.b as u32, color.a as u32);
3320
3321 let mut path = build_rounded_rect_path(&rect, border_radius, dpi_factor);
3323
3324 let x = rect.x as f64;
3325 let y = rect.y as f64;
3326 let w = rect.width as f64;
3327 let h = rect.height as f64;
3328 let sw = scaled_width as f64;
3329
3330 let ir = AzRect::from_xywh(
3332 rect.x + scaled_width,
3333 rect.y + scaled_width,
3334 rect.width - 2.0 * scaled_width,
3335 rect.height - 2.0 * scaled_width,
3336 );
3337
3338 if let Some(ir) = ir {
3339 let inner_radius = BorderRadius {
3340 top_left: (border_radius.top_left - width).max(0.0),
3341 top_right: (border_radius.top_right - width).max(0.0),
3342 bottom_right: (border_radius.bottom_right - width).max(0.0),
3343 bottom_left: (border_radius.bottom_left - width).max(0.0),
3344 };
3345 let mut inner = build_rounded_rect_path(&ir, &inner_radius, dpi_factor);
3346 path.concat_path(&mut inner, 0);
3347 }
3348
3349 match border_style {
3351 BorderStyle::Dashed | BorderStyle::Dotted => {
3352 use agg_rust::conv_stroke::ConvStroke;
3354 use agg_rust::conv_dash::ConvDash;
3355
3356 let half = sw / 2.0;
3357 let mut stroke_path = PathStorage::new();
3358 let (cx, cy, cw, ch) = (x + half, y + half, w - sw, h - sw);
3359 stroke_path.move_to(cx, cy);
3360 stroke_path.line_to(cx + cw, cy);
3361 stroke_path.line_to(cx + cw, cy + ch);
3362 stroke_path.line_to(cx, cy + ch);
3363 stroke_path.close_polygon(PATH_FLAGS_NONE);
3364
3365 let mut dashed = ConvDash::new(stroke_path);
3366 if border_style == BorderStyle::Dashed {
3367 dashed.add_dash(sw * 3.0, sw);
3368 } else {
3369 dashed.add_dash(sw, sw);
3370 }
3371
3372 let mut stroked = ConvStroke::new(dashed);
3373 stroked.set_width(sw);
3374
3375 agg_fill_path_clipped(pixmap, &mut stroked, &agg_color, FillingRule::NonZero, clip);
3376 }
3377 _ if border_radius.is_zero() => {
3378 let pw = pixmap.width;
3380 let ph = pixmap.height;
3381 let stride = (pw * 4) as i32;
3382 let mut ra = unsafe {
3383 RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), pw, ph, stride)
3384 };
3385 let mut pf = PixfmtRgba32::new(&mut ra);
3386 let mut rb = RendererBase::new(pf);
3387 if let Some(c) = clip {
3388 rb.clip_box_i(c.x as i32, c.y as i32,
3389 (c.x + c.width) as i32 - 1, (c.y + c.height) as i32 - 1);
3390 }
3391 let (xi, yi) = (x as i32, y as i32);
3392 let (x2i, y2i) = ((x + w) as i32 - 1, (y + h) as i32 - 1);
3393 let swi = sw as i32;
3394 rb.blend_bar(xi, yi, x2i, yi + swi - 1, &agg_color, 255);
3396 rb.blend_bar(xi, y2i - swi + 1, x2i, y2i, &agg_color, 255);
3398 rb.blend_bar(xi, yi + swi, xi + swi - 1, y2i - swi, &agg_color, 255);
3400 rb.blend_bar(x2i - swi + 1, yi + swi, x2i, y2i - swi, &agg_color, 255);
3402 }
3403 _ => {
3404 agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::EvenOdd, clip);
3406 }
3407 }
3408
3409 Ok(())
3410}
3411
3412fn render_border_sides(
3415 pixmap: &mut AzulPixmap,
3416 bounds: &LogicalRect,
3417 colors: [ColorU; 4], widths: [f32; 4], _styles: [azul_css::props::style::border::BorderStyle; 4],
3420 _border_radius: &BorderRadius,
3421 clip: Option<AzRect>,
3422 dpi_factor: f32,
3423) -> Result<(), String> {
3424 let rect = match logical_rect_to_az_rect(bounds, dpi_factor) {
3425 Some(r) => r,
3426 None => return Ok(()),
3427 };
3428
3429 let ox = rect.x as f64;
3431 let oy = rect.y as f64;
3432 let ow = rect.width as f64;
3433 let oh = rect.height as f64;
3434
3435 let wt = (widths[0] * dpi_factor) as f64;
3437 let wr = (widths[1] * dpi_factor) as f64;
3438 let wb = (widths[2] * dpi_factor) as f64;
3439 let wl = (widths[3] * dpi_factor) as f64;
3440
3441 let ix = ox + wl;
3442 let iy = oy + wt;
3443 let iw = ow - wl - wr;
3444 let ih = oh - wt - wb;
3445
3446 let sides: [(f64, f64, f64, f64, f64, f64, f64, f64, ColorU, f32); 4] = [
3453 (ox, oy, ox+ow, oy, ix+iw, iy, ix, iy, colors[0], widths[0]),
3455 (ox+ow, oy, ox+ow, oy+oh, ix+iw, iy+ih, ix+iw, iy, colors[1], widths[1]),
3457 (ox+ow, oy+oh, ox, oy+oh, ix, iy+ih, ix+iw, iy+ih, colors[2], widths[2]),
3459 (ox, oy+oh, ox, oy, ix, iy, ix, iy+ih, colors[3], widths[3]),
3461 ];
3462
3463 if _border_radius.is_zero() {
3464 let pw = pixmap.width;
3466 let ph = pixmap.height;
3467 let stride = (pw * 4) as i32;
3468 let mut ra = unsafe {
3469 RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), pw, ph, stride)
3470 };
3471 let mut pf = PixfmtRgba32::new(&mut ra);
3472 let mut rb = RendererBase::new(pf);
3473 if let Some(c) = clip {
3474 rb.clip_box_i(c.x as i32, c.y as i32,
3475 (c.x + c.width) as i32 - 1, (c.y + c.height) as i32 - 1);
3476 }
3477 if widths[0] > 0.0 && colors[0].a > 0 {
3479 let c = colors[0];
3480 let ac = Rgba8::new(c.r as u32, c.g as u32, c.b as u32, c.a as u32);
3481 rb.blend_bar(ox as i32, oy as i32, (ox+ow) as i32 - 1, iy as i32 - 1, &ac, 255);
3482 }
3483 if widths[2] > 0.0 && colors[2].a > 0 {
3485 let c = colors[2];
3486 let ac = Rgba8::new(c.r as u32, c.g as u32, c.b as u32, c.a as u32);
3487 rb.blend_bar(ox as i32, (iy+ih) as i32, (ox+ow) as i32 - 1, (oy+oh) as i32 - 1, &ac, 255);
3488 }
3489 if widths[3] > 0.0 && colors[3].a > 0 {
3491 let c = colors[3];
3492 let ac = Rgba8::new(c.r as u32, c.g as u32, c.b as u32, c.a as u32);
3493 rb.blend_bar(ox as i32, iy as i32, ix as i32 - 1, (iy+ih) as i32 - 1, &ac, 255);
3494 }
3495 if widths[1] > 0.0 && colors[1].a > 0 {
3497 let c = colors[1];
3498 let ac = Rgba8::new(c.r as u32, c.g as u32, c.b as u32, c.a as u32);
3499 rb.blend_bar((ix+iw) as i32, iy as i32, (ox+ow) as i32 - 1, (iy+ih) as i32 - 1, &ac, 255);
3500 }
3501 } else {
3502 for &(x0, y0, x1, y1, x2, y2, x3, y3, color, width) in &sides {
3504 if width <= 0.0 || color.a == 0 {
3505 continue;
3506 }
3507
3508 let mut path = PathStorage::new();
3509 path.move_to(x0, y0);
3510 path.line_to(x1, y1);
3511 path.line_to(x2, y2);
3512 path.line_to(x3, y3);
3513 path.close_polygon(PATH_FLAGS_NONE);
3514
3515 let agg_color = Rgba8::new(color.r as u32, color.g as u32, color.b as u32, color.a as u32);
3516 agg_fill_path_clipped(pixmap, &mut path, &agg_color, FillingRule::NonZero, clip);
3517 }
3518 }
3519
3520 Ok(())
3521}
3522
3523fn logical_rect_to_az_rect(
3524 bounds: &LogicalRect,
3525 dpi_factor: f32,
3526) -> Option<AzRect> {
3527 let x = bounds.origin.x * dpi_factor;
3528 let y = bounds.origin.y * dpi_factor;
3529 let width = bounds.size.width * dpi_factor;
3530 let height = bounds.size.height * dpi_factor;
3531
3532 AzRect::from_xywh(x, y, width, height)
3533}
3534
3535fn render_image(
3536 pixmap: &mut AzulPixmap,
3537 bounds: &LogicalRect,
3538 image: &ImageRef,
3539 clip: Option<AzRect>,
3540 dpi_factor: f32,
3541) -> Result<(), String> {
3542 let rect = match logical_rect_to_az_rect(bounds, dpi_factor) {
3543 Some(r) => r,
3544 None => return Ok(()),
3545 };
3546
3547 if let Some(ref c) = clip {
3549 if rect.clip(c).is_none() {
3550 return Ok(());
3551 }
3552 }
3553
3554 let image_data = image.get_data();
3555 let (src_rgba, src_w, src_h) = match &*image_data {
3556 DecodedImage::Raw((descriptor, data)) => {
3557 let w = descriptor.width as u32;
3558 let h = descriptor.height as u32;
3559 if w == 0 || h == 0 { return Ok(()); }
3560 let bytes = match data {
3561 azul_core::resources::ImageData::Raw(shared) => shared.as_ref(),
3562 _ => return Ok(()),
3563 };
3564
3565 let rgba = match descriptor.format {
3566 azul_core::resources::RawImageFormat::BGRA8 => {
3567 let mut out = Vec::with_capacity(bytes.len());
3568 for chunk in bytes.chunks_exact(4) {
3569 let b = chunk[0]; let g = chunk[1]; let r = chunk[2]; let a = chunk[3];
3570 out.push(r); out.push(g); out.push(b); out.push(a);
3571 }
3572 out
3573 }
3574 azul_core::resources::RawImageFormat::R8 => {
3575 let mut out = Vec::with_capacity(bytes.len() * 4);
3576 for &v in bytes {
3577 out.push(v); out.push(v); out.push(v); out.push(v);
3578 }
3579 out
3580 }
3581 _ => {
3582 let gray = Rgba8::new(200, 200, 200, 255);
3584 let mut path = build_rect_path(&rect);
3585 agg_fill_path(pixmap, &mut path, &gray, FillingRule::NonZero);
3586 return Ok(());
3587 }
3588 };
3589
3590 (rgba, w, h)
3591 }
3592 DecodedImage::NullImage { .. } | DecodedImage::Callback(_) => {
3593 let gray = Rgba8::new(200, 200, 200, 255);
3594 let mut path = build_rect_path(&rect);
3595 agg_fill_path(pixmap, &mut path, &gray, FillingRule::NonZero);
3596 return Ok(());
3597 }
3598 _ => return Ok(()),
3599 };
3600
3601 let dst_x = rect.x as i32;
3603 let dst_y = rect.y as i32;
3604 let dst_w = rect.width as u32;
3605 let dst_h = rect.height as u32;
3606 let pw = pixmap.width;
3607 let ph = pixmap.height;
3608
3609 let sx = src_w as f32 / dst_w.max(1) as f32;
3610 let sy = src_h as f32 / dst_h.max(1) as f32;
3611
3612 let (clip_x1, clip_y1, clip_x2, clip_y2) = if let Some(ref c) = clip {
3614 (c.x as i32, c.y as i32, (c.x + c.width) as i32, (c.y + c.height) as i32)
3615 } else {
3616 (0, 0, pw as i32, ph as i32)
3617 };
3618
3619 for py in 0..dst_h {
3620 for px in 0..dst_w {
3621 let tx = dst_x + px as i32;
3622 let ty = dst_y + py as i32;
3623 if tx < 0 || ty < 0 || tx >= pw as i32 || ty >= ph as i32 {
3624 continue;
3625 }
3626 if tx < clip_x1 || ty < clip_y1 || tx >= clip_x2 || ty >= clip_y2 {
3628 continue;
3629 }
3630
3631 let src_x = ((px as f32 * sx) as u32).min(src_w - 1);
3632 let src_y = ((py as f32 * sy) as u32).min(src_h - 1);
3633 let si = ((src_y * src_w + src_x) * 4) as usize;
3634 let di = ((ty as u32 * pw + tx as u32) * 4) as usize;
3635
3636 if si + 3 < src_rgba.len() && di + 3 < pixmap.data.len() {
3637 let sa = src_rgba[si + 3] as u32;
3638 if sa == 255 {
3639 pixmap.data[di] = src_rgba[si];
3640 pixmap.data[di + 1] = src_rgba[si + 1];
3641 pixmap.data[di + 2] = src_rgba[si + 2];
3642 pixmap.data[di + 3] = 255;
3643 } else if sa > 0 {
3644 let da = 255 - sa;
3646 pixmap.data[di] = ((src_rgba[si] as u32 * sa + pixmap.data[di] as u32 * da) / 255) as u8;
3647 pixmap.data[di + 1] = ((src_rgba[si + 1] as u32 * sa + pixmap.data[di + 1] as u32 * da) / 255) as u8;
3648 pixmap.data[di + 2] = ((src_rgba[si + 2] as u32 * sa + pixmap.data[di + 2] as u32 * da) / 255) as u8;
3649 pixmap.data[di + 3] = ((sa + pixmap.data[di + 3] as u32 * da / 255).min(255)) as u8;
3650 }
3651 }
3652 }
3653 }
3654
3655 Ok(())
3656}
3657
3658fn build_rect_path(rect: &AzRect) -> PathStorage {
3659 let mut path = PathStorage::new();
3660 let x = rect.x as f64;
3661 let y = rect.y as f64;
3662 let w = rect.width as f64;
3663 let h = rect.height as f64;
3664 path.move_to(x, y);
3665 path.line_to(x + w, y);
3666 path.line_to(x + w, y + h);
3667 path.line_to(x, y + h);
3668 path.close_polygon(PATH_FLAGS_NONE);
3669 path
3670}
3671
3672fn build_rounded_rect_path(
3673 rect: &AzRect,
3674 border_radius: &BorderRadius,
3675 dpi_factor: f32,
3676) -> PathStorage {
3677 let mut path = PathStorage::new();
3678
3679 let x = rect.x as f64;
3680 let y = rect.y as f64;
3681 let w = rect.width as f64;
3682 let h = rect.height as f64;
3683
3684 let tl = (border_radius.top_left * dpi_factor) as f64;
3685 let tr = (border_radius.top_right * dpi_factor) as f64;
3686 let br = (border_radius.bottom_right * dpi_factor) as f64;
3687 let bl = (border_radius.bottom_left * dpi_factor) as f64;
3688
3689 if tl <= 0.0 && tr <= 0.0 && br <= 0.0 && bl <= 0.0 {
3690 path.move_to(x, y);
3691 path.line_to(x + w, y);
3692 path.line_to(x + w, y + h);
3693 path.line_to(x, y + h);
3694 path.close_polygon(PATH_FLAGS_NONE);
3695 return path;
3696 }
3697
3698 let mut rr = RoundedRect::default_new();
3710 rr.rect(x, y, x + w, y + h);
3711 rr.radius_all(tl, tl, tr, tr, br, br, bl, bl);
3712 rr.normalize_radius();
3713 rr.set_approximation_scale(dpi_factor.max(1.0) as f64);
3714
3715 path.concat_path(&mut rr, 0);
3716 path
3717}
3718
3719pub struct ComponentPreviewOptions {
3725 pub width: Option<f32>,
3727 pub height: Option<f32>,
3729 pub dpi_factor: f32,
3731 pub background_color: ColorU,
3733}
3734
3735impl Default for ComponentPreviewOptions {
3736 fn default() -> Self {
3737 Self {
3738 width: None,
3739 height: None,
3740 dpi_factor: 1.0,
3741 background_color: ColorU { r: 255, g: 255, b: 255, a: 255 },
3742 }
3743 }
3744}
3745
3746pub struct ComponentPreviewResult {
3748 pub png_data: Vec<u8>,
3750 pub content_width: f32,
3752 pub content_height: f32,
3754}
3755
3756fn compute_content_bounds(dl: &DisplayList) -> Option<(f32, f32, f32, f32)> {
3758 let mut min_x = f32::MAX;
3759 let mut min_y = f32::MAX;
3760 let mut max_x = f32::MIN;
3761 let mut max_y = f32::MIN;
3762 let mut has_items = false;
3763
3764 for item in &dl.items {
3765 let bounds = match item {
3766 DisplayListItem::Rect { bounds, .. } => Some(*bounds),
3767 DisplayListItem::SelectionRect { bounds, .. } => Some(*bounds),
3768 DisplayListItem::Border { bounds, .. } => Some(*bounds),
3769 DisplayListItem::Text { clip_rect, .. } => Some(*clip_rect),
3770 DisplayListItem::Image { bounds, .. } => Some(*bounds),
3771 DisplayListItem::BoxShadow { bounds, .. } => Some(*bounds),
3772 DisplayListItem::PushClip { bounds, .. } => Some(*bounds),
3773 DisplayListItem::LinearGradient { bounds, .. } => Some(*bounds),
3774 DisplayListItem::RadialGradient { bounds, .. } => Some(*bounds),
3775 DisplayListItem::ConicGradient { bounds, .. } => Some(*bounds),
3776 DisplayListItem::VirtualView { bounds, .. } => Some(*bounds),
3777 DisplayListItem::ScrollBar { bounds, .. } => Some(*bounds),
3778 _ => None,
3779 };
3780 if let Some(b) = bounds {
3781 has_items = true;
3782 min_x = min_x.min(b.0.origin.x);
3783 min_y = min_y.min(b.0.origin.y);
3784 max_x = max_x.max(b.0.origin.x + b.0.size.width);
3785 max_y = max_y.max(b.0.origin.y + b.0.size.height);
3786 }
3787 }
3788
3789 if has_items {
3790 Some((min_x, min_y, max_x, max_y))
3791 } else {
3792 None
3793 }
3794}
3795
3796#[cfg(all(feature = "std", feature = "text_layout", feature = "font_loading"))]
3798pub fn render_component_preview(
3799 styled_dom: azul_core::styled_dom::StyledDom,
3800 font_manager: &FontManager<azul_css::props::basic::FontRef>,
3801 opts: ComponentPreviewOptions,
3802 system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
3803) -> Result<ComponentPreviewResult, String> {
3804 use std::collections::{BTreeMap, HashMap};
3805 use azul_core::{
3806 dom::DomId,
3807 geom::{LogicalPosition, LogicalRect, LogicalSize},
3808 resources::{IdNamespace, RendererResources},
3809 selection::{SelectionState, TextSelection},
3810 };
3811 use crate::{
3812 solver3::{
3813 self,
3814 cache::LayoutCache,
3815 display_list::DisplayList,
3816 },
3817 font_traits::TextLayoutCache,
3818 };
3819
3820 const MAX_SIZE: f32 = 4096.0;
3821
3822 let layout_width = opts.width.unwrap_or(MAX_SIZE);
3823 let layout_height = opts.height.unwrap_or(MAX_SIZE);
3824
3825 let viewport = LogicalRect {
3826 origin: LogicalPosition::zero(),
3827 size: LogicalSize {
3828 width: layout_width,
3829 height: layout_height,
3830 },
3831 };
3832
3833 let mut preview_font_manager = FontManager::from_arc_shared(
3834 font_manager.fc_cache.clone(),
3835 font_manager.parsed_fonts.clone(),
3836 ).map_err(|e| format!("Failed to create preview font manager: {:?}", e))?;
3837
3838 {
3840 use crate::solver3::getters::collect_and_resolve_font_chains_with_registration;
3841 use crate::text3::default::PathLoader;
3842
3843 let platform = azul_css::system::Platform::current();
3844
3845 let chains = collect_and_resolve_font_chains_with_registration(
3846 &styled_dom, &preview_font_manager.fc_cache, &preview_font_manager, &platform,
3847 );
3848 let loader = PathLoader::new();
3849 let _failed = preview_font_manager.load_missing_for_chains(
3850 &chains,
3851 |bytes, index| loader.load_font_shared(bytes, index),
3852 );
3853 preview_font_manager.set_font_chain_cache(chains.into_fontconfig_chains());
3854 }
3855
3856 let mut layout_cache = LayoutCache {
3858 tree: None,
3859 calculated_positions: Vec::new(),
3860 viewport: None,
3861 scroll_ids: HashMap::new(),
3862 scroll_id_to_node_id: HashMap::new(),
3863 counters: HashMap::new(),
3864 float_cache: HashMap::new(),
3865 cache_map: Default::default(),
3866 previous_positions: Vec::new(),
3867 cached_display_list: None,
3868 prev_dom_ptr: 0,
3869 prev_viewport: LogicalRect::zero(),
3870 };
3871 let mut text_cache = TextLayoutCache::new();
3872 let empty_scroll_offsets = BTreeMap::new();
3873 let empty_text_selections = BTreeMap::new();
3874 let renderer_resources = RendererResources::default();
3875 let id_namespace = IdNamespace(0xFFFF);
3876 let dom_id = DomId::ROOT_ID;
3877 let mut debug_messages = None;
3878 let get_system_time_fn = azul_core::task::GetSystemTimeCallback {
3879 cb: azul_core::task::get_system_time_libstd,
3880 };
3881
3882 let display_list = solver3::layout_document(
3883 &mut layout_cache,
3884 &mut text_cache,
3885 &styled_dom,
3886 viewport,
3887 &preview_font_manager,
3888 &empty_scroll_offsets,
3889 &empty_text_selections,
3890 &mut debug_messages,
3891 None,
3892 &renderer_resources,
3893 id_namespace,
3894 dom_id,
3895 false,
3896 Vec::new(),
3897 None, &azul_core::resources::ImageCache::default(),
3899 system_style.clone(),
3900 get_system_time_fn,
3901 ).map_err(|e| format!("Layout failed: {:?}", e))?;
3902
3903 let (render_width, render_height) = if opts.width.is_some() && opts.height.is_some() {
3905 (opts.width.unwrap(), opts.height.unwrap())
3906 } else {
3907 match compute_content_bounds(&display_list) {
3908 Some((_min_x, _min_y, max_x, max_y)) => {
3909 let w = if opts.width.is_some() { opts.width.unwrap() } else { max_x.max(1.0).ceil() };
3910 let h = if opts.height.is_some() { opts.height.unwrap() } else { max_y.max(1.0).ceil() };
3911 (w, h)
3912 }
3913 None => {
3914 return Ok(ComponentPreviewResult {
3915 png_data: Vec::new(),
3916 content_width: 0.0,
3917 content_height: 0.0,
3918 });
3919 }
3920 }
3921 };
3922
3923 let render_width = render_width.min(MAX_SIZE);
3924 let render_height = render_height.min(MAX_SIZE);
3925
3926 let dpi = opts.dpi_factor;
3928 let pixel_w = ((render_width * dpi) as u32).max(1);
3929 let pixel_h = ((render_height * dpi) as u32).max(1);
3930
3931 let mut pixmap = AzulPixmap::new(pixel_w, pixel_h)
3932 .ok_or_else(|| format!("Cannot create pixmap {}x{}", pixel_w, pixel_h))?;
3933
3934 let bg = opts.background_color;
3935 pixmap.fill(bg.r, bg.g, bg.b, bg.a);
3936
3937 let mut preview_glyph_cache = GlyphCache::new();
3938 let preview_render_state = CpuRenderState::new(ScrollOffsetMap::new())
3939 .with_system_style(system_style);
3940 render_display_list_with_state(
3941 &display_list,
3942 &mut pixmap,
3943 dpi,
3944 &renderer_resources,
3945 Some(&preview_font_manager),
3946 &mut preview_glyph_cache,
3947 &preview_render_state,
3948 )?;
3949
3950 let png_data = pixmap.encode_png()
3951 .map_err(|e| format!("PNG encoding failed: {}", e))?;
3952
3953 Ok(ComponentPreviewResult {
3954 png_data,
3955 content_width: render_width,
3956 content_height: render_height,
3957 })
3958}
3959
3960#[cfg(all(feature = "std", feature = "text_layout", feature = "font_loading"))]
3965pub fn render_dom_to_image(
3966 mut dom: azul_core::dom::Dom,
3967 css: azul_css::css::Css,
3968 width: f32,
3969 height: f32,
3970 dpi: f32,
3971) -> Result<Vec<u8>, String> {
3972 use azul_core::styled_dom::StyledDom;
3973 use crate::font_traits::FontManager;
3974
3975 let styled_dom = StyledDom::create(&mut dom, css);
3976
3977 let fc_cache = crate::font::loading::build_font_cache();
3978 let font_manager = FontManager::new(fc_cache)
3979 .map_err(|e| format!("Failed to create font manager: {:?}", e))?;
3980
3981 let opts = ComponentPreviewOptions {
3982 width: Some(width),
3983 height: Some(height),
3984 dpi_factor: dpi,
3985 background_color: azul_css::props::basic::ColorU {
3986 r: 255,
3987 g: 255,
3988 b: 255,
3989 a: 255,
3990 },
3991 };
3992
3993 let result = render_component_preview(styled_dom, &font_manager, opts, None)?;
3994 Ok(result.png_data)
3995}
3996
3997#[cfg(all(feature = "std", feature = "xml"))]
4007pub fn render_svg_to_png(
4008 svg_data: &[u8],
4009 target_width: u32,
4010 target_height: u32,
4011) -> Result<Vec<u8>, String> {
4012 let svg_str = core::str::from_utf8(svg_data)
4013 .map_err(|e| format!("SVG is not valid UTF-8: {e}"))?;
4014
4015 let nodes = crate::xml::parse_xml_string(svg_str)
4016 .map_err(|e| format!("XML parse error: {e}"))?;
4017
4018 let node_slice: &[azul_core::xml::XmlNodeChild] = nodes.as_ref();
4020 let svg_node = node_slice.iter().find_map(|n| {
4021 if let azul_core::xml::XmlNodeChild::Element(e) = n {
4022 let tag = e.node_type.as_str().to_lowercase();
4023 if tag == "svg" { Some(e) } else { None }
4024 } else { None }
4025 }).ok_or_else(|| "No <svg> root element found".to_string())?;
4026
4027 let vb = parse_viewbox(svg_node);
4029 let (vb_x, vb_y, vb_w, vb_h) = vb.unwrap_or((0.0, 0.0, target_width as f64, target_height as f64));
4030
4031 let sx = target_width as f64 / vb_w;
4032 let sy = target_height as f64 / vb_h;
4033 let scale = sx.min(sy);
4034
4035 let root_transform = TransAffine::new_custom(scale, 0.0, 0.0, scale, -vb_x * scale, -vb_y * scale);
4036
4037 let mut pixmap = AzulPixmap::new(target_width, target_height)
4038 .ok_or_else(|| "Failed to create pixmap".to_string())?;
4039 pixmap.fill(255, 255, 255, 255);
4040
4041 render_svg_group(svg_node, &mut pixmap, &root_transform);
4042
4043 pixmap.encode_png().map_err(|e| format!("PNG encode error: {e}"))
4044}
4045
4046#[cfg(all(feature = "std", feature = "xml"))]
4047fn parse_viewbox(node: &azul_core::xml::XmlNode) -> Option<(f64, f64, f64, f64)> {
4048 let vb = node.attributes.get_key("viewbox")
4049 .or_else(|| node.attributes.get_key("viewBox"))?;
4050 let nums: Vec<f64> = vb.as_str()
4051 .split(|c: char| c == ',' || c.is_ascii_whitespace())
4052 .filter(|s| !s.is_empty())
4053 .filter_map(|s| s.parse().ok())
4054 .collect();
4055 if nums.len() == 4 { Some((nums[0], nums[1], nums[2], nums[3])) } else { None }
4056}
4057
4058#[cfg(all(feature = "std", feature = "xml"))]
4060#[derive(Clone)]
4061struct SvgInheritedStyle {
4062 fill: Option<String>, stroke: Option<String>, stroke_width: Option<f64>,
4065}
4066
4067#[cfg(all(feature = "std", feature = "xml"))]
4068impl Default for SvgInheritedStyle {
4069 fn default() -> Self {
4070 Self { fill: None, stroke: None, stroke_width: None }
4071 }
4072}
4073
4074#[cfg(all(feature = "std", feature = "xml"))]
4075fn render_svg_group(
4076 node: &azul_core::xml::XmlNode,
4077 pixmap: &mut AzulPixmap,
4078 parent_transform: &TransAffine,
4079) {
4080 render_svg_group_with_style(node, pixmap, parent_transform, &SvgInheritedStyle::default());
4081}
4082
4083#[cfg(all(feature = "std", feature = "xml"))]
4084fn render_svg_group_with_style(
4085 node: &azul_core::xml::XmlNode,
4086 pixmap: &mut AzulPixmap,
4087 parent_transform: &TransAffine,
4088 parent_style: &SvgInheritedStyle,
4089) {
4090 use azul_core::xml::{XmlNodeChild, XmlNode};
4091 use agg_rust::math_stroke::{LineCap, LineJoin};
4092
4093 let group_transform = if let Some(t) = node.attributes.get_key("transform") {
4094 let mut tf = parse_svg_transform(t.as_str());
4095 tf.premultiply(parent_transform);
4096 tf
4097 } else {
4098 parent_transform.clone()
4099 };
4100
4101 let group_style = SvgInheritedStyle {
4103 fill: node.attributes.get_key("fill")
4104 .map(|s| s.as_str().to_string())
4105 .or_else(|| parent_style.fill.clone()),
4106 stroke: node.attributes.get_key("stroke")
4107 .map(|s| s.as_str().to_string())
4108 .or_else(|| parent_style.stroke.clone()),
4109 stroke_width: node.attributes.get_key("stroke-width")
4110 .and_then(|s| s.as_str().parse().ok())
4111 .or(parent_style.stroke_width),
4112 };
4113
4114 for child in node.children.as_ref().iter() {
4115 let child_node = match child {
4116 XmlNodeChild::Element(e) => e,
4117 _ => continue,
4118 };
4119
4120 let tag = child_node.node_type.as_str().to_lowercase();
4121
4122 match tag.as_str() {
4123 "g" | "svg" => {
4124 render_svg_group_with_style(child_node, pixmap, &group_transform, &group_style);
4125 }
4126 "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline" => {
4127 let path_storage = match build_agg_path(child_node) {
4128 Some(p) => p,
4129 None => continue,
4130 };
4131
4132 let mut curved = agg_rust::conv_curve::ConvCurve::new(path_storage);
4134
4135 let elem_transform = if let Some(t) = child_node.attributes.get_key("transform") {
4137 let mut tf = parse_svg_transform(t.as_str());
4138 tf.premultiply(&group_transform);
4139 tf
4140 } else {
4141 group_transform.clone()
4142 };
4143
4144 let fill_attr = child_node.attributes.get_key("fill")
4146 .map(|s| s.as_str().to_string())
4147 .or_else(|| group_style.fill.clone());
4148 let fill_color = match fill_attr.as_deref() {
4149 Some("none") => None,
4150 Some(c) => parse_svg_color(c),
4151 None => Some(Rgba8 { r: 0, g: 0, b: 0, a: 255 }), };
4153
4154 let fill_opacity = child_node.attributes.get_key("fill-opacity")
4155 .and_then(|s| s.as_str().parse::<f64>().ok())
4156 .unwrap_or(1.0);
4157
4158 let opacity = child_node.attributes.get_key("opacity")
4159 .and_then(|s| s.as_str().parse::<f64>().ok())
4160 .unwrap_or(1.0);
4161
4162 if let Some(mut color) = fill_color {
4163 color.a = ((color.a as f64) * fill_opacity * opacity).min(255.0) as u8;
4164
4165 let fill_rule_str = child_node.attributes.get_key("fill-rule")
4166 .map(|s| s.as_str().to_string());
4167 let rule = match fill_rule_str.as_deref() {
4168 Some("evenodd") => FillingRule::EvenOdd,
4169 _ => FillingRule::NonZero,
4170 };
4171
4172 let mut transformed = ConvTransform::new(&mut curved, elem_transform.clone());
4173 agg_fill_path(pixmap, &mut transformed, &color, rule);
4174 }
4175
4176 let stroke_attr = child_node.attributes.get_key("stroke")
4178 .map(|s| s.as_str().to_string())
4179 .or_else(|| group_style.stroke.clone());
4180 let stroke_color = match stroke_attr.as_deref() {
4181 Some("none") | None => None,
4182 Some(c) => parse_svg_color(c),
4183 };
4184
4185 if let Some(mut color) = stroke_color {
4186 let stroke_opacity = child_node.attributes.get_key("stroke-opacity")
4187 .and_then(|s| s.as_str().parse::<f64>().ok())
4188 .unwrap_or(1.0);
4189 color.a = ((color.a as f64) * stroke_opacity * opacity).min(255.0) as u8;
4190
4191 let stroke_width = child_node.attributes.get_key("stroke-width")
4192 .and_then(|s| s.as_str().parse::<f64>().ok())
4193 .or(group_style.stroke_width)
4194 .unwrap_or(1.0);
4195
4196 let mut conv_stroke = ConvStroke::new(&mut curved);
4197 conv_stroke.set_width(stroke_width);
4198 conv_stroke.set_line_cap(LineCap::Round);
4199 conv_stroke.set_line_join(LineJoin::Round);
4200
4201 let mut transformed = ConvTransform::new(&mut conv_stroke, elem_transform.clone());
4202 agg_fill_path(pixmap, &mut transformed, &color, FillingRule::NonZero);
4203 }
4204 }
4205 _ => {
4206 render_svg_group_with_style(child_node, pixmap, &group_transform, &group_style);
4208 }
4209 }
4210 }
4211}
4212
4213#[cfg(all(feature = "std", feature = "xml"))]
4215fn build_agg_path(node: &azul_core::xml::XmlNode) -> Option<PathStorage> {
4216 let tag = node.node_type.as_str().to_lowercase();
4217 match tag.as_str() {
4218 "path" => {
4219 let d = node.attributes.get_key("d")?;
4220 let mp = azul_core::svg_path_parser::parse_svg_path_d(d.as_str()).ok()?;
4221 Some(svg_multi_polygon_to_path_storage(&mp))
4222 }
4223 "circle" => {
4224 let cx = attr_f64(node, "cx");
4225 let cy = attr_f64(node, "cy");
4226 let r = attr_f64(node, "r");
4227 if r <= 0.0 { return None; }
4228 let mp = azul_core::svg_path_parser::svg_circle_to_paths(cx as f32, cy as f32, r as f32);
4229 let multi = azul_core::svg::SvgMultiPolygon {
4230 rings: azul_core::svg::SvgPathVec::from_vec(vec![mp]),
4231 };
4232 Some(svg_multi_polygon_to_path_storage(&multi))
4233 }
4234 "rect" => {
4235 let x = attr_f64(node, "x");
4236 let y = attr_f64(node, "y");
4237 let w = attr_f64(node, "width");
4238 let h = attr_f64(node, "height");
4239 let rx = attr_f64(node, "rx");
4240 let ry = if let Some(v) = node.attributes.get_key("ry") {
4241 v.as_str().parse().unwrap_or(rx)
4242 } else { rx };
4243 if w <= 0.0 || h <= 0.0 { return None; }
4244 let mp = azul_core::svg_path_parser::svg_rect_to_path(x as f32, y as f32, w as f32, h as f32, rx as f32, ry as f32);
4245 let multi = azul_core::svg::SvgMultiPolygon {
4246 rings: azul_core::svg::SvgPathVec::from_vec(vec![mp]),
4247 };
4248 Some(svg_multi_polygon_to_path_storage(&multi))
4249 }
4250 "ellipse" => {
4251 let cx = attr_f64(node, "cx");
4252 let cy = attr_f64(node, "cy");
4253 let rx = attr_f64(node, "rx");
4254 let ry = attr_f64(node, "ry");
4255 if rx <= 0.0 || ry <= 0.0 { return None; }
4256 let mp = azul_core::svg_path_parser::svg_circle_to_paths(cx as f32, cy as f32, 1.0);
4258 let multi = azul_core::svg::SvgMultiPolygon {
4259 rings: azul_core::svg::SvgPathVec::from_vec(vec![mp]),
4260 };
4261 let mut ps = svg_multi_polygon_to_path_storage(&multi);
4262 let mut path = PathStorage::new();
4264 const KAPPA: f64 = 0.5522847498;
4265 let kx = rx * KAPPA;
4266 let ky = ry * KAPPA;
4267 path.move_to(cx, cy - ry);
4268 path.curve4(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy);
4269 path.curve4(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry);
4270 path.curve4(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy);
4271 path.curve4(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry);
4272 path.close_polygon(PATH_FLAGS_NONE);
4273 Some(path)
4274 }
4275 "line" => {
4276 let x1 = attr_f64(node, "x1");
4277 let y1 = attr_f64(node, "y1");
4278 let x2 = attr_f64(node, "x2");
4279 let y2 = attr_f64(node, "y2");
4280 let mut path = PathStorage::new();
4281 path.move_to(x1, y1);
4282 path.line_to(x2, y2);
4283 Some(path)
4284 }
4285 "polygon" | "polyline" => {
4286 let pts_str = node.attributes.get_key("points")?;
4287 let nums: Vec<f64> = pts_str.as_str()
4288 .split(|c: char| c == ',' || c.is_ascii_whitespace())
4289 .filter(|s| !s.is_empty())
4290 .filter_map(|s| s.parse().ok())
4291 .collect();
4292 if nums.len() < 4 { return None; }
4293 let mut path = PathStorage::new();
4294 path.move_to(nums[0], nums[1]);
4295 for chunk in nums[2..].chunks_exact(2) {
4296 path.line_to(chunk[0], chunk[1]);
4297 }
4298 if tag == "polygon" {
4299 path.close_polygon(PATH_FLAGS_NONE);
4300 }
4301 Some(path)
4302 }
4303 _ => None,
4304 }
4305}
4306
4307#[cfg(all(feature = "std", feature = "xml"))]
4308fn attr_f64(node: &azul_core::xml::XmlNode, key: &str) -> f64 {
4309 node.attributes.get_key(key)
4310 .and_then(|s| s.as_str().parse().ok())
4311 .unwrap_or(0.0)
4312}
4313
4314#[cfg(all(feature = "std", feature = "xml"))]
4316fn svg_multi_polygon_to_path_storage(mp: &azul_core::svg::SvgMultiPolygon) -> PathStorage {
4317 let mut path = PathStorage::new();
4318 for ring in mp.rings.as_ref().iter() {
4319 let mut first = true;
4320 for item in ring.items.as_ref().iter() {
4321 match item {
4322 azul_core::svg::SvgPathElement::Line(l) => {
4323 if first {
4324 path.move_to(l.start.x as f64, l.start.y as f64);
4325 first = false;
4326 }
4327 path.line_to(l.end.x as f64, l.end.y as f64);
4328 }
4329 azul_core::svg::SvgPathElement::QuadraticCurve(q) => {
4330 if first {
4331 path.move_to(q.start.x as f64, q.start.y as f64);
4332 first = false;
4333 }
4334 path.curve3(q.ctrl.x as f64, q.ctrl.y as f64, q.end.x as f64, q.end.y as f64);
4335 }
4336 azul_core::svg::SvgPathElement::CubicCurve(c) => {
4337 if first {
4338 path.move_to(c.start.x as f64, c.start.y as f64);
4339 first = false;
4340 }
4341 path.curve4(
4342 c.ctrl_1.x as f64, c.ctrl_1.y as f64,
4343 c.ctrl_2.x as f64, c.ctrl_2.y as f64,
4344 c.end.x as f64, c.end.y as f64,
4345 );
4346 }
4347 }
4348 }
4349 path.close_polygon(PATH_FLAGS_NONE);
4350 }
4351 path
4352}
4353
4354#[cfg(all(feature = "std", feature = "xml"))]
4356fn parse_svg_transform(s: &str) -> TransAffine {
4357 let s = s.trim();
4358
4359 let parse_nums = |inner: &str| -> Vec<f64> {
4360 inner
4361 .split(|c: char| c == ',' || c.is_ascii_whitespace())
4362 .filter(|s| !s.is_empty())
4363 .filter_map(|s| s.parse().ok())
4364 .collect()
4365 };
4366
4367 if let Some(inner) = s.strip_prefix("matrix(").and_then(|s| s.strip_suffix(')')) {
4368 let nums = parse_nums(inner);
4369 if nums.len() == 6 {
4370 return TransAffine::new_custom(nums[0], nums[1], nums[2], nums[3], nums[4], nums[5]);
4371 }
4372 } else if let Some(inner) = s.strip_prefix("translate(").and_then(|s| s.strip_suffix(')')) {
4373 let nums = parse_nums(inner);
4374 let tx = nums.first().copied().unwrap_or(0.0);
4375 let ty = nums.get(1).copied().unwrap_or(0.0);
4376 return TransAffine::new_custom(1.0, 0.0, 0.0, 1.0, tx, ty);
4377 } else if let Some(inner) = s.strip_prefix("scale(").and_then(|s| s.strip_suffix(')')) {
4378 let nums = parse_nums(inner);
4379 let sx = nums.first().copied().unwrap_or(1.0);
4380 let sy = nums.get(1).copied().unwrap_or(sx);
4381 return TransAffine::new_custom(sx, 0.0, 0.0, sy, 0.0, 0.0);
4382 } else if let Some(inner) = s.strip_prefix("rotate(").and_then(|s| s.strip_suffix(')')) {
4383 let nums = parse_nums(inner);
4384 let angle = nums.first().copied().unwrap_or(0.0).to_radians();
4385 let cos_a = angle.cos();
4386 let sin_a = angle.sin();
4387 return TransAffine::new_custom(cos_a, sin_a, -sin_a, cos_a, 0.0, 0.0);
4388 }
4389 TransAffine::new()
4390}
4391
4392#[cfg(all(feature = "std", feature = "xml"))]
4394fn parse_svg_color(s: &str) -> Option<Rgba8> {
4395 let s = s.trim();
4396 if s.starts_with('#') {
4397 let hex = &s[1..];
4398 return match hex.len() {
4399 6 => {
4400 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
4401 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
4402 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
4403 Some(Rgba8 { r, g, b, a: 255 })
4404 }
4405 3 => {
4406 let r = u8::from_str_radix(&hex[0..1], 16).ok()? * 17;
4407 let g = u8::from_str_radix(&hex[1..2], 16).ok()? * 17;
4408 let b = u8::from_str_radix(&hex[2..3], 16).ok()? * 17;
4409 Some(Rgba8 { r, g, b, a: 255 })
4410 }
4411 _ => None,
4412 };
4413 }
4414 match s.to_lowercase().as_str() {
4415 "black" => Some(Rgba8 { r: 0, g: 0, b: 0, a: 255 }),
4416 "white" => Some(Rgba8 { r: 255, g: 255, b: 255, a: 255 }),
4417 "red" => Some(Rgba8 { r: 255, g: 0, b: 0, a: 255 }),
4418 "green" => Some(Rgba8 { r: 0, g: 128, b: 0, a: 255 }),
4419 "blue" => Some(Rgba8 { r: 0, g: 0, b: 255, a: 255 }),
4420 "yellow" => Some(Rgba8 { r: 255, g: 255, b: 0, a: 255 }),
4421 "orange" => Some(Rgba8 { r: 255, g: 165, b: 0, a: 255 }),
4422 "gold" => Some(Rgba8 { r: 255, g: 215, b: 0, a: 255 }),
4423 _ => None,
4424 }
4425}