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