1mod apply;
4mod background;
5mod color;
6mod css_resolve;
7mod layout_fix;
8#[cfg(test)]
9mod tests;
10
11pub use apply::{apply_declaration, blockify_out_of_flow_positioned};
12pub use layout_fix::{
13 constrain_auto_grid_track_items, correct_atomic_inline_spacing,
14 correct_atomic_inline_spacing_with_info, is_block_layout_child, is_collapsible_whitespace_info,
15 maximum_fixed_descendant_width, refresh_missing_text_layout_results,
16};
17
18#[allow(unused_imports)]
19pub use background::{
20 apply_background_shorthand_geometry, parse_background_position, parse_background_repeat,
21 parse_background_shorthand, parse_background_size, parse_gradient,
22};
23pub use color::resolve_css_color;
24pub use css_resolve::{
25 CalcValue, extract_font_families, one_or_two_values, parse_grid_line, parse_grid_line_end,
26 parse_grid_placement, parse_grid_template_areas, parse_grid_tracks, resolve_calc_value,
27 resolve_css_len, resolve_css_len_auto, resolve_font_size_px,
28};
29
30use crate::{perf_scope, profile_log};
31
32use crate::engine::bridge::text::{self, GlyphCluster};
33use crate::engine::css::{
34 matcher::{ElementChain, ElementInfo},
35 values::{CssValue, Unit},
36};
37use crate::engine::html::{HtmlNodeType, ScriptingMode};
38use crate::engine::layouter::css_resolver::{
39 DeclarationResolver, Properties, resolve_inline_value,
40};
41use crate::engine::layouter::dom_snapshot::{DomSnapshot, NodeId};
42use crate::engine::layouter::types::WhiteSpace;
43use crate::engine::renderer_model::Image;
44use crate::engine::tree::NodeRef;
45use crate::engine::ui::custom_node_bridge::CustomNodeBridge;
46use crate::engine::ui::registry::{ComponentRegistry, CustomNodeContext, DomWriteBack};
47
48use std::collections::{HashMap, HashSet};
49use std::sync::Arc;
50
51#[allow(unused_imports)]
52use ui_layout::{
53 AlignContent, AlignItems, AutoSizeBehavior, BoxSizing, Display, FlexDirection, FlexWrap,
54 GridPlacement, GridPlacementEnd, GridRepeat, GridTrack, InnerDisplay, ItemFragment,
55 JustifyContent, JustifyItems, LayoutChild, LayoutNode, Length, LengthOrAuto, OuterDisplay,
56 Position, Style,
57};
58
59use super::css_resolver::{
60 MediaEnvironment, ResolvedStyles, RuleSet, resolve_inline_style, set_inline_custom_property,
61};
62use super::text_layouter::TextFlowLayouter;
63#[allow(unused_imports)]
64use super::types::{
65 Background, BackgroundDimension, BackgroundOffset, BackgroundPosition, BackgroundPositionAxis,
66 BackgroundRepeat, BackgroundSize, BorderRadius, BorderStyle, Color, ColorScheme, ColorStop,
67 ContainerRole, ContainerStyle, CornerRadius, CssFloat, FontStyle, FontWeight, Gradient,
68 GradientKind, InfoNode, LineHeight, NodeKind, Overflow, RadialShape, RadialSizeKind, TextAlign,
69 TextDecoration, TextFlowStyle, TextStyle, TextTransform,
70};
71
72pub(crate) const DEFAULT_LINE_FACTOR: f32 = 1.2;
73
74const GRID_LINE_TO_END: GridPlacementEnd = GridPlacementEnd::Line(usize::MAX);
75
76pub(crate) fn element_info(html_node: &HtmlNodeType) -> Option<ElementInfo> {
77 let HtmlNodeType::Element {
78 tag_name,
79 attributes,
80 ..
81 } = html_node
82 else {
83 return None;
84 };
85 Some(ElementInfo {
86 tag_name: tag_name.clone(),
87 id: attributes
88 .iter()
89 .find(|attribute| attribute.name == "id")
90 .map(|attribute| attribute.value.clone()),
91 classes: attributes
92 .iter()
93 .find(|attribute| attribute.name == "class")
94 .map(|attribute| {
95 attribute
96 .value
97 .split_whitespace()
98 .map(str::to_string)
99 .collect()
100 })
101 .unwrap_or_default(),
102 attributes: attributes
103 .iter()
104 .map(|attribute| (attribute.name.clone(), attribute.value.clone()))
105 .collect(),
106 element_index: 1,
107 element_count: 1,
108 type_index: 1,
109 type_count: 1,
110 previous_siblings: ElementChain::default(),
111 })
112}
113
114pub(crate) fn element_sibling_infos(
115 snapshot: &DomSnapshot,
116 children: &[NodeId],
117) -> Vec<Option<ElementInfo>> {
118 let mut type_counts = HashMap::<String, usize>::new();
119 for &child in children {
120 if let Some(tag_name) = snapshot.node(child).kind.tag_name() {
121 *type_counts.entry(tag_name.to_string()).or_default() += 1;
122 }
123 }
124 let element_count = type_counts.values().sum();
125
126 let mut seen_types = HashMap::<String, usize>::new();
127 let mut previous_siblings = ElementChain::default();
128 let mut element_index = 0;
129 children
130 .iter()
131 .map(|&child| {
132 let mut info = match element_info(&snapshot.node(child).kind) {
133 Some(info) => info,
134 None => return None,
135 };
136 element_index += 1;
137 let seen = seen_types.entry(info.tag_name.clone()).or_default();
138 *seen += 1;
139 info.element_index = element_index;
140 info.element_count = element_count;
141 info.type_index = *seen;
142 info.type_count = type_counts[&info.tag_name];
143 info.previous_siblings = previous_siblings.clone();
144
145 let mut sibling = info.clone();
146 sibling.previous_siblings = ElementChain::default();
147 previous_siblings = previous_siblings.prepend(Some(sibling));
148 Some(info)
149 })
150 .collect()
151}
152
153#[derive(Clone, Default)]
160pub struct InheritedCss {
161 pub custom_props: Arc<Properties>,
165 pub text_style: TextStyle,
166 pub text_flow_style: TextFlowStyle,
167 pub color_scheme: ColorScheme,
168}
169
170pub(super) fn length_to_px(len: &Length, font_size: f32) -> f32 {
172 match len {
173 Length::Px(v) => *v,
174 Length::Percent(v) => v * font_size / 100.0,
175 Length::Add(a, b) => length_to_px(a, font_size) + length_to_px(b, font_size),
176 Length::Sub(a, b) => length_to_px(a, font_size) - length_to_px(b, font_size),
177 Length::Mul(a, f) => length_to_px(a, font_size) * f,
178 Length::Div(a, f) => length_to_px(a, font_size) / f,
179 _ => font_size * DEFAULT_LINE_FACTOR,
180 }
181}
182
183struct StackFrame {
184 dom: NodeId,
185 chain: ElementChain,
186 child: Arc<InheritedCss>,
187 kind: Option<NodeKind>,
188 style: Option<Style>,
189 parent_style: Style,
190 parent_container_style: ContainerStyle,
191 child_slots: Vec<ChildSlot>,
192 element_children: Vec<NodeId>,
193}
194
195enum ChildSlot {
196 Inline(LayoutChild, Box<InfoNode>),
197 Element(usize),
198}
199
200pub fn build_layout_and_info(
201 dom: &NodeRef<HtmlNodeType>,
202 resolved_styles: &ResolvedStyles,
203 measurer: Arc<dyn text::TextMeasurer>,
204 parent: InheritedCss,
205 chain: ElementChain,
206 system_color_scheme: ColorScheme,
207 scripting_mode: ScriptingMode,
208) -> (LayoutNode, InfoNode) {
209 build_layout_and_info_with_images(
210 dom,
211 resolved_styles,
212 measurer,
213 parent,
214 chain,
215 system_color_scheme,
216 scripting_mode,
217 &HashMap::new(),
218 )
219}
220
221#[allow(clippy::too_many_arguments)]
223pub fn build_layout_and_info_with_images(
224 dom: &NodeRef<HtmlNodeType>,
225 resolved_styles: &ResolvedStyles,
226 measurer: Arc<dyn text::TextMeasurer>,
227 parent: InheritedCss,
228 chain: ElementChain,
229 system_color_scheme: ColorScheme,
230 scripting_mode: ScriptingMode,
231 images: &HashMap<String, Image>,
232) -> (LayoutNode, InfoNode) {
233 let (snapshot, _dom_refs) = DomSnapshot::from_tree(dom);
234 let media_environment = MediaEnvironment::new((0.0, 0.0), system_color_scheme);
235 let rule_set = RuleSet::from_declarations(resolved_styles, &media_environment);
236 build_layout_and_info_from_snapshot(
237 &snapshot,
238 snapshot.roots()[0],
239 &rule_set,
240 measurer,
241 parent,
242 chain,
243 system_color_scheme,
244 scripting_mode,
245 images,
246 &HashMap::new(),
247 None,
248 )
249}
250
251#[allow(clippy::too_many_arguments)]
260pub fn build_layout_and_info_from_snapshot(
261 snapshot: &DomSnapshot,
262 root: NodeId,
263 rule_set: &RuleSet,
264 measurer: Arc<dyn text::TextMeasurer>,
265 parent: InheritedCss,
266 mut chain: ElementChain,
267 system_color_scheme: ColorScheme,
268 scripting_mode: ScriptingMode,
269 images: &HashMap<String, Image>,
270 audio: &HashMap<String, Arc<[u8]>>,
271 write_back_sender: Option<DomWriteBack>,
272) -> (LayoutNode, InfoNode) {
273 perf_scope!(total);
274
275 #[cfg(any(feature = "profile", debug_assertions))]
276 let mut css_match_time = std::time::Duration::ZERO;
277 #[cfg(any(feature = "profile", debug_assertions))]
278 let mut apply_decl_time = std::time::Duration::ZERO;
279 #[cfg(any(feature = "profile", debug_assertions))]
280 let mut custom_node_time = std::time::Duration::ZERO;
281 #[cfg(any(feature = "profile", debug_assertions))]
282 let mut text_layout_time = std::time::Duration::ZERO;
283 #[cfg(any(feature = "profile", debug_assertions))]
284 let mut exit_phase_time = std::time::Duration::ZERO;
285 #[cfg(any(feature = "profile", debug_assertions))]
286 let mut sibling_info_time = std::time::Duration::ZERO;
287 #[cfg(any(feature = "profile", debug_assertions))]
288 let mut whitespace_keep_time = std::time::Duration::ZERO;
289 #[cfg(any(feature = "profile", debug_assertions))]
290 let mut enter_prep_time = std::time::Duration::ZERO;
291 #[cfg(any(feature = "profile", debug_assertions))]
292 let mut child_slot_build_time = std::time::Duration::ZERO;
293 #[cfg(any(feature = "profile", debug_assertions))]
294 let mut leaf_assemble_time = std::time::Duration::ZERO;
295 #[cfg(any(feature = "profile", debug_assertions))]
296 let mut push_children_time = std::time::Duration::ZERO;
297 #[cfg(any(feature = "profile", debug_assertions))]
298 let mut exit_setup_time = std::time::Duration::ZERO;
299 #[cfg(any(feature = "profile", debug_assertions))]
300 let mut exit_build_time = std::time::Duration::ZERO;
301 #[cfg(any(feature = "profile", debug_assertions))]
302 let mut node_count = 0u64;
303
304 #[cfg(any(feature = "profile", debug_assertions))]
305 let mut cand_stats = CandidateMetrics::default();
306
307 let registry = ComponentRegistry::new();
308 if let Some(info) = element_info(&snapshot.node(root).kind) {
312 chain = chain.prepend(Some(info));
313 }
314
315 let mut stack: Vec<StackFrame> = Vec::new();
318 stack.push(StackFrame {
319 dom: root,
320 chain,
321 child: Arc::new(parent),
322 kind: None,
323 style: None,
324 parent_style: Style::default(),
325 parent_container_style: ContainerStyle::default(),
326 child_slots: Vec::new(),
327 element_children: Vec::new(),
328 });
329
330 let mut results: HashMap<NodeId, (LayoutNode, InfoNode)> = HashMap::new();
331
332 while let Some(top_idx) = {
335 if stack.is_empty() {
336 None
337 } else {
338 Some(stack.len() - 1)
339 }
340 } {
341 let is_entered = stack[top_idx].kind.is_some();
343
344 if !is_entered {
345 let chain_for_css = stack[top_idx].chain.clone();
348 let child_css = Arc::clone(&stack[top_idx].child);
349
350 let html_node = &snapshot.node(stack[top_idx].dom).kind;
351 let mut text_style = child_css.text_style.clone();
352 let mut text_flow_style = child_css.text_flow_style;
353 let mut container_style = ContainerStyle::default();
354 let mut style = Style::default();
355 let mut overflow = Overflow::default();
356 let parent_style = stack[top_idx].parent_style.clone();
357 let parent_container_style = stack[top_idx].parent_container_style.clone();
358 let parent_text_style = text_style.clone();
359 let parent_text_flow_style = text_flow_style;
360
361 container_style.visibility = parent_container_style.visibility;
363
364 perf_scope!(css_match);
366 let (candidates, custom_property_candidates) =
367 if let HtmlNodeType::Element { .. } = html_node {
368 Some(collect_candidates(
369 rule_set,
370 &chain_for_css,
371 #[cfg(any(feature = "profile", debug_assertions))]
372 &mut cand_stats,
373 ))
374 } else {
375 None
376 }
377 .unzip();
378 #[cfg(any(feature = "profile", debug_assertions))]
379 {
380 css_match_time += css_match.elapsed();
381 }
382
383 perf_scope!(enter_prep);
384 let mut custom_properties = Arc::clone(&child_css.custom_props);
388 if let Some(own) = custom_property_candidates
389 && !own.is_empty()
390 {
391 Arc::make_mut(&mut custom_properties).extend(own);
392 }
393
394 let used_color_scheme = {
398 let declaration = candidates
399 .as_ref()
400 .and_then(|c| c.get("color-scheme"))
401 .and_then(|d| {
402 DeclarationResolver::resolve_var(
403 &d.value,
404 &custom_properties,
405 &mut HashSet::new(),
406 )
407 });
408 resolve_used_color_scheme(
409 declaration.as_ref(),
410 child_css.color_scheme,
411 system_color_scheme,
412 )
413 };
414 #[cfg(any(feature = "profile", debug_assertions))]
415 {
416 enter_prep_time += enter_prep.elapsed();
417 }
418
419 perf_scope!(apply_decl);
421 if let Some(candidates) = &candidates {
422 let mut candidates: Vec<_> = candidates.values().collect();
428 candidates.sort_by_key(|declaration| declaration.order);
429
430 for declaration in candidates {
431 if declaration.name.starts_with("--") {
432 continue;
433 }
434 let Some(value) = DeclarationResolver::resolve_var(
435 &declaration.value,
436 &custom_properties,
437 &mut HashSet::new(),
438 ) else {
439 continue;
440 };
441 apply_declaration(
442 &declaration.name,
443 &value,
444 &mut style,
445 &mut container_style,
446 &mut text_style,
447 &mut text_flow_style,
448 &parent_style,
449 &parent_container_style,
450 &parent_text_style,
451 &parent_text_flow_style,
452 &mut overflow,
453 used_color_scheme,
454 );
455 }
456 }
457
458 if let Some(style_attr) = html_node.get_attr("style") {
464 let inline_declarations = resolve_inline_style(style_attr);
465 for (name, value, important) in &inline_declarations {
466 if name.starts_with("--") {
467 let stylesheet_important = candidates
468 .as_ref()
469 .and_then(|c| c.get(name))
470 .is_some_and(|declaration| declaration.important);
471 if *important || !stylesheet_important {
472 set_inline_custom_property(
473 Arc::make_mut(&mut custom_properties),
474 name.clone(),
475 value.clone(),
476 *important,
477 );
478 }
479 }
480 }
481 for (name, value, important) in inline_declarations {
482 if name.starts_with("--") {
483 continue;
484 }
485 let stylesheet_important = candidates
486 .as_ref()
487 .and_then(|c| c.get(&name))
488 .is_some_and(|declaration| declaration.important);
489 if !important && stylesheet_important {
490 continue;
491 }
492 let Some(value) = DeclarationResolver::resolve_var(
493 &value,
494 &custom_properties,
495 &mut HashSet::new(),
496 ) else {
497 continue;
498 };
499 apply_declaration(
500 &name,
501 &value,
502 &mut style,
503 &mut container_style,
504 &mut text_style,
505 &mut text_flow_style,
506 &parent_style,
507 &parent_container_style,
508 &parent_text_style,
509 &parent_text_flow_style,
510 &mut overflow,
511 used_color_scheme,
512 );
513 }
514 }
515
516 for attr in ["width", "height"] {
518 if let Some(value) = html_node.get_attr(attr)
519 && let Some(mut value) = resolve_inline_value(value)
520 {
521 if let CssValue::Number(v) = value {
522 value = CssValue::Length(v, Unit::Px);
523 }
524 apply_declaration(
525 attr,
526 &value,
527 &mut style,
528 &mut container_style,
529 &mut text_style,
530 &mut text_flow_style,
531 &parent_style,
532 &parent_container_style,
533 &parent_text_style,
534 &parent_text_flow_style,
535 &mut overflow,
536 used_color_scheme,
537 );
538 }
539 }
540 #[cfg(any(feature = "profile", debug_assertions))]
541 {
542 apply_decl_time += apply_decl.elapsed();
543 }
544
545 if let Background::Image { source, image, .. } = &mut container_style.background {
546 *image = images.get(source).cloned();
547 }
548
549 if container_style.css_float != CssFloat::None && !style.position.kind.is_out_of_flow()
550 {
551 style.display = Display::OutsideInner {
552 outer: OuterDisplay::Inline,
553 inner: InnerDisplay::FlowRoot,
554 };
555 style.size.auto_behavior = AutoSizeBehavior::ShrinkToFit;
556 }
557
558 blockify_out_of_flow_positioned(&mut style);
561
562 style.line_height = match text_flow_style.line_height {
564 LineHeight::Number(factor) => Length::Px(text_flow_style.font_size * factor),
565 LineHeight::Normal => Length::Px(text_flow_style.font_size * DEFAULT_LINE_FACTOR),
566 LineHeight::Px(px) => Length::Px(px),
567 };
568 container_style.text_align = text_flow_style.text_align;
569
570 if html_node.tag_name() == Some("iframe") {
576 overflow.x = true;
577 overflow.y = true;
578 if matches!(style.size.width, LengthOrAuto::Auto) {
579 style.size.width = LengthOrAuto::Length(Length::Px(300.0));
580 }
581 if matches!(style.size.height, LengthOrAuto::Auto) {
582 style.size.height = LengthOrAuto::Length(Length::Px(150.0));
583 }
584 }
585
586 let child = Arc::new(InheritedCss {
587 custom_props: custom_properties,
588 text_style: text_style.clone(),
589 text_flow_style,
590 color_scheme: used_color_scheme,
591 });
592
593 if let HtmlNodeType::Text(_) = html_node {
594 unreachable!();
595 }
596
597 if let Some(tag) = html_node.tag_name()
599 && registry.tags().contains(&tag)
600 {
601 perf_scope!(custom_node);
602 style.size.auto_behavior = AutoSizeBehavior::ShrinkToFit;
605 let media_source = html_node.get_attr("src").map(str::to_string).or_else(|| {
606 snapshot
607 .children(stack[top_idx].dom)
608 .iter()
609 .find_map(|&child| {
610 let child = &snapshot.node(child).kind;
611 (child.tag_name() == Some("source"))
612 .then(|| child.get_attr("src").map(str::to_string))
613 .flatten()
614 })
615 });
616 let node = registry
617 .create(&CustomNodeContext {
618 tag,
619 media_source: media_source.as_deref(),
620 container_style: &container_style,
621 text_style: &text_style,
622 measurer: Arc::clone(&measurer),
623 images,
624 audio,
625 get_attr: &|name| html_node.get_attr(name).map(str::to_string),
626 write_back: write_back_sender
627 .as_ref()
628 .map(|sender| (sender.clone(), stack[top_idx].dom)),
629 dom_snapshot: snapshot,
630 dom_id: stack[top_idx].dom,
631 })
632 .expect("registry must handle every tag it reports");
633
634 let bridge = CustomNodeBridge::new(Arc::clone(&node), style.clone());
635 let kind = NodeKind::Custom {
636 node,
637 scroll_x: overflow.x,
638 scroll_y: overflow.y,
639 scroll_offset_x: 0.0,
640 scroll_offset_y: 0.0,
641 style: container_style,
642 layout_style: style.clone(),
643 text_style: text_style.clone(),
644 text_flow_style,
645 };
646 let layout = LayoutNode::with_children(style.clone(), [(style, bridge)]);
647 let info = InfoNode {
648 kind,
649 children: Vec::new(),
650 dom_id: Some(stack[top_idx].dom),
651 };
652 let ptr = stack[top_idx].dom;
653 results.insert(ptr, (layout, info));
654 #[cfg(any(feature = "profile", debug_assertions))]
655 {
656 custom_node_time += custom_node.elapsed();
657 node_count += 1;
658 }
659 stack.pop();
660 continue;
661 }
662
663 let is_link = html_node.tag_name() == Some("a") && html_node.get_attr("href").is_some();
665
666 let role = match html_node.tag_name() {
667 Some("table") => ContainerRole::Table,
668 Some("thead" | "tbody" | "tfoot") => ContainerRole::TableRowGroup,
669 Some("tr") => ContainerRole::TableRow,
670 Some("td" | "th") => ContainerRole::TableCell,
671 Some("caption") => ContainerRole::TableCaption,
672 _ if is_link => ContainerRole::Link {
673 href: html_node.get_attr("href").unwrap().to_string(),
674 },
675 _ => ContainerRole::Normal,
676 };
677
678 let kind = NodeKind::Container {
679 scroll_x: overflow.x,
680 scroll_y: overflow.y,
681 scroll_offset_x: 0.0,
682 scroll_offset_y: 0.0,
683 style: container_style,
684 role,
685 };
686
687 if let Some(tag) = html_node.tag_name() {
689 match tag {
690 "table" | "tbody" | "thead" | "tfoot" => {
691 style.display = Display::OutsideInner {
692 outer: OuterDisplay::Block,
693 inner: InnerDisplay::Flex,
694 };
695 style.flex_direction = FlexDirection::Column;
696 }
697 "tr" => {
698 style.display = Display::OutsideInner {
699 outer: OuterDisplay::Block,
700 inner: InnerDisplay::Flex,
701 };
702 style.flex_direction = FlexDirection::Row;
703 }
704 _ => {}
705 }
706 }
707
708 let mut child_slots: Vec<ChildSlot> = Vec::new();
709 let mut element_kids: Vec<NodeId> = Vec::new();
710
711 perf_scope!(child_slot_build);
712 if style.display != Display::None {
713 let parent_tag_name = snapshot.node(stack[top_idx].dom).kind.tag_name();
714 for &child in snapshot.children(stack[top_idx].dom) {
715 let child_node = &snapshot.node(child).kind;
716 if let HtmlNodeType::Text(t) = child_node {
717 let t = if parent_tag_name == Some("pre") {
718 let t = t.strip_prefix('\n').unwrap_or(t);
719 normalize_whitespace(t, text_flow_style.white_space)
720 } else if t.chars().all(is_css_newline)
721 && matches!(
722 text_flow_style.white_space,
723 WhiteSpace::Normal | WhiteSpace::Nowrap
724 )
725 {
726 continue;
727 } else {
728 normalize_whitespace(t, text_flow_style.white_space)
729 };
730
731 let t = match text_style.text_transform {
732 TextTransform::None => t,
733 TextTransform::Uppercase => t.to_ascii_uppercase(),
734 TextTransform::Lowercase => t.to_ascii_lowercase(),
735 };
736 perf_scope!(text_layout);
737 let (layouter, kind) =
738 create_text_node(t, text_style.clone(), text_flow_style, &*measurer);
739 #[cfg(any(feature = "profile", debug_assertions))]
740 {
741 text_layout_time += text_layout.elapsed();
742 node_count += 1;
743 }
744 let mut inline_style = style.clone();
745 inline_style.display = Display::OutsideInner {
746 outer: OuterDisplay::Inline,
747 inner: InnerDisplay::Flow,
748 };
749 child_slots.push(ChildSlot::Inline(
750 (inline_style, layouter).into(),
751 Box::new(InfoNode {
752 kind,
753 children: Vec::new(),
754 dom_id: Some(child),
755 }),
756 ));
757 } else if child_node.tag_name() == Some("br") {
758 child_slots.push(ChildSlot::Inline(
759 ItemFragment::LineBreak.into(),
760 Box::new(InfoNode {
761 kind: NodeKind::LineBreak,
762 children: Vec::new(),
763 dom_id: Some(child),
764 }),
765 ));
766 } else if child_node.tag_name() == Some("noscript")
767 && scripting_mode == ScriptingMode::Enabled
768 {
769 } else {
771 child_slots.push(ChildSlot::Element(element_kids.len()));
772 element_kids.push(child);
773 }
774 }
775 }
776 #[cfg(any(feature = "profile", debug_assertions))]
777 {
778 child_slot_build_time += child_slot_build.elapsed();
779 }
780
781 if element_kids.is_empty() {
782 perf_scope!(leaf_assemble);
784 perf_scope!(whitespace_keep);
785 let keep = compute_whitespace_keep(&child_slots, &[]);
786 #[cfg(any(feature = "profile", debug_assertions))]
787 {
788 whitespace_keep_time += whitespace_keep.elapsed();
789 }
790 let (layout_children, info_children): (Vec<_>, Vec<_>) = child_slots
791 .into_iter()
792 .enumerate()
793 .filter_map(|(i, slot)| {
794 if !keep[i] {
795 return None;
796 }
797 match slot {
798 ChildSlot::Inline(layout, info) => Some((layout, *info)),
799 ChildSlot::Element(_) => None,
800 }
801 })
802 .unzip();
803 let layout = LayoutNode::with_children(style.clone(), layout_children);
804 let info = InfoNode {
805 kind,
806 children: info_children,
807 dom_id: Some(stack[top_idx].dom),
808 };
809 let ptr = stack[top_idx].dom;
810 results.insert(ptr, (layout, info));
811 #[cfg(any(feature = "profile", debug_assertions))]
812 {
813 leaf_assemble_time += leaf_assemble.elapsed();
814 node_count += 1;
815 }
816 stack.pop();
817 } else {
818 perf_scope!(push_children);
820 let parent_chain = stack[top_idx].chain.clone();
821 let parent_container = match &kind {
822 NodeKind::Container { style, .. } => style.clone(),
823 _ => ContainerStyle::default(),
824 };
825 stack[top_idx].kind = Some(kind);
826 stack[top_idx].parent_style = style.clone();
827 stack[top_idx].parent_container_style = parent_container;
828 stack[top_idx].style = Some(style);
829 stack[top_idx].child = child;
830 stack[top_idx].child_slots = child_slots;
831 stack[top_idx].element_children = element_kids;
832
833 let kids_for_push: Vec<_> = {
837 let f = &stack[top_idx];
838 f.element_children.clone()
839 };
840 let child_css = Arc::clone(&stack[top_idx].child);
841 perf_scope!(sibling_info);
842 let kid_infos = element_sibling_infos(snapshot, &kids_for_push);
843 #[cfg(any(feature = "profile", debug_assertions))]
844 {
845 sibling_info_time += sibling_info.elapsed();
846 }
847 let parent_style_for_children = stack[top_idx].parent_style.clone();
848 let parent_container_for_children = stack[top_idx].parent_container_style.clone();
849 for (&kid, info) in kids_for_push.iter().zip(kid_infos).rev() {
850 stack.push(StackFrame {
851 dom: kid,
852 chain: parent_chain.prepend(info),
853 child: Arc::clone(&child_css),
854 kind: None,
855 style: None,
856 parent_style: parent_style_for_children.clone(),
857 parent_container_style: parent_container_for_children.clone(),
858 child_slots: Vec::new(),
859 element_children: Vec::new(),
860 });
861 }
862 #[cfg(any(feature = "profile", debug_assertions))]
863 {
864 push_children_time += push_children.elapsed();
865 }
866 }
867 } else {
868 perf_scope!(exit_phase);
871 perf_scope!(exit_setup);
872
873 let frame = stack.swap_remove(top_idx);
874
875 let mut style = frame.style.as_ref().unwrap().clone();
876 let kind = frame.kind.as_ref().unwrap().clone();
877
878 let mut element_results: Vec<(LayoutChild, InfoNode)> = Vec::new();
880
881 for &kid in &frame.element_children {
882 if let Some((child_layout, child_info)) = results.remove(&kid) {
883 element_results.push((child_layout.into(), child_info));
884 }
885 }
886
887 let mut final_kind = kind;
889 if snapshot.node(frame.dom).kind.tag_name() == Some("html") {
890 let should_inherit = final_kind.is_container_with_transparent_bg();
891 if should_inherit {
892 for (i, &kid) in frame.element_children.iter().enumerate() {
893 if snapshot.node(kid).kind.tag_name() == Some("body")
894 && i < element_results.len()
895 {
896 let child_bg = element_results[i].1.kind.container_bg();
897 if let Some(bg) = child_bg
898 && let NodeKind::Container { ref mut style, .. } = final_kind
899 {
900 style.background = bg.clone();
901 }
902 }
903 }
904 }
905 }
906
907 #[cfg(any(feature = "profile", debug_assertions))]
908 {
909 exit_setup_time += exit_setup.elapsed();
910 }
911
912 perf_scope!(exit_build);
913 let mut element_results: Vec<_> = element_results.into_iter().map(Some).collect();
914
915 perf_scope!(whitespace_keep);
920 let keep = compute_whitespace_keep(&frame.child_slots, &element_results);
921 #[cfg(any(feature = "profile", debug_assertions))]
922 {
923 whitespace_keep_time += whitespace_keep.elapsed();
924 }
925
926 let mut all_layout: Vec<LayoutChild> = Vec::with_capacity(frame.child_slots.len());
927 let mut all_info: Vec<InfoNode> = Vec::with_capacity(frame.child_slots.len());
928
929 for (i, slot) in frame.child_slots.into_iter().enumerate() {
930 if !keep[i] {
931 continue;
932 }
933 let (lc, ic) = match slot {
934 ChildSlot::Inline(layout, info) => (layout, *info),
935 ChildSlot::Element(index) => element_results[index]
936 .take()
937 .expect("element child result must exist"),
938 };
939 all_layout.push(lc);
940 all_info.push(ic);
941 }
942
943 let keep: Vec<bool> = (0..all_layout.len())
948 .map(|index| {
949 let collapsible = is_collapsible_whitespace_info(&all_info[index]);
950 let next_to_block = index == 0
951 || index + 1 == all_layout.len()
952 || index
953 .checked_sub(1)
954 .is_some_and(|previous| is_block_layout_child(&all_layout[previous]))
955 || all_layout.get(index + 1).is_some_and(is_block_layout_child);
956 !(collapsible && next_to_block)
957 })
958 .collect();
959 all_layout = all_layout
960 .into_iter()
961 .zip(&keep)
962 .filter_map(|(layout, keep)| keep.then_some(layout))
963 .collect();
964 all_info = all_info
965 .into_iter()
966 .zip(keep)
967 .filter_map(|(info, keep)| keep.then_some(info))
968 .collect();
969
970 if style.display
976 == (Display::OutsideInner {
977 outer: OuterDisplay::Inline,
978 inner: InnerDisplay::FlowRoot,
979 })
980 && style.size.auto_behavior == AutoSizeBehavior::ShrinkToFit
981 && matches!(style.size.width, LengthOrAuto::Auto)
982 && let Some(width) = maximum_fixed_descendant_width(&all_layout)
983 {
984 style.size.width = LengthOrAuto::Length(Length::Px(width));
985 }
986
987 if matches!(
992 style.display.inner(),
993 Some(InnerDisplay::Grid | InnerDisplay::Flex)
994 ) {
995 if style.display.inner() == Some(InnerDisplay::Grid) {
996 let columns = explicit_grid_track_count(&style.grid_template_columns);
997 let rows = explicit_grid_track_count(&style.grid_template_rows);
998 for child in &mut all_layout {
999 if let LayoutChild::Node(child) = child {
1000 resolve_named_grid_area(child, &style.grid_template_areas);
1001 resolve_grid_end_line(&mut child.style.grid_column, columns);
1002 resolve_grid_end_line(&mut child.style.grid_row, rows);
1003 }
1004 }
1005 }
1006 for child in &mut all_layout {
1007 if let LayoutChild::Node(child) = child
1008 && child.style.display.outer() == Some(OuterDisplay::Inline)
1009 && !child.style.position.kind.is_out_of_flow()
1010 && let Display::OutsideInner { inner, .. } = child.style.display
1011 {
1012 child.style.display = Display::OutsideInner {
1013 outer: OuterDisplay::Block,
1014 inner,
1015 };
1016 }
1017 }
1018 }
1019
1020 let layout = LayoutNode::with_children(style, all_layout);
1021 let info = InfoNode {
1022 kind: final_kind,
1023 children: all_info,
1024 dom_id: Some(frame.dom),
1025 };
1026 let ptr = frame.dom;
1027 results.insert(ptr, (layout, info));
1028 #[cfg(any(feature = "profile", debug_assertions))]
1029 {
1030 exit_build_time += exit_build.elapsed();
1031 exit_phase_time += exit_phase.elapsed();
1032 node_count += 1;
1033 }
1034 }
1035 }
1036
1037 profile_log!(
1038 target: "LayoutRun",
1039 log::Level::Info,
1040 "[LayoutMetrics] total: {:?} (nodes: {})",
1041 total.elapsed(),
1042 node_count,
1043 );
1044 profile_log!(
1045 target: "LayoutRun",
1046 log::Level::Info,
1047 "[LayoutMetrics] css_match: {:?} | apply_decl: {:?}",
1048 css_match_time,
1049 apply_decl_time,
1050 );
1051 profile_log!(
1052 target: "LayoutRun",
1053 log::Level::Info,
1054 "[LayoutCandidates] elements: {} | examined: {} | matched: {}",
1055 cand_stats.elements_checked,
1056 cand_stats.candidates_examined,
1057 cand_stats.selectors_matched,
1058 );
1059 profile_log!(
1060 target: "LayoutRun",
1061 log::Level::Info,
1062 "[LayoutCandidates] query_time: {:?} | sel_match_time: {:?} | insert_time: {:?}",
1063 cand_stats.query_candidates_time,
1064 cand_stats.selector_match_time,
1065 cand_stats.cascade_insert_time,
1066 );
1067 profile_log!(
1068 target: "LayoutRun",
1069 log::Level::Info,
1070 "[LayoutMetrics] custom_node: {:?} | text_layout: {:?} | exit_phase: {:?}",
1071 custom_node_time,
1072 text_layout_time,
1073 exit_phase_time,
1074 );
1075 profile_log!(
1076 target: "LayoutRun",
1077 log::Level::Info,
1078 "[LayoutMetrics] sibling_info: {:?} | whitespace_keep: {:?}",
1079 sibling_info_time,
1080 whitespace_keep_time,
1081 );
1082 profile_log!(
1083 target: "LayoutRun",
1084 log::Level::Info,
1085 "[LayoutMetrics] enter_prep: {:?} | child_slot_build: {:?} | leaf_assemble: {:?}",
1086 enter_prep_time,
1087 child_slot_build_time,
1088 leaf_assemble_time,
1089 );
1090 profile_log!(
1091 target: "LayoutRun",
1092 log::Level::Info,
1093 "[LayoutMetrics] push_children: {:?} | exit_setup: {:?} | exit_build: {:?}",
1094 push_children_time,
1095 exit_setup_time,
1096 exit_build_time,
1097 );
1098 profile_log!(
1099 target: "LayoutRun",
1100 log::Level::Info,
1101 "[LayoutMetrics] rest: {:?}",
1102 total.elapsed().saturating_sub(
1103 css_match_time
1104 + apply_decl_time
1105 + custom_node_time
1106 + enter_prep_time
1107 + child_slot_build_time
1108 + leaf_assemble_time
1109 + push_children_time
1110 + exit_setup_time
1111 + exit_build_time
1112 ),
1113 );
1114
1115 results
1116 .remove(&root)
1117 .expect("root must have been processed")
1118}
1119
1120fn is_css_whitespace(c: char) -> bool {
1123 matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0c')
1124}
1125
1126fn is_css_newline(c: char) -> bool {
1127 matches!(c, '\n' | '\r')
1128}
1129
1130fn is_collapsible_whitespace_text(info: &InfoNode) -> bool {
1133 matches!(
1134 &info.kind,
1135 NodeKind::Text {
1136 text, flow_style, ..
1137 } if text.chars().all(is_css_whitespace)
1138 && matches!(
1139 flow_style.white_space,
1140 WhiteSpace::Normal | WhiteSpace::Nowrap
1141 )
1142 )
1143}
1144
1145#[derive(PartialEq)]
1147enum Neighbour {
1148 None,
1150 Block,
1152 LineBreak,
1154 Inline,
1156}
1157
1158fn neighbour(
1165 slots: &[ChildSlot],
1166 element_results: &[Option<(LayoutChild, InfoNode)>],
1167 slot_index: usize,
1168 step: isize,
1169 skip_line_break: bool,
1170) -> Neighbour {
1171 let mut idx = slot_index;
1172
1173 loop {
1174 let Some(next) = (if step < 0 {
1175 idx.checked_sub(1)
1176 } else {
1177 idx.checked_add(1)
1178 }) else {
1179 return Neighbour::None;
1180 };
1181 if next >= slots.len() {
1182 return Neighbour::None;
1183 }
1184 idx = next;
1185
1186 match &slots[idx] {
1187 ChildSlot::Element(element_index) => {
1188 let Some((child, _)) = element_results[*element_index].as_ref() else {
1189 continue;
1190 };
1191
1192 if matches!(
1194 child,
1195 LayoutChild::Node(node)
1196 if node.style.display == Display::None
1197 ) {
1198 continue;
1199 }
1200
1201 return if matches!(
1202 child,
1203 LayoutChild::Node(node)
1204 if node.style.display.outer() == Some(OuterDisplay::Block)
1205 ) {
1206 Neighbour::Block
1207 } else {
1208 Neighbour::Inline
1209 };
1210 }
1211 ChildSlot::Inline(_, info) => {
1212 if matches!(info.kind, NodeKind::LineBreak) {
1213 if !skip_line_break {
1214 return Neighbour::LineBreak;
1215 }
1216 } else if !is_collapsible_whitespace_text(info) {
1217 return Neighbour::Inline;
1218 }
1219 }
1220 }
1221 }
1222}
1223
1224fn compute_whitespace_keep(
1230 slots: &[ChildSlot],
1231 element_results: &[Option<(LayoutChild, InfoNode)>],
1232) -> Vec<bool> {
1233 (0..slots.len())
1234 .map(|i| match &slots[i] {
1235 ChildSlot::Inline(_, info) => {
1236 if !is_collapsible_whitespace_text(info) {
1237 return true;
1238 }
1239 let side_drops = |step: isize| {
1242 neighbour(slots, element_results, i, step, true) == Neighbour::Block
1243 || neighbour(slots, element_results, i, step, false) == Neighbour::LineBreak
1244 };
1245 !(side_drops(-1) || side_drops(1))
1246 }
1247 ChildSlot::Element(_) => true,
1248 })
1249 .collect()
1250}
1251
1252fn explicit_grid_track_count(tracks: &[GridTrack]) -> usize {
1255 tracks
1256 .iter()
1257 .map(|track| match track {
1258 GridTrack::Repeat(GridRepeat::Count(count), pattern) => {
1259 count.saturating_mul(explicit_grid_track_count(pattern))
1260 }
1261 GridTrack::Repeat(GridRepeat::AutoFit | GridRepeat::AutoFill, _) => 0,
1262 _ => 1,
1263 })
1264 .sum()
1265}
1266
1267fn resolve_grid_end_line(placement: &mut GridPlacement, track_count: usize) {
1268 if !matches!(placement.end, GRID_LINE_TO_END) || track_count == 0 {
1269 return;
1270 }
1271
1272 placement.end = GridPlacementEnd::Line(track_count + 1);
1273}
1274
1275fn resolve_named_grid_area(node: &mut LayoutNode, areas: &[Vec<String>]) {
1276 let Some(name) = node.style.grid_area.as_deref() else {
1277 return;
1278 };
1279 let mut min_column = usize::MAX;
1280 let mut max_column = 0;
1281 let mut min_row = usize::MAX;
1282 let mut max_row = 0;
1283 for (row, names) in areas.iter().enumerate() {
1284 for (column, area) in names.iter().enumerate() {
1285 if area == name {
1286 min_column = min_column.min(column);
1287 max_column = max_column.max(column);
1288 min_row = min_row.min(row);
1289 max_row = max_row.max(row);
1290 }
1291 }
1292 }
1293 if min_column == usize::MAX {
1294 return;
1295 }
1296 node.style.grid_column = GridPlacement {
1297 start: Some(min_column + 1),
1298 end: GridPlacementEnd::Line(max_column + 2),
1299 };
1300 node.style.grid_row = GridPlacement {
1301 start: Some(min_row + 1),
1302 end: GridPlacementEnd::Line(max_row + 2),
1303 };
1304 node.style.grid_area = None;
1305}
1306
1307pub fn normalize_whitespace(text: &str, white_space: WhiteSpace) -> String {
1310 let text = text.replace("\r\n", "\n");
1311 let text = text.replace(['\r', '\x0c'], "\n");
1312
1313 let mut result = String::new();
1314 let mut prev_was_space = false;
1315
1316 for c in text.chars() {
1317 match white_space {
1318 WhiteSpace::Normal | WhiteSpace::Nowrap => {
1319 if is_css_whitespace(c) {
1320 if !prev_was_space {
1321 result.push(' ');
1322 }
1323 prev_was_space = true;
1324 } else {
1325 result.push(c);
1326 prev_was_space = false;
1327 }
1328 }
1329
1330 WhiteSpace::Pre | WhiteSpace::PreWrap | WhiteSpace::BreakSpaces => {
1331 result.push(c);
1332 prev_was_space = false;
1333 }
1334
1335 WhiteSpace::PreLine => {
1336 if c == '\n' {
1337 result.push('\n');
1338 prev_was_space = false;
1339 } else if is_css_whitespace(c) {
1340 if !prev_was_space {
1341 result.push(' ');
1342 }
1343 prev_was_space = true;
1344 } else {
1345 result.push(c);
1346 prev_was_space = false;
1347 }
1348 }
1349 }
1350 }
1351
1352 if white_space == WhiteSpace::PreLine {
1353 while result.ends_with('\n') {
1354 result.pop();
1355 }
1356 }
1357
1358 result
1359}
1360
1361fn create_text_node(
1365 text: String,
1366 text_style: TextStyle,
1367 text_flow_style: TextFlowStyle,
1368 measurer: &dyn text::TextMeasurer,
1369) -> (TextFlowLayouter, NodeKind) {
1370 perf_scope!(measure);
1371 let request = text::TextMeasureRequest {
1372 text: text.clone(),
1373 attribute: text::TextAttribute {
1374 style: text_style.clone(),
1375 flow_style: text_flow_style,
1376 },
1377 };
1378 let clusters = measurer.measure_shaped(&request).unwrap_or_else(|_| {
1379 measurer
1380 .measure(&request)
1381 .map(|ms| {
1382 let mut offset = 0usize;
1383 ms.into_iter()
1384 .map(|f| {
1385 let pos = text[offset..]
1387 .find(&f.text)
1388 .map(|p| offset + p)
1389 .unwrap_or(offset);
1390 offset = pos + f.text.len();
1391 GlyphCluster {
1392 byte_offset: pos,
1393 width: f.width,
1394 break_allowed: true,
1395 }
1396 })
1397 .collect()
1398 })
1399 .unwrap_or_default()
1400 });
1401 profile_log!(
1402 target: "Layouter",
1403 log::Level::Info,
1404 "measure_shaped: text={:?} len={} took={:?}",
1405 crate::profile::text_preview(&text),
1406 text.len(),
1407 measure.elapsed(),
1408 );
1409
1410 let layouter = TextFlowLayouter::new(text.clone(), text_flow_style, clusters);
1411 let kind = NodeKind::Text {
1412 text,
1413 style: text_style,
1414 flow_style: text_flow_style,
1415 text_id: layouter.id,
1416 };
1417 (layouter, kind)
1418}
1419
1420enum ColorSchemePref {
1424 Normal,
1426 Light,
1427 Dark,
1428 Both,
1430}
1431
1432fn color_scheme_pref(value: Option<&CssValue>) -> ColorSchemePref {
1434 let Some(value) = value else {
1435 return ColorSchemePref::Normal;
1436 };
1437 let mut light = false;
1438 let mut dark = false;
1439 let mut has = false;
1440 let mut push = |keyword: &str| {
1441 has = true;
1442 match keyword {
1443 "light" => light = true,
1444 "dark" => dark = true,
1445 _ => {}
1447 }
1448 };
1449 match value {
1450 CssValue::Keyword(k) => push(k),
1451 CssValue::List(items) => {
1452 for item in items {
1453 if let CssValue::Keyword(k) = item {
1454 push(k);
1455 }
1456 }
1457 }
1458 _ => {}
1459 }
1460 if !has {
1461 ColorSchemePref::Normal
1462 } else if light && dark {
1463 ColorSchemePref::Both
1464 } else if light {
1465 ColorSchemePref::Light
1466 } else if dark {
1467 ColorSchemePref::Dark
1468 } else {
1469 ColorSchemePref::Normal
1470 }
1471}
1472
1473fn resolve_used_color_scheme(
1476 declaration: Option<&CssValue>,
1477 inherited: ColorScheme,
1478 system: ColorScheme,
1479) -> ColorScheme {
1480 match color_scheme_pref(declaration) {
1481 ColorSchemePref::Normal => inherited,
1482 ColorSchemePref::Light => ColorScheme::Light,
1483 ColorSchemePref::Dark => ColorScheme::Dark,
1484 ColorSchemePref::Both => system,
1485 }
1486}
1487
1488#[cfg(any(feature = "profile", debug_assertions))]
1492#[derive(Default)]
1493struct CandidateMetrics {
1494 elements_checked: u64,
1495 candidates_examined: u64,
1496 selectors_matched: u64,
1497 query_candidates_time: std::time::Duration,
1498 selector_match_time: std::time::Duration,
1499 cascade_insert_time: std::time::Duration,
1500}
1501
1502fn collect_candidates(
1503 rule_set: &RuleSet,
1504 chain: &ElementChain,
1505 #[cfg(any(feature = "profile", debug_assertions))] stats: &mut CandidateMetrics,
1506) -> (Properties, Properties) {
1507 let mut properties = HashMap::new();
1508 let mut custom_properties = HashMap::new();
1509
1510 let element = match chain.first() {
1511 Some(el) => el,
1512 None => return (properties, custom_properties),
1513 };
1514
1515 #[cfg(any(feature = "profile", debug_assertions))]
1516 {
1517 stats.elements_checked += 1;
1518 }
1519
1520 perf_scope!(query);
1523 #[cfg(any(feature = "profile", debug_assertions))]
1524 let candidates_iter: Vec<_> = rule_set.query_candidates(element).collect();
1525 #[cfg(not(any(feature = "profile", debug_assertions)))]
1526 let candidates_iter = rule_set.query_candidates(element);
1527 #[cfg(any(feature = "profile", debug_assertions))]
1528 {
1529 stats.query_candidates_time += query.elapsed();
1530 }
1531
1532 for group in candidates_iter {
1533 #[cfg(any(feature = "profile", debug_assertions))]
1534 {
1535 stats.candidates_examined += 1;
1536 }
1537
1538 perf_scope!(sel_match);
1541 let matches_sel = group.selector.matches(chain);
1542 #[cfg(any(feature = "profile", debug_assertions))]
1543 {
1544 stats.selector_match_time += sel_match.elapsed();
1545 if matches_sel {
1546 stats.selectors_matched += 1;
1547 }
1548 }
1549 if !matches_sel {
1550 continue;
1551 }
1552
1553 for &decl_idx in &group.decls {
1554 let decl = &rule_set.declarations()[decl_idx];
1555
1556 let target = if decl.name.starts_with("--") {
1557 &mut custom_properties
1558 } else {
1559 &mut properties
1560 };
1561
1562 perf_scope!(cascade);
1563 let should_replace = match target.get(&decl.name) {
1564 Some(current) => decl.outranks(current),
1565 None => true,
1566 };
1567
1568 if should_replace {
1569 target.insert(decl.name.clone(), decl.clone());
1570 }
1571
1572 #[cfg(any(feature = "profile", debug_assertions))]
1573 {
1574 stats.cascade_insert_time += cascade.elapsed();
1575 }
1576 }
1577 }
1578
1579 (properties, custom_properties)
1580}