Skip to main content

layout/
query.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Utilities for querying the layout, as needed by layout.
6use std::cell::LazyCell;
7use std::ops::Deref;
8use std::rc::Rc;
9use std::sync::Arc;
10
11use app_units::Au;
12use embedder_traits::UntrustedNodeAddress;
13use euclid::{Point2D, Rect, Size2D};
14use itertools::Itertools;
15use layout_api::{
16    AxesOverflow, BoxAreaType, CSSPixelRectVec, DangerousStyleElementOf, LayoutElement,
17    LayoutElementType, LayoutNode, LayoutNodeType, OffsetParentResponse, PhysicalSides,
18    ScrollContainerQueryFlags, ScrollContainerResponse,
19};
20use paint_api::display_list::ScrollTree;
21use script::layout_dom::ServoLayoutNode;
22use servo_arc::Arc as ServoArc;
23use servo_geometry::{FastLayoutTransform, au_rect_to_f32_rect, f32_rect_to_au_rect};
24use servo_url::ServoUrl;
25use style::computed_values::display::T as Display;
26use style::computed_values::position::T as Position;
27use style::computed_values::visibility::T as Visibility;
28use style::computed_values::white_space_collapse::T as WhiteSpaceCollapseValue;
29use style::context::{QuirksMode, SharedStyleContext, StyleContext, ThreadLocalStyleContext};
30use style::dom::NodeInfo;
31use style::properties::style_structs::Font;
32use style::properties::{
33    ComputedValues, Importance, LonghandId, PropertyDeclarationBlock, PropertyDeclarationId,
34    PropertyId, ShorthandId, SourcePropertyDeclaration, parse_one_declaration_into,
35};
36use style::selector_parser::PseudoElement;
37use style::shared_lock::SharedRwLock;
38use style::stylesheets::{CssRuleType, Origin, UrlExtraData};
39use style::stylist::RuleInclusion;
40use style::traversal::resolve_style;
41use style::values::computed::transform::Matrix3D;
42use style::values::computed::{Float, Size};
43use style::values::generics::font::LineHeight;
44use style::values::generics::position::AspectRatio;
45use style::values::specified::GenericGridTemplateComponent;
46use style::values::specified::box_::DisplayInside;
47use style::values::specified::text::TextTransformCase;
48use style_traits::{CSSPixel, ParsingMode, ToCss};
49
50use crate::cell::RefOrAtomicRef;
51use crate::display_list::{StackingContextTree, au_rect_to_length_rect};
52use crate::dom::NodeExt;
53use crate::flow::inline::construct::{TextTransformation, WhitespaceCollapse, capitalize_string};
54use crate::fragment_tree::{
55    BoxFragment, Fragment, FragmentFlags, FragmentTree, SpecificLayoutInfo, TextFragment,
56};
57use crate::layout_impl::LayoutThread;
58use crate::style_ext::ComputedValuesExt;
59use crate::taffy::SpecificTaffyGridInfo;
60
61/// Get a scroll node that would represents this [`ServoLayoutNode`]'s transform and
62/// calculate its cumulative transform from its root scroll node to the scroll node.
63fn root_transform_for_layout_node(
64    scroll_tree: &ScrollTree,
65    node: ServoLayoutNode<'_>,
66) -> Option<FastLayoutTransform> {
67    let fragments = node.fragments_for_pseudo(None);
68    let box_fragment = fragments
69        .first()
70        .and_then(Fragment::retrieve_box_fragment)?;
71    let scroll_tree_node_id = box_fragment.spatial_tree_node()?;
72    Some(scroll_tree.cumulative_node_to_root_transform(scroll_tree_node_id))
73}
74
75pub(crate) fn process_padding_request(node: ServoLayoutNode<'_>) -> Option<PhysicalSides> {
76    let fragments = node.fragments_for_pseudo(None);
77    let fragment = fragments.first()?;
78    Some(
79        fragment
80            .retrieve_box_fragment()
81            .map(|box_fragment| {
82                let padding = box_fragment.padding;
83                PhysicalSides {
84                    top: padding.top,
85                    left: padding.left,
86                    bottom: padding.bottom,
87                    right: padding.right,
88                }
89            })
90            .unwrap_or_default(),
91    )
92}
93
94pub(crate) fn process_box_area_request(
95    layout_thread: &LayoutThread,
96    stacking_context_tree: &StackingContextTree,
97    node: ServoLayoutNode<'_>,
98    area: BoxAreaType,
99    exclude_transform_and_inline: bool,
100) -> Option<Rect<Au, CSSPixel>> {
101    let fragments = node.fragments_for_pseudo(None);
102    let mut rects = fragments
103        .iter()
104        .filter(|fragment| {
105            !exclude_transform_and_inline ||
106                fragment
107                    .retrieve_box_fragment()
108                    .is_none_or(|fragment| !fragment.with_style().is_inline_box())
109        })
110        .filter_map(|node| node.cumulative_box_area_rect(area, layout_thread.into()))
111        .peekable();
112
113    rects.peek()?;
114    let rect_union = rects.fold(Rect::zero(), |unioned_rect, rect| rect.union(&unioned_rect));
115
116    if exclude_transform_and_inline {
117        return Some(rect_union);
118    }
119
120    let Some(transform) =
121        root_transform_for_layout_node(&stacking_context_tree.paint_info.scroll_tree, node)
122    else {
123        return Some(Rect::new(rect_union.origin, Size2D::zero()));
124    };
125
126    transform_au_rectangle(rect_union, transform)
127}
128
129pub(crate) fn process_box_areas_request(
130    layout_thread: &LayoutThread,
131    stacking_context_tree: &StackingContextTree,
132    node: ServoLayoutNode<'_>,
133    area: BoxAreaType,
134) -> CSSPixelRectVec {
135    let fragments = node
136        .fragments_for_pseudo(None)
137        .into_iter()
138        .filter_map(move |fragment| fragment.cumulative_box_area_rect(area, layout_thread.into()));
139
140    let Some(transform) =
141        root_transform_for_layout_node(&stacking_context_tree.paint_info.scroll_tree, node)
142    else {
143        return fragments
144            .map(|rect| Rect::new(rect.origin, Size2D::zero()))
145            .collect();
146    };
147
148    fragments
149        .filter_map(move |rect| transform_au_rectangle(rect, transform))
150        .collect()
151}
152
153pub fn process_client_rect_request(node: ServoLayoutNode<'_>) -> Rect<i32, CSSPixel> {
154    node.fragments_for_pseudo(None)
155        .first()
156        .map(Fragment::client_rect)
157        .unwrap_or_default()
158}
159
160/// Process a query for the current CSS zoom of an element.
161/// <https://drafts.csswg.org/cssom-view/#dom-element-currentcsszoom>
162///
163/// Returns the effective zoom of the element, which is the product of all zoom
164/// values from the element up to the root. Returns 1.0 if the element is not
165/// being rendered (has no associated box).
166pub fn process_current_css_zoom_query(node: ServoLayoutNode<'_>) -> f32 {
167    let Some(layout_data) = node.inner_layout_data() else {
168        return 1.0;
169    };
170    let layout_box = layout_data.self_box.borrow();
171    let Some(layout_box) = layout_box.as_ref() else {
172        return 1.0;
173    };
174    layout_box
175        .with_base(|base| base.style.effective_zoom.value())
176        .unwrap_or(1.0)
177}
178
179/// <https://drafts.csswg.org/cssom-view/#scrolling-area>
180pub fn process_node_scroll_area_request(
181    layout_thread: &LayoutThread,
182    requested_node: Option<ServoLayoutNode<'_>>,
183    fragment_tree: Option<Rc<FragmentTree>>,
184) -> Rect<i32, CSSPixel> {
185    let Some(tree) = fragment_tree else {
186        return Rect::zero();
187    };
188
189    let rect = match requested_node {
190        Some(node) => node
191            .fragments_for_pseudo(None)
192            .first()
193            .map(|fragment| fragment.scrolling_area(layout_thread))
194            .unwrap_or_default(),
195        None => tree
196            .scrollable_overflow()
197            .union(&tree.initial_containing_block),
198    };
199
200    Rect::new(
201        rect.origin.map(Au::to_f32_px),
202        rect.size.to_vector().map(Au::to_f32_px).to_size(),
203    )
204    .round()
205    .to_i32()
206}
207
208/// Return the resolved value of property for a given (pseudo)element.
209/// <https://drafts.csswg.org/cssom/#resolved-value>
210pub fn process_resolved_style_request(
211    layout_thread: &LayoutThread,
212    context: &SharedStyleContext,
213    node: ServoLayoutNode<'_>,
214    pseudo: &Option<PseudoElement>,
215    property: &PropertyId,
216) -> String {
217    if node
218        .as_element()
219        .is_none_or(|element| element.style_data().is_none())
220    {
221        return process_resolved_style_request_for_unstyled_node(context, node, pseudo, property);
222    }
223
224    // We call process_resolved_style_request after performing a whole-document
225    // traversal, so in the common case, the element is styled.
226    let layout_element = node.as_element().unwrap();
227    let layout_element = match pseudo {
228        Some(pseudo_element_type) => {
229            match layout_element.with_pseudo(*pseudo_element_type) {
230                Some(layout_element) => layout_element,
231                None => {
232                    // The pseudo doesn't exist, return nothing.  Chrome seems to query
233                    // the element itself in this case, Firefox uses the resolved value.
234                    // https://www.w3.org/Bugs/Public/show_bug.cgi?id=29006
235                    return String::new();
236                },
237            }
238        },
239        None => layout_element,
240    };
241
242    let style = &*layout_element.style(context);
243    let longhand_id = match *property {
244        PropertyId::NonCustom(id) => match id.longhand_or_shorthand() {
245            Ok(longhand_id) => longhand_id,
246            Err(shorthand_id) => return shorthand_to_css_string(shorthand_id, style),
247        },
248        PropertyId::Custom(ref name) => {
249            return style.computed_value_to_string(PropertyDeclarationId::Custom(name));
250        },
251    }
252    .to_physical(style.writing_mode);
253
254    // From <https://drafts.csswg.org/css-transforms-2/#serialization-of-the-computed-value>
255    let serialize_transform_value = |box_fragment: Option<&BoxFragment>| -> Result<String, ()> {
256        let transform_list = &style.get_box().transform;
257
258        // > When the computed value is a <transform-list>, the resolved value is one
259        // > <matrix()> function or one <matrix3d()> function computed by the following
260        // > algorithm:
261        if transform_list.0.is_empty() {
262            return Ok("none".into());
263        }
264
265        // > 1. Let transform be a 4x4 matrix initialized to the identity matrix. The
266        // >    elements m11, m22, m33 and m44 of transform must be set to 1; all other
267        // >    elements of transform must be set to 0.
268        // > 2. Post-multiply all <transform-function>s in <transform-list> to transform.
269        let length_rect = box_fragment
270            .map(|box_fragment| au_rect_to_length_rect(&box_fragment.border_rect()).to_untyped());
271        let (transform, is_3d) = transform_list.to_transform_3d_matrix(length_rect.as_ref())?;
272
273        // > 3. Chose between <matrix()> or <matrix3d()> serialization:
274        // >   ↪ If transform is a 2D matrix: Serialize transform to a <matrix()> function.
275        // >   ↪ Otherwise: Serialize transform to a <matrix3d()> function. Chose between
276        // >     <matrix()> or <matrix3d()> serialization:
277        let matrix = Matrix3D::from(transform);
278        if !is_3d {
279            Ok(matrix.into_2d()?.to_css_string())
280        } else {
281            Ok(matrix.to_css_string())
282        }
283    };
284
285    let computed_style = |fragment: Option<&Fragment>| match longhand_id {
286        LonghandId::MinWidth
287            if style.clone_min_width() == Size::Auto &&
288                !should_honor_min_size_auto(fragment, style) =>
289        {
290            String::from("0px")
291        },
292        LonghandId::MinHeight
293            if style.clone_min_height() == Size::Auto &&
294                !should_honor_min_size_auto(fragment, style) =>
295        {
296            String::from("0px")
297        },
298        LonghandId::Transform => match serialize_transform_value(None) {
299            Ok(value) => value,
300            Err(..) => style.computed_value_to_string(PropertyDeclarationId::Longhand(longhand_id)),
301        },
302        _ => style.computed_value_to_string(PropertyDeclarationId::Longhand(longhand_id)),
303    };
304
305    // https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle
306    // Here we are trying to conform to the specification that says that getComputedStyle
307    // should return the used values in certain circumstances. For size and positional
308    // properties we might need to walk the Fragment tree to figure those out. We always
309    // fall back to returning the computed value.
310
311    // For line height, the resolved value is the computed value if it
312    // is "normal" and the used value otherwise.
313    if longhand_id == LonghandId::LineHeight {
314        let font = style.get_font();
315        let font_size = font.font_size.computed_size();
316        return match font.line_height {
317            // There could be a fragment, but it's only interesting for `min-width` and `min-height`,
318            // so just pass None.
319            LineHeight::Normal => computed_style(None),
320            LineHeight::Number(value) => (font_size * value.0).to_css_string(),
321            LineHeight::Length(value) => value.0.to_css_string(),
322        };
323    }
324
325    // https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle
326    // The properties that we calculate below all resolve to the computed value
327    // when the element is display:none or display:contents.
328    let display = style.get_box().display;
329    if display.is_none() || display.is_contents() {
330        return computed_style(None);
331    }
332
333    let resolve_for_fragment = |fragment: &Fragment| {
334        if let Some(box_fragment) = fragment.retrieve_box_fragment() &&
335            style.get_box().position != Position::Static
336        {
337            let resolved_insets =
338                || box_fragment.calculate_resolved_insets_if_positioned(layout_thread.into());
339            match longhand_id {
340                LonghandId::Top => return resolved_insets().top.to_css_string(),
341                LonghandId::Right => {
342                    return resolved_insets().right.to_css_string();
343                },
344                LonghandId::Bottom => {
345                    return resolved_insets().bottom.to_css_string();
346                },
347                LonghandId::Left => {
348                    return resolved_insets().left.to_css_string();
349                },
350                LonghandId::Transform => {
351                    // If we can compute the string do it, but otherwise fallback to a cruder serialization
352                    // of the value.
353                    if let Ok(string) = serialize_transform_value(Some(&box_fragment)) {
354                        return string;
355                    }
356                },
357                _ => {},
358            }
359        }
360
361        if !matches!(
362            fragment,
363            Fragment::LayoutRoot(..) | Fragment::Box(..) | Fragment::Positioning(..)
364        ) {
365            return computed_style(Some(fragment));
366        }
367
368        // https://drafts.csswg.org/css-grid/#resolved-track-list
369        // > The grid-template-rows and grid-template-columns properties are
370        // > resolved value special case properties.
371        //
372        // > When an element generates a grid container box...
373        let specific_layout_info = fragment
374            .retrieve_box_fragment()
375            .and_then(|box_fragment| box_fragment.specific_layout_info().as_deref().cloned());
376        if display.inside() == DisplayInside::Grid &&
377            let Some(SpecificLayoutInfo::Grid(info)) = specific_layout_info &&
378            let Some(value) = resolve_grid_template(&info, style, longhand_id)
379        {
380            return value;
381        }
382
383        // https://drafts.csswg.org/cssom/#resolved-value-special-case-property-like-height
384        // > If the property applies to the element or pseudo-element and the resolved value of the
385        // > display property is not none or contents, then the resolved value is the used value.
386        // > Otherwise the resolved value is the computed value.
387        //
388        // However, all browsers ignore that for margin and padding properties, and resolve to a length
389        // even if the property doesn't apply: https://github.com/w3c/csswg-drafts/issues/10391
390        let content_rect =
391            LazyCell::new(|| fragment.base().map(|base| base.rect()).unwrap_or_default());
392        let margins = LazyCell::new(|| {
393            fragment
394                .retrieve_box_fragment()
395                .map(|fragment| fragment.margin)
396                .unwrap_or_default()
397        });
398        let padding = LazyCell::new(|| {
399            fragment
400                .retrieve_box_fragment()
401                .map(|fragment| fragment.padding)
402                .unwrap_or_default()
403        });
404        match longhand_id {
405            LonghandId::Width if resolved_size_should_be_used_value(fragment) => {
406                content_rect.size.width
407            },
408            LonghandId::Height if resolved_size_should_be_used_value(fragment) => {
409                content_rect.size.height
410            },
411            LonghandId::MarginBottom => margins.bottom,
412            LonghandId::MarginTop => margins.top,
413            LonghandId::MarginLeft => margins.left,
414            LonghandId::MarginRight => margins.right,
415            LonghandId::PaddingBottom => padding.bottom,
416            LonghandId::PaddingTop => padding.top,
417            LonghandId::PaddingLeft => padding.left,
418            LonghandId::PaddingRight => padding.right,
419            _ => return computed_style(Some(fragment)),
420        }
421        .to_css_string()
422    };
423
424    node.fragments_for_pseudo(*pseudo)
425        .first()
426        .map(resolve_for_fragment)
427        .unwrap_or_else(|| computed_style(None))
428}
429
430fn resolved_size_should_be_used_value(fragment: &Fragment) -> bool {
431    // https://drafts.csswg.org/css-sizing-3/#preferred-size-properties
432    // > Applies to: all elements except non-replaced inlines
433    match fragment {
434        Fragment::LayoutRoot(layout_root) => {
435            resolved_size_should_be_used_value(&layout_root.inner())
436        },
437        Fragment::Box(box_fragment) => !box_fragment.with_style().is_inline_box(),
438        Fragment::Float(_) |
439        Fragment::Positioning(_) |
440        Fragment::AbsoluteOrFixedPositionedPlaceholder(_) |
441        Fragment::Image(_) |
442        Fragment::IFrame(_) => true,
443        Fragment::Text(_) => false,
444    }
445}
446
447fn should_honor_min_size_auto(fragment: Option<&Fragment>, style: &ComputedValues) -> bool {
448    // <https://drafts.csswg.org/css-sizing-3/#automatic-minimum-size>
449    // For backwards-compatibility, the resolved value of an automatic minimum size is zero
450    // for boxes of all CSS2 display types: block and inline boxes, inline blocks, and all
451    // the table layout boxes. It also resolves to zero when no box is generated.
452    //
453    // <https://github.com/w3c/csswg-drafts/issues/11716>
454    // However, when a box is generated and `aspect-ratio` isn't `auto`, we need to preserve
455    // the automatic minimum size as `auto`.
456    let Some(box_fragment) = fragment.and_then(|fragment| fragment.retrieve_box_fragment()) else {
457        return false;
458    };
459    let flags = box_fragment.base.flags;
460    flags.contains(FragmentFlags::IS_FLEX_OR_GRID_ITEM) ||
461        style.clone_aspect_ratio() != AspectRatio::auto()
462}
463
464fn resolve_grid_template(
465    grid_info: &SpecificTaffyGridInfo,
466    style: &ComputedValues,
467    longhand_id: LonghandId,
468) -> Option<String> {
469    /// <https://drafts.csswg.org/css-grid/#resolved-track-list-standalone>
470    fn serialize_standalone_non_subgrid_track_list(track_sizes: &[Au]) -> Option<String> {
471        match track_sizes.is_empty() {
472            // Standalone non subgrid grids with empty track lists should compute to `none`.
473            // As of current standard, this behaviour should only invoked by `none` computed value,
474            // therefore we can fallback into computed value resolving.
475            true => None,
476            // <https://drafts.csswg.org/css-grid/#resolved-track-list-standalone>
477            // > - Every track listed individually, whether implicitly or explicitly created,
478            //     without using the repeat() notation.
479            // > - Every track size given as a length in pixels, regardless of sizing function.
480            // > - Adjacent line names collapsed into a single bracketed set.
481            // TODO: implement line names
482            false => Some(
483                track_sizes
484                    .iter()
485                    .map(|size| size.to_css_string())
486                    .join(" "),
487            ),
488        }
489    }
490
491    let (track_info, computed_value) = match longhand_id {
492        LonghandId::GridTemplateRows => (&grid_info.rows, &style.get_position().grid_template_rows),
493        LonghandId::GridTemplateColumns => (
494            &grid_info.columns,
495            &style.get_position().grid_template_columns,
496        ),
497        _ => return None,
498    };
499
500    match computed_value {
501        // <https://drafts.csswg.org/css-grid/#resolved-track-list-standalone>
502        // > When an element generates a grid container box, the resolved value of its grid-template-rows or
503        // > grid-template-columns property in a standalone axis is the used value, serialized with:
504        GenericGridTemplateComponent::None |
505        GenericGridTemplateComponent::TrackList(_) |
506        GenericGridTemplateComponent::Masonry => {
507            serialize_standalone_non_subgrid_track_list(&track_info.sizes)
508        },
509
510        // <https://drafts.csswg.org/css-grid/#resolved-track-list-subgrid>
511        // > When an element generates a grid container box that is a subgrid, the resolved value of the
512        // > grid-template-rows and grid-template-columns properties represents the used number of columns,
513        // > serialized as the subgrid keyword followed by a list representing each of its lines as a
514        // > line name set of all the line’s names explicitly defined on the subgrid (not including those
515        // > adopted from the parent grid), without using the repeat() notation.
516        // TODO: implement subgrid
517        GenericGridTemplateComponent::Subgrid(_) => None,
518    }
519}
520
521#[expect(unsafe_code)]
522pub fn process_resolved_style_request_for_unstyled_node(
523    context: &SharedStyleContext,
524    node: ServoLayoutNode<'_>,
525    pseudo: &Option<PseudoElement>,
526    property: &PropertyId,
527) -> String {
528    // In a display: none subtree. No pseudo-element exists.
529    if pseudo.is_some() {
530        return String::new();
531    }
532
533    let mut tlc = ThreadLocalStyleContext::new();
534    let mut context = StyleContext {
535        shared: context,
536        thread_local: &mut tlc,
537    };
538
539    let element = node.as_element().unwrap();
540    let styles = resolve_style(
541        &mut context,
542        unsafe { element.dangerous_style_element() },
543        RuleInclusion::All,
544        pseudo.as_ref(),
545        None,
546    );
547    let style = styles.primary();
548    let longhand_id = match *property {
549        PropertyId::NonCustom(id) => match id.longhand_or_shorthand() {
550            Ok(longhand_id) => longhand_id,
551            Err(shorthand_id) => return shorthand_to_css_string(shorthand_id, style),
552        },
553        PropertyId::Custom(ref name) => {
554            return style.computed_value_to_string(PropertyDeclarationId::Custom(name));
555        },
556    };
557
558    match longhand_id {
559        // <https://drafts.csswg.org/css-sizing-3/#automatic-minimum-size>
560        // The resolved value of an automatic minimum size is zero when no box is generated.
561        LonghandId::MinWidth if style.clone_min_width() == Size::Auto => String::from("0px"),
562        LonghandId::MinHeight if style.clone_min_height() == Size::Auto => String::from("0px"),
563
564        // No need to care about used values here, since we're on a display: none
565        // subtree, use the computed value.
566        _ => style.computed_value_to_string(PropertyDeclarationId::Longhand(longhand_id)),
567    }
568}
569
570fn shorthand_to_css_string(
571    id: style::properties::ShorthandId,
572    style: &style::properties::ComputedValues,
573) -> String {
574    use style::values::resolved::Context;
575    let mut block = PropertyDeclarationBlock::new();
576    let mut dest = String::new();
577    for longhand in id.longhands() {
578        block.push(
579            style.computed_or_resolved_declaration(
580                longhand,
581                Some(&mut Context {
582                    style,
583                    for_property: PropertyId::NonCustom(longhand.into()),
584                    current_longhand: None,
585                }),
586            ),
587            Importance::Normal,
588        );
589    }
590    match block.shorthand_to_css(id, &mut dest) {
591        Ok(_) => dest.to_owned(),
592        Err(_) => String::new(),
593    }
594}
595
596struct OffsetParentFragments {
597    parent: Arc<BoxFragment>,
598    grandparent: Option<Fragment>,
599}
600
601impl OffsetParentFragments {
602    fn grandparent_box_fragment(&self) -> Option<RefOrAtomicRef<'_, Arc<BoxFragment>>> {
603        self.grandparent
604            .as_ref()
605            .and_then(|grandparent| grandparent.retrieve_box_fragment())
606    }
607}
608/// <https://www.w3.org/TR/2016/WD-cssom-view-1-20160317/#dom-htmlelement-offsetparent>
609#[expect(unsafe_code)]
610fn offset_parent_fragments(node: ServoLayoutNode<'_>) -> Option<OffsetParentFragments> {
611    // 1. If any of the following holds true return null and terminate this algorithm:
612    //  * The element does not have an associated CSS layout box.
613    //  * The element is the root element.
614    //  * The element is the HTML body element.
615    //  * The element’s computed value of the position property is fixed.
616    let fragment = node.fragments_for_pseudo(None).first().cloned()?;
617    let flags = fragment.base()?.flags;
618    if flags.intersects(
619        FragmentFlags::IS_ROOT_ELEMENT | FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT,
620    ) {
621        return None;
622    }
623
624    if fragment
625        .retrieve_box_fragment()
626        .is_some_and(|fragment| fragment.style().get_box().position == Position::Fixed)
627    {
628        return None;
629    }
630
631    // 2.  Return the nearest ancestor element of the element for which at least one of
632    //     the following is true and terminate this algorithm if such an ancestor is found:
633    //  * The computed value of the position property is not static.
634    //  * It is the HTML body element.
635    //  * The computed value of the position property of the element is static and the
636    //    ancestor is one of the following HTML elements: td, th, or table.
637    let mut maybe_parent_node = unsafe { node.dangerous_dom_parent() };
638    while let Some(parent_node) = maybe_parent_node {
639        maybe_parent_node = unsafe { parent_node.dangerous_dom_parent() };
640
641        if let Some(parent_fragment) = parent_node.fragments_for_pseudo(None).first() {
642            let Some(parent_fragment) = parent_fragment.retrieve_box_fragment() else {
643                continue;
644            };
645
646            let grandparent_fragment =
647                maybe_parent_node.and_then(|node| node.fragments_for_pseudo(None).first().cloned());
648
649            if parent_fragment.style().get_box().position != Position::Static {
650                return Some(OffsetParentFragments {
651                    parent: parent_fragment.clone(),
652                    grandparent: grandparent_fragment,
653                });
654            }
655
656            let flags = parent_fragment.base.flags;
657            if flags.intersects(
658                FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT |
659                    FragmentFlags::IS_TABLE_TH_OR_TD_ELEMENT,
660            ) {
661                return Some(OffsetParentFragments {
662                    parent: parent_fragment.clone(),
663                    grandparent: grandparent_fragment,
664                });
665            }
666        }
667    }
668
669    None
670}
671
672#[inline]
673pub fn process_offset_parent_query(
674    layout_thread: &LayoutThread,
675    scroll_tree: &ScrollTree,
676    node: ServoLayoutNode<'_>,
677) -> Option<OffsetParentResponse> {
678    // Only consider the first fragment of the node found as per a
679    // possible interpretation of the specification: "[...] return the
680    // y-coordinate of the top border edge of the first CSS layout box
681    // associated with the element [...]"
682    //
683    // FIXME: Browsers implement this all differently (e.g., [1]) -
684    // Firefox does returns the union of all layout elements of some
685    // sort. Chrome returns the first fragment for a block element (the
686    // same as ours) or the union of all associated fragments in the
687    // first containing block fragment for an inline element. We could
688    // implement Chrome's behavior, but our fragment tree currently
689    // provides insufficient information.
690    //
691    // [1]: https://github.com/w3c/csswg-drafts/issues/4541
692    // > 1. If the element is the HTML body element or does not have any associated CSS
693    //      layout box return zero and terminate this algorithm.
694    let fragment = node.fragments_for_pseudo(None).first().cloned()?;
695    let mut border_box =
696        fragment.cumulative_box_area_rect(BoxAreaType::Border, layout_thread.into())?;
697    let cumulative_sticky_offsets = fragment
698        .retrieve_box_fragment()
699        .and_then(|box_fragment| box_fragment.spatial_tree_node())
700        .map(|node_id| {
701            scroll_tree
702                .cumulative_sticky_offsets(node_id)
703                .map(Au::from_f32_px)
704                .cast_unit()
705        });
706    border_box = border_box.translate(cumulative_sticky_offsets.unwrap_or_default());
707
708    // 2.  If the offsetParent of the element is null return the x-coordinate of the left
709    //     border edge of the first CSS layout box associated with the element, relative to
710    //     the initial containing block origin, ignoring any transforms that apply to the
711    //     element and its ancestors, and terminate this algorithm.
712    let Some(offset_parent_fragment) = offset_parent_fragments(node) else {
713        return Some(OffsetParentResponse {
714            node_address: None,
715            rect: border_box,
716        });
717    };
718
719    let parent_fragment = &offset_parent_fragment.parent;
720    let parent_is_static_body_element = parent_fragment
721        .base
722        .flags
723        .contains(FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT) &&
724        parent_fragment.style().get_box().position == Position::Static;
725
726    // For `offsetLeft`:
727    // 3. Return the result of subtracting the y-coordinate of the top padding edge of the
728    //    first CSS layout box associated with the offsetParent of the element from the
729    //    y-coordinate of the top border edge of the first CSS layout box associated with the
730    //    element, relative to the initial containing block origin, ignoring any transforms
731    //    that apply to the element and its ancestors.
732    //
733    // We generalize this for `offsetRight` as described in the specification.
734    //
735    // The spec (https://www.w3.org/TR/cssom-view-1/#extensions-to-the-htmlelement-interface)
736    // says that offsetTop/offsetLeft are always relative to the padding box of the offsetParent.
737    // However, in practice this is not true in major browsers in the case that the offsetParent is the body
738    // element and the body element is position:static. In that case offsetLeft/offsetTop are computed
739    // relative to the root node's border box.
740    //
741    // See <https://github.com/w3c/csswg-drafts/issues/10549>.
742    let parent_offset_rect = if parent_is_static_body_element {
743        if let Some(grandparent_fragment) = offset_parent_fragment.grandparent_box_fragment() {
744            grandparent_fragment.offset_by_containing_block(
745                &grandparent_fragment.border_rect(),
746                layout_thread.into(),
747            )
748        } else {
749            parent_fragment
750                .offset_by_containing_block(&parent_fragment.padding_rect(), layout_thread.into())
751        }
752    } else {
753        parent_fragment
754            .offset_by_containing_block(&parent_fragment.padding_rect(), layout_thread.into())
755    }
756    .translate(
757        cumulative_sticky_offsets
758            .and_then(|_| parent_fragment.spatial_tree_node())
759            .map(|node_id| {
760                scroll_tree
761                    .cumulative_sticky_offsets(node_id)
762                    .map(Au::from_f32_px)
763                    .cast_unit()
764            })
765            .unwrap_or_default(),
766    );
767
768    border_box = border_box.translate(-parent_offset_rect.origin.to_vector());
769
770    Some(OffsetParentResponse {
771        node_address: parent_fragment.base.tag.map(|tag| tag.node.into()),
772        rect: border_box,
773    })
774}
775
776fn style_and_flags_for_node(
777    node: &ServoLayoutNode,
778) -> Option<(ServoArc<ComputedValues>, FragmentFlags)> {
779    let layout_data = node.inner_layout_data()?;
780    let layout_box = layout_data.self_box.borrow();
781    let layout_box = layout_box.as_ref()?;
782
783    layout_box.with_base(|base| (base.style.clone(), base.base_fragment_info.flags))
784}
785
786fn is_containing_block_for_position(
787    position: Position,
788    ancestor_style: &ServoArc<ComputedValues>,
789    ancestor_flags: FragmentFlags,
790) -> bool {
791    match position {
792        Position::Static | Position::Relative | Position::Sticky => {
793            !ancestor_style.is_inline_box(ancestor_flags)
794        },
795        Position::Absolute => {
796            ancestor_style.establishes_containing_block_for_absolute_descendants(ancestor_flags)
797        },
798        Position::Fixed => {
799            ancestor_style.establishes_containing_block_for_all_descendants(ancestor_flags)
800        },
801    }
802}
803
804fn containing_block_for_node<'a>(node: ServoLayoutNode<'a>) -> Option<ServoLayoutNode<'a>> {
805    let (style, _flags) = style_and_flags_for_node(&node)?;
806
807    let mut current_position_value = style.clone_position();
808    let mut current_ancestor = node;
809
810    #[expect(unsafe_code)]
811    while let Some(ancestor) = unsafe { current_ancestor.dangerous_flat_tree_parent() } {
812        let Some((ancestor_style, ancestor_flags)) = style_and_flags_for_node(&ancestor) else {
813            continue;
814        };
815
816        if is_containing_block_for_position(current_position_value, &ancestor_style, ancestor_flags)
817        {
818            return Some(ancestor);
819        }
820
821        current_position_value = ancestor_style.clone_position();
822        current_ancestor = ancestor;
823    }
824    None
825}
826
827/// An implementation of `scrollParent` that can also be used to for `scrollIntoView`:
828/// <https://drafts.csswg.org/cssom-view/#dom-htmlelement-scrollparent>.
829///
830#[inline]
831pub(crate) fn process_scroll_container_query(
832    node: Option<ServoLayoutNode<'_>>,
833    query_flags: ScrollContainerQueryFlags,
834    viewport_overflow: AxesOverflow,
835) -> Option<ScrollContainerResponse> {
836    let Some(node) = node else {
837        return Some(ScrollContainerResponse::Viewport(viewport_overflow));
838    };
839
840    // 1. If any of the following holds true, return null and terminate this algorithm:
841    //  - The element does not have an associated box.
842    let (style, flags) = style_and_flags_for_node(&node)?;
843
844    // - The element is the root element.
845    // - The element is the body element.
846    //
847    // Note: We only do this for `scrollParent`, which needs to be null. But `scrollIntoView` on the
848    // `<body>` or root element should still bring it into view by scrolling the viewport.
849    if query_flags.contains(ScrollContainerQueryFlags::ForScrollParent) &&
850        flags.intersects(
851            FragmentFlags::IS_ROOT_ELEMENT | FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT,
852        )
853    {
854        return None;
855    }
856
857    if query_flags.contains(ScrollContainerQueryFlags::Inclusive) &&
858        style.establishes_scroll_container(flags)
859    {
860        return Some(ScrollContainerResponse::Element(
861            node.opaque().into(),
862            style.effective_overflow(flags),
863        ));
864    }
865
866    // - The element’s computed value of the position property is fixed and no ancestor
867    //   establishes a fixed position containing block.
868    //
869    // This is handled below in step 2.
870
871    // 2. Let ancestor be the containing block of the element in the flat tree and repeat these substeps:
872    // - If ancestor is the initial containing block, return the scrollingElement for the
873    //   element’s document if it is not closed-shadow-hidden from the element, otherwise
874    //   return null.
875    // - If ancestor is not closed-shadow-hidden from the element, and is a scroll
876    //   container, terminate this algorithm and return ancestor.
877    // - If the computed value of the position property of ancestor is fixed, and no
878    //   ancestor establishes a fixed position containing block, terminate this algorithm
879    //   and return null.
880    // - Let ancestor be the containing block of ancestor in the flat tree.
881    //
882    // Notes: We don't follow the specification exactly below, but we follow the spirit.
883    //
884    // TODO: Handle the situation where the ancestor is "closed-shadow-hidden" from the element.
885    let mut current_position_value = style.clone_position();
886    let mut current_ancestor = node;
887
888    #[expect(unsafe_code)]
889    while let Some(ancestor) = unsafe { current_ancestor.dangerous_flat_tree_parent() } {
890        current_ancestor = ancestor;
891
892        let Some((ancestor_style, ancestor_flags)) = style_and_flags_for_node(&ancestor) else {
893            continue;
894        };
895
896        if !is_containing_block_for_position(
897            current_position_value,
898            &ancestor_style,
899            ancestor_flags,
900        ) {
901            continue;
902        }
903
904        if ancestor_style.establishes_scroll_container(ancestor_flags) {
905            return Some(ScrollContainerResponse::Element(
906                ancestor.opaque().into(),
907                ancestor_style.effective_overflow(ancestor_flags),
908            ));
909        }
910
911        current_position_value = ancestor_style.clone_position();
912    }
913
914    match current_position_value {
915        Position::Fixed => None,
916        _ => Some(ScrollContainerResponse::Viewport(viewport_overflow)),
917    }
918}
919
920/// <https://html.spec.whatwg.org/multipage/#get-the-text-steps>
921pub fn get_the_text_steps(node: ServoLayoutNode<'_>) -> String {
922    // Step 1: If element is not being rendered or if the user agent is a non-CSS user agent, then
923    // return element's descendant text content.
924    // This is taken care of in HTMLElement code
925
926    // Step 2: Let results be a new empty list.
927    let mut results = Vec::new();
928    let mut max_req_line_break_count = 0;
929
930    // Step 3: For each child node node of element:
931    let mut state = Default::default();
932    for child in node.dom_children() {
933        // Step 1: Let current be the list resulting in running the rendered text collection steps with node.
934        let mut current = rendered_text_collection_steps(child, &mut state);
935        // Step 2: For each item item in current, append item to results.
936        results.append(&mut current);
937    }
938
939    let mut output = String::new();
940    for item in results {
941        match item {
942            InnerOrOuterTextItem::Text(s) => {
943                // Step 3.
944                if !s.is_empty() {
945                    if max_req_line_break_count > 0 {
946                        // Step 5.
947                        output.push_str(&"\u{000A}".repeat(max_req_line_break_count));
948                        max_req_line_break_count = 0;
949                    }
950                    output.push_str(&s);
951                }
952            },
953            InnerOrOuterTextItem::RequiredLineBreakCount(count) => {
954                // Step 4.
955                if output.is_empty() {
956                    // Remove required line break count at the start.
957                    continue;
958                }
959                // Store the count if it's the max of this run, but it may be ignored if no text
960                // item is found afterwards, which means that these are consecutive line breaks at
961                // the end.
962                if count > max_req_line_break_count {
963                    max_req_line_break_count = count;
964                }
965            },
966        }
967    }
968    output
969}
970
971enum InnerOrOuterTextItem {
972    Text(String),
973    RequiredLineBreakCount(usize),
974}
975
976#[derive(Clone)]
977struct RenderedTextCollectionState {
978    /// Used to make sure we don't add a `\n` before the first row
979    first_table_row: bool,
980    /// Used to make sure we don't add a `\t` before the first column
981    first_table_cell: bool,
982    /// Keeps track of whether we're inside a table, since there are special rules like ommiting everything that's not
983    /// inside a TableCell/TableCaption
984    within_table: bool,
985    /// Determines whether we truncate leading whitespaces for normal nodes or not
986    may_start_with_whitespace: bool,
987    /// Is set whenever we truncated a white space char, used to prepend a single space before the next element,
988    /// that way we truncate trailing white space without having to look ahead
989    did_truncate_trailing_white_space: bool,
990    /// Is set to true when we're rendering the children of TableCell/TableCaption elements, that way we render
991    /// everything inside those as normal, while omitting everything that's in a Table but NOT in a Cell/Caption
992    within_table_content: bool,
993}
994
995impl Default for RenderedTextCollectionState {
996    fn default() -> Self {
997        RenderedTextCollectionState {
998            first_table_row: true,
999            first_table_cell: true,
1000            may_start_with_whitespace: true,
1001            did_truncate_trailing_white_space: false,
1002            within_table: false,
1003            within_table_content: false,
1004        }
1005    }
1006}
1007
1008/// <https://html.spec.whatwg.org/multipage/#rendered-text-collection-steps>
1009#[expect(unsafe_code)]
1010fn rendered_text_collection_steps(
1011    node: ServoLayoutNode<'_>,
1012    state: &mut RenderedTextCollectionState,
1013) -> Vec<InnerOrOuterTextItem> {
1014    // Step 1. Let items be the result of running the rendered text collection
1015    // steps with each child node of node in tree order,
1016    // and then concatenating the results to a single list.
1017    let mut items = vec![];
1018    if !node.is_connected() || !(node.is_element() || node.is_text_node()) {
1019        return items;
1020    }
1021
1022    match node.type_id() {
1023        Some(LayoutNodeType::Text) => {
1024            if let Some(parent_node) = unsafe { node.dangerous_dom_parent() } {
1025                match parent_node.type_id() {
1026                    // Any text contained in these elements must be ignored.
1027                    Some(
1028                        LayoutNodeType::Element(LayoutElementType::HTMLCanvasElement) |
1029                        LayoutNodeType::Element(LayoutElementType::HTMLImageElement) |
1030                        LayoutNodeType::Element(LayoutElementType::HTMLIFrameElement) |
1031                        LayoutNodeType::Element(LayoutElementType::HTMLObjectElement) |
1032                        LayoutNodeType::Element(LayoutElementType::HTMLInputElement) |
1033                        LayoutNodeType::Element(LayoutElementType::HTMLTextAreaElement) |
1034                        LayoutNodeType::Element(LayoutElementType::HTMLMediaElement),
1035                    ) => {
1036                        return items;
1037                    },
1038                    // Select/Option/OptGroup elements are handled a bit differently.
1039                    // Basically: a Select can only contain Options or OptGroups, while
1040                    // OptGroups may also contain Options. Everything else gets ignored.
1041                    Some(LayoutNodeType::Element(LayoutElementType::HTMLOptGroupElement)) => {
1042                        if let Some(grandparent_node) =
1043                            unsafe { parent_node.dangerous_dom_parent() }
1044                        {
1045                            if !matches!(
1046                                grandparent_node.type_id(),
1047                                Some(LayoutNodeType::Element(
1048                                    LayoutElementType::HTMLSelectElement
1049                                ))
1050                            ) {
1051                                return items;
1052                            }
1053                        } else {
1054                            return items;
1055                        }
1056                    },
1057                    Some(LayoutNodeType::Element(LayoutElementType::HTMLSelectElement)) => {
1058                        return items;
1059                    },
1060                    _ => {},
1061                }
1062
1063                // Tables are also a bit special, mainly by only allowing
1064                // content within TableCell or TableCaption elements once
1065                // we're inside a Table.
1066                if state.within_table && !state.within_table_content {
1067                    return items;
1068                }
1069
1070                let Some(parent_element) = parent_node.as_element() else {
1071                    return items;
1072                };
1073                let Some(style_data) = parent_element.style_data() else {
1074                    return items;
1075                };
1076
1077                let element_data = style_data.element_data.borrow();
1078                let Some(style) = element_data.styles.get_primary() else {
1079                    return items;
1080                };
1081
1082                // Step 2: If node's computed value of 'visibility' is not 'visible', then return items.
1083                //
1084                // We need to do this check here on the Text fragment, if we did it on the element and
1085                // just skipped rendering all child nodes then there'd be no way to override the
1086                // visibility in a child node.
1087                if style.get_inherited_box().visibility != Visibility::Visible {
1088                    return items;
1089                }
1090
1091                // Step 3: If node is not being rendered, then return items. For the purpose of this step,
1092                // the following elements must act as described if the computed value of the 'display'
1093                // property is not 'none':
1094                let display = style.get_box().display;
1095                if display == Display::None {
1096                    match parent_element.type_id() {
1097                        // Even if set to Display::None, Option/OptGroup elements need to
1098                        // be rendered.
1099                        Some(
1100                            LayoutNodeType::Element(LayoutElementType::HTMLOptGroupElement) |
1101                            LayoutNodeType::Element(LayoutElementType::HTMLOptionElement),
1102                        ) => {},
1103                        _ => {
1104                            return items;
1105                        },
1106                    }
1107                }
1108
1109                let text_content = node.text_content();
1110
1111                let white_space_collapse = style.clone_white_space_collapse();
1112                let preserve_whitespace = white_space_collapse == WhiteSpaceCollapseValue::Preserve;
1113                let is_inline = matches!(
1114                    display,
1115                    Display::InlineBlock | Display::InlineFlex | Display::InlineGrid
1116                );
1117                // Now we need to decide on whether to remove beginning white space or not, this
1118                // is mainly decided by the elements we rendered before, but may be overwritten by the white-space
1119                // property.
1120                let trim_beginning_white_space =
1121                    !preserve_whitespace && (state.may_start_with_whitespace || is_inline);
1122                let with_white_space_rules_applied = WhitespaceCollapse::new(
1123                    text_content.chars(),
1124                    white_space_collapse,
1125                    trim_beginning_white_space,
1126                );
1127
1128                // Step 4: If node is a Text node, then for each CSS text box produced by node, in
1129                // content order, compute the text of the box after application of the CSS
1130                // 'white-space' processing rules and 'text-transform' rules, set items to the list
1131                // of the resulting strings, and return items. The CSS 'white-space' processing
1132                // rules are slightly modified: collapsible spaces at the end of lines are always
1133                // collapsed, but they are only removed if the line is the last line of the block,
1134                // or it ends with a br element. Soft hyphens should be preserved.
1135                let text_transform = style.clone_text_transform().case();
1136                let mut transformed_text: String =
1137                    TextTransformation::new(with_white_space_rules_applied, text_transform)
1138                        .collect();
1139
1140                // Since iterator for capitalize not doing anything, we must handle it outside here
1141                // FIXME: This assumes the element always start at a word boundary. But can fail:
1142                // a<span style="text-transform: capitalize">b</span>c
1143                if TextTransformCase::Capitalize == text_transform {
1144                    transformed_text = capitalize_string(&transformed_text, true);
1145                }
1146
1147                let is_preformatted_element =
1148                    white_space_collapse == WhiteSpaceCollapseValue::Preserve;
1149
1150                let is_final_character_whitespace = transformed_text
1151                    .chars()
1152                    .next_back()
1153                    .filter(char::is_ascii_whitespace)
1154                    .is_some();
1155
1156                let is_first_character_whitespace = transformed_text
1157                    .chars()
1158                    .next()
1159                    .filter(char::is_ascii_whitespace)
1160                    .is_some();
1161
1162                // By truncating trailing white space and then adding it back in once we
1163                // encounter another text node we can ensure no trailing white space for
1164                // normal text without having to look ahead
1165                if state.did_truncate_trailing_white_space && !is_first_character_whitespace {
1166                    items.push(InnerOrOuterTextItem::Text(String::from(" ")));
1167                };
1168
1169                if !transformed_text.is_empty() {
1170                    // Here we decide whether to keep or truncate the final white
1171                    // space character, if there is one.
1172                    if is_final_character_whitespace && !is_preformatted_element {
1173                        state.may_start_with_whitespace = false;
1174                        state.did_truncate_trailing_white_space = true;
1175                        transformed_text.pop();
1176                    } else {
1177                        state.may_start_with_whitespace = is_final_character_whitespace;
1178                        state.did_truncate_trailing_white_space = false;
1179                    }
1180                    items.push(InnerOrOuterTextItem::Text(transformed_text));
1181                }
1182            } else {
1183                // If we don't have a parent element then there's no style data available,
1184                // in this (pretty unlikely) case we just return the Text fragment as is.
1185                items.push(InnerOrOuterTextItem::Text(
1186                    node.text_content().deref().into(),
1187                ));
1188            }
1189        },
1190        Some(LayoutNodeType::Element(LayoutElementType::HTMLBRElement)) => {
1191            // Step 5: If node is a br element, then append a string containing a single U+000A
1192            // LF code point to items.
1193            state.did_truncate_trailing_white_space = false;
1194            state.may_start_with_whitespace = true;
1195            items.push(InnerOrOuterTextItem::Text(String::from("\u{000A}")));
1196        },
1197        _ => {
1198            // First we need to gather some infos to setup the various flags
1199            // before rendering the child nodes
1200            let Some(element) = node.as_element() else {
1201                return items;
1202            };
1203            let Some(style_data) = element.style_data() else {
1204                return items;
1205            };
1206
1207            let element_data = style_data.element_data.borrow();
1208            let Some(style) = element_data.styles.get_primary() else {
1209                return items;
1210            };
1211            let inherited_box = style.get_inherited_box();
1212
1213            if inherited_box.visibility != Visibility::Visible {
1214                // If the element is not visible, then we'll immediately render all children,
1215                // skipping all other processing.
1216                // We can't just stop here since a child can override a parents visibility.
1217                for child in node.dom_children() {
1218                    items.append(&mut rendered_text_collection_steps(child, state));
1219                }
1220                return items;
1221            }
1222
1223            let style_box = style.get_box();
1224            let display = style_box.display;
1225            let mut surrounding_line_breaks = 0;
1226
1227            // Treat absolutely positioned or floated elements like Block elements
1228            if style_box.position == Position::Absolute || style_box.float != Float::None {
1229                surrounding_line_breaks = 1;
1230            }
1231
1232            // Depending on the display property we have to do various things
1233            // before we can render the child nodes.
1234            match display {
1235                Display::Table => {
1236                    surrounding_line_breaks = 1;
1237                    state.within_table = true;
1238                },
1239                // Step 6: If node's computed value of 'display' is 'table-cell',
1240                // and node's CSS box is not the last 'table-cell' box of its
1241                // enclosing 'table-row' box, then append a string containing
1242                // a single U+0009 TAB code point to items.
1243                Display::TableCell => {
1244                    if !state.first_table_cell {
1245                        items.push(InnerOrOuterTextItem::Text(String::from(
1246                            "\u{0009}", /* tab */
1247                        )));
1248                        // Make sure we don't add a white-space we removed from the previous node
1249                        state.did_truncate_trailing_white_space = false;
1250                    }
1251                    state.first_table_cell = false;
1252                    state.within_table_content = true;
1253                },
1254                // Step 7: If node's computed value of 'display' is 'table-row',
1255                // and node's CSS box is not the last 'table-row' box of the nearest
1256                // ancestor 'table' box, then append a string containing a single U+000A
1257                // LF code point to items.
1258                Display::TableRow => {
1259                    if !state.first_table_row {
1260                        items.push(InnerOrOuterTextItem::Text(String::from(
1261                            "\u{000A}", /* Line Feed */
1262                        )));
1263                        // Make sure we don't add a white-space we removed from the previous node
1264                        state.did_truncate_trailing_white_space = false;
1265                    }
1266                    state.first_table_row = false;
1267                    state.first_table_cell = true;
1268                },
1269                // Step 9: If node's used value of 'display' is block-level or 'table-caption',
1270                // then append 1 (a required line break count) at the beginning and end of items.
1271                Display::Block => {
1272                    surrounding_line_breaks = 1;
1273                },
1274                Display::TableCaption => {
1275                    surrounding_line_breaks = 1;
1276                    state.within_table_content = true;
1277                },
1278                // InlineBlock's are a bit strange, in that they don't produce a Linebreak, yet
1279                // disable white space truncation before and after it, making it one of the few
1280                // cases where one can have multiple white space characters following one another.
1281                Display::InlineFlex | Display::InlineGrid | Display::InlineBlock
1282                    if state.did_truncate_trailing_white_space =>
1283                {
1284                    items.push(InnerOrOuterTextItem::Text(String::from(" ")));
1285                    state.did_truncate_trailing_white_space = false;
1286                    state.may_start_with_whitespace = true;
1287                },
1288                _ => {},
1289            }
1290
1291            match node.type_id() {
1292                // Step 8: If node is a p element, then append 2 (a required line break count) at
1293                // the beginning and end of items.
1294                Some(LayoutNodeType::Element(LayoutElementType::HTMLParagraphElement)) => {
1295                    surrounding_line_breaks = 2;
1296                },
1297                // Option/OptGroup elements should go on separate lines, by treating them like
1298                // Block elements we can achieve that.
1299                Some(
1300                    LayoutNodeType::Element(LayoutElementType::HTMLOptionElement) |
1301                    LayoutNodeType::Element(LayoutElementType::HTMLOptGroupElement),
1302                ) => {
1303                    surrounding_line_breaks = 1;
1304                },
1305                _ => {},
1306            }
1307
1308            if surrounding_line_breaks > 0 {
1309                items.push(InnerOrOuterTextItem::RequiredLineBreakCount(
1310                    surrounding_line_breaks,
1311                ));
1312                state.did_truncate_trailing_white_space = false;
1313                state.may_start_with_whitespace = true;
1314            }
1315
1316            match node.type_id() {
1317                // Any text/content contained in these elements is ignored.
1318                // However we still need to check whether we have to prepend a
1319                // space, since for example <span>asd <input> qwe</span> must
1320                // product "asd  qwe" (note the 2 spaces)
1321                Some(
1322                    LayoutNodeType::Element(LayoutElementType::HTMLCanvasElement) |
1323                    LayoutNodeType::Element(LayoutElementType::HTMLImageElement) |
1324                    LayoutNodeType::Element(LayoutElementType::HTMLIFrameElement) |
1325                    LayoutNodeType::Element(LayoutElementType::HTMLObjectElement) |
1326                    LayoutNodeType::Element(LayoutElementType::HTMLInputElement) |
1327                    LayoutNodeType::Element(LayoutElementType::HTMLTextAreaElement) |
1328                    LayoutNodeType::Element(LayoutElementType::HTMLMediaElement),
1329                ) => {
1330                    if display != Display::Block && state.did_truncate_trailing_white_space {
1331                        items.push(InnerOrOuterTextItem::Text(String::from(" ")));
1332                        state.did_truncate_trailing_white_space = false;
1333                    };
1334                    state.may_start_with_whitespace = false;
1335                },
1336                _ => {
1337                    // Now we can finally iterate over all children, appending whatever
1338                    // they produce to items.
1339                    for child in node.dom_children() {
1340                        items.append(&mut rendered_text_collection_steps(child, state));
1341                    }
1342                },
1343            }
1344
1345            // Depending on the display property we still need to do some
1346            // cleanup after rendering all child nodes
1347            match display {
1348                Display::InlineFlex | Display::InlineGrid | Display::InlineBlock => {
1349                    state.did_truncate_trailing_white_space = false;
1350                    state.may_start_with_whitespace = false;
1351                },
1352                Display::Table => {
1353                    state.within_table = false;
1354                },
1355                Display::TableCell | Display::TableCaption => {
1356                    state.within_table_content = false;
1357                },
1358                _ => {},
1359            }
1360
1361            if surrounding_line_breaks > 0 {
1362                items.push(InnerOrOuterTextItem::RequiredLineBreakCount(
1363                    surrounding_line_breaks,
1364                ));
1365                state.did_truncate_trailing_white_space = false;
1366                state.may_start_with_whitespace = true;
1367            }
1368        },
1369    };
1370    items
1371}
1372
1373struct ClosestFragment {
1374    fragment: Arc<TextFragment>,
1375    point_in_fragment: Point2D<Au, CSSPixel>,
1376    distance: Au,
1377    point_in_vertical_bounds: bool,
1378}
1379
1380impl ClosestFragment {
1381    fn should_replace(&self, new_distance: Au, point_in_vertical_bounds: bool) -> bool {
1382        if point_in_vertical_bounds && !self.point_in_vertical_bounds {
1383            return true;
1384        }
1385        if self.point_in_vertical_bounds && !point_in_vertical_bounds {
1386            return false;
1387        }
1388        new_distance <= self.distance
1389    }
1390}
1391
1392pub fn find_character_offset_in_fragment_descendants(
1393    node: &ServoLayoutNode,
1394    stacking_context_tree: &StackingContextTree,
1395    point_in_viewport: Point2D<Au, CSSPixel>,
1396) -> Option<usize> {
1397    fn maybe_update_closest(
1398        fragment: &Fragment,
1399        point_in_fragment: Point2D<Au, CSSPixel>,
1400        closest_relative_fragment: &mut Option<ClosestFragment>,
1401    ) {
1402        let Fragment::Text(text_fragment) = fragment else {
1403            return;
1404        };
1405
1406        let (distance, point_in_vertical_bounds) = {
1407            (
1408                text_fragment.distance_to_point_for_glyph_offset(point_in_fragment),
1409                text_fragment.point_is_within_vertical_boundaries(point_in_fragment),
1410            )
1411        };
1412
1413        if closest_relative_fragment
1414            .as_ref()
1415            .is_none_or(|closest_fragment| {
1416                closest_fragment.should_replace(distance, point_in_vertical_bounds)
1417            })
1418        {
1419            *closest_relative_fragment = Some(ClosestFragment {
1420                fragment: text_fragment.clone(),
1421                point_in_fragment,
1422                distance,
1423                point_in_vertical_bounds,
1424            });
1425        }
1426    }
1427
1428    fn collect_relevant_children(
1429        fragment: &Fragment,
1430        point_in_viewport: Point2D<Au, CSSPixel>,
1431        closest_relative_fragment: &mut Option<ClosestFragment>,
1432    ) {
1433        maybe_update_closest(fragment, point_in_viewport, closest_relative_fragment);
1434
1435        if let Some(children) = fragment.children() {
1436            for child in children.iter() {
1437                let offset = child
1438                    .base()
1439                    .map(|base| base.rect().origin)
1440                    .unwrap_or_default();
1441                let point = point_in_viewport - offset.to_vector();
1442                collect_relevant_children(child, point, closest_relative_fragment);
1443            }
1444        }
1445    }
1446
1447    let mut closest_relative_fragment = None;
1448    for fragment in &node.fragments_for_pseudo(None) {
1449        if let Some(point_in_fragment) =
1450            stacking_context_tree.offset_in_fragment(fragment, point_in_viewport)
1451        {
1452            collect_relevant_children(fragment, point_in_fragment, &mut closest_relative_fragment);
1453        }
1454    }
1455
1456    closest_relative_fragment.and_then(|closest_fragment| {
1457        closest_fragment
1458            .fragment
1459            .character_offset(closest_fragment.point_in_fragment)
1460    })
1461}
1462
1463pub fn process_containing_block_query(node: ServoLayoutNode) -> Option<UntrustedNodeAddress> {
1464    let containing_block = containing_block_for_node(node);
1465    containing_block.map(|node| node.opaque().into())
1466}
1467
1468pub fn process_containing_block_descendant_query(
1469    possible_ancestor: ServoLayoutNode,
1470    mut possible_descendant: ServoLayoutNode,
1471) -> bool {
1472    while let Some(establishing_node) = containing_block_for_node(possible_descendant) {
1473        if establishing_node == possible_ancestor {
1474            return true;
1475        }
1476        possible_descendant = establishing_node;
1477    }
1478    false
1479}
1480
1481pub fn process_resolved_font_style_query<'dom, E>(
1482    context: &SharedStyleContext,
1483    node: E,
1484    value: &str,
1485    url_data: ServoUrl,
1486    shared_lock: &SharedRwLock,
1487) -> Option<ServoArc<Font>>
1488where
1489    E: LayoutNode<'dom>,
1490{
1491    fn create_font_declaration(
1492        value: &str,
1493        url_data: &ServoUrl,
1494        quirks_mode: QuirksMode,
1495    ) -> Option<PropertyDeclarationBlock> {
1496        let mut declarations = SourcePropertyDeclaration::default();
1497        let result = parse_one_declaration_into(
1498            &mut declarations,
1499            PropertyId::NonCustom(ShorthandId::Font.into()),
1500            value,
1501            Origin::Author,
1502            &UrlExtraData(url_data.get_arc()),
1503            None,
1504            ParsingMode::DEFAULT,
1505            quirks_mode,
1506            CssRuleType::Style,
1507        );
1508        let declarations = match result {
1509            Ok(()) => {
1510                let mut block = PropertyDeclarationBlock::new();
1511                block.extend(declarations.drain(), Importance::Normal);
1512                block
1513            },
1514            Err(_) => return None,
1515        };
1516        // TODO: Force to set line-height property to 'normal' font property.
1517        Some(declarations)
1518    }
1519    fn resolve_for_declarations<'dom, E>(
1520        context: &SharedStyleContext,
1521        parent_style: Option<&ComputedValues>,
1522        declarations: PropertyDeclarationBlock,
1523        shared_lock: &SharedRwLock,
1524    ) -> ServoArc<ComputedValues>
1525    where
1526        E: LayoutNode<'dom>,
1527    {
1528        let parent_style = match parent_style {
1529            Some(parent) => parent,
1530            None => context.stylist.device().default_computed_values(),
1531        };
1532        context
1533            .stylist
1534            .compute_for_declarations::<DangerousStyleElementOf<'dom, E::ConcreteTypeBundle>>(
1535                &context.guards,
1536                parent_style,
1537                ServoArc::new(shared_lock.wrap(declarations)),
1538            )
1539    }
1540
1541    // https://html.spec.whatwg.org/multipage/#dom-context-2d-font
1542    // 1. Parse the given font property value
1543    let quirks_mode = context.quirks_mode();
1544    let declarations = create_font_declaration(value, &url_data, quirks_mode)?;
1545
1546    // TODO: Reject 'inherit' and 'initial' values for the font property.
1547
1548    // 2. Get resolved styles for the parent element
1549    let element = node.as_element().unwrap();
1550    let parent_style = if node.is_connected() {
1551        if element.style_data().is_some() {
1552            element.style(context)
1553        } else {
1554            let mut tlc = ThreadLocalStyleContext::new();
1555            let mut context = StyleContext {
1556                shared: context,
1557                thread_local: &mut tlc,
1558            };
1559            #[expect(unsafe_code)]
1560            let styles = resolve_style(
1561                &mut context,
1562                unsafe { element.dangerous_style_element() },
1563                RuleInclusion::All,
1564                None,
1565                None,
1566            );
1567            styles.primary().clone()
1568        }
1569    } else {
1570        let default_declarations =
1571            create_font_declaration("10px sans-serif", &url_data, quirks_mode).unwrap();
1572        resolve_for_declarations::<E>(context, None, default_declarations, shared_lock)
1573    };
1574
1575    // 3. Resolve the parsed value with resolved styles of the parent element
1576    let computed_values =
1577        resolve_for_declarations::<E>(context, Some(&*parent_style), declarations, shared_lock);
1578
1579    Some(computed_values.clone_font())
1580}
1581
1582pub(crate) fn transform_au_rectangle(
1583    rect_to_transform: Rect<Au, CSSPixel>,
1584    transform: FastLayoutTransform,
1585) -> Option<Rect<Au, CSSPixel>> {
1586    let rect_to_transform = &au_rect_to_f32_rect(rect_to_transform).cast_unit();
1587    let outer_transformed_rect = match transform {
1588        FastLayoutTransform::Offset(offset) => Some(rect_to_transform.translate(offset)),
1589        FastLayoutTransform::Transform { transform, .. } => {
1590            transform.outer_transformed_rect(rect_to_transform)
1591        },
1592    };
1593    outer_transformed_rect.map(|transformed_rect| f32_rect_to_au_rect(transformed_rect).cast_unit())
1594}
1595
1596pub(crate) fn process_effective_overflow_query(node: ServoLayoutNode<'_>) -> Option<AxesOverflow> {
1597    let fragments = node.fragments_for_pseudo(None);
1598    let box_fragment = fragments.first()?.retrieve_box_fragment()?;
1599
1600    Some(
1601        box_fragment
1602            .style()
1603            .effective_overflow(box_fragment.base.flags),
1604    )
1605}