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