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