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