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}