1use std::{collections::{BTreeMap, HashMap}, sync::Arc};
17
18use azul_core::{
19 dom::{DomId, FormattingContext, NodeId, NodeType, ScrollbarOrientation},
20 geom::{LogicalPosition, LogicalRect, LogicalSize},
21 gpu::GpuValueCache,
22 hit_test::{CursorType, ScrollPosition, TAG_TYPE_CURSOR, TAG_TYPE_DOM_NODE},
23 resources::{
24 IdNamespace, ImageRef, OpacityKey, RendererResources, TransformKey,
25 },
26 transform::ComputedTransform3D,
27 selection::{Selection, SelectionRange, TextSelection},
28 styled_dom::StyledDom,
29 ui_solver::GlyphInstance,
30};
31use azul_css::{
32 css::CssPropertyValue,
33 codegen::format::GetHash,
34 props::{
35 basic::{ColorU, FontRef, PixelValue},
36 layout::{LayoutDisplay, LayoutOverflow, LayoutPosition},
37 property::{CssProperty, CssPropertyType},
38 style::{
39 background::{ConicGradient, ExtendMode, LinearGradient, RadialGradient},
40 border_radius::StyleBorderRadius,
41 box_shadow::{BoxShadowClipMode, StyleBoxShadow},
42 filter::{StyleFilter, StyleFilterVec},
43 BorderStyle, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth,
44 LayoutBorderTopWidth, StyleBorderBottomColor, StyleBorderBottomStyle,
45 StyleBorderLeftColor, StyleBorderLeftStyle, StyleBorderRightColor,
46 StyleBorderRightStyle, StyleBorderTopColor, StyleBorderTopStyle,
47 },
48 },
49 LayoutDebugMessage,
50};
51
52#[cfg(feature = "text_layout")]
53use crate::text3;
54#[cfg(feature = "text_layout")]
55use crate::text3::cache::{InlineShape, PositionedItem};
56use crate::{
57 debug_info,
58 font_traits::{
59 FontHash, FontLoaderTrait, ImageSource, InlineContent, ParsedFontTrait, ShapedItem,
60 UnifiedLayout,
61 },
62 solver3::{
63 getters::{
64 get_background_color, get_background_contents, get_border_info, get_border_radius,
65 get_break_after, get_break_before, get_caret_style,
66 get_overflow_clip_margin_property, get_overflow_x, get_overflow_y,
67 get_scrollbar_gutter_property, get_scrollbar_info_from_layout, get_scrollbar_style, get_selection_style,
68 get_style_border_radius, get_visibility, get_z_index, is_forced_page_break, BorderInfo, CaretStyle,
69 ComputedScrollbarStyle, SelectionStyle,
70 },
71 layout_tree::{LayoutNode, LayoutNodeHot, LayoutNodeWarm, LayoutTree},
72 positioning::get_position_type,
73 scrollbar::{ScrollbarRequirements, compute_scrollbar_geometry_with_button_size},
74 LayoutContext, LayoutError, Result,
75 },
76};
77
78const APPROX_ASCENT_RATIO: f32 = 0.8;
79const APPROX_UNDERLINE_THICKNESS_RATIO: f32 = 0.08;
80const APPROX_UNDERLINE_OFFSET_RATIO: f32 = 0.12;
81const APPROX_STRIKETHROUGH_OFFSET_RATIO: f32 = 0.3;
82const APPROX_OVERLINE_OFFSET_RATIO: f32 = 0.85;
83const APPROX_ELLIPSIS_WIDTH_RATIO: f32 = 0.6;
84const DEFAULT_A4_WIDTH_PT: f32 = 595.0;
85const DEFAULT_SHADOW_FONT_SIZE_PX: f32 = 16.0;
86
87#[derive(Debug, Clone, Copy)]
92pub struct StyleBorderWidths {
93 pub top: Option<CssPropertyValue<LayoutBorderTopWidth>>,
95 pub right: Option<CssPropertyValue<LayoutBorderRightWidth>>,
97 pub bottom: Option<CssPropertyValue<LayoutBorderBottomWidth>>,
99 pub left: Option<CssPropertyValue<LayoutBorderLeftWidth>>,
101}
102
103#[derive(Debug, Clone, Copy)]
108pub struct StyleBorderColors {
109 pub top: Option<CssPropertyValue<StyleBorderTopColor>>,
111 pub right: Option<CssPropertyValue<StyleBorderRightColor>>,
113 pub bottom: Option<CssPropertyValue<StyleBorderBottomColor>>,
115 pub left: Option<CssPropertyValue<StyleBorderLeftColor>>,
117}
118
119#[derive(Debug, Clone, Copy)]
125pub struct StyleBorderStyles {
126 pub top: Option<CssPropertyValue<StyleBorderTopStyle>>,
128 pub right: Option<CssPropertyValue<StyleBorderRightStyle>>,
130 pub bottom: Option<CssPropertyValue<StyleBorderBottomStyle>>,
132 pub left: Option<CssPropertyValue<StyleBorderLeftStyle>>,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub struct BorderBoxRect(pub LogicalRect);
140
141#[derive(Debug, Copy, Clone, Default, PartialEq, PartialOrd, Eq, Ord, Hash)]
157pub struct WindowLogicalRect(pub LogicalRect);
158
159impl WindowLogicalRect {
160 #[inline]
161 #[must_use] pub const fn new(origin: LogicalPosition, size: LogicalSize) -> Self {
162 Self(LogicalRect::new(origin, size))
163 }
164
165 #[inline]
166 #[must_use] pub const fn zero() -> Self {
167 Self(LogicalRect::zero())
168 }
169
170 #[inline]
173 #[must_use] pub const fn inner(&self) -> &LogicalRect {
174 &self.0
175 }
176
177 #[inline]
178 #[must_use] pub const fn into_inner(self) -> LogicalRect {
179 self.0
180 }
181
182 #[inline] #[must_use] pub const fn origin(&self) -> LogicalPosition { self.0.origin }
184 #[inline] #[must_use] pub const fn size(&self) -> LogicalSize { self.0.size }
185}
186
187impl From<LogicalRect> for WindowLogicalRect {
188 #[inline]
189 fn from(r: LogicalRect) -> Self { Self(r) }
190}
191
192impl From<WindowLogicalRect> for LogicalRect {
193 #[inline]
194 fn from(w: WindowLogicalRect) -> Self { w.0 }
195}
196
197#[derive(Debug, Clone, Copy)]
199pub struct PhysicalSizeImport {
200 pub width: f32,
201 pub height: f32,
202}
203
204#[derive(Debug, Clone, PartialEq)]
212pub struct ScrollbarDrawInfo {
213 pub bounds: WindowLogicalRect,
215 pub orientation: ScrollbarOrientation,
217
218 pub track_bounds: WindowLogicalRect,
221 pub track_color: ColorU,
223
224 pub thumb_bounds: WindowLogicalRect,
227 pub thumb_color: ColorU,
229 pub thumb_border_radius: BorderRadius,
231
232 pub button_decrement_bounds: Option<WindowLogicalRect>,
235 pub button_increment_bounds: Option<WindowLogicalRect>,
237 pub button_color: ColorU,
239
240 pub opacity_key: Option<OpacityKey>,
242 pub thumb_transform_key: Option<TransformKey>,
247 pub thumb_initial_transform: ComputedTransform3D,
251 pub hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
253 pub clip_to_container_border: bool,
255 pub container_border_radius: BorderRadius,
257 pub visibility: azul_css::props::style::scrollbar::ScrollbarVisibilityMode,
260}
261
262impl BorderBoxRect {
263 #[must_use] pub fn to_content_box(
266 self,
267 padding: &crate::solver3::geometry::EdgeSizes,
268 border: &crate::solver3::geometry::EdgeSizes,
269 ) -> ContentBoxRect {
270 ContentBoxRect(LogicalRect {
271 origin: LogicalPosition {
272 x: self.0.origin.x + padding.left + border.left,
273 y: self.0.origin.y + padding.top + border.top,
274 },
275 size: LogicalSize {
276 width: self.0.size.width
277 - padding.left
278 - padding.right
279 - border.left
280 - border.right,
281 height: self.0.size.height
282 - padding.top
283 - padding.bottom
284 - border.top
285 - border.bottom,
286 },
287 })
288 }
289
290 #[must_use] pub const fn rect(&self) -> LogicalRect {
292 self.0
293 }
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299pub struct ContentBoxRect(pub LogicalRect);
300
301impl ContentBoxRect {
302 #[must_use] pub const fn rect(&self) -> LogicalRect {
304 self.0
305 }
306}
307
308#[derive(Debug, Default, Clone)]
313pub struct DisplayList {
314 pub items: Vec<DisplayListItem>,
315 pub node_mapping: Vec<Option<NodeId>>,
319 pub forced_page_breaks: Vec<f32>,
323 pub fixed_position_item_ranges: Vec<(usize, usize)>,
326}
327
328impl DisplayList {
329 pub(crate) fn patch_text_glyphs(
336 &mut self,
337 node_index: usize,
338 new_glyphs_by_run: &[Vec<GlyphInstance>],
339 ) -> Option<LogicalRect> {
340 let mut run_idx = 0;
341 let mut damage: Option<LogicalRect> = None;
342
343 for item in &mut self.items {
344 if let DisplayListItem::Text {
345 ref mut glyphs,
346 ref clip_rect,
347 source_node_index: Some(src_idx),
348 ..
349 } = item {
350 if *src_idx == node_index
351 && run_idx < new_glyphs_by_run.len() {
352 glyphs.clone_from(&new_glyphs_by_run[run_idx]);
353 let bounds = *clip_rect.inner();
354 damage = Some(damage.map_or(bounds, |d| {
355 let x = d.origin.x.min(bounds.origin.x);
359 let y = d.origin.y.min(bounds.origin.y);
360 let right = (d.origin.x + d.size.width)
361 .max(bounds.origin.x + bounds.size.width);
362 let bottom = (d.origin.y + d.size.height)
363 .max(bounds.origin.y + bounds.size.height);
364 LogicalRect {
365 origin: LogicalPosition { x, y },
366 size: LogicalSize { width: right - x, height: bottom - y },
367 }
368 }));
369 run_idx += 1;
370 }
371 }
372 }
373
374 damage
375 }
376
377 #[allow(clippy::similar_names)] pub(crate) fn compute_text_damage_rect(
381 old_items: &[PositionedItem],
382 new_items: &[PositionedItem],
383 container_origin: LogicalPosition,
384 affected_line: usize,
385 ) -> LogicalRect {
386 let expand = |items: &[PositionedItem]| -> (f32, f32, f32, f32) {
387 let mut lx = f32::MAX;
388 let mut ly = f32::MAX;
389 let mut rx = f32::MIN;
390 let mut ry = f32::MIN;
391 for item in items {
392 if item.line_index >= affected_line {
393 let bounds = item.item.bounds();
394 let x = container_origin.x + item.position.x;
395 let y = container_origin.y + item.position.y;
396 lx = lx.min(x);
397 ly = ly.min(y);
398 rx = rx.max(x + bounds.width);
399 ry = ry.max(y + bounds.height);
400 }
401 }
402 (lx, ly, rx, ry)
403 };
404
405 let (olx, oly, orx, ory) = expand(old_items);
406 let (nlx, nly, nrx, nry) = expand(new_items);
407 let min_x = olx.min(nlx);
408 let min_y = oly.min(nly);
409 let max_x = orx.max(nrx);
410 let max_y = ory.max(nry);
411
412 if min_x > max_x || min_y > max_y {
413 return LogicalRect::default();
414 }
415
416 LogicalRect {
417 origin: LogicalPosition { x: min_x, y: min_y },
418 size: LogicalSize { width: max_x - min_x, height: max_y - min_y },
419 }
420 }
421
422 #[allow(clippy::too_many_lines)] pub(crate) fn to_debug_json(&self) -> String {
426 use std::fmt::Write;
427 let mut json = String::new();
428 writeln!(json, "{{").unwrap();
429 writeln!(json, " \"total_items\": {},", self.items.len()).unwrap();
430 writeln!(json, " \"items\": [").unwrap();
431
432 let mut clip_depth = 0i32;
433 let mut scroll_depth = 0i32;
434 let mut stacking_depth = 0i32;
435
436 for (i, item) in self.items.iter().enumerate() {
437 let comma = if i < self.items.len() - 1 { "," } else { "" };
438 let node_id = self.node_mapping.get(i).and_then(|n| *n);
439
440 match item {
441 DisplayListItem::PushClip {
442 bounds,
443 border_radius,
444 } => {
445 clip_depth += 1;
446 writeln!(json, " {{").unwrap();
447 writeln!(json, " \"index\": {i},").unwrap();
448 writeln!(json, " \"type\": \"PushClip\",").unwrap();
449 writeln!(json, " \"clip_depth\": {clip_depth},").unwrap();
450 writeln!(json, " \"scroll_depth\": {scroll_depth},").unwrap();
451 writeln!(json, " \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
452 bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
453 writeln!(json, " \"border_radius\": {{ \"tl\": {:.1}, \"tr\": {:.1}, \"bl\": {:.1}, \"br\": {:.1} }},",
454 border_radius.top_left, border_radius.top_right,
455 border_radius.bottom_left, border_radius.bottom_right).unwrap();
456 writeln!(json, " \"node_id\": {node_id:?}").unwrap();
457 writeln!(json, " }}{comma}").unwrap();
458 }
459 DisplayListItem::PopClip => {
460 writeln!(json, " {{").unwrap();
461 writeln!(json, " \"index\": {i},").unwrap();
462 writeln!(json, " \"type\": \"PopClip\",").unwrap();
463 writeln!(json, " \"clip_depth_before\": {clip_depth},").unwrap();
464 writeln!(json, " \"clip_depth_after\": {}", clip_depth - 1).unwrap();
465 writeln!(json, " }}{comma}").unwrap();
466 clip_depth -= 1;
467 }
468 DisplayListItem::PushScrollFrame {
469 clip_bounds,
470 content_size,
471 scroll_id,
472 } => {
473 scroll_depth += 1;
474 writeln!(json, " {{").unwrap();
475 writeln!(json, " \"index\": {i},").unwrap();
476 writeln!(json, " \"type\": \"PushScrollFrame\",").unwrap();
477 writeln!(json, " \"clip_depth\": {clip_depth},").unwrap();
478 writeln!(json, " \"scroll_depth\": {scroll_depth},").unwrap();
479 writeln!(json, " \"clip_bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
480 clip_bounds.0.origin.x, clip_bounds.0.origin.y,
481 clip_bounds.0.size.width, clip_bounds.0.size.height).unwrap();
482 writeln!(
483 json,
484 " \"content_size\": {{ \"w\": {:.1}, \"h\": {:.1} }},",
485 content_size.width, content_size.height
486 )
487 .unwrap();
488 writeln!(json, " \"scroll_id\": {scroll_id},").unwrap();
489 writeln!(json, " \"node_id\": {node_id:?}").unwrap();
490 writeln!(json, " }}{comma}").unwrap();
491 }
492 DisplayListItem::PopScrollFrame => {
493 writeln!(json, " {{").unwrap();
494 writeln!(json, " \"index\": {i},").unwrap();
495 writeln!(json, " \"type\": \"PopScrollFrame\",").unwrap();
496 writeln!(json, " \"scroll_depth_before\": {scroll_depth},").unwrap();
497 writeln!(json, " \"scroll_depth_after\": {}", scroll_depth - 1).unwrap();
498 writeln!(json, " }}{comma}").unwrap();
499 scroll_depth -= 1;
500 }
501 DisplayListItem::PushStackingContext { z_index, bounds } => {
502 stacking_depth += 1;
503 writeln!(json, " {{").unwrap();
504 writeln!(json, " \"index\": {i},").unwrap();
505 writeln!(json, " \"type\": \"PushStackingContext\",").unwrap();
506 writeln!(json, " \"stacking_depth\": {stacking_depth},").unwrap();
507 writeln!(json, " \"z_index\": {z_index},").unwrap();
508 writeln!(json, " \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }}",
509 bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
510 writeln!(json, " }}{comma}").unwrap();
511 }
512 DisplayListItem::PopStackingContext => {
513 writeln!(json, " {{").unwrap();
514 writeln!(json, " \"index\": {i},").unwrap();
515 writeln!(json, " \"type\": \"PopStackingContext\",").unwrap();
516 writeln!(json, " \"stacking_depth_before\": {stacking_depth},").unwrap();
517 writeln!(
518 json,
519 " \"stacking_depth_after\": {}",
520 stacking_depth - 1
521 )
522 .unwrap();
523 writeln!(json, " }}{comma}").unwrap();
524 stacking_depth -= 1;
525 }
526 DisplayListItem::Rect {
527 bounds,
528 color,
529 border_radius,
530 } => {
531 writeln!(json, " {{").unwrap();
532 writeln!(json, " \"index\": {i},").unwrap();
533 writeln!(json, " \"type\": \"Rect\",").unwrap();
534 writeln!(json, " \"clip_depth\": {clip_depth},").unwrap();
535 writeln!(json, " \"scroll_depth\": {scroll_depth},").unwrap();
536 writeln!(json, " \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
537 bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
538 writeln!(
539 json,
540 " \"color\": \"rgba({},{},{},{})\",",
541 color.r, color.g, color.b, color.a
542 )
543 .unwrap();
544 writeln!(json, " \"node_id\": {node_id:?}").unwrap();
545 writeln!(json, " }}{comma}").unwrap();
546 }
547 DisplayListItem::Border { bounds, .. } => {
548 writeln!(json, " {{").unwrap();
549 writeln!(json, " \"index\": {i},").unwrap();
550 writeln!(json, " \"type\": \"Border\",").unwrap();
551 writeln!(json, " \"clip_depth\": {clip_depth},").unwrap();
552 writeln!(json, " \"scroll_depth\": {scroll_depth},").unwrap();
553 writeln!(json, " \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
554 bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
555 writeln!(json, " \"node_id\": {node_id:?}").unwrap();
556 writeln!(json, " }}{comma}").unwrap();
557 }
558 DisplayListItem::ScrollBarStyled { info } => {
559 writeln!(json, " {{").unwrap();
560 writeln!(json, " \"index\": {i},").unwrap();
561 writeln!(json, " \"type\": \"ScrollBarStyled\",").unwrap();
562 writeln!(json, " \"clip_depth\": {clip_depth},").unwrap();
563 writeln!(json, " \"scroll_depth\": {scroll_depth},").unwrap();
564 writeln!(json, " \"orientation\": \"{:?}\",", info.orientation).unwrap();
565 writeln!(json, " \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }}",
566 info.bounds.0.origin.x, info.bounds.0.origin.y,
567 info.bounds.0.size.width, info.bounds.0.size.height).unwrap();
568 writeln!(json, " }}{comma}").unwrap();
569 }
570 _ => {
571 writeln!(json, " {{").unwrap();
572 writeln!(json, " \"index\": {i},").unwrap();
573 writeln!(
574 json,
575 " \"type\": \"{:?}\",",
576 std::mem::discriminant(item)
577 )
578 .unwrap();
579 writeln!(json, " \"clip_depth\": {clip_depth},").unwrap();
580 writeln!(json, " \"scroll_depth\": {scroll_depth},").unwrap();
581 writeln!(json, " \"node_id\": {node_id:?}").unwrap();
582 writeln!(json, " }}{comma}").unwrap();
583 }
584 }
585 }
586
587 writeln!(json, " ],").unwrap();
588 writeln!(json, " \"final_clip_depth\": {clip_depth},").unwrap();
589 writeln!(json, " \"final_scroll_depth\": {scroll_depth},").unwrap();
590 writeln!(json, " \"final_stacking_depth\": {stacking_depth},").unwrap();
591 writeln!(
592 json,
593 " \"balanced\": {}",
594 clip_depth == 0 && scroll_depth == 0 && stacking_depth == 0
595 )
596 .unwrap();
597 writeln!(json, "}}").unwrap();
598
599 json
600 }
601}
602
603#[derive(Debug, Clone)]
606pub enum DisplayListItem {
607 Rect {
611 bounds: WindowLogicalRect,
613 color: ColorU,
615 border_radius: BorderRadius,
617 },
618 SelectionRect {
621 bounds: WindowLogicalRect,
623 border_radius: BorderRadius,
625 color: ColorU,
627 },
628 CursorRect {
631 bounds: WindowLogicalRect,
633 color: ColorU,
635 },
636 Border {
639 bounds: WindowLogicalRect,
641 widths: StyleBorderWidths,
643 colors: StyleBorderColors,
645 styles: StyleBorderStyles,
647 border_radius: StyleBorderRadius,
649 },
650 TextLayout {
654 layout: Arc<dyn std::any::Any + Send + Sync>, bounds: WindowLogicalRect,
656 font_hash: FontHash,
657 font_size_px: f32,
658 color: ColorU,
659 },
660 Text {
662 glyphs: Vec<GlyphInstance>,
663 font_hash: FontHash,
664 font_size_px: f32,
665 color: ColorU,
666 clip_rect: WindowLogicalRect,
667 source_node_index: Option<usize>,
670 },
671 Underline {
673 bounds: WindowLogicalRect,
674 color: ColorU,
675 thickness: f32,
676 },
677 Strikethrough {
679 bounds: WindowLogicalRect,
680 color: ColorU,
681 thickness: f32,
682 },
683 Overline {
685 bounds: WindowLogicalRect,
686 color: ColorU,
687 thickness: f32,
688 },
689 Image {
690 bounds: WindowLogicalRect,
691 image: ImageRef,
692 border_radius: BorderRadius,
693 },
694 ScrollBar {
697 bounds: WindowLogicalRect,
698 color: ColorU,
699 orientation: ScrollbarOrientation,
700 opacity_key: Option<OpacityKey>,
704 hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
707 },
708 ScrollBarStyled {
711 info: Box<ScrollbarDrawInfo>,
713 },
714
715 VirtualView {
721 child_dom_id: DomId,
723 bounds: WindowLogicalRect,
725 clip_rect: WindowLogicalRect,
727 },
728
729 VirtualViewPlaceholder {
738 node_id: NodeId,
740 bounds: WindowLogicalRect,
742 clip_rect: WindowLogicalRect,
744 },
745
746 PushClip {
750 bounds: WindowLogicalRect,
751 border_radius: BorderRadius,
752 },
753 PopClip,
755
756 PushImageMaskClip {
760 bounds: WindowLogicalRect,
762 mask_image: ImageRef,
764 mask_rect: WindowLogicalRect,
766 },
767 PopImageMaskClip,
769
770 PushScrollFrame {
773 clip_bounds: WindowLogicalRect,
775 content_size: LogicalSize,
777 scroll_id: LocalScrollId,
779 },
780 PopScrollFrame,
782
783 PushStackingContext {
786 z_index: i32,
788 bounds: WindowLogicalRect,
790 },
791 PopStackingContext,
793
794 PushReferenceFrame {
798 transform_key: TransformKey,
800 initial_transform: ComputedTransform3D,
802 bounds: WindowLogicalRect,
804 },
805 PopReferenceFrame,
807
808 HitTestArea {
810 bounds: WindowLogicalRect,
811 tag: DisplayListTagId, },
813
814 LinearGradient {
817 bounds: WindowLogicalRect,
818 gradient: LinearGradient,
819 border_radius: BorderRadius,
820 },
821 RadialGradient {
823 bounds: WindowLogicalRect,
824 gradient: RadialGradient,
825 border_radius: BorderRadius,
826 },
827 ConicGradient {
829 bounds: WindowLogicalRect,
830 gradient: ConicGradient,
831 border_radius: BorderRadius,
832 },
833
834 BoxShadow {
837 bounds: WindowLogicalRect,
838 shadow: StyleBoxShadow,
839 border_radius: BorderRadius,
840 },
841
842 PushFilter {
845 bounds: WindowLogicalRect,
846 filters: Vec<StyleFilter>,
847 },
848 PopFilter,
850
851 PushBackdropFilter {
853 bounds: WindowLogicalRect,
854 filters: Vec<StyleFilter>,
855 },
856 PopBackdropFilter,
858
859 PushOpacity {
861 bounds: WindowLogicalRect,
862 opacity: f32,
863 },
864 PopOpacity,
866
867 PushTextShadow {
869 shadow: StyleBoxShadow,
870 },
871 PopTextShadow,
873}
874
875impl DisplayListItem {
876 #[allow(clippy::float_cmp)]
883 #[allow(clippy::similar_names)] #[allow(clippy::match_same_arms)] #[allow(clippy::too_many_lines)] #[must_use] pub fn is_visually_equal(&self, other: &Self) -> bool {
887 if std::mem::discriminant(self) != std::mem::discriminant(other) {
888 return false;
889 }
890 match (self, other) {
891 (Self::Rect { bounds: b1, color: c1, border_radius: br1 },
892 Self::Rect { bounds: b2, color: c2, border_radius: br2 }) => {
893 b1 == b2 && c1 == c2 && br1.top_left == br2.top_left && br1.top_right == br2.top_right
894 && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
895 }
896 (Self::SelectionRect { bounds: b1, border_radius: br1, color: c1 },
897 Self::SelectionRect { bounds: b2, border_radius: br2, color: c2 }) => {
898 b1 == b2 && c1 == c2 && br1.top_left == br2.top_left && br1.top_right == br2.top_right
899 && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
900 }
901 (Self::CursorRect { bounds: b1, color: c1 },
902 Self::CursorRect { bounds: b2, color: c2 }) => b1 == b2 && c1 == c2,
903 (Self::Text { glyphs: g1, font_hash: fh1, font_size_px: fs1, color: c1, clip_rect: cr1, .. },
904 Self::Text { glyphs: g2, font_hash: fh2, font_size_px: fs2, color: c2, clip_rect: cr2, .. }) => {
905 cr1 == cr2 && c1 == c2 && fh1 == fh2 && fs1 == fs2 && g1.len() == g2.len()
906 && g1.iter().zip(g2.iter()).all(|(a, b)| {
907 a.index == b.index
908 && a.point.x == b.point.x
909 && a.point.y == b.point.y
910 })
911 }
912 (Self::Underline { bounds: b1, color: c1, thickness: t1 },
913 Self::Underline { bounds: b2, color: c2, thickness: t2 }) => b1 == b2 && c1 == c2 && t1 == t2,
914 (Self::Strikethrough { bounds: b1, color: c1, thickness: t1 },
915 Self::Strikethrough { bounds: b2, color: c2, thickness: t2 }) => b1 == b2 && c1 == c2 && t1 == t2,
916 (Self::Overline { bounds: b1, color: c1, thickness: t1 },
917 Self::Overline { bounds: b2, color: c2, thickness: t2 }) => b1 == b2 && c1 == c2 && t1 == t2,
918 (Self::Border { bounds: b1, widths: w1, colors: c1, styles: s1, .. },
919 Self::Border { bounds: b2, widths: w2, colors: c2, styles: s2, .. }) => {
920 b1 == b2
921 && w1.top == w2.top && w1.right == w2.right && w1.bottom == w2.bottom && w1.left == w2.left
922 && c1.top == c2.top && c1.right == c2.right && c1.bottom == c2.bottom && c1.left == c2.left
923 && s1.top == s2.top && s1.right == s2.right && s1.bottom == s2.bottom && s1.left == s2.left
924 }
925 (Self::Image { bounds: b1, image: i1, border_radius: br1 },
926 Self::Image { bounds: b2, image: i2, border_radius: br2 }) => {
927 b1 == b2
928 && std::ptr::eq(i1.data, i2.data) && br1.top_left == br2.top_left && br1.top_right == br2.top_right
930 && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
931 }
932 (Self::BoxShadow { bounds: b1, shadow: s1, border_radius: br1 },
933 Self::BoxShadow { bounds: b2, shadow: s2, border_radius: br2 }) => {
934 b1 == b2 && s1 == s2
935 && br1.top_left == br2.top_left && br1.top_right == br2.top_right
936 && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
937 }
938 (Self::LinearGradient { bounds: b1, gradient: g1, border_radius: br1 },
939 Self::LinearGradient { bounds: b2, gradient: g2, border_radius: br2 }) => {
940 b1 == b2 && g1 == g2
941 && br1.top_left == br2.top_left && br1.top_right == br2.top_right
942 && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
943 }
944 (Self::RadialGradient { bounds: b1, gradient: g1, border_radius: br1 },
945 Self::RadialGradient { bounds: b2, gradient: g2, border_radius: br2 }) => {
946 b1 == b2 && g1 == g2
947 && br1.top_left == br2.top_left && br1.top_right == br2.top_right
948 && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
949 }
950 (Self::ConicGradient { bounds: b1, gradient: g1, border_radius: br1 },
951 Self::ConicGradient { bounds: b2, gradient: g2, border_radius: br2 }) => {
952 b1 == b2 && g1 == g2
953 && br1.top_left == br2.top_left && br1.top_right == br2.top_right
954 && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
955 }
956 (Self::ScrollBar { bounds: b1, color: c1, .. },
957 Self::ScrollBar { bounds: b2, color: c2, .. }) => b1 == b2 && c1 == c2,
958 (Self::PushClip { bounds: b1, .. }, Self::PushClip { bounds: b2, .. }) => b1 == b2,
959 (Self::PushScrollFrame { clip_bounds: b1, scroll_id: s1, .. },
960 Self::PushScrollFrame { clip_bounds: b2, scroll_id: s2, .. }) => b1 == b2 && s1 == s2,
961 (Self::PushStackingContext { z_index: z1, bounds: b1 },
962 Self::PushStackingContext { z_index: z2, bounds: b2 }) => z1 == z2 && b1 == b2,
963 (Self::PushOpacity { bounds: b1, opacity: o1 },
964 Self::PushOpacity { bounds: b2, opacity: o2 }) => b1 == b2 && o1 == o2,
965 (Self::PopClip, Self::PopClip)
967 | (Self::PopImageMaskClip, Self::PopImageMaskClip)
968 | (Self::PopScrollFrame, Self::PopScrollFrame)
969 | (Self::PopStackingContext, Self::PopStackingContext)
970 | (Self::PopReferenceFrame, Self::PopReferenceFrame)
971 | (Self::PopFilter, Self::PopFilter)
972 | (Self::PopBackdropFilter, Self::PopBackdropFilter)
973 | (Self::PopOpacity, Self::PopOpacity)
974 | (Self::PopTextShadow, Self::PopTextShadow) => true,
975 (Self::HitTestArea { .. }, Self::HitTestArea { .. }) => true,
980 (Self::TextLayout { layout: l1, bounds: b1, font_hash: fh1, font_size_px: fs1, color: c1 },
986 Self::TextLayout { layout: l2, bounds: b2, font_hash: fh2, font_size_px: fs2, color: c2 }) => {
987 b1 == b2
988 && fh1 == fh2
989 && fs1 == fs2
990 && c1 == c2
991 && Arc::ptr_eq(l1, l2)
992 }
993 (Self::ScrollBarStyled { info: i1 }, Self::ScrollBarStyled { info: i2 }) => i1 == i2,
1001 (Self::VirtualView { child_dom_id: d1, bounds: b1, clip_rect: c1 },
1005 Self::VirtualView { child_dom_id: d2, bounds: b2, clip_rect: c2 }) => {
1006 d1 == d2 && b1 == b2 && c1 == c2
1007 }
1008 (Self::VirtualViewPlaceholder { bounds: b1, .. },
1009 Self::VirtualViewPlaceholder { bounds: b2, .. }) => b1 == b2,
1010 (Self::PushReferenceFrame { transform_key: k1, initial_transform: t1, bounds: b1 },
1014 Self::PushReferenceFrame { transform_key: k2, initial_transform: t2, bounds: b2 }) => {
1015 k1 == k2 && t1 == t2 && b1 == b2
1016 }
1017 (Self::PushFilter { bounds: b1, filters: f1 },
1018 Self::PushFilter { bounds: b2, filters: f2 }) => b1 == b2 && f1 == f2,
1019 (Self::PushBackdropFilter { bounds: b1, filters: f1 },
1020 Self::PushBackdropFilter { bounds: b2, filters: f2 }) => b1 == b2 && f1 == f2,
1021 (Self::PushImageMaskClip { bounds: b1, mask_image: m1, mask_rect: r1 },
1024 Self::PushImageMaskClip { bounds: b2, mask_image: m2, mask_rect: r2 }) => {
1025 b1 == b2 && r1 == r2 && m1.get_hash() == m2.get_hash()
1026 }
1027 (Self::PushTextShadow { shadow: s1 }, Self::PushTextShadow { shadow: s2 }) => s1 == s2,
1028 _ => false,
1031 }
1032 }
1033
1034 #[must_use] pub const fn is_state_management(&self) -> bool {
1037 matches!(self,
1038 Self::PushClip { .. }
1039 | Self::PopClip
1040 | Self::PushImageMaskClip { .. }
1041 | Self::PopImageMaskClip
1042 | Self::PushScrollFrame { .. }
1043 | Self::PopScrollFrame
1044 | Self::PushStackingContext { .. }
1045 | Self::PopStackingContext
1046 | Self::PushReferenceFrame { .. }
1047 | Self::PopReferenceFrame
1048 | Self::PushFilter { .. }
1049 | Self::PopFilter
1050 | Self::PushBackdropFilter { .. }
1051 | Self::PopBackdropFilter
1052 | Self::PushOpacity { .. }
1053 | Self::PopOpacity
1054 | Self::PushTextShadow { .. }
1055 | Self::PopTextShadow
1056 )
1057 }
1058
1059 #[allow(clippy::suboptimal_flops)] #[must_use] pub fn visual_bounds(&self) -> Option<LogicalRect> {
1064 match self {
1065 Self::BoxShadow { bounds, shadow, .. } => {
1066 let b = *bounds.inner();
1067 let ox = shadow
1069 .offset_x
1070 .to_pixels_internal(DEFAULT_SHADOW_FONT_SIZE_PX, DEFAULT_SHADOW_FONT_SIZE_PX)
1071 .abs();
1072 let oy = shadow
1073 .offset_y
1074 .to_pixels_internal(DEFAULT_SHADOW_FONT_SIZE_PX, DEFAULT_SHADOW_FONT_SIZE_PX)
1075 .abs();
1076 let blur = shadow
1077 .blur_radius
1078 .to_pixels_internal(DEFAULT_SHADOW_FONT_SIZE_PX, DEFAULT_SHADOW_FONT_SIZE_PX)
1079 .abs();
1080 let spread = shadow
1081 .spread_radius
1082 .to_pixels_internal(DEFAULT_SHADOW_FONT_SIZE_PX, DEFAULT_SHADOW_FONT_SIZE_PX)
1083 .abs();
1084 let expand = ox + oy + blur + spread;
1085 Some(LogicalRect {
1086 origin: LogicalPosition {
1087 x: b.origin.x - expand,
1088 y: b.origin.y - expand,
1089 },
1090 size: LogicalSize {
1091 width: b.size.width + expand * 2.0,
1092 height: b.size.height + expand * 2.0,
1093 },
1094 })
1095 }
1096 _ => self.bounds(),
1097 }
1098 }
1099
1100 #[allow(clippy::match_same_arms)] #[must_use] pub fn bounds(&self) -> Option<LogicalRect> {
1104 match self {
1105 Self::Rect { bounds, .. }
1106 | Self::SelectionRect { bounds, .. }
1107 | Self::CursorRect { bounds, .. }
1108 | Self::Border { bounds, .. }
1109 | Self::Text { clip_rect: bounds, .. }
1110 | Self::TextLayout { bounds, .. }
1111 | Self::Underline { bounds, .. }
1112 | Self::Strikethrough { bounds, .. }
1113 | Self::Overline { bounds, .. }
1114 | Self::Image { bounds, .. }
1115 | Self::ScrollBar { bounds, .. }
1116 | Self::LinearGradient { bounds, .. }
1117 | Self::RadialGradient { bounds, .. }
1118 | Self::ConicGradient { bounds, .. }
1119 | Self::BoxShadow { bounds, .. }
1120 | Self::VirtualView { bounds, .. }
1121 | Self::VirtualViewPlaceholder { bounds, .. }
1122 | Self::HitTestArea { bounds, .. }
1123 | Self::PushClip { bounds, .. }
1124 | Self::PushImageMaskClip { bounds, .. }
1125 | Self::PushScrollFrame { clip_bounds: bounds, .. }
1126 | Self::PushStackingContext { bounds, .. }
1127 | Self::PushReferenceFrame { bounds, .. }
1128 | Self::PushFilter { bounds, .. }
1129 | Self::PushBackdropFilter { bounds, .. }
1130 | Self::PushOpacity { bounds, .. } => Some(*bounds.inner()),
1131 Self::ScrollBarStyled { info, .. } => Some(*info.bounds.inner()),
1132 Self::PushTextShadow { .. } => None, Self::PopClip
1134 | Self::PopImageMaskClip
1135 | Self::PopScrollFrame
1136 | Self::PopStackingContext
1137 | Self::PopReferenceFrame
1138 | Self::PopFilter
1139 | Self::PopBackdropFilter
1140 | Self::PopOpacity
1141 | Self::PopTextShadow => None,
1142 }
1143 }
1144}
1145
1146#[derive(Debug, Copy, Clone, Default, PartialEq)]
1148pub struct BorderRadius {
1149 pub top_left: f32,
1150 pub top_right: f32,
1151 pub bottom_left: f32,
1152 pub bottom_right: f32,
1153}
1154
1155impl BorderRadius {
1156 #[must_use] pub fn is_zero(&self) -> bool {
1157 self.top_left == 0.0
1158 && self.top_right == 0.0
1159 && self.bottom_left == 0.0
1160 && self.bottom_right == 0.0
1161 }
1162}
1163
1164pub type LocalScrollId = u64;
1166pub(crate) type DisplayListTagId = (u64, u16);
1171
1172#[derive(Debug, Default)]
1174struct DisplayListBuilder {
1175 items: Vec<DisplayListItem>,
1176 node_mapping: Vec<Option<NodeId>>,
1177 current_node: Option<NodeId>,
1179 debug_messages: Vec<LayoutDebugMessage>,
1181 debug_enabled: bool,
1183 forced_page_breaks: Vec<f32>,
1185 fixed_position_item_ranges: Vec<(usize, usize)>,
1187 fixed_position_start: Option<usize>,
1189}
1190
1191impl DisplayListBuilder {
1192 pub(crate) fn new() -> Self {
1193 Self::default()
1194 }
1195
1196 pub(crate) const fn with_debug(debug_enabled: bool) -> Self {
1197 Self {
1198 items: Vec::new(),
1199 node_mapping: Vec::new(),
1200 current_node: None,
1201 debug_messages: Vec::new(),
1202 debug_enabled,
1203 forced_page_breaks: Vec::new(),
1204 fixed_position_item_ranges: Vec::new(),
1205 fixed_position_start: None,
1206 }
1207 }
1208
1209 fn debug_log(&mut self, message: String) {
1211 if self.debug_enabled {
1212 self.debug_messages.push(LayoutDebugMessage::info(message));
1213 }
1214 }
1215
1216 pub(crate) fn build_with_debug(
1218 mut self,
1219 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1220 ) -> DisplayList {
1221 if let Some(msgs) = debug_messages.as_mut() {
1223 msgs.append(&mut self.debug_messages);
1224 }
1225 DisplayList {
1226 items: self.items,
1227 node_mapping: self.node_mapping,
1228 forced_page_breaks: self.forced_page_breaks,
1229 fixed_position_item_ranges: self.fixed_position_item_ranges,
1230 }
1231 }
1232
1233 pub(crate) const fn set_current_node(&mut self, node_id: Option<NodeId>) {
1235 self.current_node = node_id;
1236 }
1237
1238 pub(crate) const fn begin_fixed_position_element(&mut self) {
1240 self.fixed_position_start = Some(self.items.len());
1241 }
1242
1243 pub(crate) fn end_fixed_position_element(&mut self) {
1246 if let Some(start) = self.fixed_position_start.take() {
1247 let end = self.items.len();
1248 if end > start {
1249 self.fixed_position_item_ranges.push((start, end));
1250 }
1251 }
1252 }
1253
1254 pub(crate) fn add_forced_page_break(&mut self, y_position: f32) {
1257 if !self.forced_page_breaks.contains(&y_position) {
1259 self.forced_page_breaks.push(y_position);
1260 self.forced_page_breaks
1261 .sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
1262 }
1263 }
1264
1265 fn push_item(&mut self, item: DisplayListItem) {
1267 self.items.push(item);
1268 self.node_mapping.push(self.current_node);
1269 }
1270
1271 pub(crate) fn build(self) -> DisplayList {
1272 DisplayList {
1273 items: self.items,
1274 node_mapping: self.node_mapping,
1275 forced_page_breaks: self.forced_page_breaks,
1276 fixed_position_item_ranges: self.fixed_position_item_ranges,
1277 }
1278 }
1279
1280 pub(crate) fn push_hit_test_area(&mut self, bounds: LogicalRect, tag: DisplayListTagId) {
1281 self.push_item(DisplayListItem::HitTestArea { bounds: bounds.into(), tag });
1282 }
1283
1284 pub(crate) fn push_scrollbar(
1286 &mut self,
1287 bounds: LogicalRect,
1288 color: ColorU,
1289 orientation: ScrollbarOrientation,
1290 opacity_key: Option<OpacityKey>,
1291 hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
1292 ) {
1293 if color.a > 0 || opacity_key.is_some() {
1294 self.push_item(DisplayListItem::ScrollBar {
1296 bounds: bounds.into(),
1297 color,
1298 orientation,
1299 opacity_key,
1300 hit_id,
1301 });
1302 }
1303 }
1304
1305 pub(crate) fn push_scrollbar_styled(&mut self, info: ScrollbarDrawInfo) {
1307 if info.thumb_color.a > 0 || info.track_color.a > 0 || info.opacity_key.is_some() {
1309 self.push_item(DisplayListItem::ScrollBarStyled {
1310 info: Box::new(info),
1311 });
1312 }
1313 }
1314
1315 pub(crate) fn push_rect(&mut self, bounds: LogicalRect, color: ColorU, border_radius: BorderRadius) {
1316 if color.a > 0 {
1317 self.push_item(DisplayListItem::Rect {
1319 bounds: bounds.into(),
1320 color,
1321 border_radius,
1322 });
1323 }
1324 }
1325
1326 pub(crate) fn push_backgrounds_and_border(
1336 &mut self,
1337 bounds: LogicalRect,
1338 background_contents: &[azul_css::props::style::StyleBackgroundContent],
1339 border_info: &BorderInfo,
1340 simple_border_radius: BorderRadius,
1341 style_border_radius: StyleBorderRadius,
1342 image_cache: &azul_core::resources::ImageCache,
1343 ) {
1344 use azul_css::props::style::StyleBackgroundContent;
1345
1346 for bg in background_contents {
1348 match bg {
1349 StyleBackgroundContent::Color(color) => {
1350 self.push_rect(bounds, *color, simple_border_radius);
1351 }
1352 StyleBackgroundContent::LinearGradient(gradient) => {
1353 self.push_linear_gradient(bounds, gradient.clone(), simple_border_radius);
1354 }
1355 StyleBackgroundContent::RadialGradient(gradient) => {
1356 self.push_radial_gradient(bounds, gradient.clone(), simple_border_radius);
1357 }
1358 StyleBackgroundContent::ConicGradient(gradient) => {
1359 self.push_conic_gradient(bounds, gradient.clone(), simple_border_radius);
1360 }
1361 StyleBackgroundContent::Image(image_id) => {
1362 if let Some(image_ref) = image_cache.get_css_image_id(image_id) {
1363 self.push_image(bounds, image_ref.clone(), simple_border_radius);
1364 }
1365 }
1366 StyleBackgroundContent::SystemColor(_s) => {
1367 }
1372 }
1373 }
1374
1375 self.push_border(
1377 bounds,
1378 border_info.widths,
1379 border_info.colors,
1380 border_info.styles,
1381 style_border_radius,
1382 );
1383 }
1384
1385 pub(crate) fn push_inline_backgrounds_and_border(
1392 &mut self,
1393 bounds: LogicalRect,
1394 background_color: Option<ColorU>,
1395 background_contents: &[azul_css::props::style::StyleBackgroundContent],
1396 border: Option<&crate::text3::cache::InlineBorderInfo>,
1397 image_cache: &azul_core::resources::ImageCache,
1398 ) {
1399 use azul_css::props::style::StyleBackgroundContent;
1400
1401 if let Some(bg_color) = background_color {
1403 self.push_rect(bounds, bg_color, BorderRadius::default());
1404 }
1405
1406 for bg in background_contents {
1408 match bg {
1409 StyleBackgroundContent::Color(color) => {
1410 self.push_rect(bounds, *color, BorderRadius::default());
1411 }
1412 StyleBackgroundContent::LinearGradient(gradient) => {
1413 self.push_linear_gradient(bounds, gradient.clone(), BorderRadius::default());
1414 }
1415 StyleBackgroundContent::RadialGradient(gradient) => {
1416 self.push_radial_gradient(bounds, gradient.clone(), BorderRadius::default());
1417 }
1418 StyleBackgroundContent::ConicGradient(gradient) => {
1419 self.push_conic_gradient(bounds, gradient.clone(), BorderRadius::default());
1420 }
1421 StyleBackgroundContent::Image(image_id) => {
1422 if let Some(image_ref) = image_cache.get_css_image_id(image_id) {
1423 self.push_image(bounds, image_ref.clone(), BorderRadius::default());
1424 }
1425 }
1426 StyleBackgroundContent::SystemColor(_s) => {
1427 }
1432 }
1433 }
1434
1435 if let Some(border) = border {
1438 let effective_left = if border.left_inset() > 0.0 { border.left } else { 0.0 };
1439 let effective_right = if border.right_inset() > 0.0 { border.right } else { 0.0 };
1440 if border.top > 0.0 || effective_right > 0.0 || border.bottom > 0.0 || effective_left > 0.0 {
1441 let border_widths = StyleBorderWidths {
1442 top: Some(CssPropertyValue::Exact(LayoutBorderTopWidth {
1443 inner: PixelValue::px(border.top),
1444 })),
1445 right: Some(CssPropertyValue::Exact(LayoutBorderRightWidth {
1446 inner: PixelValue::px(effective_right),
1447 })),
1448 bottom: Some(CssPropertyValue::Exact(LayoutBorderBottomWidth {
1449 inner: PixelValue::px(border.bottom),
1450 })),
1451 left: Some(CssPropertyValue::Exact(LayoutBorderLeftWidth {
1452 inner: PixelValue::px(effective_left),
1453 })),
1454 };
1455 let border_colors = StyleBorderColors {
1456 top: Some(CssPropertyValue::Exact(StyleBorderTopColor {
1457 inner: border.top_color,
1458 })),
1459 right: Some(CssPropertyValue::Exact(StyleBorderRightColor {
1460 inner: border.right_color,
1461 })),
1462 bottom: Some(CssPropertyValue::Exact(StyleBorderBottomColor {
1463 inner: border.bottom_color,
1464 })),
1465 left: Some(CssPropertyValue::Exact(StyleBorderLeftColor {
1466 inner: border.left_color,
1467 })),
1468 };
1469 let border_styles = StyleBorderStyles {
1470 top: Some(CssPropertyValue::Exact(StyleBorderTopStyle {
1471 inner: BorderStyle::Solid,
1472 })),
1473 right: Some(CssPropertyValue::Exact(StyleBorderRightStyle {
1474 inner: BorderStyle::Solid,
1475 })),
1476 bottom: Some(CssPropertyValue::Exact(StyleBorderBottomStyle {
1477 inner: BorderStyle::Solid,
1478 })),
1479 left: Some(CssPropertyValue::Exact(StyleBorderLeftStyle {
1480 inner: BorderStyle::Solid,
1481 })),
1482 };
1483 let radius_px = PixelValue::px(border.radius.unwrap_or(0.0));
1484 let border_radius = StyleBorderRadius {
1485 top_left: radius_px,
1486 top_right: radius_px,
1487 bottom_left: radius_px,
1488 bottom_right: radius_px,
1489 };
1490
1491 self.push_border(
1492 bounds,
1493 border_widths,
1494 border_colors,
1495 border_styles,
1496 border_radius,
1497 );
1498 }
1499 }
1500 }
1501
1502 pub(crate) fn push_linear_gradient(
1504 &mut self,
1505 bounds: LogicalRect,
1506 gradient: LinearGradient,
1507 border_radius: BorderRadius,
1508 ) {
1509 self.push_item(DisplayListItem::LinearGradient {
1510 bounds: bounds.into(),
1511 gradient,
1512 border_radius,
1513 });
1514 }
1515
1516 pub(crate) fn push_radial_gradient(
1518 &mut self,
1519 bounds: LogicalRect,
1520 gradient: RadialGradient,
1521 border_radius: BorderRadius,
1522 ) {
1523 self.push_item(DisplayListItem::RadialGradient {
1524 bounds: bounds.into(),
1525 gradient,
1526 border_radius,
1527 });
1528 }
1529
1530 pub(crate) fn push_conic_gradient(
1532 &mut self,
1533 bounds: LogicalRect,
1534 gradient: ConicGradient,
1535 border_radius: BorderRadius,
1536 ) {
1537 self.push_item(DisplayListItem::ConicGradient {
1538 bounds: bounds.into(),
1539 gradient,
1540 border_radius,
1541 });
1542 }
1543
1544 pub(crate) fn push_selection_rect(
1545 &mut self,
1546 bounds: LogicalRect,
1547 color: ColorU,
1548 border_radius: BorderRadius,
1549 ) {
1550 if color.a > 0 {
1551 self.push_item(DisplayListItem::SelectionRect {
1552 bounds: bounds.into(),
1553 color,
1554 border_radius,
1555 });
1556 }
1557 }
1558
1559 pub(crate) fn push_cursor_rect(&mut self, bounds: LogicalRect, color: ColorU) {
1560 self.push_item(DisplayListItem::CursorRect { bounds: bounds.into(), color });
1565 }
1566 pub(crate) fn push_clip(&mut self, bounds: LogicalRect, border_radius: BorderRadius) {
1567 self.push_item(DisplayListItem::PushClip {
1568 bounds: bounds.into(),
1569 border_radius,
1570 });
1571 }
1572 pub(crate) fn pop_clip(&mut self) {
1573 self.push_item(DisplayListItem::PopClip);
1574 }
1575 pub(crate) fn push_image_mask_clip(&mut self, bounds: LogicalRect, mask_image: ImageRef, mask_rect: LogicalRect) {
1576 self.push_item(DisplayListItem::PushImageMaskClip {
1577 bounds: bounds.into(),
1578 mask_image,
1579 mask_rect: mask_rect.into(),
1580 });
1581 }
1582 pub(crate) fn pop_image_mask_clip(&mut self) {
1583 self.push_item(DisplayListItem::PopImageMaskClip);
1584 }
1585 pub(crate) fn push_scroll_frame(
1586 &mut self,
1587 clip_bounds: LogicalRect,
1588 content_size: LogicalSize,
1589 scroll_id: LocalScrollId,
1590 ) {
1591 self.push_item(DisplayListItem::PushScrollFrame {
1592 clip_bounds: clip_bounds.into(),
1593 content_size,
1594 scroll_id,
1595 });
1596 }
1597 pub(crate) fn pop_scroll_frame(&mut self) {
1598 self.push_item(DisplayListItem::PopScrollFrame);
1599 }
1600 pub(crate) fn push_virtual_view_placeholder(
1601 &mut self,
1602 node_id: NodeId,
1603 bounds: LogicalRect,
1604 clip_rect: LogicalRect,
1605 ) {
1606 self.push_item(DisplayListItem::VirtualViewPlaceholder {
1607 node_id,
1608 bounds: bounds.into(),
1609 clip_rect: clip_rect.into(),
1610 });
1611 }
1612 pub(crate) fn push_border(
1613 &mut self,
1614 bounds: LogicalRect,
1615 widths: StyleBorderWidths,
1616 colors: StyleBorderColors,
1617 styles: StyleBorderStyles,
1618 border_radius: StyleBorderRadius,
1619 ) {
1620 let has_visible_border = {
1622 let has_width = widths.top.is_some()
1623 || widths.right.is_some()
1624 || widths.bottom.is_some()
1625 || widths.left.is_some();
1626 let has_style = styles.top.is_some()
1627 || styles.right.is_some()
1628 || styles.bottom.is_some()
1629 || styles.left.is_some();
1630 has_width && has_style
1631 };
1632
1633 if has_visible_border {
1634 self.push_item(DisplayListItem::Border {
1635 bounds: bounds.into(),
1636 widths,
1637 colors,
1638 styles,
1639 border_radius,
1640 });
1641 }
1642 }
1643
1644 pub(crate) fn push_stacking_context(&mut self, z_index: i32, bounds: LogicalRect) {
1645 self.push_item(DisplayListItem::PushStackingContext { z_index, bounds: bounds.into() });
1646 }
1647
1648 pub(crate) fn pop_stacking_context(&mut self) {
1649 self.push_item(DisplayListItem::PopStackingContext);
1650 }
1651
1652 pub(crate) fn push_reference_frame(
1653 &mut self,
1654 transform_key: TransformKey,
1655 initial_transform: ComputedTransform3D,
1656 bounds: LogicalRect,
1657 ) {
1658 self.push_item(DisplayListItem::PushReferenceFrame {
1659 transform_key,
1660 initial_transform,
1661 bounds: bounds.into(),
1662 });
1663 }
1664
1665 pub(crate) fn pop_reference_frame(&mut self) {
1666 self.push_item(DisplayListItem::PopReferenceFrame);
1667 }
1668
1669 pub(crate) fn push_text_run(
1670 &mut self,
1671 glyphs: Vec<GlyphInstance>,
1672 font_hash: FontHash, font_size_px: f32,
1674 color: ColorU,
1675 clip_rect: LogicalRect,
1676 source_node_index: Option<usize>,
1677 ) {
1678 self.debug_log(format!(
1679 "[push_text_run] {} glyphs, font_size={}px, color=({},{},{},{}), clip={:?}",
1680 glyphs.len(),
1681 font_size_px,
1682 color.r,
1683 color.g,
1684 color.b,
1685 color.a,
1686 clip_rect
1687 ));
1688
1689 if !glyphs.is_empty() && color.a > 0 {
1690 self.push_item(DisplayListItem::Text {
1691 glyphs,
1692 font_hash,
1693 font_size_px,
1694 color,
1695 clip_rect: clip_rect.into(),
1696 source_node_index,
1697 });
1698 } else {
1699 self.debug_log(format!(
1700 "[push_text_run] SKIPPED: glyphs.is_empty()={}, color.a={}",
1701 glyphs.is_empty(),
1702 color.a
1703 ));
1704 }
1705 }
1706
1707 pub(crate) fn push_text_layout(
1708 &mut self,
1709 layout: Arc<dyn std::any::Any + Send + Sync>,
1710 bounds: LogicalRect,
1711 font_hash: FontHash,
1712 font_size_px: f32,
1713 color: ColorU,
1714 ) {
1715 if color.a > 0 {
1716 self.push_item(DisplayListItem::TextLayout {
1717 layout,
1718 bounds: bounds.into(),
1719 font_hash,
1720 font_size_px,
1721 color,
1722 });
1723 }
1724 }
1725
1726 pub(crate) fn push_underline(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
1727 if color.a > 0 && thickness > 0.0 {
1728 self.push_item(DisplayListItem::Underline {
1729 bounds: bounds.into(),
1730 color,
1731 thickness,
1732 });
1733 }
1734 }
1735
1736 pub(crate) fn push_strikethrough(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
1737 if color.a > 0 && thickness > 0.0 {
1738 self.push_item(DisplayListItem::Strikethrough {
1739 bounds: bounds.into(),
1740 color,
1741 thickness,
1742 });
1743 }
1744 }
1745
1746 pub(crate) fn push_overline(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
1747 if color.a > 0 && thickness > 0.0 {
1748 self.push_item(DisplayListItem::Overline {
1749 bounds: bounds.into(),
1750 color,
1751 thickness,
1752 });
1753 }
1754 }
1755
1756 pub(crate) fn push_image(&mut self, bounds: LogicalRect, image: ImageRef, border_radius: BorderRadius) {
1757 self.push_item(DisplayListItem::Image { bounds: bounds.into(), image, border_radius });
1758 }
1759}
1760
1761#[allow(clippy::implicit_hasher)] pub fn generate_display_list<T: ParsedFontTrait + Sync + 'static>(
1767 ctx: &mut LayoutContext<'_, T>,
1768 tree: &LayoutTree,
1769 calculated_positions: &super::PositionVec,
1770 scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
1771 scroll_ids: &HashMap<usize, u64>,
1772 gpu_value_cache: Option<&GpuValueCache>,
1773 renderer_resources: &RendererResources,
1774 id_namespace: IdNamespace,
1775 dom_id: DomId,
1776) -> Result<DisplayList> {
1777 debug_info!(
1778 ctx,
1779 "[DisplayList] generate_display_list: tree has {} nodes, {} positions calculated",
1780 tree.nodes.len(),
1781 calculated_positions.len()
1782 );
1783
1784 debug_info!(ctx, "Starting display list generation");
1785 debug_info!(
1786 ctx,
1787 "Collecting stacking contexts from root node {}",
1788 tree.root
1789 );
1790
1791 let positioned_tree = PositionedTree {
1792 tree,
1793 calculated_positions,
1794 };
1795 let mut generator = DisplayListGenerator::new(
1796 ctx,
1797 scroll_offsets,
1798 &positioned_tree,
1799 scroll_ids,
1800 gpu_value_cache,
1801 renderer_resources,
1802 id_namespace,
1803 dom_id,
1804 );
1805
1806 let debug_enabled = generator.ctx.debug_messages.is_some();
1808 let mut builder = DisplayListBuilder::with_debug(debug_enabled);
1809
1810 {
1817 let root_node = tree.get(tree.root);
1818 if let Some(root) = root_node {
1819 if let Some(root_dom_id) = root.dom_node_id {
1820 let root_state = generator.get_styled_node_state(root_dom_id);
1821 let canvas_bg = get_background_color(
1822 generator.ctx.styled_dom,
1823 root_dom_id,
1824 &root_state,
1825 );
1826 if canvas_bg.a > 0 {
1827 let viewport_rect = LogicalRect {
1828 origin: LogicalPosition::zero(),
1829 size: generator.ctx.viewport_size,
1830 };
1831 builder.push_rect(viewport_rect, canvas_bg, BorderRadius::default());
1832 debug_info!(
1833 generator.ctx,
1834 "[DisplayList] Canvas background: color=({},{},{},{}), size={:?}",
1835 canvas_bg.r, canvas_bg.g, canvas_bg.b, canvas_bg.a,
1836 generator.ctx.viewport_size
1837 );
1838 }
1839 }
1840 }
1841 }
1842
1843 let stacking_context_tree = generator.collect_stacking_contexts(tree.root)?;
1848
1849 debug_info!(
1851 generator.ctx,
1852 "Generating display items from stacking context tree"
1853 );
1854 generator.generate_for_stacking_context(&mut builder, &stacking_context_tree)?;
1855
1856 let display_list = builder.build_with_debug(generator.ctx.debug_messages);
1858 debug_info!(
1859 generator.ctx,
1860 "[DisplayList] Generated {} display items",
1861 display_list.items.len()
1862 );
1863 Ok(display_list)
1864}
1865
1866struct DisplayListGenerator<'a, 'b, T: ParsedFontTrait> {
1868 ctx: &'a mut LayoutContext<'b, T>,
1869 scroll_offsets: &'a BTreeMap<NodeId, ScrollPosition>,
1870 positioned_tree: &'a PositionedTree<'a>,
1871 scroll_ids: &'a HashMap<usize, u64>,
1872 gpu_value_cache: Option<&'a GpuValueCache>,
1873 renderer_resources: &'a RendererResources,
1874 id_namespace: IdNamespace,
1875 dom_id: DomId,
1876}
1877
1878#[derive(Debug)]
1881struct StackingContext {
1882 node_index: usize,
1883 z_index: i32,
1884 child_contexts: Vec<StackingContext>,
1885 in_flow_children: Vec<usize>,
1887}
1888
1889impl<'a, 'b, T> DisplayListGenerator<'a, 'b, T>
1890where
1891 T: ParsedFontTrait + Sync + 'static,
1892{
1893 pub(crate) const fn new(
1894 ctx: &'a mut LayoutContext<'b, T>,
1895 scroll_offsets: &'a BTreeMap<NodeId, ScrollPosition>,
1896 positioned_tree: &'a PositionedTree<'a>,
1897 scroll_ids: &'a HashMap<usize, u64>,
1898 gpu_value_cache: Option<&'a GpuValueCache>,
1899 renderer_resources: &'a RendererResources,
1900 id_namespace: IdNamespace,
1901 dom_id: DomId,
1902 ) -> Self {
1903 Self {
1904 ctx,
1905 scroll_offsets,
1906 positioned_tree,
1907 scroll_ids,
1908 gpu_value_cache,
1909 renderer_resources,
1910 id_namespace,
1911 dom_id,
1912 }
1913 }
1914
1915 fn get_styled_node_state(&self, dom_id: NodeId) -> azul_core::styled_dom::StyledNodeState {
1917 self.ctx
1918 .styled_dom
1919 .styled_nodes
1920 .as_container()
1921 .get(dom_id)
1922 .map(|n| n.styled_node_state)
1923 .unwrap_or_default()
1924 }
1925
1926 fn is_node_hidden(&self, node_index: usize) -> bool {
1930 use azul_css::props::style::effects::StyleVisibility;
1931 let Some(node) = self.positioned_tree.tree.get(node_index) else {
1932 return false;
1933 };
1934 let Some(dom_id) = node.dom_node_id else {
1935 return false;
1936 };
1937 let node_state = self.get_styled_node_state(dom_id);
1938 matches!(
1939 get_visibility(self.ctx.styled_dom, dom_id, &node_state),
1940 crate::solver3::getters::MultiValue::Exact(
1941 StyleVisibility::Hidden | StyleVisibility::Collapse
1942 )
1943 )
1944 }
1945
1946 #[allow(clippy::match_same_arms)] fn get_cursor_type_for_text_node(&self, node_id: NodeId) -> CursorType {
1950 use azul_css::props::style::effects::StyleCursor;
1951
1952 let styled_node_state = self.get_styled_node_state(node_id);
1953 let node_data_container = self.ctx.styled_dom.node_data.as_container();
1954 let node_data = node_data_container.get(node_id);
1955
1956 if let Some(node_data) = node_data {
1958 if let Some(CssPropertyValue::Exact(cursor)) = self.ctx.styled_dom.get_css_property_cache().get_cursor(
1959 node_data,
1960 &node_id,
1961 &styled_node_state,
1962 ) {
1963 return match cursor {
1964 StyleCursor::Default => CursorType::Default,
1965 StyleCursor::Pointer => CursorType::Pointer,
1966 StyleCursor::Text => CursorType::Text,
1967 StyleCursor::Crosshair => CursorType::Crosshair,
1968 StyleCursor::Move => CursorType::Move,
1969 StyleCursor::Help => CursorType::Help,
1970 StyleCursor::Wait => CursorType::Wait,
1971 StyleCursor::Progress => CursorType::Progress,
1972 StyleCursor::NsResize => CursorType::NsResize,
1973 StyleCursor::EwResize => CursorType::EwResize,
1974 StyleCursor::NeswResize => CursorType::NeswResize,
1975 StyleCursor::NwseResize => CursorType::NwseResize,
1976 StyleCursor::NResize => CursorType::NResize,
1977 StyleCursor::SResize => CursorType::SResize,
1978 StyleCursor::EResize => CursorType::EResize,
1979 StyleCursor::WResize => CursorType::WResize,
1980 StyleCursor::Grab => CursorType::Grab,
1981 StyleCursor::Grabbing => CursorType::Grabbing,
1982 StyleCursor::RowResize => CursorType::RowResize,
1983 StyleCursor::ColResize => CursorType::ColResize,
1984 StyleCursor::SeResize | StyleCursor::NeswResize => CursorType::NeswResize,
1986 StyleCursor::ZoomIn | StyleCursor::ZoomOut => CursorType::Default,
1987 StyleCursor::Copy | StyleCursor::Alias => CursorType::Default,
1988 StyleCursor::Cell => CursorType::Crosshair,
1989 StyleCursor::AllScroll => CursorType::Move,
1990 StyleCursor::ContextMenu => CursorType::Default,
1991 StyleCursor::VerticalText => CursorType::Text,
1992 StyleCursor::Unset => CursorType::Text, };
1994 }
1995 }
1996
1997 CursorType::Text
1999 }
2000
2001 fn paint_selections(
2004 &self,
2005 builder: &mut DisplayListBuilder,
2006 node_index: usize,
2007 ) -> Result<()> {
2008 let node = self
2009 .positioned_tree
2010 .tree
2011 .get(node_index)
2012 .ok_or(LayoutError::InvalidTree)?;
2013 let Some(dom_id) = node.dom_node_id else {
2014 return Ok(());
2015 };
2016
2017 let Some(layout) = self.positioned_tree.tree.get_inline_layout_for_node(node_index) else {
2021 return Ok(());
2022 };
2023
2024 let node_pos = self
2026 .positioned_tree
2027 .calculated_positions
2028 .get(node_index)
2029 .copied()
2030 .unwrap_or_default();
2031
2032 let ifc_root_index = self.positioned_tree.tree.get_ifc_root_layout_index(node_index);
2038 let anchor_pos = self
2039 .positioned_tree
2040 .calculated_positions
2041 .get(ifc_root_index)
2042 .copied()
2043 .unwrap_or(node_pos);
2044 let anchor_bp = self
2045 .positioned_tree
2046 .tree
2047 .get(ifc_root_index).map_or_else(|| node.box_props.unpack(), |n| n.box_props.unpack());
2048 let content_box_offset_x = anchor_pos.x + anchor_bp.padding.left + anchor_bp.border.left;
2049 let content_box_offset_y = anchor_pos.y + anchor_bp.padding.top + anchor_bp.border.top;
2050
2051 let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2053 let is_selectable = super::getters::is_text_selectable(self.ctx.styled_dom, dom_id, node_state);
2054 if !is_selectable {
2055 return Ok(());
2056 }
2057
2058 if let Some(text_selection) = self.ctx.text_selections.get(&self.ctx.styled_dom.dom_id) {
2060 if let Some(range) = text_selection.affected_nodes.get(&dom_id) {
2061 let is_collapsed = text_selection.is_collapsed();
2062 if !is_collapsed {
2064 let rects = layout.get_selection_rects(range);
2065 let style = get_selection_style(self.ctx.styled_dom, Some(dom_id), self.ctx.system_style.as_ref());
2066
2067 let border_radius = BorderRadius {
2068 top_left: style.radius,
2069 top_right: style.radius,
2070 bottom_left: style.radius,
2071 bottom_right: style.radius,
2072 };
2073
2074 for mut rect in rects {
2075 rect.origin.x += content_box_offset_x;
2076 rect.origin.y += content_box_offset_y;
2077 builder.push_selection_rect(rect, style.bg_color, border_radius);
2078 }
2079 }
2080
2081 return Ok(());
2082 }
2083 }
2084
2085 Ok(())
2086 }
2087
2088 #[allow(clippy::cast_precision_loss)] fn paint_cursor(
2093 &self,
2094 builder: &mut DisplayListBuilder,
2095 node_index: usize,
2096 ) -> Result<()> {
2097 if self.ctx.cursor_locations.is_empty() {
2105 return Ok(());
2106 }
2107
2108 let node = self
2109 .positioned_tree
2110 .tree
2111 .get(node_index)
2112 .ok_or(LayoutError::InvalidTree)?;
2113 let Some(dom_id) = node.dom_node_id else {
2114 return Ok(());
2115 };
2116
2117 let is_contenteditable = super::getters::is_node_contenteditable_inherited(self.ctx.styled_dom, dom_id);
2119 if !is_contenteditable {
2120 return Ok(());
2121 }
2122
2123 let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2125 let is_selectable = super::getters::is_text_selectable(self.ctx.styled_dom, dom_id, node_state);
2126 if !is_selectable {
2127 return Ok(());
2128 }
2129
2130 let Some(layout) = self.positioned_tree.tree.get_inline_layout_for_node(node_index) else {
2132 return Ok(());
2133 };
2134
2135 let node_pos = self
2137 .positioned_tree
2138 .calculated_positions
2139 .get(node_index)
2140 .copied()
2141 .unwrap_or_default();
2142 let bp = node.box_props.unpack();
2143 let padding = &bp.padding;
2144 let border = &bp.border;
2145 let content_box_offset_x = node_pos.x + padding.left + border.left;
2146 let content_box_offset_y = node_pos.y + padding.top + border.top;
2147
2148 let style = get_caret_style(self.ctx.styled_dom, Some(dom_id));
2149
2150 let primary_idx_for_this_node = self.ctx.cursor_locations.iter().enumerate()
2153 .rev()
2154 .find(|(_, (cd, cn, _))| {
2155 *cd == self.ctx.styled_dom.dom_id && (*cn == dom_id || self.positioned_tree.tree.children(node_index).iter().any(|&child_idx| {
2156 self.positioned_tree.tree.get(child_idx)
2157 .and_then(|c| c.dom_node_id)
2158 .is_some_and(|id| id == *cn)
2159 }))
2160 })
2161 .map(|(i, _)| i);
2162
2163 for (i, (cursor_dom_id, cursor_node_id, cursor)) in self.ctx.cursor_locations.iter().enumerate() {
2164 if self.ctx.styled_dom.dom_id != *cursor_dom_id {
2166 continue;
2167 }
2168
2169 if dom_id != *cursor_node_id {
2171 let is_ifc_root_of_cursor = self.positioned_tree.tree.children(node_index)
2172 .iter()
2173 .any(|&child_idx| {
2174 self.positioned_tree.tree.get(child_idx)
2175 .and_then(|c| c.dom_node_id)
2176 .is_some_and(|id| id == *cursor_node_id)
2177 });
2178 if !is_ifc_root_of_cursor {
2179 continue;
2180 }
2181 }
2182
2183 let Some(mut rect) = layout.get_cursor_rect(cursor) else {
2185 continue;
2186 };
2187
2188 rect.origin.x += content_box_offset_x;
2189 rect.origin.y += content_box_offset_y;
2190 rect.size.width = style.width;
2191
2192 let caret_color = if self.ctx.cursor_is_visible {
2195 style.color
2196 } else {
2197 ColorU { a: 0, ..style.color }
2198 };
2199 builder.push_cursor_rect(rect, caret_color);
2200
2201 let is_primary = primary_idx_for_this_node == Some(i);
2203 if is_primary {
2204 if let Some(ref preedit) = self.ctx.preedit_text {
2205 if !preedit.is_empty() {
2206 let char_count = preedit.chars().count() as f32;
2207 let approx_char_width = style.width.max(8.0);
2208 let preedit_width = char_count * approx_char_width;
2209 let underline_bounds = LogicalRect {
2210 origin: LogicalPosition {
2211 x: rect.origin.x + rect.size.width,
2212 y: rect.origin.y + rect.size.height - 2.0,
2213 },
2214 size: LogicalSize {
2215 width: preedit_width,
2216 height: 2.0,
2217 },
2218 };
2219 builder.push_underline(underline_bounds, style.color, 2.0);
2220 }
2221 }
2222 }
2223 }
2224
2225 Ok(())
2226 }
2227
2228 fn paint_selection_and_cursor(
2231 &self,
2232 builder: &mut DisplayListBuilder,
2233 node_index: usize,
2234 ) -> Result<()> {
2235 self.paint_selections(builder, node_index)?;
2236 self.paint_cursor(builder, node_index)?;
2237 Ok(())
2238 }
2239
2240 fn collect_stacking_contexts(&mut self, node_index: usize) -> Result<StackingContext> {
2243 let node = self
2244 .positioned_tree
2245 .tree
2246 .get(node_index)
2247 .ok_or(LayoutError::InvalidTree)?;
2248 let z_index = get_z_index(self.ctx.styled_dom, node.dom_node_id);
2249
2250 if let Some(dom_id) = node.dom_node_id {
2251 let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
2252 debug_info!(
2253 self.ctx,
2254 "Collecting stacking context for node {} ({:?}), z-index={}",
2255 node_index,
2256 node_type.get_node_type(),
2257 z_index
2258 );
2259 }
2260
2261 let mut child_contexts = Vec::new();
2262 let mut in_flow_children = Vec::new();
2263
2264 for &child_index in self.positioned_tree.tree.children(node_index) {
2265 if self.establishes_stacking_context(child_index) {
2266 child_contexts.push(self.collect_stacking_contexts(child_index)?);
2267 } else {
2268 in_flow_children.push(child_index);
2269 self.find_nested_stacking_contexts(child_index, &mut child_contexts)?;
2273 }
2274 }
2275
2276 Ok(StackingContext {
2277 node_index,
2278 z_index,
2279 child_contexts,
2280 in_flow_children,
2281 })
2282 }
2283
2284 fn find_nested_stacking_contexts(
2287 &mut self,
2288 parent_index: usize,
2289 child_contexts: &mut Vec<StackingContext>,
2290 ) -> Result<()> {
2291 for &child_index in self.positioned_tree.tree.children(parent_index) {
2292 if self.establishes_stacking_context(child_index) {
2293 child_contexts.push(self.collect_stacking_contexts(child_index)?);
2294 } else {
2295 self.find_nested_stacking_contexts(child_index, child_contexts)?;
2296 }
2297 }
2298 Ok(())
2299 }
2300
2301 #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn generate_for_stacking_context(
2316 &mut self,
2317 builder: &mut DisplayListBuilder,
2318 context: &StackingContext,
2319 ) -> Result<()> {
2320 let node = self
2322 .positioned_tree
2323 .tree
2324 .get(context.node_index)
2325 .ok_or(LayoutError::InvalidTree)?;
2326
2327 if let Some(dom_id) = node.dom_node_id {
2328 let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
2329 debug_info!(
2330 self.ctx,
2331 "Painting stacking context for node {} ({:?}), z-index={}, {} child contexts, {} \
2332 in-flow children",
2333 context.node_index,
2334 node_type.get_node_type(),
2335 context.z_index,
2336 context.child_contexts.len(),
2337 context.in_flow_children.len()
2338 );
2339 }
2340
2341 builder.set_current_node(node.dom_node_id);
2345
2346 let is_fixed_position = node.dom_node_id
2348 .is_some_and(|dom_id| get_position_type(self.ctx.styled_dom, Some(dom_id)) == LayoutPosition::Fixed);
2349 if is_fixed_position {
2350 builder.begin_fixed_position_element();
2351 }
2352
2353 let has_reference_frame = node.dom_node_id.and_then(|dom_id| {
2356 self.gpu_value_cache.and_then(|cache| {
2357 let key = cache.css_transform_keys.get(&dom_id)?;
2358 let transform = cache.css_current_transform_values.get(&dom_id)?;
2359 Some((*key, *transform))
2360 })
2361 });
2362
2363 let node_pos = self
2366 .positioned_tree
2367 .calculated_positions
2368 .get(context.node_index)
2369 .copied()
2370 .unwrap_or_default();
2371 let node_size = node.used_size.unwrap_or(LogicalSize {
2372 width: 0.0,
2373 height: 0.0,
2374 });
2375 let node_bounds = LogicalRect {
2376 origin: node_pos,
2377 size: node_size,
2378 };
2379
2380 if let Some((transform_key, initial_transform)) = has_reference_frame {
2382 builder.push_reference_frame(transform_key, initial_transform, node_bounds);
2383 }
2384
2385 builder.push_stacking_context(context.z_index, node_bounds);
2386
2387 let mut pushed_opacity = false;
2389 let mut pushed_filter = false;
2390 let mut pushed_backdrop_filter = false;
2391
2392 if let Some(dom_id) = node.dom_node_id {
2393 let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
2394 let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2395
2396 let opacity = crate::solver3::getters::get_opacity(
2398 self.ctx.styled_dom, dom_id, node_state,
2399 );
2400
2401 if opacity < 1.0 {
2402 builder.push_item(DisplayListItem::PushOpacity {
2403 bounds: node_bounds.into(),
2404 opacity,
2405 });
2406 pushed_opacity = true;
2407 }
2408
2409 if let Some(filter_vec_value) = self.ctx.styled_dom.css_property_cache.ptr
2411 .get_filter(node_data, &dom_id, node_state)
2412 {
2413 if let Some(filter_vec) = filter_vec_value.get_property() {
2414 let filters: Vec<_> = filter_vec.as_ref().to_vec();
2415 if !filters.is_empty() {
2416 builder.push_item(DisplayListItem::PushFilter {
2417 bounds: node_bounds.into(),
2418 filters,
2419 });
2420 pushed_filter = true;
2421 }
2422 }
2423 }
2424
2425 if let Some(backdrop_filter_value) = self.ctx.styled_dom.css_property_cache.ptr
2427 .get_backdrop_filter(node_data, &dom_id, node_state)
2428 {
2429 if let Some(filter_vec) = backdrop_filter_value.get_property() {
2430 let filters: Vec<_> = filter_vec.as_ref().to_vec();
2431 if !filters.is_empty() {
2432 builder.push_item(DisplayListItem::PushBackdropFilter {
2433 bounds: node_bounds.into(),
2434 filters,
2435 });
2436 pushed_backdrop_filter = true;
2437 }
2438 }
2439 }
2440 }
2441
2442 let did_push_image_mask = self.push_image_mask_clip(builder, context.node_index);
2445
2446 self.paint_node_background_and_border(builder, context.node_index)?;
2452
2453 if !self.is_node_hidden(context.node_index) {
2460 if let Some(dom_id) = node.dom_node_id {
2461 let styled_node_state = self.get_styled_node_state(dom_id);
2462 let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
2463 let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
2464 if overflow_x.is_scroll() || overflow_y.is_scroll() {
2465 if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, node.dom_node_id) {
2466 builder.push_hit_test_area(node_bounds, tag_id);
2467 }
2468 }
2469 }
2470 }
2471
2472 let did_push_clip_or_scroll = self.push_node_clips(builder, context.node_index, node);
2479
2480 let mut negative_z_children: Vec<_> = context
2483 .child_contexts
2484 .iter()
2485 .filter(|c| c.z_index < 0)
2486 .collect();
2487 negative_z_children.sort_by_key(|c| c.z_index);
2488 for child in negative_z_children {
2489 self.generate_for_stacking_context(builder, child)?;
2490 }
2491
2492 self.paint_in_flow_descendants(builder, context.node_index, &context.in_flow_children)?;
2494
2495 for child in context.child_contexts.iter().filter(|c| c.z_index == 0) {
2498 self.generate_for_stacking_context(builder, child)?;
2499 }
2500
2501 let mut positive_z_children: Vec<_> = context
2504 .child_contexts
2505 .iter()
2506 .filter(|c| c.z_index > 0)
2507 .collect();
2508
2509 positive_z_children.sort_by_key(|c| c.z_index);
2510
2511 for child in positive_z_children {
2512 self.generate_for_stacking_context(builder, child)?;
2513 }
2514
2515 if did_push_image_mask {
2517 builder.pop_image_mask_clip();
2518 }
2519
2520 if pushed_backdrop_filter {
2522 builder.push_item(DisplayListItem::PopBackdropFilter);
2523 }
2524 if pushed_filter {
2525 builder.push_item(DisplayListItem::PopFilter);
2526 }
2527 if pushed_opacity {
2528 builder.push_item(DisplayListItem::PopOpacity);
2529 }
2530
2531 builder.pop_stacking_context();
2533
2534 if has_reference_frame.is_some() {
2536 builder.pop_reference_frame();
2537 }
2538
2539 if is_fixed_position {
2541 builder.end_fixed_position_element();
2542 }
2543
2544 if did_push_clip_or_scroll {
2548 if let Some(dom_id) = node.dom_node_id {
2550 if self.is_virtual_view_node(dom_id) {
2551 builder.push_virtual_view_placeholder(dom_id, node_bounds, node_bounds);
2552 }
2553 }
2554 self.pop_node_clips(builder, node);
2555 } else {
2556 if let Some(dom_id) = node.dom_node_id {
2558 if self.is_virtual_view_node(dom_id) {
2559 builder.push_virtual_view_placeholder(dom_id, node_bounds, node_bounds);
2560 }
2561 }
2562 }
2563
2564 self.paint_scrollbars(builder, context.node_index)?;
2567
2568 Ok(())
2569 }
2570
2571 #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn paint_in_flow_descendants(
2574 &mut self,
2575 builder: &mut DisplayListBuilder,
2576 node_index: usize,
2577 children_indices: &[usize],
2578 ) -> Result<()> {
2579 self.paint_selection_and_cursor(builder, node_index)?;
2585
2586 self.paint_node_content(builder, node_index)?;
2588
2589 let mut non_float_children = Vec::new();
2600 let mut float_children = Vec::new();
2601 let mut dragging_children = Vec::new();
2602
2603 for &child_index in children_indices {
2604 if self.establishes_stacking_context(child_index) {
2607 continue;
2608 }
2609 let child_node = self
2610 .positioned_tree
2611 .tree
2612 .get(child_index)
2613 .ok_or(LayoutError::InvalidTree)?;
2614
2615 let is_dragging = child_node.dom_node_id.is_some_and(|dom_id| {
2617 let styled_node_state = self.get_styled_node_state(dom_id);
2618 styled_node_state.dragging
2619 });
2620
2621 if is_dragging {
2622 dragging_children.push(child_index);
2623 continue;
2624 }
2625
2626 let is_float = if let Some(dom_id) = child_node.dom_node_id {
2628 use crate::solver3::getters::get_float;
2629 let styled_node_state = self.get_styled_node_state(dom_id);
2630 let float_value = get_float(self.ctx.styled_dom, dom_id, &styled_node_state);
2631 !matches!(
2632 float_value.unwrap_or_default(),
2633 azul_css::props::layout::LayoutFloat::None
2634 )
2635 } else {
2636 false
2637 };
2638
2639 if is_float {
2640 float_children.push(child_index);
2641 } else {
2642 non_float_children.push(child_index);
2643 }
2644 }
2645
2646 for child_index in non_float_children {
2648 let child_node = self
2649 .positioned_tree
2650 .tree
2651 .get(child_index)
2652 .ok_or(LayoutError::InvalidTree)?;
2653
2654 let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
2656 self.gpu_value_cache.and_then(|cache| {
2657 let key = cache.css_transform_keys.get(&dom_id)?;
2658 let transform = cache.css_current_transform_values.get(&dom_id)?;
2659 Some((*key, *transform))
2660 })
2661 });
2662
2663 if let Some((transform_key, initial_transform)) = child_ref_frame {
2665 let child_pos = self
2666 .positioned_tree
2667 .calculated_positions
2668 .get(child_index)
2669 .copied()
2670 .unwrap_or_default();
2671 let child_size = child_node.used_size.unwrap_or(LogicalSize {
2672 width: 0.0,
2673 height: 0.0,
2674 });
2675 let child_bounds = LogicalRect {
2676 origin: child_pos,
2677 size: child_size,
2678 };
2679 builder.set_current_node(child_node.dom_node_id);
2680 builder.push_reference_frame(transform_key, initial_transform, child_bounds);
2681 }
2682
2683 let did_push_child_image_mask = self.push_image_mask_clip(builder, child_index);
2685
2686 self.paint_node_background_and_border(builder, child_index)?;
2690
2691 let did_push_clip = self.push_node_clips(builder, child_index, child_node);
2693
2694 self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
2696
2697 if let Some(dom_id) = child_node.dom_node_id {
2699 if self.is_virtual_view_node(dom_id) {
2700 let child_bounds = self.get_paint_rect(child_index).unwrap_or_default();
2701 builder.push_virtual_view_placeholder(dom_id, child_bounds, child_bounds);
2702 }
2703 }
2704
2705 if did_push_clip {
2707 self.pop_node_clips(builder, child_node);
2708 }
2709
2710 if did_push_child_image_mask {
2712 builder.pop_image_mask_clip();
2713 }
2714
2715 self.paint_scrollbars(builder, child_index)?;
2717
2718 if child_ref_frame.is_some() {
2720 builder.pop_reference_frame();
2721 }
2722 }
2723
2724 for child_index in float_children {
2727 let child_node = self
2728 .positioned_tree
2729 .tree
2730 .get(child_index)
2731 .ok_or(LayoutError::InvalidTree)?;
2732
2733 let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
2735 self.gpu_value_cache.and_then(|cache| {
2736 let key = cache.css_transform_keys.get(&dom_id)?;
2737 let transform = cache.css_current_transform_values.get(&dom_id)?;
2738 Some((*key, *transform))
2739 })
2740 });
2741
2742 if let Some((transform_key, initial_transform)) = child_ref_frame {
2744 let child_pos = self
2745 .positioned_tree
2746 .calculated_positions
2747 .get(child_index)
2748 .copied()
2749 .unwrap_or_default();
2750 let child_size = child_node.used_size.unwrap_or(LogicalSize {
2751 width: 0.0,
2752 height: 0.0,
2753 });
2754 let child_bounds = LogicalRect {
2755 origin: child_pos,
2756 size: child_size,
2757 };
2758 builder.set_current_node(child_node.dom_node_id);
2759 builder.push_reference_frame(transform_key, initial_transform, child_bounds);
2760 }
2761
2762 let did_push_child_image_mask = self.push_image_mask_clip(builder, child_index);
2764 self.paint_node_background_and_border(builder, child_index)?;
2765 let did_push_clip = self.push_node_clips(builder, child_index, child_node);
2766 self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
2767
2768 if let Some(dom_id) = child_node.dom_node_id {
2770 if self.is_virtual_view_node(dom_id) {
2771 let child_bounds = self.get_paint_rect(child_index).unwrap_or_default();
2772 builder.push_virtual_view_placeholder(dom_id, child_bounds, child_bounds);
2773 }
2774 }
2775
2776 if did_push_clip {
2777 self.pop_node_clips(builder, child_node);
2778 }
2779 if did_push_child_image_mask {
2780 builder.pop_image_mask_clip();
2781 }
2782
2783 self.paint_scrollbars(builder, child_index)?;
2785
2786 if child_ref_frame.is_some() {
2788 builder.pop_reference_frame();
2789 }
2790 }
2791
2792 for child_index in dragging_children {
2794 let child_node = self
2795 .positioned_tree
2796 .tree
2797 .get(child_index)
2798 .ok_or(LayoutError::InvalidTree)?;
2799
2800 let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
2802 self.gpu_value_cache.and_then(|cache| {
2803 let key = cache.css_transform_keys.get(&dom_id)?;
2804 let transform = cache.css_current_transform_values.get(&dom_id)?;
2805 Some((*key, *transform))
2806 })
2807 });
2808
2809 if let Some((transform_key, initial_transform)) = child_ref_frame {
2811 let child_pos = self
2812 .positioned_tree
2813 .calculated_positions
2814 .get(child_index)
2815 .copied()
2816 .unwrap_or_default();
2817 let child_size = child_node.used_size.unwrap_or(LogicalSize {
2818 width: 0.0,
2819 height: 0.0,
2820 });
2821 let child_bounds = LogicalRect {
2822 origin: child_pos,
2823 size: child_size,
2824 };
2825 builder.set_current_node(child_node.dom_node_id);
2826 builder.push_reference_frame(transform_key, initial_transform, child_bounds);
2827 }
2828
2829 let did_push_child_image_mask = self.push_image_mask_clip(builder, child_index);
2831 self.paint_node_background_and_border(builder, child_index)?;
2832 let did_push_clip = self.push_node_clips(builder, child_index, child_node);
2833 self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
2834
2835 if let Some(dom_id) = child_node.dom_node_id {
2837 if self.is_virtual_view_node(dom_id) {
2838 let child_bounds = self.get_paint_rect(child_index).unwrap_or_default();
2839 builder.push_virtual_view_placeholder(dom_id, child_bounds, child_bounds);
2840 }
2841 }
2842
2843 if did_push_clip {
2844 self.pop_node_clips(builder, child_node);
2845 }
2846 if did_push_child_image_mask {
2847 builder.pop_image_mask_clip();
2848 }
2849
2850 self.paint_scrollbars(builder, child_index)?;
2852
2853 if child_ref_frame.is_some() {
2855 builder.pop_reference_frame();
2856 }
2857 }
2858
2859 Ok(())
2860 }
2861
2862 fn is_virtual_view_node(&self, dom_id: NodeId) -> bool {
2864 let node_data_container = self.ctx.styled_dom.node_data.as_container();
2865 node_data_container
2866 .get(dom_id)
2867 .is_some_and(|nd| matches!(nd.get_node_type(), NodeType::VirtualView))
2868 }
2869
2870 fn push_image_mask_clip(
2873 &self,
2874 builder: &mut DisplayListBuilder,
2875 node_index: usize,
2876 ) -> bool {
2877 let Some(node) = self.positioned_tree.tree.get(node_index) else {
2878 return false;
2879 };
2880 let Some(dom_id) = node.dom_node_id else {
2881 return false;
2882 };
2883 let node_data_container = self.ctx.styled_dom.node_data.as_container();
2884 let Some(node_data) = node_data_container.get(dom_id) else {
2885 return false;
2886 };
2887 match node_data.get_svg_data() {
2888 Some(azul_core::dom::SvgNodeData::ImageClipMask(clip_mask)) => {
2889 let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
2890 let mask_rect = LogicalRect {
2892 origin: LogicalPosition {
2893 x: paint_rect.origin.x + clip_mask.rect.origin.x,
2894 y: paint_rect.origin.y + clip_mask.rect.origin.y,
2895 },
2896 size: clip_mask.rect.size,
2897 };
2898 builder.push_image_mask_clip(
2899 paint_rect,
2900 clip_mask.image.clone(),
2901 mask_rect,
2902 );
2903 true
2904 }
2905 #[cfg(feature = "cpurender")]
2906 Some(azul_core::dom::SvgNodeData::Path(svg_clip)) => {
2907 let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
2908 rasterize_svg_clip_to_r8(svg_clip, &paint_rect).is_some_and(|mask_image| {
2909 builder.push_image_mask_clip(paint_rect, mask_image, paint_rect);
2910 true
2911 })
2912 }
2913 #[cfg(not(feature = "cpurender"))]
2914 Some(azul_core::dom::SvgNodeData::Path(_)) => false,
2915 Some(_) => false,
2917 None => false,
2918 }
2919 }
2920
2921 #[allow(clippy::similar_names)] fn push_node_clips(
2937 &self,
2938 builder: &mut DisplayListBuilder,
2939 node_index: usize,
2940 node: &LayoutNodeHot,
2941 ) -> bool {
2942 let Some(dom_id) = node.dom_node_id else {
2943 return false;
2944 };
2945
2946 let styled_node_state = self.get_styled_node_state(dom_id);
2947
2948 let raw_overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
2949 let raw_overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
2950 let overflow_x = raw_overflow_x.resolve_computed(&raw_overflow_y);
2952 let overflow_y = raw_overflow_y.resolve_computed(&raw_overflow_x);
2953
2954 let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
2955 let element_size = PhysicalSizeImport {
2956 width: paint_rect.size.width,
2957 height: paint_rect.size.height,
2958 };
2959 let border_radius = get_border_radius(
2960 self.ctx.styled_dom,
2961 dom_id,
2962 &styled_node_state,
2963 element_size,
2964 self.ctx.viewport_size,
2965 );
2966
2967 let has_clip_path = super::getters::get_clip_path(
2973 self.ctx.styled_dom, dom_id, &styled_node_state,
2974 ).is_some_and(|clip_path| if let Some((clip_rect, radius)) = resolve_clip_path(&clip_path, paint_rect) {
2975 let br = if radius > 0.0 {
2976 BorderRadius {
2977 top_left: radius,
2978 top_right: radius,
2979 bottom_left: radius,
2980 bottom_right: radius,
2981 }
2982 } else {
2983 BorderRadius::default()
2984 };
2985 builder.push_clip(clip_rect, br);
2986 true
2987 } else {
2988 false
2989 });
2990
2991 let needs_clip = overflow_x.is_clipped() || overflow_y.is_clipped();
2994
2995 if !needs_clip {
2996 return has_clip_path;
2997 }
2998
2999 let ox_clip = overflow_x.is_clipped() && !overflow_x.is_scroll() && !overflow_x.is_auto_overflow();
3004 let oy_clip = overflow_y.is_clipped() && !overflow_y.is_scroll() && !overflow_y.is_auto_overflow();
3005 let ox_visible = !overflow_x.is_clipped();
3006 let oy_visible = !overflow_y.is_clipped();
3007 let border_radius = if (ox_clip && oy_visible) || (oy_clip && ox_visible)
3008 {
3009 BorderRadius::default()
3010 } else {
3011 border_radius
3012 };
3013
3014 let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
3015
3016 let bp = node.box_props.unpack();
3017 let border = &bp.border;
3018
3019 let scrollbar_info = self.positioned_tree.tree.warm(node_index)
3021 .and_then(|w| w.scrollbar_info)
3022 .unwrap_or_default();
3023
3024 let mut clip_rect = LogicalRect {
3032 origin: LogicalPosition {
3033 x: paint_rect.origin.x + border.left,
3034 y: paint_rect.origin.y + border.top,
3035 },
3036 size: LogicalSize {
3037 width: (paint_rect.size.width
3039 - border.left
3040 - border.right
3041 - scrollbar_info.scrollbar_width)
3042 .max(0.0),
3043 height: (paint_rect.size.height
3044 - border.top
3045 - border.bottom
3046 - scrollbar_info.scrollbar_height)
3047 .max(0.0),
3048 },
3049 };
3050
3051 apply_overflow_clip_margin(
3055 &mut clip_rect,
3056 &overflow_x,
3057 &overflow_y,
3058 self.ctx.styled_dom,
3059 dom_id,
3060 &styled_node_state,
3061 );
3062
3063 let is_virtual_view = self.is_virtual_view_node(dom_id);
3064
3065 builder.push_clip(clip_rect, border_radius);
3069 if (overflow_x.is_scroll() || overflow_y.is_scroll()) && !is_virtual_view {
3076 let scroll_id = self.scroll_ids.get(&node_index).copied().unwrap_or(0);
3077 let content_size = get_scroll_content_size(node, self.positioned_tree.tree.warm(node_index));
3078 builder.push_scroll_frame(clip_rect, content_size, scroll_id);
3079 }
3080
3081 true
3082 }
3083
3084 fn pop_node_clips(&self, builder: &mut DisplayListBuilder, node: &LayoutNodeHot) {
3086 let Some(dom_id) = node.dom_node_id else {
3087 return;
3088 };
3089
3090 let styled_node_state = self.get_styled_node_state(dom_id);
3091 let raw_overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
3104 let raw_overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
3105 let overflow_x = raw_overflow_x.resolve_computed(&raw_overflow_y);
3106 let overflow_y = raw_overflow_y.resolve_computed(&raw_overflow_x);
3107
3108 let paint_rect = self
3109 .get_paint_rect(
3110 self.positioned_tree
3111 .tree
3112 .nodes
3113 .iter()
3114 .position(|n| n.dom_node_id == Some(dom_id))
3115 .unwrap_or(0),
3116 )
3117 .unwrap_or_default();
3118
3119 let element_size = PhysicalSizeImport {
3120 width: paint_rect.size.width,
3121 height: paint_rect.size.height,
3122 };
3123 let border_radius = get_border_radius(
3124 self.ctx.styled_dom,
3125 dom_id,
3126 &styled_node_state,
3127 element_size,
3128 self.ctx.viewport_size,
3129 );
3130
3131 let needs_clip =
3132 overflow_x.is_clipped() || overflow_y.is_clipped();
3133
3134 let is_virtual_view = self.is_virtual_view_node(dom_id);
3135
3136 if needs_clip {
3137 if (overflow_x.is_scroll() || overflow_y.is_scroll()) && !is_virtual_view {
3141 builder.pop_scroll_frame();
3142 }
3143 builder.pop_clip();
3144 }
3145
3146 if let Some(clip_path) = super::getters::get_clip_path(
3151 self.ctx.styled_dom, dom_id, &styled_node_state,
3152 ) {
3153 if resolve_clip_path(&clip_path, paint_rect).is_some() {
3154 builder.pop_clip();
3155 }
3156 }
3157
3158 }
3159
3160 fn get_paint_rect(&self, node_index: usize) -> Option<LogicalRect> {
3178 let node = self.positioned_tree.tree.get(node_index)?;
3179 let pos = self
3180 .positioned_tree
3181 .calculated_positions
3182 .get(node_index)
3183 .copied()
3184 .unwrap_or_default();
3185 let size = node.used_size.unwrap_or_default();
3186
3187 Some(LogicalRect::new(pos, size))
3192 }
3193
3194 #[allow(clippy::too_many_lines)] fn paint_node_background_and_border(
3197 &mut self,
3198 builder: &mut DisplayListBuilder,
3199 node_index: usize,
3200 ) -> Result<()> {
3201 let Some(paint_rect) = self.get_paint_rect(node_index) else {
3202 return Ok(());
3203 };
3204 let node = self
3205 .positioned_tree
3206 .tree
3207 .get(node_index)
3208 .ok_or(LayoutError::InvalidTree)?;
3209
3210 builder.set_current_node(node.dom_node_id);
3212
3213 if let Some(dom_id) = node.dom_node_id {
3216 let break_before = get_break_before(self.ctx.styled_dom, Some(dom_id));
3217 let break_after = get_break_after(self.ctx.styled_dom, Some(dom_id));
3218
3219 if is_forced_page_break(break_before) {
3221 let y_position = paint_rect.origin.y;
3222 builder.add_forced_page_break(y_position);
3223 debug_info!(
3224 self.ctx,
3225 "Registered forced page break BEFORE node {} at y={}",
3226 node_index,
3227 y_position
3228 );
3229 }
3230
3231 if is_forced_page_break(break_after) {
3233 let y_position = paint_rect.origin.y + paint_rect.size.height;
3234 builder.add_forced_page_break(y_position);
3235 debug_info!(
3236 self.ctx,
3237 "Registered forced page break AFTER node {} at y={}",
3238 node_index,
3239 y_position
3240 );
3241 }
3242 }
3243
3244 if self.is_node_hidden(node_index) {
3248 return Ok(());
3249 }
3250
3251 let warm = self.positioned_tree.tree.warm(node_index);
3260 let parent_is_flex_or_grid = warm
3261 .and_then(|w| w.parent_formatting_context.as_ref().map(|fc| matches!(fc, FormattingContext::Flex | FormattingContext::Grid)))
3262 .unwrap_or(false);
3263
3264 if let Some(dom_id) = node.dom_node_id {
3265 let display = {
3266 use crate::solver3::getters::get_display_property;
3267 get_display_property(self.ctx.styled_dom, Some(dom_id))
3268 .unwrap_or(LayoutDisplay::Inline)
3269 };
3270
3271 if display == LayoutDisplay::InlineBlock || display == LayoutDisplay::Inline {
3272 debug_info!(
3273 self.ctx,
3274 "[paint_node] node {} has display={:?}, parent_formatting_context={:?}, parent_is_flex_or_grid={}",
3275 node_index,
3276 display,
3277 warm.and_then(|w| w.parent_formatting_context.as_ref()),
3278 parent_is_flex_or_grid
3279 );
3280
3281 if !parent_is_flex_or_grid {
3282 if display == LayoutDisplay::InlineBlock
3291 && self.establishes_stacking_context(node_index)
3292 {
3293 } else {
3295 return Ok(());
3296 }
3297 }
3298 }
3300 }
3301
3302 if matches!(node.formatting_context,
3311 FormattingContext::TableRowGroup | FormattingContext::TableRow |
3312 FormattingContext::TableColumnGroup
3313 ) {
3314 return Ok(());
3315 }
3316
3317 if matches!(node.formatting_context, FormattingContext::Table) {
3319 debug_info!(
3320 self.ctx,
3321 "Painting table backgrounds/borders for node {} at {:?}",
3322 node_index,
3323 paint_rect
3324 );
3325 return self.paint_table_items(builder, node_index);
3327 }
3328
3329 if let Some(dom_id) = node.dom_node_id {
3330 let styled_node_state = self.get_styled_node_state(dom_id);
3331 let background_contents =
3332 get_background_contents(self.ctx.styled_dom, dom_id, &styled_node_state);
3333 let border_info = get_border_info(self.ctx.styled_dom, dom_id, &styled_node_state);
3334
3335 let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
3336 debug_info!(
3337 self.ctx,
3338 "Painting background/border for node {} ({:?}) at {:?}, backgrounds={:?}",
3339 node_index,
3340 node_type.get_node_type(),
3341 paint_rect,
3342 background_contents.len()
3343 );
3344
3345 let element_size = PhysicalSizeImport {
3348 width: paint_rect.size.width,
3349 height: paint_rect.size.height,
3350 };
3351 let simple_border_radius = get_border_radius(
3352 self.ctx.styled_dom,
3353 dom_id,
3354 &styled_node_state,
3355 element_size,
3356 self.ctx.viewport_size,
3357 );
3358 let style_border_radius =
3359 get_style_border_radius(self.ctx.styled_dom, dom_id, &styled_node_state);
3360
3361 let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
3363
3364 for shadow in [
3369 super::getters::get_box_shadow_left(self.ctx.styled_dom, dom_id, node_state),
3370 super::getters::get_box_shadow_right(self.ctx.styled_dom, dom_id, node_state),
3371 super::getters::get_box_shadow_top(self.ctx.styled_dom, dom_id, node_state),
3372 super::getters::get_box_shadow_bottom(self.ctx.styled_dom, dom_id, node_state),
3373 ].into_iter().flatten() {
3374 builder.push_item(DisplayListItem::BoxShadow {
3375 bounds: paint_rect.into(),
3376 shadow,
3377 border_radius: simple_border_radius,
3378 });
3379 }
3380
3381 builder.push_backgrounds_and_border(
3383 paint_rect,
3384 &background_contents,
3385 &border_info,
3386 simple_border_radius,
3387 style_border_radius,
3388 self.ctx.image_cache,
3389 );
3390
3391 }
3392
3393 Ok(())
3394 }
3395
3396 fn paint_table_items(
3419 &self,
3420 builder: &mut DisplayListBuilder,
3421 table_index: usize,
3422 ) -> Result<()> {
3423 let table_node = self
3424 .positioned_tree
3425 .tree
3426 .get(table_index)
3427 .ok_or(LayoutError::InvalidTree)?;
3428
3429 let Some(table_paint_rect) = self.get_paint_rect(table_index) else {
3430 return Ok(());
3431 };
3432
3433 if let Some(dom_id) = table_node.dom_node_id {
3435 let styled_node_state = self.get_styled_node_state(dom_id);
3436 let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
3437 let element_size = PhysicalSizeImport {
3438 width: table_paint_rect.size.width,
3439 height: table_paint_rect.size.height,
3440 };
3441 let border_radius = get_border_radius(
3442 self.ctx.styled_dom,
3443 dom_id,
3444 &styled_node_state,
3445 element_size,
3446 self.ctx.viewport_size,
3447 );
3448
3449 builder.push_rect(table_paint_rect, bg_color, border_radius);
3450 }
3451
3452 for &child_idx in self.positioned_tree.tree.children(table_index) {
3457 let child_node = self.positioned_tree.tree.get(child_idx);
3458 if let Some(node) = child_node {
3459 if matches!(node.formatting_context, FormattingContext::TableColumnGroup) {
3460 self.paint_element_background(builder, child_idx);
3462
3463 for &col_idx in self.positioned_tree.tree.children(child_idx) {
3465 self.paint_element_background(builder, col_idx);
3466 }
3467 }
3468 }
3469 }
3470
3471 for &child_idx in self.positioned_tree.tree.children(table_index) {
3475 let child_node = self.positioned_tree.tree.get(child_idx);
3476 if let Some(node) = child_node {
3477 match node.formatting_context {
3478 FormattingContext::TableRowGroup => {
3479 self.paint_element_background(builder, child_idx);
3481
3482 for &row_idx in self.positioned_tree.tree.children(child_idx) {
3484 self.paint_table_row_and_cells(builder, row_idx);
3485 }
3486 }
3487 FormattingContext::TableRow => {
3488 self.paint_table_row_and_cells(builder, child_idx);
3490 }
3491 _ => {}
3492 }
3493 }
3494 }
3495
3496 Ok(())
3503 }
3504
3505 fn paint_table_row_and_cells(
3509 &self,
3510 builder: &mut DisplayListBuilder,
3511 row_idx: usize,
3512 ) {
3513 if let Some(row_node) = self.positioned_tree.tree.get(row_idx) {
3518 if let Some(dom_id) = row_node.dom_node_id {
3519 let styled_node_state = self.get_styled_node_state(dom_id);
3520 let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
3521 if bg_color.a > 0 {
3522 let mut min_x = f32::MAX;
3524 let mut min_y = f32::MAX;
3525 let mut max_x = f32::MIN;
3526 let mut max_y = f32::MIN;
3527 for &cell_idx in self.positioned_tree.tree.children(row_idx) {
3528 if let Some(cell_rect) = self.get_paint_rect(cell_idx) {
3529 min_x = min_x.min(cell_rect.origin.x);
3530 min_y = min_y.min(cell_rect.origin.y);
3531 max_x = max_x.max(cell_rect.origin.x + cell_rect.size.width);
3532 max_y = max_y.max(cell_rect.origin.y + cell_rect.size.height);
3533 }
3534 }
3535 if min_x < max_x && min_y < max_y {
3536 let row_rect = LogicalRect::new(
3537 LogicalPosition::new(min_x, min_y),
3538 LogicalSize::new(max_x - min_x, max_y - min_y),
3539 );
3540 builder.push_rect(row_rect, bg_color, BorderRadius::default());
3541 }
3542 }
3543 }
3544 }
3545
3546 if let Some(_node) = self.positioned_tree.tree.get(row_idx) {
3548 for &cell_idx in self.positioned_tree.tree.children(row_idx) {
3549 self.paint_element_background(builder, cell_idx);
3550 }
3551 }
3552
3553 }
3554
3555 fn paint_element_background(
3558 &self,
3559 builder: &mut DisplayListBuilder,
3560 node_index: usize,
3561 ) {
3562 let Some(paint_rect) = self.get_paint_rect(node_index) else {
3563 return;
3564 };
3565
3566 let Some(node) = self.positioned_tree.tree.get(node_index) else {
3567 return;
3568 };
3569 let Some(dom_id) = node.dom_node_id else {
3570 return;
3571 };
3572
3573 let styled_node_state = self.get_styled_node_state(dom_id);
3574 let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
3575
3576 if bg_color.a == 0 {
3578 return;
3579 }
3580
3581 let element_size = PhysicalSizeImport {
3582 width: paint_rect.size.width,
3583 height: paint_rect.size.height,
3584 };
3585 let border_radius = get_border_radius(
3586 self.ctx.styled_dom,
3587 dom_id,
3588 &styled_node_state,
3589 element_size,
3590 self.ctx.viewport_size,
3591 );
3592
3593 builder.push_rect(paint_rect, bg_color, border_radius);
3594
3595 }
3596
3597 #[allow(clippy::too_many_lines)] fn paint_node_content(
3600 &mut self,
3601 builder: &mut DisplayListBuilder,
3602 node_index: usize,
3603 ) -> Result<()> {
3604 if self.is_node_hidden(node_index) {
3606 return Ok(());
3607 }
3608
3609 let node = self
3610 .positioned_tree
3611 .tree
3612 .get(node_index)
3613 .ok_or(LayoutError::InvalidTree)?;
3614 let node_warm = self.positioned_tree.tree.warm(node_index);
3615
3616 builder.set_current_node(node.dom_node_id);
3618
3619 let Some(mut paint_rect) = self.get_paint_rect(node_index) else {
3620 return Ok(());
3621 };
3622
3623 if paint_rect.size.width == 0.0 || paint_rect.size.height == 0.0 {
3626 if let Some(cached_layout) = node_warm.and_then(|w| w.inline_layout_result.as_ref()) {
3627 let content_bounds = cached_layout.layout.bounds();
3628 paint_rect.size.width = content_bounds.width;
3629 paint_rect.size.height = content_bounds.height;
3630 }
3631 }
3632
3633 if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, node.dom_node_id) {
3638 let is_scrollable = if let Some(dom_id) = node.dom_node_id {
3639 let styled_node_state = self.get_styled_node_state(dom_id);
3640 let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
3641 let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
3642 overflow_x.is_scroll() || overflow_y.is_scroll()
3643 } else {
3644 false
3645 };
3646
3647 if !is_scrollable {
3653 builder.push_hit_test_area(paint_rect, tag_id);
3654 }
3655 }
3656
3657 if let Some(cached_layout) = node_warm.and_then(|w| w.inline_layout_result.as_ref()) {
3659 let inline_layout = &cached_layout.layout;
3660 debug_info!(
3661 self.ctx,
3662 "[paint_node] node {} has inline_layout with {} items",
3663 node_index,
3664 inline_layout.items.len()
3665 );
3666
3667 if let Some(dom_id) = node.dom_node_id {
3668 let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
3669 debug_info!(
3670 self.ctx,
3671 "Painting inline content for node {} ({:?}) at {:?}, {} layout items",
3672 node_index,
3673 node_type.get_node_type(),
3674 paint_rect,
3675 inline_layout.items.len()
3676 );
3677 }
3678
3679 let border_box = BorderBoxRect(paint_rect);
3683 let nbp = node.box_props.unpack();
3684 let mut content_box_rect =
3685 border_box.to_content_box(&nbp.padding, &nbp.border).rect();
3686
3687 let viewport_clip_rect = content_box_rect;
3691
3692 let content_size = get_scroll_content_size(node, node_warm);
3696 if content_size.height > content_box_rect.size.height {
3697 content_box_rect.size.height = content_size.height;
3698 }
3699 if content_size.width > content_box_rect.size.width {
3700 content_box_rect.size.width = content_size.width;
3701 }
3702
3703 let mut pushed_text_shadow = false;
3705 if let Some(dom_id) = node.dom_node_id {
3706 let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
3707 let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
3708 if let Some(shadow_val) = self.ctx.styled_dom.css_property_cache.ptr
3709 .get_text_shadow(node_data, &dom_id, node_state)
3710 {
3711 if let Some(shadow) = shadow_val.get_property() {
3712 builder.push_item(DisplayListItem::PushTextShadow {
3713 shadow: (**shadow),
3714 });
3715 pushed_text_shadow = true;
3716 }
3717 }
3718 }
3719
3720 self.paint_inline_content(builder, content_box_rect, viewport_clip_rect, inline_layout, node_index);
3721
3722 if pushed_text_shadow {
3723 builder.push_item(DisplayListItem::PopTextShadow);
3724 }
3725 } else if let Some(dom_id) = node.dom_node_id {
3726 let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
3730 if let NodeType::Image(image_ref) = node_data.get_node_type() {
3731 debug_info!(
3732 self.ctx,
3733 "Painting image for node {} at {:?}",
3734 node_index,
3735 paint_rect
3736 );
3737 let styled_node_state = self.get_styled_node_state(dom_id);
3739 let element_size = PhysicalSizeImport {
3740 width: paint_rect.size.width,
3741 height: paint_rect.size.height,
3742 };
3743 let border_radius = get_border_radius(
3744 self.ctx.styled_dom,
3745 dom_id,
3746 &styled_node_state,
3747 element_size,
3748 self.ctx.viewport_size,
3749 );
3750 builder.push_image(paint_rect, image_ref.as_ref().clone(), border_radius);
3752 }
3753 }
3754
3755 Ok(())
3756 }
3757
3758 #[allow(clippy::too_many_lines)] fn paint_scrollbars(&self, builder: &mut DisplayListBuilder, node_index: usize) -> Result<()> {
3762 if self.is_node_hidden(node_index) {
3765 return Ok(());
3766 }
3767
3768 let node = self
3769 .positioned_tree
3770 .tree
3771 .get(node_index)
3772 .ok_or(LayoutError::InvalidTree)?;
3773
3774 let Some(paint_rect) = self.get_paint_rect(node_index) else {
3775 return Ok(());
3776 };
3777
3778 let scrollbar_info = self.positioned_tree.tree.warm(node_index)
3780 .and_then(|w| w.scrollbar_info)
3781 .unwrap_or_default();
3782
3783 let node_id = node.dom_node_id;
3785
3786 let scrollbar_style = node_id
3788 .map(|nid| {
3789 let node_state =
3790 &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
3791 crate::solver3::getters::get_scrollbar_style_cached(self.ctx, nid, node_state)
3792 })
3793 .unwrap_or_default();
3794
3795 if matches!(
3797 scrollbar_style.width_mode,
3798 azul_css::props::style::scrollbar::LayoutScrollbarWidth::None
3799 ) {
3800 return Ok(());
3801 }
3802
3803 let scrollbar_gutter = node_id
3806 .and_then(|nid| {
3807 let node_state =
3808 &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
3809 get_scrollbar_gutter_property(self.ctx.styled_dom, nid, node_state).exact()
3810 })
3811 .unwrap_or_default();
3812 let gutter_is_stable = matches!(
3813 scrollbar_gutter,
3814 azul_css::props::layout::overflow::StyleScrollbarGutter::Stable
3815 | azul_css::props::layout::overflow::StyleScrollbarGutter::StableBothEdges
3816 );
3817 let gutter_both_edges = matches!(
3818 scrollbar_gutter,
3819 azul_css::props::layout::overflow::StyleScrollbarGutter::StableBothEdges
3820 );
3821
3822 if gutter_is_stable {
3823 let gbp = node.box_props.unpack();
3824 let border = &gbp.border;
3825 let gutter_width = scrollbar_style.visual_width_px;
3826 let bg_color = node_id
3828 .map_or(ColorU::TRANSPARENT, |nid| {
3829 let node_state =
3830 &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
3831 get_background_color(self.ctx.styled_dom, nid, node_state)
3832 });
3833
3834 if !scrollbar_info.needs_vertical && gutter_width > 0.0 {
3835 let gutter_rect = LogicalRect {
3837 origin: LogicalPosition::new(
3838 paint_rect.origin.x + paint_rect.size.width - border.right - gutter_width,
3839 paint_rect.origin.y + border.top,
3840 ),
3841 size: LogicalSize::new(
3842 gutter_width,
3843 (paint_rect.size.height - border.top - border.bottom).max(0.0),
3844 ),
3845 };
3846 builder.push_rect(gutter_rect, bg_color, BorderRadius::default());
3847
3848 if gutter_both_edges {
3850 let left_gutter_rect = LogicalRect {
3851 origin: LogicalPosition::new(
3852 paint_rect.origin.x + border.left,
3853 paint_rect.origin.y + border.top,
3854 ),
3855 size: LogicalSize::new(
3856 gutter_width,
3857 (paint_rect.size.height - border.top - border.bottom).max(0.0),
3858 ),
3859 };
3860 builder.push_rect(left_gutter_rect, bg_color, BorderRadius::default());
3861 }
3862 }
3863 }
3864
3865 let sbp = node.box_props.unpack();
3867 let border = &sbp.border;
3868
3869 let container_border_radius = node_id
3871 .map(|nid| {
3872 let node_state =
3873 &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
3874 let element_size = PhysicalSizeImport {
3875 width: paint_rect.size.width,
3876 height: paint_rect.size.height,
3877 };
3878 let viewport_size =
3879 LogicalSize::new(self.ctx.viewport_size.width, self.ctx.viewport_size.height);
3880 get_border_radius(
3881 self.ctx.styled_dom,
3882 nid,
3883 node_state,
3884 element_size,
3885 viewport_size,
3886 )
3887 })
3888 .unwrap_or_default();
3889
3890 let inner_rect = LogicalRect {
3893 origin: LogicalPosition::new(
3894 paint_rect.origin.x + border.left,
3895 paint_rect.origin.y + border.top,
3896 ),
3897 size: LogicalSize::new(
3898 (paint_rect.size.width - border.left - border.right).max(0.0),
3899 (paint_rect.size.height - border.top - border.bottom).max(0.0),
3900 ),
3901 };
3902
3903 let (scroll_offset_x, scroll_offset_y) = node_id
3907 .and_then(|nid| {
3908 self.scroll_offsets.get(&nid).map(|pos| {
3909 (
3910 pos.children_rect.origin.x - pos.parent_rect.origin.x,
3911 pos.children_rect.origin.y - pos.parent_rect.origin.y,
3912 )
3913 })
3914 })
3915 .unwrap_or((0.0, 0.0));
3916
3917 let content_size = node_id
3923 .and_then(|nid| self.scroll_offsets.get(&nid)).map_or_else(|| self.positioned_tree.tree.get_content_size(node_index), |pos| pos.children_rect.size);
3924
3925 let thumb_radius = scrollbar_style.visual_width_px / 2.0;
3927 let thumb_border_radius = BorderRadius {
3928 top_left: thumb_radius,
3929 top_right: thumb_radius,
3930 bottom_left: thumb_radius,
3931 bottom_right: thumb_radius,
3932 };
3933
3934 if scrollbar_info.needs_vertical {
3935 let opacity_key = node_id.map(|nid| {
3942 self.gpu_value_cache
3943 .and_then(|cache| {
3944 cache
3945 .scrollbar_v_opacity_keys
3946 .get(&(self.dom_id, nid))
3947 .copied()
3948 })
3949 .unwrap_or_else(OpacityKey::unique)
3950 });
3951
3952 let button_size = if scrollbar_style.show_scroll_buttons {
3954 scrollbar_style.scroll_button_size_px
3955 } else {
3956 0.0
3957 };
3958 let v_geom = compute_scrollbar_geometry_with_button_size(
3959 ScrollbarOrientation::Vertical,
3960 inner_rect,
3961 content_size,
3962 scroll_offset_y,
3963 scrollbar_style.visual_width_px,
3964 scrollbar_info.needs_horizontal,
3965 button_size,
3966 );
3967
3968 let thumb_bounds = LogicalRect {
3970 origin: LogicalPosition::new(
3971 v_geom.track_rect.origin.x,
3972 v_geom.track_rect.origin.y + v_geom.button_size,
3973 ),
3974 size: LogicalSize::new(v_geom.width_px, v_geom.thumb_length),
3975 };
3976
3977 let thumb_transform_key = node_id.map(|nid| {
3982 self.gpu_value_cache
3983 .and_then(|cache| cache.transform_keys.get(&nid).copied())
3984 .unwrap_or_else(TransformKey::unique)
3985 });
3986
3987 let thumb_initial_transform =
3989 ComputedTransform3D::new_translation(0.0, v_geom.thumb_offset, 0.0);
3990
3991 let hit_id = node_id
3993 .map(|nid| azul_core::hit_test::ScrollbarHitId::VerticalThumb(self.dom_id, nid));
3994
3995 let (button_decrement_bounds, button_increment_bounds) = if scrollbar_style.show_scroll_buttons && v_geom.button_size > 0.0 {
3997 (
3998 Some(LogicalRect {
3999 origin: v_geom.track_rect.origin,
4000 size: LogicalSize::new(v_geom.button_size, v_geom.button_size),
4001 }),
4002 Some(LogicalRect {
4003 origin: LogicalPosition::new(
4004 v_geom.track_rect.origin.x,
4005 v_geom.track_rect.origin.y + v_geom.track_rect.size.height - v_geom.button_size,
4006 ),
4007 size: LogicalSize::new(v_geom.button_size, v_geom.button_size),
4008 }),
4009 )
4010 } else {
4011 (None, None)
4012 };
4013 builder.push_scrollbar_styled(ScrollbarDrawInfo {
4014 bounds: v_geom.track_rect.into(),
4015 orientation: ScrollbarOrientation::Vertical,
4016 track_bounds: v_geom.track_rect.into(),
4017 track_color: scrollbar_style.track_color,
4018 thumb_bounds: thumb_bounds.into(),
4019 thumb_color: scrollbar_style.thumb_color,
4020 thumb_border_radius,
4021 button_decrement_bounds: button_decrement_bounds.map(Into::into),
4022 button_increment_bounds: button_increment_bounds.map(Into::into),
4023 button_color: scrollbar_style.button_color,
4024 opacity_key,
4025 thumb_transform_key,
4026 thumb_initial_transform,
4027 hit_id,
4028 clip_to_container_border: scrollbar_style.clip_to_container_border,
4029 container_border_radius,
4030 visibility: scrollbar_style.visibility,
4031 });
4032 }
4033
4034 if scrollbar_info.needs_horizontal {
4035 let opacity_key = node_id.map(|nid| {
4037 self.gpu_value_cache
4038 .and_then(|cache| {
4039 cache
4040 .scrollbar_h_opacity_keys
4041 .get(&(self.dom_id, nid))
4042 .copied()
4043 })
4044 .unwrap_or_else(OpacityKey::unique)
4045 });
4046
4047 let h_button_size = if scrollbar_style.show_scroll_buttons {
4049 scrollbar_style.scroll_button_size_px
4050 } else {
4051 0.0
4052 };
4053 let h_geom = compute_scrollbar_geometry_with_button_size(
4054 ScrollbarOrientation::Horizontal,
4055 inner_rect,
4056 content_size,
4057 scroll_offset_x,
4058 scrollbar_style.visual_width_px,
4059 scrollbar_info.needs_vertical,
4060 h_button_size,
4061 );
4062
4063 let thumb_bounds = LogicalRect {
4065 origin: LogicalPosition::new(
4066 h_geom.track_rect.origin.x + h_geom.button_size,
4067 h_geom.track_rect.origin.y,
4068 ),
4069 size: LogicalSize::new(h_geom.thumb_length, h_geom.width_px),
4070 };
4071
4072 let thumb_transform_key = node_id.map(|nid| {
4074 self.gpu_value_cache
4075 .and_then(|cache| cache.h_transform_keys.get(&nid).copied())
4076 .unwrap_or_else(TransformKey::unique)
4077 });
4078 let thumb_initial_transform =
4079 ComputedTransform3D::new_translation(h_geom.thumb_offset, 0.0, 0.0);
4080
4081 let hit_id = node_id
4083 .map(|nid| azul_core::hit_test::ScrollbarHitId::HorizontalThumb(self.dom_id, nid));
4084
4085 let (button_decrement_bounds, button_increment_bounds) = if scrollbar_style.show_scroll_buttons && h_geom.button_size > 0.0 {
4087 (
4088 Some(LogicalRect {
4089 origin: h_geom.track_rect.origin,
4090 size: LogicalSize::new(h_geom.button_size, h_geom.button_size),
4091 }),
4092 Some(LogicalRect {
4093 origin: LogicalPosition::new(
4094 h_geom.track_rect.origin.x + h_geom.track_rect.size.width - h_geom.button_size,
4095 h_geom.track_rect.origin.y,
4096 ),
4097 size: LogicalSize::new(h_geom.button_size, h_geom.button_size),
4098 }),
4099 )
4100 } else {
4101 (None, None)
4102 };
4103 builder.push_scrollbar_styled(ScrollbarDrawInfo {
4104 bounds: h_geom.track_rect.into(),
4105 orientation: ScrollbarOrientation::Horizontal,
4106 track_bounds: h_geom.track_rect.into(),
4107 track_color: scrollbar_style.track_color,
4108 thumb_bounds: thumb_bounds.into(),
4109 thumb_color: scrollbar_style.thumb_color,
4110 thumb_border_radius,
4111 button_decrement_bounds: button_decrement_bounds.map(Into::into),
4112 button_increment_bounds: button_increment_bounds.map(Into::into),
4113 button_color: scrollbar_style.button_color,
4114 opacity_key,
4115 thumb_transform_key,
4116 thumb_initial_transform,
4117 hit_id,
4118 clip_to_container_border: scrollbar_style.clip_to_container_border,
4119 container_border_radius,
4120 visibility: scrollbar_style.visibility,
4121 });
4122 }
4123
4124 Ok(())
4125 }
4126
4127 #[allow(clippy::suboptimal_flops)] #[allow(clippy::too_many_lines)] fn paint_inline_content(
4131 &self,
4132 builder: &mut DisplayListBuilder,
4133 container_rect: LogicalRect,
4134 viewport_clip_rect: LogicalRect,
4135 layout: &UnifiedLayout,
4136 source_node_index: usize,
4137 ) {
4138 let layout_bounds = layout.bounds();
4154 let actual_bounds = if layout_bounds.width > 0.0 && layout_bounds.height > 0.0 {
4155 LogicalRect {
4156 origin: container_rect.origin,
4157 size: LogicalSize {
4158 width: layout_bounds.width,
4159 height: layout_bounds.height,
4160 },
4161 }
4162 } else {
4163 LogicalRect {
4166 origin: container_rect.origin,
4167 size: LogicalSize::default(),
4168 }
4169 };
4170
4171 if layout_bounds.width > 0.0 || layout_bounds.height > 0.0 {
4175 builder.push_text_layout(
4176 Arc::new(layout.clone()),
4177 actual_bounds,
4178 FontHash::from_hash(0), 12.0, ColorU {
4181 r: 0,
4182 g: 0,
4183 b: 0,
4184 a: 255,
4185 }, );
4187 }
4188
4189 let glyph_runs = crate::text3::glyphs::get_glyph_runs_simple(layout);
4190
4191 for glyph_run in &glyph_runs {
4194 if let (Some(first_glyph), Some(last_glyph)) =
4196 (glyph_run.glyphs.first(), glyph_run.glyphs.last())
4197 {
4198 let run_start_x = container_rect.origin.x + first_glyph.point.x;
4200 let run_end_x = container_rect.origin.x + last_glyph.point.x;
4201 let run_width = (run_end_x - run_start_x).max(0.0);
4202
4203 if run_width <= 0.0 {
4205 continue;
4206 }
4207
4208 let baseline_y = container_rect.origin.y + first_glyph.point.y;
4210 let font_size = glyph_run.font_size_px;
4211 let ascent = font_size * APPROX_ASCENT_RATIO;
4212
4213 let mut run_bounds = LogicalRect::new(
4214 LogicalPosition::new(run_start_x, baseline_y - ascent),
4215 LogicalSize::new(run_width, font_size),
4216 );
4217
4218 if let Some(border) = &glyph_run.border {
4221 let left_inset = border.left_inset();
4222 let right_inset = border.right_inset();
4223 let top_inset = border.top_inset();
4224 let bottom_inset = border.bottom_inset();
4225
4226 run_bounds.origin.x -= left_inset;
4227 run_bounds.origin.y -= top_inset;
4228 run_bounds.size.width += left_inset + right_inset;
4229 run_bounds.size.height += top_inset + bottom_inset;
4230 }
4231
4232 builder.push_inline_backgrounds_and_border(
4233 run_bounds,
4234 glyph_run.background_color,
4235 &glyph_run.background_content,
4236 glyph_run.border.as_ref(),
4237 self.ctx.image_cache,
4238 );
4239 }
4240 }
4241
4242 for glyph_run in &glyph_runs {
4244 let clip_rect = viewport_clip_rect;
4248
4249 let offset_glyphs: Vec<GlyphInstance> = glyph_run
4252 .glyphs
4253 .iter()
4254 .map(|g| {
4255 let mut g = *g;
4256 g.point.x += container_rect.origin.x;
4257 g.point.y += container_rect.origin.y;
4258 g
4259 })
4260 .collect();
4261
4262 builder.push_text_run(
4264 offset_glyphs,
4265 FontHash::from_hash(glyph_run.font_hash),
4266 glyph_run.font_size_px,
4267 glyph_run.color,
4268 clip_rect,
4269 Some(source_node_index),
4270 );
4271
4272 let needs_underline = glyph_run.text_decoration.underline || glyph_run.is_ime_preview;
4274 let needs_strikethrough = glyph_run.text_decoration.strikethrough;
4275 let needs_overline = glyph_run.text_decoration.overline;
4276
4277 if needs_underline || needs_strikethrough || needs_overline {
4278 if let (Some(first_glyph), Some(last_glyph)) =
4280 (glyph_run.glyphs.first(), glyph_run.glyphs.last())
4281 {
4282 let decoration_start_x = container_rect.origin.x + first_glyph.point.x;
4283 let decoration_end_x = container_rect.origin.x + last_glyph.point.x;
4284 let decoration_width = decoration_end_x - decoration_start_x;
4285
4286 let font_size = glyph_run.font_size_px;
4289 let thickness = (font_size * APPROX_UNDERLINE_THICKNESS_RATIO).max(1.0);
4290
4291 let baseline_y = container_rect.origin.y + first_glyph.point.y;
4293
4294 if needs_underline {
4295 let underline_y = baseline_y + (font_size * APPROX_UNDERLINE_OFFSET_RATIO);
4298 let underline_bounds = LogicalRect::new(
4299 LogicalPosition::new(decoration_start_x, underline_y),
4300 LogicalSize::new(decoration_width, thickness),
4301 );
4302 builder.push_underline(underline_bounds, glyph_run.color, thickness);
4303 }
4304
4305 if needs_strikethrough {
4306 let strikethrough_y = baseline_y - (font_size * APPROX_STRIKETHROUGH_OFFSET_RATIO);
4308 let strikethrough_bounds = LogicalRect::new(
4309 LogicalPosition::new(decoration_start_x, strikethrough_y),
4310 LogicalSize::new(decoration_width, thickness),
4311 );
4312 builder.push_strikethrough(
4313 strikethrough_bounds,
4314 glyph_run.color,
4315 thickness,
4316 );
4317 }
4318
4319 if needs_overline {
4320 let overline_y = baseline_y - (font_size * APPROX_OVERLINE_OFFSET_RATIO);
4322 let overline_bounds = LogicalRect::new(
4323 LogicalPosition::new(decoration_start_x, overline_y),
4324 LogicalSize::new(decoration_width, thickness),
4325 );
4326 builder.push_overline(overline_bounds, glyph_run.color, thickness);
4327 }
4328 }
4329 }
4330 }
4331
4332 for glyph_run in &glyph_runs {
4335 let Some(source_node_id) = glyph_run.source_node_id else {
4337 continue;
4338 };
4339
4340 if let (Some(first_glyph), Some(last_glyph)) =
4342 (glyph_run.glyphs.first(), glyph_run.glyphs.last())
4343 {
4344 let run_start_x = container_rect.origin.x + first_glyph.point.x;
4345 let run_end_x = container_rect.origin.x + last_glyph.point.x;
4346 let run_width = (run_end_x - run_start_x).max(0.0);
4347
4348 if run_width <= 0.0 {
4350 continue;
4351 }
4352
4353 let baseline_y = container_rect.origin.y + first_glyph.point.y;
4355 let font_size = glyph_run.font_size_px;
4356 let ascent = font_size * APPROX_ASCENT_RATIO;
4357
4358 let run_bounds = LogicalRect::new(
4359 LogicalPosition::new(run_start_x, baseline_y - ascent),
4360 LogicalSize::new(run_width, font_size),
4361 );
4362
4363 let cursor_type = self.get_cursor_type_for_text_node(source_node_id);
4366
4367 let tag_value = ((self.dom_id.inner as u64) << 32) | (source_node_id.index() as u64);
4371 let tag_type = TAG_TYPE_CURSOR | (cursor_type as u16);
4372 let tag_id = (tag_value, tag_type);
4373
4374 builder.push_hit_test_area(run_bounds, tag_id);
4375 }
4376 }
4377
4378 for positioned_item in &layout.items {
4382 self.paint_inline_object(builder, container_rect.origin, positioned_item);
4383 }
4384 }
4385
4386 fn paint_inline_object(
4388 &self,
4389 builder: &mut DisplayListBuilder,
4390 base_pos: LogicalPosition,
4391 positioned_item: &PositionedItem,
4392 ) {
4393 let ShapedItem::Object {
4394 content, bounds, ..
4395 } = &positioned_item.item
4396 else {
4397 return;
4399 };
4400
4401 let object_bounds = LogicalRect::new(
4404 LogicalPosition::new(
4405 base_pos.x + positioned_item.position.x,
4406 base_pos.y + positioned_item.position.y,
4407 ),
4408 LogicalSize::new(bounds.width, bounds.height),
4409 );
4410
4411 match content {
4412 InlineContent::Image(image) => {
4413 if let Some(image_ref) = get_image_ref_for_image_source(
4414 &image.source,
4415 self.ctx.image_cache,
4416 object_bounds.size,
4417 ) {
4418 builder.push_image(object_bounds, image_ref, BorderRadius::default());
4419 }
4420 }
4421 InlineContent::Shape(shape) => {
4422 self.paint_inline_shape(builder, object_bounds, shape, bounds);
4423 }
4424 _ => {}
4425 }
4426 }
4427
4428 fn paint_inline_shape(
4431 &self,
4432 builder: &mut DisplayListBuilder,
4433 object_bounds: LogicalRect,
4434 shape: &InlineShape,
4435 bounds: &crate::text3::cache::Rect,
4436 ) {
4437 let Some(node_id) = shape.source_node_id else {
4440 return;
4441 };
4442
4443 if let Some(indices) = self.positioned_tree.tree.dom_to_layout.get(&node_id) {
4448 if let Some(&idx) = indices.first() {
4449 if self.establishes_stacking_context(idx) {
4450 return;
4451 }
4452 }
4453 }
4454
4455 let styled_node_state =
4456 &self.ctx.styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
4457
4458 let background_contents =
4460 get_background_contents(self.ctx.styled_dom, node_id, styled_node_state);
4461
4462 let border_info = get_border_info(self.ctx.styled_dom, node_id, styled_node_state);
4464
4465 let margins = self.positioned_tree.tree.dom_to_layout.get(&node_id).map_or_else(
4468 crate::solver3::geometry::EdgeSizes::default,
4469 |indices| indices.first().map_or_else(
4470 crate::solver3::geometry::EdgeSizes::default,
4471 |&idx| self.positioned_tree.tree.nodes[idx].box_props.unpack().margin,
4472 ),
4473 );
4474
4475 let border_box_bounds = LogicalRect {
4477 origin: LogicalPosition {
4478 x: object_bounds.origin.x + margins.left,
4479 y: object_bounds.origin.y + margins.top,
4480 },
4481 size: LogicalSize {
4482 width: (object_bounds.size.width - margins.left - margins.right).max(0.0),
4483 height: (object_bounds.size.height - margins.top - margins.bottom).max(0.0),
4484 },
4485 };
4486
4487 let element_size = PhysicalSizeImport {
4488 width: border_box_bounds.size.width,
4489 height: border_box_bounds.size.height,
4490 };
4491
4492 let simple_border_radius = get_border_radius(
4494 self.ctx.styled_dom,
4495 node_id,
4496 styled_node_state,
4497 element_size,
4498 self.ctx.viewport_size,
4499 );
4500
4501 let style_border_radius =
4503 get_style_border_radius(self.ctx.styled_dom, node_id, styled_node_state);
4504
4505 builder.push_backgrounds_and_border(
4507 border_box_bounds,
4508 &background_contents,
4509 &border_info,
4510 simple_border_radius,
4511 style_border_radius,
4512 self.ctx.image_cache,
4513 );
4514
4515 if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, Some(node_id)) {
4519 builder.push_hit_test_area(border_box_bounds, tag_id);
4520 }
4521
4522 }
4523
4524 fn establishes_stacking_context(&self, node_index: usize) -> bool {
4531 let Some(node) = self.positioned_tree.tree.get(node_index) else {
4532 return false;
4533 };
4534 let Some(dom_id) = node.dom_node_id else {
4535 return false;
4536 };
4537
4538 let position = get_position_type(self.ctx.styled_dom, Some(dom_id));
4539 let z_auto = crate::solver3::getters::is_z_index_auto(self.ctx.styled_dom, Some(dom_id));
4540
4541 if position == LayoutPosition::Fixed || position == LayoutPosition::Sticky {
4543 return true;
4544 }
4545
4546 if position == LayoutPosition::Absolute {
4549 return !z_auto;
4550 }
4551
4552 if position == LayoutPosition::Relative && !z_auto {
4554 return true;
4555 }
4556
4557 if let Some(styled_node) = self.ctx.styled_dom.styled_nodes.as_container().get(dom_id) {
4558 let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
4559 let node_state =
4560 &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
4561
4562 if crate::solver3::getters::get_opacity(
4564 self.ctx.styled_dom, dom_id, node_state,
4565 ) < 1.0 {
4566 return true;
4567 }
4568
4569 if let Some(t) = crate::solver3::getters::get_transform(
4571 self.ctx.styled_dom, dom_id, node_state,
4572 ) {
4573 if !t.is_empty() {
4574 return true;
4575 }
4576 }
4577 }
4578
4579 false
4580 }
4581}
4582
4583#[derive(Debug)]
4589pub struct PositionedTree<'a> {
4590 pub tree: &'a LayoutTree,
4592 pub calculated_positions: &'a super::PositionVec,
4594}
4595
4596#[allow(clippy::trivially_copy_pass_by_ref)] fn apply_overflow_clip_margin(
4602 clip_rect: &mut LogicalRect,
4603 overflow_x: &super::getters::MultiValue<LayoutOverflow>,
4604 overflow_y: &super::getters::MultiValue<LayoutOverflow>,
4605 styled_dom: &StyledDom,
4606 dom_id: NodeId,
4607 styled_node_state: &azul_core::styled_dom::StyledNodeState,
4608) {
4609 if !overflow_x.is_clip() && !overflow_y.is_clip() {
4610 return;
4611 }
4612 let clip_margin = get_overflow_clip_margin_property(styled_dom, dom_id, styled_node_state);
4613 let Some(margin_val) = clip_margin.exact() else {
4614 return;
4615 };
4616 let m = margin_val.inner.to_pixels_internal(0.0, 0.0, 0.0).max(0.0);
4617 if m <= 0.0 {
4618 return;
4619 }
4620 if overflow_x.is_clip() {
4621 clip_rect.origin.x -= m;
4622 clip_rect.size.width += m * 2.0;
4623 }
4624 if overflow_y.is_clip() {
4625 clip_rect.origin.y -= m;
4626 clip_rect.size.height += m * 2.0;
4627 }
4628}
4629
4630fn get_scroll_id(id: Option<NodeId>) -> LocalScrollId {
4631 id.map_or(0, |i| i.index() as u64)
4632}
4633
4634fn get_scroll_content_size(node: &LayoutNodeHot, warm: Option<&LayoutNodeWarm>) -> LogicalSize {
4639 if let Some(overflow_size) = warm.and_then(|w| w.overflow_content_size) {
4641 return overflow_size;
4642 }
4643
4644 let mut content_size = node.used_size.unwrap_or_default();
4646
4647 if let Some(cached_layout) = warm.and_then(|w| w.inline_layout_result.as_ref()) {
4649 let text_layout = &cached_layout.layout;
4650 let mut max_x: f32 = 0.0;
4652 let mut max_y: f32 = 0.0;
4653
4654 for positioned_item in &text_layout.items {
4655 let item_bounds = positioned_item.item.bounds();
4656 let item_right = positioned_item.position.x + item_bounds.width;
4657 let item_bottom = positioned_item.position.y + item_bounds.height;
4658
4659 max_x = max_x.max(item_right);
4660 max_y = max_y.max(item_bottom);
4661 }
4662
4663 content_size.width = content_size.width.max(max_x);
4665 content_size.height = content_size.height.max(max_y);
4666 }
4667
4668 content_size
4669}
4670
4671fn get_tag_id(dom: &StyledDom, id: Option<NodeId>) -> Option<DisplayListTagId> {
4672 let node_id = id?;
4673 let tag_mapping = dom.tag_ids_to_node_ids.as_ref().iter().find(|m| {
4674 m.node_id.into_crate_internal() == Some(node_id)
4675 })?;
4676 Some((tag_mapping.tag_id.inner, TAG_TYPE_DOM_NODE))
4677}
4678
4679#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn get_image_ref_for_image_source(
4686 source: &ImageSource,
4687 image_cache: &azul_core::resources::ImageCache,
4688 target_size: LogicalSize,
4689) -> Option<ImageRef> {
4690 match source {
4691 ImageSource::Ref(image_ref) => Some(image_ref.clone()),
4692 ImageSource::Url(url) => {
4693 let css_id: azul_css::AzString = url.clone().into();
4696 image_cache.get_css_image_id(&css_id).cloned()
4697 }
4698 ImageSource::Data(bytes) => {
4699 #[cfg(all(feature = "std", feature = "image_decoding"))]
4703 {
4704 use crate::image::decode::{
4705 decode_raw_image_from_any_bytes, ResultRawImageDecodeImageError,
4706 };
4707 if let ResultRawImageDecodeImageError::Ok(raw) =
4708 decode_raw_image_from_any_bytes(bytes)
4709 {
4710 return ImageRef::new_rawimage(raw);
4711 }
4712 None
4713 }
4714 #[cfg(not(all(feature = "std", feature = "image_decoding")))]
4715 {
4716 let _ = bytes;
4717 None
4718 }
4719 }
4720 ImageSource::Svg(svg) => {
4721 #[cfg(feature = "cpurender")]
4724 {
4725 let w = (target_size.width.round() as u32).max(1);
4726 let h = (target_size.height.round() as u32).max(1);
4727 crate::cpurender::render_svg_to_imageref(svg.as_bytes(), w, h).ok()
4728 }
4729 #[cfg(not(feature = "cpurender"))]
4730 {
4731 let _ = (svg, target_size);
4732 None
4733 }
4734 }
4735 ImageSource::Placeholder(_) => {
4736 None
4738 }
4739 }
4740}
4741
4742fn get_display_item_bounds(item: &DisplayListItem) -> Option<WindowLogicalRect> {
4744 item.bounds().map(WindowLogicalRect::from)
4745}
4746
4747#[allow(clippy::match_same_arms)] #[allow(clippy::too_many_lines)] fn clip_and_offset_display_item(
4752 item: &DisplayListItem,
4753 page_top: f32,
4754 page_bottom: f32,
4755) -> Option<DisplayListItem> {
4756 match item {
4757 DisplayListItem::Rect {
4758 bounds,
4759 color,
4760 border_radius,
4761 } => clip_rect_item(bounds.into_inner(), *color, *border_radius, page_top, page_bottom),
4762
4763 DisplayListItem::Border {
4764 bounds,
4765 widths,
4766 colors,
4767 styles,
4768 border_radius,
4769 } => clip_border_item(
4770 bounds.into_inner(),
4771 *widths,
4772 *colors,
4773 *styles,
4774 *border_radius,
4775 page_top,
4776 page_bottom,
4777 ),
4778
4779 DisplayListItem::SelectionRect {
4780 bounds,
4781 border_radius,
4782 color,
4783 } => clip_selection_rect_item(bounds.into_inner(), *border_radius, *color, page_top, page_bottom),
4784
4785 DisplayListItem::CursorRect { bounds, color } => {
4786 clip_cursor_rect_item(bounds.into_inner(), *color, page_top, page_bottom)
4787 }
4788
4789 DisplayListItem::Image { bounds, image, border_radius } => {
4790 clip_image_item(bounds.into_inner(), image.clone(), *border_radius, page_top, page_bottom)
4791 }
4792
4793 DisplayListItem::TextLayout {
4794 layout,
4795 bounds,
4796 font_hash,
4797 font_size_px,
4798 color,
4799 } => clip_text_layout_item(
4800 layout,
4801 bounds.into_inner(),
4802 *font_hash,
4803 *font_size_px,
4804 *color,
4805 page_top,
4806 page_bottom,
4807 ),
4808
4809 DisplayListItem::Text {
4810 glyphs,
4811 font_hash,
4812 font_size_px,
4813 color,
4814 clip_rect,
4815 ..
4816 } => clip_text_item(
4817 glyphs,
4818 *font_hash,
4819 *font_size_px,
4820 *color,
4821 clip_rect.into_inner(),
4822 page_top,
4823 page_bottom,
4824 ),
4825
4826 DisplayListItem::Underline {
4827 bounds,
4828 color,
4829 thickness,
4830 } => clip_text_decoration_item(
4831 bounds.into_inner(),
4832 *color,
4833 *thickness,
4834 TextDecorationType::Underline,
4835 page_top,
4836 page_bottom,
4837 ),
4838
4839 DisplayListItem::Strikethrough {
4840 bounds,
4841 color,
4842 thickness,
4843 } => clip_text_decoration_item(
4844 bounds.into_inner(),
4845 *color,
4846 *thickness,
4847 TextDecorationType::Strikethrough,
4848 page_top,
4849 page_bottom,
4850 ),
4851
4852 DisplayListItem::Overline {
4853 bounds,
4854 color,
4855 thickness,
4856 } => clip_text_decoration_item(
4857 bounds.into_inner(),
4858 *color,
4859 *thickness,
4860 TextDecorationType::Overline,
4861 page_top,
4862 page_bottom,
4863 ),
4864
4865 DisplayListItem::ScrollBar {
4866 bounds,
4867 color,
4868 orientation,
4869 opacity_key,
4870 hit_id,
4871 } => clip_scrollbar_item(
4872 bounds.into_inner(),
4873 *color,
4874 *orientation,
4875 *opacity_key,
4876 *hit_id,
4877 page_top,
4878 page_bottom,
4879 ),
4880
4881 DisplayListItem::HitTestArea { bounds, tag } => {
4882 clip_hit_test_area_item(bounds.into_inner(), *tag, page_top, page_bottom)
4883 }
4884
4885 DisplayListItem::VirtualView {
4886 child_dom_id,
4887 bounds,
4888 clip_rect,
4889 } => clip_virtual_view_item(*child_dom_id, bounds.into_inner(), clip_rect.into_inner(), page_top, page_bottom),
4890
4891 DisplayListItem::ScrollBarStyled { info } => {
4893 let bounds = info.bounds;
4894 if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
4895 None
4896 } else {
4897 let mut clipped_info = (**info).clone();
4899 let y_offset = -page_top;
4900 clipped_info.bounds = offset_rect_y(clipped_info.bounds.into_inner(), y_offset).into();
4901 clipped_info.track_bounds = offset_rect_y(clipped_info.track_bounds.into_inner(), y_offset).into();
4902 clipped_info.thumb_bounds = offset_rect_y(clipped_info.thumb_bounds.into_inner(), y_offset).into();
4903 if let Some(b) = clipped_info.button_decrement_bounds {
4904 clipped_info.button_decrement_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
4905 }
4906 if let Some(b) = clipped_info.button_increment_bounds {
4907 clipped_info.button_increment_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
4908 }
4909 Some(DisplayListItem::ScrollBarStyled {
4910 info: Box::new(clipped_info),
4911 })
4912 }
4913 }
4914
4915 DisplayListItem::PushClip { .. }
4917 | DisplayListItem::PopClip
4918 | DisplayListItem::PushScrollFrame { .. }
4919 | DisplayListItem::PopScrollFrame
4920 | DisplayListItem::PushStackingContext { .. }
4921 | DisplayListItem::PopStackingContext
4922 | DisplayListItem::VirtualViewPlaceholder { .. } => None,
4923
4924 DisplayListItem::LinearGradient {
4926 bounds,
4927 gradient,
4928 border_radius,
4929 } => {
4930 if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
4931 None
4932 } else {
4933 Some(DisplayListItem::LinearGradient {
4934 bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
4935 gradient: gradient.clone(),
4936 border_radius: *border_radius,
4937 })
4938 }
4939 }
4940 DisplayListItem::RadialGradient {
4941 bounds,
4942 gradient,
4943 border_radius,
4944 } => {
4945 if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
4946 None
4947 } else {
4948 Some(DisplayListItem::RadialGradient {
4949 bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
4950 gradient: gradient.clone(),
4951 border_radius: *border_radius,
4952 })
4953 }
4954 }
4955 DisplayListItem::ConicGradient {
4956 bounds,
4957 gradient,
4958 border_radius,
4959 } => {
4960 if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
4961 None
4962 } else {
4963 Some(DisplayListItem::ConicGradient {
4964 bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
4965 gradient: gradient.clone(),
4966 border_radius: *border_radius,
4967 })
4968 }
4969 }
4970
4971 DisplayListItem::BoxShadow {
4973 bounds,
4974 shadow,
4975 border_radius,
4976 } => {
4977 if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
4978 None
4979 } else {
4980 Some(DisplayListItem::BoxShadow {
4981 bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
4982 shadow: *shadow,
4983 border_radius: *border_radius,
4984 })
4985 }
4986 }
4987
4988 DisplayListItem::PushFilter { .. }
4990 | DisplayListItem::PopFilter
4991 | DisplayListItem::PushBackdropFilter { .. }
4992 | DisplayListItem::PopBackdropFilter
4993 | DisplayListItem::PushOpacity { .. }
4994 | DisplayListItem::PopOpacity
4995 | DisplayListItem::PushReferenceFrame { .. }
4996 | DisplayListItem::PopReferenceFrame
4997 | DisplayListItem::PushTextShadow { .. }
4998 | DisplayListItem::PopTextShadow
4999 | DisplayListItem::PushImageMaskClip { .. }
5000 | DisplayListItem::PopImageMaskClip => None,
5001 }
5002}
5003
5004#[derive(Debug, Clone, Copy)]
5008enum TextDecorationType {
5009 Underline,
5010 Strikethrough,
5011 Overline,
5012}
5013
5014fn clip_rect_item(
5016 bounds: LogicalRect,
5017 color: ColorU,
5018 border_radius: BorderRadius,
5019 page_top: f32,
5020 page_bottom: f32,
5021) -> Option<DisplayListItem> {
5022 clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::Rect {
5023 bounds: clipped.into(),
5024 color,
5025 border_radius,
5026 })
5027}
5028
5029fn clip_border_item(
5031 bounds: LogicalRect,
5032 widths: StyleBorderWidths,
5033 colors: StyleBorderColors,
5034 styles: StyleBorderStyles,
5035 border_radius: StyleBorderRadius,
5036 page_top: f32,
5037 page_bottom: f32,
5038) -> Option<DisplayListItem> {
5039 let original_bounds = bounds;
5040 clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| {
5041 let new_widths = adjust_border_widths_for_clipping(
5042 widths,
5043 original_bounds,
5044 clipped,
5045 page_top,
5046 page_bottom,
5047 );
5048 DisplayListItem::Border {
5049 bounds: clipped.into(),
5050 widths: new_widths,
5051 colors,
5052 styles,
5053 border_radius,
5054 }
5055 })
5056}
5057
5058fn adjust_border_widths_for_clipping(
5061 mut widths: StyleBorderWidths,
5062 original_bounds: LogicalRect,
5063 clipped: LogicalRect,
5064 page_top: f32,
5065 page_bottom: f32,
5066) -> StyleBorderWidths {
5067 if clipped.origin.y > 0.0 && original_bounds.origin.y < page_top {
5069 widths.top = None;
5070 }
5071
5072 let original_bottom = original_bounds.origin.y + original_bounds.size.height;
5074 let clipped_bottom = clipped.origin.y + clipped.size.height;
5075 if original_bottom > page_bottom && clipped_bottom >= page_bottom - page_top - 1.0 {
5076 widths.bottom = None;
5077 }
5078
5079 widths
5080}
5081
5082fn clip_selection_rect_item(
5084 bounds: LogicalRect,
5085 border_radius: BorderRadius,
5086 color: ColorU,
5087 page_top: f32,
5088 page_bottom: f32,
5089) -> Option<DisplayListItem> {
5090 clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::SelectionRect {
5091 bounds: clipped.into(),
5092 border_radius,
5093 color,
5094 })
5095}
5096
5097fn clip_cursor_rect_item(
5099 bounds: LogicalRect,
5100 color: ColorU,
5101 page_top: f32,
5102 page_bottom: f32,
5103) -> Option<DisplayListItem> {
5104 clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::CursorRect {
5105 bounds: clipped.into(),
5106 color,
5107 })
5108}
5109
5110fn clip_image_item(
5112 bounds: LogicalRect,
5113 image: ImageRef,
5114 border_radius: BorderRadius,
5115 page_top: f32,
5116 page_bottom: f32,
5117) -> Option<DisplayListItem> {
5118 if !rect_intersects(&bounds, page_top, page_bottom) {
5119 return None;
5120 }
5121 clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::Image {
5122 bounds: clipped.into(),
5123 image,
5124 border_radius,
5125 })
5126}
5127
5128fn clip_text_layout_item(
5130 layout: &Arc<dyn std::any::Any + Send + Sync>,
5131 bounds: LogicalRect,
5132 font_hash: FontHash,
5133 font_size_px: f32,
5134 color: ColorU,
5135 page_top: f32,
5136 page_bottom: f32,
5137) -> Option<DisplayListItem> {
5138 if !rect_intersects(&bounds, page_top, page_bottom) {
5139 return None;
5140 }
5141
5142 #[cfg(feature = "text_layout")]
5144 if let Some(unified_layout) = layout.downcast_ref::<UnifiedLayout>() {
5145 return clip_unified_layout(
5146 unified_layout,
5147 bounds,
5148 font_hash,
5149 font_size_px,
5150 color,
5151 page_top,
5152 page_bottom,
5153 );
5154 }
5155
5156 Some(DisplayListItem::TextLayout {
5158 layout: layout.clone(),
5159 bounds: offset_rect_y(bounds, -page_top).into(),
5160 font_hash,
5161 font_size_px,
5162 color,
5163 })
5164}
5165
5166#[cfg(feature = "text_layout")]
5168fn clip_unified_layout(
5169 unified_layout: &UnifiedLayout,
5170 bounds: LogicalRect,
5171 font_hash: FontHash,
5172 font_size_px: f32,
5173 color: ColorU,
5174 page_top: f32,
5175 page_bottom: f32,
5176) -> Option<DisplayListItem> {
5177 let layout_origin_y = bounds.origin.y;
5178 let layout_origin_x = bounds.origin.x;
5179
5180 let filtered_items: Vec<_> = unified_layout
5182 .items
5183 .iter()
5184 .filter(|item| item_center_on_page(item, layout_origin_y, page_top, page_bottom))
5185 .cloned()
5186 .collect();
5187
5188 if filtered_items.is_empty() {
5189 return None;
5190 }
5191
5192 let new_origin_y = (layout_origin_y - page_top).max(0.0);
5194
5195 let (offset_items, min_y, max_y, max_width) =
5197 transform_items_to_page_coords(filtered_items, layout_origin_y, page_top, new_origin_y);
5198
5199 let new_layout = UnifiedLayout {
5200 items: offset_items,
5201 overflow: unified_layout.overflow.clone(),
5202 };
5203
5204 let new_bounds = LogicalRect {
5205 origin: LogicalPosition {
5206 x: layout_origin_x,
5207 y: new_origin_y,
5208 },
5209 size: LogicalSize {
5210 width: max_width.max(bounds.size.width),
5211 height: (max_y - min_y.min(0.0)).max(0.0),
5212 },
5213 };
5214
5215 Some(DisplayListItem::TextLayout {
5216 layout: Arc::new(new_layout),
5217 bounds: new_bounds.into(),
5218 font_hash,
5219 font_size_px,
5220 color,
5221 })
5222}
5223
5224#[cfg(feature = "text_layout")]
5226fn item_center_on_page(
5227 item: &PositionedItem,
5228 layout_origin_y: f32,
5229 page_top: f32,
5230 page_bottom: f32,
5231) -> bool {
5232 let item_y_absolute = layout_origin_y + item.position.y;
5233 let item_height = item.item.bounds().height;
5234 let item_center_y = item_y_absolute + (item_height / 2.0);
5235 item_center_y >= page_top && item_center_y < page_bottom
5236}
5237
5238#[cfg(feature = "text_layout")]
5241fn transform_items_to_page_coords(
5242 items: Vec<PositionedItem>,
5243 layout_origin_y: f32,
5244 page_top: f32,
5245 new_origin_y: f32,
5246) -> (Vec<PositionedItem>, f32, f32, f32) {
5247 let mut min_y = f32::MAX;
5248 let mut max_y = f32::MIN;
5249 let mut max_width = 0.0f32;
5250
5251 let offset_items: Vec<_> = items
5252 .into_iter()
5253 .map(|mut item| {
5254 let abs_y = layout_origin_y + item.position.y;
5255 let page_y = abs_y - page_top;
5256 let new_item_y = page_y - new_origin_y;
5257
5258 let item_bounds = item.item.bounds();
5259 min_y = min_y.min(new_item_y);
5260 max_y = max_y.max(new_item_y + item_bounds.height);
5261 max_width = max_width.max(item.position.x + item_bounds.width);
5262
5263 item.position.y = new_item_y;
5264 item
5265 })
5266 .collect();
5267
5268 (offset_items, min_y, max_y, max_width)
5269}
5270
5271fn clip_text_item(
5273 glyphs: &[GlyphInstance],
5274 font_hash: FontHash,
5275 font_size_px: f32,
5276 color: ColorU,
5277 clip_rect: LogicalRect,
5278 page_top: f32,
5279 page_bottom: f32,
5280) -> Option<DisplayListItem> {
5281 if !rect_intersects(&clip_rect, page_top, page_bottom) {
5282 return None;
5283 }
5284
5285 let page_glyphs: Vec<_> = glyphs
5287 .iter()
5288 .filter(|g| g.point.y >= page_top && g.point.y < page_bottom)
5289 .map(|g| GlyphInstance {
5290 index: g.index,
5291 point: LogicalPosition {
5292 x: g.point.x,
5293 y: g.point.y - page_top,
5294 },
5295 size: g.size,
5296 })
5297 .collect();
5298
5299 if page_glyphs.is_empty() {
5300 return None;
5301 }
5302
5303 Some(DisplayListItem::Text {
5304 glyphs: page_glyphs,
5305 font_hash,
5306 font_size_px,
5307 color,
5308 clip_rect: offset_rect_y(clip_rect, -page_top).into(),
5309 source_node_index: None,
5310 })
5311}
5312
5313fn clip_text_decoration_item(
5315 bounds: LogicalRect,
5316 color: ColorU,
5317 thickness: f32,
5318 decoration_type: TextDecorationType,
5319 page_top: f32,
5320 page_bottom: f32,
5321) -> Option<DisplayListItem> {
5322 clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| match decoration_type {
5323 TextDecorationType::Underline => DisplayListItem::Underline {
5324 bounds: clipped.into(),
5325 color,
5326 thickness,
5327 },
5328 TextDecorationType::Strikethrough => DisplayListItem::Strikethrough {
5329 bounds: clipped.into(),
5330 color,
5331 thickness,
5332 },
5333 TextDecorationType::Overline => DisplayListItem::Overline {
5334 bounds: clipped.into(),
5335 color,
5336 thickness,
5337 },
5338 })
5339}
5340
5341fn clip_scrollbar_item(
5343 bounds: LogicalRect,
5344 color: ColorU,
5345 orientation: ScrollbarOrientation,
5346 opacity_key: Option<OpacityKey>,
5347 hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
5348 page_top: f32,
5349 page_bottom: f32,
5350) -> Option<DisplayListItem> {
5351 clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::ScrollBar {
5352 bounds: clipped.into(),
5353 color,
5354 orientation,
5355 opacity_key,
5356 hit_id,
5357 })
5358}
5359
5360fn clip_hit_test_area_item(
5362 bounds: LogicalRect,
5363 tag: DisplayListTagId,
5364 page_top: f32,
5365 page_bottom: f32,
5366) -> Option<DisplayListItem> {
5367 clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::HitTestArea {
5368 bounds: clipped.into(),
5369 tag,
5370 })
5371}
5372
5373fn clip_virtual_view_item(
5375 child_dom_id: DomId,
5376 bounds: LogicalRect,
5377 clip_rect: LogicalRect,
5378 page_top: f32,
5379 page_bottom: f32,
5380) -> Option<DisplayListItem> {
5381 clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::VirtualView {
5382 child_dom_id,
5383 bounds: clipped.into(),
5384 clip_rect: offset_rect_y(clip_rect, -page_top).into(),
5385 })
5386}
5387
5388fn clip_rect_bounds(bounds: LogicalRect, page_top: f32, page_bottom: f32) -> Option<LogicalRect> {
5391 let item_top = bounds.origin.y;
5392 let item_bottom = bounds.origin.y + bounds.size.height;
5393
5394 if item_bottom <= page_top || item_top >= page_bottom {
5396 return None;
5397 }
5398
5399 let clipped_top = item_top.max(page_top);
5401 let clipped_bottom = item_bottom.min(page_bottom);
5402 let clipped_height = clipped_bottom - clipped_top;
5403
5404 let page_relative_y = clipped_top - page_top;
5406
5407 Some(LogicalRect {
5408 origin: LogicalPosition {
5409 x: bounds.origin.x,
5410 y: page_relative_y,
5411 },
5412 size: LogicalSize {
5413 width: bounds.size.width,
5414 height: clipped_height,
5415 },
5416 })
5417}
5418
5419fn rect_intersects(bounds: &LogicalRect, page_top: f32, page_bottom: f32) -> bool {
5421 let item_top = bounds.origin.y;
5422 let item_bottom = bounds.origin.y + bounds.size.height;
5423 item_bottom > page_top && item_top < page_bottom
5424}
5425
5426fn offset_rect_y(bounds: LogicalRect, offset_y: f32) -> LogicalRect {
5428 LogicalRect {
5429 origin: LogicalPosition {
5430 x: bounds.origin.x,
5431 y: bounds.origin.y + offset_y,
5432 },
5433 size: bounds.size,
5434 }
5435}
5436
5437use azul_css::props::layout::fragmentation::{BreakInside, PageBreak};
5446
5447use crate::solver3::pagination::{
5448 HeaderFooterConfig, MarginBoxContent, PageInfo, TableHeaderInfo, TableHeaderTracker,
5449};
5450
5451#[derive(Debug, Clone, Default)]
5453pub struct SlicerConfig {
5454 pub page_content_height: f32,
5456 pub page_gap: f32,
5459 pub allow_clipping: bool,
5461 pub header_footer: HeaderFooterConfig,
5463 pub page_width: f32,
5465 pub table_headers: TableHeaderTracker,
5467}
5468
5469impl SlicerConfig {
5470 #[must_use] pub fn simple(page_height: f32) -> Self {
5472 Self {
5473 page_content_height: page_height,
5474 page_gap: 0.0,
5475 allow_clipping: true,
5476 header_footer: HeaderFooterConfig::default(),
5477 page_width: DEFAULT_A4_WIDTH_PT, table_headers: TableHeaderTracker::default(),
5479 }
5480 }
5481
5482 #[must_use] pub fn with_gap(page_height: f32, gap: f32) -> Self {
5484 Self {
5485 page_content_height: page_height,
5486 page_gap: gap,
5487 allow_clipping: true,
5488 header_footer: HeaderFooterConfig::default(),
5489 page_width: DEFAULT_A4_WIDTH_PT,
5490 table_headers: TableHeaderTracker::default(),
5491 }
5492 }
5493
5494 #[must_use] pub fn with_header_footer(mut self, config: HeaderFooterConfig) -> Self {
5496 self.header_footer = config;
5497 self
5498 }
5499
5500 #[must_use] pub const fn with_page_width(mut self, width: f32) -> Self {
5502 self.page_width = width;
5503 self
5504 }
5505
5506 #[must_use] pub fn with_table_headers(mut self, tracker: TableHeaderTracker) -> Self {
5508 self.table_headers = tracker;
5509 self
5510 }
5511
5512 pub fn register_table_header(&mut self, info: TableHeaderInfo) {
5514 self.table_headers.register_table_header(info);
5515 }
5516
5517 #[must_use] pub fn page_slot_height(&self) -> f32 {
5519 self.page_content_height + self.page_gap
5520 }
5521
5522 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[must_use] pub fn page_for_y(&self, y: f32) -> usize {
5525 if self.page_slot_height() <= 0.0 {
5526 return 0;
5527 }
5528 (y / self.page_slot_height()).floor() as usize
5529 }
5530
5531 #[allow(clippy::cast_precision_loss)] #[must_use] pub fn page_bounds(&self, page_index: usize) -> (f32, f32) {
5534 let start = page_index as f32 * self.page_slot_height();
5535 let end = start + self.page_content_height;
5536 (start, end)
5537 }
5538}
5539
5540#[allow(clippy::too_many_lines)] pub fn paginate_display_list_with_slicer_and_breaks(
5552 full_display_list: DisplayList,
5553 config: &SlicerConfig,
5554 renderer_resources: &RendererResources,
5555) -> Result<Vec<DisplayList>> {
5556 if config.page_content_height <= 0.0 || config.page_content_height >= f32::MAX {
5557 return Ok(vec![full_display_list]);
5558 }
5559
5560 let base_header_space = if config.header_footer.show_header {
5562 config.header_footer.header_height
5563 } else {
5564 0.0
5565 };
5566 let base_footer_space = if config.header_footer.show_footer {
5567 config.header_footer.footer_height
5568 } else {
5569 0.0
5570 };
5571
5572 let normal_page_content_height =
5574 config.page_content_height - base_header_space - base_footer_space;
5575 let first_page_content_height = if config.header_footer.skip_first_page {
5576 config.page_content_height
5578 } else {
5579 normal_page_content_height
5580 };
5581
5582 let page_breaks = calculate_page_break_positions(
5592 &full_display_list,
5593 first_page_content_height,
5594 normal_page_content_height,
5595 );
5596
5597 let num_pages = page_breaks.len();
5598
5599 let mut pages: Vec<DisplayList> = Vec::with_capacity(num_pages);
5601
5602 for (page_idx, &(content_start_y, content_end_y)) in page_breaks.iter().enumerate() {
5603 let page_info = PageInfo::new(page_idx + 1, num_pages);
5605
5606 let skip_this_page = config.header_footer.skip_first_page && page_info.is_first;
5608 let header_space = if config.header_footer.show_header && !skip_this_page {
5609 config.header_footer.header_height
5610 } else {
5611 0.0
5612 };
5613 let footer_space = if config.header_footer.show_footer && !skip_this_page {
5614 config.header_footer.footer_height
5615 } else {
5616 0.0
5617 };
5618
5619 let _ = footer_space; let mut page_items = Vec::new();
5622 let mut page_node_mapping = Vec::new();
5623
5624 if config.header_footer.show_header && !skip_this_page {
5626 let header_text = config.header_footer.header_text(page_info);
5627 if !header_text.is_empty() {
5628 let header_items = generate_text_display_items(
5629 &header_text,
5630 LogicalRect {
5631 origin: LogicalPosition { x: 0.0, y: 0.0 },
5632 size: LogicalSize {
5633 width: config.page_width,
5634 height: config.header_footer.header_height,
5635 },
5636 },
5637 config.header_footer.font_size,
5638 config.header_footer.text_color,
5639 TextAlignment::Center,
5640 renderer_resources,
5641 );
5642 for item in header_items {
5643 page_items.push(item);
5644 page_node_mapping.push(None);
5645 }
5646 }
5647 }
5648
5649 let repeated_headers = config.table_headers.get_repeated_headers_for_page(
5651 page_idx,
5652 content_start_y,
5653 content_end_y,
5654 );
5655
5656 let mut thead_total_height = 0.0f32;
5657 for (y_offset_from_page_top, thead_items, thead_height) in repeated_headers {
5658 let thead_y = header_space + y_offset_from_page_top;
5659 for item in thead_items {
5660 let translated_item = offset_display_item_y(item, thead_y);
5661 page_items.push(translated_item);
5662 page_node_mapping.push(None);
5663 }
5664 thead_total_height = thead_total_height.max(thead_height);
5665 }
5666
5667 let content_y_offset = header_space + thead_total_height;
5669
5670 for (item_idx, item) in full_display_list.items.iter().enumerate() {
5672 let is_fixed = full_display_list.fixed_position_item_ranges.iter()
5674 .any(|&(start, end)| item_idx >= start && item_idx < end);
5675 if is_fixed {
5676 continue;
5677 }
5678 if let Some(clipped_item) =
5679 clip_and_offset_display_item(item, content_start_y, content_end_y)
5680 {
5681 let final_item = if content_y_offset > 0.0 {
5682 offset_display_item_y(&clipped_item, content_y_offset)
5683 } else {
5684 clipped_item
5685 };
5686 page_items.push(final_item);
5687 let node_mapping = full_display_list
5688 .node_mapping
5689 .get(item_idx)
5690 .copied()
5691 .flatten();
5692 page_node_mapping.push(node_mapping);
5693 }
5694 }
5695
5696 for &(start, end) in &full_display_list.fixed_position_item_ranges {
5700 for item_idx in start..end {
5701 if let Some(item) = full_display_list.items.get(item_idx) {
5702 let final_item = if content_y_offset > 0.0 {
5703 offset_display_item_y(item, content_y_offset)
5704 } else {
5705 item.clone()
5706 };
5707 page_items.push(final_item);
5708 let node_mapping = full_display_list
5709 .node_mapping
5710 .get(item_idx)
5711 .copied()
5712 .flatten();
5713 page_node_mapping.push(node_mapping);
5714 }
5715 }
5716 }
5717
5718 if config.header_footer.show_footer && !skip_this_page {
5720 let footer_text = config.header_footer.footer_text(page_info);
5721 if !footer_text.is_empty() {
5722 let footer_y = config.page_content_height - config.header_footer.footer_height;
5723 let footer_items = generate_text_display_items(
5724 &footer_text,
5725 LogicalRect {
5726 origin: LogicalPosition {
5727 x: 0.0,
5728 y: footer_y,
5729 },
5730 size: LogicalSize {
5731 width: config.page_width,
5732 height: config.header_footer.footer_height,
5733 },
5734 },
5735 config.header_footer.font_size,
5736 config.header_footer.text_color,
5737 TextAlignment::Center,
5738 renderer_resources,
5739 );
5740 for item in footer_items {
5741 page_items.push(item);
5742 page_node_mapping.push(None);
5743 }
5744 }
5745 }
5746
5747 pages.push(DisplayList {
5748 items: page_items,
5749 node_mapping: page_node_mapping,
5750 forced_page_breaks: Vec::new(),
5751 fixed_position_item_ranges: Vec::new(), });
5753 }
5754
5755 if pages.is_empty() {
5757 pages.push(DisplayList::default());
5758 }
5759
5760 Ok(pages)
5761}
5762
5763fn calculate_page_break_positions(
5771 display_list: &DisplayList,
5772 first_page_height: f32,
5773 normal_page_height: f32,
5774) -> Vec<(f32, f32)> {
5775 let total_height = calculate_display_list_height(display_list);
5776
5777 if total_height <= 0.0 || first_page_height <= 0.0 {
5778 return vec![(0.0, total_height.max(first_page_height))];
5779 }
5780
5781 let mut break_points: Vec<f32> = Vec::new();
5783
5784 for &forced_break_y in &display_list.forced_page_breaks {
5786 if forced_break_y > 0.0 && forced_break_y < total_height {
5787 break_points.push(forced_break_y);
5788 }
5789 }
5790
5791 let mut y = first_page_height;
5793 #[allow(clippy::while_float)] while y < total_height {
5795 break_points.push(y);
5796 y += normal_page_height;
5797 }
5798
5799 break_points.sort_by(|a, b| a.partial_cmp(b).unwrap());
5801 break_points.dedup_by(|a, b| (*a - *b).abs() < 1.0); let mut page_breaks: Vec<(f32, f32)> = Vec::new();
5805 let mut page_start = 0.0f32;
5806
5807 for break_y in break_points {
5808 if break_y > page_start {
5809 page_breaks.push((page_start, break_y));
5810 page_start = break_y;
5811 }
5812 }
5813
5814 if page_start < total_height {
5816 page_breaks.push((page_start, total_height));
5817 }
5818
5819 if page_breaks.is_empty() {
5821 page_breaks.push((0.0, total_height.max(first_page_height)));
5822 }
5823
5824 page_breaks
5825}
5826
5827#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5829enum TextAlignment {
5830 Left,
5831 Center,
5832 Right,
5833}
5834
5835#[allow(clippy::too_many_lines)] fn offset_display_item_y(item: &DisplayListItem, y_offset: f32) -> DisplayListItem {
5838 if y_offset == 0.0 {
5839 return item.clone();
5840 }
5841
5842 match item {
5843 DisplayListItem::Rect {
5844 bounds,
5845 color,
5846 border_radius,
5847 } => DisplayListItem::Rect {
5848 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5849 color: *color,
5850 border_radius: *border_radius,
5851 },
5852 DisplayListItem::Border {
5853 bounds,
5854 widths,
5855 colors,
5856 styles,
5857 border_radius,
5858 } => DisplayListItem::Border {
5859 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5860 widths: *widths,
5861 colors: *colors,
5862 styles: *styles,
5863 border_radius: *border_radius,
5864 },
5865 DisplayListItem::Text {
5866 glyphs,
5867 font_hash,
5868 font_size_px,
5869 color,
5870 clip_rect,
5871 ..
5872 } => {
5873 let offset_glyphs: Vec<GlyphInstance> = glyphs
5874 .iter()
5875 .map(|g| GlyphInstance {
5876 index: g.index,
5877 point: LogicalPosition {
5878 x: g.point.x,
5879 y: g.point.y + y_offset,
5880 },
5881 size: g.size,
5882 })
5883 .collect();
5884 DisplayListItem::Text {
5885 glyphs: offset_glyphs,
5886 font_hash: *font_hash,
5887 font_size_px: *font_size_px,
5888 color: *color,
5889 clip_rect: offset_rect_y(clip_rect.into_inner(), y_offset).into(),
5890 source_node_index: None,
5891 }
5892 }
5893 DisplayListItem::TextLayout {
5894 layout,
5895 bounds,
5896 font_hash,
5897 font_size_px,
5898 color,
5899 } => DisplayListItem::TextLayout {
5900 layout: layout.clone(),
5901 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5902 font_hash: *font_hash,
5903 font_size_px: *font_size_px,
5904 color: *color,
5905 },
5906 DisplayListItem::Image { bounds, image, border_radius } => DisplayListItem::Image {
5907 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5908 image: image.clone(),
5909 border_radius: *border_radius,
5910 },
5911 DisplayListItem::SelectionRect {
5913 bounds,
5914 border_radius,
5915 color,
5916 } => DisplayListItem::SelectionRect {
5917 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5918 border_radius: *border_radius,
5919 color: *color,
5920 },
5921 DisplayListItem::CursorRect { bounds, color } => DisplayListItem::CursorRect {
5922 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5923 color: *color,
5924 },
5925 DisplayListItem::Underline {
5926 bounds,
5927 color,
5928 thickness,
5929 } => DisplayListItem::Underline {
5930 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5931 color: *color,
5932 thickness: *thickness,
5933 },
5934 DisplayListItem::Strikethrough {
5935 bounds,
5936 color,
5937 thickness,
5938 } => DisplayListItem::Strikethrough {
5939 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5940 color: *color,
5941 thickness: *thickness,
5942 },
5943 DisplayListItem::Overline {
5944 bounds,
5945 color,
5946 thickness,
5947 } => DisplayListItem::Overline {
5948 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5949 color: *color,
5950 thickness: *thickness,
5951 },
5952 DisplayListItem::ScrollBar {
5953 bounds,
5954 color,
5955 orientation,
5956 opacity_key,
5957 hit_id,
5958 } => DisplayListItem::ScrollBar {
5959 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5960 color: *color,
5961 orientation: *orientation,
5962 opacity_key: *opacity_key,
5963 hit_id: *hit_id,
5964 },
5965 DisplayListItem::HitTestArea { bounds, tag } => DisplayListItem::HitTestArea {
5966 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5967 tag: *tag,
5968 },
5969 DisplayListItem::PushClip {
5970 bounds,
5971 border_radius,
5972 } => DisplayListItem::PushClip {
5973 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5974 border_radius: *border_radius,
5975 },
5976 DisplayListItem::PushScrollFrame {
5977 clip_bounds,
5978 content_size,
5979 scroll_id,
5980 } => DisplayListItem::PushScrollFrame {
5981 clip_bounds: offset_rect_y(clip_bounds.into_inner(), y_offset).into(),
5982 content_size: *content_size,
5983 scroll_id: *scroll_id,
5984 },
5985 DisplayListItem::PushStackingContext { bounds, z_index } => {
5986 DisplayListItem::PushStackingContext {
5987 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5988 z_index: *z_index,
5989 }
5990 }
5991 DisplayListItem::VirtualView {
5992 child_dom_id,
5993 bounds,
5994 clip_rect,
5995 } => DisplayListItem::VirtualView {
5996 child_dom_id: *child_dom_id,
5997 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5998 clip_rect: offset_rect_y(clip_rect.into_inner(), y_offset).into(),
5999 },
6000 DisplayListItem::VirtualViewPlaceholder {
6001 node_id,
6002 bounds,
6003 clip_rect,
6004 } => DisplayListItem::VirtualViewPlaceholder {
6005 node_id: *node_id,
6006 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
6007 clip_rect: offset_rect_y(clip_rect.into_inner(), y_offset).into(),
6008 },
6009 DisplayListItem::PopClip => DisplayListItem::PopClip,
6011 DisplayListItem::PopScrollFrame => DisplayListItem::PopScrollFrame,
6012 DisplayListItem::PopStackingContext => DisplayListItem::PopStackingContext,
6013
6014 DisplayListItem::LinearGradient {
6016 bounds,
6017 gradient,
6018 border_radius,
6019 } => DisplayListItem::LinearGradient {
6020 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
6021 gradient: gradient.clone(),
6022 border_radius: *border_radius,
6023 },
6024 DisplayListItem::RadialGradient {
6025 bounds,
6026 gradient,
6027 border_radius,
6028 } => DisplayListItem::RadialGradient {
6029 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
6030 gradient: gradient.clone(),
6031 border_radius: *border_radius,
6032 },
6033 DisplayListItem::ConicGradient {
6034 bounds,
6035 gradient,
6036 border_radius,
6037 } => DisplayListItem::ConicGradient {
6038 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
6039 gradient: gradient.clone(),
6040 border_radius: *border_radius,
6041 },
6042
6043 DisplayListItem::BoxShadow {
6045 bounds,
6046 shadow,
6047 border_radius,
6048 } => DisplayListItem::BoxShadow {
6049 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
6050 shadow: *shadow,
6051 border_radius: *border_radius,
6052 },
6053
6054 DisplayListItem::PushFilter { bounds, filters } => DisplayListItem::PushFilter {
6056 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
6057 filters: filters.clone(),
6058 },
6059 DisplayListItem::PopFilter => DisplayListItem::PopFilter,
6060 DisplayListItem::PushBackdropFilter { bounds, filters } => {
6061 DisplayListItem::PushBackdropFilter {
6062 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
6063 filters: filters.clone(),
6064 }
6065 }
6066 DisplayListItem::PopBackdropFilter => DisplayListItem::PopBackdropFilter,
6067 DisplayListItem::PushOpacity { bounds, opacity } => DisplayListItem::PushOpacity {
6068 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
6069 opacity: *opacity,
6070 },
6071 DisplayListItem::PopOpacity => DisplayListItem::PopOpacity,
6072 DisplayListItem::ScrollBarStyled { info } => {
6073 let mut offset_info = (**info).clone();
6074 offset_info.bounds = offset_rect_y(offset_info.bounds.into_inner(), y_offset).into();
6075 offset_info.track_bounds = offset_rect_y(offset_info.track_bounds.into_inner(), y_offset).into();
6076 offset_info.thumb_bounds = offset_rect_y(offset_info.thumb_bounds.into_inner(), y_offset).into();
6077 if let Some(b) = offset_info.button_decrement_bounds {
6078 offset_info.button_decrement_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
6079 }
6080 if let Some(b) = offset_info.button_increment_bounds {
6081 offset_info.button_increment_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
6082 }
6083 DisplayListItem::ScrollBarStyled {
6084 info: Box::new(offset_info),
6085 }
6086 }
6087
6088 DisplayListItem::PushReferenceFrame {
6090 transform_key,
6091 initial_transform,
6092 bounds,
6093 } => DisplayListItem::PushReferenceFrame {
6094 transform_key: *transform_key,
6095 initial_transform: *initial_transform,
6096 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
6097 },
6098 DisplayListItem::PopReferenceFrame => DisplayListItem::PopReferenceFrame,
6099 DisplayListItem::PushTextShadow { shadow } => DisplayListItem::PushTextShadow {
6100 shadow: *shadow,
6101 },
6102 DisplayListItem::PopTextShadow => DisplayListItem::PopTextShadow,
6103 DisplayListItem::PushImageMaskClip {
6104 bounds,
6105 mask_image,
6106 mask_rect,
6107 } => DisplayListItem::PushImageMaskClip {
6108 bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
6109 mask_image: mask_image.clone(),
6110 mask_rect: offset_rect_y(mask_rect.into_inner(), y_offset).into(),
6111 },
6112 DisplayListItem::PopImageMaskClip => DisplayListItem::PopImageMaskClip,
6113 }
6114}
6115
6116fn generate_text_display_items(
6138 text: &str,
6139 bounds: LogicalRect,
6140 font_size: f32,
6141 color: ColorU,
6142 alignment: TextAlignment,
6143 renderer_resources: &RendererResources,
6144) -> Vec<DisplayListItem> {
6145 if text.is_empty() || font_size <= 0.0 {
6146 return Vec::new();
6147 }
6148
6149 let Some((_font_key, (font_ref, _instances))) =
6153 renderer_resources.currently_registered_fonts.iter().next()
6154 else {
6155 return Vec::new();
6156 };
6157
6158 let parsed = crate::font_ref_to_parsed_font(font_ref);
6159 let units_per_em = f32::from(parsed.font_metrics.units_per_em);
6160 if units_per_em <= 0.0 {
6161 return Vec::new();
6162 }
6163 let scale = font_size / units_per_em;
6164 let font_hash = parsed.hash;
6165
6166 let mut shaped: Vec<(u16, f32)> = Vec::new(); let mut total_width = 0.0f32;
6169 for c in text.chars() {
6170 let gid = parsed.lookup_glyph_index(c as u32).unwrap_or(0);
6171 let advance = f32::from(parsed.get_horizontal_advance(gid)) * scale;
6172 shaped.push((gid, advance));
6173 total_width += advance;
6174 }
6175
6176 if shaped.is_empty() {
6177 return Vec::new();
6178 }
6179
6180 let start_x = match alignment {
6182 TextAlignment::Center => (bounds.size.width - total_width).mul_add(0.5, bounds.origin.x),
6183 TextAlignment::Right => bounds.origin.x + (bounds.size.width - total_width),
6184 TextAlignment::Left => bounds.origin.x,
6185 };
6186
6187 let ascent_px = parsed.font_metrics.ascent * scale;
6190 let descent_px = parsed.font_metrics.descent * scale; let text_height = ascent_px - descent_px;
6192 let baseline_y =
6193 bounds.origin.y + (bounds.size.height - text_height).mul_add(0.5, ascent_px);
6194
6195 let mut pen_x = start_x;
6196 let mut glyphs: Vec<GlyphInstance> = Vec::with_capacity(shaped.len());
6197 for (gid, advance) in shaped {
6198 let size = parsed
6199 .get_glyph_size(gid, font_size)
6200 .unwrap_or(LogicalSize {
6201 width: advance,
6202 height: font_size,
6203 });
6204 glyphs.push(GlyphInstance {
6205 index: u32::from(gid),
6206 point: LogicalPosition {
6207 x: pen_x,
6208 y: baseline_y,
6209 },
6210 size,
6211 });
6212 pen_x += advance;
6213 }
6214
6215 vec![DisplayListItem::Text {
6216 glyphs,
6217 font_hash: FontHash::from_hash(font_hash),
6218 font_size_px: font_size,
6219 color,
6220 clip_rect: bounds.into(),
6221 source_node_index: None,
6222 }]
6223}
6224
6225fn calculate_display_list_height(display_list: &DisplayList) -> f32 {
6227 let mut max_bottom = 0.0f32;
6228
6229 for item in &display_list.items {
6230 if let Some(bounds) = get_display_item_bounds(item) {
6231 if bounds.0.size.height < 0.1 {
6233 continue;
6234 }
6235
6236 let item_bottom = bounds.0.origin.y + bounds.0.size.height;
6237 if item_bottom > max_bottom {
6238 max_bottom = item_bottom;
6239 }
6240 }
6241 }
6242
6243 max_bottom
6244}
6245
6246#[derive(Debug, Clone, Copy, Default)]
6248#[allow(clippy::struct_field_names)]
6250struct BreakProperties {
6251 break_before: PageBreak,
6252 break_after: PageBreak,
6253 break_inside: BreakInside,
6254}
6255
6256pub(crate) fn apply_text_overflow_ellipsis(
6292 display_list: &mut DisplayList,
6293 container_bounds: LogicalRect,
6294 _ellipsis: &str,
6295) {
6296 let container_right = container_bounds.origin.x + container_bounds.size.width;
6297
6298 for item in &mut display_list.items {
6301 if let DisplayListItem::Text {
6302 glyphs,
6303 font_size_px,
6304 clip_rect,
6305 ..
6306 } = item {
6307 if glyphs.is_empty() {
6308 continue;
6309 }
6310
6311 let last_glyph = &glyphs[glyphs.len() - 1];
6313 let last_glyph_right = last_glyph.point.x + last_glyph.size.width;
6314
6315 if last_glyph_right <= container_right {
6316 continue; }
6318
6319 let ellipsis_width = *font_size_px * APPROX_ELLIPSIS_WIDTH_RATIO;
6321 let truncation_edge = container_right - ellipsis_width;
6322
6323 let mut keep_count = 0;
6325 for (i, glyph) in glyphs.iter().enumerate() {
6326 let glyph_right = glyph.point.x + glyph.size.width;
6327 if glyph_right > truncation_edge {
6328 break;
6329 }
6330 keep_count = i + 1;
6331 }
6332
6333 glyphs.truncate(keep_count);
6335
6336 let ellipsis_x = glyphs.last().map_or(container_bounds.origin.x, |last| last.point.x + last.size.width);
6341
6342 let ellipsis_glyph = GlyphInstance {
6343 index: 0x2026, point: LogicalPosition::new(ellipsis_x, glyphs.first().map_or(
6345 container_bounds.origin.y,
6346 |g| g.point.y,
6347 )),
6348 size: LogicalSize::new(ellipsis_width, *font_size_px),
6349 };
6350
6351 glyphs.push(ellipsis_glyph);
6352
6353 *clip_rect = container_bounds.into();
6356 }
6357 }
6358}
6359
6360#[allow(clippy::many_single_char_names)] pub(crate) fn resolve_clip_path(
6390 clip_path: &azul_css::props::layout::shape::ClipPath,
6391 node_bounds: LogicalRect,
6392) -> Option<(LogicalRect, f32)> {
6393 use azul_css::props::layout::shape::ClipPath;
6394 use azul_css::shape::CssShape;
6395
6396 match clip_path {
6397 ClipPath::None => None,
6398 ClipPath::Shape(shape) => {
6399 match shape {
6400 CssShape::Inset(inset) => {
6401 let x = node_bounds.origin.x + inset.inset_left;
6404 let y = node_bounds.origin.y + inset.inset_top;
6405 let w = (node_bounds.size.width - inset.inset_left - inset.inset_right).max(0.0);
6406 let h = (node_bounds.size.height - inset.inset_top - inset.inset_bottom).max(0.0);
6407 let radius = match inset.border_radius {
6408 azul_css::corety::OptionF32::Some(r) => r,
6409 azul_css::corety::OptionF32::None => 0.0,
6410 };
6411 Some((LogicalRect {
6412 origin: LogicalPosition::new(x, y),
6413 size: LogicalSize::new(w, h),
6414 }, radius))
6415 }
6416 CssShape::Circle(circle) => {
6417 let cx = node_bounds.origin.x + circle.center.x;
6421 let cy = node_bounds.origin.y + circle.center.y;
6422 let r = circle.radius;
6423 Some((LogicalRect {
6424 origin: LogicalPosition::new(cx - r, cy - r),
6425 size: LogicalSize::new(r * 2.0, r * 2.0),
6426 }, r))
6427 }
6428 CssShape::Ellipse(ellipse) => {
6429 let cx = node_bounds.origin.x + ellipse.center.x;
6431 let cy = node_bounds.origin.y + ellipse.center.y;
6432 let rx = ellipse.radius_x;
6433 let ry = ellipse.radius_y;
6434 let radius = rx.min(ry);
6435 Some((LogicalRect {
6436 origin: LogicalPosition::new(cx - rx, cy - ry),
6437 size: LogicalSize::new(rx * 2.0, ry * 2.0),
6438 }, radius))
6439 }
6440 CssShape::Polygon(polygon) => {
6441 if polygon.points.is_empty() {
6443 return None;
6444 }
6445 let mut min_x = f32::INFINITY;
6446 let mut min_y = f32::INFINITY;
6447 let mut max_x = f32::NEG_INFINITY;
6448 let mut max_y = f32::NEG_INFINITY;
6449 for point in &polygon.points {
6450 let px = node_bounds.origin.x + point.x;
6452 let py = node_bounds.origin.y + point.y;
6453 min_x = min_x.min(px);
6454 min_y = min_y.min(py);
6455 max_x = max_x.max(px);
6456 max_y = max_y.max(py);
6457 }
6458 Some((LogicalRect {
6459 origin: LogicalPosition::new(min_x, min_y),
6460 size: LogicalSize::new((max_x - min_x).max(0.0), (max_y - min_y).max(0.0)),
6461 }, 0.0))
6462 }
6463 CssShape::Path(_) => {
6464 None
6467 }
6468 }
6469 }
6470 }
6471}
6472
6473pub(crate) fn apply_clip_path(
6485 display_list: &mut DisplayList,
6486 start_index: usize,
6487 clip_rect: LogicalRect,
6488 border_radius: f32,
6489) {
6490 let br = if border_radius > 0.0 {
6491 BorderRadius {
6492 top_left: border_radius,
6493 top_right: border_radius,
6494 bottom_left: border_radius,
6495 bottom_right: border_radius,
6496 }
6497 } else {
6498 BorderRadius::default()
6499 };
6500
6501 display_list.items.insert(start_index, DisplayListItem::PushClip {
6503 bounds: clip_rect.into(),
6504 border_radius: br,
6505 });
6506 if display_list.node_mapping.len() >= start_index {
6508 display_list.node_mapping.insert(start_index, None);
6509 }
6510
6511 display_list.items.push(DisplayListItem::PopClip);
6513 display_list.node_mapping.push(None);
6514}
6515
6516#[cfg(feature = "cpurender")]
6520#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] fn rasterize_svg_clip_to_r8(
6522 svg_clip: &azul_core::svg::SvgMultiPolygon,
6523 paint_rect: &LogicalRect,
6524) -> Option<ImageRef> {
6525 use agg_rust::{
6526 basics::FillingRule,
6527 color::Rgba8,
6528 path_storage::PathStorage,
6529 pixfmt_rgba::PixfmtRgba32,
6530 rasterizer_scanline_aa::RasterizerScanlineAa,
6531 renderer_base::RendererBase,
6532 renderer_scanline::render_scanlines_aa_solid,
6533 rendering_buffer::RowAccessor,
6534 scanline_u::ScanlineU8,
6535 };
6536 use azul_core::resources::{ImageRef, RawImage, RawImageFormat, RawImageData};
6537
6538 let w = paint_rect.size.width.ceil() as u32;
6539 let h = paint_rect.size.height.ceil() as u32;
6540 if w == 0 || h == 0 {
6541 return None;
6542 }
6543
6544 let mut path = PathStorage::new();
6546 for ring in svg_clip.rings.as_ref() {
6547 let mut first = true;
6548 for item in ring.items.as_ref() {
6549 match item {
6550 azul_core::svg::SvgPathElement::Line(l) => {
6551 if first {
6552 path.move_to(
6553 f64::from(l.start.x - paint_rect.origin.x),
6554 f64::from(l.start.y - paint_rect.origin.y),
6555 );
6556 first = false;
6557 }
6558 path.line_to(
6559 f64::from(l.end.x - paint_rect.origin.x),
6560 f64::from(l.end.y - paint_rect.origin.y),
6561 );
6562 }
6563 azul_core::svg::SvgPathElement::QuadraticCurve(q) => {
6564 if first {
6565 path.move_to(
6566 f64::from(q.start.x - paint_rect.origin.x),
6567 f64::from(q.start.y - paint_rect.origin.y),
6568 );
6569 first = false;
6570 }
6571 path.curve3(
6572 f64::from(q.ctrl.x - paint_rect.origin.x),
6573 f64::from(q.ctrl.y - paint_rect.origin.y),
6574 f64::from(q.end.x - paint_rect.origin.x),
6575 f64::from(q.end.y - paint_rect.origin.y),
6576 );
6577 }
6578 azul_core::svg::SvgPathElement::CubicCurve(c) => {
6579 if first {
6580 path.move_to(
6581 f64::from(c.start.x - paint_rect.origin.x),
6582 f64::from(c.start.y - paint_rect.origin.y),
6583 );
6584 first = false;
6585 }
6586 path.curve4(
6587 f64::from(c.ctrl_1.x - paint_rect.origin.x),
6588 f64::from(c.ctrl_1.y - paint_rect.origin.y),
6589 f64::from(c.ctrl_2.x - paint_rect.origin.x),
6590 f64::from(c.ctrl_2.y - paint_rect.origin.y),
6591 f64::from(c.end.x - paint_rect.origin.x),
6592 f64::from(c.end.y - paint_rect.origin.y),
6593 );
6594 }
6595 }
6596 }
6597 }
6598
6599 let mut rgba_buf = vec![0u8; (w * h * 4) as usize];
6601 {
6602 let stride = (w * 4) as i32;
6603 let mut ra = unsafe {
6604 RowAccessor::new_with_buf(rgba_buf.as_mut_ptr(), w, h, stride)
6605 };
6606 let pf = PixfmtRgba32::new(&mut ra);
6607 let mut rb = RendererBase::new(pf);
6608
6609 let mut ras = RasterizerScanlineAa::new();
6610 ras.filling_rule(FillingRule::NonZero);
6611 ras.add_path(&mut path, 0);
6612
6613 let mut sl = ScanlineU8::new();
6614 let white = Rgba8 { r: 255, g: 255, b: 255, a: 255 };
6615 render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &white);
6616 }
6617
6618 let r8_data: Vec<u8> = rgba_buf.chunks_exact(4).map(|px| px[3]).collect();
6620
6621 ImageRef::new_rawimage(RawImage {
6622 pixels: RawImageData::U8(r8_data.into()),
6623 width: w as usize,
6624 height: h as usize,
6625 premultiplied_alpha: false,
6626 data_format: RawImageFormat::R8,
6627 tag: Vec::new().into(),
6628 })
6629}
6630
6631#[cfg(test)]
6632mod pagination_text_tests {
6633 use super::*;
6634 use crate::font::parsed::ParsedFont;
6635 use azul_core::resources::{FontKey, IdNamespace};
6636
6637 fn load_test_font() -> Option<ParsedFont> {
6639 let candidates = [
6640 "/System/Library/Fonts/Supplemental/Times New Roman.ttf",
6641 "/System/Library/Fonts/Helvetica.ttc",
6642 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
6643 "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
6644 "C:/Windows/Fonts/arial.ttf",
6645 ];
6646 for path in candidates {
6647 if let Ok(bytes) = std::fs::read(path) {
6648 let arc = Arc::new(rust_fontconfig::FontBytes::Owned(
6649 Arc::from(bytes.as_slice()),
6650 ));
6651 if let Some(font) =
6652 ParsedFont::from_bytes(&bytes, 0, &mut Vec::new()).map(|f| f.with_source_bytes(arc))
6653 {
6654 return Some(font);
6655 }
6656 }
6657 }
6658 None
6659 }
6660
6661 fn renderer_resources_with(font: ParsedFont) -> RendererResources {
6662 let mut rr = RendererResources::default();
6663 let font_ref = crate::parsed_font_to_font_ref(font);
6664 let key = FontKey::unique(IdNamespace(0));
6665 let hash = crate::font_ref_to_parsed_font(&font_ref).hash;
6666 rr.font_hash_map.insert(hash, key);
6667 rr.currently_registered_fonts
6668 .insert(key, (font_ref, BTreeMap::default()));
6669 rr
6670 }
6671
6672 #[test]
6675 fn generate_text_display_items_emits_glyphs() {
6676 let Some(font) = load_test_font() else {
6677 eprintln!("[skip] no system font available");
6678 return;
6679 };
6680 let expected_hash = font.hash;
6681 let rr = renderer_resources_with(font);
6682
6683 let bounds = LogicalRect {
6684 origin: LogicalPosition { x: 0.0, y: 0.0 },
6685 size: LogicalSize { width: 400.0, height: 30.0 },
6686 };
6687 let items = generate_text_display_items(
6688 "Page 1 of 3",
6689 bounds,
6690 14.0,
6691 ColorU { r: 0, g: 0, b: 0, a: 255 },
6692 TextAlignment::Center,
6693 &rr,
6694 );
6695
6696 assert_eq!(items.len(), 1, "expected exactly one Text item");
6697 match &items[0] {
6698 DisplayListItem::Text { glyphs, font_hash, color, .. } => {
6699 assert_eq!(glyphs.len(), "Page 1 of 3".chars().count());
6700 assert_eq!(font_hash.font_hash, expected_hash, "must use registered font hash");
6701 assert_ne!(font_hash.font_hash, 0, "hash 0 resolves no font");
6702 assert_eq!(color.a, 255);
6703 let p_gid = glyphs[0].index;
6705 assert_ne!(p_gid, 'P' as u32, "glyph index must be a GID, not a codepoint");
6706 assert!(glyphs[1].point.x > glyphs[0].point.x, "pen did not advance");
6708 }
6709 other => panic!("expected DisplayListItem::Text, got {other:?}"),
6710 }
6711 }
6712
6713 #[test]
6715 fn generate_text_display_items_empty_without_font() {
6716 let rr = RendererResources::default();
6717 let bounds = LogicalRect {
6718 origin: LogicalPosition { x: 0.0, y: 0.0 },
6719 size: LogicalSize { width: 400.0, height: 30.0 },
6720 };
6721 let items = generate_text_display_items(
6722 "Header",
6723 bounds,
6724 14.0,
6725 ColorU { r: 0, g: 0, b: 0, a: 255 },
6726 TextAlignment::Center,
6727 &rr,
6728 );
6729 assert!(items.is_empty());
6730 }
6731}
6732
6733#[cfg(test)]
6734#[allow(clippy::float_cmp, clippy::too_many_lines)]
6735mod autotest_generated {
6736 use super::*;
6737
6738 fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
6743 LogicalRect::new(LogicalPosition::new(x, y), LogicalSize::new(w, h))
6744 }
6745
6746 fn opaque() -> ColorU {
6747 ColorU { r: 10, g: 20, b: 30, a: 255 }
6748 }
6749
6750 fn glyph(index: u32, x: f32, y: f32) -> GlyphInstance {
6751 GlyphInstance {
6752 index,
6753 point: LogicalPosition::new(x, y),
6754 size: LogicalSize::new(8.0, 12.0),
6755 }
6756 }
6757
6758 fn no_widths() -> StyleBorderWidths {
6759 StyleBorderWidths { top: None, right: None, bottom: None, left: None }
6760 }
6761
6762 fn all_widths() -> StyleBorderWidths {
6763 StyleBorderWidths {
6764 top: Some(CssPropertyValue::Exact(LayoutBorderTopWidth::default())),
6765 right: Some(CssPropertyValue::Exact(LayoutBorderRightWidth::default())),
6766 bottom: Some(CssPropertyValue::Exact(LayoutBorderBottomWidth::default())),
6767 left: Some(CssPropertyValue::Exact(LayoutBorderLeftWidth::default())),
6768 }
6769 }
6770
6771 fn no_colors() -> StyleBorderColors {
6772 StyleBorderColors { top: None, right: None, bottom: None, left: None }
6773 }
6774
6775 fn no_styles() -> StyleBorderStyles {
6776 StyleBorderStyles { top: None, right: None, bottom: None, left: None }
6777 }
6778
6779 fn all_styles() -> StyleBorderStyles {
6780 StyleBorderStyles {
6781 top: Some(CssPropertyValue::Exact(StyleBorderTopStyle::default())),
6782 right: Some(CssPropertyValue::Exact(StyleBorderRightStyle::default())),
6783 bottom: Some(CssPropertyValue::Exact(StyleBorderBottomStyle::default())),
6784 left: Some(CssPropertyValue::Exact(StyleBorderLeftStyle::default())),
6785 }
6786 }
6787
6788 fn zero_style_radius() -> StyleBorderRadius {
6789 StyleBorderRadius {
6790 top_left: PixelValue::zero(),
6791 top_right: PixelValue::zero(),
6792 bottom_left: PixelValue::zero(),
6793 bottom_right: PixelValue::zero(),
6794 }
6795 }
6796
6797 fn test_image() -> ImageRef {
6798 ImageRef::null_image(4, 4, azul_core::resources::RawImageFormat::RGBA8, Vec::new())
6799 }
6800
6801 fn text_item(src: Option<usize>, clip: LogicalRect, glyphs: Vec<GlyphInstance>) -> DisplayListItem {
6802 DisplayListItem::Text {
6803 glyphs,
6804 font_hash: FontHash::from_hash(7),
6805 font_size_px: 16.0,
6806 color: opaque(),
6807 clip_rect: clip.into(),
6808 source_node_index: src,
6809 }
6810 }
6811
6812 fn list_of(items: Vec<DisplayListItem>) -> DisplayList {
6813 let node_mapping = vec![None; items.len()];
6814 DisplayList { items, node_mapping, ..DisplayList::default() }
6815 }
6816
6817 #[cfg(feature = "text_layout")]
6818 fn positioned(line_index: usize, x: f32, y: f32, w: f32, h: f32) -> PositionedItem {
6819 PositionedItem {
6820 item: crate::text3::cache::ShapedItem::Tab {
6821 source: azul_core::selection::ContentIndex { run_index: 0, item_index: 0 },
6822 bounds: crate::text3::cache::Rect { x: 0.0, y: 0.0, width: w, height: h },
6823 },
6824 position: crate::text3::cache::Point { x, y },
6825 line_index,
6826 }
6827 }
6828
6829 #[test]
6834 fn window_logical_rect_accessors_roundtrip() {
6835 let origin = LogicalPosition::new(-3.5, 12.25);
6836 let size = LogicalSize::new(100.0, 40.0);
6837 let w = WindowLogicalRect::new(origin, size);
6838
6839 assert_eq!(w.origin(), origin);
6840 assert_eq!(w.size(), size);
6841 assert_eq!(*w.inner(), LogicalRect::new(origin, size));
6842 assert_eq!(w.into_inner(), LogicalRect::new(origin, size));
6843 assert_eq!(WindowLogicalRect::from(w.into_inner()), w);
6845 assert_eq!(LogicalRect::from(w), w.into_inner());
6846 }
6847
6848 #[test]
6849 fn window_logical_rect_zero_is_neutral() {
6850 let z = WindowLogicalRect::zero();
6851 assert_eq!(z, WindowLogicalRect::default());
6852 assert_eq!(z.origin(), LogicalPosition::zero());
6853 assert_eq!(z.size(), LogicalSize::zero());
6854 assert_eq!(z.into_inner(), LogicalRect::zero());
6855 }
6856
6857 #[test]
6858 fn window_logical_rect_extreme_values_do_not_panic() {
6859 for (x, y, w, h) in [
6860 (f32::MAX, f32::MAX, f32::MAX, f32::MAX),
6861 (f32::MIN, f32::MIN, 0.0, 0.0),
6862 (f32::INFINITY, f32::NEG_INFINITY, f32::INFINITY, 0.0),
6863 (f32::NAN, f32::NAN, f32::NAN, f32::NAN),
6864 ] {
6865 let r = WindowLogicalRect::new(LogicalPosition::new(x, y), LogicalSize::new(w, h));
6866 assert_eq!(r.origin().x.to_bits(), x.to_bits());
6868 assert_eq!(r.size().height.to_bits(), h.to_bits());
6869 let _ = format!("{r:?}");
6870 }
6871 }
6872
6873 #[test]
6874 fn border_box_to_content_box_subtracts_padding_and_border() {
6875 let bb = BorderBoxRect(rect(10.0, 20.0, 100.0, 50.0));
6876 let padding = crate::solver3::geometry::EdgeSizes { top: 1.0, right: 2.0, bottom: 3.0, left: 4.0 };
6877 let border = crate::solver3::geometry::EdgeSizes { top: 5.0, right: 6.0, bottom: 7.0, left: 8.0 };
6878
6879 let cb = bb.to_content_box(&padding, &border);
6880 assert_eq!(cb.rect(), rect(22.0, 26.0, 80.0, 34.0));
6881 assert_eq!(bb.rect(), rect(10.0, 20.0, 100.0, 50.0), "receiver copy is unchanged");
6882 }
6883
6884 #[test]
6885 fn border_box_to_content_box_zero_edges_is_identity() {
6886 let bb = BorderBoxRect(rect(1.0, 2.0, 3.0, 4.0));
6887 let zero = crate::solver3::geometry::EdgeSizes::default();
6888 assert_eq!(bb.to_content_box(&zero, &zero).rect(), bb.rect());
6889 }
6890
6891 #[test]
6892 fn border_box_to_content_box_overinset_yields_negative_size_not_a_panic() {
6893 let bb = BorderBoxRect(rect(0.0, 0.0, 10.0, 10.0));
6897 let big = crate::solver3::geometry::EdgeSizes { top: 50.0, right: 50.0, bottom: 50.0, left: 50.0 };
6898 let cb = bb.to_content_box(&big, &big);
6899 assert!(cb.rect().size.width < 0.0);
6900 assert!(cb.rect().size.height < 0.0);
6901 }
6902
6903 #[test]
6904 fn border_box_to_content_box_nan_and_inf_do_not_panic() {
6905 let bb = BorderBoxRect(rect(0.0, 0.0, f32::MAX, f32::MAX));
6906 let nan = crate::solver3::geometry::EdgeSizes {
6907 top: f32::NAN, right: f32::NAN, bottom: f32::NAN, left: f32::NAN,
6908 };
6909 let inf = crate::solver3::geometry::EdgeSizes {
6910 top: f32::INFINITY, right: f32::INFINITY, bottom: f32::INFINITY, left: f32::INFINITY,
6911 };
6912 assert!(bb.to_content_box(&nan, &nan).rect().size.width.is_nan());
6913 assert!(bb.to_content_box(&inf, &inf).rect().size.width.is_infinite());
6915 }
6916
6917 #[test]
6918 fn content_box_rect_getter_returns_wrapped_rect() {
6919 let r = rect(-1.0, -2.0, 0.0, 0.0);
6920 assert_eq!(ContentBoxRect(r).rect(), r);
6921 assert_eq!(ContentBoxRect(LogicalRect::zero()).rect(), LogicalRect::zero());
6922 }
6923
6924 #[test]
6929 fn border_radius_is_zero_basic() {
6930 assert!(BorderRadius::default().is_zero());
6931 assert!(!BorderRadius { top_left: 1.0, ..BorderRadius::default() }.is_zero());
6932 assert!(!BorderRadius { bottom_right: 0.001, ..BorderRadius::default() }.is_zero());
6933 }
6934
6935 #[test]
6936 fn border_radius_is_zero_edge_floats() {
6937 assert!(BorderRadius { top_left: -0.0, top_right: -0.0, bottom_left: -0.0, bottom_right: -0.0 }.is_zero());
6939 assert!(!BorderRadius { top_left: f32::NAN, ..BorderRadius::default() }.is_zero());
6941 assert!(!BorderRadius { top_right: f32::INFINITY, ..BorderRadius::default() }.is_zero());
6942 assert!(!BorderRadius { bottom_left: -5.0, ..BorderRadius::default() }.is_zero());
6944 }
6945
6946 #[test]
6951 fn is_state_management_true_for_push_pop_only() {
6952 let state = [
6953 DisplayListItem::PushClip { bounds: rect(0.0, 0.0, 1.0, 1.0).into(), border_radius: BorderRadius::default() },
6954 DisplayListItem::PopClip,
6955 DisplayListItem::PopImageMaskClip,
6956 DisplayListItem::PopScrollFrame,
6957 DisplayListItem::PopStackingContext,
6958 DisplayListItem::PopReferenceFrame,
6959 DisplayListItem::PopFilter,
6960 DisplayListItem::PopBackdropFilter,
6961 DisplayListItem::PopOpacity,
6962 DisplayListItem::PopTextShadow,
6963 DisplayListItem::PushStackingContext { z_index: 0, bounds: WindowLogicalRect::zero() },
6964 DisplayListItem::PushOpacity { bounds: WindowLogicalRect::zero(), opacity: 0.5 },
6965 DisplayListItem::PushTextShadow { shadow: StyleBoxShadow::default() },
6966 ];
6967 for item in &state {
6968 assert!(item.is_state_management(), "{item:?} must be state management");
6969 }
6970
6971 let drawing = [
6972 DisplayListItem::Rect { bounds: WindowLogicalRect::zero(), color: opaque(), border_radius: BorderRadius::default() },
6973 DisplayListItem::CursorRect { bounds: WindowLogicalRect::zero(), color: opaque() },
6974 DisplayListItem::HitTestArea { bounds: WindowLogicalRect::zero(), tag: (0, TAG_TYPE_DOM_NODE) },
6976 text_item(None, LogicalRect::zero(), Vec::new()),
6977 ];
6978 for item in &drawing {
6979 assert!(!item.is_state_management(), "{item:?} must NOT be state management");
6980 }
6981 }
6982
6983 #[test]
6984 fn bounds_reports_none_only_for_pop_and_text_shadow() {
6985 let r = rect(1.0, 2.0, 3.0, 4.0);
6986 assert_eq!(
6987 DisplayListItem::Rect { bounds: r.into(), color: opaque(), border_radius: BorderRadius::default() }.bounds(),
6988 Some(r)
6989 );
6990 assert_eq!(text_item(Some(0), r, vec![glyph(1, 999.0, 999.0)]).bounds(), Some(r));
6992 assert_eq!(
6993 DisplayListItem::PushScrollFrame {
6994 clip_bounds: r.into(),
6995 content_size: LogicalSize::new(9.0, 9.0),
6996 scroll_id: 3,
6997 }.bounds(),
6998 Some(r)
6999 );
7000 assert_eq!(DisplayListItem::PopClip.bounds(), None);
7001 assert_eq!(DisplayListItem::PopOpacity.bounds(), None);
7002 assert_eq!(
7003 DisplayListItem::PushTextShadow { shadow: StyleBoxShadow::default() }.bounds(),
7004 None,
7005 "a text shadow has no bounds of its own"
7006 );
7007 }
7008
7009 #[test]
7010 fn bounds_on_degenerate_rects_does_not_panic() {
7011 for r in [
7012 LogicalRect::zero(),
7013 rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
7014 rect(f32::MIN, f32::MIN, f32::MAX, f32::MAX),
7015 rect(0.0, 0.0, -10.0, -10.0),
7016 ] {
7017 let item = DisplayListItem::Rect { bounds: r.into(), color: opaque(), border_radius: BorderRadius::default() };
7018 assert!(item.bounds().is_some());
7019 assert!(item.visual_bounds().is_some());
7020 }
7021 }
7022
7023 #[test]
7024 fn visual_bounds_matches_bounds_for_non_shadow_items() {
7025 let r = rect(5.0, 6.0, 7.0, 8.0);
7026 let item = DisplayListItem::Rect { bounds: r.into(), color: opaque(), border_radius: BorderRadius::default() };
7027 assert_eq!(item.visual_bounds(), item.bounds());
7028 assert_eq!(DisplayListItem::PopClip.visual_bounds(), None);
7029 }
7030
7031 #[test]
7032 fn visual_bounds_expands_box_shadow_by_offset_blur_and_spread() {
7033 use azul_css::props::basic::pixel::PixelValueNoPercent;
7034 let shadow = StyleBoxShadow {
7035 offset_x: PixelValueNoPercent { inner: PixelValue::const_px(2) },
7036 offset_y: PixelValueNoPercent { inner: PixelValue::const_px(3) },
7037 blur_radius: PixelValueNoPercent { inner: PixelValue::const_px(4) },
7038 spread_radius: PixelValueNoPercent { inner: PixelValue::const_px(5) },
7039 clip_mode: BoxShadowClipMode::default(),
7040 color: ColorU::BLACK,
7041 };
7042 let item = DisplayListItem::BoxShadow {
7043 bounds: rect(100.0, 100.0, 50.0, 50.0).into(),
7044 shadow,
7045 border_radius: BorderRadius::default(),
7046 };
7047 let vb = item.visual_bounds().expect("box shadow has visual bounds");
7049 assert_eq!(vb, rect(86.0, 86.0, 78.0, 78.0));
7050 let b = item.bounds().unwrap();
7052 assert!(vb.origin.x < b.origin.x && vb.size.width > b.size.width);
7053 }
7054
7055 #[test]
7056 fn visual_bounds_box_shadow_with_negative_offsets_uses_absolute_values() {
7057 use azul_css::props::basic::pixel::PixelValueNoPercent;
7058 let shadow = StyleBoxShadow {
7059 offset_x: PixelValueNoPercent { inner: PixelValue::const_px(-10) },
7060 offset_y: PixelValueNoPercent { inner: PixelValue::const_px(-10) },
7061 blur_radius: PixelValueNoPercent { inner: PixelValue::const_px(0) },
7062 spread_radius: PixelValueNoPercent { inner: PixelValue::const_px(0) },
7063 clip_mode: BoxShadowClipMode::default(),
7064 color: ColorU::BLACK,
7065 };
7066 let item = DisplayListItem::BoxShadow {
7067 bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
7068 shadow,
7069 border_radius: BorderRadius::default(),
7070 };
7071 assert_eq!(item.visual_bounds().unwrap(), rect(-20.0, -20.0, 50.0, 50.0));
7073 }
7074
7075 #[test]
7080 fn is_visually_equal_reflexive_and_discriminant_guarded() {
7081 let a = DisplayListItem::Rect {
7082 bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
7083 color: opaque(),
7084 border_radius: BorderRadius::default(),
7085 };
7086 assert!(a.is_visually_equal(&a));
7087 assert!(!a.is_visually_equal(&DisplayListItem::PopClip), "different variants are never equal");
7088 assert!(!DisplayListItem::PopClip.is_visually_equal(&a));
7089 }
7090
7091 #[test]
7092 fn is_visually_equal_detects_field_changes() {
7093 let base = DisplayListItem::Rect {
7094 bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
7095 color: opaque(),
7096 border_radius: BorderRadius::default(),
7097 };
7098 let moved = DisplayListItem::Rect {
7099 bounds: rect(1.0, 0.0, 10.0, 10.0).into(),
7100 color: opaque(),
7101 border_radius: BorderRadius::default(),
7102 };
7103 let recolored = DisplayListItem::Rect {
7104 bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
7105 color: ColorU { r: 255, g: 0, b: 0, a: 255 },
7106 border_radius: BorderRadius::default(),
7107 };
7108 let rounded = DisplayListItem::Rect {
7109 bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
7110 color: opaque(),
7111 border_radius: BorderRadius { top_left: 4.0, ..BorderRadius::default() },
7112 };
7113 assert!(!base.is_visually_equal(&moved));
7114 assert!(!base.is_visually_equal(&recolored));
7115 assert!(!base.is_visually_equal(&rounded));
7116 }
7117
7118 #[test]
7119 fn is_visually_equal_pops_are_always_equal() {
7120 for (a, b) in [
7121 (DisplayListItem::PopClip, DisplayListItem::PopClip),
7122 (DisplayListItem::PopScrollFrame, DisplayListItem::PopScrollFrame),
7123 (DisplayListItem::PopOpacity, DisplayListItem::PopOpacity),
7124 (DisplayListItem::PopTextShadow, DisplayListItem::PopTextShadow),
7125 ] {
7126 assert!(a.is_visually_equal(&b));
7127 }
7128 assert!(!DisplayListItem::PopClip.is_visually_equal(&DisplayListItem::PopOpacity));
7129 }
7130
7131 #[test]
7132 fn is_visually_equal_hit_test_areas_never_damage() {
7133 let a = DisplayListItem::HitTestArea { bounds: rect(0.0, 0.0, 1.0, 1.0).into(), tag: (1, TAG_TYPE_DOM_NODE) };
7136 let b = DisplayListItem::HitTestArea { bounds: rect(500.0, 900.0, 7.0, 7.0).into(), tag: (99, TAG_TYPE_CURSOR) };
7137 assert!(a.is_visually_equal(&b));
7138 }
7139
7140 #[test]
7141 fn is_visually_equal_text_layout_uses_arc_pointer_identity() {
7142 let shared: Arc<dyn std::any::Any + Send + Sync> = Arc::new(42u32);
7143 let other: Arc<dyn std::any::Any + Send + Sync> = Arc::new(42u32);
7144 let make = |layout: Arc<dyn std::any::Any + Send + Sync>| DisplayListItem::TextLayout {
7145 layout,
7146 bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
7147 font_hash: FontHash::from_hash(1),
7148 font_size_px: 16.0,
7149 color: opaque(),
7150 };
7151 assert!(make(shared.clone()).is_visually_equal(&make(shared)), "same Arc => reuse => no damage");
7152 assert!(
7153 !make(Arc::new(42u32)).is_visually_equal(&make(other)),
7154 "distinct allocations => conservatively different"
7155 );
7156 }
7157
7158 #[test]
7159 fn is_visually_equal_image_uses_pointer_identity_not_content() {
7160 let img = test_image();
7161 let a = DisplayListItem::Image {
7162 bounds: rect(0.0, 0.0, 4.0, 4.0).into(),
7163 image: img.clone(),
7164 border_radius: BorderRadius::default(),
7165 };
7166 let same_alloc = DisplayListItem::Image {
7167 bounds: rect(0.0, 0.0, 4.0, 4.0).into(),
7168 image: img,
7169 border_radius: BorderRadius::default(),
7170 };
7171 assert!(a.is_visually_equal(&same_alloc));
7172
7173 let b = DisplayListItem::Image {
7175 bounds: rect(0.0, 0.0, 4.0, 4.0).into(),
7176 image: test_image(),
7177 border_radius: BorderRadius::default(),
7178 };
7179 assert!(!a.is_visually_equal(&b));
7180 }
7181
7182 #[test]
7183 fn is_visually_equal_text_compares_glyph_ids_and_positions() {
7184 let clip = rect(0.0, 0.0, 100.0, 20.0);
7185 let a = text_item(Some(1), clip, vec![glyph(5, 0.0, 10.0), glyph(6, 8.0, 10.0)]);
7186 let same = text_item(Some(999), clip, vec![glyph(5, 0.0, 10.0), glyph(6, 8.0, 10.0)]);
7187 let diff_gid = text_item(Some(1), clip, vec![glyph(5, 0.0, 10.0), glyph(7, 8.0, 10.0)]);
7188 let diff_pos = text_item(Some(1), clip, vec![glyph(5, 0.0, 10.0), glyph(6, 9.0, 10.0)]);
7189 let shorter = text_item(Some(1), clip, vec![glyph(5, 0.0, 10.0)]);
7190 let empty = text_item(Some(1), clip, Vec::new());
7191
7192 assert!(a.is_visually_equal(&same), "source_node_index is not a visual property");
7193 assert!(!a.is_visually_equal(&diff_gid));
7194 assert!(!a.is_visually_equal(&diff_pos));
7195 assert!(!a.is_visually_equal(&shorter), "glyph count mismatch => different");
7196 assert!(!a.is_visually_equal(&empty));
7197 assert!(empty.is_visually_equal(&empty), "empty glyph runs are equal to each other");
7198 }
7199
7200 #[test]
7201 fn is_visually_equal_nan_thickness_is_conservatively_unequal() {
7202 let a = DisplayListItem::Underline {
7206 bounds: rect(0.0, 0.0, 10.0, 1.0).into(),
7207 color: opaque(),
7208 thickness: f32::NAN,
7209 };
7210 assert!(!a.is_visually_equal(&a));
7211 }
7212
7213 #[test]
7214 fn is_visually_equal_bounds_are_quantized_to_a_thousandth_of_a_pixel() {
7215 let a = DisplayListItem::Rect {
7218 bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
7219 color: opaque(),
7220 border_radius: BorderRadius::default(),
7221 };
7222 let jittered = DisplayListItem::Rect {
7223 bounds: rect(0.000_01, 0.0, 10.0, 10.0).into(),
7224 color: opaque(),
7225 border_radius: BorderRadius::default(),
7226 };
7227 assert!(a.is_visually_equal(&jittered));
7228
7229 let moved = DisplayListItem::Rect {
7231 bounds: rect(1.0, 0.0, 10.0, 10.0).into(),
7232 color: opaque(),
7233 border_radius: BorderRadius::default(),
7234 };
7235 assert!(!a.is_visually_equal(&moved));
7236 }
7237
7238 #[test]
7243 fn builder_new_is_empty_and_matches_with_debug_false() {
7244 let b = DisplayListBuilder::new();
7245 assert!(b.items.is_empty());
7246 assert!(b.node_mapping.is_empty());
7247 assert!(!b.debug_enabled);
7248 assert!(b.forced_page_breaks.is_empty());
7249 assert!(b.fixed_position_item_ranges.is_empty());
7250 assert!(b.fixed_position_start.is_none());
7251
7252 let dl = DisplayListBuilder::new().build();
7253 assert!(dl.items.is_empty());
7254 assert!(dl.node_mapping.is_empty());
7255 }
7256
7257 #[test]
7258 fn builder_with_debug_toggles_message_collection() {
7259 let mut off = DisplayListBuilder::with_debug(false);
7260 off.debug_log("dropped".to_string());
7261 assert!(off.debug_messages.is_empty());
7262
7263 let mut on = DisplayListBuilder::with_debug(true);
7264 on.debug_log(String::new());
7265 on.debug_log("x".repeat(100_000)); on.debug_log("🦀 unicode \u{0}\u{FFFD} nul".to_string());
7267 assert_eq!(on.debug_messages.len(), 3);
7268 }
7269
7270 #[test]
7271 fn builder_push_item_keeps_node_mapping_in_lockstep() {
7272 let mut b = DisplayListBuilder::new();
7273 b.set_current_node(Some(NodeId::new(4)));
7274 b.push_rect(rect(0.0, 0.0, 1.0, 1.0), opaque(), BorderRadius::default());
7275 b.set_current_node(None);
7276 b.pop_clip();
7277
7278 let dl = b.build();
7279 assert_eq!(dl.items.len(), 2);
7280 assert_eq!(dl.items.len(), dl.node_mapping.len(), "node_mapping must parallel items");
7281 assert_eq!(dl.node_mapping[0], Some(NodeId::new(4)));
7282 assert_eq!(dl.node_mapping[1], None);
7283 }
7284
7285 #[test]
7286 fn builder_skips_fully_transparent_fills() {
7287 let mut b = DisplayListBuilder::new();
7288 b.push_rect(rect(0.0, 0.0, 10.0, 10.0), ColorU::TRANSPARENT, BorderRadius::default());
7289 b.push_selection_rect(rect(0.0, 0.0, 10.0, 10.0), ColorU::TRANSPARENT, BorderRadius::default());
7290 b.push_scrollbar(rect(0.0, 0.0, 10.0, 10.0), ColorU::TRANSPARENT, ScrollbarOrientation::Vertical, None, None);
7291 assert!(b.items.is_empty(), "alpha == 0 with no opacity key paints nothing");
7292
7293 b.push_scrollbar(
7295 rect(0.0, 0.0, 10.0, 10.0),
7296 ColorU::TRANSPARENT,
7297 ScrollbarOrientation::Vertical,
7298 Some(OpacityKey::unique()),
7299 None,
7300 );
7301 assert_eq!(b.items.len(), 1);
7302 }
7303
7304 #[test]
7305 fn builder_cursor_rect_is_emitted_even_when_invisible() {
7306 let mut b = DisplayListBuilder::new();
7309 b.push_cursor_rect(rect(0.0, 0.0, 1.0, 16.0), ColorU::TRANSPARENT);
7310 assert_eq!(b.items.len(), 1);
7311 assert!(matches!(b.items[0], DisplayListItem::CursorRect { .. }));
7312 }
7313
7314 #[test]
7315 fn builder_text_decorations_require_positive_thickness_and_alpha() {
7316 let bounds = rect(0.0, 0.0, 10.0, 2.0);
7317 for thickness in [0.0, -1.0, f32::NAN, f32::NEG_INFINITY] {
7318 let mut b = DisplayListBuilder::new();
7319 b.push_underline(bounds, opaque(), thickness);
7320 b.push_strikethrough(bounds, opaque(), thickness);
7321 b.push_overline(bounds, opaque(), thickness);
7322 assert!(b.items.is_empty(), "thickness {thickness} must not paint");
7323 }
7324
7325 let mut b = DisplayListBuilder::new();
7327 b.push_underline(bounds, opaque(), f32::INFINITY);
7328 assert_eq!(b.items.len(), 1);
7329
7330 let mut b = DisplayListBuilder::new();
7332 b.push_overline(bounds, ColorU::TRANSPARENT, 3.0);
7333 assert!(b.items.is_empty());
7334 }
7335
7336 #[test]
7337 fn builder_text_run_skips_empty_glyphs_and_transparent_color() {
7338 let clip = rect(0.0, 0.0, 100.0, 20.0);
7339 let mut b = DisplayListBuilder::new();
7340 b.push_text_run(Vec::new(), FontHash::invalid(), 16.0, opaque(), clip, Some(0));
7341 b.push_text_run(vec![glyph(1, 0.0, 0.0)], FontHash::invalid(), 16.0, ColorU::TRANSPARENT, clip, Some(0));
7342 assert!(b.items.is_empty());
7343
7344 b.push_text_run(vec![glyph(1, 0.0, 0.0)], FontHash::invalid(), f32::NAN, opaque(), clip, None);
7346 b.push_text_run(vec![glyph(2, 0.0, 0.0)], FontHash::invalid(), f32::MAX, opaque(), clip, None);
7347 assert_eq!(b.items.len(), 2);
7348 }
7349
7350 #[test]
7351 fn builder_border_requires_both_a_width_and_a_style() {
7352 let bounds = rect(0.0, 0.0, 10.0, 10.0);
7353
7354 let mut b = DisplayListBuilder::new();
7355 b.push_border(bounds, no_widths(), no_colors(), no_styles(), zero_style_radius());
7356 assert!(b.items.is_empty(), "no widths + no styles => nothing to draw");
7357
7358 let mut b = DisplayListBuilder::new();
7359 b.push_border(bounds, all_widths(), no_colors(), no_styles(), zero_style_radius());
7360 assert!(b.items.is_empty(), "width without style => nothing to draw");
7361
7362 let mut b = DisplayListBuilder::new();
7363 b.push_border(bounds, no_widths(), no_colors(), all_styles(), zero_style_radius());
7364 assert!(b.items.is_empty(), "style without width => nothing to draw");
7365
7366 let mut b = DisplayListBuilder::new();
7367 b.push_border(bounds, all_widths(), no_colors(), all_styles(), zero_style_radius());
7368 assert_eq!(b.items.len(), 1);
7369 }
7370
7371 #[test]
7372 fn builder_forced_page_breaks_are_deduped_and_sorted() {
7373 let mut b = DisplayListBuilder::new();
7374 b.add_forced_page_break(300.0);
7375 b.add_forced_page_break(100.0);
7376 b.add_forced_page_break(300.0); b.add_forced_page_break(200.0);
7378 b.add_forced_page_break(0.0);
7379 b.add_forced_page_break(-50.0); b.add_forced_page_break(f32::INFINITY);
7381 b.add_forced_page_break(f32::NEG_INFINITY);
7382 assert_eq!(
7383 b.forced_page_breaks,
7384 vec![f32::NEG_INFINITY, -50.0, 0.0, 100.0, 200.0, 300.0, f32::INFINITY]
7385 );
7386 }
7387
7388 #[test]
7389 fn builder_forced_page_break_nan_is_never_deduped() {
7390 let mut b = DisplayListBuilder::new();
7394 b.add_forced_page_break(f32::NAN);
7395 b.add_forced_page_break(f32::NAN);
7396 b.add_forced_page_break(100.0);
7397 assert_eq!(b.forced_page_breaks.len(), 3, "a NaN break is never deduped");
7398 assert_eq!(b.forced_page_breaks.iter().filter(|v| v.is_nan()).count(), 2);
7399 }
7400
7401 #[test]
7402 fn builder_fixed_position_ranges_need_a_begin_and_at_least_one_item() {
7403 let mut b = DisplayListBuilder::new();
7405 b.end_fixed_position_element();
7406 assert!(b.fixed_position_item_ranges.is_empty());
7407
7408 let mut b = DisplayListBuilder::new();
7410 b.begin_fixed_position_element();
7411 b.end_fixed_position_element();
7412 assert!(b.fixed_position_item_ranges.is_empty());
7413
7414 let mut b = DisplayListBuilder::new();
7416 b.pop_clip(); b.begin_fixed_position_element();
7418 b.push_rect(rect(0.0, 0.0, 1.0, 1.0), opaque(), BorderRadius::default());
7419 b.push_rect(rect(0.0, 0.0, 2.0, 2.0), opaque(), BorderRadius::default());
7420 b.end_fixed_position_element();
7421 assert_eq!(b.fixed_position_item_ranges, vec![(1, 3)]);
7422
7423 b.end_fixed_position_element();
7425 assert_eq!(b.fixed_position_item_ranges.len(), 1);
7426 }
7427
7428 #[test]
7429 fn builder_build_with_debug_transfers_messages() {
7430 let mut b = DisplayListBuilder::with_debug(true);
7431 b.debug_log("hello".to_string());
7432 b.push_rect(rect(0.0, 0.0, 1.0, 1.0), opaque(), BorderRadius::default());
7433
7434 let mut sink = Some(vec![LayoutDebugMessage::info("pre-existing".to_string())]);
7435 let dl = b.build_with_debug(&mut sink);
7436 let msgs = sink.expect("sink stays Some");
7437 assert_eq!(msgs.len(), 2, "messages are appended, not replaced");
7438 assert_eq!(dl.items.len(), 1);
7439
7440 let mut b = DisplayListBuilder::with_debug(true);
7442 b.debug_log("dropped".to_string());
7443 let mut none_sink: Option<Vec<LayoutDebugMessage>> = None;
7444 let dl = b.build_with_debug(&mut none_sink);
7445 assert!(none_sink.is_none());
7446 assert!(dl.items.is_empty());
7447 }
7448
7449 #[test]
7450 fn builder_stack_pushes_accept_extreme_arguments() {
7451 let mut b = DisplayListBuilder::new();
7452 b.push_stacking_context(i32::MIN, rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN));
7454 b.push_stacking_context(i32::MAX, rect(0.0, 0.0, -1.0, -1.0));
7455 b.pop_stacking_context();
7456 b.push_clip(rect(f32::MIN, f32::MIN, f32::MAX, f32::MAX), BorderRadius { top_left: f32::INFINITY, ..BorderRadius::default() });
7457 b.pop_clip();
7458 b.push_scroll_frame(LogicalRect::zero(), LogicalSize::new(f32::MAX, f32::MAX), u64::MAX);
7459 b.pop_scroll_frame();
7460 b.push_image_mask_clip(LogicalRect::zero(), test_image(), rect(0.0, 0.0, -5.0, -5.0));
7461 b.pop_image_mask_clip();
7462 b.push_reference_frame(TransformKey::unique(), ComputedTransform3D::IDENTITY, LogicalRect::zero());
7463 b.pop_reference_frame();
7464 b.push_virtual_view_placeholder(NodeId::ZERO, LogicalRect::zero(), LogicalRect::zero());
7465 b.push_hit_test_area(rect(0.0, 0.0, 1.0, 1.0), (u64::MAX, TAG_TYPE_CURSOR));
7466 b.push_image(LogicalRect::zero(), test_image(), BorderRadius::default());
7467 b.push_linear_gradient(LogicalRect::zero(), LinearGradient::default(), BorderRadius::default());
7468 b.push_radial_gradient(LogicalRect::zero(), RadialGradient::default(), BorderRadius::default());
7469 b.push_conic_gradient(LogicalRect::zero(), ConicGradient::default(), BorderRadius::default());
7470
7471 let dl = b.build();
7472 assert_eq!(dl.items.len(), 17);
7473 assert_eq!(dl.items.len(), dl.node_mapping.len());
7474 }
7475
7476 #[test]
7481 fn rect_intersects_uses_a_half_open_page_interval() {
7482 let page = (100.0f32, 200.0f32);
7483 assert!(rect_intersects(&rect(0.0, 120.0, 10.0, 10.0), page.0, page.1));
7485 assert!(rect_intersects(&rect(0.0, 50.0, 10.0, 300.0), page.0, page.1));
7487 assert!(!rect_intersects(&rect(0.0, 0.0, 10.0, 10.0), page.0, page.1));
7489 assert!(!rect_intersects(&rect(0.0, 500.0, 10.0, 10.0), page.0, page.1));
7490 assert!(!rect_intersects(&rect(0.0, 90.0, 10.0, 10.0), page.0, page.1));
7492 assert!(!rect_intersects(&rect(0.0, 200.0, 10.0, 10.0), page.0, page.1));
7494 assert!(rect_intersects(&rect(0.0, 150.0, 10.0, 0.0), page.0, page.1));
7496 assert!(!rect_intersects(&rect(0.0, 100.0, 10.0, 0.0), page.0, page.1));
7498 assert!(!rect_intersects(&rect(0.0, 200.0, 10.0, 0.0), page.0, page.1));
7499 }
7500
7501 #[test]
7502 fn rect_intersects_with_nan_is_false_not_a_panic() {
7503 assert!(!rect_intersects(&rect(0.0, f32::NAN, 10.0, 10.0), 0.0, 100.0));
7504 assert!(!rect_intersects(&rect(0.0, 10.0, 10.0, f32::NAN), 0.0, 100.0));
7505 assert!(!rect_intersects(&rect(0.0, 10.0, 10.0, 10.0), f32::NAN, f32::NAN));
7506 assert!(rect_intersects(&rect(0.0, 10.0, 10.0, 10.0), f32::NEG_INFINITY, f32::INFINITY));
7508 }
7509
7510 #[test]
7511 fn clip_rect_bounds_clips_and_rebases_to_page_relative_coords() {
7512 let clipped = clip_rect_bounds(rect(5.0, 50.0, 20.0, 100.0), 100.0, 200.0).unwrap();
7514 assert_eq!(clipped, rect(5.0, 0.0, 20.0, 50.0));
7515
7516 let clipped = clip_rect_bounds(rect(5.0, 180.0, 20.0, 100.0), 100.0, 200.0).unwrap();
7518 assert_eq!(clipped, rect(5.0, 80.0, 20.0, 20.0));
7519
7520 let clipped = clip_rect_bounds(rect(5.0, 120.0, 20.0, 30.0), 100.0, 200.0).unwrap();
7522 assert_eq!(clipped, rect(5.0, 20.0, 20.0, 30.0));
7523
7524 let clipped = clip_rect_bounds(rect(0.0, 0.0, 20.0, 10_000.0), 100.0, 200.0).unwrap();
7526 assert_eq!(clipped, rect(0.0, 0.0, 20.0, 100.0));
7527 }
7528
7529 #[test]
7530 fn clip_rect_bounds_rejects_off_page_and_edge_touching_rects() {
7531 assert_eq!(clip_rect_bounds(rect(0.0, 0.0, 10.0, 10.0), 100.0, 200.0), None);
7532 assert_eq!(clip_rect_bounds(rect(0.0, 300.0, 10.0, 10.0), 100.0, 200.0), None);
7533 assert_eq!(clip_rect_bounds(rect(0.0, 90.0, 10.0, 10.0), 100.0, 200.0), None);
7535 assert_eq!(clip_rect_bounds(rect(0.0, 200.0, 10.0, 10.0), 100.0, 200.0), None);
7537 }
7538
7539 #[test]
7540 fn clip_rect_bounds_zero_height_rects() {
7541 assert_eq!(clip_rect_bounds(rect(0.0, 100.0, 10.0, 0.0), 100.0, 200.0), None);
7543 assert_eq!(
7545 clip_rect_bounds(rect(0.0, 150.0, 10.0, 0.0), 100.0, 200.0),
7546 Some(rect(0.0, 50.0, 10.0, 0.0))
7547 );
7548 }
7549
7550 #[test]
7551 fn clip_rect_bounds_with_an_inverted_page_produces_a_degenerate_rect_not_a_panic() {
7552 let out = clip_rect_bounds(rect(0.0, 0.0, 10.0, 500.0), 200.0, 100.0)
7556 .expect("an inverted page is not rejected");
7557 assert!(out.size.height < 0.0, "no clamp: an inverted page yields a negative height");
7558 }
7559
7560 #[test]
7561 fn clip_rect_bounds_with_extreme_pages_does_not_panic() {
7562 let full = clip_rect_bounds(rect(0.0, 10.0, 10.0, 10.0), f32::NEG_INFINITY, f32::INFINITY)
7564 .expect("an infinite page contains everything");
7565 assert_eq!(full.size, LogicalSize::new(10.0, 10.0));
7566
7567 let nan_page = clip_rect_bounds(rect(0.0, 10.0, 10.0, 10.0), f32::NAN, f32::NAN)
7571 .expect("NaN bounds fail both rejection tests");
7572 assert!(nan_page.origin.y.is_nan(), "the page-relative rebase propagates NaN");
7573 assert_eq!(nan_page.size.height, 10.0, "min/max ignore NaN, so the height survives");
7574
7575 assert_eq!(
7577 clip_rect_bounds(rect(0.0, 0.0, 10.0, f32::MAX), 0.0, 100.0),
7578 Some(rect(0.0, 0.0, 10.0, 100.0))
7579 );
7580 }
7581
7582 #[test]
7583 fn offset_rect_y_only_moves_y_and_preserves_size() {
7584 assert_eq!(offset_rect_y(rect(1.0, 2.0, 3.0, 4.0), 0.0), rect(1.0, 2.0, 3.0, 4.0));
7585 assert_eq!(offset_rect_y(rect(1.0, 2.0, 3.0, 4.0), 10.0), rect(1.0, 12.0, 3.0, 4.0));
7586 assert_eq!(offset_rect_y(rect(1.0, 2.0, 3.0, 4.0), -10.0), rect(1.0, -8.0, 3.0, 4.0));
7587
7588 assert!(offset_rect_y(rect(0.0, 0.0, 1.0, 1.0), f32::INFINITY).origin.y.is_infinite());
7590 assert!(offset_rect_y(rect(0.0, 0.0, 1.0, 1.0), f32::NAN).origin.y.is_nan());
7591 assert!(offset_rect_y(rect(0.0, f32::MAX, 1.0, 1.0), f32::MAX).origin.y.is_infinite());
7593 assert_eq!(offset_rect_y(rect(0.0, 0.0, 3.0, 4.0), f32::NAN).size, LogicalSize::new(3.0, 4.0));
7595 }
7596
7597 #[test]
7602 fn clip_rect_item_drops_off_page_and_rebases_on_page() {
7603 assert!(clip_rect_item(rect(0.0, 0.0, 10.0, 10.0), opaque(), BorderRadius::default(), 100.0, 200.0).is_none());
7604
7605 let item = clip_rect_item(rect(0.0, 150.0, 10.0, 100.0), opaque(), BorderRadius::default(), 100.0, 200.0)
7606 .expect("overlaps the page");
7607 match item {
7608 DisplayListItem::Rect { bounds, color, .. } => {
7609 assert_eq!(bounds.into_inner(), rect(0.0, 50.0, 10.0, 50.0));
7610 assert_eq!(color, opaque());
7611 }
7612 other => panic!("expected Rect, got {other:?}"),
7613 }
7614 }
7615
7616 #[test]
7617 fn clip_cursor_selection_hittest_and_scrollbar_items_share_the_page_test() {
7618 let off = rect(0.0, 0.0, 10.0, 10.0);
7619 let on = rect(0.0, 120.0, 10.0, 10.0);
7620 let (top, bottom) = (100.0, 200.0);
7621
7622 assert!(clip_cursor_rect_item(off, opaque(), top, bottom).is_none());
7623 assert!(clip_cursor_rect_item(on, opaque(), top, bottom).is_some());
7624
7625 assert!(clip_selection_rect_item(off, BorderRadius::default(), opaque(), top, bottom).is_none());
7626 assert!(clip_selection_rect_item(on, BorderRadius::default(), opaque(), top, bottom).is_some());
7627
7628 assert!(clip_hit_test_area_item(off, (1, TAG_TYPE_DOM_NODE), top, bottom).is_none());
7629 assert!(clip_hit_test_area_item(on, (1, TAG_TYPE_DOM_NODE), top, bottom).is_some());
7630
7631 assert!(clip_scrollbar_item(off, opaque(), ScrollbarOrientation::Vertical, None, None, top, bottom).is_none());
7632 assert!(clip_scrollbar_item(on, opaque(), ScrollbarOrientation::Vertical, None, None, top, bottom).is_some());
7633
7634 assert!(clip_image_item(off, test_image(), BorderRadius::default(), top, bottom).is_none());
7635 assert!(clip_image_item(on, test_image(), BorderRadius::default(), top, bottom).is_some());
7636
7637 assert!(clip_virtual_view_item(DomId::ROOT_ID, off, off, top, bottom).is_none());
7638 assert!(clip_virtual_view_item(DomId::ROOT_ID, on, on, top, bottom).is_some());
7639 }
7640
7641 #[test]
7642 fn clip_text_decoration_item_preserves_the_decoration_kind() {
7643 let on = rect(0.0, 120.0, 10.0, 2.0);
7644 let (top, bottom) = (100.0, 200.0);
7645
7646 assert!(matches!(
7647 clip_text_decoration_item(on, opaque(), 1.0, TextDecorationType::Underline, top, bottom),
7648 Some(DisplayListItem::Underline { .. })
7649 ));
7650 assert!(matches!(
7651 clip_text_decoration_item(on, opaque(), 1.0, TextDecorationType::Strikethrough, top, bottom),
7652 Some(DisplayListItem::Strikethrough { .. })
7653 ));
7654 assert!(matches!(
7655 clip_text_decoration_item(on, opaque(), 1.0, TextDecorationType::Overline, top, bottom),
7656 Some(DisplayListItem::Overline { .. })
7657 ));
7658 assert!(clip_text_decoration_item(
7660 rect(0.0, 0.0, 10.0, 2.0), opaque(), f32::NAN, TextDecorationType::Underline, top, bottom
7661 ).is_none());
7662 }
7663
7664 #[test]
7665 fn clip_text_item_filters_glyphs_by_baseline_into_a_half_open_page() {
7666 let clip = rect(0.0, 90.0, 200.0, 120.0);
7667 let glyphs = vec![
7668 glyph(1, 0.0, 99.0), glyph(2, 8.0, 100.0), glyph(3, 16.0, 150.0), glyph(4, 24.0, 200.0), glyph(5, 32.0, 250.0), ];
7674
7675 let item = clip_text_item(&glyphs, FontHash::from_hash(9), 16.0, opaque(), clip, 100.0, 200.0)
7676 .expect("some glyphs are on the page");
7677 match item {
7678 DisplayListItem::Text { glyphs: kept, clip_rect, source_node_index, .. } => {
7679 assert_eq!(kept.iter().map(|g| g.index).collect::<Vec<_>>(), vec![2, 3]);
7680 assert_eq!(kept[0].point.y, 0.0);
7682 assert_eq!(kept[1].point.y, 50.0);
7683 assert_eq!(kept[0].point.x, 8.0);
7685 assert_eq!(clip_rect.into_inner().origin.y, -10.0);
7686 assert_eq!(source_node_index, None, "the paginated copy loses its source node");
7687 }
7688 other => panic!("expected Text, got {other:?}"),
7689 }
7690 }
7691
7692 #[test]
7693 fn clip_text_item_returns_none_when_nothing_survives() {
7694 let clip = rect(0.0, 90.0, 200.0, 120.0);
7695 let outside = vec![glyph(1, 0.0, 95.0), glyph(2, 8.0, 400.0)];
7697 assert!(clip_text_item(&outside, FontHash::from_hash(1), 16.0, opaque(), clip, 100.0, 200.0).is_none());
7698
7699 assert!(clip_text_item(&[], FontHash::from_hash(1), 16.0, opaque(), clip, 100.0, 200.0).is_none());
7701
7702 assert!(clip_text_item(
7704 &[glyph(1, 0.0, 150.0)], FontHash::from_hash(1), 16.0, opaque(),
7705 rect(0.0, 0.0, 10.0, 10.0), 100.0, 200.0
7706 ).is_none());
7707
7708 let nan_glyphs = vec![glyph(1, 0.0, f32::NAN)];
7710 assert!(clip_text_item(&nan_glyphs, FontHash::from_hash(1), 16.0, opaque(), clip, 100.0, 200.0).is_none());
7711 }
7712
7713 #[test]
7714 fn clip_border_item_hides_the_bottom_border_when_clipped_at_the_bottom() {
7715 let (top, bottom) = (100.0f32, 200.0f32);
7716 let original = rect(0.0, 50.0, 100.0, 200.0);
7718 let item = clip_border_item(original, all_widths(), no_colors(), all_styles(), zero_style_radius(), top, bottom)
7719 .expect("overlaps the page");
7720 match item {
7721 DisplayListItem::Border { widths, .. } => {
7722 assert!(widths.bottom.is_none(), "a border cut by the page edge must not draw its bottom rule");
7723 assert!(widths.left.is_some() && widths.right.is_some(), "side borders survive");
7724 }
7725 other => panic!("expected Border, got {other:?}"),
7726 }
7727 }
7728
7729 #[test]
7730 fn adjust_border_widths_keeps_every_side_for_a_fully_contained_border() {
7731 let (top, bottom) = (100.0f32, 200.0f32);
7732 let original = rect(0.0, 110.0, 100.0, 50.0); let clipped = clip_rect_bounds(original, top, bottom).unwrap();
7734 assert_eq!(clipped, rect(0.0, 10.0, 100.0, 50.0), "unclipped => only rebased");
7735
7736 let widths = adjust_border_widths_for_clipping(all_widths(), original, clipped, top, bottom);
7737 assert!(widths.top.is_some());
7738 assert!(widths.bottom.is_some());
7739 assert!(widths.left.is_some());
7740 assert!(widths.right.is_some());
7741 }
7742
7743 #[test]
7744 fn adjust_border_widths_with_nan_page_bounds_does_not_panic() {
7745 let original = rect(0.0, 0.0, 10.0, 10.0);
7746 let clipped = rect(0.0, 0.0, 10.0, 10.0);
7747 let w = adjust_border_widths_for_clipping(all_widths(), original, clipped, f32::NAN, f32::NAN);
7748 assert!(w.top.is_some() && w.bottom.is_some());
7750 }
7751
7752 #[test]
7753 fn clip_and_offset_display_item_drops_state_management_commands() {
7754 for item in [
7757 DisplayListItem::PushClip { bounds: rect(0.0, 120.0, 10.0, 10.0).into(), border_radius: BorderRadius::default() },
7758 DisplayListItem::PopClip,
7759 DisplayListItem::PushScrollFrame {
7760 clip_bounds: rect(0.0, 120.0, 10.0, 10.0).into(),
7761 content_size: LogicalSize::new(10.0, 10.0),
7762 scroll_id: 1,
7763 },
7764 DisplayListItem::PopScrollFrame,
7765 DisplayListItem::PushStackingContext { z_index: 0, bounds: rect(0.0, 120.0, 10.0, 10.0).into() },
7766 DisplayListItem::PopStackingContext,
7767 DisplayListItem::PopOpacity,
7768 DisplayListItem::PopTextShadow,
7769 DisplayListItem::VirtualViewPlaceholder {
7770 node_id: NodeId::ZERO,
7771 bounds: rect(0.0, 120.0, 10.0, 10.0).into(),
7772 clip_rect: rect(0.0, 120.0, 10.0, 10.0).into(),
7773 },
7774 ] {
7775 assert!(
7776 clip_and_offset_display_item(&item, 100.0, 200.0).is_none(),
7777 "{item:?} must be dropped by the paginator"
7778 );
7779 }
7780 }
7781
7782 #[test]
7783 fn clip_and_offset_display_item_dispatches_drawing_items() {
7784 let on_page = DisplayListItem::Rect {
7785 bounds: rect(0.0, 150.0, 10.0, 10.0).into(),
7786 color: opaque(),
7787 border_radius: BorderRadius::default(),
7788 };
7789 let clipped = clip_and_offset_display_item(&on_page, 100.0, 200.0).expect("on page");
7790 assert_eq!(clipped.bounds(), Some(rect(0.0, 50.0, 10.0, 10.0)));
7791
7792 let off_page = DisplayListItem::Rect {
7793 bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
7794 color: opaque(),
7795 border_radius: BorderRadius::default(),
7796 };
7797 assert!(clip_and_offset_display_item(&off_page, 100.0, 200.0).is_none());
7798
7799 let grad = DisplayListItem::LinearGradient {
7801 bounds: rect(0.0, 150.0, 10.0, 10.0).into(),
7802 gradient: LinearGradient::default(),
7803 border_radius: BorderRadius::default(),
7804 };
7805 assert!(clip_and_offset_display_item(&grad, 100.0, 200.0).is_some());
7806 assert!(clip_and_offset_display_item(&grad, 1000.0, 2000.0).is_none());
7807 }
7808
7809 #[test]
7814 fn get_display_item_bounds_mirrors_item_bounds() {
7815 let r = rect(1.0, 2.0, 3.0, 4.0);
7816 let item = DisplayListItem::Rect { bounds: r.into(), color: opaque(), border_radius: BorderRadius::default() };
7817 assert_eq!(get_display_item_bounds(&item), Some(WindowLogicalRect::from(r)));
7818 assert_eq!(get_display_item_bounds(&DisplayListItem::PopClip), None);
7819 }
7820
7821 #[test]
7822 fn offset_display_item_y_zero_offset_is_a_pure_clone() {
7823 let item = text_item(Some(3), rect(0.0, 10.0, 100.0, 20.0), vec![glyph(1, 5.0, 15.0)]);
7824 let same = offset_display_item_y(&item, 0.0);
7825 assert!(item.is_visually_equal(&same));
7826 assert_eq!(same.bounds(), item.bounds());
7827 }
7828
7829 #[test]
7830 fn offset_display_item_y_moves_glyphs_and_clip_together() {
7831 let item = text_item(Some(3), rect(0.0, 10.0, 100.0, 20.0), vec![glyph(1, 5.0, 15.0), glyph(2, 13.0, 15.0)]);
7832 match offset_display_item_y(&item, -10.0) {
7833 DisplayListItem::Text { glyphs, clip_rect, .. } => {
7834 assert_eq!(clip_rect.into_inner(), rect(0.0, 0.0, 100.0, 20.0));
7835 assert_eq!(glyphs[0].point.y, 5.0);
7836 assert_eq!(glyphs[1].point.y, 5.0);
7837 assert_eq!(glyphs[0].point.x, 5.0, "X must not move");
7838 }
7839 other => panic!("expected Text, got {other:?}"),
7840 }
7841 }
7842
7843 #[test]
7844 fn offset_display_item_y_with_nonfinite_offsets_does_not_panic() {
7845 let item = DisplayListItem::Rect {
7846 bounds: rect(0.0, 10.0, 5.0, 5.0).into(),
7847 color: opaque(),
7848 border_radius: BorderRadius::default(),
7849 };
7850 assert!(offset_display_item_y(&item, f32::NAN).bounds().unwrap().origin.y.is_nan());
7851 assert!(offset_display_item_y(&item, f32::INFINITY).bounds().unwrap().origin.y.is_infinite());
7852 assert!(offset_display_item_y(&item, f32::MIN).bounds().unwrap().origin.y.is_finite());
7853 }
7854
7855 #[test]
7856 fn calculate_display_list_height_ignores_hairline_items() {
7857 assert_eq!(calculate_display_list_height(&DisplayList::default()), 0.0);
7858
7859 let dl = list_of(vec![
7861 DisplayListItem::Rect {
7862 bounds: rect(0.0, 5000.0, 10.0, 0.05).into(),
7863 color: opaque(),
7864 border_radius: BorderRadius::default(),
7865 },
7866 DisplayListItem::Rect {
7867 bounds: rect(0.0, 10.0, 10.0, 40.0).into(),
7868 color: opaque(),
7869 border_radius: BorderRadius::default(),
7870 },
7871 ]);
7872 assert_eq!(calculate_display_list_height(&dl), 50.0);
7873 }
7874
7875 #[test]
7876 fn calculate_display_list_height_never_returns_negative_or_nan() {
7877 let dl = list_of(vec![
7880 DisplayListItem::Rect {
7881 bounds: rect(0.0, -500.0, 10.0, 100.0).into(),
7882 color: opaque(),
7883 border_radius: BorderRadius::default(),
7884 },
7885 DisplayListItem::Rect {
7886 bounds: rect(0.0, f32::NAN, 10.0, f32::NAN).into(),
7887 color: opaque(),
7888 border_radius: BorderRadius::default(),
7889 },
7890 DisplayListItem::PopClip, ]);
7892 let h = calculate_display_list_height(&dl);
7893 assert!(!h.is_nan(), "a NaN total height would panic the page-break sort");
7894 assert_eq!(h, 0.0);
7895 }
7896
7897 #[test]
7902 fn get_scroll_id_maps_node_index_and_collides_none_with_node_zero() {
7903 assert_eq!(get_scroll_id(None), 0);
7904 assert_eq!(get_scroll_id(Some(NodeId::new(5))), 5);
7905 assert_eq!(get_scroll_id(Some(NodeId::new(usize::MAX))), usize::MAX as u64);
7906 assert_eq!(get_scroll_id(Some(NodeId::ZERO)), get_scroll_id(None));
7908 }
7909
7910 #[test]
7915 fn slicer_config_builders_set_the_fields_they_name() {
7916 let simple = SlicerConfig::simple(800.0);
7917 assert_eq!(simple.page_content_height, 800.0);
7918 assert_eq!(simple.page_gap, 0.0);
7919 assert!(simple.allow_clipping);
7920 assert_eq!(simple.page_width, DEFAULT_A4_WIDTH_PT);
7921 assert_eq!(simple.page_slot_height(), 800.0);
7922
7923 let gapped = SlicerConfig::with_gap(800.0, 40.0);
7924 assert_eq!(gapped.page_gap, 40.0);
7925 assert_eq!(gapped.page_slot_height(), 840.0);
7926
7927 let wide = SlicerConfig::simple(800.0).with_page_width(1000.0);
7928 assert_eq!(wide.page_width, 1000.0);
7929 assert_eq!(wide.page_content_height, 800.0, "with_page_width must not disturb the height");
7930 }
7931
7932 #[test]
7933 fn slicer_config_builders_accept_extreme_values() {
7934 for h in [0.0, -100.0, f32::MAX, f32::INFINITY, f32::NAN] {
7935 let c = SlicerConfig::simple(h);
7936 assert_eq!(c.page_content_height.to_bits(), h.to_bits());
7937 let _ = c.page_slot_height();
7938 let _ = c.page_for_y(10.0);
7939 let _ = c.page_bounds(0);
7940 }
7941 assert!(SlicerConfig::with_gap(100.0, f32::NAN).page_slot_height().is_nan());
7943 }
7944
7945 #[test]
7946 fn page_for_y_basic_and_boundary() {
7947 let c = SlicerConfig::simple(100.0);
7948 assert_eq!(c.page_for_y(0.0), 0);
7949 assert_eq!(c.page_for_y(99.999), 0);
7950 assert_eq!(c.page_for_y(100.0), 1, "the page boundary belongs to the next page");
7951 assert_eq!(c.page_for_y(250.0), 2);
7952
7953 let g = SlicerConfig::with_gap(100.0, 20.0);
7955 assert_eq!(g.page_for_y(119.0), 0);
7956 assert_eq!(g.page_for_y(120.0), 1);
7957 }
7958
7959 #[test]
7960 fn page_for_y_saturates_instead_of_wrapping_or_trapping() {
7961 let c = SlicerConfig::simple(100.0);
7962 assert_eq!(c.page_for_y(-1.0), 0);
7965 assert_eq!(c.page_for_y(-1e30), 0);
7966 assert_eq!(c.page_for_y(f32::NEG_INFINITY), 0);
7967 assert_eq!(c.page_for_y(f32::NAN), 0);
7969 assert_eq!(c.page_for_y(f32::INFINITY), usize::MAX);
7971 assert_eq!(c.page_for_y(f32::MAX), usize::MAX);
7972 }
7973
7974 #[test]
7975 fn page_for_y_with_a_nonpositive_slot_is_always_page_zero() {
7976 assert_eq!(SlicerConfig::default().page_for_y(f32::MAX), 0);
7978 assert_eq!(SlicerConfig::simple(0.0).page_for_y(500.0), 0);
7979 assert_eq!(SlicerConfig::with_gap(100.0, -100.0).page_for_y(500.0), 0, "slot == 0");
7980 assert_eq!(SlicerConfig::with_gap(100.0, -500.0).page_for_y(500.0), 0, "slot < 0");
7981 assert_eq!(SlicerConfig::simple(f32::NAN).page_for_y(500.0), 0);
7984 }
7985
7986 #[test]
7987 fn page_bounds_are_contiguous_without_a_gap_and_spaced_with_one() {
7988 let c = SlicerConfig::simple(100.0);
7989 assert_eq!(c.page_bounds(0), (0.0, 100.0));
7990 assert_eq!(c.page_bounds(1), (100.0, 200.0));
7991 assert_eq!(c.page_bounds(3), (300.0, 400.0));
7992
7993 let g = SlicerConfig::with_gap(100.0, 20.0);
7994 assert_eq!(g.page_bounds(0), (0.0, 100.0));
7995 assert_eq!(g.page_bounds(2), (240.0, 340.0), "the gap is dead space between pages");
7996
7997 for page in 0..5usize {
7999 let (start, _end) = g.page_bounds(page);
8000 assert_eq!(g.page_for_y(start), page);
8001 }
8002 }
8003
8004 #[test]
8005 fn page_bounds_at_extreme_indices_do_not_panic() {
8006 let c = SlicerConfig::simple(100.0);
8007 let (start, end) = c.page_bounds(usize::MAX);
8008 assert!(start.is_finite() && end.is_finite(), "usize::MAX as f32 * 100 stays in f32 range");
8009 assert!(start > 0.0);
8010
8011 assert_eq!(SlicerConfig::default().page_bounds(9999), (0.0, 0.0));
8013 }
8014
8015 #[test]
8020 fn page_breaks_for_an_empty_or_zero_height_list_is_a_single_page() {
8021 let pages = calculate_page_break_positions(&DisplayList::default(), 100.0, 100.0);
8022 assert_eq!(pages, vec![(0.0, 100.0)], "an empty document still has one page");
8023
8024 let dl = list_of(vec![DisplayListItem::Rect {
8026 bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
8027 color: opaque(),
8028 border_radius: BorderRadius::default(),
8029 }]);
8030 assert_eq!(calculate_page_break_positions(&dl, 0.0, 100.0), vec![(0.0, 250.0)]);
8031 assert_eq!(calculate_page_break_positions(&dl, -50.0, 100.0), vec![(0.0, 250.0)]);
8032 }
8033
8034 #[test]
8035 fn page_breaks_split_at_regular_intervals() {
8036 let dl = list_of(vec![DisplayListItem::Rect {
8037 bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
8038 color: opaque(),
8039 border_radius: BorderRadius::default(),
8040 }]);
8041 let pages = calculate_page_break_positions(&dl, 100.0, 100.0);
8042 assert_eq!(pages, vec![(0.0, 100.0), (100.0, 200.0), (200.0, 250.0)]);
8043
8044 for w in pages.windows(2) {
8046 assert_eq!(w[0].1, w[1].0);
8047 }
8048 assert_eq!(pages.first().unwrap().0, 0.0);
8049 assert_eq!(pages.last().unwrap().1, 250.0);
8050 }
8051
8052 #[test]
8053 fn page_breaks_honour_forced_breaks_and_merge_near_duplicates() {
8054 let mut dl = list_of(vec![DisplayListItem::Rect {
8055 bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
8056 color: opaque(),
8057 border_radius: BorderRadius::default(),
8058 }]);
8059 dl.forced_page_breaks = vec![50.0];
8060 assert_eq!(
8061 calculate_page_break_positions(&dl, 100.0, 100.0),
8062 vec![(0.0, 50.0), (50.0, 100.0), (100.0, 200.0), (200.0, 250.0)]
8063 );
8064
8065 dl.forced_page_breaks = vec![100.5];
8068 let pages = calculate_page_break_positions(&dl, 100.0, 100.0);
8069 assert_eq!(pages, vec![(0.0, 100.0), (100.0, 200.0), (200.0, 250.0)]);
8070 assert!(pages.iter().all(|(s, e)| e > s), "no zero-height pages");
8071 }
8072
8073 #[test]
8074 fn page_breaks_ignore_out_of_range_and_nan_forced_breaks() {
8075 let mut dl = list_of(vec![DisplayListItem::Rect {
8078 bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
8079 color: opaque(),
8080 border_radius: BorderRadius::default(),
8081 }]);
8082 dl.forced_page_breaks = vec![f32::NAN, -10.0, 0.0, 250.0, 9999.0, f32::INFINITY];
8083 let pages = calculate_page_break_positions(&dl, 100.0, 100.0);
8084 assert_eq!(pages, vec![(0.0, 100.0), (100.0, 200.0), (200.0, 250.0)]);
8086 }
8087
8088 #[test]
8089 fn page_breaks_with_a_nan_first_page_height_still_yields_one_page() {
8090 let dl = list_of(vec![DisplayListItem::Rect {
8091 bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
8092 color: opaque(),
8093 border_radius: BorderRadius::default(),
8094 }]);
8095 let pages = calculate_page_break_positions(&dl, f32::NAN, 100.0);
8098 assert_eq!(pages, vec![(0.0, 250.0)]);
8099 }
8100
8101 #[test]
8106 fn paginate_with_a_degenerate_page_height_returns_the_list_unsliced() {
8107 let rr = RendererResources::default();
8108 let dl = list_of(vec![DisplayListItem::Rect {
8109 bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
8110 color: opaque(),
8111 border_radius: BorderRadius::default(),
8112 }]);
8113
8114 for h in [0.0, -1.0, f32::MAX, f32::INFINITY] {
8115 let pages = paginate_display_list_with_slicer_and_breaks(dl.clone(), &SlicerConfig::simple(h), &rr)
8116 .expect("degenerate page height must not error");
8117 assert_eq!(pages.len(), 1, "page height {h} => no slicing");
8118 assert_eq!(pages[0].items.len(), 1);
8119 }
8120 }
8121
8122 #[test]
8123 fn paginate_slices_content_into_pages() {
8124 let rr = RendererResources::default();
8125 let dl = list_of(vec![DisplayListItem::Rect {
8126 bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
8127 color: opaque(),
8128 border_radius: BorderRadius::default(),
8129 }]);
8130 let pages = paginate_display_list_with_slicer_and_breaks(dl, &SlicerConfig::simple(100.0), &rr)
8131 .expect("pagination succeeds");
8132 assert_eq!(pages.len(), 3);
8133 for page in &pages {
8135 let r = page.items.iter().find_map(DisplayListItem::bounds).expect("each page keeps a slice");
8136 assert_eq!(r.origin.y, 0.0);
8137 assert!(r.size.height > 0.0 && r.size.height <= 100.0);
8138 }
8139 }
8140
8141 #[test]
8146 fn patch_text_glyphs_replaces_matching_runs_and_returns_their_damage() {
8147 let clip = rect(10.0, 20.0, 100.0, 30.0);
8148 let mut dl = list_of(vec![text_item(Some(7), clip, vec![glyph(1, 0.0, 0.0)])]);
8149
8150 let damage = dl.patch_text_glyphs(7, &[vec![glyph(42, 5.0, 6.0), glyph(43, 13.0, 6.0)]]);
8151 assert_eq!(damage, Some(clip), "damage covers the run's clip rect");
8152 match &dl.items[0] {
8153 DisplayListItem::Text { glyphs, .. } => {
8154 assert_eq!(glyphs.iter().map(|g| g.index).collect::<Vec<_>>(), vec![42, 43]);
8155 }
8156 other => panic!("expected Text, got {other:?}"),
8157 }
8158 }
8159
8160 #[test]
8161 fn patch_text_glyphs_unions_damage_across_runs() {
8162 let a = rect(0.0, 0.0, 50.0, 10.0);
8163 let b = rect(100.0, 200.0, 50.0, 10.0);
8164 let mut dl = list_of(vec![
8165 text_item(Some(3), a, vec![glyph(1, 0.0, 0.0)]),
8166 text_item(Some(3), b, vec![glyph(2, 0.0, 0.0)]),
8167 ]);
8168
8169 let damage = dl.patch_text_glyphs(3, &[vec![glyph(9, 0.0, 0.0)], vec![glyph(10, 0.0, 0.0)]])
8170 .expect("both runs matched");
8171 assert_eq!(damage, rect(0.0, 0.0, 150.0, 210.0));
8173 }
8174
8175 #[test]
8176 fn patch_text_glyphs_returns_none_when_nothing_matches() {
8177 let clip = rect(0.0, 0.0, 10.0, 10.0);
8178 let mut dl = list_of(vec![
8179 text_item(Some(7), clip, vec![glyph(1, 0.0, 0.0)]),
8180 text_item(None, clip, vec![glyph(2, 0.0, 0.0)]), DisplayListItem::PopClip,
8182 ]);
8183
8184 assert_eq!(dl.patch_text_glyphs(8, &[vec![glyph(9, 0.0, 0.0)]]), None, "wrong node index");
8185 assert_eq!(dl.patch_text_glyphs(usize::MAX, &[vec![glyph(9, 0.0, 0.0)]]), None);
8186 assert_eq!(dl.patch_text_glyphs(7, &[]), None, "no replacement runs => nothing to do");
8187
8188 match &dl.items[0] {
8190 DisplayListItem::Text { glyphs, .. } => assert_eq!(glyphs[0].index, 1),
8191 other => panic!("expected Text, got {other:?}"),
8192 }
8193 }
8194
8195 #[test]
8196 fn patch_text_glyphs_stops_when_it_runs_out_of_replacement_runs() {
8197 let clip = rect(0.0, 0.0, 10.0, 10.0);
8198 let mut dl = list_of(vec![
8199 text_item(Some(1), clip, vec![glyph(100, 0.0, 0.0)]),
8200 text_item(Some(1), clip, vec![glyph(200, 0.0, 0.0)]),
8201 text_item(Some(1), clip, vec![glyph(300, 0.0, 0.0)]),
8202 ]);
8203
8204 assert!(dl.patch_text_glyphs(1, &[vec![glyph(999, 0.0, 0.0)]]).is_some());
8206 let ids: Vec<u32> = dl.items.iter().map(|i| match i {
8207 DisplayListItem::Text { glyphs, .. } => glyphs[0].index,
8208 other => panic!("expected Text, got {other:?}"),
8209 }).collect();
8210 assert_eq!(ids, vec![999, 200, 300]);
8211 }
8212
8213 #[test]
8214 fn patch_text_glyphs_accepts_an_empty_replacement_run() {
8215 let clip = rect(0.0, 0.0, 10.0, 10.0);
8216 let mut dl = list_of(vec![text_item(Some(0), clip, vec![glyph(1, 0.0, 0.0)])]);
8217 assert_eq!(dl.patch_text_glyphs(0, &[Vec::new()]), Some(clip));
8219 match &dl.items[0] {
8220 DisplayListItem::Text { glyphs, .. } => assert!(glyphs.is_empty()),
8221 other => panic!("expected Text, got {other:?}"),
8222 }
8223 }
8224
8225 #[cfg(feature = "text_layout")]
8230 #[test]
8231 fn compute_text_damage_rect_empty_inputs_yield_the_zero_rect() {
8232 let r = DisplayList::compute_text_damage_rect(&[], &[], LogicalPosition::new(100.0, 200.0), 0);
8233 assert_eq!(r, LogicalRect::zero(), "no items => no damage (not a MAX..MIN garbage rect)");
8234 }
8235
8236 #[cfg(feature = "text_layout")]
8237 #[test]
8238 fn compute_text_damage_rect_translates_by_the_container_origin() {
8239 let old = vec![positioned(0, 10.0, 20.0, 30.0, 40.0)];
8240 let r = DisplayList::compute_text_damage_rect(&old, &[], LogicalPosition::new(100.0, 200.0), 0);
8241 assert_eq!(r, rect(110.0, 220.0, 30.0, 40.0));
8242 }
8243
8244 #[cfg(feature = "text_layout")]
8245 #[test]
8246 fn compute_text_damage_rect_unions_old_and_new() {
8247 let old = vec![positioned(0, 0.0, 0.0, 10.0, 10.0)];
8248 let new = vec![positioned(0, 90.0, 190.0, 10.0, 10.0)];
8249 let r = DisplayList::compute_text_damage_rect(&old, &new, LogicalPosition::zero(), 0);
8250 assert_eq!(r, rect(0.0, 0.0, 100.0, 200.0), "damage must cover both the before and after ink");
8251 }
8252
8253 #[cfg(feature = "text_layout")]
8254 #[test]
8255 fn compute_text_damage_rect_skips_lines_before_the_affected_line() {
8256 let items = vec![
8257 positioned(0, 0.0, 0.0, 1000.0, 10.0), positioned(5, 10.0, 50.0, 20.0, 10.0), ];
8260 let r = DisplayList::compute_text_damage_rect(&items, &items, LogicalPosition::zero(), 5);
8261 assert_eq!(r, rect(10.0, 50.0, 20.0, 10.0));
8262
8263 let none = DisplayList::compute_text_damage_rect(&items, &items, LogicalPosition::zero(), usize::MAX);
8265 assert_eq!(none, LogicalRect::zero());
8266 }
8267
8268 #[cfg(feature = "text_layout")]
8269 #[test]
8270 fn compute_text_damage_rect_nan_positions_are_ignored_not_propagated() {
8271 let nan_only = vec![positioned(0, f32::NAN, f32::NAN, 10.0, 10.0)];
8273 let r = DisplayList::compute_text_damage_rect(&nan_only, &[], LogicalPosition::zero(), 0);
8274 assert_eq!(r, LogicalRect::zero(), "an all-NaN run must not produce a NaN damage rect");
8275
8276 let mixed = vec![positioned(0, f32::NAN, 0.0, 10.0, 10.0), positioned(0, 5.0, 5.0, 10.0, 10.0)];
8280 let r = DisplayList::compute_text_damage_rect(&mixed, &[], LogicalPosition::zero(), 0);
8281 assert!(!r.origin.x.is_nan() && !r.size.width.is_nan());
8282 assert_eq!(r, rect(5.0, 0.0, 10.0, 15.0));
8283 }
8284
8285 #[cfg(feature = "text_layout")]
8290 #[test]
8291 fn item_center_on_page_uses_the_item_midpoint_half_open() {
8292 let item = positioned(0, 0.0, 0.0, 10.0, 20.0); assert!(item_center_on_page(&item, 90.0, 100.0, 200.0));
8295 assert!(!item_center_on_page(&item, 89.99, 100.0, 200.0));
8297 assert!(!item_center_on_page(&item, 190.0, 100.0, 200.0));
8299 assert!(item_center_on_page(&item, 189.0, 100.0, 200.0));
8300 }
8301
8302 #[cfg(feature = "text_layout")]
8303 #[test]
8304 fn item_center_on_page_with_nonfinite_values_is_false_not_a_panic() {
8305 let item = positioned(0, 0.0, f32::NAN, 10.0, 20.0);
8306 assert!(!item_center_on_page(&item, 0.0, 100.0, 200.0));
8307
8308 let inf = positioned(0, 0.0, f32::INFINITY, 10.0, 20.0);
8309 assert!(!item_center_on_page(&inf, 0.0, 100.0, 200.0));
8310
8311 let ok = positioned(0, 0.0, 150.0, 10.0, 20.0);
8312 assert!(!item_center_on_page(&ok, 0.0, f32::NAN, f32::NAN));
8313 }
8314
8315 #[cfg(feature = "text_layout")]
8316 #[test]
8317 fn transform_items_to_page_coords_rebases_and_reports_extents() {
8318 let items = vec![
8319 positioned(0, 0.0, 10.0, 30.0, 20.0),
8320 positioned(1, 5.0, 40.0, 50.0, 20.0),
8321 ];
8322 let (out, min_y, max_y, max_width) = transform_items_to_page_coords(items, 100.0, 100.0, 0.0);
8324 assert_eq!(out.len(), 2);
8325 assert_eq!(out[0].position.y, 10.0);
8326 assert_eq!(out[1].position.y, 40.0);
8327 assert_eq!(out[0].position.x, 0.0, "X is never rebased");
8328 assert_eq!(min_y, 10.0);
8329 assert_eq!(max_y, 60.0);
8330 assert_eq!(max_width, 55.0, "max_width is position.x + item width");
8331 }
8332
8333 #[cfg(feature = "text_layout")]
8334 #[test]
8335 fn transform_items_to_page_coords_on_an_empty_input_returns_sentinel_extents() {
8336 let (out, min_y, max_y, max_width) = transform_items_to_page_coords(Vec::new(), 0.0, 0.0, 0.0);
8337 assert!(out.is_empty());
8338 assert_eq!(min_y, f32::MAX);
8340 assert_eq!(max_y, f32::MIN);
8341 assert_eq!(max_width, 0.0);
8342 }
8343
8344 #[test]
8349 fn to_debug_json_on_an_empty_list_reports_a_balanced_zero_item_list() {
8350 let json = DisplayList::default().to_debug_json();
8351 assert!(json.contains("\"total_items\": 0"));
8352 assert!(json.contains("\"balanced\": true"));
8353 assert!(json.contains("\"final_clip_depth\": 0"));
8354 }
8355
8356 #[test]
8357 fn to_debug_json_flags_an_unbalanced_clip_stack() {
8358 let dl = list_of(vec![DisplayListItem::PushClip {
8360 bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
8361 border_radius: BorderRadius::default(),
8362 }]);
8363 let json = dl.to_debug_json();
8364 assert!(json.contains("\"final_clip_depth\": 1"));
8365 assert!(json.contains("\"balanced\": false"));
8366
8367 let json = list_of(vec![DisplayListItem::PopClip]).to_debug_json();
8369 assert!(json.contains("\"final_clip_depth\": -1"));
8370 assert!(json.contains("\"balanced\": false"));
8371 }
8372
8373 #[test]
8374 fn to_debug_json_survives_a_truncated_node_mapping_and_extreme_values() {
8375 let dl = DisplayList {
8377 items: vec![
8378 DisplayListItem::PushClip {
8379 bounds: rect(f32::NAN, f32::INFINITY, f32::MAX, -1.0).into(),
8380 border_radius: BorderRadius { top_left: f32::NAN, ..BorderRadius::default() },
8381 },
8382 DisplayListItem::PopClip,
8383 DisplayListItem::PushStackingContext { z_index: i32::MIN, bounds: WindowLogicalRect::zero() },
8384 DisplayListItem::PopStackingContext,
8385 DisplayListItem::PushScrollFrame {
8386 clip_bounds: WindowLogicalRect::zero(),
8387 content_size: LogicalSize::new(f32::MAX, f32::MAX),
8388 scroll_id: u64::MAX,
8389 },
8390 DisplayListItem::PopScrollFrame,
8391 DisplayListItem::PopTextShadow, ],
8393 node_mapping: Vec::new(), ..DisplayList::default()
8395 };
8396 let json = dl.to_debug_json();
8397 assert!(json.contains("\"total_items\": 7"));
8398 assert!(json.contains("\"balanced\": true"), "every push is matched by a pop");
8399 }
8400
8401 #[test]
8406 fn ellipsis_leaves_non_overflowing_text_alone() {
8407 let container = rect(0.0, 0.0, 100.0, 20.0);
8408 let glyphs = vec![glyph(1, 0.0, 10.0), glyph(2, 10.0, 10.0)]; let mut dl = list_of(vec![text_item(Some(0), rect(0.0, 0.0, 100.0, 20.0), glyphs.clone())]);
8410
8411 apply_text_overflow_ellipsis(&mut dl, container, "…");
8412 match &dl.items[0] {
8413 DisplayListItem::Text { glyphs: g, .. } => {
8414 assert_eq!(g.len(), glyphs.len());
8415 assert_eq!(g.iter().map(|x| x.index).collect::<Vec<_>>(), vec![1, 2]);
8416 }
8417 other => panic!("expected Text, got {other:?}"),
8418 }
8419 }
8420
8421 #[test]
8422 fn ellipsis_truncates_overflowing_text_and_appends_u2026() {
8423 let container = rect(0.0, 0.0, 50.0, 20.0);
8424 let glyphs: Vec<_> = (0..6).map(|i| glyph(i + 1, (i as f32) * 10.0, 10.0)).collect();
8426 let mut dl = list_of(vec![text_item(Some(0), rect(0.0, 0.0, 500.0, 20.0), glyphs)]);
8427
8428 apply_text_overflow_ellipsis(&mut dl, container, "…");
8429 match &dl.items[0] {
8430 DisplayListItem::Text { glyphs: g, clip_rect, .. } => {
8431 assert_eq!(g.len(), 5, "4 kept glyphs + 1 ellipsis");
8434 assert_eq!(g.last().unwrap().index, 0x2026, "U+2026 HORIZONTAL ELLIPSIS");
8435 assert_eq!(g[3].index, 4, "the last kept glyph");
8436 assert_eq!(clip_rect.into_inner(), container);
8438 }
8439 other => panic!("expected Text, got {other:?}"),
8440 }
8441 }
8442
8443 #[test]
8444 fn ellipsis_on_a_container_too_narrow_for_any_glyph_leaves_only_the_ellipsis() {
8445 let container = rect(3.0, 4.0, 1.0, 20.0);
8448 let glyphs = vec![glyph(1, 0.0, 10.0), glyph(2, 10.0, 10.0)];
8449 let mut dl = list_of(vec![text_item(Some(0), rect(0.0, 0.0, 500.0, 20.0), glyphs)]);
8450
8451 apply_text_overflow_ellipsis(&mut dl, container, "…");
8452 match &dl.items[0] {
8453 DisplayListItem::Text { glyphs: g, .. } => {
8454 assert_eq!(g.len(), 1);
8455 assert_eq!(g[0].index, 0x2026);
8456 assert_eq!(g[0].point, LogicalPosition::new(3.0, 4.0), "anchored to the container origin");
8457 }
8458 other => panic!("expected Text, got {other:?}"),
8459 }
8460 }
8461
8462 #[test]
8463 fn ellipsis_skips_empty_runs_and_non_text_items() {
8464 let container = rect(0.0, 0.0, 1.0, 20.0);
8465 let mut dl = list_of(vec![
8466 text_item(Some(0), rect(0.0, 0.0, 500.0, 20.0), Vec::new()),
8467 DisplayListItem::PopClip,
8468 DisplayListItem::Rect {
8469 bounds: rect(0.0, 0.0, 900.0, 20.0).into(),
8470 color: opaque(),
8471 border_radius: BorderRadius::default(),
8472 },
8473 ]);
8474 apply_text_overflow_ellipsis(&mut dl, container, "…");
8475 match &dl.items[0] {
8476 DisplayListItem::Text { glyphs, .. } => assert!(glyphs.is_empty(), "an empty run is left alone"),
8477 other => panic!("expected Text, got {other:?}"),
8478 }
8479 assert_eq!(dl.items.len(), 3, "no items added or removed");
8480 }
8481
8482 #[test]
8483 fn ellipsis_with_a_nan_font_size_does_not_panic() {
8484 let container = rect(0.0, 0.0, 50.0, 20.0);
8487 let glyphs = vec![glyph(1, 0.0, 10.0), glyph(2, 100.0, 10.0)];
8488 let mut dl = list_of(vec![DisplayListItem::Text {
8489 glyphs,
8490 font_hash: FontHash::invalid(),
8491 font_size_px: f32::NAN,
8492 color: opaque(),
8493 clip_rect: rect(0.0, 0.0, 500.0, 20.0).into(),
8494 source_node_index: None,
8495 }]);
8496
8497 apply_text_overflow_ellipsis(&mut dl, container, "…");
8498 match &dl.items[0] {
8499 DisplayListItem::Text { glyphs: g, .. } => {
8500 assert_eq!(g.len(), 3, "both glyphs kept + ellipsis");
8501 assert_eq!(g.last().unwrap().index, 0x2026);
8502 assert!(g.last().unwrap().size.width.is_nan());
8503 }
8504 other => panic!("expected Text, got {other:?}"),
8505 }
8506 }
8507
8508 #[test]
8513 fn resolve_clip_path_none_is_no_clip() {
8514 use azul_css::props::layout::shape::ClipPath;
8515 assert_eq!(resolve_clip_path(&ClipPath::None, rect(0.0, 0.0, 100.0, 100.0)), None);
8516 assert_eq!(resolve_clip_path(&ClipPath::default(), rect(0.0, 0.0, 100.0, 100.0)), None);
8517 }
8518
8519 #[test]
8520 fn resolve_clip_path_inset_shrinks_from_every_edge() {
8521 use azul_css::{corety::OptionF32, props::layout::shape::ClipPath, shape::{CssShape, ShapeInset}};
8522 let path = ClipPath::Shape(CssShape::Inset(ShapeInset {
8523 inset_top: 10.0,
8524 inset_right: 20.0,
8525 inset_bottom: 30.0,
8526 inset_left: 40.0,
8527 border_radius: OptionF32::Some(6.0),
8528 }));
8529 let (r, radius) = resolve_clip_path(&path, rect(100.0, 200.0, 300.0, 400.0)).expect("inset clips");
8530 assert_eq!(r, rect(140.0, 210.0, 240.0, 360.0));
8531 assert_eq!(radius, 6.0);
8532 }
8533
8534 #[test]
8535 fn resolve_clip_path_over_inset_clamps_the_size_to_zero() {
8536 use azul_css::{corety::OptionF32, props::layout::shape::ClipPath, shape::{CssShape, ShapeInset}};
8537 let path = ClipPath::Shape(CssShape::Inset(ShapeInset {
8538 inset_top: 500.0,
8539 inset_right: 500.0,
8540 inset_bottom: 500.0,
8541 inset_left: 500.0,
8542 border_radius: OptionF32::None,
8543 }));
8544 let (r, radius) = resolve_clip_path(&path, rect(0.0, 0.0, 100.0, 100.0)).expect("still returns a rect");
8545 assert_eq!(r.size, LogicalSize::zero(), "insets larger than the box collapse, they do not go negative");
8546 assert_eq!(radius, 0.0, "OptionF32::None => radius 0");
8547 }
8548
8549 #[test]
8550 fn resolve_clip_path_circle_and_ellipse_use_their_bounding_box() {
8551 use azul_css::{props::layout::shape::ClipPath, shape::{CssShape, ShapeCircle, ShapeEllipse, ShapePoint}};
8552
8553 let circle = ClipPath::Shape(CssShape::Circle(ShapeCircle {
8554 center: ShapePoint { x: 50.0, y: 50.0 },
8555 radius: 20.0,
8556 }));
8557 let (r, radius) = resolve_clip_path(&circle, rect(100.0, 100.0, 200.0, 200.0)).expect("circle clips");
8558 assert_eq!(r, rect(130.0, 130.0, 40.0, 40.0), "centre is relative to the node origin");
8559 assert_eq!(radius, 20.0);
8560
8561 let ellipse = ClipPath::Shape(CssShape::Ellipse(ShapeEllipse {
8562 center: ShapePoint { x: 50.0, y: 50.0 },
8563 radius_x: 10.0,
8564 radius_y: 30.0,
8565 }));
8566 let (r, radius) = resolve_clip_path(&ellipse, rect(0.0, 0.0, 100.0, 100.0)).expect("ellipse clips");
8567 assert_eq!(r, rect(40.0, 20.0, 20.0, 60.0));
8568 assert_eq!(radius, 10.0, "the rounding uses the SMALLER of the two radii");
8569 }
8570
8571 #[test]
8572 fn resolve_clip_path_circle_with_a_degenerate_radius_does_not_panic() {
8573 use azul_css::{props::layout::shape::ClipPath, shape::{CssShape, ShapeCircle, ShapePoint}};
8574 for radius in [0.0, -10.0, f32::NAN, f32::INFINITY] {
8575 let path = ClipPath::Shape(CssShape::Circle(ShapeCircle {
8576 center: ShapePoint { x: 0.0, y: 0.0 },
8577 radius,
8578 }));
8579 let out = resolve_clip_path(&path, rect(0.0, 0.0, 100.0, 100.0));
8582 assert!(out.is_some(), "radius {radius} must still resolve");
8583 }
8584 }
8585
8586 #[test]
8587 fn resolve_clip_path_polygon_bbox_and_empty_polygon() {
8588 use azul_css::{props::layout::shape::ClipPath, shape::{CssShape, ShapePoint, ShapePolygon}};
8589
8590 let empty = ClipPath::Shape(CssShape::Polygon(ShapePolygon { points: Vec::new().into() }));
8592 assert_eq!(resolve_clip_path(&empty, rect(0.0, 0.0, 100.0, 100.0)), None);
8593
8594 let tri = ClipPath::Shape(CssShape::Polygon(ShapePolygon {
8596 points: vec![
8597 ShapePoint { x: 10.0, y: 90.0 },
8598 ShapePoint { x: 50.0, y: 10.0 },
8599 ShapePoint { x: 90.0, y: 90.0 },
8600 ].into(),
8601 }));
8602 let (r, radius) = resolve_clip_path(&tri, rect(1000.0, 2000.0, 100.0, 100.0)).expect("polygon clips");
8603 assert_eq!(r, rect(1010.0, 2010.0, 80.0, 80.0));
8604 assert_eq!(radius, 0.0, "a polygon bbox is never rounded");
8605 }
8606
8607 #[test]
8608 fn resolve_clip_path_polygon_with_nan_points_collapses_instead_of_panicking() {
8609 use azul_css::{props::layout::shape::ClipPath, shape::{CssShape, ShapePoint, ShapePolygon}};
8610 let nan_poly = ClipPath::Shape(CssShape::Polygon(ShapePolygon {
8611 points: vec![ShapePoint { x: f32::NAN, y: f32::NAN }].into(),
8612 }));
8613 let (r, _) = resolve_clip_path(&nan_poly, rect(0.0, 0.0, 100.0, 100.0)).expect("still resolves");
8614 assert_eq!(r.size, LogicalSize::zero());
8617 }
8618
8619 #[test]
8620 fn resolve_clip_path_svg_path_is_unsupported_and_does_not_clip() {
8621 use azul_css::{props::layout::shape::ClipPath, shape::{CssShape, ShapePath}};
8622 let path = ClipPath::Shape(CssShape::Path(ShapePath {
8623 data: String::from("M 0 0 L 10 10 Z").into(),
8624 }));
8625 assert_eq!(
8626 resolve_clip_path(&path, rect(0.0, 0.0, 100.0, 100.0)),
8627 None,
8628 "path() clip-paths are not implemented => no clipping (rather than a wrong clip)"
8629 );
8630 }
8631
8632 #[test]
8637 fn apply_clip_path_wraps_the_tail_of_the_list_and_keeps_the_mapping_in_sync() {
8638 let mut dl = list_of(vec![
8639 DisplayListItem::Rect {
8640 bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
8641 color: opaque(),
8642 border_radius: BorderRadius::default(),
8643 },
8644 DisplayListItem::Rect {
8645 bounds: rect(0.0, 0.0, 20.0, 20.0).into(),
8646 color: opaque(),
8647 border_radius: BorderRadius::default(),
8648 },
8649 ]);
8650
8651 apply_clip_path(&mut dl, 1, rect(5.0, 5.0, 50.0, 50.0), 8.0);
8652
8653 assert_eq!(dl.items.len(), 4, "PushClip inserted + PopClip appended");
8654 assert_eq!(dl.items.len(), dl.node_mapping.len(), "node_mapping must stay parallel to items");
8655 assert!(matches!(dl.items[1], DisplayListItem::PushClip { .. }));
8656 assert!(matches!(dl.items[3], DisplayListItem::PopClip));
8657 assert_eq!(dl.node_mapping[1], None);
8658 assert_eq!(dl.node_mapping[3], None);
8659
8660 match &dl.items[1] {
8661 DisplayListItem::PushClip { bounds, border_radius } => {
8662 assert_eq!(bounds.into_inner(), rect(5.0, 5.0, 50.0, 50.0));
8663 assert_eq!(border_radius.top_left, 8.0);
8665 assert_eq!(border_radius.bottom_right, 8.0);
8666 }
8667 other => panic!("expected PushClip, got {other:?}"),
8668 }
8669 }
8670
8671 #[test]
8672 fn apply_clip_path_with_a_nonpositive_radius_uses_square_corners() {
8673 for radius in [0.0, -5.0, f32::NAN] {
8674 let mut dl = list_of(vec![DisplayListItem::PopClip]);
8675 apply_clip_path(&mut dl, 0, rect(0.0, 0.0, 10.0, 10.0), radius);
8676 match &dl.items[0] {
8677 DisplayListItem::PushClip { border_radius, .. } => {
8678 assert!(border_radius.is_zero(), "radius {radius} must not round the clip");
8679 }
8680 other => panic!("expected PushClip, got {other:?}"),
8681 }
8682 }
8683 }
8684
8685 #[test]
8686 fn apply_clip_path_at_the_end_index_appends_rather_than_panicking() {
8687 let mut dl = list_of(vec![DisplayListItem::PopClip]);
8688 apply_clip_path(&mut dl, 1, rect(0.0, 0.0, 10.0, 10.0), 0.0);
8690 assert_eq!(dl.items.len(), 3);
8691 assert_eq!(dl.items.len(), dl.node_mapping.len());
8692 assert!(matches!(dl.items[1], DisplayListItem::PushClip { .. }));
8693 assert!(matches!(dl.items[2], DisplayListItem::PopClip));
8694 }
8695
8696 #[test]
8697 #[should_panic(expected = "insertion index")]
8698 fn apply_clip_path_past_the_end_panics_on_the_vec_insert() {
8699 let mut dl = DisplayList::default();
8703 apply_clip_path(&mut dl, 3, rect(0.0, 0.0, 10.0, 10.0), 0.0);
8704 }
8705}