use alloc::{boxed::Box, collections::btree_map::BTreeMap, string::String, vec::Vec};
use core::{
fmt,
hash::{Hash, Hasher},
};
use azul_css::{
css::Css,
props::{
basic::{StyleFontFamily, StyleFontFamilyVec, StyleFontSize},
property::{
BoxDecorationBreakValue, BreakInsideValue, CaretAnimationDurationValue,
CaretColorValue, ColumnCountValue, ColumnFillValue, ColumnRuleColorValue,
ColumnRuleStyleValue, ColumnRuleWidthValue, ColumnSpanValue, ColumnWidthValue,
ContentValue, CounterIncrementValue, CounterResetValue, CssProperty, CssPropertyType,
RelayoutScope,
FlowFromValue, FlowIntoValue, LayoutAlignContentValue, LayoutAlignItemsValue,
LayoutAlignSelfValue, LayoutBorderBottomWidthValue, LayoutBorderLeftWidthValue,
LayoutBorderRightWidthValue, LayoutBorderTopWidthValue, LayoutBoxSizingValue,
LayoutClearValue, LayoutColumnGapValue, LayoutDisplayValue, LayoutFlexBasisValue,
LayoutFlexDirectionValue, LayoutFlexGrowValue, LayoutFlexShrinkValue,
LayoutFlexWrapValue, LayoutFloatValue, LayoutGapValue, LayoutGridAutoColumnsValue,
LayoutGridAutoFlowValue, LayoutGridAutoRowsValue, LayoutGridColumnValue,
LayoutGridRowValue, LayoutGridTemplateColumnsValue, LayoutGridTemplateRowsValue,
LayoutHeightValue, LayoutInsetBottomValue, LayoutJustifyContentValue,
LayoutJustifyItemsValue, LayoutJustifySelfValue, LayoutLeftValue,
LayoutMarginBottomValue, LayoutMarginLeftValue, LayoutMarginRightValue,
LayoutMarginTopValue, LayoutMaxHeightValue, LayoutMaxWidthValue, LayoutMinHeightValue,
LayoutMinWidthValue, LayoutOverflowValue, LayoutPaddingBottomValue,
LayoutPaddingLeftValue, LayoutPaddingRightValue, LayoutPaddingTopValue,
LayoutPositionValue, LayoutRightValue, LayoutRowGapValue, LayoutScrollbarWidthValue,
LayoutTextJustifyValue, LayoutTopValue, LayoutWidthValue, LayoutWritingModeValue,
LayoutZIndexValue, OrphansValue, PageBreakValue,
SelectionBackgroundColorValue, SelectionColorValue, ShapeImageThresholdValue,
ShapeMarginValue, ShapeOutsideValue, StringSetValue, StyleBackfaceVisibilityValue,
StyleBackgroundContentVecValue, StyleBackgroundPositionVecValue,
StyleBackgroundRepeatVecValue, StyleBackgroundSizeVecValue,
StyleBorderBottomColorValue, StyleBorderBottomLeftRadiusValue,
StyleBorderBottomRightRadiusValue, StyleBorderBottomStyleValue,
StyleBorderLeftColorValue, StyleBorderLeftStyleValue, StyleBorderRightColorValue,
StyleBorderRightStyleValue, StyleBorderTopColorValue, StyleBorderTopLeftRadiusValue,
StyleBorderTopRightRadiusValue, StyleBorderTopStyleValue, StyleBoxShadowValue,
StyleCursorValue, StyleDirectionValue, StyleFilterVecValue, StyleFontFamilyVecValue,
StyleFontSizeValue, StyleFontValue, StyleHyphensValue, StyleLetterSpacingValue,
StyleLineHeightValue, StyleMixBlendModeValue, StyleOpacityValue,
StylePerspectiveOriginValue, StyleScrollbarColorValue, StyleTabSizeValue,
StyleTextAlignValue, StyleTextColorValue, StyleTransformOriginValue,
StyleTransformVecValue, StyleVisibilityValue, StyleWhiteSpaceValue,
StyleWordSpacingValue, WidowsValue,
},
style::StyleTextColor,
},
AzString,
};
use crate::{
callbacks::Update,
dom::{Dom, DomId, NodeData, NodeDataVec, OptionTabIndex, TabIndex, TagId},
events::{RelayoutNodes, RestyleNodes},
id::{
Node, NodeDataContainer, NodeDataContainerRef, NodeDataContainerRefMut, NodeHierarchy,
NodeId,
},
menu::Menu,
prop_cache::{CssPropertyCache, CssPropertyCachePtr},
refany::RefAny,
resources::{Au, ImageCache, ImageRef, ImmediateFontId, RendererResources},
style::{
construct_html_cascade_tree, matches_html_element, rule_ends_with, CascadeInfo,
CascadeInfoVec,
},
FastBTreeSet, OrderedMap,
};
#[repr(C)]
#[derive(Debug, Clone, PartialEq, Hash, PartialOrd, Eq, Ord)]
pub struct ChangedCssProperty {
pub previous_state: StyledNodeState,
pub previous_prop: CssProperty,
pub current_state: StyledNodeState,
pub current_prop: CssProperty,
}
impl_option!(
ChangedCssProperty,
OptionChangedCssProperty,
copy = false,
[Debug, Clone, PartialEq, Hash, PartialOrd, Eq, Ord]
);
impl_vec!(ChangedCssProperty, ChangedCssPropertyVec, ChangedCssPropertyVecDestructor, ChangedCssPropertyVecDestructorType, ChangedCssPropertyVecSlice, OptionChangedCssProperty);
impl_vec_debug!(ChangedCssProperty, ChangedCssPropertyVec);
impl_vec_partialord!(ChangedCssProperty, ChangedCssPropertyVec);
impl_vec_clone!(
ChangedCssProperty,
ChangedCssPropertyVec,
ChangedCssPropertyVecDestructor
);
impl_vec_partialeq!(ChangedCssProperty, ChangedCssPropertyVec);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FocusChange {
pub lost_focus: Option<NodeId>,
pub gained_focus: Option<NodeId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HoverChange {
pub left_nodes: Vec<NodeId>,
pub entered_nodes: Vec<NodeId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActiveChange {
pub deactivated: Vec<NodeId>,
pub activated: Vec<NodeId>,
}
#[derive(Debug, Clone, Default)]
pub struct RestyleResult {
pub changed_nodes: RestyleNodes,
pub needs_layout: bool,
pub needs_display_list: bool,
pub gpu_only_changes: bool,
pub max_relayout_scope: RelayoutScope,
}
impl RestyleResult {
#[must_use] pub fn has_changes(&self) -> bool {
!self.changed_nodes.is_empty()
}
pub fn merge(&mut self, other: Self) {
for (node_id, changes) in other.changed_nodes {
self.changed_nodes.entry(node_id).or_default().extend(changes);
}
self.needs_layout = self.needs_layout || other.needs_layout;
self.needs_display_list = self.needs_display_list || other.needs_display_list;
self.gpu_only_changes = self.gpu_only_changes && other.gpu_only_changes;
if other.max_relayout_scope > self.max_relayout_scope {
self.max_relayout_scope = other.max_relayout_scope;
}
}
}
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Hash, PartialOrd, Eq, Ord, Default)]
pub struct StyledNodeState {
pub hover: bool,
pub active: bool,
pub focused: bool,
pub disabled: bool,
pub checked: bool,
pub focus_within: bool,
pub visited: bool,
pub backdrop: bool,
pub dragging: bool,
pub drag_over: bool,
}
impl fmt::Debug for StyledNodeState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut v = Vec::new();
if self.hover {
v.push("hover");
}
if self.active {
v.push("active");
}
if self.focused {
v.push("focused");
}
if self.disabled {
v.push("disabled");
}
if self.checked {
v.push("checked");
}
if self.focus_within {
v.push("focus_within");
}
if self.visited {
v.push("visited");
}
if self.backdrop {
v.push("backdrop");
}
if self.dragging {
v.push("dragging");
}
if self.drag_over {
v.push("drag_over");
}
if v.is_empty() {
v.push("normal");
}
write!(f, "{v:?}")
}
}
impl StyledNodeState {
#[must_use] pub const fn new() -> Self {
Self {
hover: false,
active: false,
focused: false,
disabled: false,
checked: false,
focus_within: false,
visited: false,
backdrop: false,
dragging: false,
drag_over: false,
}
}
#[must_use] pub const fn has_state(&self, state_type: u8) -> bool {
match state_type {
0 => true, 1 => self.hover,
2 => self.active,
3 => self.focused,
4 => self.disabled,
5 => self.checked,
6 => self.focus_within,
7 => self.visited,
8 => self.backdrop,
9 => self.dragging,
10 => self.drag_over,
_ => false,
}
}
#[must_use] pub const fn is_normal(&self) -> bool {
!self.hover
&& !self.active
&& !self.focused
&& !self.disabled
&& !self.checked
&& !self.focus_within
&& !self.visited
&& !self.backdrop
&& !self.dragging
&& !self.drag_over
}
#[must_use] pub const fn from_pseudo_state_flags(flags: &azul_css::dynamic_selector::PseudoStateFlags) -> Self {
Self {
hover: flags.hover,
active: flags.active,
focused: flags.focused,
disabled: flags.disabled,
checked: flags.checked,
focus_within: flags.focus_within,
visited: flags.visited,
backdrop: flags.backdrop,
dragging: flags.dragging,
drag_over: flags.drag_over,
}
}
}
#[allow(missing_copy_implementations)]
#[repr(C)]
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd)]
pub struct StyledNode {
pub styled_node_state: StyledNodeState,
}
impl_option!(
StyledNode,
OptionStyledNode,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd]
);
impl_vec!(StyledNode, StyledNodeVec, StyledNodeVecDestructor, StyledNodeVecDestructorType, StyledNodeVecSlice, OptionStyledNode);
impl_vec_mut!(StyledNode, StyledNodeVec);
impl_vec_debug!(StyledNode, StyledNodeVec);
impl_vec_partialord!(StyledNode, StyledNodeVec);
impl_vec_clone!(StyledNode, StyledNodeVec, StyledNodeVecDestructor);
impl_vec_partialeq!(StyledNode, StyledNodeVec);
impl StyledNodeVec {
#[must_use] pub fn as_container(&self) -> NodeDataContainerRef<'_, StyledNode> {
NodeDataContainerRef {
internal: self.as_ref(),
}
}
pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, StyledNode> {
NodeDataContainerRefMut {
internal: self.as_mut(),
}
}
}
#[test]
#[allow(clippy::used_underscore_binding)] fn test_css_styling_with_nested_divs() {
let s = "
html, body, p {
margin: 0;
padding: 0;
}
#div1 {
border: solid black;
height: 2in;
position: absolute;
top: 1in;
width: 3in;
}
div div {
background: blue;
height: 1in;
position: fixed;
width: 1in;
}
";
let css = azul_css::parser2::new_from_str(s);
let mut _styled_dom = Dom::create_body()
.with_children(
vec![Dom::create_div()
.with_ids_and_classes(
vec![crate::dom::IdOrClass::Id("div1".to_string().into())].into(),
)
.with_children(vec![Dom::create_div()].into())]
.into(),
);
_styled_dom.add_component_css(css.0);
}
#[test]
fn test_recompute_preserves_hot_flag_has_background() {
use azul_css::compact_cache::HOT_FLAG_HAS_BACKGROUND;
let css_str = "
body { margin: 0; padding: 0; }
.painted { background: red; width: 100px; height: 100px; }
";
let css = azul_css::parser2::new_from_str(css_str).0;
let mut dom = Dom::create_body().with_children(
vec![Dom::create_div().with_class("painted".to_string().into())].into(),
);
let mut styled = StyledDom::create(&mut dom, css);
let any_bg_frame1 = {
let cache = styled
.css_property_cache
.ptr
.compact_cache
.as_ref()
.expect("compact_cache populated by create_from_compact_dom");
(0..styled.node_hierarchy.as_ref().len())
.any(|i| cache.tier2_cold[i].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0)
};
assert!(
any_bg_frame1,
"frame 1: expected HOT_FLAG_HAS_BACKGROUND on the .painted node",
);
styled.recompute_inheritance_and_compact_cache();
let any_bg_frame2 = {
let cache = styled
.css_property_cache
.ptr
.compact_cache
.as_ref()
.expect("compact_cache rebuilt by recompute_inheritance_and_compact_cache");
(0..styled.node_hierarchy.as_ref().len())
.any(|i| cache.tier2_cold[i].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0)
};
assert!(
any_bg_frame2,
"frame ≥2 after recompute_inheritance_and_compact_cache: \
HOT_FLAG_HAS_BACKGROUND disappeared. The recompute path must \
use build_compact_cache_with_inheritance (not plain \
build_compact_cache) so apply_css_property_to_compact runs and \
populates hot_flags for the renderer's negative fast-paths.",
);
}
#[derive(Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
pub struct StyleFontFamilyHash(pub u64);
impl ::core::fmt::Debug for StyleFontFamilyHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "StyleFontFamilyHash({})", self.0)
}
}
impl StyleFontFamilyHash {
#[must_use] pub fn new(family: &StyleFontFamily) -> Self {
use core::hash::Hasher;
let mut hasher = crate::hash::DefaultHasher::new();
family.hash(&mut hasher);
Self(hasher.finish())
}
}
#[derive(Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
pub struct StyleFontFamiliesHash(pub u64);
impl ::core::fmt::Debug for StyleFontFamiliesHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "StyleFontFamiliesHash({})", self.0)
}
}
impl StyleFontFamiliesHash {
#[must_use] pub fn new(families: &[StyleFontFamily]) -> Self {
use core::hash::Hasher;
let mut hasher = crate::hash::DefaultHasher::new();
families.len().hash(&mut hasher);
for f in families {
f.hash(&mut hasher);
}
Self(hasher.finish())
}
}
#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
#[repr(C)]
pub struct NodeHierarchyItemId {
inner: usize,
}
impl fmt::Debug for NodeHierarchyItemId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.into_crate_internal() {
Some(n) => write!(f, "Some(NodeId({n}))"),
None => write!(f, "None"),
}
}
}
impl fmt::Display for NodeHierarchyItemId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl NodeHierarchyItemId {
pub const NONE: Self = Self { inner: 0 };
#[inline]
#[must_use] pub const fn from_raw(value: usize) -> Self {
Self { inner: value }
}
#[inline]
#[must_use] pub const fn into_raw(&self) -> usize {
self.inner
}
}
impl_option!(
NodeHierarchyItemId,
OptionNodeHierarchyItemId,
[Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl_vec!(NodeHierarchyItemId, NodeHierarchyItemIdVec, NodeHierarchyItemIdVecDestructor, NodeHierarchyItemIdVecDestructorType, NodeHierarchyItemIdVecSlice, OptionNodeHierarchyItemId);
impl_vec_mut!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
impl_vec_debug!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
impl_vec_ord!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
impl_vec_eq!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
impl_vec_hash!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
impl_vec_partialord!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
impl_vec_clone!(NodeHierarchyItemId, NodeHierarchyItemIdVec, NodeHierarchyItemIdVecDestructor);
impl_vec_partialeq!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
impl NodeHierarchyItemId {
#[inline]
#[must_use] pub const fn into_crate_internal(&self) -> Option<NodeId> {
NodeId::from_usize(self.inner)
}
#[inline]
#[must_use] pub const fn from_crate_internal(t: Option<NodeId>) -> Self {
Self {
inner: NodeId::into_raw(&t),
}
}
}
impl From<Option<NodeId>> for NodeHierarchyItemId {
#[inline]
fn from(opt: Option<NodeId>) -> Self {
Self::from_crate_internal(opt)
}
}
impl From<NodeHierarchyItemId> for Option<NodeId> {
#[inline]
fn from(id: NodeHierarchyItemId) -> Self {
id.into_crate_internal()
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
#[repr(C)]
pub struct NodeHierarchyItem {
pub parent: usize,
pub previous_sibling: usize,
pub next_sibling: usize,
pub last_child: usize,
}
impl_option!(
NodeHierarchyItem,
OptionNodeHierarchyItem,
[Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash]
);
impl NodeHierarchyItem {
#[must_use] pub const fn zeroed() -> Self {
Self {
parent: 0,
previous_sibling: 0,
next_sibling: 0,
last_child: 0,
}
}
}
impl From<Node> for NodeHierarchyItem {
fn from(node: Node) -> Self {
Self {
parent: NodeId::into_raw(&node.parent),
previous_sibling: NodeId::into_raw(&node.previous_sibling),
next_sibling: NodeId::into_raw(&node.next_sibling),
last_child: NodeId::into_raw(&node.last_child),
}
}
}
impl NodeHierarchyItem {
#[must_use] pub const fn parent_id(&self) -> Option<NodeId> {
NodeId::from_usize(self.parent)
}
#[must_use] pub const fn previous_sibling_id(&self) -> Option<NodeId> {
NodeId::from_usize(self.previous_sibling)
}
#[must_use] pub const fn next_sibling_id(&self) -> Option<NodeId> {
NodeId::from_usize(self.next_sibling)
}
#[must_use] pub fn first_child_id(&self, current_node_id: NodeId) -> Option<NodeId> {
self.last_child_id().map(|_| current_node_id + 1)
}
#[must_use] pub const fn last_child_id(&self) -> Option<NodeId> {
NodeId::from_usize(self.last_child)
}
}
impl_vec!(NodeHierarchyItem, NodeHierarchyItemVec, NodeHierarchyItemVecDestructor, NodeHierarchyItemVecDestructorType, NodeHierarchyItemVecSlice, OptionNodeHierarchyItem);
impl_vec_mut!(NodeHierarchyItem, NodeHierarchyItemVec);
impl_vec_debug!(AzNode, NodeHierarchyItemVec);
impl_vec_partialord!(AzNode, NodeHierarchyItemVec);
impl_vec_clone!(
NodeHierarchyItem,
NodeHierarchyItemVec,
NodeHierarchyItemVecDestructor
);
impl_vec_partialeq!(AzNode, NodeHierarchyItemVec);
impl NodeHierarchyItemVec {
#[must_use] pub fn as_container(&self) -> NodeDataContainerRef<'_, NodeHierarchyItem> {
NodeDataContainerRef {
internal: self.as_ref(),
}
}
pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, NodeHierarchyItem> {
NodeDataContainerRefMut {
internal: self.as_mut(),
}
}
}
impl NodeDataContainerRef<'_, NodeHierarchyItem> {
#[inline]
#[must_use] pub fn subtree_len(&self, parent_id: NodeId) -> usize {
let self_item_index = parent_id.index();
let next_item_index = self[parent_id].next_sibling_id().map_or_else(|| self.len(), |s| s.index());
next_item_index.saturating_sub(self_item_index).saturating_sub(1)
}
}
#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
#[repr(C)]
pub struct ParentWithNodeDepth {
pub depth: usize,
pub node_id: NodeHierarchyItemId,
}
impl_option!(
ParentWithNodeDepth,
OptionParentWithNodeDepth,
[Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash]
);
impl fmt::Debug for ParentWithNodeDepth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{{ depth: {}, node: {:?} }}",
self.depth,
self.node_id.into_crate_internal()
)
}
}
impl_vec!(ParentWithNodeDepth, ParentWithNodeDepthVec, ParentWithNodeDepthVecDestructor, ParentWithNodeDepthVecDestructorType, ParentWithNodeDepthVecSlice, OptionParentWithNodeDepth);
impl_vec_mut!(ParentWithNodeDepth, ParentWithNodeDepthVec);
impl_vec_debug!(ParentWithNodeDepth, ParentWithNodeDepthVec);
impl_vec_partialord!(ParentWithNodeDepth, ParentWithNodeDepthVec);
impl_vec_clone!(
ParentWithNodeDepth,
ParentWithNodeDepthVec,
ParentWithNodeDepthVecDestructor
);
impl_vec_partialeq!(ParentWithNodeDepth, ParentWithNodeDepthVec);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
#[repr(C)]
pub struct TagIdToNodeIdMapping {
pub tag_id: TagId,
pub node_id: NodeHierarchyItemId,
pub tab_index: OptionTabIndex,
}
impl_option!(
TagIdToNodeIdMapping,
OptionTagIdToNodeIdMapping,
copy = false,
[Debug, Clone, PartialEq, Eq, Ord, PartialOrd]
);
impl_vec!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec, TagIdToNodeIdMappingVecDestructor, TagIdToNodeIdMappingVecDestructorType, TagIdToNodeIdMappingVecSlice, OptionTagIdToNodeIdMapping);
impl_vec_mut!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
impl_vec_debug!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
impl_vec_partialord!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
impl_vec_clone!(
TagIdToNodeIdMapping,
TagIdToNodeIdMappingVec,
TagIdToNodeIdMappingVecDestructor
);
impl_vec_partialeq!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
#[derive(Debug, Clone, PartialEq, PartialOrd)]
#[repr(C)]
pub struct ContentGroup {
pub root: NodeHierarchyItemId,
pub children: ContentGroupVec,
}
impl_option!(
ContentGroup,
OptionContentGroup,
copy = false,
[Debug, Clone, PartialEq, PartialOrd]
);
impl_vec!(ContentGroup, ContentGroupVec, ContentGroupVecDestructor, ContentGroupVecDestructorType, ContentGroupVecSlice, OptionContentGroup);
impl_vec_mut!(ContentGroup, ContentGroupVec);
impl_vec_debug!(ContentGroup, ContentGroupVec);
impl_vec_partialord!(ContentGroup, ContentGroupVec);
impl_vec_clone!(ContentGroup, ContentGroupVec, ContentGroupVecDestructor);
impl_vec_partialeq!(ContentGroup, ContentGroupVec);
#[derive(Debug, PartialEq, Clone)]
#[repr(C)]
pub struct StyledDom {
pub root: NodeHierarchyItemId,
pub node_hierarchy: NodeHierarchyItemVec,
pub node_data: NodeDataVec,
pub styled_nodes: StyledNodeVec,
pub cascade_info: CascadeInfoVec,
pub nodes_with_window_callbacks: NodeHierarchyItemIdVec,
pub nodes_with_datasets: NodeHierarchyItemIdVec,
pub tag_ids_to_node_ids: TagIdToNodeIdMappingVec,
pub non_leaf_nodes: ParentWithNodeDepthVec,
pub css_property_cache: CssPropertyCachePtr,
pub dom_id: DomId,
}
impl_option!(
StyledDom,
OptionStyledDom,
copy = false,
[Debug, Clone, PartialEq]
);
impl Default for StyledDom {
fn default() -> Self {
let root_node: NodeHierarchyItem = Node::ROOT.into();
let root_node_id: NodeHierarchyItemId =
NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO));
Self {
root: root_node_id,
node_hierarchy: vec![root_node].into(),
node_data: vec![NodeData::create_body()].into(),
styled_nodes: vec![StyledNode::default()].into(),
cascade_info: vec![CascadeInfo {
index_in_parent: 0,
is_last_child: true,
}]
.into(),
tag_ids_to_node_ids: Vec::new().into(),
non_leaf_nodes: vec![ParentWithNodeDepth {
depth: 0,
node_id: root_node_id,
}]
.into(),
nodes_with_window_callbacks: Vec::new().into(),
nodes_with_datasets: Vec::new().into(),
css_property_cache: CssPropertyCachePtr::new(CssPropertyCache::empty(1)),
dom_id: DomId::ROOT_ID,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct StyledDomMemoryReport {
pub node_count: usize,
pub node_hierarchy_bytes: usize,
pub node_data_bytes: usize,
pub styled_nodes_bytes: usize,
pub cascade_info_bytes: usize,
pub tag_ids_bytes: usize,
pub non_leaf_nodes_bytes: usize,
pub callback_vecs_bytes: usize,
pub css_property_cache: crate::prop_cache::CssPropertyCacheBreakdown,
}
impl StyledDomMemoryReport {
#[must_use] pub const fn total_bytes(&self) -> usize {
self.node_hierarchy_bytes
+ self.node_data_bytes
+ self.styled_nodes_bytes
+ self.cascade_info_bytes
+ self.tag_ids_bytes
+ self.non_leaf_nodes_bytes
+ self.callback_vecs_bytes
+ self.css_property_cache.total_bytes()
}
}
impl StyledDom {
#[must_use] pub fn memory_report(&self) -> StyledDomMemoryReport {
let n = self.node_data.len();
StyledDomMemoryReport {
node_count: n,
node_hierarchy_bytes: size_of_val(self.node_hierarchy.as_ref()),
node_data_bytes: {
let base = n * size_of::<NodeData>();
let mut inner = 0usize;
for nd in self.node_data.as_ref() {
inner += nd.get_callbacks().len() * 64; inner += nd.style.rules.as_ref().len() * 64;
}
base + inner
},
styled_nodes_bytes: n * size_of::<StyledNode>(),
cascade_info_bytes: n * size_of::<CascadeInfo>(),
tag_ids_bytes: size_of_val(self.tag_ids_to_node_ids.as_ref()),
non_leaf_nodes_bytes: size_of_val(self.non_leaf_nodes.as_ref()),
callback_vecs_bytes:
self.nodes_with_window_callbacks.as_ref().len() * 8
+ self.nodes_with_datasets.as_ref().len() * 8,
css_property_cache: self.css_property_cache.ptr.memory_breakdown(),
}
}
pub fn create(dom: &mut Dom, css: Css) -> Self {
use core::mem;
let mut swap_dom = Dom::create_body();
mem::swap(dom, &mut swap_dom);
let compact_dom: CompactDom = swap_dom.into();
let node_hierarchy: NodeHierarchyItemVec = compact_dom
.node_hierarchy
.as_ref()
.internal
.iter()
.map(|i| (*i).into())
.collect::<Vec<NodeHierarchyItem>>()
.into();
Self::create_from_compact_dom(compact_dom, css, node_hierarchy)
}
#[must_use] pub fn create_from_fast_dom(fast_dom: crate::dom::FastDom) -> Self {
use azul_css::css::Css;
let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
let css_entries = fast_dom.css.into_library_owned_vec();
{
let hierarchy = fast_dom.node_hierarchy.as_container();
for css_with_id in css_entries {
let owner = css_with_id.node_id;
let end = if owner < hierarchy.len() {
owner + hierarchy.subtree_len(NodeId::new(owner))
} else {
owner
};
for mut rule in css_with_id.css.rules.into_library_owned_vec() {
rule.path.push_front_scope(owner, end);
combined_rules.push(rule);
}
}
}
let combined_css = if combined_rules.is_empty() {
Css::empty()
} else {
Css::new(combined_rules)
};
let node_hierarchy_items = fast_dom.node_hierarchy;
let nodes: Vec<Node> = node_hierarchy_items.as_ref()
.iter()
.map(|item| Node {
parent: NodeId::from_usize(item.parent),
previous_sibling: NodeId::from_usize(item.previous_sibling),
next_sibling: NodeId::from_usize(item.next_sibling),
last_child: NodeId::from_usize(item.last_child),
})
.collect();
let node_hierarchy_internal = NodeHierarchy { internal: nodes };
let node_data_vec = fast_dom.node_data.into_library_owned_vec();
let compact_dom = CompactDom {
node_hierarchy: node_hierarchy_internal,
node_data: NodeDataContainer { internal: node_data_vec },
root: NodeId::ZERO,
};
Self::create_from_compact_dom(compact_dom, combined_css, node_hierarchy_items)
}
#[allow(clippy::similar_names)] #[allow(clippy::too_many_lines)] fn create_from_compact_dom(
compact_dom: CompactDom,
mut css: Css,
node_hierarchy: NodeHierarchyItemVec,
) -> Self {
use crate::dom::EventFilter;
static CASCADE_BREAKDOWN: crate::sync::OnceLock<bool> = crate::sync::OnceLock::new();
let cascade_dbg = *CASCADE_BREAKDOWN.get_or_init(crate::profile::memory_enabled);
let node_count = compact_dom.len();
let non_leaf_nodes = compact_dom
.node_hierarchy
.as_ref()
.get_parents_sorted_by_depth();
let mut styled_nodes = vec![
StyledNode {
styled_node_state: StyledNodeState::new()
};
node_count
];
let mut css_property_cache = CssPropertyCache::empty(compact_dom.node_data.len());
let html_tree = construct_html_cascade_tree(
&compact_dom.node_hierarchy.as_ref(),
&non_leaf_nodes[..],
&compact_dom.node_data.as_ref(),
);
let non_leaf_nodes = non_leaf_nodes
.iter()
.map(|(depth, node_id)| ParentWithNodeDepth {
depth: *depth,
node_id: NodeHierarchyItemId::from_crate_internal(Some(*node_id)),
})
.collect::<Vec<_>>();
let non_leaf_nodes: ParentWithNodeDepthVec = non_leaf_nodes.into();
let _restyle_tag_ids = css_property_cache.restyle(
&mut css,
&compact_dom.node_data.as_ref(),
&node_hierarchy,
&non_leaf_nodes,
&html_tree.as_ref(),
);
drop(css);
css_property_cache.apply_ua_css(compact_dom.node_data.as_ref().internal);
css_property_cache.compute_inherited_values(
node_hierarchy.as_container().internal,
compact_dom.node_data.as_ref().internal,
);
let prev_font_hashes: Vec<u64> = css_property_cache.compact_cache
.as_ref()
.map(|c| c.prev_font_hashes.clone())
.unwrap_or_default();
let compact = css_property_cache.build_compact_cache_with_inheritance(
compact_dom.node_data.as_ref().internal,
node_hierarchy.as_container().internal,
&prev_font_hashes,
);
css_property_cache.compact_cache = Some(compact);
let pre_prune = if cascade_dbg {
Some(css_property_cache.memory_breakdown())
} else { None };
css_property_cache.prune_compact_normal_props();
if let Some(pre) = pre_prune {
let post = css_property_cache.memory_breakdown();
#[cfg(feature = "std")]
eprintln!("[PRUNE] css_props {} → {} KiB cascaded {} → {} KiB (saved {} KiB)",
pre.css_props_bytes / 1024, post.css_props_bytes / 1024,
pre.cascaded_props_bytes / 1024, post.cascaded_props_bytes / 1024,
(pre.total_bytes().saturating_sub(post.total_bytes())) / 1024);
#[cfg(not(feature = "std"))]
let _ = post;
}
let tag_ids = css_property_cache.generate_tag_ids(
&compact_dom.node_data.as_ref(),
&node_hierarchy,
);
if cascade_dbg {
let bd = css_property_cache.memory_breakdown();
#[cfg(feature = "std")]
eprintln!("[CASCADE] {} nodes cascaded_props={} KiB css_props={} KiB compact={} KiB computed={} KiB total={} KiB",
node_count,
bd.cascaded_props_bytes / 1024, bd.css_props_bytes / 1024,
bd.compact_cache_bytes / 1024, bd.computed_values_bytes / 1024,
bd.total_bytes() / 1024);
#[cfg(not(feature = "std"))]
let _ = bd;
}
let has_any_callbacks = compact_dom.node_data.as_ref().internal.iter()
.any(|c| !c.get_callbacks().is_empty() || c.get_dataset().is_some());
let (nodes_with_window_callbacks, nodes_with_datasets) = if has_any_callbacks {
let mut win_cbs = Vec::new();
let mut datasets = Vec::new();
for (node_id, c) in compact_dom.node_data.as_ref().internal.iter().enumerate() {
let cbs = c.get_callbacks();
let has_dataset = c.get_dataset().is_some();
if !cbs.is_empty() || has_dataset {
datasets.push(NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_id))));
}
for cb in cbs {
if let EventFilter::Window(_) = cb.event {
win_cbs.push(NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_id))));
break;
}
}
}
(win_cbs, datasets)
} else {
(Vec::new(), Vec::new())
};
let mut styled_dom = Self {
root: NodeHierarchyItemId::from_crate_internal(Some(compact_dom.root)),
node_hierarchy,
node_data: compact_dom.node_data.internal.into(),
cascade_info: html_tree.internal.into(),
styled_nodes: styled_nodes.into(),
tag_ids_to_node_ids: tag_ids.into(),
nodes_with_window_callbacks: nodes_with_window_callbacks.into(),
nodes_with_datasets: nodes_with_datasets.into(),
non_leaf_nodes,
css_property_cache: CssPropertyCachePtr::new(css_property_cache),
dom_id: DomId::ROOT_ID,
};
#[cfg(feature = "table_layout")]
if let Err(_e) = crate::dom_table::generate_anonymous_table_elements(&mut styled_dom) {
}
styled_dom
}
#[must_use] pub fn create_from_dom(mut dom: Dom) -> Self {
use azul_css::css::Css;
dom.fixup_children_estimated();
let mut next_scope_id = 0usize;
scope_inline_css(&mut dom, &mut next_scope_id);
let mut all_css = Vec::new();
collect_css_from_dom(&dom, &mut all_css);
let mut combined_css = if all_css.is_empty() {
Css::empty()
} else {
let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
for css in all_css {
combined_rules.extend(css.rules.into_library_owned_vec());
}
Css::new(combined_rules)
};
strip_css_from_dom(&mut dom);
Self::create(&mut dom, combined_css)
}
pub fn append_child(&mut self, other: Self) {
let self_root_id = self.root.into_crate_internal().unwrap_or(NodeId::ZERO);
let current_root_children_count = self_root_id
.az_children(&self.node_hierarchy.as_container())
.count();
self.append_child_with_index(other, current_root_children_count);
self.finalize_non_leaf_nodes();
}
pub fn append_child_with_index(&mut self, mut other: Self, child_index: usize) {
let self_len = self.node_hierarchy.as_ref().len();
let other_len = other.node_hierarchy.as_ref().len();
let self_root_id = self.root.into_crate_internal().unwrap_or(NodeId::ZERO);
let other_root_id = other.root.into_crate_internal().unwrap_or(NodeId::ZERO);
other.cascade_info.as_mut()[other_root_id.index()].index_in_parent =
u32::try_from(child_index).unwrap_or(u32::MAX);
other.cascade_info.as_mut()[other_root_id.index()].is_last_child = true;
self.cascade_info.append(&mut other.cascade_info);
for other in other.node_hierarchy.as_mut().iter_mut() {
if other.parent != 0 {
other.parent += self_len;
}
if other.previous_sibling != 0 {
other.previous_sibling += self_len;
}
if other.next_sibling != 0 {
other.next_sibling += self_len;
}
if other.last_child != 0 {
other.last_child += self_len;
}
}
other.node_hierarchy.as_container_mut()[other_root_id].parent =
NodeId::into_raw(&Some(self_root_id));
let current_last_child = self.node_hierarchy.as_container()[self_root_id].last_child_id();
other.node_hierarchy.as_container_mut()[other_root_id].previous_sibling =
NodeId::into_raw(¤t_last_child);
if let Some(current_last) = current_last_child {
if self.node_hierarchy.as_container_mut()[current_last]
.next_sibling_id()
.is_some()
{
self.node_hierarchy.as_container_mut()[current_last].next_sibling +=
other_root_id.index() + other_len;
} else {
self.node_hierarchy.as_container_mut()[current_last].next_sibling =
NodeId::into_raw(&Some(NodeId::new(self_len + other_root_id.index())));
}
}
self.node_hierarchy.as_container_mut()[self_root_id].last_child =
NodeId::into_raw(&Some(NodeId::new(self_len + other_root_id.index())));
self.node_hierarchy.append(&mut other.node_hierarchy);
self.node_data.append(&mut other.node_data);
self.styled_nodes.append(&mut other.styled_nodes);
self.get_css_property_cache_mut()
.append(other.get_css_property_cache_mut());
for tag_id_node_id in &mut other.tag_ids_to_node_ids {
tag_id_node_id.node_id.inner += self_len;
}
self.tag_ids_to_node_ids
.append(&mut other.tag_ids_to_node_ids);
for nid in &mut other.nodes_with_window_callbacks {
nid.inner += self_len;
}
self.nodes_with_window_callbacks
.append(&mut other.nodes_with_window_callbacks);
for nid in &mut other.nodes_with_datasets {
nid.inner += self_len;
}
self.nodes_with_datasets
.append(&mut other.nodes_with_datasets);
if other_len != 1 {
for other_non_leaf_node in &mut other.non_leaf_nodes {
other_non_leaf_node.node_id.inner += self_len;
other_non_leaf_node.depth += 1;
}
self.non_leaf_nodes.append(&mut other.non_leaf_nodes);
}
}
pub fn finalize_non_leaf_nodes(&mut self) {
self.non_leaf_nodes.sort_by(|a, b| a.depth.cmp(&b.depth));
}
#[must_use] pub fn with_child(mut self, other: Self) -> Self {
self.append_child(other);
self
}
pub fn set_context_menu(&mut self, context_menu: Menu) {
if let Some(root_id) = self.root.into_crate_internal() {
self.node_data.as_container_mut()[root_id].set_context_menu(context_menu);
}
}
#[must_use] pub fn with_context_menu(mut self, context_menu: Menu) -> Self {
self.set_context_menu(context_menu);
self
}
pub fn set_menu_bar(&mut self, menu_bar: Menu) {
if let Some(root_id) = self.root.into_crate_internal() {
self.node_data.as_container_mut()[root_id].set_menu_bar(menu_bar);
}
}
#[must_use] pub fn with_menu_bar(mut self, menu_bar: Menu) -> Self {
self.set_menu_bar(menu_bar);
self
}
pub fn recompute_inheritance_and_compact_cache(&mut self) {
let prev_font_hashes: Vec<u64> = self.css_property_cache
.downcast_mut()
.compact_cache
.as_ref()
.map(|c| c.prev_font_hashes.clone())
.unwrap_or_default();
let compact = self.css_property_cache
.downcast_mut()
.build_compact_cache_with_inheritance(
self.node_data.as_container().internal,
self.node_hierarchy.as_container().internal,
&prev_font_hashes,
);
self.css_property_cache.downcast_mut().compact_cache = Some(compact);
}
pub fn restyle(&mut self, mut css: Css) {
let _stale_tag_ids = self.css_property_cache.downcast_mut().restyle(
&mut css,
&self.node_data.as_container(),
&self.node_hierarchy,
&self.non_leaf_nodes,
&self.cascade_info.as_container(),
);
self.css_property_cache
.downcast_mut()
.apply_ua_css(self.node_data.as_container().internal);
self.css_property_cache
.downcast_mut()
.compute_inherited_values(
self.node_hierarchy.as_container().internal,
self.node_data.as_container().internal,
);
let prev_font_hashes: Vec<u64> = self
.css_property_cache
.downcast_mut()
.compact_cache
.as_ref()
.map(|c| c.prev_font_hashes.clone())
.unwrap_or_default();
self.css_property_cache.downcast_mut().compact_cache = None;
let compact = self
.css_property_cache
.downcast_mut()
.build_compact_cache_with_inheritance(
self.node_data.as_container().internal,
self.node_hierarchy.as_container().internal,
&prev_font_hashes,
);
self.css_property_cache.downcast_mut().compact_cache = Some(compact);
self.css_property_cache
.downcast_mut()
.invalidate_resolved_font_sizes();
let new_tag_ids = self.css_property_cache.downcast_mut().generate_tag_ids(
&self.node_data.as_container(),
&self.node_hierarchy,
);
self.tag_ids_to_node_ids = new_tag_ids.into();
}
#[inline]
#[must_use] pub const fn node_count(&self) -> usize {
self.node_data.len()
}
#[inline]
#[must_use] pub fn get_css_property_cache(&self) -> &CssPropertyCache {
&self.css_property_cache.ptr
}
#[inline]
pub fn get_css_property_cache_mut(&mut self) -> &mut CssPropertyCache {
&mut self.css_property_cache.ptr
}
#[inline]
#[must_use] pub fn get_styled_node_state(&self, node_id: &NodeId) -> StyledNodeState {
self.styled_nodes.as_container()[*node_id]
.styled_node_state
}
#[must_use]
pub fn restyle_nodes_hover(
&mut self,
nodes: &[NodeId],
new_hover_state: bool,
) -> RestyleNodes {
self.restyle_nodes_state(
nodes,
new_hover_state,
|state, val| state.hover = val,
azul_css::dynamic_selector::PseudoStateType::Hover,
)
}
#[must_use]
pub fn restyle_nodes_active(
&mut self,
nodes: &[NodeId],
new_active_state: bool,
) -> RestyleNodes {
self.restyle_nodes_state(
nodes,
new_active_state,
|state, val| state.active = val,
azul_css::dynamic_selector::PseudoStateType::Active,
)
}
#[must_use]
pub fn restyle_nodes_focus(
&mut self,
nodes: &[NodeId],
new_focus_state: bool,
) -> RestyleNodes {
self.restyle_nodes_state(
nodes,
new_focus_state,
|state, val| state.focused = val,
azul_css::dynamic_selector::PseudoStateType::Focus,
)
}
fn restyle_nodes_state(
&mut self,
nodes: &[NodeId],
new_state_value: bool,
set_state: impl Fn(&mut StyledNodeState, bool),
pseudo_state_type: azul_css::dynamic_selector::PseudoStateType,
) -> RestyleNodes {
let node_count = self.node_count();
let nodes: Vec<NodeId> = nodes
.iter()
.copied()
.filter(|nid| nid.index() < node_count)
.collect();
let old_node_states = nodes
.iter()
.map(|nid| {
self.styled_nodes.as_container()[*nid]
.styled_node_state
})
.collect::<Vec<_>>();
for nid in &nodes {
set_state(
&mut self.styled_nodes.as_container_mut()[*nid].styled_node_state,
new_state_value,
);
}
let css_property_cache = self.get_css_property_cache();
let styled_nodes = self.styled_nodes.as_container();
let node_data = self.node_data.as_container();
let v = nodes
.iter()
.zip(old_node_states.iter())
.filter_map(|(node_id, old_node_state)| {
let mut keys_normal: Vec<_> = CssPropertyCache::prop_types_for_state(
css_property_cache.css_props.get_slice(node_id.index()),
pseudo_state_type,
).collect();
let mut keys_inherited: Vec<_> = CssPropertyCache::prop_types_for_state(
css_property_cache.cascaded_props.get_slice(node_id.index()),
pseudo_state_type,
).collect();
let keys_inline: Vec<CssPropertyType> = {
use azul_css::dynamic_selector::DynamicSelector;
node_data[*node_id]
.style
.iter_inline_properties()
.filter_map(|(prop, conds)| {
let matches = conds.as_slice().iter().any(|c| {
matches!(c, DynamicSelector::PseudoState(pst) if *pst == pseudo_state_type)
});
if matches {
Some(prop.get_type())
} else {
None
}
})
.collect()
};
let mut keys_inline_ref: Vec<_> = keys_inline.iter().collect();
keys_normal.append(&mut keys_inherited);
keys_normal.append(&mut keys_inline_ref);
let node_properties_that_could_have_changed = keys_normal;
if node_properties_that_could_have_changed.is_empty() {
return None;
}
let new_node_state = &styled_nodes[*node_id].styled_node_state;
let node_data = &node_data[*node_id];
let changes = node_properties_that_could_have_changed
.into_iter()
.filter_map(|prop| {
let old = css_property_cache.get_property_slow(
node_data,
node_id,
old_node_state,
prop,
);
let new = css_property_cache.get_property_slow(
node_data,
node_id,
new_node_state,
prop,
);
if old == new {
None
} else {
Some(ChangedCssProperty {
previous_state: *old_node_state,
previous_prop: old.map_or_else(|| CssProperty::auto(*prop), Clone::clone),
current_state: *new_node_state,
current_prop: new.map_or_else(|| CssProperty::auto(*prop), Clone::clone),
})
}
})
.collect::<Vec<_>>();
if changes.is_empty() {
None
} else {
Some((*node_id, changes))
}
})
.collect::<Vec<_>>();
v.into_iter().collect()
}
#[must_use]
pub fn restyle_on_state_change(
&mut self,
focus_changes: Option<FocusChange>,
hover_changes: Option<HoverChange>,
active_changes: Option<ActiveChange>,
) -> RestyleResult {
let mut result = RestyleResult {
gpu_only_changes: true,
..RestyleResult::default()
};
let mut process_changes = |changes: RestyleNodes| {
for (node_id, props) in changes {
for change in &props {
let prop_type = change.current_prop.get_type();
let scope = prop_type.relayout_scope( true);
if scope > result.max_relayout_scope {
result.max_relayout_scope = scope;
}
if scope != RelayoutScope::None {
result.needs_layout = true;
result.gpu_only_changes = false;
}
if !prop_type.is_gpu_only_property() {
result.gpu_only_changes = false;
}
result.needs_display_list = true;
}
result.changed_nodes.entry(node_id).or_default().extend(props);
}
};
if let Some(focus) = focus_changes {
if let Some(old) = focus.lost_focus {
let changes = self.restyle_nodes_focus(&[old], false);
process_changes(changes);
}
if let Some(new) = focus.gained_focus {
let changes = self.restyle_nodes_focus(&[new], true);
process_changes(changes);
}
}
if let Some(hover) = hover_changes {
if !hover.left_nodes.is_empty() {
let changes = self.restyle_nodes_hover(&hover.left_nodes, false);
process_changes(changes);
}
if !hover.entered_nodes.is_empty() {
let changes = self.restyle_nodes_hover(&hover.entered_nodes, true);
process_changes(changes);
}
}
if let Some(active) = active_changes {
if !active.deactivated.is_empty() {
let changes = self.restyle_nodes_active(&active.deactivated, false);
process_changes(changes);
}
if !active.activated.is_empty() {
let changes = self.restyle_nodes_active(&active.activated, true);
process_changes(changes);
}
}
if result.changed_nodes.is_empty() {
result.needs_display_list = false;
result.gpu_only_changes = false;
}
if result.needs_layout {
result.needs_display_list = true;
result.gpu_only_changes = false;
}
result
}
#[must_use]
pub fn restyle_user_property(
&mut self,
node_id: &NodeId,
new_properties: &[CssProperty],
) -> RestyleNodes {
let mut map = BTreeMap::default();
if new_properties.is_empty() {
return map;
}
let node_count = self.node_data.as_ref().len();
if node_id.index() >= node_count {
return map;
}
let node_data = self.node_data.as_container();
let node_data = &node_data[*node_id];
let node_states = &self.styled_nodes.as_container();
let old_node_state = &node_states[*node_id].styled_node_state;
let changes: Vec<ChangedCssProperty> = {
let css_property_cache = self.get_css_property_cache();
new_properties
.iter()
.filter_map(|new_prop| {
let old_prop = css_property_cache.get_property_slow(
node_data,
node_id,
old_node_state,
&new_prop.get_type(),
);
let old_prop = old_prop.map_or_else(|| CssProperty::auto(new_prop.get_type()), Clone::clone);
if old_prop == *new_prop {
None
} else {
Some(ChangedCssProperty {
previous_state: *old_node_state,
previous_prop: old_prop,
current_state: *old_node_state,
current_prop: new_prop.clone(),
})
}
})
.collect()
};
let css_property_cache_mut = self.get_css_property_cache_mut();
if css_property_cache_mut.user_overridden_properties.len() < node_count {
css_property_cache_mut
.user_overridden_properties
.resize(node_count, Vec::new());
}
for new_prop in new_properties {
let prop_type = new_prop.get_type();
let vec = &mut css_property_cache_mut
.user_overridden_properties[node_id.index()];
if new_prop.is_initial() {
if let Ok(idx) = vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
vec.remove(idx);
}
} else {
match vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
Ok(idx) => vec[idx].1 = new_prop.clone(),
Err(idx) => vec.insert(idx, (prop_type, new_prop.clone())),
}
}
}
if !changes.is_empty() {
map.insert(*node_id, changes);
}
map
}
#[must_use] pub fn get_html_string(&self, custom_head: &str, custom_body: &str, test_mode: bool) -> String {
let css_property_cache = self.get_css_property_cache();
let mut output = String::new();
let mut should_print_close_tag_after_node: BTreeMap<NodeId, Vec<(NodeId, usize)>> = BTreeMap::new();
let should_print_close_tag_debug = self
.non_leaf_nodes
.iter()
.filter_map(|p| {
let parent_node_id = p.node_id.into_crate_internal()?;
let mut total_last_child = None;
recursive_get_last_child(
parent_node_id,
self.node_hierarchy.as_ref(),
&mut total_last_child,
);
let total_last_child = total_last_child?;
Some((parent_node_id, (total_last_child, p.depth)))
})
.collect::<BTreeMap<_, _>>();
for (parent_id, (last_child, parent_depth)) in should_print_close_tag_debug {
should_print_close_tag_after_node
.entry(last_child)
.or_default()
.push((parent_id, parent_depth));
}
let mut all_node_depths = self
.non_leaf_nodes
.iter()
.filter_map(|p| {
let parent_node_id = p.node_id.into_crate_internal()?;
Some((parent_node_id, p.depth))
})
.collect::<BTreeMap<_, _>>();
for (parent_node_id, parent_depth) in self
.non_leaf_nodes
.iter()
.filter_map(|p| Some((p.node_id.into_crate_internal()?, p.depth)))
{
for child_id in parent_node_id.az_children(&self.node_hierarchy.as_container()) {
all_node_depths.insert(child_id, parent_depth + 1);
}
}
for node_id in self.node_hierarchy.as_container().linear_iter() {
let depth = all_node_depths.get(&node_id).copied().unwrap_or(0);
let node_data = &self.node_data.as_container()[node_id];
let node_state = &self.styled_nodes.as_container()[node_id].styled_node_state;
let tabs = String::from(" ").repeat(depth);
output.push_str("\r\n");
output.push_str(&tabs);
output.push_str(&node_data.debug_print_start(css_property_cache, &node_id, node_state));
if let Some(content) = node_data.get_node_type().format().as_ref() {
output.push_str(content);
}
let node_has_children = self.node_hierarchy.as_container()[node_id]
.first_child_id(node_id)
.is_some();
if !node_has_children {
let node_data = &self.node_data.as_container()[node_id];
output.push_str(&node_data.debug_print_end());
}
if let Some(close_tag_vec) = should_print_close_tag_after_node.get(&node_id) {
let mut close_tag_vec = close_tag_vec.clone();
close_tag_vec.sort_by(|a, b| b.1.cmp(&a.1)); for (close_tag_parent_id, close_tag_depth) in close_tag_vec {
let node_data = &self.node_data.as_container()[close_tag_parent_id];
let tabs = String::from(" ").repeat(close_tag_depth);
output.push_str("\r\n");
output.push_str(&tabs);
output.push_str(&node_data.debug_print_end());
}
}
}
if test_mode {
output
} else {
format!(
"
<html>
<head>
<style>* {{ margin:0px; padding:0px; }}</style>
{custom_head}
</head>
{output}
{custom_body}
</html>
"
)
}
}
#[must_use] pub fn get_rects_in_rendering_order(&self) -> ContentGroup {
Self::determine_rendering_order(
self.non_leaf_nodes.as_ref(),
&self.node_hierarchy.as_container(),
&self.styled_nodes.as_container(),
&self.node_data.as_container(),
self.get_css_property_cache(),
)
}
fn determine_rendering_order(
non_leaf_nodes: &[ParentWithNodeDepth],
node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
styled_nodes: &NodeDataContainerRef<'_, StyledNode>,
node_data_container: &NodeDataContainerRef<'_, NodeData>,
css_property_cache: &CssPropertyCache,
) -> ContentGroup {
let children_sorted = non_leaf_nodes
.iter()
.filter_map(|parent| {
Some((
parent.node_id,
sort_children_by_position(
parent.node_id.into_crate_internal()?,
node_hierarchy,
styled_nodes,
node_data_container,
css_property_cache,
),
))
})
.collect::<Vec<_>>();
let children_sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> =
children_sorted.into_iter().collect();
let mut root_content_group = ContentGroup {
root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
children: Vec::new().into(),
};
fill_content_group_children(&mut root_content_group, &children_sorted);
root_content_group
}
#[must_use] pub fn swap_with_default(&mut self) -> Self {
let mut new = Self::default();
core::mem::swap(self, &mut new);
new
}
}
#[derive(Debug, PartialEq, PartialOrd, Eq)]
pub struct CompactDom {
pub node_hierarchy: NodeHierarchy,
pub node_data: NodeDataContainer<NodeData>,
pub root: NodeId,
}
impl CompactDom {
#[inline]
#[must_use] pub fn len(&self) -> usize {
self.node_hierarchy.as_ref().len()
}
#[inline]
#[must_use] pub fn is_empty(&self) -> bool {
self.node_hierarchy.as_ref().is_empty()
}
}
impl From<Dom> for CompactDom {
fn from(dom: Dom) -> Self {
convert_dom_into_compact_dom(dom)
}
}
#[must_use] pub fn convert_dom_into_compact_dom(mut dom: Dom) -> CompactDom {
fn convert_dom_into_compact_dom_internal(
dom: &mut Dom,
node_hierarchy: &mut [Node],
node_data: &mut Vec<NodeData>,
parent_node_id: NodeId,
node: Node,
cur_node_id: &mut usize,
) {
node_hierarchy[parent_node_id.index()] = node;
let copy = dom.root.copy_special_moving_complex();
node_data[parent_node_id.index()] = copy;
*cur_node_id += 1;
let mut previous_sibling_id = None;
let children_len = dom.children.len();
for (child_index, child_dom) in dom.children.as_mut().iter_mut().enumerate() {
let child_node_id = NodeId::new(*cur_node_id);
let is_last_child = (child_index + 1) == children_len;
let child_dom_is_empty = child_dom.children.is_empty();
let child_node = Node {
parent: Some(parent_node_id),
previous_sibling: previous_sibling_id,
next_sibling: if is_last_child {
None
} else {
Some(child_node_id + child_dom.estimated_total_children + 1)
},
last_child: if child_dom_is_empty {
None
} else {
Some(child_node_id + child_dom.estimated_total_children)
},
};
previous_sibling_id = Some(child_node_id);
convert_dom_into_compact_dom_internal(
child_dom,
node_hierarchy,
node_data,
child_node_id,
child_node,
cur_node_id,
);
}
}
let sum_nodes = dom.fixup_children_estimated();
let mut node_hierarchy = vec![Node::ROOT; sum_nodes + 1];
let mut node_data = vec![NodeData::create_div(); sum_nodes + 1];
let mut cur_node_id = 0;
let root_node_id = NodeId::ZERO;
let root_node = Node {
parent: None,
previous_sibling: None,
next_sibling: None,
last_child: if dom.children.is_empty() {
None
} else {
Some(root_node_id + dom.estimated_total_children)
},
};
convert_dom_into_compact_dom_internal(
&mut dom,
&mut node_hierarchy,
&mut node_data,
root_node_id,
root_node,
&mut cur_node_id,
);
CompactDom {
node_hierarchy: NodeHierarchy {
internal: node_hierarchy,
},
node_data: NodeDataContainer {
internal: node_data,
},
root: root_node_id,
}
}
fn scope_inline_css(dom: &mut Dom, next_id: &mut usize) {
let start = *next_id;
let end = start + dom.estimated_total_children;
for css in dom.css.as_mut().iter_mut() {
for rule in css.rules.as_mut().iter_mut() {
rule.path.push_front_scope(start, end);
}
}
*next_id += 1;
for child in dom.children.as_mut().iter_mut() {
scope_inline_css(child, next_id);
}
}
fn collect_css_from_dom(dom: &Dom, out: &mut Vec<Css>) {
for child in &dom.children {
collect_css_from_dom(child, out);
}
for css in &dom.css {
out.push(css.clone());
}
}
fn strip_css_from_dom(dom: &mut Dom) {
dom.css = Vec::new().into();
for child in dom.children.as_mut().iter_mut() {
strip_css_from_dom(child);
}
}
fn fill_content_group_children(
group: &mut ContentGroup,
children_sorted: &BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>>,
) {
if let Some(c) = children_sorted.get(&group.root) {
group.children = c
.iter()
.map(|child| ContentGroup {
root: *child,
children: Vec::new().into(),
})
.collect::<Vec<ContentGroup>>()
.into();
for c in group.children.as_mut() {
fill_content_group_children(c, children_sorted);
}
}
}
fn sort_children_by_position(
parent: NodeId,
node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
rectangles: &NodeDataContainerRef<'_, StyledNode>,
node_data_container: &NodeDataContainerRef<'_, NodeData>,
css_property_cache: &CssPropertyCache,
) -> Vec<NodeHierarchyItemId> {
use azul_css::props::layout::LayoutPosition::Absolute;
let children_positions = parent
.az_children(node_hierarchy)
.map(|nid| {
let position = css_property_cache
.get_position(
&node_data_container[nid],
&nid,
&rectangles[nid].styled_node_state,
)
.and_then(|p| (*p).get_property_or_default())
.unwrap_or_default();
let id = NodeHierarchyItemId::from_crate_internal(Some(nid));
(id, position)
})
.collect::<Vec<_>>();
let mut not_absolute_children = children_positions
.iter()
.filter_map(|(node_id, position)| {
if *position == Absolute {
None
} else {
Some(*node_id)
}
})
.collect::<Vec<_>>();
let mut absolute_children = children_positions
.iter()
.filter_map(|(node_id, position)| {
if *position == Absolute {
Some(*node_id)
} else {
None
}
})
.collect::<Vec<_>>();
not_absolute_children.append(&mut absolute_children);
not_absolute_children
}
fn recursive_get_last_child(
node_id: NodeId,
node_hierarchy: &[NodeHierarchyItem],
target: &mut Option<NodeId>,
) {
match node_hierarchy[node_id.index()].last_child_id() {
None => (),
Some(s) => {
*target = Some(s);
recursive_get_last_child(s, node_hierarchy, target);
}
}
}
#[must_use] pub fn is_before_in_document_order(
hierarchy: &NodeHierarchyItemVec,
node_a: NodeId,
node_b: NodeId,
) -> bool {
if node_a == node_b {
return false;
}
let hierarchy = hierarchy.as_container();
let path_a = get_path_to_root(&hierarchy, node_a);
let path_b = get_path_to_root(&hierarchy, node_b);
let min_len = path_a.len().min(path_b.len());
for i in 0..min_len {
if path_a[i] != path_b[i] {
let child_towards_a = path_a[i];
let child_towards_b = path_b[i];
return child_towards_a.index() < child_towards_b.index();
}
}
path_a.len() < path_b.len()
}
fn get_path_to_root(
hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
node: NodeId,
) -> Vec<NodeId> {
let mut path = Vec::new();
let mut current = Some(node);
while let Some(node_id) = current {
path.push(node_id);
current = hierarchy.get(node_id).and_then(NodeHierarchyItem::parent_id);
}
path.reverse();
path
}
#[must_use] pub fn collect_nodes_in_document_order(
hierarchy: &NodeHierarchyItemVec,
start_node: NodeId,
end_node: NodeId,
) -> Vec<NodeId> {
if start_node == end_node {
return vec![start_node];
}
let hierarchy_container = hierarchy.as_container();
let hierarchy_slice = hierarchy.as_ref();
let mut result = Vec::new();
let mut in_range = false;
let mut stack: Vec<NodeId> = vec![NodeId::ZERO];
while let Some(current) = stack.pop() {
if current == start_node {
in_range = true;
}
if in_range {
result.push(current);
}
if current == end_node {
break;
}
if let Some(item) = hierarchy_container.get(current) {
if let Some(first_child) = item.first_child_id(current) {
let mut children = Vec::new();
let mut child = Some(first_child);
while let Some(child_id) = child {
children.push(child_id);
child = hierarchy_container.get(child_id).and_then(NodeHierarchyItem::next_sibling_id);
}
for child_id in children.into_iter().rev() {
stack.push(child_id);
}
}
}
}
result
}
#[must_use] pub fn is_layout_equivalent(old: &StyledDom, new: &StyledDom) -> bool {
use crate::dom::NodeType;
use crate::resources::DecodedImage;
let old_nodes = old.node_data.as_ref();
let new_nodes = new.node_data.as_ref();
if old_nodes.len() != new_nodes.len() {
return false;
}
let old_hier = old.node_hierarchy.as_ref();
let new_hier = new.node_hierarchy.as_ref();
if old_hier.len() != new_hier.len() {
return false;
}
if old_hier != new_hier {
return false;
}
for (old_node, new_node) in old_nodes.iter().zip(new_nodes.iter()) {
if core::mem::discriminant(&old_node.node_type)
!= core::mem::discriminant(&new_node.node_type)
{
return false;
}
match (&old_node.node_type, &new_node.node_type) {
(NodeType::Image(old_img), NodeType::Image(new_img)) => {
match (old_img.get_data(), new_img.get_data()) {
(DecodedImage::Callback(old_cb), DecodedImage::Callback(new_cb)) => {
if old_cb.callback.cb != new_cb.callback.cb {
return false;
}
if old_cb.refany.get_type_id() != new_cb.refany.get_type_id() {
return false;
}
}
_ => {
if old_img != new_img {
return false;
}
}
}
}
_ => {
if old_node.node_type != new_node.node_type {
return false;
}
}
}
{
use crate::dom::AttributeType;
let old_ids_classes: Vec<_> = old_node.attributes().as_ref().iter()
.filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
.collect();
let new_ids_classes: Vec<_> = new_node.attributes().as_ref().iter()
.filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
.collect();
if old_ids_classes != new_ids_classes {
return false;
}
}
if old_node.style != new_node.style {
return false;
}
let old_cbs = old_node.callbacks.as_ref();
let new_cbs = new_node.callbacks.as_ref();
if old_cbs.len() != new_cbs.len() {
return false;
}
for (old_cb, new_cb) in old_cbs.iter().zip(new_cbs.iter()) {
if old_cb.event != new_cb.event {
return false;
}
}
if old_node.attributes().as_ref() != new_node.attributes().as_ref() {
return false;
}
}
let old_styled = old.styled_nodes.as_ref();
let new_styled = new.styled_nodes.as_ref();
if old_styled.len() != new_styled.len() {
return false;
}
if old_styled != new_styled {
return false;
}
true
}
#[cfg(test)]
mod audit_tests {
use super::*;
use azul_css::props::basic::StyleFontFamily;
fn fam(name: &str) -> StyleFontFamily {
StyleFontFamily::System(name.to_string().into())
}
#[test]
fn style_font_families_hash_is_length_sensitive() {
let a = StyleFontFamiliesHash::new(&[fam("Arial")]);
let a2 = StyleFontFamiliesHash::new(&[fam("Arial")]);
assert_eq!(a, a2, "hash must be deterministic");
let two = StyleFontFamiliesHash::new(&[fam("Arial"), fam("Helvetica")]);
assert_ne!(a, two, "different-length family lists must not collide");
let empty = StyleFontFamiliesHash::new(&[]);
assert_ne!(empty, a);
assert_ne!(empty, two);
let rev = StyleFontFamiliesHash::new(&[fam("Helvetica"), fam("Arial")]);
assert_ne!(two, rev);
}
}
#[cfg(test)]
#[allow(clippy::too_many_lines)]
mod autotest_generated {
use azul_css::{
dynamic_selector::PseudoStateFlags,
props::basic::StyleFontFamily,
};
use super::*;
const fn raw_item(parent: usize, prev: usize, next: usize, last: usize) -> NodeHierarchyItem {
NodeHierarchyItem {
parent,
previous_sibling: prev,
next_sibling: next,
last_child: last,
}
}
fn flat_body(n: usize) -> StyledDom {
let children: Vec<Dom> = (0..n).map(|_| Dom::create_div()).collect();
let mut dom = Dom::create_body().with_children(children.into());
StyledDom::create(&mut dom, Css::empty())
}
fn nested_body() -> StyledDom {
let mut dom = Dom::create_body().with_children(
vec![Dom::create_div().with_children(vec![Dom::create_div()].into())].into(),
);
StyledDom::create(&mut dom, Css::empty())
}
fn parse_css(s: &str) -> Css {
azul_css::parser2::new_from_str(s).0
}
fn family(name: &str) -> StyleFontFamily {
StyleFontFamily::System(name.to_string().into())
}
const fn pseudo_flags(all: bool) -> PseudoStateFlags {
PseudoStateFlags {
hover: all,
active: all,
focused: all,
disabled: all,
checked: all,
focus_within: all,
visited: all,
backdrop: all,
dragging: all,
drag_over: all,
}
}
fn empty_menu() -> Menu {
let items: Vec<crate::menu::MenuItem> = Vec::new();
Menu::create(items.into())
}
#[test]
fn restyle_result_default_reports_no_changes() {
let r = RestyleResult::default();
assert!(!r.has_changes());
assert!(!r.needs_layout);
assert!(!r.needs_display_list);
assert!(!r.gpu_only_changes);
assert_eq!(r.max_relayout_scope, RelayoutScope::None);
}
#[test]
fn restyle_result_has_changes_keys_off_node_map_not_property_count() {
let mut r = RestyleResult::default();
r.changed_nodes.insert(NodeId::ZERO, Vec::new());
assert!(r.has_changes());
r.changed_nodes.clear();
assert!(!r.has_changes());
}
#[test]
fn restyle_result_merge_ors_layout_flags_and_ands_gpu_only() {
let mut a = RestyleResult {
needs_layout: false,
needs_display_list: false,
gpu_only_changes: true,
..RestyleResult::default()
};
let b = RestyleResult {
needs_layout: true,
needs_display_list: true,
gpu_only_changes: true,
..RestyleResult::default()
};
a.merge(b);
assert!(a.needs_layout, "needs_layout is OR-ed");
assert!(a.needs_display_list, "needs_display_list is OR-ed");
assert!(a.gpu_only_changes, "true && true stays true");
let mut c = RestyleResult {
gpu_only_changes: true,
..RestyleResult::default()
};
c.merge(RestyleResult {
gpu_only_changes: false,
..RestyleResult::default()
});
assert!(!c.gpu_only_changes, "gpu_only_changes is AND-ed");
}
#[test]
fn restyle_result_merge_keeps_the_most_expensive_scope() {
let mut low = RestyleResult {
max_relayout_scope: RelayoutScope::None,
..RestyleResult::default()
};
low.merge(RestyleResult {
max_relayout_scope: RelayoutScope::Full,
..RestyleResult::default()
});
assert_eq!(low.max_relayout_scope, RelayoutScope::Full);
let mut high = RestyleResult {
max_relayout_scope: RelayoutScope::Full,
..RestyleResult::default()
};
high.merge(RestyleResult {
max_relayout_scope: RelayoutScope::IfcOnly,
..RestyleResult::default()
});
assert_eq!(high.max_relayout_scope, RelayoutScope::Full);
}
#[test]
fn restyle_result_merge_of_default_is_not_the_identity_for_gpu_only() {
let mut a = RestyleResult {
gpu_only_changes: true,
..RestyleResult::default()
};
a.merge(RestyleResult::default());
assert!(!a.gpu_only_changes);
assert!(!a.has_changes());
}
#[test]
fn restyle_result_merge_concatenates_changes_for_the_same_node() {
let prop = |t| ChangedCssProperty {
previous_state: StyledNodeState::new(),
previous_prop: CssProperty::auto(t),
current_state: StyledNodeState::new(),
current_prop: CssProperty::initial(t),
};
let mut a = RestyleResult::default();
a.changed_nodes
.insert(NodeId::ZERO, vec![prop(CssPropertyType::Width)]);
let mut b = RestyleResult::default();
b.changed_nodes
.insert(NodeId::ZERO, vec![prop(CssPropertyType::Height)]);
b.changed_nodes
.insert(NodeId::new(1), vec![prop(CssPropertyType::Opacity)]);
a.merge(b);
assert_eq!(a.changed_nodes.len(), 2);
assert_eq!(
a.changed_nodes[&NodeId::ZERO].len(),
2,
"changes for the same node are appended, not replaced"
);
assert_eq!(a.changed_nodes[&NodeId::new(1)].len(), 1);
assert!(a.has_changes());
}
#[test]
fn styled_node_state_new_is_all_false_and_normal() {
let s = StyledNodeState::new();
assert!(s.is_normal());
assert!(!s.hover);
assert!(!s.active);
assert!(!s.focused);
assert!(!s.disabled);
assert!(!s.checked);
assert!(!s.focus_within);
assert!(!s.visited);
assert!(!s.backdrop);
assert!(!s.dragging);
assert!(!s.drag_over);
assert_eq!(s, StyledNodeState::default());
}
#[test]
fn styled_node_state_has_state_zero_is_always_true() {
assert!(StyledNodeState::new().has_state(0));
assert!(StyledNodeState::from_pseudo_state_flags(&pseudo_flags(true)).has_state(0));
}
#[test]
fn styled_node_state_has_state_maps_every_index_exactly_once() {
let setters: [(u8, fn(&mut StyledNodeState)); 10] = [
(1, |s| s.hover = true),
(2, |s| s.active = true),
(3, |s| s.focused = true),
(4, |s| s.disabled = true),
(5, |s| s.checked = true),
(6, |s| s.focus_within = true),
(7, |s| s.visited = true),
(8, |s| s.backdrop = true),
(9, |s| s.dragging = true),
(10, |s| s.drag_over = true),
];
for (expected_idx, set) in setters {
let mut s = StyledNodeState::new();
set(&mut s);
assert!(!s.is_normal(), "state {expected_idx} must not be 'normal'");
for idx in 1..=10u8 {
assert_eq!(
s.has_state(idx),
idx == expected_idx,
"state index {idx} misreported for setter {expected_idx}"
);
}
}
}
#[test]
fn styled_node_state_has_state_is_false_for_every_out_of_range_u8() {
let all_on = StyledNodeState::from_pseudo_state_flags(&pseudo_flags(true));
for idx in 11..=u8::MAX {
assert!(!StyledNodeState::new().has_state(idx));
assert!(
!all_on.has_state(idx),
"unknown state index {idx} must be inactive even when every flag is set"
);
}
}
#[test]
fn styled_node_state_from_pseudo_state_flags_roundtrips_every_field() {
let all_on = StyledNodeState::from_pseudo_state_flags(&pseudo_flags(true));
assert!(!all_on.is_normal());
for idx in 0..=10u8 {
assert!(all_on.has_state(idx), "state {idx} should be active");
}
let all_off = StyledNodeState::from_pseudo_state_flags(&pseudo_flags(false));
assert!(all_off.is_normal());
assert_eq!(all_off, StyledNodeState::new());
}
#[test]
fn styled_node_state_debug_lists_active_states_and_normal_when_empty() {
assert_eq!(format!("{:?}", StyledNodeState::new()), "[\"normal\"]");
let mut s = StyledNodeState::new();
s.hover = true;
s.drag_over = true;
let dbg = format!("{s:?}");
assert!(dbg.contains("hover"), "{dbg}");
assert!(dbg.contains("drag_over"), "{dbg}");
assert!(!dbg.contains("normal"), "{dbg}");
}
#[test]
fn styled_node_vec_empty_container_is_empty_and_get_returns_none() {
let v: StyledNodeVec = Vec::new().into();
let c = v.as_container();
assert_eq!(c.len(), 0);
assert!(c.is_empty());
assert!(c.get(NodeId::ZERO).is_none());
assert!(c.get(NodeId::new(usize::MAX)).is_none());
}
#[test]
fn styled_node_vec_container_mut_writes_are_visible_through_container() {
let mut v: StyledNodeVec = vec![StyledNode::default(), StyledNode::default()].into();
{
let mut c = v.as_container_mut();
c[NodeId::new(1)].styled_node_state.hover = true;
}
let c = v.as_container();
assert_eq!(c.len(), 2);
assert!(!c[NodeId::ZERO].styled_node_state.hover);
assert!(c[NodeId::new(1)].styled_node_state.hover);
assert!(c.get(NodeId::new(2)).is_none());
}
#[test]
fn style_font_family_hash_is_deterministic_and_input_sensitive() {
assert_eq!(
StyleFontFamilyHash::new(&family("Arial")),
StyleFontFamilyHash::new(&family("Arial"))
);
assert_ne!(
StyleFontFamilyHash::new(&family("Arial")),
StyleFontFamilyHash::new(&family("Ariaĺ"))
);
assert_ne!(
StyleFontFamilyHash::new(&StyleFontFamily::System("x".to_string().into())),
StyleFontFamilyHash::new(&StyleFontFamily::File("x".to_string().into()))
);
}
#[test]
fn style_font_family_hash_handles_empty_unicode_and_huge_names() {
let empty = family("");
let unicode = family("🦀 ノート ﷽ عربى");
let huge = family(&"A".repeat(100_000));
assert_eq!(StyleFontFamilyHash::new(&empty), StyleFontFamilyHash::new(&empty));
assert_eq!(
StyleFontFamilyHash::new(&unicode),
StyleFontFamilyHash::new(&unicode)
);
assert_eq!(StyleFontFamilyHash::new(&huge), StyleFontFamilyHash::new(&huge));
assert_ne!(StyleFontFamilyHash::new(&empty), StyleFontFamilyHash::new(&unicode));
assert_ne!(StyleFontFamilyHash::new(&empty), StyleFontFamilyHash::new(&huge));
}
#[test]
fn style_font_families_hash_empty_slice_is_stable_and_distinct() {
let empty = StyleFontFamiliesHash::new(&[]);
assert_eq!(empty, StyleFontFamiliesHash::new(&[]));
assert_ne!(empty, StyleFontFamiliesHash::new(&[family("")]));
}
#[test]
fn style_font_families_hash_scales_to_large_lists_and_is_length_sensitive() {
let big: Vec<StyleFontFamily> = (0..1000).map(|i| family(&format!("font-{i}"))).collect();
let one_shorter = &big[..999];
assert_eq!(
StyleFontFamiliesHash::new(&big),
StyleFontFamiliesHash::new(&big),
"hashing 1000 families must be deterministic"
);
assert_ne!(
StyleFontFamiliesHash::new(&big),
StyleFontFamiliesHash::new(one_shorter),
"the length prefix must separate [0..1000) from [0..999)"
);
}
#[test]
fn node_hierarchy_item_id_none_is_zero() {
assert_eq!(NodeHierarchyItemId::NONE.into_raw(), 0);
assert_eq!(NodeHierarchyItemId::NONE.into_crate_internal(), None);
assert_eq!(NodeHierarchyItemId::from_crate_internal(None).into_raw(), 0);
assert_eq!(NodeHierarchyItemId::from_raw(0).into_crate_internal(), None);
assert_eq!(NodeHierarchyItemId::from_crate_internal(None), NodeHierarchyItemId::NONE);
}
#[test]
fn node_hierarchy_item_id_encode_decode_roundtrip_at_boundaries() {
for idx in [0usize, 1, 2, 1023, usize::MAX / 2, usize::MAX - 1] {
let id = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(idx)));
assert_eq!(id.into_raw(), idx + 1, "1-based encoding for {idx}");
assert_eq!(
id.into_crate_internal(),
Some(NodeId::new(idx)),
"decode(encode(x)) == x for {idx}"
);
}
}
#[test]
fn node_hierarchy_item_id_raw_roundtrip_is_identity_even_at_usize_max() {
for raw in [0usize, 1, 2, 7, u32::MAX as usize, usize::MAX] {
let decoded = NodeHierarchyItemId::from_raw(raw).into_crate_internal();
let reencoded = NodeHierarchyItemId::from_crate_internal(decoded).into_raw();
assert_eq!(reencoded, raw, "encode(decode(raw)) must be identity for {raw}");
}
}
#[test]
fn node_hierarchy_item_id_from_raw_decodes_one_based() {
assert_eq!(
NodeHierarchyItemId::from_raw(1).into_crate_internal(),
Some(NodeId::ZERO),
"raw 1 is NodeId(0), NOT NodeId(1)"
);
assert_eq!(
NodeHierarchyItemId::from_raw(usize::MAX).into_crate_internal(),
Some(NodeId::new(usize::MAX - 1))
);
}
#[test]
fn node_hierarchy_item_id_debug_and_display_agree() {
let none = NodeHierarchyItemId::NONE;
assert_eq!(format!("{none:?}"), "None");
assert_eq!(format!("{none}"), format!("{none:?}"));
let some = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(5)));
assert_eq!(format!("{some:?}"), "Some(NodeId(5))");
assert_eq!(format!("{some}"), format!("{some:?}"));
let max = NodeHierarchyItemId::from_raw(usize::MAX);
assert!(!format!("{max:?}").is_empty());
}
#[test]
fn node_hierarchy_item_id_ordering_follows_raw_value() {
let a = NodeHierarchyItemId::from_raw(0);
let b = NodeHierarchyItemId::from_raw(1);
let c = NodeHierarchyItemId::from_raw(usize::MAX);
assert!(a < b);
assert!(b < c);
assert_eq!(a, NodeHierarchyItemId::NONE);
}
#[test]
fn node_hierarchy_item_id_from_impls_match_the_explicit_ones() {
let opt = Some(NodeId::new(41));
let via_from: NodeHierarchyItemId = opt.into();
assert_eq!(via_from, NodeHierarchyItemId::from_crate_internal(opt));
let back: Option<NodeId> = via_from.into();
assert_eq!(back, opt);
let none: NodeHierarchyItemId = None.into();
assert_eq!(none.into_raw(), 0);
}
#[test]
fn node_hierarchy_item_zeroed_has_no_links() {
let z = NodeHierarchyItem::zeroed();
assert_eq!(z.parent_id(), None);
assert_eq!(z.previous_sibling_id(), None);
assert_eq!(z.next_sibling_id(), None);
assert_eq!(z.last_child_id(), None);
assert_eq!(z.first_child_id(NodeId::ZERO), None);
assert_eq!(z.first_child_id(NodeId::new(usize::MAX)), None);
assert_eq!(z, NodeHierarchyItem::from(Node::ROOT));
}
#[test]
fn node_hierarchy_item_getters_decode_the_one_based_fields() {
let item = raw_item(1, 2, 3, 4);
assert_eq!(item.parent_id(), Some(NodeId::new(0)));
assert_eq!(item.previous_sibling_id(), Some(NodeId::new(1)));
assert_eq!(item.next_sibling_id(), Some(NodeId::new(2)));
assert_eq!(item.last_child_id(), Some(NodeId::new(3)));
assert_eq!(item.first_child_id(NodeId::new(7)), Some(NodeId::new(8)));
}
#[test]
fn node_hierarchy_item_getters_at_usize_max_do_not_overflow() {
let item = raw_item(usize::MAX, usize::MAX, usize::MAX, usize::MAX);
assert_eq!(item.parent_id(), Some(NodeId::new(usize::MAX - 1)));
assert_eq!(item.previous_sibling_id(), Some(NodeId::new(usize::MAX - 1)));
assert_eq!(item.next_sibling_id(), Some(NodeId::new(usize::MAX - 1)));
assert_eq!(item.last_child_id(), Some(NodeId::new(usize::MAX - 1)));
assert_eq!(
item.first_child_id(NodeId::new(usize::MAX)),
Some(NodeId::new(usize::MAX)),
"first_child_id must saturate, never wrap to NodeId(0)"
);
}
#[test]
fn node_hierarchy_item_from_node_preserves_every_link() {
let node = Node {
parent: Some(NodeId::new(3)),
previous_sibling: None,
next_sibling: Some(NodeId::new(9)),
last_child: Some(NodeId::new(12)),
};
let item: NodeHierarchyItem = node.into();
assert_eq!(item.parent_id(), node.parent);
assert_eq!(item.previous_sibling_id(), node.previous_sibling);
assert_eq!(item.next_sibling_id(), node.next_sibling);
assert_eq!(item.last_child_id(), node.last_child);
}
#[test]
fn node_hierarchy_item_vec_containers_read_and_write() {
let mut v: NodeHierarchyItemVec = vec![NodeHierarchyItem::zeroed(); 2].into();
{
let mut c = v.as_container_mut();
c[NodeId::new(1)].parent = 1; }
let c = v.as_container();
assert_eq!(c.len(), 2);
assert_eq!(c[NodeId::new(1)].parent_id(), Some(NodeId::ZERO));
assert!(c.get(NodeId::new(2)).is_none());
let empty: NodeHierarchyItemVec = Vec::new().into();
assert!(empty.as_container().is_empty());
}
#[test]
fn subtree_len_counts_descendants_of_a_real_tree() {
let sd = nested_body();
let h = sd.node_hierarchy.as_container();
assert_eq!(h.len(), 3);
assert_eq!(h.subtree_len(NodeId::ZERO), 2, "root has 2 descendants");
assert_eq!(h.subtree_len(NodeId::new(1)), 1);
assert_eq!(h.subtree_len(NodeId::new(2)), 0, "a leaf has no descendants");
}
#[test]
fn subtree_len_saturates_on_a_malformed_backwards_next_sibling() {
let v: NodeHierarchyItemVec = vec![
raw_item(0, 0, 0, 0),
raw_item(0, 0, 0, 0),
raw_item(0, 0, 1, 0),
]
.into();
let c = v.as_container();
assert_eq!(c.subtree_len(NodeId::new(2)), 0);
let v2: NodeHierarchyItemVec = vec![raw_item(0, 0, 0, 0), raw_item(0, 0, 2, 0)].into();
assert_eq!(v2.as_container().subtree_len(NodeId::new(1)), 0);
}
#[test]
fn memory_report_default_total_is_zero() {
assert_eq!(StyledDomMemoryReport::default().total_bytes(), 0);
}
#[test]
fn memory_report_total_bytes_sums_every_field() {
let r = StyledDomMemoryReport {
node_count: 3,
node_hierarchy_bytes: 1,
node_data_bytes: 2,
styled_nodes_bytes: 4,
cascade_info_bytes: 8,
tag_ids_bytes: 16,
non_leaf_nodes_bytes: 32,
callback_vecs_bytes: 64,
..StyledDomMemoryReport::default()
};
assert_eq!(r.total_bytes(), 127, "node_count must NOT be part of the sum");
let extreme = StyledDomMemoryReport {
node_data_bytes: usize::MAX,
..StyledDomMemoryReport::default()
};
assert_eq!(extreme.total_bytes(), usize::MAX);
}
#[test]
fn memory_report_tracks_node_count_and_is_monotonic_in_dom_size() {
let small = flat_body(1).memory_report();
let large = flat_body(50).memory_report();
assert_eq!(small.node_count, 2);
assert_eq!(large.node_count, 51);
assert!(large.total_bytes() > small.total_bytes());
assert!(small.total_bytes() >= small.node_hierarchy_bytes + small.node_data_bytes);
let d = StyledDom::default().memory_report();
assert_eq!(d.node_count, 1);
assert!(d.total_bytes() > 0);
}
#[test]
fn default_styled_dom_is_a_single_rooted_body() {
let sd = StyledDom::default();
assert_eq!(sd.node_count(), 1);
assert_eq!(sd.root.into_crate_internal(), Some(NodeId::ZERO));
assert_eq!(sd.node_hierarchy.as_ref().len(), 1);
assert_eq!(sd.styled_nodes.as_ref().len(), 1);
assert_eq!(sd.cascade_info.as_ref().len(), 1);
assert_eq!(sd.non_leaf_nodes.as_ref().len(), 1);
assert_eq!(sd.non_leaf_nodes.as_ref()[0].depth, 0);
assert!(sd.tag_ids_to_node_ids.as_ref().is_empty());
assert!(sd.get_styled_node_state(&NodeId::ZERO).is_normal());
}
#[test]
fn create_empties_the_source_dom() {
let mut dom = Dom::create_body().with_children(vec![Dom::create_div(); 3].into());
let sd = StyledDom::create(&mut dom, Css::empty());
assert_eq!(sd.node_count(), 4);
assert!(
dom.children.as_ref().is_empty(),
"the source Dom must be left empty (it is swapped out, not cloned)"
);
}
#[test]
fn create_keeps_every_parallel_array_the_same_length() {
for n in [0usize, 1, 3, 64] {
let sd = flat_body(n);
let count = sd.node_count();
assert_eq!(count, n + 1);
assert_eq!(sd.node_hierarchy.as_ref().len(), count);
assert_eq!(sd.styled_nodes.as_ref().len(), count);
assert_eq!(sd.cascade_info.as_ref().len(), count);
}
}
#[test]
fn create_survives_malformed_truncated_and_unicode_css() {
let cases: Vec<String> = vec![
String::new(),
"}}}{{{".to_string(),
"div {".to_string(),
"div { color: }".to_string(),
"div { : red; }".to_string(),
"@media".to_string(),
"/* unterminated comment".to_string(),
"div { width: 99999999999999999999999px; }".to_string(),
"div { width: -0px; opacity: 1e400; }".to_string(),
"div { width: NaNpx; height: infpx; }".to_string(),
"* { color: #ZZZZZZ; }".to_string(),
"日本語 { content: \"🦀\"; }".to_string(),
".\u{202e}rtl { color: red; }".to_string(),
"a".repeat(10_000),
"div { color: red; }".repeat(500),
];
for case in &cases {
let css = parse_css(case);
let mut dom = Dom::create_body().with_children(vec![Dom::create_div()].into());
let sd = StyledDom::create(&mut dom, css);
assert_eq!(
sd.node_count(),
2,
"CSS must never change the node count; failing input: {case:?}"
);
}
}
#[test]
fn create_handles_deep_and_wide_doms() {
let mut deep = Dom::create_div();
for _ in 0..63 {
deep = Dom::create_div().with_children(vec![deep].into());
}
let mut deep_body = Dom::create_body().with_children(vec![deep].into());
let sd = StyledDom::create(&mut deep_body, Css::empty());
assert_eq!(sd.node_count(), 65);
assert_eq!(
sd.non_leaf_nodes.as_ref().len(),
64,
"every node except the innermost leaf is a parent"
);
let wide = flat_body(1000);
assert_eq!(wide.node_count(), 1001);
assert_eq!(wide.node_hierarchy.as_container().subtree_len(NodeId::ZERO), 1000);
assert_eq!(wide.non_leaf_nodes.as_ref().len(), 1);
}
#[test]
fn create_from_dom_collects_scoped_css_without_changing_the_tree() {
let dom = Dom::create_body().with_children(
vec![
Dom::create_div().with_css("color: red"),
Dom::create_div().with_children(vec![Dom::create_div().with_css("width: 5px")].into()),
]
.into(),
);
let sd = StyledDom::create_from_dom(dom);
assert_eq!(sd.node_count(), 4);
assert_eq!(sd.node_hierarchy.as_ref().len(), 4);
assert!(sd.get_css_property_cache().compact_cache.is_some());
}
#[test]
fn create_from_dom_on_a_bare_leaf_produces_one_node() {
let sd = StyledDom::create_from_dom(Dom::create_div());
assert_eq!(sd.node_count(), 1);
assert_eq!(sd.root.into_crate_internal(), Some(NodeId::ZERO));
}
#[test]
fn append_child_grows_the_node_count_by_the_child_dom_size() {
let mut base = flat_body(2);
base.append_child(flat_body(3));
assert_eq!(base.node_count(), 3 + 4);
assert_eq!(base.node_hierarchy.as_ref().len(), 7);
assert_eq!(base.styled_nodes.as_ref().len(), 7);
assert_eq!(base.cascade_info.as_ref().len(), 7);
}
#[test]
fn append_child_links_the_new_root_as_the_last_sibling() {
let mut base = flat_body(2);
base.append_child(StyledDom::default());
let h = base.node_hierarchy.as_container();
let children: Vec<NodeId> = NodeId::ZERO.az_children(&h).collect();
assert_eq!(
children,
vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)],
"the appended root must become the last direct child"
);
assert_eq!(h[NodeId::new(3)].parent_id(), Some(NodeId::ZERO));
assert_eq!(h[NodeId::new(3)].previous_sibling_id(), Some(NodeId::new(2)));
assert_eq!(h[NodeId::new(3)].next_sibling_id(), None);
}
#[test]
fn append_child_keeps_the_root_children_reachable_for_a_nested_dom() {
let mut base = nested_body(); base.append_child(StyledDom::default());
assert_eq!(base.node_count(), 4);
let h = base.node_hierarchy.as_container();
let children: Vec<NodeId> = NodeId::ZERO.az_children(&h).collect();
assert_eq!(
children,
vec![NodeId::new(1), NodeId::new(3)],
"after append_child the root must have exactly its old child plus the appended root"
);
}
#[test]
fn append_child_with_index_saturates_the_u32_cascade_index() {
for (child_index, expected) in [
(0usize, 0u32),
(7, 7),
(u32::MAX as usize, u32::MAX),
(u32::MAX as usize + 1, u32::MAX),
(usize::MAX, u32::MAX),
] {
let mut base = flat_body(0); base.append_child_with_index(StyledDom::default(), child_index);
assert_eq!(
base.cascade_info.as_ref()[1].index_in_parent,
expected,
"child_index {child_index} must saturate to {expected}, never wrap"
);
assert!(base.cascade_info.as_ref()[1].is_last_child);
assert_eq!(base.node_count(), 2);
}
}
#[test]
fn finalize_non_leaf_nodes_sorts_by_depth_and_is_idempotent() {
let mut base = flat_body(1);
base.append_child_with_index(flat_body(2), 1);
base.append_child_with_index(flat_body(2), 2);
base.finalize_non_leaf_nodes();
let depths: Vec<usize> = base.non_leaf_nodes.as_ref().iter().map(|p| p.depth).collect();
let mut sorted = depths.clone();
sorted.sort_unstable();
assert_eq!(depths, sorted, "non_leaf_nodes must be depth-ordered");
base.finalize_non_leaf_nodes();
let again: Vec<usize> = base.non_leaf_nodes.as_ref().iter().map(|p| p.depth).collect();
assert_eq!(depths, again, "finalize must be idempotent");
}
#[test]
fn with_child_matches_append_child() {
let mut appended = flat_body(2);
appended.append_child(flat_body(1));
let built = flat_body(2).with_child(flat_body(1));
assert_eq!(built.node_count(), appended.node_count());
assert_eq!(
built.node_hierarchy.as_ref(),
appended.node_hierarchy.as_ref()
);
}
#[test]
fn swap_with_default_returns_the_old_dom_and_resets_self() {
let mut sd = flat_body(3);
let old = sd.swap_with_default();
assert_eq!(old.node_count(), 4);
assert_eq!(sd.node_count(), 1, "self must be left as the default StyledDom");
assert_eq!(sd.root.into_crate_internal(), Some(NodeId::ZERO));
}
#[test]
fn context_menu_and_menu_bar_are_stored_on_the_root_node() {
let mut sd = flat_body(1);
assert!(sd.node_data.as_container()[NodeId::ZERO].get_context_menu().is_none());
sd.set_context_menu(empty_menu());
sd.set_menu_bar(empty_menu());
let data = sd.node_data.as_container();
assert!(data[NodeId::ZERO].get_context_menu().is_some());
assert!(data[NodeId::ZERO].get_menu_bar().is_some());
assert!(data[NodeId::new(1)].get_context_menu().is_none());
assert!(data[NodeId::new(1)].get_menu_bar().is_none());
}
#[test]
fn menu_builders_are_equivalent_to_the_setters_and_dont_touch_the_tree() {
let sd = StyledDom::default()
.with_context_menu(empty_menu())
.with_menu_bar(empty_menu());
assert_eq!(sd.node_count(), 1);
let data = sd.node_data.as_container();
assert!(data[NodeId::ZERO].get_context_menu().is_some());
assert!(data[NodeId::ZERO].get_menu_bar().is_some());
}
#[test]
fn restyle_nodes_hover_sets_and_clears_the_state_flag() {
let mut sd = flat_body(2);
let _ = sd.restyle_nodes_hover(&[NodeId::new(1)], true);
assert!(sd.get_styled_node_state(&NodeId::new(1)).hover);
assert!(!sd.get_styled_node_state(&NodeId::new(2)).hover);
let _ = sd.restyle_nodes_hover(&[NodeId::new(1)], false);
assert!(!sd.get_styled_node_state(&NodeId::new(1)).hover);
assert!(sd.get_styled_node_state(&NodeId::new(1)).is_normal());
}
#[test]
fn restyle_nodes_active_and_focus_set_independent_flags() {
let mut sd = flat_body(1);
let _ = sd.restyle_nodes_active(&[NodeId::ZERO], true);
let _ = sd.restyle_nodes_focus(&[NodeId::ZERO], true);
let state = sd.get_styled_node_state(&NodeId::ZERO);
assert!(state.active);
assert!(state.focused);
assert!(!state.hover, "hover must be untouched");
assert!(!state.is_normal());
}
#[test]
fn restyle_nodes_ignores_out_of_range_node_ids_instead_of_panicking() {
let mut sd = flat_body(1); let changed = sd.restyle_nodes_hover(&[NodeId::new(2), NodeId::new(usize::MAX)], true);
assert!(changed.is_empty());
assert!(!sd.get_styled_node_state(&NodeId::ZERO).hover);
assert!(!sd.get_styled_node_state(&NodeId::new(1)).hover);
let _ = sd.restyle_nodes_hover(&[NodeId::new(1), NodeId::new(999)], true);
assert!(sd.get_styled_node_state(&NodeId::new(1)).hover);
}
#[test]
fn restyle_nodes_handles_empty_and_duplicated_input() {
let mut sd = flat_body(1);
assert!(sd.restyle_nodes_focus(&[], true).is_empty());
let _ = sd.restyle_nodes_focus(&[NodeId::ZERO, NodeId::ZERO, NodeId::ZERO], true);
assert!(sd.get_styled_node_state(&NodeId::ZERO).focused);
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn get_styled_node_state_panics_on_an_out_of_range_node_id() {
let sd = flat_body(1);
let _ = sd.get_styled_node_state(&NodeId::new(99));
}
#[test]
fn restyle_on_state_change_with_no_changes_reports_nothing_to_do() {
let mut sd = flat_body(2);
let r = sd.restyle_on_state_change(None, None, None);
assert!(!r.has_changes());
assert!(!r.needs_layout);
assert!(!r.needs_display_list);
assert!(!r.gpu_only_changes);
assert_eq!(r.max_relayout_scope, RelayoutScope::None);
}
#[test]
fn restyle_on_state_change_tolerates_stale_node_ids() {
let mut sd = flat_body(1);
let r = sd.restyle_on_state_change(
Some(FocusChange {
lost_focus: Some(NodeId::new(500)),
gained_focus: Some(NodeId::new(usize::MAX)),
}),
Some(HoverChange {
left_nodes: vec![NodeId::new(700)],
entered_nodes: vec![NodeId::new(800)],
}),
Some(ActiveChange {
deactivated: vec![NodeId::new(900)],
activated: vec![NodeId::new(1000)],
}),
);
assert!(!r.has_changes(), "stale ids must be filtered, not applied");
assert_eq!(sd.node_count(), 2);
}
#[test]
fn restyle_on_state_change_applies_state_to_valid_nodes() {
let mut sd = flat_body(1);
let r = sd.restyle_on_state_change(
None,
Some(HoverChange {
left_nodes: Vec::new(),
entered_nodes: vec![NodeId::new(1)],
}),
None,
);
assert!(sd.get_styled_node_state(&NodeId::new(1)).hover);
assert!(
r.changed_nodes.keys().all(|n| *n == NodeId::new(1)),
"only the node whose state actually changed may be reported"
);
}
#[test]
fn restyle_user_property_rejects_empty_lists_and_stale_nodes() {
let mut sd = flat_body(1);
assert!(sd.restyle_user_property(&NodeId::ZERO, &[]).is_empty());
assert!(
sd.restyle_user_property(
&NodeId::new(50),
&[CssProperty::auto(CssPropertyType::Width)]
)
.is_empty(),
"an out-of-range node id must be a no-op, not a panic"
);
assert!(
sd.get_css_property_cache()
.user_overridden_properties
.iter()
.all(Vec::is_empty),
"a rejected call must not record an override"
);
}
#[test]
fn restyle_user_property_stores_the_override_and_initial_removes_it() {
let mut sd = flat_body(1);
let node = NodeId::ZERO;
let _ = sd.restyle_user_property(&node, &[CssProperty::auto(CssPropertyType::Width)]);
{
let overrides = &sd.get_css_property_cache().user_overridden_properties;
assert_eq!(overrides.len(), sd.node_count(), "table grows to cover the DOM");
assert_eq!(overrides[0].len(), 1);
assert_eq!(overrides[0][0].0, CssPropertyType::Width);
}
let _ = sd.restyle_user_property(&node, &[CssProperty::none(CssPropertyType::Width)]);
assert_eq!(sd.get_css_property_cache().user_overridden_properties[0].len(), 1);
let _ = sd.restyle_user_property(&node, &[CssProperty::initial(CssPropertyType::Width)]);
assert!(sd.get_css_property_cache().user_overridden_properties[0].is_empty());
let _ = sd.restyle_user_property(&node, &[CssProperty::initial(CssPropertyType::Height)]);
assert!(sd.get_css_property_cache().user_overridden_properties[0].is_empty());
}
#[test]
fn restyle_and_recompute_preserve_the_tree_and_rebuild_the_compact_cache() {
let mut sd = flat_body(3);
let before = sd.node_count();
sd.restyle(parse_css("div { color: red; } body > div:hover { color: blue; }"));
assert_eq!(sd.node_count(), before);
assert!(sd.get_css_property_cache().compact_cache.is_some());
sd.restyle(parse_css("}}} div { : ; }"));
assert_eq!(sd.node_count(), before);
sd.recompute_inheritance_and_compact_cache();
assert_eq!(sd.node_count(), before);
assert!(sd.get_css_property_cache().compact_cache.is_some());
}
#[test]
fn get_css_property_cache_mut_sees_the_same_cache_as_the_shared_getter() {
let mut sd = flat_body(1);
let node_count = sd.node_count();
sd.get_css_property_cache_mut()
.user_overridden_properties
.resize(node_count, Vec::new());
assert_eq!(
sd.get_css_property_cache().user_overridden_properties.len(),
node_count
);
}
#[test]
fn get_html_string_test_mode_omits_the_html_wrapper() {
let sd = flat_body(2);
let out = sd.get_html_string("HEAD_MARK", "BODY_MARK", true);
assert!(!out.is_empty());
assert!(!out.contains("HEAD_MARK"), "test_mode must not emit the custom head");
assert!(!out.contains("BODY_MARK"), "test_mode must not emit the custom body");
assert!(!out.contains("<html>"));
}
#[test]
fn get_html_string_embeds_custom_head_and_body_verbatim() {
let sd = flat_body(1);
let head = "🦀 <meta charset=\"utf-8\"> & ünïcödé";
let body = "x".repeat(10_000);
let out = sd.get_html_string(head, &body, false);
assert!(out.contains("<html>"));
assert!(out.contains(head));
assert!(out.contains(&body));
}
#[test]
fn get_html_string_does_not_panic_on_extreme_doms() {
assert!(!StyledDom::default().get_html_string("", "", true).is_empty());
assert!(!flat_body(0).get_html_string("", "", true).is_empty());
assert!(!nested_body().get_html_string("", "", true).is_empty());
assert!(!flat_body(200).get_html_string("", "", true).is_empty());
}
#[test]
fn get_rects_in_rendering_order_is_a_permutation_of_the_children() {
let sd = flat_body(3);
let group = sd.get_rects_in_rendering_order();
assert_eq!(group.root.into_crate_internal(), Some(NodeId::ZERO));
let mut ids: Vec<usize> = group
.children
.as_ref()
.iter()
.filter_map(|c| c.root.into_crate_internal())
.map(|n| n.index())
.collect();
ids.sort_unstable();
assert_eq!(ids, vec![1, 2, 3], "every child appears exactly once");
}
#[test]
fn get_rects_in_rendering_order_nests_grandchildren() {
let sd = nested_body(); let group = sd.get_rects_in_rendering_order();
assert_eq!(group.children.as_ref().len(), 1);
let child = &group.children.as_ref()[0];
assert_eq!(child.root.into_crate_internal(), Some(NodeId::new(1)));
assert_eq!(child.children.as_ref().len(), 1);
assert_eq!(
child.children.as_ref()[0].root.into_crate_internal(),
Some(NodeId::new(2))
);
}
#[test]
fn determine_rendering_order_with_no_parents_yields_a_childless_root() {
let sd = StyledDom::default();
let hierarchy = sd.node_hierarchy.as_container();
let styled = sd.styled_nodes.as_container();
let data = sd.node_data.as_container();
let group = StyledDom::determine_rendering_order(
&[],
&hierarchy,
&styled,
&data,
sd.get_css_property_cache(),
);
assert_eq!(group.root.into_crate_internal(), Some(NodeId::ZERO));
assert!(group.children.as_ref().is_empty());
}
#[test]
fn sort_children_by_position_returns_every_child_of_a_leaf_free_parent() {
let sd = flat_body(3);
let hierarchy = sd.node_hierarchy.as_container();
let styled = sd.styled_nodes.as_container();
let data = sd.node_data.as_container();
let sorted = sort_children_by_position(
NodeId::ZERO,
&hierarchy,
&styled,
&data,
sd.get_css_property_cache(),
);
assert_eq!(sorted.len(), 3);
let leaf = sort_children_by_position(
NodeId::new(3),
&hierarchy,
&styled,
&data,
sd.get_css_property_cache(),
);
assert!(leaf.is_empty());
}
#[test]
fn fill_content_group_children_builds_the_nested_group_tree() {
let id = |i: usize| NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(i)));
let mut sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> = BTreeMap::new();
sorted.insert(id(0), vec![id(1), id(2)]);
sorted.insert(id(1), vec![id(3)]);
let mut group = ContentGroup {
root: id(0),
children: Vec::new().into(),
};
fill_content_group_children(&mut group, &sorted);
assert_eq!(group.children.as_ref().len(), 2);
assert_eq!(group.children.as_ref()[0].root, id(1));
assert_eq!(group.children.as_ref()[0].children.as_ref().len(), 1);
assert_eq!(group.children.as_ref()[0].children.as_ref()[0].root, id(3));
assert!(
group.children.as_ref()[1].children.as_ref().is_empty(),
"a node with no entry in the map is a leaf"
);
}
#[test]
fn fill_content_group_children_leaves_an_unknown_root_untouched() {
let sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> = BTreeMap::new();
let mut group = ContentGroup {
root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(9))),
children: Vec::new().into(),
};
fill_content_group_children(&mut group, &sorted);
assert!(group.children.as_ref().is_empty());
}
#[test]
fn recursive_get_last_child_descends_to_the_deepest_last_child() {
let items = vec![
raw_item(0, 0, 0, 2), raw_item(1, 0, 0, 3), raw_item(2, 0, 0, 0), ];
let mut target = None;
recursive_get_last_child(NodeId::ZERO, &items, &mut target);
assert_eq!(target, Some(NodeId::new(2)));
}
#[test]
fn recursive_get_last_child_leaves_the_target_untouched_for_a_leaf() {
let items = vec![raw_item(0, 0, 0, 0)];
let mut target = None;
recursive_get_last_child(NodeId::ZERO, &items, &mut target);
assert_eq!(target, None);
let mut preset = Some(NodeId::new(7));
recursive_get_last_child(NodeId::ZERO, &items, &mut preset);
assert_eq!(preset, Some(NodeId::new(7)));
}
#[test]
fn get_path_to_root_is_root_first_and_tolerates_unknown_nodes() {
let sd = nested_body(); let h = sd.node_hierarchy.as_container();
assert_eq!(get_path_to_root(&h, NodeId::ZERO), vec![NodeId::ZERO]);
assert_eq!(
get_path_to_root(&h, NodeId::new(2)),
vec![NodeId::ZERO, NodeId::new(1), NodeId::new(2)]
);
assert_eq!(
get_path_to_root(&h, NodeId::new(9999)),
vec![NodeId::new(9999)]
);
}
#[test]
fn is_before_in_document_order_is_false_for_identical_nodes() {
let sd = flat_body(2);
assert!(!is_before_in_document_order(
&sd.node_hierarchy,
NodeId::new(1),
NodeId::new(1)
));
}
#[test]
fn is_before_in_document_order_orders_ancestors_and_siblings() {
let sd = flat_body(3); let h = &sd.node_hierarchy;
assert!(is_before_in_document_order(h, NodeId::ZERO, NodeId::new(1)));
assert!(!is_before_in_document_order(h, NodeId::new(1), NodeId::ZERO));
assert!(is_before_in_document_order(h, NodeId::new(1), NodeId::new(3)));
assert!(!is_before_in_document_order(h, NodeId::new(3), NodeId::new(1)));
}
#[test]
fn is_before_in_document_order_is_antisymmetric_across_a_nested_tree() {
let sd = nested_body();
let h = &sd.node_hierarchy;
for a in 0..3 {
for b in 0..3 {
let ab = is_before_in_document_order(h, NodeId::new(a), NodeId::new(b));
let ba = is_before_in_document_order(h, NodeId::new(b), NodeId::new(a));
if a == b {
assert!(!ab && !ba, "a node is never before itself");
} else {
assert_ne!(ab, ba, "exactly one of ({a},{b}) / ({b},{a}) must hold");
}
}
}
}
#[test]
fn is_before_in_document_order_is_deterministic_for_unknown_nodes() {
let sd = flat_body(1);
let h = &sd.node_hierarchy;
assert!(is_before_in_document_order(h, NodeId::ZERO, NodeId::new(usize::MAX)));
assert!(!is_before_in_document_order(h, NodeId::new(usize::MAX), NodeId::ZERO));
}
#[test]
fn collect_nodes_in_document_order_start_equals_end() {
let sd = flat_body(2);
assert_eq!(
collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::new(2), NodeId::new(2)),
vec![NodeId::new(2)]
);
assert_eq!(
collect_nodes_in_document_order(
&sd.node_hierarchy,
NodeId::new(usize::MAX),
NodeId::new(usize::MAX)
),
vec![NodeId::new(usize::MAX)]
);
}
#[test]
fn collect_nodes_in_document_order_walks_the_tree_in_pre_order() {
let sd = flat_body(3); assert_eq!(
collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::ZERO, NodeId::new(3)),
vec![NodeId::ZERO, NodeId::new(1), NodeId::new(2), NodeId::new(3)]
);
assert_eq!(
collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::new(1), NodeId::new(2)),
vec![NodeId::new(1), NodeId::new(2)]
);
let nested = nested_body();
assert_eq!(
collect_nodes_in_document_order(&nested.node_hierarchy, NodeId::ZERO, NodeId::new(2)),
vec![NodeId::ZERO, NodeId::new(1), NodeId::new(2)]
);
}
#[test]
fn collect_nodes_in_document_order_terminates_when_end_precedes_start() {
let sd = flat_body(3);
let out = collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::new(2), NodeId::new(1));
assert!(out.is_empty());
}
#[test]
fn collect_nodes_in_document_order_with_an_unreachable_end_stops_at_the_tree_end() {
let sd = flat_body(3);
let out = collect_nodes_in_document_order(
&sd.node_hierarchy,
NodeId::new(1),
NodeId::new(usize::MAX),
);
assert_eq!(
out,
vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)],
"an end node that is never reached must terminate at the end of the traversal"
);
}
#[test]
fn is_layout_equivalent_holds_for_independently_built_identical_doms() {
assert!(is_layout_equivalent(&flat_body(3), &flat_body(3)));
assert!(is_layout_equivalent(
&StyledDom::default(),
&StyledDom::default()
));
assert!(is_layout_equivalent(&nested_body(), &nested_body()));
}
#[test]
fn is_layout_equivalent_rejects_a_different_node_count() {
assert!(!is_layout_equivalent(&flat_body(3), &flat_body(4)));
assert!(!is_layout_equivalent(&flat_body(0), &flat_body(1)));
}
#[test]
fn is_layout_equivalent_rejects_a_different_structure() {
assert!(!is_layout_equivalent(&nested_body(), &flat_body(2)));
}
#[test]
fn is_layout_equivalent_rejects_a_changed_class() {
let build = |class: &str| {
let mut dom = Dom::create_body().with_children(
vec![Dom::create_div().with_class(class.to_string().into())].into(),
);
StyledDom::create(&mut dom, Css::empty())
};
assert!(is_layout_equivalent(&build("a"), &build("a")));
assert!(!is_layout_equivalent(&build("a"), &build("b")));
}
#[test]
fn is_layout_equivalent_rejects_a_changed_pseudo_state() {
let base = flat_body(2);
let mut hovered = flat_body(2);
let _ = hovered.restyle_nodes_hover(&[NodeId::new(1)], true);
assert!(
!is_layout_equivalent(&base, &hovered),
":hover changes CSS resolution, so the DOMs are not layout-equivalent"
);
}
#[test]
fn compact_dom_len_and_is_empty() {
let single = convert_dom_into_compact_dom(Dom::create_div());
assert_eq!(single.len(), 1);
assert!(!single.is_empty());
let tree = convert_dom_into_compact_dom(
Dom::create_body().with_children(vec![Dom::create_div(); 4].into()),
);
assert_eq!(tree.len(), 5);
assert!(!tree.is_empty());
let empty = CompactDom {
node_hierarchy: NodeHierarchy {
internal: Vec::new(),
},
node_data: NodeDataContainer {
internal: Vec::new(),
},
root: NodeId::ZERO,
};
assert_eq!(empty.len(), 0);
assert!(empty.is_empty());
}
#[test]
fn convert_dom_into_compact_dom_links_flat_siblings() {
let compact = convert_dom_into_compact_dom(
Dom::create_body().with_children(vec![Dom::create_div(); 3].into()),
);
assert_eq!(compact.len(), 4);
assert_eq!(compact.root, NodeId::ZERO);
let h = compact.node_hierarchy.as_ref();
assert_eq!(h[NodeId::ZERO].parent, None);
assert_eq!(h[NodeId::ZERO].last_child, Some(NodeId::new(3)));
for i in 1..=3usize {
assert_eq!(h[NodeId::new(i)].parent, Some(NodeId::ZERO));
let expected_next = if i == 3 { None } else { Some(NodeId::new(i + 1)) };
assert_eq!(h[NodeId::new(i)].next_sibling, expected_next);
let expected_prev = if i == 1 { None } else { Some(NodeId::new(i - 1)) };
assert_eq!(h[NodeId::new(i)].previous_sibling, expected_prev);
assert_eq!(h[NodeId::new(i)].last_child, None, "the children are leaves");
}
}
#[test]
fn convert_dom_into_compact_dom_last_child_is_the_last_direct_child() {
let sd = nested_body();
let h = sd.node_hierarchy.as_container();
let last_direct_child = NodeId::ZERO.az_children(&h).last();
assert_eq!(last_direct_child, Some(NodeId::new(1)));
assert_eq!(
h[NodeId::ZERO].last_child_id(),
last_direct_child,
"last_child_id() must agree with the forward child iteration"
);
}
#[test]
fn convert_dom_into_compact_dom_handles_an_empty_and_a_deep_tree() {
assert_eq!(convert_dom_into_compact_dom(Dom::create_body()).len(), 1);
let mut deep = Dom::create_div();
for _ in 0..64 {
deep = Dom::create_div().with_children(vec![deep].into());
}
let compact = convert_dom_into_compact_dom(deep);
assert_eq!(compact.len(), 65);
let h = compact.node_hierarchy.as_ref();
for i in 1..65usize {
assert_eq!(h[NodeId::new(i)].parent, Some(NodeId::new(i - 1)));
}
}
#[test]
fn scope_inline_css_advances_next_id_once_per_node() {
let mut dom = Dom::create_body().with_children(
vec![
Dom::create_div().with_children(vec![Dom::create_div()].into()),
Dom::create_div(),
]
.into(),
);
let _ = dom.fixup_children_estimated();
let mut next = 0usize;
scope_inline_css(&mut dom, &mut next);
assert_eq!(next, 4, "4 nodes → the counter must land on 4 (pre-order ids 0..3)");
}
#[test]
fn scope_inline_css_from_zero_and_from_a_large_offset() {
let mut leaf = Dom::create_div();
let _ = leaf.fixup_children_estimated();
let mut next = 0usize;
scope_inline_css(&mut leaf, &mut next);
assert_eq!(next, 1, "a single leaf consumes exactly one id");
let mut dom = Dom::create_body().with_children(vec![Dom::create_div(); 2].into());
let _ = dom.fixup_children_estimated();
let mut big = 1_000_000usize;
scope_inline_css(&mut dom, &mut big);
assert_eq!(big, 1_000_003);
}
#[test]
fn scope_inline_css_preserves_the_rule_count_of_every_node() {
let mut dom = Dom::create_body()
.with_css("color: red")
.with_children(vec![Dom::create_div().with_css("width: 5px")].into());
let _ = dom.fixup_children_estimated();
let rules_before: usize = dom
.css
.as_ref()
.iter()
.map(|c| c.rules.as_ref().len())
.sum::<usize>()
+ dom.children.as_ref()[0]
.css
.as_ref()
.iter()
.map(|c| c.rules.as_ref().len())
.sum::<usize>();
assert!(rules_before > 0, "with_css must produce at least one rule");
let mut next = 0usize;
scope_inline_css(&mut dom, &mut next);
let rules_after: usize = dom
.css
.as_ref()
.iter()
.map(|c| c.rules.as_ref().len())
.sum::<usize>()
+ dom.children.as_ref()[0]
.css
.as_ref()
.iter()
.map(|c| c.rules.as_ref().len())
.sum::<usize>();
assert_eq!(
rules_before, rules_after,
"scoping rewrites paths in place; it must not add or drop rules"
);
assert_eq!(next, 2);
}
#[test]
fn collect_css_from_dom_yields_inner_css_before_outer_css() {
let outer = parse_css("div { color: red; } span { color: blue; }");
let inner = parse_css("p { color: green; }");
let outer_rules = outer.rules.as_ref().len();
let inner_rules = inner.rules.as_ref().len();
assert_ne!(
outer_rules, inner_rules,
"the two stylesheets must be distinguishable by rule count"
);
let mut child = Dom::create_div();
child.add_component_css(inner);
let mut dom = Dom::create_body().with_children(vec![child].into());
dom.add_component_css(outer);
let mut out = Vec::new();
collect_css_from_dom(&dom, &mut out);
assert_eq!(out.len(), 2);
assert_eq!(
out[0].rules.as_ref().len(),
inner_rules,
"deeper CSS is collected first (lower cascade priority)"
);
assert_eq!(out[1].rules.as_ref().len(), outer_rules);
}
#[test]
fn collect_css_from_dom_on_a_css_free_tree_appends_nothing() {
let dom = Dom::create_body().with_children(vec![Dom::create_div(); 3].into());
let mut out = Vec::new();
collect_css_from_dom(&dom, &mut out);
assert!(out.is_empty());
let mut prefilled = vec![Css::empty()];
collect_css_from_dom(&dom, &mut prefilled);
assert_eq!(prefilled.len(), 1);
}
#[test]
fn strip_css_from_dom_clears_every_node_recursively() {
let mut dom = Dom::create_body()
.with_css("color: red")
.with_children(
vec![Dom::create_div()
.with_css("width: 5px")
.with_children(vec![Dom::create_div().with_css("height: 5px")].into())]
.into(),
);
assert!(!dom.css.as_ref().is_empty());
strip_css_from_dom(&mut dom);
assert!(dom.css.as_ref().is_empty());
let child = &dom.children.as_ref()[0];
assert!(child.css.as_ref().is_empty());
assert!(child.children.as_ref()[0].css.as_ref().is_empty());
strip_css_from_dom(&mut dom);
assert!(dom.css.as_ref().is_empty());
}
}