Skip to main content

layout/
query.rs

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