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