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