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