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