1use crate::engine::animation::{AnimationInputs, ElementAnimationState, parse as anim_parse};
4use crate::engine::css::values::CssValue;
5
6mod args;
7#[macro_use]
8mod munch;
9#[macro_use]
10mod parse_args;
11mod anchor;
12mod apply;
13mod background;
14mod color;
15mod css_resolve;
16mod layout_fix;
17#[cfg(test)]
18mod tests;
19
20pub use anchor::{parse_anchor_spec, resolve_anchor_positioning};
21pub use apply::{apply_declaration, blockify_out_of_flow_positioned};
22pub use layout_fix::{
23 constrain_auto_grid_track_items, correct_atomic_inline_spacing,
24 correct_atomic_inline_spacing_with_info, is_block_layout_child, is_collapsible_whitespace_info,
25 refresh_missing_text_layout_results,
26};
27
28#[allow(unused_imports)]
29pub use background::{
30 apply_background_shorthand_geometry, parse_background_position, parse_background_repeat,
31 parse_background_shorthand, parse_background_size, parse_gradient,
32};
33pub use color::resolve_css_color;
34pub use css_resolve::{
35 CalcValue, extract_font_families, one_or_two_values, parse_grid_line, parse_grid_line_end,
36 parse_grid_placement, parse_grid_template_areas, parse_grid_tracks, resolve_calc_value,
37 resolve_css_len, resolve_css_len_auto, resolve_font_size_px,
38};
39
40use crate::{perf_scope, profile_log};
41
42use crate::engine::bridge::audio::AudioSinkFactory;
43use crate::engine::bridge::text::{self, GlyphCluster};
44use crate::engine::css::{
45 matcher::{ElementChain, ElementInfo},
46 values::Unit,
47};
48use crate::engine::html::util::split_html_space;
49use crate::engine::html::{HtmlNodeType, ScriptingMode};
50use crate::engine::layouter::css_resolver::{
51 DeclarationResolver, Properties, PseudoElement, resolve_inline_value,
52};
53use crate::engine::layouter::dom_snapshot::{DomSnapshot, NodeId};
54use crate::engine::layouter::types::WhiteSpace;
55use crate::engine::renderer_model::Image;
56use crate::engine::svg::{SvgContent, collect_svg};
57use crate::engine::tree::NodeRef;
58use crate::engine::ui::custom_node::CustomNode;
59use crate::engine::ui::custom_node_bridge::CustomNodeBridge;
60use crate::engine::ui::registry::{ComponentRegistry, CustomNodeContext, DomWriteBack};
61
62use std::borrow::Cow;
63use std::collections::{HashMap, HashSet};
64use std::sync::Arc;
65
66#[allow(unused_imports)]
67use ui_layout::{
68 AlignContent, AlignItems, AutoSizeBehavior, BoxSizing, Clear as LayoutClear, Display,
69 FlexDirection, FlexWrap, Float as LayoutFloat, GridPlacement, GridPlacementEnd, GridRepeat,
70 GridTrack, InnerDisplay, ItemFragment, JustifyContent, JustifyItems, LayoutChild, LayoutNode,
71 Length, LengthOrAuto, OuterDisplay, Position, Style,
72};
73
74use super::css_resolver::{
75 MediaEnvironment, ResolvedStyles, RuleSet, resolve_inline_style, set_inline_custom_property,
76};
77use super::match_cache::SelectorMatchCache;
78use super::text_layouter::TextFlowLayouter;
79#[allow(unused_imports)]
80use super::types::{
81 Background, BackgroundDimension, BackgroundOffset, BackgroundPosition, BackgroundPositionAxis,
82 BackgroundRepeat, BackgroundSize, BorderRadius, BorderStyle, Color, ColorScheme, ColorStop,
83 ContainerRole, ContainerStyle, CornerRadius, CssClear, CssFloat, FontStyle, FontWeight,
84 Gradient, GradientKind, InfoNode, InteractionState, LineHeight, NodeKind, Overflow,
85 RadialShape, RadialSizeKind, TextAlign, TextDecoration, TextFlowStyle, TextStyle,
86 TextTransform,
87};
88
89pub(crate) const DEFAULT_LINE_FACTOR: f32 = 1.2;
90
91const GRID_LINE_TO_END: GridPlacementEnd = GridPlacementEnd::Line(usize::MAX);
92
93pub(crate) fn element_info(html_node: &HtmlNodeType) -> Option<ElementInfo> {
94 let HtmlNodeType::Element {
95 tag_name,
96 attributes,
97 ..
98 } = html_node
99 else {
100 return None;
101 };
102 Some(ElementInfo {
103 tag_name: tag_name.as_str().into(),
104 id: attributes
105 .iter()
106 .find(|attribute| attribute.name == "id")
107 .map(|attribute| Arc::from(attribute.value.as_str())),
108 classes: attributes
109 .iter()
110 .find(|attribute| attribute.name == "class")
111 .map(|attribute| {
112 split_html_space(&attribute.value)
113 .map(Arc::<str>::from)
114 .collect()
115 })
116 .unwrap_or_default(),
117 attributes: attributes
118 .iter()
119 .map(|attribute| {
120 (
121 Arc::<str>::from(attribute.name.as_str()),
122 Arc::<str>::from(attribute.value.as_str()),
123 )
124 })
125 .collect(),
126 element_index: 1,
127 element_count: 1,
128 type_index: 1,
129 type_count: 1,
130 previous_siblings: ElementChain::default(),
131 has_parent: true,
136 is_empty: false,
137 lang: None,
138 checked: false,
139 hovered: false,
140 active: false,
141 })
142}
143
144pub(crate) fn is_element_node(snapshot: &DomSnapshot, id: NodeId) -> bool {
148 matches!(snapshot.node(id).kind, HtmlNodeType::Element { .. })
149}
150
151pub(crate) fn own_lang(snapshot: &DomSnapshot, id: NodeId) -> Option<Arc<str>> {
153 snapshot
154 .node(id)
155 .kind
156 .get_attr("lang")
157 .map(str::trim)
158 .filter(|value| !value.is_empty())
159 .map(Arc::<str>::from)
160}
161
162pub(crate) fn is_empty_element(snapshot: &DomSnapshot, id: NodeId) -> bool {
166 snapshot
167 .children(id)
168 .iter()
169 .all(|&child| match &snapshot.node(child).kind {
170 HtmlNodeType::Text(text) => text.is_empty(),
171 HtmlNodeType::Element { .. } => false,
172 _ => true,
173 })
174}
175
176pub(crate) fn element_sibling_infos(
177 snapshot: &DomSnapshot,
178 children: &[NodeId],
179 parent_lang: Option<&str>,
180 interaction: &InteractionState,
181) -> Vec<Option<ElementInfo>> {
182 let mut type_counts = HashMap::<Arc<str>, usize>::new();
183 for &child in children {
184 if let Some(tag_name) = snapshot.node(child).kind.tag_name() {
185 *type_counts.entry(Arc::from(tag_name)).or_default() += 1;
186 }
187 }
188 let element_count = type_counts.values().sum();
189
190 let mut seen_types = HashMap::<Arc<str>, usize>::new();
191 let mut previous_siblings = ElementChain::default();
192 let mut element_index = 0;
193 children
194 .iter()
195 .map(|&child| {
196 let mut info = match element_info(&snapshot.node(child).kind) {
197 Some(info) => info,
198 None => return None,
199 };
200 element_index += 1;
201 let seen = seen_types.entry(Arc::clone(&info.tag_name)).or_default();
202 *seen += 1;
203 info.element_index = element_index;
204 info.element_count = element_count;
205 info.type_index = *seen;
206 info.type_count = type_counts[&info.tag_name];
207 info.previous_siblings = previous_siblings.clone();
208 info.is_empty = is_empty_element(snapshot, child);
209 info.checked = snapshot.node(child).checked;
210 info.lang = own_lang(snapshot, child).or_else(|| parent_lang.map(Arc::<str>::from));
213 info.hovered = interaction.hovered_path.contains(&child);
214 info.active = interaction.active_path.contains(&child);
215
216 let mut sibling = info.clone();
217 sibling.previous_siblings = ElementChain::default();
218 previous_siblings = previous_siblings.prepend(Some(sibling));
219 Some(info)
220 })
221 .collect()
222}
223
224#[derive(Clone, Default)]
231pub struct InheritedCss {
232 pub custom_props: Arc<Properties>,
236 pub text_style: TextStyle,
237 pub text_flow_style: TextFlowStyle,
238 pub color_scheme: ColorScheme,
239}
240
241pub(super) fn length_to_px(len: &Length, font_size: f32) -> f32 {
243 match len {
244 Length::Px(v) => *v,
245 Length::Percent(v) => v * font_size / 100.0,
246 Length::Add(a, b) => length_to_px(a, font_size) + length_to_px(b, font_size),
247 Length::Sub(a, b) => length_to_px(a, font_size) - length_to_px(b, font_size),
248 Length::Mul(a, f) => length_to_px(a, font_size) * f,
249 Length::Div(a, f) => length_to_px(a, font_size) / f,
250 _ => font_size * DEFAULT_LINE_FACTOR,
251 }
252}
253
254struct StackFrame {
255 dom: NodeId,
256 lang: Option<Arc<str>>,
259 chain: ElementChain,
260 child: Arc<InheritedCss>,
261 kind: Option<NodeKind>,
262 style: Option<Style>,
263 parent_style: Style,
264 parent_container_style: ContainerStyle,
265 child_slots: Vec<ChildSlot>,
266 element_children: Vec<NodeId>,
267 counter_state: HashMap<String, i32>,
271}
272
273enum ChildSlot {
274 Inline(LayoutChild, Box<InfoNode>),
275 Element(usize),
276}
277
278#[allow(clippy::too_many_arguments)]
283pub fn build_layout_and_info(
284 dom: &NodeRef<HtmlNodeType>,
285 resolved_styles: &ResolvedStyles,
286 measurer: Arc<dyn text::TextMeasurer>,
287 audio_sinks: Arc<dyn AudioSinkFactory>,
288 parent: InheritedCss,
289 chain: ElementChain,
290 system_color_scheme: ColorScheme,
291 scripting_mode: ScriptingMode,
292 viewport: (f32, f32),
293) -> (LayoutNode, InfoNode) {
294 build_layout_and_info_with_images(
295 dom,
296 resolved_styles,
297 measurer,
298 audio_sinks,
299 parent,
300 chain,
301 system_color_scheme,
302 scripting_mode,
303 viewport,
304 &HashMap::new(),
305 )
306}
307
308#[allow(clippy::too_many_arguments)]
313pub fn build_layout_and_info_with_images(
314 dom: &NodeRef<HtmlNodeType>,
315 resolved_styles: &ResolvedStyles,
316 measurer: Arc<dyn text::TextMeasurer>,
317 audio_sinks: Arc<dyn AudioSinkFactory>,
318 parent: InheritedCss,
319 chain: ElementChain,
320 system_color_scheme: ColorScheme,
321 scripting_mode: ScriptingMode,
322 viewport: (f32, f32),
323 images: &HashMap<String, Image>,
324) -> (LayoutNode, InfoNode) {
325 let (snapshot, _dom_refs) = DomSnapshot::from_tree(dom);
326 let media_environment = MediaEnvironment::new(viewport, system_color_scheme);
327 let rule_set = RuleSet::from_declarations(resolved_styles, &media_environment);
328 let (layout, info, _) = build_layout_and_info_from_snapshot(
329 &snapshot,
330 snapshot.roots()[0],
331 &rule_set,
332 measurer,
333 audio_sinks,
334 parent,
335 chain,
336 system_color_scheme,
337 scripting_mode,
338 images,
339 &HashMap::new(),
340 None,
341 None,
342 &InteractionState::default(),
343 &HashMap::new(),
344 &HashMap::new(),
345 );
346 (layout, info)
347}
348
349#[allow(clippy::too_many_arguments)]
362pub fn build_layout_and_info_from_snapshot(
363 snapshot: &DomSnapshot,
364 root: NodeId,
365 rule_set: &RuleSet,
366 measurer: Arc<dyn text::TextMeasurer>,
367 audio_sinks: Arc<dyn AudioSinkFactory>,
368 parent: InheritedCss,
369 mut chain: ElementChain,
370 system_color_scheme: ColorScheme,
371 scripting_mode: ScriptingMode,
372 images: &HashMap<String, Image>,
373 audio: &HashMap<String, Arc<[u8]>>,
374 write_back_sender: Option<DomWriteBack>,
375 mut match_cache: Option<&mut SelectorMatchCache>,
376 interaction: &InteractionState,
377 existing_custom_nodes: &HashMap<u32, Arc<dyn CustomNode>>,
378 animated_overrides: &HashMap<NodeId, HashMap<String, CssValue>>,
379) -> (
380 LayoutNode,
381 InfoNode,
382 crate::engine::animation::AnimationInputs,
383) {
384 perf_scope!(total);
385
386 #[cfg(any(feature = "profile", debug_assertions))]
387 let mut css_match_time = std::time::Duration::ZERO;
388 #[cfg(any(feature = "profile", debug_assertions))]
389 let mut apply_decl_time = std::time::Duration::ZERO;
390 #[cfg(any(feature = "profile", debug_assertions))]
391 let mut custom_node_time = std::time::Duration::ZERO;
392 #[cfg(any(feature = "profile", debug_assertions))]
393 let mut text_layout_time = std::time::Duration::ZERO;
394 #[cfg(any(feature = "profile", debug_assertions))]
395 let mut exit_phase_time = std::time::Duration::ZERO;
396 #[cfg(any(feature = "profile", debug_assertions))]
397 let mut sibling_info_time = std::time::Duration::ZERO;
398 #[cfg(any(feature = "profile", debug_assertions))]
399 let mut whitespace_keep_time = std::time::Duration::ZERO;
400 #[cfg(any(feature = "profile", debug_assertions))]
401 let mut enter_prep_time = std::time::Duration::ZERO;
402 #[cfg(any(feature = "profile", debug_assertions))]
403 let mut child_slot_build_time = std::time::Duration::ZERO;
404 #[cfg(any(feature = "profile", debug_assertions))]
405 let mut leaf_assemble_time = std::time::Duration::ZERO;
406 #[cfg(any(feature = "profile", debug_assertions))]
407 let mut push_children_time = std::time::Duration::ZERO;
408 #[cfg(any(feature = "profile", debug_assertions))]
409 let mut exit_setup_time = std::time::Duration::ZERO;
410 #[cfg(any(feature = "profile", debug_assertions))]
411 let mut exit_build_time = std::time::Duration::ZERO;
412 #[cfg(any(feature = "profile", debug_assertions))]
413 let mut node_count = 0u64;
414
415 #[cfg(any(feature = "profile", debug_assertions))]
416 let mut cand_stats = CandidateMetrics::default();
417
418 let registry = ComponentRegistry::new();
419 if let Some(mut info) = element_info(&snapshot.node(root).kind) {
423 info.has_parent = false;
426 info.is_empty = is_empty_element(snapshot, root);
427 info.hovered = interaction.hovered_path.contains(&root);
428 info.active = interaction.active_path.contains(&root);
429 chain = chain.prepend(Some(info));
430 }
431
432 let mut stack: Vec<StackFrame> = Vec::new();
435 stack.push(StackFrame {
436 dom: root,
437 lang: own_lang(snapshot, root),
438 chain,
439 child: Arc::new(parent),
440 kind: None,
441 style: None,
442 parent_style: Style::default(),
443 parent_container_style: ContainerStyle::default(),
444 child_slots: Vec::new(),
445 element_children: Vec::new(),
446 counter_state: HashMap::new(),
447 });
448
449 let mut results: HashMap<NodeId, (LayoutNode, InfoNode)> = HashMap::new();
450 let mut animation_inputs: AnimationInputs = HashMap::new();
451
452 while let Some(top_idx) = {
455 if stack.is_empty() {
456 None
457 } else {
458 Some(stack.len() - 1)
459 }
460 } {
461 let is_entered = stack[top_idx].kind.is_some();
463
464 if !is_entered {
465 let chain_for_css = stack[top_idx].chain.clone();
468 let child_css = Arc::clone(&stack[top_idx].child);
469
470 let html_node = &snapshot.node(stack[top_idx].dom).kind;
471 let node_id = stack[top_idx].dom;
472 let mut text_style = child_css.text_style.clone();
473 let mut text_flow_style = child_css.text_flow_style;
474 let mut container_style = ContainerStyle::default();
475 if let HtmlNodeType::Element { tag_name, .. } = html_node {
476 container_style.tag = Some(tag_name.clone());
477 }
478 let mut style = Style::default();
479 let mut overflow = Overflow::default();
480 let parent_style = stack[top_idx].parent_style.clone();
481 let parent_container_style = stack[top_idx].parent_container_style.clone();
482 let parent_text_style = text_style.clone();
483 let parent_text_flow_style = text_flow_style;
484
485 container_style.visibility = parent_container_style.visibility;
487
488 perf_scope!(css_match);
490 let (candidates, custom_property_candidates, pseudo_candidates) =
491 if let HtmlNodeType::Element { .. } = html_node {
492 let (candidates, custom_property_candidates, pseudo_candidates) =
493 collect_candidates(
494 rule_set,
495 &chain_for_css,
496 node_id,
497 match_cache.as_deref_mut(),
498 #[cfg(any(feature = "profile", debug_assertions))]
499 &mut cand_stats,
500 );
501 (
502 Some(candidates),
503 Some(custom_property_candidates),
504 pseudo_candidates,
505 )
506 } else {
507 (None, None, HashMap::new())
508 };
509 #[cfg(any(feature = "profile", debug_assertions))]
510 {
511 css_match_time += css_match.elapsed();
512 }
513
514 perf_scope!(enter_prep);
515 let mut custom_properties = Arc::clone(&child_css.custom_props);
519 if let Some(own) = custom_property_candidates
520 && !own.is_empty()
521 {
522 Arc::make_mut(&mut custom_properties).extend(own);
523 }
524
525 let used_color_scheme = {
529 let declaration = candidates
530 .as_ref()
531 .and_then(|c| c.get("color-scheme"))
532 .and_then(|d| {
533 DeclarationResolver::resolve_var(
534 &d.value,
535 &custom_properties,
536 &mut HashSet::new(),
537 )
538 });
539 resolve_used_color_scheme(
540 declaration.as_ref(),
541 child_css.color_scheme,
542 system_color_scheme,
543 )
544 };
545 #[cfg(any(feature = "profile", debug_assertions))]
546 {
547 enter_prep_time += enter_prep.elapsed();
548 }
549
550 for attr in ["width", "height"] {
555 if let Some(value) = html_node.get_attr(attr)
556 && let Some(mut value) = resolve_inline_value(value)
557 {
558 if let CssValue::Number(v) = value {
559 value = CssValue::Length(v, Unit::Px);
560 }
561 apply_declaration(
562 attr,
563 &value,
564 &mut style,
565 &mut container_style,
566 &mut text_style,
567 &mut text_flow_style,
568 &parent_style,
569 &parent_container_style,
570 &parent_text_style,
571 &parent_text_flow_style,
572 &mut overflow,
573 used_color_scheme,
574 );
575 }
576 }
577
578 perf_scope!(apply_decl);
580 let mut anim_effective: std::collections::HashMap<String, CssValue> =
581 std::collections::HashMap::new();
582 let mut anim_transition = anim_parse::TransitionInputs::default();
583 let mut anim_animation = anim_parse::AnimationInputs::default();
584 let mut counter_resets: Vec<(String, i32)> = Vec::new();
586 let mut counter_increments: Vec<(String, i32)> = Vec::new();
587 if let Some(candidates) = &candidates {
588 let mut candidates: Vec<_> = candidates.values().collect();
594 candidates.sort_by_key(|declaration| declaration.order);
595
596 for declaration in candidates {
597 if declaration.name.starts_with("--") {
598 continue;
599 }
600 let Some(value) = DeclarationResolver::resolve_var(
601 &declaration.value,
602 &custom_properties,
603 &mut HashSet::new(),
604 ) else {
605 continue;
606 };
607
608 if declaration.name == "counter-reset" {
611 counter_resets = parse_counter_reset(&value);
612 continue;
613 }
614 if declaration.name == "counter-increment" {
615 counter_increments = parse_counter_increment(&value);
616 continue;
617 }
618
619 apply_declaration(
620 &declaration.name,
621 &value,
622 &mut style,
623 &mut container_style,
624 &mut text_style,
625 &mut text_flow_style,
626 &parent_style,
627 &parent_container_style,
628 &parent_text_style,
629 &parent_text_flow_style,
630 &mut overflow,
631 used_color_scheme,
632 );
633 collect_animation_decl(
634 &declaration.name,
635 &value,
636 &mut anim_effective,
637 &mut anim_transition,
638 &mut anim_animation,
639 );
640 }
641 }
642
643 let mut counter_state = stack[top_idx].counter_state.clone();
646 for (name, val) in &counter_resets {
647 counter_state.insert(name.clone(), *val);
648 }
649 for (name, amount) in &counter_increments {
650 *counter_state.entry(name.clone()).or_insert(0) += amount;
651 }
652 stack[top_idx].counter_state = counter_state.clone();
654
655 if let Some(style_attr) = html_node.get_attr("style") {
661 let inline_declarations = resolve_inline_style(style_attr);
662 for (name, value, important) in &inline_declarations {
663 if name.starts_with("--") {
664 let stylesheet_important = candidates
665 .as_ref()
666 .and_then(|c| c.get(name))
667 .is_some_and(|declaration| declaration.important);
668 if *important || !stylesheet_important {
669 set_inline_custom_property(
670 Arc::make_mut(&mut custom_properties),
671 name.clone(),
672 value.clone(),
673 *important,
674 );
675 }
676 }
677 }
678 for (name, value, important) in inline_declarations {
679 if name.starts_with("--") {
680 continue;
681 }
682 let stylesheet_important = candidates
683 .as_ref()
684 .and_then(|c| c.get(&name))
685 .is_some_and(|declaration| declaration.important);
686 if !important && stylesheet_important {
687 continue;
688 }
689 let Some(value) = DeclarationResolver::resolve_var(
690 &value,
691 &custom_properties,
692 &mut HashSet::new(),
693 ) else {
694 continue;
695 };
696 apply_declaration(
697 &name,
698 &value,
699 &mut style,
700 &mut container_style,
701 &mut text_style,
702 &mut text_flow_style,
703 &parent_style,
704 &parent_container_style,
705 &parent_text_style,
706 &parent_text_flow_style,
707 &mut overflow,
708 used_color_scheme,
709 );
710 collect_animation_decl(
711 &name,
712 &value,
713 &mut anim_effective,
714 &mut anim_transition,
715 &mut anim_animation,
716 );
717 }
718 }
719 #[cfg(any(feature = "profile", debug_assertions))]
720 {
721 apply_decl_time += apply_decl.elapsed();
722 }
723
724 if matches!(html_node, HtmlNodeType::Element { .. }) {
726 let transitions = anim_parse::build_transition_specs(&anim_transition);
727 let animations = anim_parse::build_animation_specs(&anim_animation);
728 let has_animation = animations
729 .iter()
730 .any(|a| a.name != "none" && !a.name.is_empty());
731 if !transitions.is_empty() || has_animation {
732 let mut base_values = std::collections::HashMap::new();
733 for key in [
734 "opacity",
735 "width",
736 "height",
737 "top",
738 "right",
739 "bottom",
740 "left",
741 "margin-top",
742 "margin-right",
743 "margin-bottom",
744 "margin-left",
745 "padding-top",
746 "padding-right",
747 "padding-bottom",
748 "padding-left",
749 "font-size",
750 "line-height",
751 "color",
752 "transform",
753 "background-color",
754 "border-top-color",
755 "border-right-color",
756 "border-bottom-color",
757 "border-left-color",
758 ] {
759 if let Some(v) = anim_effective.get(key) {
760 base_values.insert(key.to_string(), v.clone());
761 }
762 }
763
764 base_values
766 .entry("opacity".to_string())
767 .or_insert(CssValue::Number(container_style.opacity));
768 base_values.entry("color".to_string()).or_insert_with(|| {
769 CssValue::Color(format!(
770 "{:02x}{:02x}{:02x}{:02x}",
771 text_style.color.0,
772 text_style.color.1,
773 text_style.color.2,
774 text_style.color.3
775 ))
776 });
777 base_values
778 .entry("transform".to_string())
779 .or_insert(CssValue::Keyword("none".into()));
780 if let Some(color) = container_style.background.color {
781 base_values
782 .entry("background-color".to_string())
783 .or_insert_with(|| {
784 CssValue::Color(format!(
785 "{:02x}{:02x}{:02x}{:02x}",
786 color.0, color.1, color.2, color.3
787 ))
788 });
789 }
790
791 animation_inputs.insert(
792 node_id,
793 ElementAnimationState {
794 transitions,
795 animations,
796 base_values,
797 },
798 );
799 }
800 }
801
802 if let Some(overrides) = animated_overrides.get(&stack[top_idx].dom) {
804 for (name, value) in overrides {
805 let Some(value) = DeclarationResolver::resolve_var(
806 value,
807 &custom_properties,
808 &mut HashSet::new(),
809 ) else {
810 continue;
811 };
812 apply_declaration(
813 name,
814 &value,
815 &mut style,
816 &mut container_style,
817 &mut text_style,
818 &mut text_flow_style,
819 &parent_style,
820 &parent_container_style,
821 &parent_text_style,
822 &parent_text_flow_style,
823 &mut overflow,
824 used_color_scheme,
825 );
826 }
827 }
828
829 for layer in &mut container_style.background.layers {
830 if let Some(crate::engine::layouter::types::BackgroundImage::Image {
831 source,
832 image,
833 }) = &mut layer.image
834 {
835 *image = images.get(source).cloned();
836 }
837 }
838
839 if !style.position.kind.is_out_of_flow() {
842 style.float = match container_style.css_float {
843 CssFloat::None => LayoutFloat::None,
844 CssFloat::Left => LayoutFloat::Left,
845 CssFloat::Right => LayoutFloat::Right,
846 };
847 style.clear = match container_style.css_clear {
848 CssClear::None => LayoutClear::None,
849 CssClear::Left => LayoutClear::Left,
850 CssClear::Right => LayoutClear::Right,
851 CssClear::Both => LayoutClear::Both,
852 };
853
854 if style.float != LayoutFloat::None {
855 style.size.auto_behavior = AutoSizeBehavior::ShrinkToFit;
856 if matches!(
857 style.display.inner(),
858 Some(InnerDisplay::Flow) | Some(InnerDisplay::FlowRoot)
859 ) {
860 style.display = Display::OutsideInner {
861 outer: OuterDisplay::Inline,
862 inner: InnerDisplay::FlowRoot,
863 };
864 }
865 }
866 }
867
868 blockify_out_of_flow_positioned(&mut style);
871
872 style.line_height = match text_flow_style.line_height {
874 LineHeight::Number(factor) => Length::Px(text_flow_style.font_size * factor),
875 LineHeight::Normal => Length::Px(text_flow_style.font_size * DEFAULT_LINE_FACTOR),
876 LineHeight::Px(px) => Length::Px(px),
877 };
878 container_style.text_align = text_flow_style.text_align;
879
880 if html_node.tag_name() == Some("iframe") {
886 overflow.x = true;
887 overflow.y = true;
888 if matches!(style.size.width, LengthOrAuto::Auto) {
889 style.size.width = LengthOrAuto::Length(Length::Px(300.0));
890 }
891 if matches!(style.size.height, LengthOrAuto::Auto) {
892 style.size.height = LengthOrAuto::Length(Length::Px(150.0));
893 }
894 }
895
896 let svg_content = if html_node.tag_name() == Some("svg") {
901 collect_svg(
902 snapshot,
903 stack[top_idx].dom,
904 used_color_scheme,
905 text_style.color,
906 )
907 } else {
908 None
909 };
910
911 if let Some(SvgContent {
916 view_box: (_, _, vbw, vbh),
917 ..
918 }) = svg_content.as_ref()
919 && *vbw > 0.0
920 && *vbh > 0.0
921 && matches!(style.size.height, LengthOrAuto::Auto)
922 && let Some(width) = style.size.width.clone().length()
923 && let Length::Px(width_px) = width
924 && width_px > 0.0
925 {
926 style.size.height = LengthOrAuto::Length(Length::Px(width_px * *vbh / *vbw));
927 }
928
929 let is_svg_element = svg_content.is_some();
932
933 let child = Arc::new(InheritedCss {
934 custom_props: custom_properties,
935 text_style: text_style.clone(),
936 text_flow_style,
937 color_scheme: used_color_scheme,
938 });
939
940 if let HtmlNodeType::Text(_) = html_node {
941 unreachable!();
942 }
943
944 if let Some(tag) = html_node.tag_name()
946 && registry.tags().contains(&tag)
947 {
948 perf_scope!(custom_node);
949 style.size.auto_behavior = AutoSizeBehavior::ShrinkToFit;
952 let media_source = html_node.get_attr("src").map(str::to_string).or_else(|| {
953 snapshot
954 .children(stack[top_idx].dom)
955 .iter()
956 .find_map(|&child| {
957 let child = &snapshot.node(child).kind;
958 (child.tag_name() == Some("source"))
959 .then(|| child.get_attr("src").map(str::to_string))
960 .flatten()
961 })
962 });
963 let reused = existing_custom_nodes.get(&stack[top_idx].dom).cloned();
964 if let Some(node) = &reused {
965 let checked = snapshot.node(stack[top_idx].dom).checked;
966 node.set_checked(checked);
967 }
968 let node = reused.unwrap_or_else(|| {
969 registry
970 .create(&CustomNodeContext {
971 tag,
972 media_source: media_source.as_deref(),
973 container_style: &container_style,
974 text_style: &text_style,
975 measurer: Arc::clone(&measurer),
976 audio_sinks: &*audio_sinks,
977 images,
978 audio,
979 get_attr: &|name| html_node.get_attr(name).map(str::to_string),
980 write_back: write_back_sender
981 .as_ref()
982 .map(|sender| (sender.clone(), stack[top_idx].dom)),
983 dom_snapshot: snapshot,
984 dom_id: stack[top_idx].dom,
985 })
986 .expect("registry must handle every tag it reports")
987 });
988
989 let bridge = CustomNodeBridge::new_content(Arc::clone(&node), &style);
990 let container_opacity = container_style.opacity;
991 let kind = NodeKind::Custom {
992 node,
993 scroll_x: overflow.x,
994 scroll_y: overflow.y,
995 scroll_offset_x: 0.0,
996 scroll_offset_y: 0.0,
997 style: container_style,
998 layout_style: style.clone(),
999 text_style: text_style.clone(),
1000 text_flow_style,
1001 };
1002
1003 let mut layout_children: Vec<LayoutChild> = Vec::new();
1007 let mut info_children: Vec<InfoNode> = Vec::new();
1008 let add_pseudo_slot =
1009 |pseudo: &Properties,
1010 layout_children: &mut Vec<LayoutChild>,
1011 info_children: &mut Vec<InfoNode>| {
1012 let dom_node = &snapshot.node(stack[top_idx].dom);
1013 let get_attr = |name: &str| dom_node.kind.get_attr(name).map(|s| s.to_string());
1014 if let Some(ChildSlot::Inline(fragment, pseudo_info)) = build_pseudo_slot(
1015 pseudo,
1016 &child_css.custom_props,
1017 &text_style,
1018 text_flow_style,
1019 used_color_scheme,
1020 &*measurer,
1021 images,
1022 &get_attr,
1023 &stack[top_idx].counter_state,
1024 ) {
1025 layout_children.push(fragment);
1026 info_children.push(*pseudo_info);
1027 }
1028 };
1029 let hidden = style.display == Display::None;
1030 if !hidden && let Some(pseudo) = pseudo_candidates.get(&PseudoElement::Before) {
1031 add_pseudo_slot(pseudo, &mut layout_children, &mut info_children);
1032 }
1033 layout_children.push((style.clone(), bridge).into());
1034 info_children.push(InfoNode {
1038 kind: NodeKind::Container {
1039 scroll_x: false,
1040 scroll_y: false,
1041 scroll_offset_x: 0.0,
1042 scroll_offset_y: 0.0,
1043 style: ContainerStyle::default(),
1044 role: ContainerRole::Normal,
1045 },
1046 children: Vec::new(),
1047 dom_id: None,
1048 });
1049 if !hidden && let Some(pseudo) = pseudo_candidates.get(&PseudoElement::After) {
1050 add_pseudo_slot(pseudo, &mut layout_children, &mut info_children);
1051 }
1052
1053 let layout = LayoutNode::with_children(style.clone(), layout_children);
1054 let info = InfoNode {
1055 kind,
1056 children: info_children,
1057 dom_id: Some(stack[top_idx].dom),
1058 };
1059 let ptr = stack[top_idx].dom;
1060 if let std::collections::hash_map::Entry::Vacant(e) = animation_inputs.entry(ptr) {
1062 let transitions = anim_parse::build_transition_specs(&anim_transition);
1063 let animations = anim_parse::build_animation_specs(&anim_animation);
1064 let has_animation = animations
1065 .iter()
1066 .any(|a| a.name != "none" && !a.name.is_empty());
1067 if !transitions.is_empty() || has_animation {
1068 let mut base_values = std::collections::HashMap::new();
1069 for key in [
1070 "opacity",
1071 "width",
1072 "height",
1073 "top",
1074 "right",
1075 "bottom",
1076 "left",
1077 "margin-top",
1078 "margin-right",
1079 "margin-bottom",
1080 "margin-left",
1081 "padding-top",
1082 "padding-right",
1083 "padding-bottom",
1084 "padding-left",
1085 "font-size",
1086 "line-height",
1087 "color",
1088 "transform",
1089 "background-color",
1090 "border-top-color",
1091 "border-right-color",
1092 "border-bottom-color",
1093 "border-left-color",
1094 ] {
1095 if let Some(v) = anim_effective.get(key) {
1096 base_values.insert(key.to_string(), v.clone());
1097 }
1098 }
1099 base_values
1100 .entry("opacity".to_string())
1101 .or_insert(CssValue::Number(container_opacity));
1102 base_values.entry("color".to_string()).or_insert_with(|| {
1103 CssValue::Color(format!(
1104 "{:02x}{:02x}{:02x}{:02x}",
1105 text_style.color.0,
1106 text_style.color.1,
1107 text_style.color.2,
1108 text_style.color.3
1109 ))
1110 });
1111 base_values
1112 .entry("transform".to_string())
1113 .or_insert(CssValue::Keyword("none".into()));
1114 e.insert(ElementAnimationState {
1115 transitions,
1116 animations,
1117 base_values,
1118 });
1119 }
1120 }
1121 results.insert(ptr, (layout, info));
1122 #[cfg(any(feature = "profile", debug_assertions))]
1123 {
1124 custom_node_time += custom_node.elapsed();
1125 node_count += 1;
1126 }
1127 stack.pop();
1128 continue;
1129 }
1130
1131 let is_link = html_node.tag_name() == Some("a") && html_node.get_attr("href").is_some();
1133
1134 let role = match html_node.tag_name() {
1135 Some("table") => ContainerRole::Table,
1136 Some("thead" | "tbody" | "tfoot") => ContainerRole::TableRowGroup,
1137 Some("tr") => ContainerRole::TableRow,
1138 Some("td" | "th") => ContainerRole::TableCell,
1139 Some("caption") => ContainerRole::TableCaption,
1140 _ if is_link => ContainerRole::Link {
1141 href: html_node.get_attr("href").unwrap().to_string(),
1142 },
1143 _ => ContainerRole::Normal,
1144 };
1145
1146 let kind = if let Some(SvgContent {
1147 view_box, shapes, ..
1148 }) = svg_content
1149 {
1150 NodeKind::Svg {
1151 view_box,
1152 shapes,
1153 scroll_x: overflow.x,
1154 scroll_y: overflow.y,
1155 scroll_offset_x: 0.0,
1156 scroll_offset_y: 0.0,
1157 style: container_style,
1158 }
1159 } else {
1160 NodeKind::Container {
1161 scroll_x: overflow.x,
1162 scroll_y: overflow.y,
1163 scroll_offset_x: 0.0,
1164 scroll_offset_y: 0.0,
1165 style: container_style,
1166 role,
1167 }
1168 };
1169
1170 if let Some(tag) = html_node.tag_name() {
1172 match tag {
1173 "table" | "tbody" | "thead" | "tfoot" => {
1174 style.display = Display::OutsideInner {
1175 outer: OuterDisplay::Block,
1176 inner: InnerDisplay::Flex,
1177 };
1178 style.flex_direction = FlexDirection::Column;
1179 }
1180 "tr" => {
1181 style.display = Display::OutsideInner {
1182 outer: OuterDisplay::Block,
1183 inner: InnerDisplay::Flex,
1184 };
1185 style.flex_direction = FlexDirection::Row;
1186 }
1187 _ => {}
1188 }
1189 }
1190
1191 let mut child_slots: Vec<ChildSlot> = Vec::new();
1192 let mut element_kids: Vec<NodeId> = Vec::new();
1193
1194 perf_scope!(child_slot_build);
1195 if style.display != Display::None && !is_svg_element {
1198 if let Some(pseudo) = pseudo_candidates.get(&PseudoElement::Before) {
1200 let dom_node = &snapshot.node(stack[top_idx].dom);
1201 let get_attr = |name: &str| dom_node.kind.get_attr(name).map(|s| s.to_string());
1202 if let Some(slot) = build_pseudo_slot(
1203 pseudo,
1204 &child_css.custom_props,
1205 &text_style,
1206 text_flow_style,
1207 used_color_scheme,
1208 &*measurer,
1209 images,
1210 &get_attr,
1211 &stack[top_idx].counter_state,
1212 )
1213 {
1214 child_slots.push(slot);
1215 #[cfg(any(feature = "profile", debug_assertions))]
1216 {
1217 node_count += 1;
1218 }
1219 }
1220 }
1221 let parent_tag_name = snapshot.node(stack[top_idx].dom).kind.tag_name();
1222 for &child in snapshot.children(stack[top_idx].dom) {
1223 let child_node = &snapshot.node(child).kind;
1224 if let HtmlNodeType::Text(t) = child_node {
1225 let t = if parent_tag_name == Some("pre") {
1226 let t = t.strip_prefix('\n').unwrap_or(t);
1227 normalize_whitespace(t, text_flow_style.white_space)
1228 } else if t.chars().all(is_css_newline)
1229 && matches!(
1230 text_flow_style.white_space,
1231 WhiteSpace::Normal | WhiteSpace::Nowrap
1232 )
1233 {
1234 continue;
1235 } else {
1236 normalize_whitespace(t, text_flow_style.white_space)
1237 };
1238
1239 let t = match text_style.text_transform {
1240 TextTransform::None => t.into_owned(),
1241 TextTransform::Uppercase => t.to_ascii_uppercase(),
1242 TextTransform::Lowercase => t.to_ascii_lowercase(),
1243 };
1244 perf_scope!(text_layout);
1245 let (layouter, kind) =
1246 create_text_node(t, text_style.clone(), text_flow_style, &*measurer);
1247 #[cfg(any(feature = "profile", debug_assertions))]
1248 {
1249 text_layout_time += text_layout.elapsed();
1250 node_count += 1;
1251 }
1252 let mut inline_style = style.clone();
1253 inline_style.display = Display::OutsideInner {
1254 outer: OuterDisplay::Inline,
1255 inner: InnerDisplay::Flow,
1256 };
1257 child_slots.push(ChildSlot::Inline(
1258 (inline_style, layouter).into(),
1259 Box::new(InfoNode {
1260 kind,
1261 children: Vec::new(),
1262 dom_id: Some(child),
1263 }),
1264 ));
1265 } else if child_node.tag_name() == Some("br") {
1266 child_slots.push(ChildSlot::Inline(
1267 ItemFragment::LineBreak.into(),
1268 Box::new(InfoNode {
1269 kind: NodeKind::LineBreak,
1270 children: Vec::new(),
1271 dom_id: Some(child),
1272 }),
1273 ));
1274 } else if child_node.tag_name() == Some("noscript")
1275 && scripting_mode == ScriptingMode::Enabled
1276 {
1277 } else if child_node.tag_name() == Some("template")
1279 || matches!(child_node, HtmlNodeType::DocumentFragment)
1280 {
1281 } else {
1283 child_slots.push(ChildSlot::Element(element_kids.len()));
1284 element_kids.push(child);
1285 }
1286 }
1287
1288 if let Some(pseudo) = pseudo_candidates.get(&PseudoElement::After) {
1290 let dom_node = &snapshot.node(stack[top_idx].dom);
1291 let get_attr = |name: &str| dom_node.kind.get_attr(name).map(|s| s.to_string());
1292 if let Some(slot) = build_pseudo_slot(
1293 pseudo,
1294 &child_css.custom_props,
1295 &text_style,
1296 text_flow_style,
1297 used_color_scheme,
1298 &*measurer,
1299 images,
1300 &get_attr,
1301 &stack[top_idx].counter_state,
1302 )
1303 {
1304 child_slots.push(slot);
1305 #[cfg(any(feature = "profile", debug_assertions))]
1306 {
1307 node_count += 1;
1308 }
1309 }
1310 }
1311 }
1312 #[cfg(any(feature = "profile", debug_assertions))]
1313 {
1314 child_slot_build_time += child_slot_build.elapsed();
1315 }
1316
1317 if element_kids.is_empty() {
1318 perf_scope!(leaf_assemble);
1320 perf_scope!(whitespace_keep);
1321 let keep = compute_whitespace_keep(&child_slots, &[]);
1322 #[cfg(any(feature = "profile", debug_assertions))]
1323 {
1324 whitespace_keep_time += whitespace_keep.elapsed();
1325 }
1326 let (layout_children, info_children): (Vec<_>, Vec<_>) = child_slots
1327 .into_iter()
1328 .enumerate()
1329 .filter_map(|(i, slot)| {
1330 if !keep[i] {
1331 return None;
1332 }
1333 match slot {
1334 ChildSlot::Inline(layout, info) => Some((layout, *info)),
1335 ChildSlot::Element(_) => None,
1336 }
1337 })
1338 .unzip();
1339 let layout = LayoutNode::with_children(style.clone(), layout_children);
1340 let info = InfoNode {
1341 kind,
1342 children: info_children,
1343 dom_id: Some(stack[top_idx].dom),
1344 };
1345 let ptr = stack[top_idx].dom;
1346 results.insert(ptr, (layout, info));
1347 #[cfg(any(feature = "profile", debug_assertions))]
1348 {
1349 leaf_assemble_time += leaf_assemble.elapsed();
1350 node_count += 1;
1351 }
1352 stack.pop();
1353 } else {
1354 perf_scope!(push_children);
1356 let parent_chain = stack[top_idx].chain.clone();
1357 let parent_container = match &kind {
1358 NodeKind::Container { style, .. } => style.clone(),
1359 _ => ContainerStyle::default(),
1360 };
1361 stack[top_idx].kind = Some(kind);
1362 stack[top_idx].parent_style = style.clone();
1363 stack[top_idx].parent_container_style = parent_container;
1364 stack[top_idx].style = Some(style);
1365 stack[top_idx].child = child;
1366 stack[top_idx].child_slots = child_slots;
1367 stack[top_idx].element_children = element_kids;
1368
1369 let kids_for_push: Vec<_> = {
1373 let f = &stack[top_idx];
1374 f.element_children.clone()
1375 };
1376 let child_css = Arc::clone(&stack[top_idx].child);
1377 perf_scope!(sibling_info);
1378 let parent_lang = stack[top_idx]
1379 .lang
1380 .clone()
1381 .or_else(|| own_lang(snapshot, stack[top_idx].dom));
1382 let mut kid_infos = element_sibling_infos(
1383 snapshot,
1384 &kids_for_push,
1385 parent_lang.as_deref(),
1386 interaction,
1387 );
1388 if !is_element_node(snapshot, stack[top_idx].dom) {
1389 for info in kid_infos.iter_mut().flatten() {
1390 info.has_parent = false;
1391 }
1392 }
1393 #[cfg(any(feature = "profile", debug_assertions))]
1394 {
1395 sibling_info_time += sibling_info.elapsed();
1396 }
1397 let parent_style_for_children = stack[top_idx].parent_style.clone();
1398 let parent_container_for_children = stack[top_idx].parent_container_style.clone();
1399 for (&kid, info) in kids_for_push.iter().zip(kid_infos).rev() {
1400 let kid_lang = info.as_ref().and_then(|info| info.lang.clone());
1401 stack.push(StackFrame {
1402 dom: kid,
1403 lang: kid_lang,
1404 chain: parent_chain.prepend(info),
1405 child: Arc::clone(&child_css),
1406 kind: None,
1407 style: None,
1408 parent_style: parent_style_for_children.clone(),
1409 parent_container_style: parent_container_for_children.clone(),
1410 child_slots: Vec::new(),
1411 element_children: Vec::new(),
1412 counter_state: stack[top_idx].counter_state.clone(),
1413 });
1414 }
1415 #[cfg(any(feature = "profile", debug_assertions))]
1416 {
1417 push_children_time += push_children.elapsed();
1418 }
1419 }
1420 } else {
1421 perf_scope!(exit_phase);
1424 perf_scope!(exit_setup);
1425
1426 let frame = stack.swap_remove(top_idx);
1427
1428 let style = frame.style.as_ref().unwrap().clone();
1429 let kind = frame.kind.as_ref().unwrap().clone();
1430
1431 let mut element_results: Vec<(LayoutChild, InfoNode)> = Vec::new();
1433
1434 for &kid in &frame.element_children {
1435 if let Some((child_layout, child_info)) = results.remove(&kid) {
1436 element_results.push((child_layout.into(), child_info));
1437 }
1438 }
1439
1440 let mut final_kind = kind;
1442 if snapshot.node(frame.dom).kind.tag_name() == Some("html") {
1443 let should_inherit = final_kind.is_container_with_transparent_bg();
1444 if should_inherit {
1445 for (_, child_info) in &element_results {
1446 if child_info
1447 .dom_id
1448 .and_then(|id| snapshot.node(id).kind.tag_name())
1449 == Some("body")
1450 {
1451 let child_bg = child_info.kind.container_bg();
1452 if let Some(bg) = child_bg
1453 && let NodeKind::Container { ref mut style, .. } = final_kind
1454 {
1455 style.background = bg.clone();
1456 }
1457 }
1458 }
1459 }
1460 }
1461
1462 #[cfg(any(feature = "profile", debug_assertions))]
1463 {
1464 exit_setup_time += exit_setup.elapsed();
1465 }
1466
1467 perf_scope!(exit_build);
1468 let mut element_results: Vec<_> = element_results.into_iter().map(Some).collect();
1469
1470 perf_scope!(whitespace_keep);
1475 let keep = compute_whitespace_keep(&frame.child_slots, &element_results);
1476 #[cfg(any(feature = "profile", debug_assertions))]
1477 {
1478 whitespace_keep_time += whitespace_keep.elapsed();
1479 }
1480
1481 let mut all_layout: Vec<LayoutChild> = Vec::with_capacity(frame.child_slots.len());
1482 let mut all_info: Vec<InfoNode> = Vec::with_capacity(frame.child_slots.len());
1483
1484 for (i, slot) in frame.child_slots.into_iter().enumerate() {
1485 if !keep[i] {
1486 continue;
1487 }
1488 let (lc, ic) = match slot {
1489 ChildSlot::Inline(layout, info) => (layout, *info),
1490 ChildSlot::Element(index) => element_results[index]
1491 .take()
1492 .expect("element child result must exist"),
1493 };
1494 all_layout.push(lc);
1495 all_info.push(ic);
1496 }
1497
1498 let keep: Vec<bool> = (0..all_layout.len())
1503 .map(|index| {
1504 let white_space = match &all_info[index].kind {
1505 NodeKind::Text { flow_style, .. } => flow_style.white_space,
1506 _ => WhiteSpace::Normal,
1507 };
1508 let collapsible = is_collapsible_whitespace_info(&all_info[index], white_space);
1509 let next_to_block = index == 0
1510 || index + 1 == all_layout.len()
1511 || index
1512 .checked_sub(1)
1513 .is_some_and(|previous| is_block_layout_child(&all_layout[previous]))
1514 || all_layout.get(index + 1).is_some_and(is_block_layout_child);
1515 !(collapsible && next_to_block)
1516 })
1517 .collect();
1518 all_layout = all_layout
1519 .into_iter()
1520 .zip(&keep)
1521 .filter_map(|(layout, keep)| keep.then_some(layout))
1522 .collect();
1523 all_info = all_info
1524 .into_iter()
1525 .zip(keep)
1526 .filter_map(|(info, keep)| keep.then_some(info))
1527 .collect();
1528
1529 if matches!(
1534 style.display.inner(),
1535 Some(InnerDisplay::Grid | InnerDisplay::Flex)
1536 ) {
1537 if style.display.inner() == Some(InnerDisplay::Grid) {
1538 let columns = explicit_grid_track_count(&style.grid_template_columns);
1539 let rows = explicit_grid_track_count(&style.grid_template_rows);
1540 for child in &mut all_layout {
1541 if let LayoutChild::Node(child) = child {
1542 resolve_named_grid_area(child, &style.grid_template_areas);
1543 resolve_grid_end_line(&mut child.style.grid_column, columns);
1544 resolve_grid_end_line(&mut child.style.grid_row, rows);
1545 }
1546 }
1547 }
1548 for child in &mut all_layout {
1549 if let LayoutChild::Node(child) = child
1550 && child.style.display.outer() == Some(OuterDisplay::Inline)
1551 && !child.style.position.kind.is_out_of_flow()
1552 && let Display::OutsideInner { inner, .. } = child.style.display
1553 {
1554 child.style.display = Display::OutsideInner {
1555 outer: OuterDisplay::Block,
1556 inner,
1557 };
1558 }
1559 }
1560 }
1561
1562 let layout = LayoutNode::with_children(style, all_layout);
1563 let info = InfoNode {
1564 kind: final_kind,
1565 children: all_info,
1566 dom_id: Some(frame.dom),
1567 };
1568 let ptr = frame.dom;
1569 results.insert(ptr, (layout, info));
1570 #[cfg(any(feature = "profile", debug_assertions))]
1571 {
1572 exit_build_time += exit_build.elapsed();
1573 exit_phase_time += exit_phase.elapsed();
1574 node_count += 1;
1575 }
1576 }
1577 }
1578
1579 profile_log!(
1580 target: "LayoutRun",
1581 log::Level::Info,
1582 "[LayoutMetrics] total: {:?} (nodes: {})",
1583 total.elapsed(),
1584 node_count,
1585 );
1586 profile_log!(
1587 target: "LayoutRun",
1588 log::Level::Info,
1589 "[LayoutMetrics] css_match: {:?} | apply_decl: {:?}",
1590 css_match_time,
1591 apply_decl_time,
1592 );
1593 profile_log!(
1594 target: "LayoutRun",
1595 log::Level::Info,
1596 "[LayoutCandidates] elements: {} | examined: {} | matched: {}",
1597 cand_stats.elements_checked,
1598 cand_stats.candidates_examined,
1599 cand_stats.selectors_matched,
1600 );
1601 profile_log!(
1602 target: "LayoutRun",
1603 log::Level::Info,
1604 "[LayoutCandidates] query_time: {:?} | sel_match_time: {:?} | insert_time: {:?}",
1605 cand_stats.query_candidates_time,
1606 cand_stats.selector_match_time,
1607 cand_stats.cascade_insert_time,
1608 );
1609 profile_log!(
1610 target: "LayoutRun",
1611 log::Level::Info,
1612 "[LayoutMetrics] custom_node: {:?} | text_layout: {:?} | exit_phase: {:?}",
1613 custom_node_time,
1614 text_layout_time,
1615 exit_phase_time,
1616 );
1617 profile_log!(
1618 target: "LayoutRun",
1619 log::Level::Info,
1620 "[LayoutMetrics] sibling_info: {:?} | whitespace_keep: {:?}",
1621 sibling_info_time,
1622 whitespace_keep_time,
1623 );
1624 profile_log!(
1625 target: "LayoutRun",
1626 log::Level::Info,
1627 "[LayoutMetrics] enter_prep: {:?} | child_slot_build: {:?} | leaf_assemble: {:?}",
1628 enter_prep_time,
1629 child_slot_build_time,
1630 leaf_assemble_time,
1631 );
1632 profile_log!(
1633 target: "LayoutRun",
1634 log::Level::Info,
1635 "[LayoutMetrics] push_children: {:?} | exit_setup: {:?} | exit_build: {:?}",
1636 push_children_time,
1637 exit_setup_time,
1638 exit_build_time,
1639 );
1640 profile_log!(
1641 target: "LayoutRun",
1642 log::Level::Info,
1643 "[LayoutMetrics] rest: {:?}",
1644 total.elapsed().saturating_sub(
1645 css_match_time
1646 + apply_decl_time
1647 + custom_node_time
1648 + enter_prep_time
1649 + child_slot_build_time
1650 + leaf_assemble_time
1651 + push_children_time
1652 + exit_setup_time
1653 + exit_build_time
1654 ),
1655 );
1656
1657 let (layout, info_node) = results
1658 .remove(&root)
1659 .expect("root must have been processed");
1660 (layout, info_node, animation_inputs)
1661}
1662
1663fn collect_animation_decl(
1664 name: &str,
1665 value: &CssValue,
1666 effective: &mut std::collections::HashMap<String, CssValue>,
1667 transition: &mut anim_parse::TransitionInputs,
1668 animation: &mut anim_parse::AnimationInputs,
1669) {
1670 let lname = name.to_ascii_lowercase();
1671 match lname.as_str() {
1672 "transition-property" => transition.property = Some(value.clone()),
1673 "transition-duration" => transition.duration = Some(value.clone()),
1674 "transition-delay" => transition.delay = Some(value.clone()),
1675 "transition-timing-function" => transition.timing_function = Some(value.clone()),
1676 "transition" => {
1677 let groups = split_css_comma_groups(value);
1679 let mut props: Vec<CssValue> = Vec::new();
1680 let mut durs: Vec<CssValue> = Vec::new();
1681 let mut delays: Vec<CssValue> = Vec::new();
1682 let mut easings: Vec<CssValue> = Vec::new();
1683 for group in groups {
1684 let mut prop: Option<CssValue> = None;
1685 let mut times: Vec<CssValue> = Vec::new();
1686 let mut easing: Option<CssValue> = None;
1687 for item in flatten_css_value_items(&group) {
1688 if anim_parse::parse_time_ms(&item).is_some() {
1689 if times.len() < 2 {
1690 times.push(item);
1691 }
1692 continue;
1693 }
1694 if easing.is_none() && anim_parse::parse_easing(&item).is_some() {
1695 easing = Some(item);
1696 continue;
1697 }
1698 if prop.is_none()
1699 && let CssValue::Keyword(_) | CssValue::String(_) = item
1700 {
1701 prop = Some(item);
1702 }
1703 }
1704 props.push(prop.unwrap_or_else(|| CssValue::Keyword("all".into())));
1705 durs.push(
1706 times
1707 .first()
1708 .cloned()
1709 .unwrap_or(CssValue::Length(0.0, Unit::S)),
1710 );
1711 delays.push(
1712 times
1713 .get(1)
1714 .cloned()
1715 .unwrap_or(CssValue::Length(0.0, Unit::S)),
1716 );
1717 easings.push(easing.unwrap_or_else(|| CssValue::Keyword("ease".into())));
1718 }
1719 if !props.is_empty() {
1720 transition.property = Some(join_css_comma_groups(props));
1721 transition.duration = Some(join_css_comma_groups(durs));
1722 transition.delay = Some(join_css_comma_groups(delays));
1723 transition.timing_function = Some(join_css_comma_groups(easings));
1724 }
1725 }
1726 "animation-name" => animation.name = Some(value.clone()),
1727 "animation-duration" => animation.duration = Some(value.clone()),
1728 "animation-delay" => animation.delay = Some(value.clone()),
1729 "animation-timing-function" => animation.timing_function = Some(value.clone()),
1730 "animation-iteration-count" => animation.iteration_count = Some(value.clone()),
1731 "animation-direction" => animation.direction = Some(value.clone()),
1732 "animation-fill-mode" => animation.fill_mode = Some(value.clone()),
1733 "animation-play-state" => animation.play_state = Some(value.clone()),
1734 "animation" => {
1735 let groups = split_css_comma_groups(value);
1737 let mut names: Vec<CssValue> = Vec::new();
1738 let mut durs: Vec<CssValue> = Vec::new();
1739 let mut delays: Vec<CssValue> = Vec::new();
1740 let mut easings: Vec<CssValue> = Vec::new();
1741 let mut iters: Vec<CssValue> = Vec::new();
1742 let mut dirs: Vec<CssValue> = Vec::new();
1743 let mut fills: Vec<CssValue> = Vec::new();
1744 let mut states: Vec<CssValue> = Vec::new();
1745 for group in groups {
1746 let mut name: Option<CssValue> = None;
1747 let mut times: Vec<CssValue> = Vec::new();
1748 let mut easing: Option<CssValue> = None;
1749 let mut iter: Option<CssValue> = None;
1750 let mut dir: Option<CssValue> = None;
1751 let mut fill: Option<CssValue> = None;
1752 let mut state: Option<CssValue> = None;
1753 for item in flatten_css_value_items(&group) {
1754 if anim_parse::parse_time_ms(&item).is_some() && times.len() < 2 {
1755 times.push(item);
1756 continue;
1757 }
1758 if easing.is_none() && anim_parse::parse_easing(&item).is_some() {
1759 easing = Some(item);
1760 continue;
1761 }
1762 if iter.is_none() && anim_parse::parse_iteration_count(&item).is_some() {
1763 iter = Some(item);
1764 continue;
1765 }
1766 if dir.is_none() && anim_parse::parse_direction(&item).is_some() {
1767 dir = Some(item);
1768 continue;
1769 }
1770 if fill.is_none() && anim_parse::parse_fill_mode(&item).is_some() {
1771 fill = Some(item);
1772 continue;
1773 }
1774 if state.is_none() && anim_parse::parse_play_state(&item).is_some() {
1775 state = Some(item);
1776 continue;
1777 }
1778 if name.is_none()
1779 && let CssValue::Keyword(_) | CssValue::String(_) = item
1780 {
1781 name = Some(item);
1782 }
1783 }
1784 names.push(name.unwrap_or_else(|| CssValue::Keyword("none".into())));
1785 durs.push(
1786 times
1787 .first()
1788 .cloned()
1789 .unwrap_or(CssValue::Length(0.0, Unit::S)),
1790 );
1791 delays.push(
1792 times
1793 .get(1)
1794 .cloned()
1795 .unwrap_or(CssValue::Length(0.0, Unit::S)),
1796 );
1797 easings.push(easing.unwrap_or_else(|| CssValue::Keyword("ease".into())));
1798 iters.push(iter.unwrap_or(CssValue::Number(1.0)));
1799 dirs.push(dir.unwrap_or_else(|| CssValue::Keyword("normal".into())));
1800 fills.push(fill.unwrap_or_else(|| CssValue::Keyword("none".into())));
1801 states.push(state.unwrap_or_else(|| CssValue::Keyword("running".into())));
1802 }
1803 if !names.is_empty() {
1804 animation.name = Some(join_css_comma_groups(names));
1805 animation.duration = Some(join_css_comma_groups(durs));
1806 animation.delay = Some(join_css_comma_groups(delays));
1807 animation.timing_function = Some(join_css_comma_groups(easings));
1808 animation.iteration_count = Some(join_css_comma_groups(iters));
1809 animation.direction = Some(join_css_comma_groups(dirs));
1810 animation.fill_mode = Some(join_css_comma_groups(fills));
1811 animation.play_state = Some(join_css_comma_groups(states));
1812 }
1813 }
1814 _ => {}
1815 }
1816
1817 if matches!(
1818 lname.as_str(),
1819 "opacity"
1820 | "width"
1821 | "height"
1822 | "top"
1823 | "right"
1824 | "bottom"
1825 | "left"
1826 | "margin-top"
1827 | "margin-right"
1828 | "margin-bottom"
1829 | "margin-left"
1830 | "padding-top"
1831 | "padding-right"
1832 | "padding-bottom"
1833 | "padding-left"
1834 | "font-size"
1835 | "line-height"
1836 | "color"
1837 | "transform"
1838 | "background-color"
1839 | "border-top-color"
1840 | "border-right-color"
1841 | "border-bottom-color"
1842 | "border-left-color"
1843 ) {
1844 effective.insert(lname, value.clone());
1845 }
1846}
1847
1848fn split_css_comma_groups(value: &CssValue) -> Vec<Vec<CssValue>> {
1849 match value {
1850 CssValue::List(items) => {
1851 let mut groups: Vec<Vec<CssValue>> = vec![Vec::new()];
1852 for item in items {
1853 if matches!(item, CssValue::Keyword(k) if k == ",") {
1854 groups.push(Vec::new());
1855 } else {
1856 groups.last_mut().unwrap().push(item.clone());
1857 }
1858 }
1859 if groups.is_empty() {
1860 vec![vec![]]
1861 } else {
1862 groups
1863 }
1864 }
1865 other => vec![vec![other.clone()]],
1866 }
1867}
1868
1869fn flatten_css_value_items(items: &[CssValue]) -> Vec<CssValue> {
1870 let mut out = Vec::new();
1871 for item in items {
1872 match item {
1873 CssValue::List(inner) => out.extend(inner.clone()),
1874 _ => out.push(item.clone()),
1875 }
1876 }
1877 out
1878}
1879
1880fn join_css_comma_groups(groups: Vec<CssValue>) -> CssValue {
1881 if groups.len() <= 1 {
1882 return groups
1883 .into_iter()
1884 .next()
1885 .unwrap_or(CssValue::Keyword("none".into()));
1886 }
1887 let mut out: Vec<CssValue> = Vec::new();
1888 for (i, g) in groups.into_iter().enumerate() {
1889 if i > 0 {
1890 out.push(CssValue::Keyword(",".into()));
1891 }
1892 out.push(g);
1893 }
1894 CssValue::List(out)
1895}
1896
1897fn is_css_whitespace(c: char) -> bool {
1900 matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0c')
1901}
1902
1903fn is_css_newline(c: char) -> bool {
1904 matches!(c, '\n' | '\r')
1905}
1906
1907fn is_collapsible_whitespace_text(info: &InfoNode) -> bool {
1910 matches!(
1911 &info.kind,
1912 NodeKind::Text {
1913 text, flow_style, ..
1914 } if text.chars().all(is_css_whitespace)
1915 && matches!(
1916 flow_style.white_space,
1917 WhiteSpace::Normal | WhiteSpace::Nowrap
1918 )
1919 )
1920}
1921
1922#[derive(PartialEq)]
1924enum Neighbour {
1925 None,
1927 Block,
1929 LineBreak,
1931 Inline,
1933}
1934
1935fn neighbour(
1942 slots: &[ChildSlot],
1943 element_results: &[Option<(LayoutChild, InfoNode)>],
1944 slot_index: usize,
1945 step: isize,
1946 skip_line_break: bool,
1947) -> Neighbour {
1948 let mut idx = slot_index;
1949
1950 loop {
1951 let Some(next) = (if step < 0 {
1952 idx.checked_sub(1)
1953 } else {
1954 idx.checked_add(1)
1955 }) else {
1956 return Neighbour::None;
1957 };
1958 if next >= slots.len() {
1959 return Neighbour::None;
1960 }
1961 idx = next;
1962
1963 match &slots[idx] {
1964 ChildSlot::Element(element_index) => {
1965 let Some((child, _)) = element_results[*element_index].as_ref() else {
1966 continue;
1967 };
1968
1969 if matches!(
1971 child,
1972 LayoutChild::Node(node)
1973 if node.style.display == Display::None
1974 ) {
1975 continue;
1976 }
1977
1978 return if matches!(
1979 child,
1980 LayoutChild::Node(node)
1981 if node.style.display.outer() == Some(OuterDisplay::Block)
1982 ) {
1983 Neighbour::Block
1984 } else {
1985 Neighbour::Inline
1986 };
1987 }
1988 ChildSlot::Inline(_, info) => {
1989 if matches!(info.kind, NodeKind::LineBreak) {
1990 if !skip_line_break {
1991 return Neighbour::LineBreak;
1992 }
1993 } else if !is_collapsible_whitespace_text(info) {
1994 return Neighbour::Inline;
1995 }
1996 }
1997 }
1998 }
1999}
2000
2001fn compute_whitespace_keep(
2007 slots: &[ChildSlot],
2008 element_results: &[Option<(LayoutChild, InfoNode)>],
2009) -> Vec<bool> {
2010 (0..slots.len())
2011 .map(|i| match &slots[i] {
2012 ChildSlot::Inline(_, info) => {
2013 if !is_collapsible_whitespace_text(info) {
2014 return true;
2015 }
2016 let side_drops = |step: isize| {
2019 neighbour(slots, element_results, i, step, true) == Neighbour::Block
2020 || neighbour(slots, element_results, i, step, false) == Neighbour::LineBreak
2021 };
2022 !(side_drops(-1) || side_drops(1))
2023 }
2024 ChildSlot::Element(_) => true,
2025 })
2026 .collect()
2027}
2028
2029fn explicit_grid_track_count(tracks: &[GridTrack]) -> usize {
2032 tracks
2033 .iter()
2034 .map(|track| match track {
2035 GridTrack::Repeat(GridRepeat::Count(count), pattern) => {
2036 count.saturating_mul(explicit_grid_track_count(pattern))
2037 }
2038 GridTrack::Repeat(GridRepeat::AutoFit | GridRepeat::AutoFill, _) => 0,
2039 _ => 1,
2040 })
2041 .sum()
2042}
2043
2044fn resolve_grid_end_line(placement: &mut GridPlacement, track_count: usize) {
2045 if !matches!(placement.end, GRID_LINE_TO_END) || track_count == 0 {
2046 return;
2047 }
2048
2049 placement.end = GridPlacementEnd::Line(track_count + 1);
2050}
2051
2052fn resolve_named_grid_area(node: &mut LayoutNode, areas: &[Vec<String>]) {
2053 let Some(name) = node.style.grid_area.as_deref() else {
2054 return;
2055 };
2056 let mut min_column = usize::MAX;
2057 let mut max_column = 0;
2058 let mut min_row = usize::MAX;
2059 let mut max_row = 0;
2060 for (row, names) in areas.iter().enumerate() {
2061 for (column, area) in names.iter().enumerate() {
2062 if area == name {
2063 min_column = min_column.min(column);
2064 max_column = max_column.max(column);
2065 min_row = min_row.min(row);
2066 max_row = max_row.max(row);
2067 }
2068 }
2069 }
2070 if min_column == usize::MAX {
2071 return;
2072 }
2073 node.style.grid_column = GridPlacement {
2074 start: Some(min_column + 1),
2075 end: GridPlacementEnd::Line(max_column + 2),
2076 };
2077 node.style.grid_row = GridPlacement {
2078 start: Some(min_row + 1),
2079 end: GridPlacementEnd::Line(max_row + 2),
2080 };
2081 node.style.grid_area = None;
2082}
2083
2084pub fn normalize_whitespace(text: &str, white_space: WhiteSpace) -> Cow<'_, str> {
2094 let is_pre_like = matches!(
2097 white_space,
2098 WhiteSpace::Pre | WhiteSpace::PreWrap | WhiteSpace::BreakSpaces
2099 );
2100 let has_segment_break = text.as_bytes().iter().any(|&b| b == b'\r' || b == b'\x0c');
2101
2102 if is_pre_like {
2103 if !has_segment_break {
2104 return Cow::Borrowed(text);
2105 }
2106 let mut result = String::with_capacity(text.len());
2108 let mut chars = text.chars().peekable();
2109 while let Some(c) = chars.next() {
2110 match c {
2111 '\r' => {
2112 if chars.peek() == Some(&'\n') {
2113 chars.next();
2114 }
2115 result.push('\n');
2116 }
2117 '\x0c' => result.push('\n'),
2118 _ => result.push(c),
2119 }
2120 }
2121 return Cow::Owned(result);
2122 }
2123
2124 let needs_rewrite = has_segment_break || {
2130 let mut prev_was_space = false;
2131 let mut rewrite = false;
2132 for c in text.chars() {
2133 let is_space = is_css_whitespace(c);
2134 if is_space && (prev_was_space || c != ' ') {
2135 rewrite = true;
2136 break;
2137 }
2138 prev_was_space = is_space;
2139 }
2140 rewrite
2141 };
2142
2143 if !needs_rewrite {
2144 return Cow::Borrowed(text);
2145 }
2146
2147 let text = text.replace("\r\n", "\n");
2148 let text = text.replace(['\r', '\x0c'], "\n");
2149
2150 let mut result = String::new();
2151 let mut prev_was_space = false;
2152
2153 for c in text.chars() {
2154 match white_space {
2155 WhiteSpace::Normal | WhiteSpace::Nowrap => {
2156 if is_css_whitespace(c) {
2157 if !prev_was_space {
2158 result.push(' ');
2159 }
2160 prev_was_space = true;
2161 } else {
2162 result.push(c);
2163 prev_was_space = false;
2164 }
2165 }
2166
2167 WhiteSpace::Pre | WhiteSpace::PreWrap | WhiteSpace::BreakSpaces => {
2168 result.push(c);
2169 prev_was_space = false;
2170 }
2171
2172 WhiteSpace::PreLine => {
2173 if c == '\n' {
2174 result.push('\n');
2175 prev_was_space = false;
2176 } else if is_css_whitespace(c) {
2177 if !prev_was_space {
2178 result.push(' ');
2179 }
2180 prev_was_space = true;
2181 } else {
2182 result.push(c);
2183 prev_was_space = false;
2184 }
2185 }
2186 }
2187 }
2188
2189 Cow::Owned(result)
2190}
2191
2192fn create_text_node(
2196 text: String,
2197 text_style: TextStyle,
2198 text_flow_style: TextFlowStyle,
2199 measurer: &dyn text::TextMeasurer,
2200) -> (TextFlowLayouter, NodeKind) {
2201 perf_scope!(measure);
2202 let request = text::TextMeasureRequest {
2203 text: text.clone(),
2204 attribute: text::TextAttribute {
2205 style: text_style.clone(),
2206 flow_style: text_flow_style,
2207 },
2208 };
2209 let clusters = measurer.measure_shaped(&request).unwrap_or_else(|_| {
2210 measurer
2211 .measure(&request)
2212 .map(|ms| {
2213 let mut offset = 0usize;
2214 ms.into_iter()
2215 .map(|f| {
2216 let pos = text[offset..]
2218 .find(&f.text)
2219 .map(|p| offset + p)
2220 .unwrap_or(offset);
2221 offset = pos + f.text.len();
2222 GlyphCluster {
2223 byte_offset: pos,
2224 width: f.width,
2225 break_allowed: true,
2226 }
2227 })
2228 .collect()
2229 })
2230 .unwrap_or_default()
2231 });
2232 profile_log!(
2233 target: "Layouter",
2234 log::Level::Info,
2235 "measure_shaped: text={:?} len={} took={:?}",
2236 crate::profile::text_preview(&text),
2237 text.len(),
2238 measure.elapsed(),
2239 );
2240
2241 let layouter = TextFlowLayouter::new(text.clone(), text_flow_style, clusters);
2242 let kind = NodeKind::Text {
2243 text,
2244 style: text_style,
2245 flow_style: text_flow_style,
2246 text_id: layouter.id,
2247 };
2248 (layouter, kind)
2249}
2250
2251#[allow(clippy::too_many_arguments)]
2259fn build_pseudo_slot(
2260 declarations: &Properties,
2261 custom_properties: &Properties,
2262 inherited_text_style: &TextStyle,
2263 inherited_text_flow_style: TextFlowStyle,
2264 color_scheme: ColorScheme,
2265 measurer: &dyn text::TextMeasurer,
2266 images: &HashMap<String, Image>,
2267 get_attr: &dyn Fn(&str) -> Option<String>,
2268 counter_state: &HashMap<String, i32>,
2269) -> Option<ChildSlot> {
2270 let mut style = Style::default();
2271 let mut container_style = ContainerStyle::default();
2272 let mut text_style = inherited_text_style.clone();
2273 let mut text_flow_style = inherited_text_flow_style;
2274 let mut overflow = Overflow::default();
2275
2276 let mut cascade: Vec<_> = declarations.values().collect();
2279 cascade.sort_by_key(|declaration| declaration.order);
2280
2281 let mut content: Option<CssValue> = None;
2282 for declaration in cascade {
2283 if declaration.name.starts_with("--") {
2284 continue;
2285 }
2286 if declaration.name == "content" {
2287 content = Some(declaration.value.clone());
2288 continue;
2289 }
2290 let Some(value) = DeclarationResolver::resolve_var(
2291 &declaration.value,
2292 custom_properties,
2293 &mut HashSet::new(),
2294 ) else {
2295 continue;
2296 };
2297 apply_declaration(
2298 &declaration.name,
2299 &value,
2300 &mut style,
2301 &mut container_style,
2302 &mut text_style,
2303 &mut text_flow_style,
2304 &Style::default(),
2305 &ContainerStyle::default(),
2306 inherited_text_style,
2307 &inherited_text_flow_style,
2308 &mut overflow,
2309 color_scheme,
2310 );
2311 }
2312
2313 let content_text = match &content {
2315 None => String::new(),
2316 Some(CssValue::Keyword(k))
2317 if k.eq_ignore_ascii_case("none") || k.eq_ignore_ascii_case("normal") =>
2318 {
2319 String::new()
2320 }
2321 Some(CssValue::Keyword(k)) => match k.to_ascii_lowercase().as_str() {
2322 "open-quote" => "\"".to_string(),
2323 "close-quote" => "\"".to_string(),
2324 "no-open-quote" | "no-close-quote" => String::new(),
2325 _ => String::new(),
2326 },
2327 Some(CssValue::String(s)) => s.clone(),
2328 Some(CssValue::Function(fn_name, args)) => {
2329 resolve_content_function(fn_name, args, get_attr, counter_state)
2330 }
2331 Some(CssValue::List(items)) => {
2332 let mut joined = String::new();
2335 for item in items {
2336 match item {
2337 CssValue::String(s) => joined.push_str(s.as_str()),
2338 CssValue::Keyword(k) => match k.to_ascii_lowercase().as_str() {
2339 "open-quote" => joined.push_str("\""),
2340 "close-quote" => joined.push_str("\""),
2341 "no-open-quote" | "no-close-quote" => {}
2342 _ => {}
2343 },
2344 CssValue::Function(fn_name, args) => {
2345 let resolved =
2346 resolve_content_function(fn_name, args, get_attr, counter_state);
2347 if !resolved.is_empty() {
2348 joined.push_str(&resolved);
2349 }
2350 }
2351 _ => {}
2352 }
2353 }
2354 joined
2355 }
2356 Some(_) => String::new(),
2357 };
2358
2359 if let Some(CssValue::Function(fn_name, args)) = &content
2362 && fn_name.eq_ignore_ascii_case("url")
2363 {
2364 for arg_group in args.iter().flatten() {
2365 if let CssValue::String(source) = arg_group {
2366 container_style.background.layers.push(
2367 crate::engine::layouter::types::BackgroundLayer {
2368 image: Some(
2369 crate::engine::layouter::types::BackgroundImage::Image {
2370 source: source.clone(),
2371 image: None,
2372 },
2373 ),
2374 position: Default::default(),
2375 size: Default::default(),
2376 repeat: Default::default(),
2377 },
2378 );
2379 break;
2380 }
2381 }
2382 }
2383
2384 let has_styling = container_style.background.color.is_some_and(|c| c.3 > 0)
2385 || container_style
2386 .background
2387 .layers
2388 .iter()
2389 .any(|layer| layer.image.is_some())
2390 || matches!(
2391 (&style.size.width, &style.size.height),
2392 (LengthOrAuto::Length(l), _) | (_, LengthOrAuto::Length(l))
2393 if !matches!(l, Length::Px(0.0))
2394 )
2395 || [
2396 &style.spacing.padding_top,
2397 &style.spacing.padding_right,
2398 &style.spacing.padding_bottom,
2399 &style.spacing.padding_left,
2400 &style.spacing.border_top,
2401 &style.spacing.border_right,
2402 &style.spacing.border_bottom,
2403 &style.spacing.border_left,
2404 ]
2405 .iter()
2406 .any(|p| !matches!(p, Length::Px(0.0)));
2407
2408 if content_text.is_empty() && !has_styling {
2409 return None;
2410 }
2411
2412 style.line_height = match text_flow_style.line_height {
2414 LineHeight::Number(factor) => Length::Px(text_flow_style.font_size * factor),
2415 LineHeight::Normal => Length::Px(text_flow_style.font_size * DEFAULT_LINE_FACTOR),
2416 LineHeight::Px(px) => Length::Px(px),
2417 };
2418
2419 for layer in &mut container_style.background.layers {
2421 if let Some(crate::engine::layouter::types::BackgroundImage::Image { source, image }) =
2422 &mut layer.image
2423 {
2424 *image = images.get(source).cloned();
2425 }
2426 }
2427
2428 style.display = Display::OutsideInner {
2431 outer: OuterDisplay::Inline,
2432 inner: InnerDisplay::FlowRoot,
2433 };
2434
2435 let mut inline_style = style.clone();
2436 let mut slot_children: Vec<LayoutChild> = Vec::new();
2437 let mut slot_info: Vec<InfoNode> = Vec::new();
2438
2439 if !content_text.is_empty() {
2440 let content_text = normalize_whitespace(&content_text, text_flow_style.white_space);
2441 let content_text = match text_style.text_transform {
2442 TextTransform::None => content_text.into_owned(),
2443 TextTransform::Uppercase => content_text.to_ascii_uppercase(),
2444 TextTransform::Lowercase => content_text.to_ascii_lowercase(),
2445 };
2446 let (layouter, kind) =
2447 create_text_node(content_text, text_style.clone(), text_flow_style, measurer);
2448 inline_style.display = Display::OutsideInner {
2449 outer: OuterDisplay::Inline,
2450 inner: InnerDisplay::Flow,
2451 };
2452 slot_children.push((inline_style, layouter).into());
2453 slot_info.push(InfoNode {
2454 kind,
2455 children: Vec::new(),
2456 dom_id: None,
2457 });
2458 }
2459
2460 let layout = LayoutNode::with_children(style, slot_children);
2461 let info = InfoNode {
2462 kind: NodeKind::Container {
2463 scroll_x: overflow.x,
2464 scroll_y: overflow.y,
2465 scroll_offset_x: 0.0,
2466 scroll_offset_y: 0.0,
2467 style: container_style,
2468 role: ContainerRole::Normal,
2469 },
2470 children: slot_info,
2471 dom_id: None,
2472 };
2473 Some(ChildSlot::Inline(layout.into(), Box::new(info)))
2474}
2475
2476fn resolve_content_function(
2484 fn_name: &str,
2485 args: &[Vec<CssValue>],
2486 get_attr: &dyn Fn(&str) -> Option<String>,
2487 counter_state: &HashMap<String, i32>,
2488) -> String {
2489 let flat: Vec<&CssValue> = args.iter().flatten().collect();
2490
2491 match fn_name {
2492 "attr" => {
2493 if let Some(CssValue::String(attr_name)) = flat.first() {
2494 get_attr(attr_name).unwrap_or_default()
2495 } else if let Some(CssValue::Keyword(attr_name)) = flat.first() {
2496 get_attr(attr_name).unwrap_or_default()
2497 } else {
2498 String::new()
2499 }
2500 }
2501 "counter" => {
2502 if let Some(CssValue::Keyword(name)) = flat.first() {
2503 counter_state
2504 .get(name.as_str())
2505 .map(|v| v.to_string())
2506 .unwrap_or_else(|| "0".to_string())
2507 } else {
2508 "0".to_string()
2509 }
2510 }
2511 "counters" => {
2512 if let Some(CssValue::Keyword(name)) = flat.first() {
2513 counter_state
2514 .get(name.as_str())
2515 .map(|v| v.to_string())
2516 .unwrap_or_else(|| "0".to_string())
2517 } else {
2518 "0".to_string()
2519 }
2520 }
2521 "url" => {
2522 String::new()
2523 }
2524 _ => String::new(),
2525 }
2526}
2527
2528fn parse_counter_reset(value: &CssValue) -> Vec<(String, i32)> {
2536 match value {
2537 CssValue::Keyword(k) if k.eq_ignore_ascii_case("none") => Vec::new(),
2538 CssValue::List(items) => {
2539 let mut result = Vec::new();
2540 let mut iter = items.iter();
2541 while let Some(name_val) = iter.next() {
2542 let name = match name_val {
2543 CssValue::Keyword(k) => k.to_string(),
2544 _ => continue,
2545 };
2546 let val = match iter.next() {
2547 Some(CssValue::Number(n)) => *n as i32,
2548 _ => 0,
2549 };
2550 result.push((name, val));
2551 }
2552 result
2553 }
2554 CssValue::Keyword(k) => {
2555 if k.eq_ignore_ascii_case("none") {
2556 Vec::new()
2557 } else {
2558 vec![(k.to_string(), 0)]
2559 }
2560 }
2561 _ => Vec::new(),
2562 }
2563}
2564
2565fn parse_counter_increment(value: &CssValue) -> Vec<(String, i32)> {
2572 match value {
2573 CssValue::Keyword(k) if k.eq_ignore_ascii_case("none") => Vec::new(),
2574 CssValue::List(items) => {
2575 let mut result = Vec::new();
2576 let mut iter = items.iter();
2577 while let Some(name_val) = iter.next() {
2578 let name = match name_val {
2579 CssValue::Keyword(k) => k.to_string(),
2580 _ => continue,
2581 };
2582 let val = match iter.next() {
2583 Some(CssValue::Number(n)) => *n as i32,
2584 _ => 1,
2585 };
2586 result.push((name, val));
2587 }
2588 result
2589 }
2590 CssValue::Keyword(k) => {
2591 if k.eq_ignore_ascii_case("none") {
2592 Vec::new()
2593 } else {
2594 vec![(k.to_string(), 1)]
2595 }
2596 }
2597 _ => Vec::new(),
2598 }
2599}
2600
2601enum ColorSchemePref {
2605 Normal,
2607 Light,
2608 Dark,
2609 Both,
2611}
2612
2613fn color_scheme_pref(value: Option<&CssValue>) -> ColorSchemePref {
2615 let Some(value) = value else {
2616 return ColorSchemePref::Normal;
2617 };
2618 let mut light = false;
2619 let mut dark = false;
2620 let mut has = false;
2621 let mut push = |keyword: &str| {
2622 has = true;
2623 match keyword {
2624 "light" => light = true,
2625 "dark" => dark = true,
2626 _ => {}
2628 }
2629 };
2630 match value {
2631 CssValue::Keyword(k) => push(k),
2632 CssValue::List(items) => {
2633 for item in items {
2634 if let CssValue::Keyword(k) = item {
2635 push(k);
2636 }
2637 }
2638 }
2639 _ => {}
2640 }
2641 if !has {
2642 ColorSchemePref::Normal
2643 } else if light && dark {
2644 ColorSchemePref::Both
2645 } else if light {
2646 ColorSchemePref::Light
2647 } else if dark {
2648 ColorSchemePref::Dark
2649 } else {
2650 ColorSchemePref::Normal
2651 }
2652}
2653
2654fn resolve_used_color_scheme(
2657 declaration: Option<&CssValue>,
2658 inherited: ColorScheme,
2659 system: ColorScheme,
2660) -> ColorScheme {
2661 match color_scheme_pref(declaration) {
2662 ColorSchemePref::Normal => inherited,
2663 ColorSchemePref::Light => ColorScheme::Light,
2664 ColorSchemePref::Dark => ColorScheme::Dark,
2665 ColorSchemePref::Both => system,
2666 }
2667}
2668
2669#[cfg(any(feature = "profile", debug_assertions))]
2673#[derive(Default)]
2674struct CandidateMetrics {
2675 elements_checked: u64,
2676 candidates_examined: u64,
2677 selectors_matched: u64,
2678 query_candidates_time: std::time::Duration,
2679 selector_match_time: std::time::Duration,
2680 cascade_insert_time: std::time::Duration,
2681}
2682
2683fn collect_candidates(
2684 rule_set: &RuleSet,
2685 chain: &ElementChain,
2686 node_id: NodeId,
2687 mut match_cache: Option<&mut SelectorMatchCache>,
2688 #[cfg(any(feature = "profile", debug_assertions))] stats: &mut CandidateMetrics,
2689) -> (Properties, Properties, HashMap<PseudoElement, Properties>) {
2690 let mut properties = HashMap::new();
2691 let mut custom_properties = HashMap::new();
2692 let mut pseudo_properties: HashMap<PseudoElement, Properties> = HashMap::new();
2693
2694 let element = match chain.first() {
2695 Some(el) => el,
2696 None => return (properties, custom_properties, pseudo_properties),
2697 };
2698
2699 #[cfg(any(feature = "profile", debug_assertions))]
2700 {
2701 stats.elements_checked += 1;
2702 }
2703
2704 perf_scope!(query);
2707 #[cfg(any(feature = "profile", debug_assertions))]
2708 let candidates_iter: Vec<_> = rule_set.query_candidates(element).collect();
2709 #[cfg(not(any(feature = "profile", debug_assertions)))]
2710 let candidates_iter = rule_set.query_candidates(element);
2711 #[cfg(any(feature = "profile", debug_assertions))]
2712 {
2713 stats.query_candidates_time += query.elapsed();
2714 }
2715
2716 for group in candidates_iter {
2717 #[cfg(any(feature = "profile", debug_assertions))]
2718 {
2719 stats.candidates_examined += 1;
2720 }
2721
2722 perf_scope!(sel_match);
2725 let matches_sel = match match_cache.as_deref_mut() {
2726 Some(cache) => {
2727 cache.get_or_insert(&group.selector, node_id, || group.selector.matches(chain))
2728 }
2729 None => group.selector.matches(chain),
2730 };
2731 #[cfg(any(feature = "profile", debug_assertions))]
2732 {
2733 stats.selector_match_time += sel_match.elapsed();
2734 if matches_sel {
2735 stats.selectors_matched += 1;
2736 }
2737 }
2738 if !matches_sel {
2739 continue;
2740 }
2741
2742 for &decl_idx in &group.decls {
2743 let decl = &rule_set.declarations()[decl_idx];
2744
2745 let target: &mut Properties = if decl.pseudo != PseudoElement::None {
2746 pseudo_properties.entry(decl.pseudo).or_default()
2747 } else if decl.name.starts_with("--") {
2748 &mut custom_properties
2749 } else {
2750 &mut properties
2751 };
2752
2753 perf_scope!(cascade);
2754 let should_replace = match target.get(&decl.name) {
2755 Some(current) => decl.outranks(current),
2756 None => true,
2757 };
2758
2759 if should_replace {
2760 target.insert(decl.name.clone(), decl.clone());
2761 }
2762
2763 #[cfg(any(feature = "profile", debug_assertions))]
2764 {
2765 stats.cascade_insert_time += cascade.elapsed();
2766 }
2767 }
2768 }
2769
2770 (properties, custom_properties, pseudo_properties)
2771}