use std::{collections::{BTreeMap, HashMap}, sync::Arc};
use azul_core::{
dom::{DomId, FormattingContext, NodeId, NodeType, ScrollbarOrientation},
geom::{LogicalPosition, LogicalRect, LogicalSize},
gpu::GpuValueCache,
hit_test::ScrollPosition,
hit_test_tag::{CursorType, TAG_TYPE_CURSOR, TAG_TYPE_DOM_NODE},
resources::{
IdNamespace, ImageRef, OpacityKey, RendererResources, TransformKey,
},
transform::ComputedTransform3D,
selection::{Selection, SelectionRange, TextSelection},
styled_dom::StyledDom,
ui_solver::GlyphInstance,
};
use azul_css::{
css::CssPropertyValue,
format_rust_code::GetHash,
props::{
basic::{ColorU, FontRef, PixelValue},
layout::{LayoutDisplay, LayoutOverflow, LayoutPosition},
property::{CssProperty, CssPropertyType},
style::{
background::{ConicGradient, ExtendMode, LinearGradient, RadialGradient},
border_radius::StyleBorderRadius,
box_shadow::{BoxShadowClipMode, StyleBoxShadow},
filter::{StyleFilter, StyleFilterVec},
BorderStyle, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth,
LayoutBorderTopWidth, StyleBorderBottomColor, StyleBorderBottomStyle,
StyleBorderLeftColor, StyleBorderLeftStyle, StyleBorderRightColor,
StyleBorderRightStyle, StyleBorderTopColor, StyleBorderTopStyle,
},
},
LayoutDebugMessage,
};
#[cfg(feature = "text_layout")]
use crate::text3;
#[cfg(feature = "text_layout")]
use crate::text3::cache::{InlineShape, PositionedItem};
use crate::{
debug_info,
font_traits::{
FontHash, FontLoaderTrait, ImageSource, InlineContent, ParsedFontTrait, ShapedItem,
UnifiedLayout,
},
solver3::{
getters::{
get_background_color, get_background_contents, get_border_info, get_border_radius,
get_break_after, get_break_before, get_caret_style,
get_overflow_clip_margin_property, get_overflow_x, get_overflow_y,
get_scrollbar_gutter_property, get_scrollbar_info_from_layout, get_scrollbar_style, get_selection_style,
get_style_border_radius, get_visibility, get_z_index, is_forced_page_break, BorderInfo, CaretStyle,
ComputedScrollbarStyle, SelectionStyle,
},
layout_tree::{LayoutNode, LayoutNodeHot, LayoutNodeWarm, LayoutTree},
positioning::get_position_type,
scrollbar::{ScrollbarRequirements, compute_scrollbar_geometry_with_button_size},
LayoutContext, LayoutError, Result,
},
};
#[derive(Debug, Clone, Copy)]
pub struct StyleBorderWidths {
pub top: Option<CssPropertyValue<LayoutBorderTopWidth>>,
pub right: Option<CssPropertyValue<LayoutBorderRightWidth>>,
pub bottom: Option<CssPropertyValue<LayoutBorderBottomWidth>>,
pub left: Option<CssPropertyValue<LayoutBorderLeftWidth>>,
}
#[derive(Debug, Clone, Copy)]
pub struct StyleBorderColors {
pub top: Option<CssPropertyValue<StyleBorderTopColor>>,
pub right: Option<CssPropertyValue<StyleBorderRightColor>>,
pub bottom: Option<CssPropertyValue<StyleBorderBottomColor>>,
pub left: Option<CssPropertyValue<StyleBorderLeftColor>>,
}
#[derive(Debug, Clone, Copy)]
pub struct StyleBorderStyles {
pub top: Option<CssPropertyValue<StyleBorderTopStyle>>,
pub right: Option<CssPropertyValue<StyleBorderRightStyle>>,
pub bottom: Option<CssPropertyValue<StyleBorderBottomStyle>>,
pub left: Option<CssPropertyValue<StyleBorderLeftStyle>>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BorderBoxRect(pub LogicalRect);
#[derive(Debug, Copy, Clone, Default, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub struct WindowLogicalRect(pub LogicalRect);
impl WindowLogicalRect {
#[inline]
pub const fn new(origin: LogicalPosition, size: LogicalSize) -> Self {
Self(LogicalRect::new(origin, size))
}
#[inline]
pub const fn zero() -> Self {
Self(LogicalRect::zero())
}
#[inline]
pub const fn inner(&self) -> &LogicalRect {
&self.0
}
#[inline]
pub const fn into_inner(self) -> LogicalRect {
self.0
}
#[inline] pub fn origin(&self) -> LogicalPosition { self.0.origin }
#[inline] pub fn size(&self) -> LogicalSize { self.0.size }
}
impl From<LogicalRect> for WindowLogicalRect {
#[inline]
fn from(r: LogicalRect) -> Self { Self(r) }
}
impl From<WindowLogicalRect> for LogicalRect {
#[inline]
fn from(w: WindowLogicalRect) -> Self { w.0 }
}
#[derive(Debug, Clone, Copy)]
pub struct PhysicalSizeImport {
pub width: f32,
pub height: f32,
}
#[derive(Debug, Clone)]
pub struct ScrollbarDrawInfo {
pub bounds: WindowLogicalRect,
pub orientation: ScrollbarOrientation,
pub track_bounds: WindowLogicalRect,
pub track_color: ColorU,
pub thumb_bounds: WindowLogicalRect,
pub thumb_color: ColorU,
pub thumb_border_radius: BorderRadius,
pub button_decrement_bounds: Option<WindowLogicalRect>,
pub button_increment_bounds: Option<WindowLogicalRect>,
pub button_color: ColorU,
pub opacity_key: Option<OpacityKey>,
pub thumb_transform_key: Option<TransformKey>,
pub thumb_initial_transform: ComputedTransform3D,
pub hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
pub clip_to_container_border: bool,
pub container_border_radius: BorderRadius,
pub visibility: azul_css::props::style::scrollbar::ScrollbarVisibilityMode,
}
impl BorderBoxRect {
pub fn to_content_box(
self,
padding: &crate::solver3::geometry::EdgeSizes,
border: &crate::solver3::geometry::EdgeSizes,
) -> ContentBoxRect {
ContentBoxRect(LogicalRect {
origin: LogicalPosition {
x: self.0.origin.x + padding.left + border.left,
y: self.0.origin.y + padding.top + border.top,
},
size: LogicalSize {
width: self.0.size.width
- padding.left
- padding.right
- border.left
- border.right,
height: self.0.size.height
- padding.top
- padding.bottom
- border.top
- border.bottom,
},
})
}
pub fn rect(&self) -> LogicalRect {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ContentBoxRect(pub LogicalRect);
impl ContentBoxRect {
pub fn rect(&self) -> LogicalRect {
self.0
}
}
#[derive(Debug, Default, Clone)]
pub struct DisplayList {
pub items: Vec<DisplayListItem>,
pub node_mapping: Vec<Option<NodeId>>,
pub forced_page_breaks: Vec<f32>,
pub fixed_position_item_ranges: Vec<(usize, usize)>,
}
impl DisplayList {
pub fn patch_text_glyphs(
&mut self,
node_index: usize,
new_glyphs_by_run: &[Vec<GlyphInstance>],
) -> Option<LogicalRect> {
let mut run_idx = 0;
let mut damage: Option<LogicalRect> = None;
for item in &mut self.items {
if let DisplayListItem::Text {
ref mut glyphs,
ref clip_rect,
source_node_index: Some(src_idx),
..
} = item {
if *src_idx == node_index {
if run_idx < new_glyphs_by_run.len() {
*glyphs = new_glyphs_by_run[run_idx].clone();
let bounds = *clip_rect.inner();
damage = Some(match damage {
Some(d) => crate::cpurender::union_rect(&d, &bounds),
None => bounds,
});
run_idx += 1;
}
}
}
}
damage
}
pub fn compute_text_damage_rect(
old_items: &[super::super::text3::cache::PositionedItem],
new_items: &[super::super::text3::cache::PositionedItem],
container_origin: LogicalPosition,
affected_line: usize,
) -> LogicalRect {
let expand = |items: &[super::super::text3::cache::PositionedItem]| -> (f32, f32, f32, f32) {
let mut lx = f32::MAX;
let mut ly = f32::MAX;
let mut rx = f32::MIN;
let mut ry = f32::MIN;
for item in items {
if item.line_index >= affected_line {
let bounds = item.item.bounds();
let x = container_origin.x + item.position.x;
let y = container_origin.y + item.position.y;
lx = lx.min(x);
ly = ly.min(y);
rx = rx.max(x + bounds.width);
ry = ry.max(y + bounds.height);
}
}
(lx, ly, rx, ry)
};
let (olx, oly, orx, ory) = expand(old_items);
let (nlx, nly, nrx, nry) = expand(new_items);
let min_x = olx.min(nlx);
let min_y = oly.min(nly);
let max_x = orx.max(nrx);
let max_y = ory.max(nry);
if min_x > max_x || min_y > max_y {
return LogicalRect::default();
}
LogicalRect {
origin: LogicalPosition { x: min_x, y: min_y },
size: LogicalSize { width: max_x - min_x, height: max_y - min_y },
}
}
pub fn to_debug_json(&self) -> String {
use std::fmt::Write;
let mut json = String::new();
writeln!(json, "{{").unwrap();
writeln!(json, " \"total_items\": {},", self.items.len()).unwrap();
writeln!(json, " \"items\": [").unwrap();
let mut clip_depth = 0i32;
let mut scroll_depth = 0i32;
let mut stacking_depth = 0i32;
for (i, item) in self.items.iter().enumerate() {
let comma = if i < self.items.len() - 1 { "," } else { "" };
let node_id = self.node_mapping.get(i).and_then(|n| *n);
match item {
DisplayListItem::PushClip {
bounds,
border_radius,
} => {
clip_depth += 1;
writeln!(json, " {{").unwrap();
writeln!(json, " \"index\": {},", i).unwrap();
writeln!(json, " \"type\": \"PushClip\",").unwrap();
writeln!(json, " \"clip_depth\": {},", clip_depth).unwrap();
writeln!(json, " \"scroll_depth\": {},", scroll_depth).unwrap();
writeln!(json, " \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
writeln!(json, " \"border_radius\": {{ \"tl\": {:.1}, \"tr\": {:.1}, \"bl\": {:.1}, \"br\": {:.1} }},",
border_radius.top_left, border_radius.top_right,
border_radius.bottom_left, border_radius.bottom_right).unwrap();
writeln!(json, " \"node_id\": {:?}", node_id).unwrap();
writeln!(json, " }}{}", comma).unwrap();
}
DisplayListItem::PopClip => {
writeln!(json, " {{").unwrap();
writeln!(json, " \"index\": {},", i).unwrap();
writeln!(json, " \"type\": \"PopClip\",").unwrap();
writeln!(json, " \"clip_depth_before\": {},", clip_depth).unwrap();
writeln!(json, " \"clip_depth_after\": {}", clip_depth - 1).unwrap();
writeln!(json, " }}{}", comma).unwrap();
clip_depth -= 1;
}
DisplayListItem::PushScrollFrame {
clip_bounds,
content_size,
scroll_id,
} => {
scroll_depth += 1;
writeln!(json, " {{").unwrap();
writeln!(json, " \"index\": {},", i).unwrap();
writeln!(json, " \"type\": \"PushScrollFrame\",").unwrap();
writeln!(json, " \"clip_depth\": {},", clip_depth).unwrap();
writeln!(json, " \"scroll_depth\": {},", scroll_depth).unwrap();
writeln!(json, " \"clip_bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
clip_bounds.0.origin.x, clip_bounds.0.origin.y,
clip_bounds.0.size.width, clip_bounds.0.size.height).unwrap();
writeln!(
json,
" \"content_size\": {{ \"w\": {:.1}, \"h\": {:.1} }},",
content_size.width, content_size.height
)
.unwrap();
writeln!(json, " \"scroll_id\": {},", scroll_id).unwrap();
writeln!(json, " \"node_id\": {:?}", node_id).unwrap();
writeln!(json, " }}{}", comma).unwrap();
}
DisplayListItem::PopScrollFrame => {
writeln!(json, " {{").unwrap();
writeln!(json, " \"index\": {},", i).unwrap();
writeln!(json, " \"type\": \"PopScrollFrame\",").unwrap();
writeln!(json, " \"scroll_depth_before\": {},", scroll_depth).unwrap();
writeln!(json, " \"scroll_depth_after\": {}", scroll_depth - 1).unwrap();
writeln!(json, " }}{}", comma).unwrap();
scroll_depth -= 1;
}
DisplayListItem::PushStackingContext { z_index, bounds } => {
stacking_depth += 1;
writeln!(json, " {{").unwrap();
writeln!(json, " \"index\": {},", i).unwrap();
writeln!(json, " \"type\": \"PushStackingContext\",").unwrap();
writeln!(json, " \"stacking_depth\": {},", stacking_depth).unwrap();
writeln!(json, " \"z_index\": {},", z_index).unwrap();
writeln!(json, " \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }}",
bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
writeln!(json, " }}{}", comma).unwrap();
}
DisplayListItem::PopStackingContext => {
writeln!(json, " {{").unwrap();
writeln!(json, " \"index\": {},", i).unwrap();
writeln!(json, " \"type\": \"PopStackingContext\",").unwrap();
writeln!(json, " \"stacking_depth_before\": {},", stacking_depth).unwrap();
writeln!(
json,
" \"stacking_depth_after\": {}",
stacking_depth - 1
)
.unwrap();
writeln!(json, " }}{}", comma).unwrap();
stacking_depth -= 1;
}
DisplayListItem::Rect {
bounds,
color,
border_radius,
} => {
writeln!(json, " {{").unwrap();
writeln!(json, " \"index\": {},", i).unwrap();
writeln!(json, " \"type\": \"Rect\",").unwrap();
writeln!(json, " \"clip_depth\": {},", clip_depth).unwrap();
writeln!(json, " \"scroll_depth\": {},", scroll_depth).unwrap();
writeln!(json, " \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
writeln!(
json,
" \"color\": \"rgba({},{},{},{})\",",
color.r, color.g, color.b, color.a
)
.unwrap();
writeln!(json, " \"node_id\": {:?}", node_id).unwrap();
writeln!(json, " }}{}", comma).unwrap();
}
DisplayListItem::Border { bounds, .. } => {
writeln!(json, " {{").unwrap();
writeln!(json, " \"index\": {},", i).unwrap();
writeln!(json, " \"type\": \"Border\",").unwrap();
writeln!(json, " \"clip_depth\": {},", clip_depth).unwrap();
writeln!(json, " \"scroll_depth\": {},", scroll_depth).unwrap();
writeln!(json, " \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
writeln!(json, " \"node_id\": {:?}", node_id).unwrap();
writeln!(json, " }}{}", comma).unwrap();
}
DisplayListItem::ScrollBarStyled { info } => {
writeln!(json, " {{").unwrap();
writeln!(json, " \"index\": {},", i).unwrap();
writeln!(json, " \"type\": \"ScrollBarStyled\",").unwrap();
writeln!(json, " \"clip_depth\": {},", clip_depth).unwrap();
writeln!(json, " \"scroll_depth\": {},", scroll_depth).unwrap();
writeln!(json, " \"orientation\": \"{:?}\",", info.orientation).unwrap();
writeln!(json, " \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }}",
info.bounds.0.origin.x, info.bounds.0.origin.y,
info.bounds.0.size.width, info.bounds.0.size.height).unwrap();
writeln!(json, " }}{}", comma).unwrap();
}
_ => {
writeln!(json, " {{").unwrap();
writeln!(json, " \"index\": {},", i).unwrap();
writeln!(
json,
" \"type\": \"{:?}\",",
std::mem::discriminant(item)
)
.unwrap();
writeln!(json, " \"clip_depth\": {},", clip_depth).unwrap();
writeln!(json, " \"scroll_depth\": {},", scroll_depth).unwrap();
writeln!(json, " \"node_id\": {:?}", node_id).unwrap();
writeln!(json, " }}{}", comma).unwrap();
}
}
}
writeln!(json, " ],").unwrap();
writeln!(json, " \"final_clip_depth\": {},", clip_depth).unwrap();
writeln!(json, " \"final_scroll_depth\": {},", scroll_depth).unwrap();
writeln!(json, " \"final_stacking_depth\": {},", stacking_depth).unwrap();
writeln!(
json,
" \"balanced\": {}",
clip_depth == 0 && scroll_depth == 0 && stacking_depth == 0
)
.unwrap();
writeln!(json, "}}").unwrap();
json
}
}
#[derive(Debug, Clone)]
pub enum DisplayListItem {
Rect {
bounds: WindowLogicalRect,
color: ColorU,
border_radius: BorderRadius,
},
SelectionRect {
bounds: WindowLogicalRect,
border_radius: BorderRadius,
color: ColorU,
},
CursorRect {
bounds: WindowLogicalRect,
color: ColorU,
},
Border {
bounds: WindowLogicalRect,
widths: StyleBorderWidths,
colors: StyleBorderColors,
styles: StyleBorderStyles,
border_radius: StyleBorderRadius,
},
TextLayout {
layout: Arc<dyn std::any::Any + Send + Sync>, bounds: WindowLogicalRect,
font_hash: FontHash,
font_size_px: f32,
color: ColorU,
},
Text {
glyphs: Vec<GlyphInstance>,
font_hash: FontHash,
font_size_px: f32,
color: ColorU,
clip_rect: WindowLogicalRect,
source_node_index: Option<usize>,
},
Underline {
bounds: WindowLogicalRect,
color: ColorU,
thickness: f32,
},
Strikethrough {
bounds: WindowLogicalRect,
color: ColorU,
thickness: f32,
},
Overline {
bounds: WindowLogicalRect,
color: ColorU,
thickness: f32,
},
Image {
bounds: WindowLogicalRect,
image: ImageRef,
border_radius: BorderRadius,
},
ScrollBar {
bounds: WindowLogicalRect,
color: ColorU,
orientation: ScrollbarOrientation,
opacity_key: Option<OpacityKey>,
hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
},
ScrollBarStyled {
info: Box<ScrollbarDrawInfo>,
},
VirtualView {
child_dom_id: DomId,
bounds: WindowLogicalRect,
clip_rect: WindowLogicalRect,
},
VirtualViewPlaceholder {
node_id: NodeId,
bounds: WindowLogicalRect,
clip_rect: WindowLogicalRect,
},
PushClip {
bounds: WindowLogicalRect,
border_radius: BorderRadius,
},
PopClip,
PushImageMaskClip {
bounds: WindowLogicalRect,
mask_image: ImageRef,
mask_rect: WindowLogicalRect,
},
PopImageMaskClip,
PushScrollFrame {
clip_bounds: WindowLogicalRect,
content_size: LogicalSize,
scroll_id: LocalScrollId,
},
PopScrollFrame,
PushStackingContext {
z_index: i32,
bounds: WindowLogicalRect,
},
PopStackingContext,
PushReferenceFrame {
transform_key: TransformKey,
initial_transform: ComputedTransform3D,
bounds: WindowLogicalRect,
},
PopReferenceFrame,
HitTestArea {
bounds: WindowLogicalRect,
tag: DisplayListTagId, },
LinearGradient {
bounds: WindowLogicalRect,
gradient: LinearGradient,
border_radius: BorderRadius,
},
RadialGradient {
bounds: WindowLogicalRect,
gradient: RadialGradient,
border_radius: BorderRadius,
},
ConicGradient {
bounds: WindowLogicalRect,
gradient: ConicGradient,
border_radius: BorderRadius,
},
BoxShadow {
bounds: WindowLogicalRect,
shadow: StyleBoxShadow,
border_radius: BorderRadius,
},
PushFilter {
bounds: WindowLogicalRect,
filters: Vec<StyleFilter>,
},
PopFilter,
PushBackdropFilter {
bounds: WindowLogicalRect,
filters: Vec<StyleFilter>,
},
PopBackdropFilter,
PushOpacity {
bounds: WindowLogicalRect,
opacity: f32,
},
PopOpacity,
PushTextShadow {
shadow: azul_css::props::style::box_shadow::StyleBoxShadow,
},
PopTextShadow,
}
impl DisplayListItem {
pub fn is_visually_equal(&self, other: &Self) -> bool {
if std::mem::discriminant(self) != std::mem::discriminant(other) {
return false;
}
match (self, other) {
(Self::Rect { bounds: b1, color: c1, border_radius: br1 },
Self::Rect { bounds: b2, color: c2, border_radius: br2 }) => {
b1 == b2 && c1 == c2 && br1.top_left == br2.top_left && br1.top_right == br2.top_right
&& br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
}
(Self::SelectionRect { bounds: b1, border_radius: br1, color: c1 },
Self::SelectionRect { bounds: b2, border_radius: br2, color: c2 }) => {
b1 == b2 && c1 == c2 && br1.top_left == br2.top_left && br1.top_right == br2.top_right
&& br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
}
(Self::CursorRect { bounds: b1, color: c1 },
Self::CursorRect { bounds: b2, color: c2 }) => b1 == b2 && c1 == c2,
(Self::Text { glyphs: g1, font_hash: fh1, font_size_px: fs1, color: c1, clip_rect: cr1, .. },
Self::Text { glyphs: g2, font_hash: fh2, font_size_px: fs2, color: c2, clip_rect: cr2, .. }) => {
cr1 == cr2 && c1 == c2 && fh1 == fh2 && fs1 == fs2 && g1.len() == g2.len()
&& g1.iter().zip(g2.iter()).all(|(a, b)| {
a.index == b.index
&& a.point.x == b.point.x
&& a.point.y == b.point.y
})
}
(Self::Underline { bounds: b1, color: c1, thickness: t1 },
Self::Underline { bounds: b2, color: c2, thickness: t2 }) => b1 == b2 && c1 == c2 && t1 == t2,
(Self::Strikethrough { bounds: b1, color: c1, thickness: t1 },
Self::Strikethrough { bounds: b2, color: c2, thickness: t2 }) => b1 == b2 && c1 == c2 && t1 == t2,
(Self::Overline { bounds: b1, color: c1, thickness: t1 },
Self::Overline { bounds: b2, color: c2, thickness: t2 }) => b1 == b2 && c1 == c2 && t1 == t2,
(Self::Border { bounds: b1, widths: w1, colors: c1, styles: s1, .. },
Self::Border { bounds: b2, widths: w2, colors: c2, styles: s2, .. }) => {
b1 == b2
&& w1.top == w2.top && w1.right == w2.right && w1.bottom == w2.bottom && w1.left == w2.left
&& c1.top == c2.top && c1.right == c2.right && c1.bottom == c2.bottom && c1.left == c2.left
&& s1.top == s2.top && s1.right == s2.right && s1.bottom == s2.bottom && s1.left == s2.left
}
(Self::Image { bounds: b1, image: i1, border_radius: br1 },
Self::Image { bounds: b2, image: i2, border_radius: br2 }) => {
b1 == b2
&& i1.data as usize == i2.data as usize && br1.top_left == br2.top_left && br1.top_right == br2.top_right
&& br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
}
(Self::BoxShadow { bounds: b1, shadow: s1, border_radius: br1 },
Self::BoxShadow { bounds: b2, shadow: s2, border_radius: br2 }) => {
b1 == b2 && s1 == s2
&& br1.top_left == br2.top_left && br1.top_right == br2.top_right
&& br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
}
(Self::LinearGradient { bounds: b1, gradient: g1, border_radius: br1 },
Self::LinearGradient { bounds: b2, gradient: g2, border_radius: br2 }) => {
b1 == b2 && g1 == g2
&& br1.top_left == br2.top_left && br1.top_right == br2.top_right
&& br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
}
(Self::RadialGradient { bounds: b1, gradient: g1, border_radius: br1 },
Self::RadialGradient { bounds: b2, gradient: g2, border_radius: br2 }) => {
b1 == b2 && g1 == g2
&& br1.top_left == br2.top_left && br1.top_right == br2.top_right
&& br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
}
(Self::ConicGradient { bounds: b1, gradient: g1, border_radius: br1 },
Self::ConicGradient { bounds: b2, gradient: g2, border_radius: br2 }) => {
b1 == b2 && g1 == g2
&& br1.top_left == br2.top_left && br1.top_right == br2.top_right
&& br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
}
(Self::ScrollBar { bounds: b1, color: c1, .. },
Self::ScrollBar { bounds: b2, color: c2, .. }) => b1 == b2 && c1 == c2,
(Self::PushClip { bounds: b1, .. }, Self::PushClip { bounds: b2, .. }) => b1 == b2,
(Self::PushScrollFrame { clip_bounds: b1, scroll_id: s1, .. },
Self::PushScrollFrame { clip_bounds: b2, scroll_id: s2, .. }) => b1 == b2 && s1 == s2,
(Self::PushStackingContext { z_index: z1, bounds: b1 },
Self::PushStackingContext { z_index: z2, bounds: b2 }) => z1 == z2 && b1 == b2,
(Self::PushOpacity { bounds: b1, opacity: o1 },
Self::PushOpacity { bounds: b2, opacity: o2 }) => b1 == b2 && o1 == o2,
(Self::PopClip, Self::PopClip)
| (Self::PopImageMaskClip, Self::PopImageMaskClip)
| (Self::PopScrollFrame, Self::PopScrollFrame)
| (Self::PopStackingContext, Self::PopStackingContext)
| (Self::PopReferenceFrame, Self::PopReferenceFrame)
| (Self::PopFilter, Self::PopFilter)
| (Self::PopBackdropFilter, Self::PopBackdropFilter)
| (Self::PopOpacity, Self::PopOpacity)
| (Self::PopTextShadow, Self::PopTextShadow) => true,
_ => false,
}
}
pub fn is_state_management(&self) -> bool {
matches!(self,
Self::PushClip { .. }
| Self::PopClip
| Self::PushImageMaskClip { .. }
| Self::PopImageMaskClip
| Self::PushScrollFrame { .. }
| Self::PopScrollFrame
| Self::PushStackingContext { .. }
| Self::PopStackingContext
| Self::PushReferenceFrame { .. }
| Self::PopReferenceFrame
| Self::PushFilter { .. }
| Self::PopFilter
| Self::PushBackdropFilter { .. }
| Self::PopBackdropFilter
| Self::PushOpacity { .. }
| Self::PopOpacity
| Self::PushTextShadow { .. }
| Self::PopTextShadow
)
}
pub fn visual_bounds(&self) -> Option<LogicalRect> {
match self {
Self::BoxShadow { bounds, shadow, .. } => {
let b = *bounds.inner();
let ox = shadow.offset_x.to_pixels_internal(16.0, 16.0).abs();
let oy = shadow.offset_y.to_pixels_internal(16.0, 16.0).abs();
let blur = shadow.blur_radius.to_pixels_internal(16.0, 16.0).abs();
let spread = shadow.spread_radius.to_pixels_internal(16.0, 16.0).abs();
let expand = ox + oy + blur + spread;
Some(LogicalRect {
origin: LogicalPosition {
x: b.origin.x - expand,
y: b.origin.y - expand,
},
size: LogicalSize {
width: b.size.width + expand * 2.0,
height: b.size.height + expand * 2.0,
},
})
}
_ => self.bounds(),
}
}
pub fn bounds(&self) -> Option<LogicalRect> {
match self {
Self::Rect { bounds, .. }
| Self::SelectionRect { bounds, .. }
| Self::CursorRect { bounds, .. }
| Self::Border { bounds, .. }
| Self::Text { clip_rect: bounds, .. }
| Self::TextLayout { bounds, .. }
| Self::Underline { bounds, .. }
| Self::Strikethrough { bounds, .. }
| Self::Overline { bounds, .. }
| Self::Image { bounds, .. }
| Self::ScrollBar { bounds, .. }
| Self::LinearGradient { bounds, .. }
| Self::RadialGradient { bounds, .. }
| Self::ConicGradient { bounds, .. }
| Self::BoxShadow { bounds, .. }
| Self::VirtualView { bounds, .. }
| Self::VirtualViewPlaceholder { bounds, .. }
| Self::HitTestArea { bounds, .. }
| Self::PushClip { bounds, .. }
| Self::PushImageMaskClip { bounds, .. }
| Self::PushScrollFrame { clip_bounds: bounds, .. }
| Self::PushStackingContext { bounds, .. }
| Self::PushReferenceFrame { bounds, .. }
| Self::PushFilter { bounds, .. }
| Self::PushBackdropFilter { bounds, .. }
| Self::PushOpacity { bounds, .. } => Some(*bounds.inner()),
Self::ScrollBarStyled { info, .. } => Some(*info.bounds.inner()),
Self::PushTextShadow { .. } => None, Self::PopClip
| Self::PopImageMaskClip
| Self::PopScrollFrame
| Self::PopStackingContext
| Self::PopReferenceFrame
| Self::PopFilter
| Self::PopBackdropFilter
| Self::PopOpacity
| Self::PopTextShadow => None,
}
}
}
#[derive(Debug, Copy, Clone, Default)]
pub struct BorderRadius {
pub top_left: f32,
pub top_right: f32,
pub bottom_left: f32,
pub bottom_right: f32,
}
impl BorderRadius {
pub fn is_zero(&self) -> bool {
self.top_left == 0.0
&& self.top_right == 0.0
&& self.bottom_left == 0.0
&& self.bottom_right == 0.0
}
}
pub type LocalScrollId = u64;
pub type DisplayListTagId = (u64, u16);
#[derive(Debug, Default)]
struct DisplayListBuilder {
items: Vec<DisplayListItem>,
node_mapping: Vec<Option<NodeId>>,
current_node: Option<NodeId>,
debug_messages: Vec<LayoutDebugMessage>,
debug_enabled: bool,
forced_page_breaks: Vec<f32>,
fixed_position_item_ranges: Vec<(usize, usize)>,
fixed_position_start: Option<usize>,
}
impl DisplayListBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_debug(debug_enabled: bool) -> Self {
Self {
items: Vec::new(),
node_mapping: Vec::new(),
current_node: None,
debug_messages: Vec::new(),
debug_enabled,
forced_page_breaks: Vec::new(),
fixed_position_item_ranges: Vec::new(),
fixed_position_start: None,
}
}
fn debug_log(&mut self, message: String) {
if self.debug_enabled {
self.debug_messages.push(LayoutDebugMessage::info(message));
}
}
pub fn build_with_debug(
mut self,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> DisplayList {
if let Some(msgs) = debug_messages.as_mut() {
msgs.append(&mut self.debug_messages);
}
DisplayList {
items: self.items,
node_mapping: self.node_mapping,
forced_page_breaks: self.forced_page_breaks,
fixed_position_item_ranges: self.fixed_position_item_ranges,
}
}
pub fn set_current_node(&mut self, node_id: Option<NodeId>) {
self.current_node = node_id;
}
pub fn begin_fixed_position_element(&mut self) {
self.fixed_position_start = Some(self.items.len());
}
pub fn end_fixed_position_element(&mut self) {
if let Some(start) = self.fixed_position_start.take() {
let end = self.items.len();
if end > start {
self.fixed_position_item_ranges.push((start, end));
}
}
}
pub fn add_forced_page_break(&mut self, y_position: f32) {
if !self.forced_page_breaks.contains(&y_position) {
self.forced_page_breaks.push(y_position);
self.forced_page_breaks.sort_by(|a, b| a.partial_cmp(b).unwrap());
}
}
fn push_item(&mut self, item: DisplayListItem) {
self.items.push(item);
self.node_mapping.push(self.current_node);
}
pub fn build(self) -> DisplayList {
DisplayList {
items: self.items,
node_mapping: self.node_mapping,
forced_page_breaks: self.forced_page_breaks,
fixed_position_item_ranges: self.fixed_position_item_ranges,
}
}
pub fn push_hit_test_area(&mut self, bounds: LogicalRect, tag: DisplayListTagId) {
self.push_item(DisplayListItem::HitTestArea { bounds: bounds.into(), tag });
}
pub fn push_scrollbar(
&mut self,
bounds: LogicalRect,
color: ColorU,
orientation: ScrollbarOrientation,
opacity_key: Option<OpacityKey>,
hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
) {
if color.a > 0 || opacity_key.is_some() {
self.push_item(DisplayListItem::ScrollBar {
bounds: bounds.into(),
color,
orientation,
opacity_key,
hit_id,
});
}
}
pub fn push_scrollbar_styled(&mut self, info: ScrollbarDrawInfo) {
if info.thumb_color.a > 0 || info.track_color.a > 0 || info.opacity_key.is_some() {
self.push_item(DisplayListItem::ScrollBarStyled {
info: Box::new(info),
});
}
}
pub fn push_rect(&mut self, bounds: LogicalRect, color: ColorU, border_radius: BorderRadius) {
if color.a > 0 {
self.push_item(DisplayListItem::Rect {
bounds: bounds.into(),
color,
border_radius,
});
}
}
pub fn push_backgrounds_and_border(
&mut self,
bounds: LogicalRect,
background_contents: &[azul_css::props::style::StyleBackgroundContent],
border_info: &BorderInfo,
simple_border_radius: BorderRadius,
style_border_radius: StyleBorderRadius,
image_cache: &azul_core::resources::ImageCache,
) {
use azul_css::props::style::StyleBackgroundContent;
for bg in background_contents {
match bg {
StyleBackgroundContent::Color(color) => {
self.push_rect(bounds, *color, simple_border_radius);
}
StyleBackgroundContent::LinearGradient(gradient) => {
self.push_linear_gradient(bounds, gradient.clone(), simple_border_radius);
}
StyleBackgroundContent::RadialGradient(gradient) => {
self.push_radial_gradient(bounds, gradient.clone(), simple_border_radius);
}
StyleBackgroundContent::ConicGradient(gradient) => {
self.push_conic_gradient(bounds, gradient.clone(), simple_border_radius);
}
StyleBackgroundContent::Image(image_id) => {
if let Some(image_ref) = image_cache.get_css_image_id(image_id) {
self.push_image(bounds, image_ref.clone(), simple_border_radius);
}
}
}
}
self.push_border(
bounds,
border_info.widths,
border_info.colors,
border_info.styles,
style_border_radius,
);
}
pub fn push_inline_backgrounds_and_border(
&mut self,
bounds: LogicalRect,
background_color: Option<ColorU>,
background_contents: &[azul_css::props::style::StyleBackgroundContent],
border: Option<&crate::text3::cache::InlineBorderInfo>,
image_cache: &azul_core::resources::ImageCache,
) {
use azul_css::props::style::StyleBackgroundContent;
if let Some(bg_color) = background_color {
self.push_rect(bounds, bg_color, BorderRadius::default());
}
for bg in background_contents {
match bg {
StyleBackgroundContent::Color(color) => {
self.push_rect(bounds, *color, BorderRadius::default());
}
StyleBackgroundContent::LinearGradient(gradient) => {
self.push_linear_gradient(bounds, gradient.clone(), BorderRadius::default());
}
StyleBackgroundContent::RadialGradient(gradient) => {
self.push_radial_gradient(bounds, gradient.clone(), BorderRadius::default());
}
StyleBackgroundContent::ConicGradient(gradient) => {
self.push_conic_gradient(bounds, gradient.clone(), BorderRadius::default());
}
StyleBackgroundContent::Image(image_id) => {
if let Some(image_ref) = image_cache.get_css_image_id(image_id) {
self.push_image(bounds, image_ref.clone(), BorderRadius::default());
}
}
}
}
if let Some(border) = border {
let effective_left = if border.left_inset() > 0.0 { border.left } else { 0.0 };
let effective_right = if border.right_inset() > 0.0 { border.right } else { 0.0 };
if border.top > 0.0 || effective_right > 0.0 || border.bottom > 0.0 || effective_left > 0.0 {
let border_widths = StyleBorderWidths {
top: Some(CssPropertyValue::Exact(LayoutBorderTopWidth {
inner: PixelValue::px(border.top),
})),
right: Some(CssPropertyValue::Exact(LayoutBorderRightWidth {
inner: PixelValue::px(effective_right),
})),
bottom: Some(CssPropertyValue::Exact(LayoutBorderBottomWidth {
inner: PixelValue::px(border.bottom),
})),
left: Some(CssPropertyValue::Exact(LayoutBorderLeftWidth {
inner: PixelValue::px(effective_left),
})),
};
let border_colors = StyleBorderColors {
top: Some(CssPropertyValue::Exact(StyleBorderTopColor {
inner: border.top_color,
})),
right: Some(CssPropertyValue::Exact(StyleBorderRightColor {
inner: border.right_color,
})),
bottom: Some(CssPropertyValue::Exact(StyleBorderBottomColor {
inner: border.bottom_color,
})),
left: Some(CssPropertyValue::Exact(StyleBorderLeftColor {
inner: border.left_color,
})),
};
let border_styles = StyleBorderStyles {
top: Some(CssPropertyValue::Exact(StyleBorderTopStyle {
inner: BorderStyle::Solid,
})),
right: Some(CssPropertyValue::Exact(StyleBorderRightStyle {
inner: BorderStyle::Solid,
})),
bottom: Some(CssPropertyValue::Exact(StyleBorderBottomStyle {
inner: BorderStyle::Solid,
})),
left: Some(CssPropertyValue::Exact(StyleBorderLeftStyle {
inner: BorderStyle::Solid,
})),
};
let radius_px = PixelValue::px(border.radius.unwrap_or(0.0));
let border_radius = StyleBorderRadius {
top_left: radius_px,
top_right: radius_px,
bottom_left: radius_px,
bottom_right: radius_px,
};
self.push_border(
bounds,
border_widths,
border_colors,
border_styles,
border_radius,
);
}
}
}
pub fn push_linear_gradient(
&mut self,
bounds: LogicalRect,
gradient: LinearGradient,
border_radius: BorderRadius,
) {
self.push_item(DisplayListItem::LinearGradient {
bounds: bounds.into(),
gradient,
border_radius,
});
}
pub fn push_radial_gradient(
&mut self,
bounds: LogicalRect,
gradient: RadialGradient,
border_radius: BorderRadius,
) {
self.push_item(DisplayListItem::RadialGradient {
bounds: bounds.into(),
gradient,
border_radius,
});
}
pub fn push_conic_gradient(
&mut self,
bounds: LogicalRect,
gradient: ConicGradient,
border_radius: BorderRadius,
) {
self.push_item(DisplayListItem::ConicGradient {
bounds: bounds.into(),
gradient,
border_radius,
});
}
pub fn push_selection_rect(
&mut self,
bounds: LogicalRect,
color: ColorU,
border_radius: BorderRadius,
) {
if color.a > 0 {
self.push_item(DisplayListItem::SelectionRect {
bounds: bounds.into(),
color,
border_radius,
});
}
}
pub fn push_cursor_rect(&mut self, bounds: LogicalRect, color: ColorU) {
if color.a > 0 {
self.push_item(DisplayListItem::CursorRect { bounds: bounds.into(), color });
}
}
pub fn push_clip(&mut self, bounds: LogicalRect, border_radius: BorderRadius) {
self.push_item(DisplayListItem::PushClip {
bounds: bounds.into(),
border_radius,
});
}
pub fn pop_clip(&mut self) {
self.push_item(DisplayListItem::PopClip);
}
pub fn push_image_mask_clip(&mut self, bounds: LogicalRect, mask_image: ImageRef, mask_rect: LogicalRect) {
self.push_item(DisplayListItem::PushImageMaskClip {
bounds: bounds.into(),
mask_image,
mask_rect: mask_rect.into(),
});
}
pub fn pop_image_mask_clip(&mut self) {
self.push_item(DisplayListItem::PopImageMaskClip);
}
pub fn push_scroll_frame(
&mut self,
clip_bounds: LogicalRect,
content_size: LogicalSize,
scroll_id: LocalScrollId,
) {
self.push_item(DisplayListItem::PushScrollFrame {
clip_bounds: clip_bounds.into(),
content_size,
scroll_id,
});
}
pub fn pop_scroll_frame(&mut self) {
self.push_item(DisplayListItem::PopScrollFrame);
}
pub fn push_virtual_view_placeholder(
&mut self,
node_id: NodeId,
bounds: LogicalRect,
clip_rect: LogicalRect,
) {
self.push_item(DisplayListItem::VirtualViewPlaceholder {
node_id,
bounds: bounds.into(),
clip_rect: clip_rect.into(),
});
}
pub fn push_border(
&mut self,
bounds: LogicalRect,
widths: StyleBorderWidths,
colors: StyleBorderColors,
styles: StyleBorderStyles,
border_radius: StyleBorderRadius,
) {
let has_visible_border = {
let has_width = widths.top.is_some()
|| widths.right.is_some()
|| widths.bottom.is_some()
|| widths.left.is_some();
let has_style = styles.top.is_some()
|| styles.right.is_some()
|| styles.bottom.is_some()
|| styles.left.is_some();
has_width && has_style
};
if has_visible_border {
self.push_item(DisplayListItem::Border {
bounds: bounds.into(),
widths,
colors,
styles,
border_radius,
});
}
}
pub fn push_stacking_context(&mut self, z_index: i32, bounds: LogicalRect) {
self.push_item(DisplayListItem::PushStackingContext { z_index, bounds: bounds.into() });
}
pub fn pop_stacking_context(&mut self) {
self.push_item(DisplayListItem::PopStackingContext);
}
pub fn push_reference_frame(
&mut self,
transform_key: TransformKey,
initial_transform: ComputedTransform3D,
bounds: LogicalRect,
) {
self.push_item(DisplayListItem::PushReferenceFrame {
transform_key,
initial_transform,
bounds: bounds.into(),
});
}
pub fn pop_reference_frame(&mut self) {
self.push_item(DisplayListItem::PopReferenceFrame);
}
pub fn push_text_run(
&mut self,
glyphs: Vec<GlyphInstance>,
font_hash: FontHash, font_size_px: f32,
color: ColorU,
clip_rect: LogicalRect,
source_node_index: Option<usize>,
) {
self.debug_log(format!(
"[push_text_run] {} glyphs, font_size={}px, color=({},{},{},{}), clip={:?}",
glyphs.len(),
font_size_px,
color.r,
color.g,
color.b,
color.a,
clip_rect
));
if !glyphs.is_empty() && color.a > 0 {
self.push_item(DisplayListItem::Text {
glyphs,
font_hash,
font_size_px,
color,
clip_rect: clip_rect.into(),
source_node_index,
});
} else {
self.debug_log(format!(
"[push_text_run] SKIPPED: glyphs.is_empty()={}, color.a={}",
glyphs.is_empty(),
color.a
));
}
}
pub fn push_text_layout(
&mut self,
layout: Arc<dyn std::any::Any + Send + Sync>,
bounds: LogicalRect,
font_hash: FontHash,
font_size_px: f32,
color: ColorU,
) {
if color.a > 0 {
self.push_item(DisplayListItem::TextLayout {
layout,
bounds: bounds.into(),
font_hash,
font_size_px,
color,
});
}
}
pub fn push_underline(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
if color.a > 0 && thickness > 0.0 {
self.push_item(DisplayListItem::Underline {
bounds: bounds.into(),
color,
thickness,
});
}
}
pub fn push_strikethrough(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
if color.a > 0 && thickness > 0.0 {
self.push_item(DisplayListItem::Strikethrough {
bounds: bounds.into(),
color,
thickness,
});
}
}
pub fn push_overline(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
if color.a > 0 && thickness > 0.0 {
self.push_item(DisplayListItem::Overline {
bounds: bounds.into(),
color,
thickness,
});
}
}
pub fn push_image(&mut self, bounds: LogicalRect, image: ImageRef, border_radius: BorderRadius) {
self.push_item(DisplayListItem::Image { bounds: bounds.into(), image, border_radius });
}
}
pub fn generate_display_list<T: ParsedFontTrait + Sync + 'static>(
ctx: &mut LayoutContext<T>,
tree: &LayoutTree,
calculated_positions: &super::PositionVec,
scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
scroll_ids: &HashMap<usize, u64>,
gpu_value_cache: Option<&GpuValueCache>,
renderer_resources: &RendererResources,
id_namespace: IdNamespace,
dom_id: DomId,
) -> Result<DisplayList> {
debug_info!(
ctx,
"[DisplayList] generate_display_list: tree has {} nodes, {} positions calculated",
tree.nodes.len(),
calculated_positions.len()
);
debug_info!(ctx, "Starting display list generation");
debug_info!(
ctx,
"Collecting stacking contexts from root node {}",
tree.root
);
let positioned_tree = PositionedTree {
tree,
calculated_positions,
};
let mut generator = DisplayListGenerator::new(
ctx,
scroll_offsets,
&positioned_tree,
scroll_ids,
gpu_value_cache,
renderer_resources,
id_namespace,
dom_id,
);
let debug_enabled = generator.ctx.debug_messages.is_some();
let mut builder = DisplayListBuilder::with_debug(debug_enabled);
{
let root_node = tree.get(tree.root);
if let Some(root) = root_node {
if let Some(root_dom_id) = root.dom_node_id {
let root_state = generator.get_styled_node_state(root_dom_id);
let canvas_bg = get_background_color(
generator.ctx.styled_dom,
root_dom_id,
&root_state,
);
if canvas_bg.a > 0 {
let viewport_rect = LogicalRect {
origin: LogicalPosition::zero(),
size: generator.ctx.viewport_size,
};
builder.push_rect(viewport_rect, canvas_bg, BorderRadius::default());
debug_info!(
generator.ctx,
"[DisplayList] Canvas background: color=({},{},{},{}), size={:?}",
canvas_bg.r, canvas_bg.g, canvas_bg.b, canvas_bg.a,
generator.ctx.viewport_size
);
}
}
}
}
let stacking_context_tree = generator.collect_stacking_contexts(tree.root)?;
debug_info!(
generator.ctx,
"Generating display items from stacking context tree"
);
generator.generate_for_stacking_context(&mut builder, &stacking_context_tree)?;
let display_list = builder.build_with_debug(generator.ctx.debug_messages);
debug_info!(
generator.ctx,
"[DisplayList] Generated {} display items",
display_list.items.len()
);
Ok(display_list)
}
struct DisplayListGenerator<'a, 'b, T: ParsedFontTrait> {
ctx: &'a mut LayoutContext<'b, T>,
scroll_offsets: &'a BTreeMap<NodeId, ScrollPosition>,
positioned_tree: &'a PositionedTree<'a>,
scroll_ids: &'a HashMap<usize, u64>,
gpu_value_cache: Option<&'a GpuValueCache>,
renderer_resources: &'a RendererResources,
id_namespace: IdNamespace,
dom_id: DomId,
}
#[derive(Debug)]
struct StackingContext {
node_index: usize,
z_index: i32,
child_contexts: Vec<StackingContext>,
in_flow_children: Vec<usize>,
}
impl<'a, 'b, T> DisplayListGenerator<'a, 'b, T>
where
T: ParsedFontTrait + Sync + 'static,
{
pub fn new(
ctx: &'a mut LayoutContext<'b, T>,
scroll_offsets: &'a BTreeMap<NodeId, ScrollPosition>,
positioned_tree: &'a PositionedTree<'a>,
scroll_ids: &'a HashMap<usize, u64>,
gpu_value_cache: Option<&'a GpuValueCache>,
renderer_resources: &'a RendererResources,
id_namespace: IdNamespace,
dom_id: DomId,
) -> Self {
Self {
ctx,
scroll_offsets,
positioned_tree,
scroll_ids,
gpu_value_cache,
renderer_resources,
id_namespace,
dom_id,
}
}
fn get_styled_node_state(&self, dom_id: NodeId) -> azul_core::styled_dom::StyledNodeState {
self.ctx
.styled_dom
.styled_nodes
.as_container()
.get(dom_id)
.map(|n| n.styled_node_state.clone())
.unwrap_or_default()
}
fn is_node_hidden(&self, node_index: usize) -> bool {
use azul_css::props::style::effects::StyleVisibility;
let node = match self.positioned_tree.tree.get(node_index) {
Some(n) => n,
None => return false,
};
let dom_id = match node.dom_node_id {
Some(id) => id,
None => return false,
};
let node_state = self.get_styled_node_state(dom_id);
match get_visibility(self.ctx.styled_dom, dom_id, &node_state) {
crate::solver3::getters::MultiValue::Exact(StyleVisibility::Hidden)
| crate::solver3::getters::MultiValue::Exact(StyleVisibility::Collapse) => true,
_ => false,
}
}
fn get_cursor_type_for_text_node(&self, node_id: NodeId) -> CursorType {
use azul_css::props::style::effects::StyleCursor;
let styled_node_state = self.get_styled_node_state(node_id);
let node_data_container = self.ctx.styled_dom.node_data.as_container();
let node_data = node_data_container.get(node_id);
if let Some(node_data) = node_data {
if let Some(cursor_value) = self.ctx.styled_dom.get_css_property_cache().get_cursor(
node_data,
&node_id,
&styled_node_state,
) {
if let CssPropertyValue::Exact(cursor) = cursor_value {
return match cursor {
StyleCursor::Default => CursorType::Default,
StyleCursor::Pointer => CursorType::Pointer,
StyleCursor::Text => CursorType::Text,
StyleCursor::Crosshair => CursorType::Crosshair,
StyleCursor::Move => CursorType::Move,
StyleCursor::Help => CursorType::Help,
StyleCursor::Wait => CursorType::Wait,
StyleCursor::Progress => CursorType::Progress,
StyleCursor::NsResize => CursorType::NsResize,
StyleCursor::EwResize => CursorType::EwResize,
StyleCursor::NeswResize => CursorType::NeswResize,
StyleCursor::NwseResize => CursorType::NwseResize,
StyleCursor::NResize => CursorType::NResize,
StyleCursor::SResize => CursorType::SResize,
StyleCursor::EResize => CursorType::EResize,
StyleCursor::WResize => CursorType::WResize,
StyleCursor::Grab => CursorType::Grab,
StyleCursor::Grabbing => CursorType::Grabbing,
StyleCursor::RowResize => CursorType::RowResize,
StyleCursor::ColResize => CursorType::ColResize,
StyleCursor::SeResize | StyleCursor::NeswResize => CursorType::NeswResize,
StyleCursor::ZoomIn | StyleCursor::ZoomOut => CursorType::Default,
StyleCursor::Copy | StyleCursor::Alias => CursorType::Default,
StyleCursor::Cell => CursorType::Crosshair,
StyleCursor::AllScroll => CursorType::Move,
StyleCursor::ContextMenu => CursorType::Default,
StyleCursor::VerticalText => CursorType::Text,
StyleCursor::Unset => CursorType::Text, };
}
}
}
CursorType::Text
}
fn paint_selections(
&self,
builder: &mut DisplayListBuilder,
node_index: usize,
) -> Result<()> {
let node = self
.positioned_tree
.tree
.get(node_index)
.ok_or(LayoutError::InvalidTree)?;
let Some(dom_id) = node.dom_node_id else {
return Ok(());
};
let Some(layout) = self.positioned_tree.tree.get_inline_layout_for_node(node_index) else {
return Ok(());
};
let node_pos = self
.positioned_tree
.calculated_positions
.get(node_index)
.copied()
.unwrap_or_default();
let bp = node.box_props.unpack();
let padding = &bp.padding;
let border = &bp.border;
let content_box_offset_x = node_pos.x + padding.left + border.left;
let content_box_offset_y = node_pos.y + padding.top + border.top;
let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
let is_selectable = super::getters::is_text_selectable(self.ctx.styled_dom, dom_id, node_state);
if !is_selectable {
return Ok(());
}
if let Some(text_selection) = self.ctx.text_selections.get(&self.ctx.styled_dom.dom_id) {
if let Some(range) = text_selection.affected_nodes.get(&dom_id) {
let is_collapsed = text_selection.is_collapsed();
if !is_collapsed {
let rects = layout.get_selection_rects(range);
let style = get_selection_style(self.ctx.styled_dom, Some(dom_id), self.ctx.system_style.as_ref());
let border_radius = BorderRadius {
top_left: style.radius,
top_right: style.radius,
bottom_left: style.radius,
bottom_right: style.radius,
};
for mut rect in rects {
rect.origin.x += content_box_offset_x;
rect.origin.y += content_box_offset_y;
builder.push_selection_rect(rect, style.bg_color, border_radius);
}
}
return Ok(());
}
}
Ok(())
}
fn paint_cursor(
&self,
builder: &mut DisplayListBuilder,
node_index: usize,
) -> Result<()> {
if !self.ctx.cursor_is_visible {
return Ok(());
}
if self.ctx.cursor_locations.is_empty() {
return Ok(());
}
let node = self
.positioned_tree
.tree
.get(node_index)
.ok_or(LayoutError::InvalidTree)?;
let Some(dom_id) = node.dom_node_id else {
return Ok(());
};
let is_contenteditable = super::getters::is_node_contenteditable_inherited(self.ctx.styled_dom, dom_id);
if !is_contenteditable {
return Ok(());
}
let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
let is_selectable = super::getters::is_text_selectable(self.ctx.styled_dom, dom_id, node_state);
if !is_selectable {
return Ok(());
}
let Some(layout) = self.positioned_tree.tree.get_inline_layout_for_node(node_index) else {
return Ok(());
};
let node_pos = self
.positioned_tree
.calculated_positions
.get(node_index)
.copied()
.unwrap_or_default();
let bp = node.box_props.unpack();
let padding = &bp.padding;
let border = &bp.border;
let content_box_offset_x = node_pos.x + padding.left + border.left;
let content_box_offset_y = node_pos.y + padding.top + border.top;
let style = get_caret_style(self.ctx.styled_dom, Some(dom_id));
let primary_idx_for_this_node = self.ctx.cursor_locations.iter().enumerate()
.rev()
.find(|(_, (cd, cn, _))| {
*cd == self.ctx.styled_dom.dom_id && (*cn == dom_id || self.positioned_tree.tree.children(node_index).iter().any(|&child_idx| {
self.positioned_tree.tree.get(child_idx)
.and_then(|c| c.dom_node_id)
.map(|id| id == *cn)
.unwrap_or(false)
}))
})
.map(|(i, _)| i);
for (i, (cursor_dom_id, cursor_node_id, cursor)) in self.ctx.cursor_locations.iter().enumerate() {
if self.ctx.styled_dom.dom_id != *cursor_dom_id {
continue;
}
if dom_id != *cursor_node_id {
let is_ifc_root_of_cursor = self.positioned_tree.tree.children(node_index)
.iter()
.any(|&child_idx| {
self.positioned_tree.tree.get(child_idx)
.and_then(|c| c.dom_node_id)
.map(|id| id == *cursor_node_id)
.unwrap_or(false)
});
if !is_ifc_root_of_cursor {
continue;
}
}
let Some(mut rect) = layout.get_cursor_rect(cursor) else {
continue;
};
rect.origin.x += content_box_offset_x;
rect.origin.y += content_box_offset_y;
rect.size.width = style.width;
builder.push_cursor_rect(rect, style.color);
let is_primary = primary_idx_for_this_node == Some(i);
if is_primary {
if let Some(ref preedit) = self.ctx.preedit_text {
if !preedit.is_empty() {
let char_count = preedit.chars().count() as f32;
let approx_char_width = style.width.max(8.0);
let preedit_width = char_count * approx_char_width;
let underline_bounds = azul_core::geom::LogicalRect {
origin: azul_core::geom::LogicalPosition {
x: rect.origin.x + rect.size.width,
y: rect.origin.y + rect.size.height - 2.0,
},
size: azul_core::geom::LogicalSize {
width: preedit_width,
height: 2.0,
},
};
builder.push_underline(underline_bounds, style.color, 2.0);
}
}
}
}
Ok(())
}
fn paint_selection_and_cursor(
&self,
builder: &mut DisplayListBuilder,
node_index: usize,
) -> Result<()> {
self.paint_selections(builder, node_index)?;
self.paint_cursor(builder, node_index)?;
Ok(())
}
fn collect_stacking_contexts(&mut self, node_index: usize) -> Result<StackingContext> {
let node = self
.positioned_tree
.tree
.get(node_index)
.ok_or(LayoutError::InvalidTree)?;
let z_index = get_z_index(self.ctx.styled_dom, node.dom_node_id);
if let Some(dom_id) = node.dom_node_id {
let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
debug_info!(
self.ctx,
"Collecting stacking context for node {} ({:?}), z-index={}",
node_index,
node_type.get_node_type(),
z_index
);
}
let mut child_contexts = Vec::new();
let mut in_flow_children = Vec::new();
for &child_index in self.positioned_tree.tree.children(node_index) {
if self.establishes_stacking_context(child_index) {
child_contexts.push(self.collect_stacking_contexts(child_index)?);
} else {
in_flow_children.push(child_index);
self.find_nested_stacking_contexts(child_index, &mut child_contexts)?;
}
}
Ok(StackingContext {
node_index,
z_index,
child_contexts,
in_flow_children,
})
}
fn find_nested_stacking_contexts(
&mut self,
parent_index: usize,
child_contexts: &mut Vec<StackingContext>,
) -> Result<()> {
for &child_index in self.positioned_tree.tree.children(parent_index) {
if self.establishes_stacking_context(child_index) {
child_contexts.push(self.collect_stacking_contexts(child_index)?);
} else {
self.find_nested_stacking_contexts(child_index, child_contexts)?;
}
}
Ok(())
}
fn generate_for_stacking_context(
&mut self,
builder: &mut DisplayListBuilder,
context: &StackingContext,
) -> Result<()> {
let node = self
.positioned_tree
.tree
.get(context.node_index)
.ok_or(LayoutError::InvalidTree)?;
if let Some(dom_id) = node.dom_node_id {
let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
debug_info!(
self.ctx,
"Painting stacking context for node {} ({:?}), z-index={}, {} child contexts, {} \
in-flow children",
context.node_index,
node_type.get_node_type(),
context.z_index,
context.child_contexts.len(),
context.in_flow_children.len()
);
}
builder.set_current_node(node.dom_node_id);
let is_fixed_position = node.dom_node_id
.map(|dom_id| get_position_type(self.ctx.styled_dom, Some(dom_id)) == LayoutPosition::Fixed)
.unwrap_or(false);
if is_fixed_position {
builder.begin_fixed_position_element();
}
let has_reference_frame = node.dom_node_id.and_then(|dom_id| {
self.gpu_value_cache.and_then(|cache| {
let key = cache.css_transform_keys.get(&dom_id)?;
let transform = cache.css_current_transform_values.get(&dom_id)?;
Some((*key, *transform))
})
});
let node_pos = self
.positioned_tree
.calculated_positions
.get(context.node_index)
.copied()
.unwrap_or_default();
let node_size = node.used_size.unwrap_or(LogicalSize {
width: 0.0,
height: 0.0,
});
let node_bounds = LogicalRect {
origin: node_pos,
size: node_size,
};
if let Some((transform_key, initial_transform)) = has_reference_frame {
builder.push_reference_frame(transform_key, initial_transform, node_bounds);
}
builder.push_stacking_context(context.z_index, node_bounds);
let mut pushed_opacity = false;
let mut pushed_filter = false;
let mut pushed_backdrop_filter = false;
if let Some(dom_id) = node.dom_node_id {
let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
let opacity = crate::solver3::getters::get_opacity(
self.ctx.styled_dom, dom_id, node_state,
);
if opacity < 1.0 {
builder.push_item(DisplayListItem::PushOpacity {
bounds: node_bounds.into(),
opacity,
});
pushed_opacity = true;
}
if let Some(filter_vec_value) = self.ctx.styled_dom.css_property_cache.ptr
.get_filter(node_data, &dom_id, node_state)
{
if let Some(filter_vec) = filter_vec_value.get_property() {
let filters: Vec<_> = filter_vec.as_ref().to_vec();
if !filters.is_empty() {
builder.push_item(DisplayListItem::PushFilter {
bounds: node_bounds.into(),
filters,
});
pushed_filter = true;
}
}
}
if let Some(backdrop_filter_value) = self.ctx.styled_dom.css_property_cache.ptr
.get_backdrop_filter(node_data, &dom_id, node_state)
{
if let Some(filter_vec) = backdrop_filter_value.get_property() {
let filters: Vec<_> = filter_vec.as_ref().to_vec();
if !filters.is_empty() {
builder.push_item(DisplayListItem::PushBackdropFilter {
bounds: node_bounds.into(),
filters,
});
pushed_backdrop_filter = true;
}
}
}
}
let did_push_image_mask = self.push_image_mask_clip(builder, context.node_index);
self.paint_node_background_and_border(builder, context.node_index)?;
if !self.is_node_hidden(context.node_index) {
if let Some(dom_id) = node.dom_node_id {
let styled_node_state = self.get_styled_node_state(dom_id);
let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
if overflow_x.is_scroll() || overflow_y.is_scroll() {
if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, node.dom_node_id) {
builder.push_hit_test_area(node_bounds, tag_id);
}
}
}
}
let did_push_clip_or_scroll = self.push_node_clips(builder, context.node_index, node)?;
let mut negative_z_children: Vec<_> = context
.child_contexts
.iter()
.filter(|c| c.z_index < 0)
.collect();
negative_z_children.sort_by_key(|c| c.z_index);
for child in negative_z_children {
self.generate_for_stacking_context(builder, child)?;
}
self.paint_in_flow_descendants(builder, context.node_index, &context.in_flow_children)?;
for child in context.child_contexts.iter().filter(|c| c.z_index == 0) {
self.generate_for_stacking_context(builder, child)?;
}
let mut positive_z_children: Vec<_> = context
.child_contexts
.iter()
.filter(|c| c.z_index > 0)
.collect();
positive_z_children.sort_by_key(|c| c.z_index);
for child in positive_z_children {
self.generate_for_stacking_context(builder, child)?;
}
if did_push_image_mask {
builder.pop_image_mask_clip();
}
if pushed_backdrop_filter {
builder.push_item(DisplayListItem::PopBackdropFilter);
}
if pushed_filter {
builder.push_item(DisplayListItem::PopFilter);
}
if pushed_opacity {
builder.push_item(DisplayListItem::PopOpacity);
}
builder.pop_stacking_context();
if has_reference_frame.is_some() {
builder.pop_reference_frame();
}
if is_fixed_position {
builder.end_fixed_position_element();
}
if did_push_clip_or_scroll {
if let Some(dom_id) = node.dom_node_id {
if self.is_virtual_view_node(dom_id) {
builder.push_virtual_view_placeholder(dom_id, node_bounds, node_bounds);
}
}
self.pop_node_clips(builder, node)?;
} else {
if let Some(dom_id) = node.dom_node_id {
if self.is_virtual_view_node(dom_id) {
builder.push_virtual_view_placeholder(dom_id, node_bounds, node_bounds);
}
}
}
self.paint_scrollbars(builder, context.node_index)?;
Ok(())
}
fn paint_in_flow_descendants(
&mut self,
builder: &mut DisplayListBuilder,
node_index: usize,
children_indices: &[usize],
) -> Result<()> {
self.paint_selection_and_cursor(builder, node_index)?;
self.paint_node_content(builder, node_index)?;
let mut non_float_children = Vec::new();
let mut float_children = Vec::new();
let mut dragging_children = Vec::new();
for &child_index in children_indices {
if self.establishes_stacking_context(child_index) {
continue;
}
let child_node = self
.positioned_tree
.tree
.get(child_index)
.ok_or(LayoutError::InvalidTree)?;
let is_dragging = if let Some(dom_id) = child_node.dom_node_id {
let styled_node_state = self.get_styled_node_state(dom_id);
styled_node_state.dragging
} else {
false
};
if is_dragging {
dragging_children.push(child_index);
continue;
}
let is_float = if let Some(dom_id) = child_node.dom_node_id {
use crate::solver3::getters::get_float;
let styled_node_state = self.get_styled_node_state(dom_id);
let float_value = get_float(self.ctx.styled_dom, dom_id, &styled_node_state);
!matches!(
float_value.unwrap_or_default(),
azul_css::props::layout::LayoutFloat::None
)
} else {
false
};
if is_float {
float_children.push(child_index);
} else {
non_float_children.push(child_index);
}
}
for child_index in non_float_children {
let child_node = self
.positioned_tree
.tree
.get(child_index)
.ok_or(LayoutError::InvalidTree)?;
let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
self.gpu_value_cache.and_then(|cache| {
let key = cache.css_transform_keys.get(&dom_id)?;
let transform = cache.css_current_transform_values.get(&dom_id)?;
Some((*key, *transform))
})
});
if let Some((transform_key, initial_transform)) = child_ref_frame {
let child_pos = self
.positioned_tree
.calculated_positions
.get(child_index)
.copied()
.unwrap_or_default();
let child_size = child_node.used_size.unwrap_or(LogicalSize {
width: 0.0,
height: 0.0,
});
let child_bounds = LogicalRect {
origin: child_pos,
size: child_size,
};
builder.set_current_node(child_node.dom_node_id);
builder.push_reference_frame(transform_key, initial_transform, child_bounds);
}
let did_push_child_image_mask = self.push_image_mask_clip(builder, child_index);
self.paint_node_background_and_border(builder, child_index)?;
let did_push_clip = self.push_node_clips(builder, child_index, child_node)?;
self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
if let Some(dom_id) = child_node.dom_node_id {
if self.is_virtual_view_node(dom_id) {
let child_bounds = self.get_paint_rect(child_index).unwrap_or_default();
builder.push_virtual_view_placeholder(dom_id, child_bounds, child_bounds);
}
}
if did_push_clip {
self.pop_node_clips(builder, child_node)?;
}
if did_push_child_image_mask {
builder.pop_image_mask_clip();
}
self.paint_scrollbars(builder, child_index)?;
if child_ref_frame.is_some() {
builder.pop_reference_frame();
}
}
for child_index in float_children {
let child_node = self
.positioned_tree
.tree
.get(child_index)
.ok_or(LayoutError::InvalidTree)?;
let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
self.gpu_value_cache.and_then(|cache| {
let key = cache.css_transform_keys.get(&dom_id)?;
let transform = cache.css_current_transform_values.get(&dom_id)?;
Some((*key, *transform))
})
});
if let Some((transform_key, initial_transform)) = child_ref_frame {
let child_pos = self
.positioned_tree
.calculated_positions
.get(child_index)
.copied()
.unwrap_or_default();
let child_size = child_node.used_size.unwrap_or(LogicalSize {
width: 0.0,
height: 0.0,
});
let child_bounds = LogicalRect {
origin: child_pos,
size: child_size,
};
builder.set_current_node(child_node.dom_node_id);
builder.push_reference_frame(transform_key, initial_transform, child_bounds);
}
let did_push_child_image_mask = self.push_image_mask_clip(builder, child_index);
self.paint_node_background_and_border(builder, child_index)?;
let did_push_clip = self.push_node_clips(builder, child_index, child_node)?;
self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
if let Some(dom_id) = child_node.dom_node_id {
if self.is_virtual_view_node(dom_id) {
let child_bounds = self.get_paint_rect(child_index).unwrap_or_default();
builder.push_virtual_view_placeholder(dom_id, child_bounds, child_bounds);
}
}
if did_push_clip {
self.pop_node_clips(builder, child_node)?;
}
if did_push_child_image_mask {
builder.pop_image_mask_clip();
}
self.paint_scrollbars(builder, child_index)?;
if child_ref_frame.is_some() {
builder.pop_reference_frame();
}
}
for child_index in dragging_children {
let child_node = self
.positioned_tree
.tree
.get(child_index)
.ok_or(LayoutError::InvalidTree)?;
let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
self.gpu_value_cache.and_then(|cache| {
let key = cache.css_transform_keys.get(&dom_id)?;
let transform = cache.css_current_transform_values.get(&dom_id)?;
Some((*key, *transform))
})
});
if let Some((transform_key, initial_transform)) = child_ref_frame {
let child_pos = self
.positioned_tree
.calculated_positions
.get(child_index)
.copied()
.unwrap_or_default();
let child_size = child_node.used_size.unwrap_or(LogicalSize {
width: 0.0,
height: 0.0,
});
let child_bounds = LogicalRect {
origin: child_pos,
size: child_size,
};
builder.set_current_node(child_node.dom_node_id);
builder.push_reference_frame(transform_key, initial_transform, child_bounds);
}
let did_push_child_image_mask = self.push_image_mask_clip(builder, child_index);
self.paint_node_background_and_border(builder, child_index)?;
let did_push_clip = self.push_node_clips(builder, child_index, child_node)?;
self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
if let Some(dom_id) = child_node.dom_node_id {
if self.is_virtual_view_node(dom_id) {
let child_bounds = self.get_paint_rect(child_index).unwrap_or_default();
builder.push_virtual_view_placeholder(dom_id, child_bounds, child_bounds);
}
}
if did_push_clip {
self.pop_node_clips(builder, child_node)?;
}
if did_push_child_image_mask {
builder.pop_image_mask_clip();
}
self.paint_scrollbars(builder, child_index)?;
if child_ref_frame.is_some() {
builder.pop_reference_frame();
}
}
Ok(())
}
fn is_virtual_view_node(&self, dom_id: NodeId) -> bool {
let node_data_container = self.ctx.styled_dom.node_data.as_container();
node_data_container
.get(dom_id)
.map(|nd| matches!(nd.get_node_type(), NodeType::VirtualView))
.unwrap_or(false)
}
fn push_image_mask_clip(
&self,
builder: &mut DisplayListBuilder,
node_index: usize,
) -> bool {
let node = match self.positioned_tree.tree.get(node_index) {
Some(n) => n,
None => return false,
};
let dom_id = match node.dom_node_id {
Some(id) => id,
None => return false,
};
let node_data_container = self.ctx.styled_dom.node_data.as_container();
let node_data = match node_data_container.get(dom_id) {
Some(nd) => nd,
None => return false,
};
match node_data.get_svg_data() {
Some(azul_core::dom::SvgNodeData::ImageClipMask(clip_mask)) => {
let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
let mask_rect = LogicalRect {
origin: LogicalPosition {
x: paint_rect.origin.x + clip_mask.rect.origin.x,
y: paint_rect.origin.y + clip_mask.rect.origin.y,
},
size: clip_mask.rect.size,
};
builder.push_image_mask_clip(
paint_rect,
clip_mask.image.clone(),
mask_rect,
);
true
}
#[cfg(feature = "cpurender")]
Some(azul_core::dom::SvgNodeData::Path(svg_clip)) => {
let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
if let Some(mask_image) = rasterize_svg_clip_to_r8(svg_clip, &paint_rect) {
builder.push_image_mask_clip(paint_rect, mask_image, paint_rect);
true
} else {
false
}
}
#[cfg(not(feature = "cpurender"))]
Some(azul_core::dom::SvgNodeData::Path(_)) => false,
Some(_) => false,
None => false,
}
}
fn push_node_clips(
&self,
builder: &mut DisplayListBuilder,
node_index: usize,
node: &LayoutNodeHot,
) -> Result<bool> {
let Some(dom_id) = node.dom_node_id else {
return Ok(false);
};
let styled_node_state = self.get_styled_node_state(dom_id);
let raw_overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
let raw_overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
let overflow_x = raw_overflow_x.resolve_computed(&raw_overflow_y);
let overflow_y = raw_overflow_y.resolve_computed(&raw_overflow_x);
let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
let element_size = PhysicalSizeImport {
width: paint_rect.size.width,
height: paint_rect.size.height,
};
let border_radius = get_border_radius(
self.ctx.styled_dom,
dom_id,
&styled_node_state,
element_size,
self.ctx.viewport_size,
);
let has_clip_path = if let Some(clip_path) = super::getters::get_clip_path(
self.ctx.styled_dom, dom_id, &styled_node_state,
) {
if let Some((clip_rect, radius)) = resolve_clip_path(&clip_path, paint_rect) {
let br = if radius > 0.0 {
BorderRadius {
top_left: radius,
top_right: radius,
bottom_left: radius,
bottom_right: radius,
}
} else {
BorderRadius::default()
};
builder.push_clip(clip_rect, br);
true
} else {
false
}
} else {
false
};
let needs_clip = overflow_x.is_clipped() || overflow_y.is_clipped();
if !needs_clip {
return Ok(has_clip_path);
}
let ox_clip = overflow_x.is_clipped() && !overflow_x.is_scroll() && !overflow_x.is_auto_overflow();
let oy_clip = overflow_y.is_clipped() && !overflow_y.is_scroll() && !overflow_y.is_auto_overflow();
let ox_visible = !overflow_x.is_clipped();
let oy_visible = !overflow_y.is_clipped();
let border_radius = if (ox_clip && oy_visible) || (oy_clip && ox_visible)
{
BorderRadius::default()
} else {
border_radius
};
let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
let bp = node.box_props.unpack();
let border = &bp.border;
let scrollbar_info = self.positioned_tree.tree.warm(node_index)
.and_then(|w| w.scrollbar_info.clone())
.unwrap_or_default();
let mut clip_rect = LogicalRect {
origin: LogicalPosition {
x: paint_rect.origin.x + border.left,
y: paint_rect.origin.y + border.top,
},
size: LogicalSize {
width: (paint_rect.size.width
- border.left
- border.right
- scrollbar_info.scrollbar_width)
.max(0.0),
height: (paint_rect.size.height
- border.top
- border.bottom
- scrollbar_info.scrollbar_height)
.max(0.0),
},
};
apply_overflow_clip_margin(
&mut clip_rect,
&overflow_x,
&overflow_y,
self.ctx.styled_dom,
dom_id,
&styled_node_state,
);
let is_virtual_view = self.is_virtual_view_node(dom_id);
if overflow_x.is_scroll() || overflow_y.is_scroll() {
if is_virtual_view {
builder.push_clip(clip_rect, border_radius);
} else {
builder.push_clip(clip_rect, border_radius);
let scroll_id = self.scroll_ids.get(&node_index).copied().unwrap_or(0);
let content_size = get_scroll_content_size(node, self.positioned_tree.tree.warm(node_index));
builder.push_scroll_frame(clip_rect, content_size, scroll_id);
}
} else {
builder.push_clip(clip_rect, border_radius);
}
Ok(true)
}
fn pop_node_clips(&self, builder: &mut DisplayListBuilder, node: &LayoutNodeHot) -> Result<()> {
let Some(dom_id) = node.dom_node_id else {
return Ok(());
};
let styled_node_state = self.get_styled_node_state(dom_id);
let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
let paint_rect = self
.get_paint_rect(
self.positioned_tree
.tree
.nodes
.iter()
.position(|n| n.dom_node_id == Some(dom_id))
.unwrap_or(0),
)
.unwrap_or_default();
let element_size = PhysicalSizeImport {
width: paint_rect.size.width,
height: paint_rect.size.height,
};
let border_radius = get_border_radius(
self.ctx.styled_dom,
dom_id,
&styled_node_state,
element_size,
self.ctx.viewport_size,
);
let needs_clip =
overflow_x.is_clipped() || overflow_y.is_clipped();
let is_virtual_view = self.is_virtual_view_node(dom_id);
if needs_clip {
if (overflow_x.is_scroll() || overflow_y.is_scroll()) && !is_virtual_view {
builder.pop_scroll_frame();
builder.pop_clip();
} else {
builder.pop_clip();
}
}
if let Some(clip_path) = super::getters::get_clip_path(
self.ctx.styled_dom, dom_id, &styled_node_state,
) {
if resolve_clip_path(&clip_path, paint_rect).is_some() {
builder.pop_clip();
}
}
Ok(())
}
fn get_paint_rect(&self, node_index: usize) -> Option<LogicalRect> {
let node = self.positioned_tree.tree.get(node_index)?;
let pos = self
.positioned_tree
.calculated_positions
.get(node_index)
.copied()
.unwrap_or_default();
let size = node.used_size.unwrap_or_default();
Some(LogicalRect::new(pos, size))
}
fn paint_node_background_and_border(
&mut self,
builder: &mut DisplayListBuilder,
node_index: usize,
) -> Result<()> {
let Some(paint_rect) = self.get_paint_rect(node_index) else {
return Ok(());
};
let node = self
.positioned_tree
.tree
.get(node_index)
.ok_or(LayoutError::InvalidTree)?;
builder.set_current_node(node.dom_node_id);
if let Some(dom_id) = node.dom_node_id {
let break_before = get_break_before(self.ctx.styled_dom, Some(dom_id));
let break_after = get_break_after(self.ctx.styled_dom, Some(dom_id));
if is_forced_page_break(break_before) {
let y_position = paint_rect.origin.y;
builder.add_forced_page_break(y_position);
debug_info!(
self.ctx,
"Registered forced page break BEFORE node {} at y={}",
node_index,
y_position
);
}
if is_forced_page_break(break_after) {
let y_position = paint_rect.origin.y + paint_rect.size.height;
builder.add_forced_page_break(y_position);
debug_info!(
self.ctx,
"Registered forced page break AFTER node {} at y={}",
node_index,
y_position
);
}
}
if self.is_node_hidden(node_index) {
return Ok(());
}
let warm = self.positioned_tree.tree.warm(node_index);
let parent_is_flex_or_grid = warm
.and_then(|w| w.parent_formatting_context.as_ref().map(|fc| matches!(fc, FormattingContext::Flex | FormattingContext::Grid)))
.unwrap_or(false);
if let Some(dom_id) = node.dom_node_id {
let display = {
use crate::solver3::getters::get_display_property;
get_display_property(self.ctx.styled_dom, Some(dom_id))
.unwrap_or(LayoutDisplay::Inline)
};
if display == LayoutDisplay::InlineBlock || display == LayoutDisplay::Inline {
debug_info!(
self.ctx,
"[paint_node] node {} has display={:?}, parent_formatting_context={:?}, parent_is_flex_or_grid={}",
node_index,
display,
warm.and_then(|w| w.parent_formatting_context.as_ref()),
parent_is_flex_or_grid
);
if !parent_is_flex_or_grid {
if display == LayoutDisplay::InlineBlock
&& self.establishes_stacking_context(node_index)
{
} else {
return Ok(());
}
}
}
}
if matches!(node.formatting_context,
FormattingContext::TableRowGroup | FormattingContext::TableRow |
FormattingContext::TableColumnGroup
) {
return Ok(());
}
if matches!(node.formatting_context, FormattingContext::Table) {
debug_info!(
self.ctx,
"Painting table backgrounds/borders for node {} at {:?}",
node_index,
paint_rect
);
return self.paint_table_items(builder, node_index);
}
if let Some(dom_id) = node.dom_node_id {
let styled_node_state = self.get_styled_node_state(dom_id);
let background_contents =
get_background_contents(self.ctx.styled_dom, dom_id, &styled_node_state);
let border_info = get_border_info(self.ctx.styled_dom, dom_id, &styled_node_state);
let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
debug_info!(
self.ctx,
"Painting background/border for node {} ({:?}) at {:?}, backgrounds={:?}",
node_index,
node_type.get_node_type(),
paint_rect,
background_contents.len()
);
let element_size = PhysicalSizeImport {
width: paint_rect.size.width,
height: paint_rect.size.height,
};
let simple_border_radius = get_border_radius(
self.ctx.styled_dom,
dom_id,
&styled_node_state,
element_size,
self.ctx.viewport_size,
);
let style_border_radius =
get_style_border_radius(self.ctx.styled_dom, dom_id, &styled_node_state);
let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
for shadow in [
super::getters::get_box_shadow_left(self.ctx.styled_dom, dom_id, node_state),
super::getters::get_box_shadow_right(self.ctx.styled_dom, dom_id, node_state),
super::getters::get_box_shadow_top(self.ctx.styled_dom, dom_id, node_state),
super::getters::get_box_shadow_bottom(self.ctx.styled_dom, dom_id, node_state),
] {
if let Some(shadow) = shadow {
builder.push_item(DisplayListItem::BoxShadow {
bounds: paint_rect.into(),
shadow,
border_radius: simple_border_radius,
});
}
}
builder.push_backgrounds_and_border(
paint_rect,
&background_contents,
&border_info,
simple_border_radius,
style_border_radius,
self.ctx.image_cache,
);
}
Ok(())
}
fn paint_table_items(
&self,
builder: &mut DisplayListBuilder,
table_index: usize,
) -> Result<()> {
let table_node = self
.positioned_tree
.tree
.get(table_index)
.ok_or(LayoutError::InvalidTree)?;
let Some(table_paint_rect) = self.get_paint_rect(table_index) else {
return Ok(());
};
if let Some(dom_id) = table_node.dom_node_id {
let styled_node_state = self.get_styled_node_state(dom_id);
let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
let element_size = PhysicalSizeImport {
width: table_paint_rect.size.width,
height: table_paint_rect.size.height,
};
let border_radius = get_border_radius(
self.ctx.styled_dom,
dom_id,
&styled_node_state,
element_size,
self.ctx.viewport_size,
);
builder.push_rect(table_paint_rect, bg_color, border_radius);
}
for &child_idx in self.positioned_tree.tree.children(table_index) {
let child_node = self.positioned_tree.tree.get(child_idx);
if let Some(node) = child_node {
if matches!(node.formatting_context, FormattingContext::TableColumnGroup) {
self.paint_element_background(builder, child_idx)?;
for &col_idx in self.positioned_tree.tree.children(child_idx) {
self.paint_element_background(builder, col_idx)?;
}
}
}
}
for &child_idx in self.positioned_tree.tree.children(table_index) {
let child_node = self.positioned_tree.tree.get(child_idx);
if let Some(node) = child_node {
match node.formatting_context {
FormattingContext::TableRowGroup => {
self.paint_element_background(builder, child_idx)?;
for &row_idx in self.positioned_tree.tree.children(child_idx) {
self.paint_table_row_and_cells(builder, row_idx)?;
}
}
FormattingContext::TableRow => {
self.paint_table_row_and_cells(builder, child_idx)?;
}
_ => {}
}
}
}
Ok(())
}
fn paint_table_row_and_cells(
&self,
builder: &mut DisplayListBuilder,
row_idx: usize,
) -> Result<()> {
if let Some(row_node) = self.positioned_tree.tree.get(row_idx) {
if let Some(dom_id) = row_node.dom_node_id {
let styled_node_state = self.get_styled_node_state(dom_id);
let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
if bg_color.a > 0 {
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
for &cell_idx in self.positioned_tree.tree.children(row_idx) {
if let Some(cell_rect) = self.get_paint_rect(cell_idx) {
min_x = min_x.min(cell_rect.origin.x);
min_y = min_y.min(cell_rect.origin.y);
max_x = max_x.max(cell_rect.origin.x + cell_rect.size.width);
max_y = max_y.max(cell_rect.origin.y + cell_rect.size.height);
}
}
if min_x < max_x && min_y < max_y {
let row_rect = LogicalRect::new(
LogicalPosition::new(min_x, min_y),
LogicalSize::new(max_x - min_x, max_y - min_y),
);
builder.push_rect(row_rect, bg_color, BorderRadius::default());
}
}
}
}
if let Some(_node) = self.positioned_tree.tree.get(row_idx) {
for &cell_idx in self.positioned_tree.tree.children(row_idx) {
self.paint_element_background(builder, cell_idx)?;
}
}
Ok(())
}
fn paint_element_background(
&self,
builder: &mut DisplayListBuilder,
node_index: usize,
) -> Result<()> {
let Some(paint_rect) = self.get_paint_rect(node_index) else {
return Ok(());
};
let Some(node) = self.positioned_tree.tree.get(node_index) else {
return Ok(());
};
let Some(dom_id) = node.dom_node_id else {
return Ok(());
};
let styled_node_state = self.get_styled_node_state(dom_id);
let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
if bg_color.a == 0 {
return Ok(());
}
let element_size = PhysicalSizeImport {
width: paint_rect.size.width,
height: paint_rect.size.height,
};
let border_radius = get_border_radius(
self.ctx.styled_dom,
dom_id,
&styled_node_state,
element_size,
self.ctx.viewport_size,
);
builder.push_rect(paint_rect, bg_color, border_radius);
Ok(())
}
fn paint_node_content(
&mut self,
builder: &mut DisplayListBuilder,
node_index: usize,
) -> Result<()> {
if self.is_node_hidden(node_index) {
return Ok(());
}
let node = self
.positioned_tree
.tree
.get(node_index)
.ok_or(LayoutError::InvalidTree)?;
let node_warm = self.positioned_tree.tree.warm(node_index);
builder.set_current_node(node.dom_node_id);
let Some(mut paint_rect) = self.get_paint_rect(node_index) else {
return Ok(());
};
if paint_rect.size.width == 0.0 || paint_rect.size.height == 0.0 {
if let Some(cached_layout) = node_warm.and_then(|w| w.inline_layout_result.as_ref()) {
let content_bounds = cached_layout.layout.bounds();
paint_rect.size.width = content_bounds.width;
paint_rect.size.height = content_bounds.height;
}
}
if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, node.dom_node_id) {
let is_scrollable = if let Some(dom_id) = node.dom_node_id {
let styled_node_state = self.get_styled_node_state(dom_id);
let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
overflow_x.is_scroll() || overflow_y.is_scroll()
} else {
false
};
if !is_scrollable {
builder.push_hit_test_area(paint_rect, tag_id);
}
}
if let Some(cached_layout) = node_warm.and_then(|w| w.inline_layout_result.as_ref()) {
let inline_layout = &cached_layout.layout;
debug_info!(
self.ctx,
"[paint_node] node {} has inline_layout with {} items",
node_index,
inline_layout.items.len()
);
if let Some(dom_id) = node.dom_node_id {
let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
debug_info!(
self.ctx,
"Painting inline content for node {} ({:?}) at {:?}, {} layout items",
node_index,
node_type.get_node_type(),
paint_rect,
inline_layout.items.len()
);
}
let border_box = BorderBoxRect(paint_rect);
let nbp = node.box_props.unpack();
let mut content_box_rect =
border_box.to_content_box(&nbp.padding, &nbp.border).rect();
let viewport_clip_rect = content_box_rect;
let content_size = get_scroll_content_size(node, node_warm);
if content_size.height > content_box_rect.size.height {
content_box_rect.size.height = content_size.height;
}
if content_size.width > content_box_rect.size.width {
content_box_rect.size.width = content_size.width;
}
let mut pushed_text_shadow = false;
if let Some(dom_id) = node.dom_node_id {
let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
if let Some(shadow_val) = self.ctx.styled_dom.css_property_cache.ptr
.get_text_shadow(node_data, &dom_id, node_state)
{
if let Some(shadow) = shadow_val.get_property() {
builder.push_item(DisplayListItem::PushTextShadow {
shadow: (**shadow).clone(),
});
pushed_text_shadow = true;
}
}
}
self.paint_inline_content(builder, content_box_rect, viewport_clip_rect, inline_layout, node_index)?;
if pushed_text_shadow {
builder.push_item(DisplayListItem::PopTextShadow);
}
} else if let Some(dom_id) = node.dom_node_id {
let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
if let NodeType::Image(image_ref) = node_data.get_node_type() {
debug_info!(
self.ctx,
"Painting image for node {} at {:?}",
node_index,
paint_rect
);
let styled_node_state = self.get_styled_node_state(dom_id);
let element_size = PhysicalSizeImport {
width: paint_rect.size.width,
height: paint_rect.size.height,
};
let border_radius = get_border_radius(
self.ctx.styled_dom,
dom_id,
&styled_node_state,
element_size,
self.ctx.viewport_size,
);
builder.push_image(paint_rect, image_ref.as_ref().clone(), border_radius);
}
}
Ok(())
}
fn paint_scrollbars(&self, builder: &mut DisplayListBuilder, node_index: usize) -> Result<()> {
if self.is_node_hidden(node_index) {
return Ok(());
}
let node = self
.positioned_tree
.tree
.get(node_index)
.ok_or(LayoutError::InvalidTree)?;
let Some(paint_rect) = self.get_paint_rect(node_index) else {
return Ok(());
};
let scrollbar_info = self.positioned_tree.tree.warm(node_index)
.and_then(|w| w.scrollbar_info.clone())
.unwrap_or_default();
let node_id = node.dom_node_id;
let scrollbar_style = node_id
.map(|nid| {
let node_state =
&self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
crate::solver3::getters::get_scrollbar_style_cached(self.ctx, nid, node_state)
})
.unwrap_or_default();
if matches!(
scrollbar_style.width_mode,
azul_css::props::style::scrollbar::LayoutScrollbarWidth::None
) {
return Ok(());
}
let scrollbar_gutter = node_id
.and_then(|nid| {
let node_state =
&self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
get_scrollbar_gutter_property(self.ctx.styled_dom, nid, node_state).exact()
})
.unwrap_or_default();
let gutter_is_stable = matches!(
scrollbar_gutter,
azul_css::props::layout::overflow::StyleScrollbarGutter::Stable
| azul_css::props::layout::overflow::StyleScrollbarGutter::StableBothEdges
);
let gutter_both_edges = matches!(
scrollbar_gutter,
azul_css::props::layout::overflow::StyleScrollbarGutter::StableBothEdges
);
if gutter_is_stable {
let gbp = node.box_props.unpack();
let border = &gbp.border;
let gutter_width = scrollbar_style.visual_width_px;
let bg_color = node_id
.map(|nid| {
let node_state =
&self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
get_background_color(self.ctx.styled_dom, nid, node_state)
})
.unwrap_or(ColorU::TRANSPARENT);
if !scrollbar_info.needs_vertical && gutter_width > 0.0 {
let gutter_rect = LogicalRect {
origin: LogicalPosition::new(
paint_rect.origin.x + paint_rect.size.width - border.right - gutter_width,
paint_rect.origin.y + border.top,
),
size: LogicalSize::new(
gutter_width,
(paint_rect.size.height - border.top - border.bottom).max(0.0),
),
};
builder.push_rect(gutter_rect, bg_color, BorderRadius::default());
if gutter_both_edges {
let left_gutter_rect = LogicalRect {
origin: LogicalPosition::new(
paint_rect.origin.x + border.left,
paint_rect.origin.y + border.top,
),
size: LogicalSize::new(
gutter_width,
(paint_rect.size.height - border.top - border.bottom).max(0.0),
),
};
builder.push_rect(left_gutter_rect, bg_color, BorderRadius::default());
}
}
}
let sbp = node.box_props.unpack();
let border = &sbp.border;
let container_border_radius = node_id
.map(|nid| {
let node_state =
&self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
let element_size = PhysicalSizeImport {
width: paint_rect.size.width,
height: paint_rect.size.height,
};
let viewport_size =
LogicalSize::new(self.ctx.viewport_size.width, self.ctx.viewport_size.height);
get_border_radius(
self.ctx.styled_dom,
nid,
node_state,
element_size,
viewport_size,
)
})
.unwrap_or_default();
let inner_rect = LogicalRect {
origin: LogicalPosition::new(
paint_rect.origin.x + border.left,
paint_rect.origin.y + border.top,
),
size: LogicalSize::new(
(paint_rect.size.width - border.left - border.right).max(0.0),
(paint_rect.size.height - border.top - border.bottom).max(0.0),
),
};
let (scroll_offset_x, scroll_offset_y) = node_id
.and_then(|nid| {
self.scroll_offsets.get(&nid).map(|pos| {
(
pos.children_rect.origin.x - pos.parent_rect.origin.x,
pos.children_rect.origin.y - pos.parent_rect.origin.y,
)
})
})
.unwrap_or((0.0, 0.0));
let content_size = node_id
.and_then(|nid| self.scroll_offsets.get(&nid))
.map(|pos| pos.children_rect.size)
.unwrap_or_else(|| self.positioned_tree.tree.get_content_size(node_index));
let thumb_radius = scrollbar_style.visual_width_px / 2.0;
let thumb_border_radius = BorderRadius {
top_left: thumb_radius,
top_right: thumb_radius,
bottom_left: thumb_radius,
bottom_right: thumb_radius,
};
if scrollbar_info.needs_vertical {
let opacity_key = node_id.map(|nid| {
self.gpu_value_cache
.and_then(|cache| {
cache
.scrollbar_v_opacity_keys
.get(&(self.dom_id, nid))
.copied()
})
.unwrap_or_else(|| OpacityKey::unique())
});
let button_size = if scrollbar_style.show_scroll_buttons {
scrollbar_style.scroll_button_size_px
} else {
0.0
};
let v_geom = compute_scrollbar_geometry_with_button_size(
ScrollbarOrientation::Vertical,
inner_rect,
content_size,
scroll_offset_y,
scrollbar_style.visual_width_px,
scrollbar_info.needs_horizontal,
button_size,
);
let thumb_bounds = LogicalRect {
origin: LogicalPosition::new(
v_geom.track_rect.origin.x,
v_geom.track_rect.origin.y + v_geom.button_size,
),
size: LogicalSize::new(v_geom.width_px, v_geom.thumb_length),
};
let thumb_transform_key = node_id.map(|nid| {
self.gpu_value_cache
.and_then(|cache| cache.transform_keys.get(&nid).copied())
.unwrap_or_else(|| TransformKey::unique())
});
let thumb_initial_transform =
ComputedTransform3D::new_translation(0.0, v_geom.thumb_offset, 0.0);
let hit_id = node_id
.map(|nid| azul_core::hit_test::ScrollbarHitId::VerticalThumb(self.dom_id, nid));
let (button_decrement_bounds, button_increment_bounds) = if scrollbar_style.show_scroll_buttons && v_geom.button_size > 0.0 {
(
Some(LogicalRect {
origin: v_geom.track_rect.origin,
size: LogicalSize::new(v_geom.button_size, v_geom.button_size),
}),
Some(LogicalRect {
origin: LogicalPosition::new(
v_geom.track_rect.origin.x,
v_geom.track_rect.origin.y + v_geom.track_rect.size.height - v_geom.button_size,
),
size: LogicalSize::new(v_geom.button_size, v_geom.button_size),
}),
)
} else {
(None, None)
};
builder.push_scrollbar_styled(ScrollbarDrawInfo {
bounds: v_geom.track_rect.into(),
orientation: ScrollbarOrientation::Vertical,
track_bounds: v_geom.track_rect.into(),
track_color: scrollbar_style.track_color,
thumb_bounds: thumb_bounds.into(),
thumb_color: scrollbar_style.thumb_color,
thumb_border_radius,
button_decrement_bounds: button_decrement_bounds.map(|b| b.into()),
button_increment_bounds: button_increment_bounds.map(|b| b.into()),
button_color: scrollbar_style.button_color,
opacity_key,
thumb_transform_key,
thumb_initial_transform,
hit_id,
clip_to_container_border: scrollbar_style.clip_to_container_border,
container_border_radius,
visibility: scrollbar_style.visibility,
});
}
if scrollbar_info.needs_horizontal {
let opacity_key = node_id.map(|nid| {
self.gpu_value_cache
.and_then(|cache| {
cache
.scrollbar_h_opacity_keys
.get(&(self.dom_id, nid))
.copied()
})
.unwrap_or_else(|| OpacityKey::unique())
});
let h_button_size = if scrollbar_style.show_scroll_buttons {
scrollbar_style.scroll_button_size_px
} else {
0.0
};
let h_geom = compute_scrollbar_geometry_with_button_size(
ScrollbarOrientation::Horizontal,
inner_rect,
content_size,
scroll_offset_x,
scrollbar_style.visual_width_px,
scrollbar_info.needs_vertical,
h_button_size,
);
let thumb_bounds = LogicalRect {
origin: LogicalPosition::new(
h_geom.track_rect.origin.x + h_geom.button_size,
h_geom.track_rect.origin.y,
),
size: LogicalSize::new(h_geom.thumb_length, h_geom.width_px),
};
let thumb_transform_key = node_id.map(|nid| {
self.gpu_value_cache
.and_then(|cache| cache.h_transform_keys.get(&nid).copied())
.unwrap_or_else(|| TransformKey::unique())
});
let thumb_initial_transform =
ComputedTransform3D::new_translation(h_geom.thumb_offset, 0.0, 0.0);
let hit_id = node_id
.map(|nid| azul_core::hit_test::ScrollbarHitId::HorizontalThumb(self.dom_id, nid));
let (button_decrement_bounds, button_increment_bounds) = if scrollbar_style.show_scroll_buttons && h_geom.button_size > 0.0 {
(
Some(LogicalRect {
origin: h_geom.track_rect.origin,
size: LogicalSize::new(h_geom.button_size, h_geom.button_size),
}),
Some(LogicalRect {
origin: LogicalPosition::new(
h_geom.track_rect.origin.x + h_geom.track_rect.size.width - h_geom.button_size,
h_geom.track_rect.origin.y,
),
size: LogicalSize::new(h_geom.button_size, h_geom.button_size),
}),
)
} else {
(None, None)
};
builder.push_scrollbar_styled(ScrollbarDrawInfo {
bounds: h_geom.track_rect.into(),
orientation: ScrollbarOrientation::Horizontal,
track_bounds: h_geom.track_rect.into(),
track_color: scrollbar_style.track_color,
thumb_bounds: thumb_bounds.into(),
thumb_color: scrollbar_style.thumb_color,
thumb_border_radius,
button_decrement_bounds: button_decrement_bounds.map(|b| b.into()),
button_increment_bounds: button_increment_bounds.map(|b| b.into()),
button_color: scrollbar_style.button_color,
opacity_key,
thumb_transform_key,
thumb_initial_transform,
hit_id,
clip_to_container_border: scrollbar_style.clip_to_container_border,
container_border_radius,
visibility: scrollbar_style.visibility,
});
}
Ok(())
}
fn paint_inline_content(
&self,
builder: &mut DisplayListBuilder,
container_rect: LogicalRect,
viewport_clip_rect: LogicalRect,
layout: &UnifiedLayout,
source_node_index: usize,
) -> Result<()> {
let layout_bounds = layout.bounds();
let actual_bounds = if layout_bounds.width > 0.0 && layout_bounds.height > 0.0 {
LogicalRect {
origin: container_rect.origin,
size: LogicalSize {
width: layout_bounds.width,
height: layout_bounds.height,
},
}
} else {
LogicalRect {
origin: container_rect.origin,
size: LogicalSize::default(),
}
};
if layout_bounds.width > 0.0 || layout_bounds.height > 0.0 {
builder.push_text_layout(
Arc::new(layout.clone()) as Arc<dyn std::any::Any + Send + Sync>,
actual_bounds,
FontHash::from_hash(0), 12.0, ColorU {
r: 0,
g: 0,
b: 0,
a: 255,
}, );
}
let glyph_runs = crate::text3::glyphs::get_glyph_runs_simple(layout);
for glyph_run in glyph_runs.iter() {
if let (Some(first_glyph), Some(last_glyph)) =
(glyph_run.glyphs.first(), glyph_run.glyphs.last())
{
let run_start_x = container_rect.origin.x + first_glyph.point.x;
let run_end_x = container_rect.origin.x + last_glyph.point.x;
let run_width = (run_end_x - run_start_x).max(0.0);
if run_width <= 0.0 {
continue;
}
let baseline_y = container_rect.origin.y + first_glyph.point.y;
let font_size = glyph_run.font_size_px;
let ascent = font_size * 0.8;
let mut run_bounds = LogicalRect::new(
LogicalPosition::new(run_start_x, baseline_y - ascent),
LogicalSize::new(run_width, font_size),
);
if let Some(border) = &glyph_run.border {
let left_inset = border.left_inset();
let right_inset = border.right_inset();
let top_inset = border.top_inset();
let bottom_inset = border.bottom_inset();
run_bounds.origin.x -= left_inset;
run_bounds.origin.y -= top_inset;
run_bounds.size.width += left_inset + right_inset;
run_bounds.size.height += top_inset + bottom_inset;
}
builder.push_inline_backgrounds_and_border(
run_bounds,
glyph_run.background_color,
&glyph_run.background_content,
glyph_run.border.as_ref(),
self.ctx.image_cache,
);
}
}
for (_idx, glyph_run) in glyph_runs.iter().enumerate() {
let clip_rect = viewport_clip_rect;
let offset_glyphs: Vec<GlyphInstance> = glyph_run
.glyphs
.iter()
.map(|g| {
let mut g = g.clone();
g.point.x += container_rect.origin.x;
g.point.y += container_rect.origin.y;
g
})
.collect();
builder.push_text_run(
offset_glyphs,
FontHash::from_hash(glyph_run.font_hash),
glyph_run.font_size_px,
glyph_run.color,
clip_rect,
Some(source_node_index),
);
let needs_underline = glyph_run.text_decoration.underline || glyph_run.is_ime_preview;
let needs_strikethrough = glyph_run.text_decoration.strikethrough;
let needs_overline = glyph_run.text_decoration.overline;
if needs_underline || needs_strikethrough || needs_overline {
if let (Some(first_glyph), Some(last_glyph)) =
(glyph_run.glyphs.first(), glyph_run.glyphs.last())
{
let decoration_start_x = container_rect.origin.x + first_glyph.point.x;
let decoration_end_x = container_rect.origin.x + last_glyph.point.x;
let decoration_width = decoration_end_x - decoration_start_x;
let font_size = glyph_run.font_size_px;
let thickness = (font_size * 0.08).max(1.0);
let baseline_y = container_rect.origin.y + first_glyph.point.y;
if needs_underline {
let underline_y = baseline_y + (font_size * 0.12);
let underline_bounds = LogicalRect::new(
LogicalPosition::new(decoration_start_x, underline_y),
LogicalSize::new(decoration_width, thickness),
);
builder.push_underline(underline_bounds, glyph_run.color, thickness);
}
if needs_strikethrough {
let strikethrough_y = baseline_y - (font_size * 0.3);
let strikethrough_bounds = LogicalRect::new(
LogicalPosition::new(decoration_start_x, strikethrough_y),
LogicalSize::new(decoration_width, thickness),
);
builder.push_strikethrough(
strikethrough_bounds,
glyph_run.color,
thickness,
);
}
if needs_overline {
let overline_y = baseline_y - (font_size * 0.85);
let overline_bounds = LogicalRect::new(
LogicalPosition::new(decoration_start_x, overline_y),
LogicalSize::new(decoration_width, thickness),
);
builder.push_overline(overline_bounds, glyph_run.color, thickness);
}
}
}
}
for glyph_run in glyph_runs.iter() {
let Some(source_node_id) = glyph_run.source_node_id else {
continue;
};
if let (Some(first_glyph), Some(last_glyph)) =
(glyph_run.glyphs.first(), glyph_run.glyphs.last())
{
let run_start_x = container_rect.origin.x + first_glyph.point.x;
let run_end_x = container_rect.origin.x + last_glyph.point.x;
let run_width = (run_end_x - run_start_x).max(0.0);
if run_width <= 0.0 {
continue;
}
let baseline_y = container_rect.origin.y + first_glyph.point.y;
let font_size = glyph_run.font_size_px;
let ascent = font_size * 0.8;
let run_bounds = LogicalRect::new(
LogicalPosition::new(run_start_x, baseline_y - ascent),
LogicalSize::new(run_width, font_size),
);
let cursor_type = self.get_cursor_type_for_text_node(source_node_id);
let tag_value = ((self.dom_id.inner as u64) << 32) | (source_node_id.index() as u64);
let tag_type = TAG_TYPE_CURSOR | (cursor_type as u16);
let tag_id = (tag_value, tag_type);
builder.push_hit_test_area(run_bounds, tag_id);
}
}
for positioned_item in &layout.items {
self.paint_inline_object(builder, container_rect.origin, positioned_item)?;
}
Ok(())
}
fn paint_inline_object(
&self,
builder: &mut DisplayListBuilder,
base_pos: LogicalPosition,
positioned_item: &PositionedItem,
) -> Result<()> {
let ShapedItem::Object {
content, bounds, ..
} = &positioned_item.item
else {
return Ok(());
};
let object_bounds = LogicalRect::new(
LogicalPosition::new(
base_pos.x + positioned_item.position.x,
base_pos.y + positioned_item.position.y,
),
LogicalSize::new(bounds.width, bounds.height),
);
match content {
InlineContent::Image(image) => {
if let Some(image_ref) = get_image_ref_for_image_source(&image.source) {
builder.push_image(object_bounds, image_ref, BorderRadius::default());
}
}
InlineContent::Shape(shape) => {
self.paint_inline_shape(builder, object_bounds, shape, bounds)?;
}
_ => {}
}
Ok(())
}
fn paint_inline_shape(
&self,
builder: &mut DisplayListBuilder,
object_bounds: LogicalRect,
shape: &InlineShape,
bounds: &crate::text3::cache::Rect,
) -> Result<()> {
let Some(node_id) = shape.source_node_id else {
return Ok(());
};
if let Some(indices) = self.positioned_tree.tree.dom_to_layout.get(&node_id) {
if let Some(&idx) = indices.first() {
if self.establishes_stacking_context(idx) {
return Ok(());
}
}
}
let styled_node_state =
&self.ctx.styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
let background_contents =
get_background_contents(self.ctx.styled_dom, node_id, styled_node_state);
let border_info = get_border_info(self.ctx.styled_dom, node_id, styled_node_state);
let margins = if let Some(indices) = self.positioned_tree.tree.dom_to_layout.get(&node_id) {
if let Some(&idx) = indices.first() {
self.positioned_tree.tree.nodes[idx].box_props.unpack().margin
} else {
Default::default()
}
} else {
Default::default()
};
let border_box_bounds = LogicalRect {
origin: LogicalPosition {
x: object_bounds.origin.x + margins.left,
y: object_bounds.origin.y + margins.top,
},
size: LogicalSize {
width: (object_bounds.size.width - margins.left - margins.right).max(0.0),
height: (object_bounds.size.height - margins.top - margins.bottom).max(0.0),
},
};
let element_size = PhysicalSizeImport {
width: border_box_bounds.size.width,
height: border_box_bounds.size.height,
};
let simple_border_radius = get_border_radius(
self.ctx.styled_dom,
node_id,
styled_node_state,
element_size,
self.ctx.viewport_size,
);
let style_border_radius =
get_style_border_radius(self.ctx.styled_dom, node_id, styled_node_state);
builder.push_backgrounds_and_border(
border_box_bounds,
&background_contents,
&border_info,
simple_border_radius,
style_border_radius,
self.ctx.image_cache,
);
if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, Some(node_id)) {
builder.push_hit_test_area(border_box_bounds, tag_id);
}
Ok(())
}
fn establishes_stacking_context(&self, node_index: usize) -> bool {
let Some(node) = self.positioned_tree.tree.get(node_index) else {
return false;
};
let Some(dom_id) = node.dom_node_id else {
return false;
};
let position = get_position_type(self.ctx.styled_dom, Some(dom_id));
let z_auto = crate::solver3::getters::is_z_index_auto(self.ctx.styled_dom, Some(dom_id));
if position == LayoutPosition::Fixed || position == LayoutPosition::Sticky {
return true;
}
if position == LayoutPosition::Absolute {
return !z_auto;
}
if position == LayoutPosition::Relative && !z_auto {
return true;
}
if let Some(styled_node) = self.ctx.styled_dom.styled_nodes.as_container().get(dom_id) {
let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
let node_state =
&self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
if crate::solver3::getters::get_opacity(
self.ctx.styled_dom, dom_id, node_state,
) < 1.0 {
return true;
}
if let Some(t) = crate::solver3::getters::get_transform(
self.ctx.styled_dom, dom_id, node_state,
) {
if !t.is_empty() {
return true;
}
}
}
false
}
}
pub fn node_establishes_stacking_context(
styled_dom: &StyledDom,
dom_id: NodeId,
) -> bool {
let position = crate::solver3::positioning::get_position_type(styled_dom, Some(dom_id));
let z_auto = crate::solver3::getters::is_z_index_auto(styled_dom, Some(dom_id));
if position == LayoutPosition::Fixed || position == LayoutPosition::Sticky {
return true;
}
if position == LayoutPosition::Absolute && !z_auto {
return true;
}
if position == LayoutPosition::Relative && !z_auto {
return true;
}
let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
if crate::solver3::getters::get_opacity(styled_dom, dom_id, node_state) < 1.0 {
return true;
}
if let Some(t) = crate::solver3::getters::get_transform(styled_dom, dom_id, node_state) {
if !t.is_empty() {
return true;
}
}
false
}
pub struct PositionedTree<'a> {
pub tree: &'a LayoutTree,
pub calculated_positions: &'a super::PositionVec,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverflowBehavior {
Visible,
Hidden,
Clip,
Scroll,
Auto,
}
impl OverflowBehavior {
pub fn is_clipped(&self) -> bool {
matches!(self, Self::Hidden | Self::Clip | Self::Scroll | Self::Auto)
}
pub fn is_scroll(&self) -> bool {
matches!(self, Self::Scroll | Self::Auto)
}
}
fn apply_overflow_clip_margin(
clip_rect: &mut LogicalRect,
overflow_x: &super::getters::MultiValue<LayoutOverflow>,
overflow_y: &super::getters::MultiValue<LayoutOverflow>,
styled_dom: &StyledDom,
dom_id: NodeId,
styled_node_state: &azul_core::styled_dom::StyledNodeState,
) {
if !overflow_x.is_clip() && !overflow_y.is_clip() {
return;
}
let clip_margin = get_overflow_clip_margin_property(styled_dom, dom_id, styled_node_state);
let Some(margin_val) = clip_margin.exact() else {
return;
};
let m = margin_val.inner.to_pixels_internal(0.0, 0.0, 0.0).max(0.0);
if m <= 0.0 {
return;
}
if overflow_x.is_clip() {
clip_rect.origin.x -= m;
clip_rect.size.width += m * 2.0;
}
if overflow_y.is_clip() {
clip_rect.origin.y -= m;
clip_rect.size.height += m * 2.0;
}
}
fn get_scroll_id(id: Option<NodeId>) -> LocalScrollId {
id.map(|i| i.index() as u64).unwrap_or(0)
}
fn get_scroll_content_size(node: &LayoutNodeHot, warm: Option<&LayoutNodeWarm>) -> LogicalSize {
if let Some(overflow_size) = warm.and_then(|w| w.overflow_content_size) {
return overflow_size;
}
let mut content_size = node.used_size.unwrap_or_default();
if let Some(cached_layout) = warm.and_then(|w| w.inline_layout_result.as_ref()) {
let text_layout = &cached_layout.layout;
let mut max_x: f32 = 0.0;
let mut max_y: f32 = 0.0;
for positioned_item in &text_layout.items {
let item_bounds = positioned_item.item.bounds();
let item_right = positioned_item.position.x + item_bounds.width;
let item_bottom = positioned_item.position.y + item_bounds.height;
max_x = max_x.max(item_right);
max_y = max_y.max(item_bottom);
}
content_size.width = content_size.width.max(max_x);
content_size.height = content_size.height.max(max_y);
}
content_size
}
fn get_tag_id(dom: &StyledDom, id: Option<NodeId>) -> Option<DisplayListTagId> {
let node_id = id?;
let tag_mapping = dom.tag_ids_to_node_ids.as_ref().iter().find(|m| {
m.node_id.into_crate_internal() == Some(node_id)
})?;
Some((tag_mapping.tag_id.inner, TAG_TYPE_DOM_NODE))
}
fn get_image_ref_for_image_source(
source: &ImageSource,
) -> Option<ImageRef> {
match source {
ImageSource::Ref(image_ref) => Some(image_ref.clone()),
ImageSource::Url(_url) => {
None
}
ImageSource::Data(_) | ImageSource::Svg(_) | ImageSource::Placeholder(_) => {
None
}
}
}
fn get_display_item_bounds(item: &DisplayListItem) -> Option<WindowLogicalRect> {
match item {
DisplayListItem::Rect { bounds, .. } => Some(*bounds),
DisplayListItem::SelectionRect { bounds, .. } => Some(*bounds),
DisplayListItem::CursorRect { bounds, .. } => Some(*bounds),
DisplayListItem::Border { bounds, .. } => Some(*bounds),
DisplayListItem::TextLayout { bounds, .. } => Some(*bounds),
DisplayListItem::Text { clip_rect, .. } => Some(*clip_rect),
DisplayListItem::Underline { bounds, .. } => Some(*bounds),
DisplayListItem::Strikethrough { bounds, .. } => Some(*bounds),
DisplayListItem::Overline { bounds, .. } => Some(*bounds),
DisplayListItem::Image { bounds, .. } => Some(*bounds),
DisplayListItem::ScrollBar { bounds, .. } => Some(*bounds),
DisplayListItem::ScrollBarStyled { info } => Some(info.bounds),
DisplayListItem::PushClip { bounds, .. } => Some(*bounds),
DisplayListItem::PushScrollFrame { clip_bounds, .. } => Some(*clip_bounds),
DisplayListItem::HitTestArea { bounds, .. } => Some(*bounds),
DisplayListItem::PushStackingContext { bounds, .. } => Some(*bounds),
DisplayListItem::VirtualView { bounds, .. } => Some(*bounds),
_ => None,
}
}
fn clip_and_offset_display_item(
item: &DisplayListItem,
page_top: f32,
page_bottom: f32,
) -> Option<DisplayListItem> {
match item {
DisplayListItem::Rect {
bounds,
color,
border_radius,
} => clip_rect_item(bounds.into_inner(), *color, *border_radius, page_top, page_bottom),
DisplayListItem::Border {
bounds,
widths,
colors,
styles,
border_radius,
} => clip_border_item(
bounds.into_inner(),
*widths,
*colors,
*styles,
border_radius.clone(),
page_top,
page_bottom,
),
DisplayListItem::SelectionRect {
bounds,
border_radius,
color,
} => clip_selection_rect_item(bounds.into_inner(), *border_radius, *color, page_top, page_bottom),
DisplayListItem::CursorRect { bounds, color } => {
clip_cursor_rect_item(bounds.into_inner(), *color, page_top, page_bottom)
}
DisplayListItem::Image { bounds, image, border_radius } => {
clip_image_item(bounds.into_inner(), image.clone(), *border_radius, page_top, page_bottom)
}
DisplayListItem::TextLayout {
layout,
bounds,
font_hash,
font_size_px,
color,
} => clip_text_layout_item(
layout,
bounds.into_inner(),
*font_hash,
*font_size_px,
*color,
page_top,
page_bottom,
),
DisplayListItem::Text {
glyphs,
font_hash,
font_size_px,
color,
clip_rect,
..
} => clip_text_item(
glyphs,
*font_hash,
*font_size_px,
*color,
clip_rect.into_inner(),
page_top,
page_bottom,
),
DisplayListItem::Underline {
bounds,
color,
thickness,
} => clip_text_decoration_item(
bounds.into_inner(),
*color,
*thickness,
TextDecorationType::Underline,
page_top,
page_bottom,
),
DisplayListItem::Strikethrough {
bounds,
color,
thickness,
} => clip_text_decoration_item(
bounds.into_inner(),
*color,
*thickness,
TextDecorationType::Strikethrough,
page_top,
page_bottom,
),
DisplayListItem::Overline {
bounds,
color,
thickness,
} => clip_text_decoration_item(
bounds.into_inner(),
*color,
*thickness,
TextDecorationType::Overline,
page_top,
page_bottom,
),
DisplayListItem::ScrollBar {
bounds,
color,
orientation,
opacity_key,
hit_id,
} => clip_scrollbar_item(
bounds.into_inner(),
*color,
*orientation,
*opacity_key,
*hit_id,
page_top,
page_bottom,
),
DisplayListItem::HitTestArea { bounds, tag } => {
clip_hit_test_area_item(bounds.into_inner(), *tag, page_top, page_bottom)
}
DisplayListItem::VirtualView {
child_dom_id,
bounds,
clip_rect,
} => clip_virtual_view_item(*child_dom_id, bounds.into_inner(), clip_rect.into_inner(), page_top, page_bottom),
DisplayListItem::ScrollBarStyled { info } => {
let bounds = info.bounds;
if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
None
} else {
let mut clipped_info = (**info).clone();
let y_offset = -page_top;
clipped_info.bounds = offset_rect_y(clipped_info.bounds.into_inner(), y_offset).into();
clipped_info.track_bounds = offset_rect_y(clipped_info.track_bounds.into_inner(), y_offset).into();
clipped_info.thumb_bounds = offset_rect_y(clipped_info.thumb_bounds.into_inner(), y_offset).into();
if let Some(b) = clipped_info.button_decrement_bounds {
clipped_info.button_decrement_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
}
if let Some(b) = clipped_info.button_increment_bounds {
clipped_info.button_increment_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
}
Some(DisplayListItem::ScrollBarStyled {
info: Box::new(clipped_info),
})
}
}
DisplayListItem::PushClip { .. }
| DisplayListItem::PopClip
| DisplayListItem::PushScrollFrame { .. }
| DisplayListItem::PopScrollFrame
| DisplayListItem::PushStackingContext { .. }
| DisplayListItem::PopStackingContext
| DisplayListItem::VirtualViewPlaceholder { .. } => None,
DisplayListItem::LinearGradient {
bounds,
gradient,
border_radius,
} => {
if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
None
} else {
Some(DisplayListItem::LinearGradient {
bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
gradient: gradient.clone(),
border_radius: *border_radius,
})
}
}
DisplayListItem::RadialGradient {
bounds,
gradient,
border_radius,
} => {
if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
None
} else {
Some(DisplayListItem::RadialGradient {
bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
gradient: gradient.clone(),
border_radius: *border_radius,
})
}
}
DisplayListItem::ConicGradient {
bounds,
gradient,
border_radius,
} => {
if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
None
} else {
Some(DisplayListItem::ConicGradient {
bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
gradient: gradient.clone(),
border_radius: *border_radius,
})
}
}
DisplayListItem::BoxShadow {
bounds,
shadow,
border_radius,
} => {
if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
None
} else {
Some(DisplayListItem::BoxShadow {
bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
shadow: *shadow,
border_radius: *border_radius,
})
}
}
DisplayListItem::PushFilter { .. }
| DisplayListItem::PopFilter
| DisplayListItem::PushBackdropFilter { .. }
| DisplayListItem::PopBackdropFilter
| DisplayListItem::PushOpacity { .. }
| DisplayListItem::PopOpacity
| DisplayListItem::PushReferenceFrame { .. }
| DisplayListItem::PopReferenceFrame
| DisplayListItem::PushTextShadow { .. }
| DisplayListItem::PopTextShadow
| DisplayListItem::PushImageMaskClip { .. }
| DisplayListItem::PopImageMaskClip => None,
}
}
#[derive(Debug, Clone, Copy)]
enum TextDecorationType {
Underline,
Strikethrough,
Overline,
}
fn clip_rect_item(
bounds: LogicalRect,
color: ColorU,
border_radius: BorderRadius,
page_top: f32,
page_bottom: f32,
) -> Option<DisplayListItem> {
clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::Rect {
bounds: clipped.into(),
color,
border_radius,
})
}
fn clip_border_item(
bounds: LogicalRect,
widths: StyleBorderWidths,
colors: StyleBorderColors,
styles: StyleBorderStyles,
border_radius: StyleBorderRadius,
page_top: f32,
page_bottom: f32,
) -> Option<DisplayListItem> {
let original_bounds = bounds;
clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| {
let new_widths = adjust_border_widths_for_clipping(
widths,
original_bounds,
clipped,
page_top,
page_bottom,
);
DisplayListItem::Border {
bounds: clipped.into(),
widths: new_widths,
colors,
styles,
border_radius,
}
})
}
fn adjust_border_widths_for_clipping(
mut widths: StyleBorderWidths,
original_bounds: LogicalRect,
clipped: LogicalRect,
page_top: f32,
page_bottom: f32,
) -> StyleBorderWidths {
if clipped.origin.y > 0.0 && original_bounds.origin.y < page_top {
widths.top = None;
}
let original_bottom = original_bounds.origin.y + original_bounds.size.height;
let clipped_bottom = clipped.origin.y + clipped.size.height;
if original_bottom > page_bottom && clipped_bottom >= page_bottom - page_top - 1.0 {
widths.bottom = None;
}
widths
}
fn clip_selection_rect_item(
bounds: LogicalRect,
border_radius: BorderRadius,
color: ColorU,
page_top: f32,
page_bottom: f32,
) -> Option<DisplayListItem> {
clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::SelectionRect {
bounds: clipped.into(),
border_radius,
color,
})
}
fn clip_cursor_rect_item(
bounds: LogicalRect,
color: ColorU,
page_top: f32,
page_bottom: f32,
) -> Option<DisplayListItem> {
clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::CursorRect {
bounds: clipped.into(),
color,
})
}
fn clip_image_item(
bounds: LogicalRect,
image: ImageRef,
border_radius: BorderRadius,
page_top: f32,
page_bottom: f32,
) -> Option<DisplayListItem> {
if !rect_intersects(&bounds, page_top, page_bottom) {
return None;
}
clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::Image {
bounds: clipped.into(),
image,
border_radius,
})
}
fn clip_text_layout_item(
layout: &Arc<dyn std::any::Any + Send + Sync>,
bounds: LogicalRect,
font_hash: FontHash,
font_size_px: f32,
color: ColorU,
page_top: f32,
page_bottom: f32,
) -> Option<DisplayListItem> {
if !rect_intersects(&bounds, page_top, page_bottom) {
return None;
}
#[cfg(feature = "text_layout")]
if let Some(unified_layout) = layout.downcast_ref::<crate::text3::cache::UnifiedLayout>() {
return clip_unified_layout(
unified_layout,
bounds,
font_hash,
font_size_px,
color,
page_top,
page_bottom,
);
}
Some(DisplayListItem::TextLayout {
layout: layout.clone(),
bounds: offset_rect_y(bounds, -page_top).into(),
font_hash,
font_size_px,
color,
})
}
#[cfg(feature = "text_layout")]
fn clip_unified_layout(
unified_layout: &crate::text3::cache::UnifiedLayout,
bounds: LogicalRect,
font_hash: FontHash,
font_size_px: f32,
color: ColorU,
page_top: f32,
page_bottom: f32,
) -> Option<DisplayListItem> {
let layout_origin_y = bounds.origin.y;
let layout_origin_x = bounds.origin.x;
let filtered_items: Vec<_> = unified_layout
.items
.iter()
.filter(|item| item_center_on_page(item, layout_origin_y, page_top, page_bottom))
.cloned()
.collect();
if filtered_items.is_empty() {
return None;
}
let new_origin_y = (layout_origin_y - page_top).max(0.0);
let (offset_items, min_y, max_y, max_width) =
transform_items_to_page_coords(filtered_items, layout_origin_y, page_top, new_origin_y);
let new_layout = crate::text3::cache::UnifiedLayout {
items: offset_items,
overflow: unified_layout.overflow.clone(),
};
let new_bounds = LogicalRect {
origin: LogicalPosition {
x: layout_origin_x,
y: new_origin_y,
},
size: LogicalSize {
width: max_width.max(bounds.size.width),
height: (max_y - min_y.min(0.0)).max(0.0),
},
};
Some(DisplayListItem::TextLayout {
layout: Arc::new(new_layout) as Arc<dyn std::any::Any + Send + Sync>,
bounds: new_bounds.into(),
font_hash,
font_size_px,
color,
})
}
#[cfg(feature = "text_layout")]
fn item_center_on_page(
item: &crate::text3::cache::PositionedItem,
layout_origin_y: f32,
page_top: f32,
page_bottom: f32,
) -> bool {
let item_y_absolute = layout_origin_y + item.position.y;
let item_height = item.item.bounds().height;
let item_center_y = item_y_absolute + (item_height / 2.0);
item_center_y >= page_top && item_center_y < page_bottom
}
#[cfg(feature = "text_layout")]
fn transform_items_to_page_coords(
items: Vec<crate::text3::cache::PositionedItem>,
layout_origin_y: f32,
page_top: f32,
new_origin_y: f32,
) -> (Vec<crate::text3::cache::PositionedItem>, f32, f32, f32) {
let mut min_y = f32::MAX;
let mut max_y = f32::MIN;
let mut max_width = 0.0f32;
let offset_items: Vec<_> = items
.into_iter()
.map(|mut item| {
let abs_y = layout_origin_y + item.position.y;
let page_y = abs_y - page_top;
let new_item_y = page_y - new_origin_y;
let item_bounds = item.item.bounds();
min_y = min_y.min(new_item_y);
max_y = max_y.max(new_item_y + item_bounds.height);
max_width = max_width.max(item.position.x + item_bounds.width);
item.position.y = new_item_y;
item
})
.collect();
(offset_items, min_y, max_y, max_width)
}
fn clip_text_item(
glyphs: &[GlyphInstance],
font_hash: FontHash,
font_size_px: f32,
color: ColorU,
clip_rect: LogicalRect,
page_top: f32,
page_bottom: f32,
) -> Option<DisplayListItem> {
if !rect_intersects(&clip_rect, page_top, page_bottom) {
return None;
}
let page_glyphs: Vec<_> = glyphs
.iter()
.filter(|g| g.point.y >= page_top && g.point.y < page_bottom)
.map(|g| GlyphInstance {
index: g.index,
point: LogicalPosition {
x: g.point.x,
y: g.point.y - page_top,
},
size: g.size,
})
.collect();
if page_glyphs.is_empty() {
return None;
}
Some(DisplayListItem::Text {
glyphs: page_glyphs,
font_hash,
font_size_px,
color,
clip_rect: offset_rect_y(clip_rect, -page_top).into(),
source_node_index: None,
})
}
fn clip_text_decoration_item(
bounds: LogicalRect,
color: ColorU,
thickness: f32,
decoration_type: TextDecorationType,
page_top: f32,
page_bottom: f32,
) -> Option<DisplayListItem> {
clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| match decoration_type {
TextDecorationType::Underline => DisplayListItem::Underline {
bounds: clipped.into(),
color,
thickness,
},
TextDecorationType::Strikethrough => DisplayListItem::Strikethrough {
bounds: clipped.into(),
color,
thickness,
},
TextDecorationType::Overline => DisplayListItem::Overline {
bounds: clipped.into(),
color,
thickness,
},
})
}
fn clip_scrollbar_item(
bounds: LogicalRect,
color: ColorU,
orientation: ScrollbarOrientation,
opacity_key: Option<OpacityKey>,
hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
page_top: f32,
page_bottom: f32,
) -> Option<DisplayListItem> {
clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::ScrollBar {
bounds: clipped.into(),
color,
orientation,
opacity_key,
hit_id,
})
}
fn clip_hit_test_area_item(
bounds: LogicalRect,
tag: DisplayListTagId,
page_top: f32,
page_bottom: f32,
) -> Option<DisplayListItem> {
clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::HitTestArea {
bounds: clipped.into(),
tag,
})
}
fn clip_virtual_view_item(
child_dom_id: DomId,
bounds: LogicalRect,
clip_rect: LogicalRect,
page_top: f32,
page_bottom: f32,
) -> Option<DisplayListItem> {
clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::VirtualView {
child_dom_id,
bounds: clipped.into(),
clip_rect: offset_rect_y(clip_rect, -page_top).into(),
})
}
fn clip_rect_bounds(bounds: LogicalRect, page_top: f32, page_bottom: f32) -> Option<LogicalRect> {
let item_top = bounds.origin.y;
let item_bottom = bounds.origin.y + bounds.size.height;
if item_bottom <= page_top || item_top >= page_bottom {
return None;
}
let clipped_top = item_top.max(page_top);
let clipped_bottom = item_bottom.min(page_bottom);
let clipped_height = clipped_bottom - clipped_top;
let page_relative_y = clipped_top - page_top;
Some(LogicalRect {
origin: LogicalPosition {
x: bounds.origin.x,
y: page_relative_y,
},
size: LogicalSize {
width: bounds.size.width,
height: clipped_height,
},
})
}
fn rect_intersects(bounds: &LogicalRect, page_top: f32, page_bottom: f32) -> bool {
let item_top = bounds.origin.y;
let item_bottom = bounds.origin.y + bounds.size.height;
item_bottom > page_top && item_top < page_bottom
}
fn offset_rect_y(bounds: LogicalRect, offset_y: f32) -> LogicalRect {
LogicalRect {
origin: LogicalPosition {
x: bounds.origin.x,
y: bounds.origin.y + offset_y,
},
size: bounds.size,
}
}
use azul_css::props::layout::fragmentation::{BreakInside, PageBreak};
use crate::solver3::pagination::{
HeaderFooterConfig, MarginBoxContent, PageInfo, TableHeaderInfo, TableHeaderTracker,
};
#[derive(Debug, Clone, Default)]
pub struct SlicerConfig {
pub page_content_height: f32,
pub page_gap: f32,
pub allow_clipping: bool,
pub header_footer: HeaderFooterConfig,
pub page_width: f32,
pub table_headers: TableHeaderTracker,
}
impl SlicerConfig {
pub fn simple(page_height: f32) -> Self {
Self {
page_content_height: page_height,
page_gap: 0.0,
allow_clipping: true,
header_footer: HeaderFooterConfig::default(),
page_width: 595.0, table_headers: TableHeaderTracker::default(),
}
}
pub fn with_gap(page_height: f32, gap: f32) -> Self {
Self {
page_content_height: page_height,
page_gap: gap,
allow_clipping: true,
header_footer: HeaderFooterConfig::default(),
page_width: 595.0,
table_headers: TableHeaderTracker::default(),
}
}
pub fn with_header_footer(mut self, config: HeaderFooterConfig) -> Self {
self.header_footer = config;
self
}
pub fn with_page_width(mut self, width: f32) -> Self {
self.page_width = width;
self
}
pub fn with_table_headers(mut self, tracker: TableHeaderTracker) -> Self {
self.table_headers = tracker;
self
}
pub fn register_table_header(&mut self, info: TableHeaderInfo) {
self.table_headers.register_table_header(info);
}
pub fn page_slot_height(&self) -> f32 {
self.page_content_height + self.page_gap
}
pub fn page_for_y(&self, y: f32) -> usize {
if self.page_slot_height() <= 0.0 {
return 0;
}
(y / self.page_slot_height()).floor() as usize
}
pub fn page_bounds(&self, page_index: usize) -> (f32, f32) {
let start = page_index as f32 * self.page_slot_height();
let end = start + self.page_content_height;
(start, end)
}
}
pub fn paginate_display_list_with_slicer_and_breaks(
full_display_list: DisplayList,
config: &SlicerConfig,
) -> Result<Vec<DisplayList>> {
if config.page_content_height <= 0.0 || config.page_content_height >= f32::MAX {
return Ok(vec![full_display_list]);
}
let base_header_space = if config.header_footer.show_header {
config.header_footer.header_height
} else {
0.0
};
let base_footer_space = if config.header_footer.show_footer {
config.header_footer.footer_height
} else {
0.0
};
let normal_page_content_height =
config.page_content_height - base_header_space - base_footer_space;
let first_page_content_height = if config.header_footer.skip_first_page {
config.page_content_height
} else {
normal_page_content_height
};
let page_breaks = calculate_page_break_positions(
&full_display_list,
first_page_content_height,
normal_page_content_height,
);
let num_pages = page_breaks.len();
let mut pages: Vec<DisplayList> = Vec::with_capacity(num_pages);
for (page_idx, &(content_start_y, content_end_y)) in page_breaks.iter().enumerate() {
let page_info = PageInfo::new(page_idx + 1, num_pages);
let skip_this_page = config.header_footer.skip_first_page && page_info.is_first;
let header_space = if config.header_footer.show_header && !skip_this_page {
config.header_footer.header_height
} else {
0.0
};
let footer_space = if config.header_footer.show_footer && !skip_this_page {
config.header_footer.footer_height
} else {
0.0
};
let _ = footer_space;
let mut page_items = Vec::new();
let mut page_node_mapping = Vec::new();
if config.header_footer.show_header && !skip_this_page {
let header_text = config.header_footer.header_text(page_info);
if !header_text.is_empty() {
let header_items = generate_text_display_items(
&header_text,
LogicalRect {
origin: LogicalPosition { x: 0.0, y: 0.0 },
size: LogicalSize {
width: config.page_width,
height: config.header_footer.header_height,
},
},
config.header_footer.font_size,
config.header_footer.text_color,
TextAlignment::Center,
);
for item in header_items {
page_items.push(item);
page_node_mapping.push(None);
}
}
}
let repeated_headers = config.table_headers.get_repeated_headers_for_page(
page_idx,
content_start_y,
content_end_y,
);
let mut thead_total_height = 0.0f32;
for (y_offset_from_page_top, thead_items, thead_height) in repeated_headers {
let thead_y = header_space + y_offset_from_page_top;
for item in thead_items {
let translated_item = offset_display_item_y(item, thead_y);
page_items.push(translated_item);
page_node_mapping.push(None);
}
thead_total_height = thead_total_height.max(thead_height);
}
let content_y_offset = header_space + thead_total_height;
for (item_idx, item) in full_display_list.items.iter().enumerate() {
let is_fixed = full_display_list.fixed_position_item_ranges.iter()
.any(|&(start, end)| item_idx >= start && item_idx < end);
if is_fixed {
continue;
}
if let Some(clipped_item) =
clip_and_offset_display_item(item, content_start_y, content_end_y)
{
let final_item = if content_y_offset > 0.0 {
offset_display_item_y(&clipped_item, content_y_offset)
} else {
clipped_item
};
page_items.push(final_item);
let node_mapping = full_display_list
.node_mapping
.get(item_idx)
.copied()
.flatten();
page_node_mapping.push(node_mapping);
}
}
for &(start, end) in &full_display_list.fixed_position_item_ranges {
for item_idx in start..end {
if let Some(item) = full_display_list.items.get(item_idx) {
let final_item = if content_y_offset > 0.0 {
offset_display_item_y(item, content_y_offset)
} else {
item.clone()
};
page_items.push(final_item);
let node_mapping = full_display_list
.node_mapping
.get(item_idx)
.copied()
.flatten();
page_node_mapping.push(node_mapping);
}
}
}
if config.header_footer.show_footer && !skip_this_page {
let footer_text = config.header_footer.footer_text(page_info);
if !footer_text.is_empty() {
let footer_y = config.page_content_height - config.header_footer.footer_height;
let footer_items = generate_text_display_items(
&footer_text,
LogicalRect {
origin: LogicalPosition {
x: 0.0,
y: footer_y,
},
size: LogicalSize {
width: config.page_width,
height: config.header_footer.footer_height,
},
},
config.header_footer.font_size,
config.header_footer.text_color,
TextAlignment::Center,
);
for item in footer_items {
page_items.push(item);
page_node_mapping.push(None);
}
}
}
pages.push(DisplayList {
items: page_items,
node_mapping: page_node_mapping,
forced_page_breaks: Vec::new(),
fixed_position_item_ranges: Vec::new(), });
}
if pages.is_empty() {
pages.push(DisplayList::default());
}
Ok(pages)
}
fn calculate_page_break_positions(
display_list: &DisplayList,
first_page_height: f32,
normal_page_height: f32,
) -> Vec<(f32, f32)> {
let total_height = calculate_display_list_height(display_list);
if total_height <= 0.0 || first_page_height <= 0.0 {
return vec![(0.0, total_height.max(first_page_height))];
}
let mut break_points: Vec<f32> = Vec::new();
for &forced_break_y in &display_list.forced_page_breaks {
if forced_break_y > 0.0 && forced_break_y < total_height {
break_points.push(forced_break_y);
}
}
let mut y = first_page_height;
while y < total_height {
break_points.push(y);
y += normal_page_height;
}
break_points.sort_by(|a, b| a.partial_cmp(b).unwrap());
break_points.dedup_by(|a, b| (*a - *b).abs() < 1.0);
let mut page_breaks: Vec<(f32, f32)> = Vec::new();
let mut page_start = 0.0f32;
for break_y in break_points {
if break_y > page_start {
page_breaks.push((page_start, break_y));
page_start = break_y;
}
}
if page_start < total_height {
page_breaks.push((page_start, total_height));
}
if page_breaks.is_empty() {
page_breaks.push((0.0, total_height.max(first_page_height)));
}
page_breaks
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextAlignment {
Left,
Center,
Right,
}
fn offset_display_item_y(item: &DisplayListItem, y_offset: f32) -> DisplayListItem {
if y_offset == 0.0 {
return item.clone();
}
match item {
DisplayListItem::Rect {
bounds,
color,
border_radius,
} => DisplayListItem::Rect {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
color: *color,
border_radius: *border_radius,
},
DisplayListItem::Border {
bounds,
widths,
colors,
styles,
border_radius,
} => DisplayListItem::Border {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
widths: widths.clone(),
colors: *colors,
styles: *styles,
border_radius: border_radius.clone(),
},
DisplayListItem::Text {
glyphs,
font_hash,
font_size_px,
color,
clip_rect,
..
} => {
let offset_glyphs: Vec<GlyphInstance> = glyphs
.iter()
.map(|g| GlyphInstance {
index: g.index,
point: LogicalPosition {
x: g.point.x,
y: g.point.y + y_offset,
},
size: g.size,
})
.collect();
DisplayListItem::Text {
glyphs: offset_glyphs,
font_hash: *font_hash,
font_size_px: *font_size_px,
color: *color,
clip_rect: offset_rect_y(clip_rect.into_inner(), y_offset).into(),
source_node_index: None,
}
}
DisplayListItem::TextLayout {
layout,
bounds,
font_hash,
font_size_px,
color,
} => DisplayListItem::TextLayout {
layout: layout.clone(),
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
font_hash: *font_hash,
font_size_px: *font_size_px,
color: *color,
},
DisplayListItem::Image { bounds, image, border_radius } => DisplayListItem::Image {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
image: image.clone(),
border_radius: *border_radius,
},
DisplayListItem::SelectionRect {
bounds,
border_radius,
color,
} => DisplayListItem::SelectionRect {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
border_radius: *border_radius,
color: *color,
},
DisplayListItem::CursorRect { bounds, color } => DisplayListItem::CursorRect {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
color: *color,
},
DisplayListItem::Underline {
bounds,
color,
thickness,
} => DisplayListItem::Underline {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
color: *color,
thickness: *thickness,
},
DisplayListItem::Strikethrough {
bounds,
color,
thickness,
} => DisplayListItem::Strikethrough {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
color: *color,
thickness: *thickness,
},
DisplayListItem::Overline {
bounds,
color,
thickness,
} => DisplayListItem::Overline {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
color: *color,
thickness: *thickness,
},
DisplayListItem::ScrollBar {
bounds,
color,
orientation,
opacity_key,
hit_id,
} => DisplayListItem::ScrollBar {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
color: *color,
orientation: *orientation,
opacity_key: *opacity_key,
hit_id: *hit_id,
},
DisplayListItem::HitTestArea { bounds, tag } => DisplayListItem::HitTestArea {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
tag: *tag,
},
DisplayListItem::PushClip {
bounds,
border_radius,
} => DisplayListItem::PushClip {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
border_radius: *border_radius,
},
DisplayListItem::PushScrollFrame {
clip_bounds,
content_size,
scroll_id,
} => DisplayListItem::PushScrollFrame {
clip_bounds: offset_rect_y(clip_bounds.into_inner(), y_offset).into(),
content_size: *content_size,
scroll_id: *scroll_id,
},
DisplayListItem::PushStackingContext { bounds, z_index } => {
DisplayListItem::PushStackingContext {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
z_index: *z_index,
}
}
DisplayListItem::VirtualView {
child_dom_id,
bounds,
clip_rect,
} => DisplayListItem::VirtualView {
child_dom_id: *child_dom_id,
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
clip_rect: offset_rect_y(clip_rect.into_inner(), y_offset).into(),
},
DisplayListItem::VirtualViewPlaceholder {
node_id,
bounds,
clip_rect,
} => DisplayListItem::VirtualViewPlaceholder {
node_id: *node_id,
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
clip_rect: offset_rect_y(clip_rect.into_inner(), y_offset).into(),
},
DisplayListItem::PopClip => DisplayListItem::PopClip,
DisplayListItem::PopScrollFrame => DisplayListItem::PopScrollFrame,
DisplayListItem::PopStackingContext => DisplayListItem::PopStackingContext,
DisplayListItem::LinearGradient {
bounds,
gradient,
border_radius,
} => DisplayListItem::LinearGradient {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
gradient: gradient.clone(),
border_radius: *border_radius,
},
DisplayListItem::RadialGradient {
bounds,
gradient,
border_radius,
} => DisplayListItem::RadialGradient {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
gradient: gradient.clone(),
border_radius: *border_radius,
},
DisplayListItem::ConicGradient {
bounds,
gradient,
border_radius,
} => DisplayListItem::ConicGradient {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
gradient: gradient.clone(),
border_radius: *border_radius,
},
DisplayListItem::BoxShadow {
bounds,
shadow,
border_radius,
} => DisplayListItem::BoxShadow {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
shadow: *shadow,
border_radius: *border_radius,
},
DisplayListItem::PushFilter { bounds, filters } => DisplayListItem::PushFilter {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
filters: filters.clone(),
},
DisplayListItem::PopFilter => DisplayListItem::PopFilter,
DisplayListItem::PushBackdropFilter { bounds, filters } => {
DisplayListItem::PushBackdropFilter {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
filters: filters.clone(),
}
}
DisplayListItem::PopBackdropFilter => DisplayListItem::PopBackdropFilter,
DisplayListItem::PushOpacity { bounds, opacity } => DisplayListItem::PushOpacity {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
opacity: *opacity,
},
DisplayListItem::PopOpacity => DisplayListItem::PopOpacity,
DisplayListItem::ScrollBarStyled { info } => {
let mut offset_info = (**info).clone();
offset_info.bounds = offset_rect_y(offset_info.bounds.into_inner(), y_offset).into();
offset_info.track_bounds = offset_rect_y(offset_info.track_bounds.into_inner(), y_offset).into();
offset_info.thumb_bounds = offset_rect_y(offset_info.thumb_bounds.into_inner(), y_offset).into();
if let Some(b) = offset_info.button_decrement_bounds {
offset_info.button_decrement_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
}
if let Some(b) = offset_info.button_increment_bounds {
offset_info.button_increment_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
}
DisplayListItem::ScrollBarStyled {
info: Box::new(offset_info),
}
}
DisplayListItem::PushReferenceFrame {
transform_key,
initial_transform,
bounds,
} => DisplayListItem::PushReferenceFrame {
transform_key: *transform_key,
initial_transform: *initial_transform,
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
},
DisplayListItem::PopReferenceFrame => DisplayListItem::PopReferenceFrame,
DisplayListItem::PushTextShadow { shadow } => DisplayListItem::PushTextShadow {
shadow: shadow.clone(),
},
DisplayListItem::PopTextShadow => DisplayListItem::PopTextShadow,
DisplayListItem::PushImageMaskClip {
bounds,
mask_image,
mask_rect,
} => DisplayListItem::PushImageMaskClip {
bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
mask_image: mask_image.clone(),
mask_rect: offset_rect_y(mask_rect.into_inner(), y_offset).into(),
},
DisplayListItem::PopImageMaskClip => DisplayListItem::PopImageMaskClip,
}
}
fn generate_text_display_items(
text: &str,
bounds: LogicalRect,
font_size: f32,
color: ColorU,
alignment: TextAlignment,
) -> Vec<DisplayListItem> {
use crate::font_traits::FontHash;
if text.is_empty() {
return Vec::new();
}
let char_width = font_size * 0.5;
let text_width = text.len() as f32 * char_width;
let x_offset = match alignment {
TextAlignment::Left => bounds.origin.x,
TextAlignment::Center => bounds.origin.x + (bounds.size.width - text_width) / 2.0,
TextAlignment::Right => bounds.origin.x + bounds.size.width - text_width,
};
let y_pos = bounds.origin.y + (bounds.size.height + font_size) / 2.0 - font_size * 0.2;
let glyphs: Vec<GlyphInstance> = text
.chars()
.enumerate()
.filter(|(_, c)| !c.is_control())
.map(|(i, c)| GlyphInstance {
index: c as u32, point: LogicalPosition {
x: x_offset + i as f32 * char_width,
y: y_pos,
},
size: LogicalSize::new(char_width, font_size),
})
.collect();
if glyphs.is_empty() {
return Vec::new();
}
vec![DisplayListItem::Text {
glyphs,
font_hash: FontHash::from_hash(0), font_size_px: font_size,
color,
clip_rect: bounds.into(),
source_node_index: None,
}]
}
fn calculate_display_list_height(display_list: &DisplayList) -> f32 {
let mut max_bottom = 0.0f32;
for item in &display_list.items {
if let Some(bounds) = get_display_item_bounds(item) {
if bounds.0.size.height < 0.1 {
continue;
}
let item_bottom = bounds.0.origin.y + bounds.0.size.height;
if item_bottom > max_bottom {
max_bottom = item_bottom;
}
}
}
max_bottom
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BreakProperties {
pub break_before: PageBreak,
pub break_after: PageBreak,
pub break_inside: BreakInside,
}
pub fn apply_text_overflow_ellipsis(
display_list: &mut DisplayList,
container_bounds: LogicalRect,
_ellipsis: &str,
) {
let container_right = container_bounds.origin.x + container_bounds.size.width;
for item in display_list.items.iter_mut() {
match item {
DisplayListItem::Text {
glyphs,
font_size_px,
clip_rect,
..
} => {
if glyphs.is_empty() {
continue;
}
let last_glyph = &glyphs[glyphs.len() - 1];
let last_glyph_right = last_glyph.point.x + last_glyph.size.width;
if last_glyph_right <= container_right {
continue; }
let ellipsis_width = *font_size_px * 0.6;
let truncation_edge = container_right - ellipsis_width;
let mut keep_count = 0;
for (i, glyph) in glyphs.iter().enumerate() {
let glyph_right = glyph.point.x + glyph.size.width;
if glyph_right > truncation_edge {
break;
}
keep_count = i + 1;
}
glyphs.truncate(keep_count);
let ellipsis_x = if let Some(last) = glyphs.last() {
last.point.x + last.size.width
} else {
container_bounds.origin.x
};
let ellipsis_glyph = GlyphInstance {
index: 0x2026, point: LogicalPosition::new(ellipsis_x, glyphs.first().map_or(
container_bounds.origin.y,
|g| g.point.y,
)),
size: LogicalSize::new(ellipsis_width, *font_size_px),
};
glyphs.push(ellipsis_glyph);
*clip_rect = container_bounds.into();
}
_ => {} }
}
}
pub fn resolve_clip_path(
clip_path: &azul_css::props::layout::shape::ClipPath,
node_bounds: LogicalRect,
) -> Option<(LogicalRect, f32)> {
use azul_css::props::layout::shape::ClipPath;
use azul_css::shape::CssShape;
match clip_path {
ClipPath::None => None,
ClipPath::Shape(shape) => {
match shape {
CssShape::Inset(inset) => {
let x = node_bounds.origin.x + inset.inset_left;
let y = node_bounds.origin.y + inset.inset_top;
let w = (node_bounds.size.width - inset.inset_left - inset.inset_right).max(0.0);
let h = (node_bounds.size.height - inset.inset_top - inset.inset_bottom).max(0.0);
let radius = match inset.border_radius {
azul_css::corety::OptionF32::Some(r) => r,
azul_css::corety::OptionF32::None => 0.0,
};
Some((LogicalRect {
origin: LogicalPosition::new(x, y),
size: LogicalSize::new(w, h),
}, radius))
}
CssShape::Circle(circle) => {
let cx = node_bounds.origin.x + circle.center.x;
let cy = node_bounds.origin.y + circle.center.y;
let r = circle.radius;
Some((LogicalRect {
origin: LogicalPosition::new(cx - r, cy - r),
size: LogicalSize::new(r * 2.0, r * 2.0),
}, r))
}
CssShape::Ellipse(ellipse) => {
let cx = node_bounds.origin.x + ellipse.center.x;
let cy = node_bounds.origin.y + ellipse.center.y;
let rx = ellipse.radius_x;
let ry = ellipse.radius_y;
let radius = rx.min(ry);
Some((LogicalRect {
origin: LogicalPosition::new(cx - rx, cy - ry),
size: LogicalSize::new(rx * 2.0, ry * 2.0),
}, radius))
}
CssShape::Polygon(polygon) => {
if polygon.points.is_empty() {
return None;
}
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut max_y = f32::NEG_INFINITY;
for point in polygon.points.iter() {
let px = node_bounds.origin.x + point.x;
let py = node_bounds.origin.y + point.y;
min_x = min_x.min(px);
min_y = min_y.min(py);
max_x = max_x.max(px);
max_y = max_y.max(py);
}
Some((LogicalRect {
origin: LogicalPosition::new(min_x, min_y),
size: LogicalSize::new((max_x - min_x).max(0.0), (max_y - min_y).max(0.0)),
}, 0.0))
}
CssShape::Path(_) => {
None
}
}
}
}
}
pub fn apply_clip_path(
display_list: &mut DisplayList,
start_index: usize,
clip_rect: LogicalRect,
border_radius: f32,
) {
let br = if border_radius > 0.0 {
BorderRadius {
top_left: border_radius,
top_right: border_radius,
bottom_left: border_radius,
bottom_right: border_radius,
}
} else {
BorderRadius::default()
};
display_list.items.insert(start_index, DisplayListItem::PushClip {
bounds: clip_rect.into(),
border_radius: br,
});
if display_list.node_mapping.len() >= start_index {
display_list.node_mapping.insert(start_index, None);
}
display_list.items.push(DisplayListItem::PopClip);
display_list.node_mapping.push(None);
}
#[cfg(feature = "cpurender")]
fn rasterize_svg_clip_to_r8(
svg_clip: &azul_core::svg::SvgMultiPolygon,
paint_rect: &LogicalRect,
) -> Option<azul_core::resources::ImageRef> {
use agg_rust::{
basics::FillingRule,
color::Rgba8,
path_storage::PathStorage,
pixfmt_rgba::PixfmtRgba32,
rasterizer_scanline_aa::RasterizerScanlineAa,
renderer_base::RendererBase,
renderer_scanline::render_scanlines_aa_solid,
rendering_buffer::RowAccessor,
scanline_u::ScanlineU8,
};
use azul_core::resources::{ImageRef, RawImage, RawImageFormat, RawImageData};
let w = paint_rect.size.width.ceil() as u32;
let h = paint_rect.size.height.ceil() as u32;
if w == 0 || h == 0 {
return None;
}
let mut path = PathStorage::new();
for ring in svg_clip.rings.as_ref().iter() {
let mut first = true;
for item in ring.items.as_ref().iter() {
match item {
azul_core::svg::SvgPathElement::Line(l) => {
if first {
path.move_to(
(l.start.x - paint_rect.origin.x) as f64,
(l.start.y - paint_rect.origin.y) as f64,
);
first = false;
}
path.line_to(
(l.end.x - paint_rect.origin.x) as f64,
(l.end.y - paint_rect.origin.y) as f64,
);
}
azul_core::svg::SvgPathElement::QuadraticCurve(q) => {
if first {
path.move_to(
(q.start.x - paint_rect.origin.x) as f64,
(q.start.y - paint_rect.origin.y) as f64,
);
first = false;
}
path.curve3(
(q.ctrl.x - paint_rect.origin.x) as f64,
(q.ctrl.y - paint_rect.origin.y) as f64,
(q.end.x - paint_rect.origin.x) as f64,
(q.end.y - paint_rect.origin.y) as f64,
);
}
azul_core::svg::SvgPathElement::CubicCurve(c) => {
if first {
path.move_to(
(c.start.x - paint_rect.origin.x) as f64,
(c.start.y - paint_rect.origin.y) as f64,
);
first = false;
}
path.curve4(
(c.ctrl_1.x - paint_rect.origin.x) as f64,
(c.ctrl_1.y - paint_rect.origin.y) as f64,
(c.ctrl_2.x - paint_rect.origin.x) as f64,
(c.ctrl_2.y - paint_rect.origin.y) as f64,
(c.end.x - paint_rect.origin.x) as f64,
(c.end.y - paint_rect.origin.y) as f64,
);
}
}
}
}
let mut rgba_buf = vec![0u8; (w * h * 4) as usize];
{
let stride = (w * 4) as i32;
let mut ra = unsafe {
RowAccessor::new_with_buf(rgba_buf.as_mut_ptr(), w, h, stride)
};
let pf = PixfmtRgba32::new(&mut ra);
let mut rb = RendererBase::new(pf);
let mut ras = RasterizerScanlineAa::new();
ras.filling_rule(FillingRule::NonZero);
ras.add_path(&mut path, 0);
let mut sl = ScanlineU8::new();
let white = Rgba8 { r: 255, g: 255, b: 255, a: 255 };
render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &white);
}
let r8_data: Vec<u8> = rgba_buf.chunks_exact(4).map(|px| px[3]).collect();
ImageRef::new_rawimage(RawImage {
pixels: RawImageData::U8(r8_data.into()),
width: w as usize,
height: h as usize,
premultiplied_alpha: false,
data_format: RawImageFormat::R8,
tag: Vec::new().into(),
})
}