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