style/
style_adjuster.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//! A struct to encapsulate all the style fixups and flags propagations
6//! a computed style needs in order for it to adhere to the CSS spec.
7
8use crate::computed_value_flags::ComputedValueFlags;
9use crate::dom::TElement;
10use crate::logical_geometry::PhysicalSide;
11use crate::properties::longhands::display::computed_value::T as Display;
12use crate::properties::longhands::float::computed_value::T as Float;
13use crate::properties::longhands::position::computed_value::T as Position;
14#[cfg(feature = "gecko")]
15use crate::properties::longhands::{
16    contain::computed_value::T as Contain, container_type::computed_value::T as ContainerType,
17    content_visibility::computed_value::T as ContentVisibility,
18    overflow_x::computed_value::T as Overflow,
19};
20use crate::properties::{ComputedValues, StyleBuilder};
21use crate::values::computed::position::{
22    PositionTryFallbacksTryTactic, PositionTryFallbacksTryTacticKeyword, TryTacticAdjustment,
23};
24use crate::values::specified::align::AlignFlags;
25
26#[cfg(feature = "gecko")]
27use selectors::parser::PseudoElement;
28
29/// A struct that implements all the adjustment methods.
30///
31/// NOTE(emilio): If new adjustments are introduced that depend on reset
32/// properties of the parent, you may need tweaking the
33/// `ChildCascadeRequirement` code in `matching.rs`.
34///
35/// NOTE(emilio): Also, if new adjustments are introduced that break the
36/// following invariant:
37///
38///   Given same tag name, namespace, rules and parent style, two elements would
39///   end up with exactly the same style.
40///
41/// Then you need to adjust the lookup_by_rules conditions in the sharing cache.
42pub struct StyleAdjuster<'a, 'b: 'a> {
43    style: &'a mut StyleBuilder<'b>,
44}
45
46#[cfg(feature = "gecko")]
47fn is_topmost_svg_svg_element<E>(e: E) -> bool
48where
49    E: TElement,
50{
51    debug_assert!(e.is_svg_element());
52    if e.local_name() != &*atom!("svg") {
53        return false;
54    }
55
56    let parent = match e.traversal_parent() {
57        Some(n) => n,
58        None => return true,
59    };
60
61    if !parent.is_svg_element() {
62        return true;
63    }
64
65    parent.local_name() == &*atom!("foreignObject")
66}
67
68// https://drafts.csswg.org/css-display/#unbox
69#[cfg(feature = "gecko")]
70fn is_effective_display_none_for_display_contents<E>(element: E) -> bool
71where
72    E: TElement,
73{
74    use crate::Atom;
75
76    const SPECIAL_HTML_ELEMENTS: [Atom; 16] = [
77        atom!("br"),
78        atom!("wbr"),
79        atom!("meter"),
80        atom!("progress"),
81        atom!("canvas"),
82        atom!("embed"),
83        atom!("object"),
84        atom!("audio"),
85        atom!("iframe"),
86        atom!("img"),
87        atom!("video"),
88        atom!("frame"),
89        atom!("frameset"),
90        atom!("input"),
91        atom!("textarea"),
92        atom!("select"),
93    ];
94
95    // https://drafts.csswg.org/css-display/#unbox-svg
96    //
97    // There's a note about "Unknown elements", but there's not a good way to
98    // know what that means, or to get that information from here, and no other
99    // UA implements this either.
100    const SPECIAL_SVG_ELEMENTS: [Atom; 6] = [
101        atom!("svg"),
102        atom!("a"),
103        atom!("g"),
104        atom!("use"),
105        atom!("tspan"),
106        atom!("textPath"),
107    ];
108
109    // https://drafts.csswg.org/css-display/#unbox-html
110    if element.is_html_element() {
111        let local_name = element.local_name();
112        return SPECIAL_HTML_ELEMENTS
113            .iter()
114            .any(|name| &**name == local_name);
115    }
116
117    // https://drafts.csswg.org/css-display/#unbox-svg
118    if element.is_svg_element() {
119        if is_topmost_svg_svg_element(element) {
120            return true;
121        }
122        let local_name = element.local_name();
123        return !SPECIAL_SVG_ELEMENTS
124            .iter()
125            .any(|name| &**name == local_name);
126    }
127
128    // https://drafts.csswg.org/css-display/#unbox-mathml
129    if element.is_mathml_element() {
130        return true;
131    }
132
133    false
134}
135
136impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> {
137    /// Trivially constructs a new StyleAdjuster.
138    #[inline]
139    pub fn new(style: &'a mut StyleBuilder<'b>) -> Self {
140        StyleAdjuster { style }
141    }
142
143    /// <https://fullscreen.spec.whatwg.org/#new-stacking-layer>
144    ///
145    ///    Any position value other than 'absolute' and 'fixed' are
146    ///    computed to 'absolute' if the element is in a top layer.
147    ///
148    fn adjust_for_top_layer(&mut self) {
149        if !self.style.in_top_layer() {
150            return;
151        }
152        if !self.style.is_absolutely_positioned() {
153            self.style.mutate_box().set_position(Position::Absolute);
154        }
155        if self.style.get_box().clone_display().is_contents() {
156            self.style.mutate_box().set_display(Display::Block);
157        }
158    }
159
160    /// -webkit-box with line-clamp and vertical orientation gets turned into
161    /// flow-root at computed-value time.
162    ///
163    /// This makes the element not be a flex container, with all that it
164    /// implies, but it should be safe. It matches blink, see
165    /// https://bugzilla.mozilla.org/show_bug.cgi?id=1786147#c10
166    #[cfg(feature = "gecko")]
167    fn adjust_for_webkit_line_clamp(&mut self) {
168        use crate::properties::longhands::_moz_box_orient::computed_value::T as BoxOrient;
169        use crate::values::specified::box_::{DisplayInside, DisplayOutside};
170        let box_style = self.style.get_box();
171        if box_style.clone__webkit_line_clamp().is_none() {
172            return;
173        }
174        let disp = box_style.clone_display();
175        if disp.inside() != DisplayInside::WebkitBox {
176            return;
177        }
178        if self.style.get_xul().clone__moz_box_orient() != BoxOrient::Vertical {
179            return;
180        }
181        let new_display = if disp.outside() == DisplayOutside::Block {
182            Display::FlowRoot
183        } else {
184            debug_assert_eq!(disp.outside(), DisplayOutside::Inline);
185            Display::InlineBlock
186        };
187        self.style
188            .mutate_box()
189            .set_adjusted_display(new_display, false);
190    }
191
192    /// CSS 2.1 section 9.7:
193    ///
194    ///    If 'position' has the value 'absolute' or 'fixed', [...] the computed
195    ///    value of 'float' is 'none'.
196    ///
197    fn adjust_for_position(&mut self) {
198        if self.style.is_absolutely_positioned() && self.style.is_floating() {
199            self.style.mutate_box().set_float(Float::None);
200        }
201    }
202
203    /// Whether we should skip any item-based display property blockification on
204    /// this element.
205    fn skip_item_display_fixup<E>(&self, element: Option<E>) -> bool
206    where
207        E: TElement,
208    {
209        if let Some(pseudo) = self.style.pseudo {
210            return pseudo.skip_item_display_fixup();
211        }
212
213        element.is_some_and(|e| e.skip_item_display_fixup())
214    }
215
216    /// Apply the blockification rules based on the table in CSS 2.2 section 9.7.
217    /// <https://drafts.csswg.org/css2/visuren.html#dis-pos-flo>
218    /// A ::marker pseudo-element with 'list-style-position:outside' needs to
219    /// have its 'display' blockified, unless the ::marker is for an inline
220    /// list-item (for which 'list-style-position:outside' behaves as 'inside').
221    /// https://drafts.csswg.org/css-lists-3/#list-style-position-property
222    fn blockify_if_necessary<E>(&mut self, layout_parent_style: &ComputedValues, element: Option<E>)
223    where
224        E: TElement,
225    {
226        let mut blockify = false;
227        macro_rules! blockify_if {
228            ($if_what:expr) => {
229                if !blockify {
230                    blockify = $if_what;
231                }
232            };
233        }
234
235        blockify_if!(self.style.is_root_element);
236        if !self.skip_item_display_fixup(element) {
237            let parent_display = layout_parent_style.get_box().clone_display();
238            blockify_if!(parent_display.is_item_container());
239        }
240
241        let is_item_or_root = blockify;
242
243        blockify_if!(self.style.is_floating());
244        blockify_if!(self.style.is_absolutely_positioned());
245
246        if !blockify {
247            return;
248        }
249
250        let display = self.style.get_box().clone_display();
251        let blockified_display = display.equivalent_block_display(self.style.is_root_element);
252        if display != blockified_display {
253            self.style
254                .mutate_box()
255                .set_adjusted_display(blockified_display, is_item_or_root);
256        }
257    }
258
259    /// Compute a few common flags for both text and element's style.
260    fn set_bits(&mut self) {
261        let box_style = self.style.get_box();
262        let display = box_style.clone_display();
263
264        if !display.is_contents() {
265            if !self
266                .style
267                .get_text()
268                .clone_text_decoration_line()
269                .is_empty()
270            {
271                self.style
272                    .add_flags(ComputedValueFlags::HAS_TEXT_DECORATION_LINES);
273            }
274
275            if self.style.get_effects().clone_opacity() == 0. {
276                self.style
277                    .add_flags(ComputedValueFlags::IS_IN_OPACITY_ZERO_SUBTREE);
278            }
279        } else if self
280            .style
281            .get_parent_box()
282            .clone_display()
283            .is_item_container()
284            || self
285                .style
286                .get_parent_flags()
287                .contains(ComputedValueFlags::DIPLAY_CONTENTS_IN_ITEM_CONTAINER)
288        {
289            self.style
290                .add_flags(ComputedValueFlags::DIPLAY_CONTENTS_IN_ITEM_CONTAINER);
291        }
292
293        if self.style.pseudo.is_some_and(|p| p.is_first_line()) {
294            self.style
295                .add_flags(ComputedValueFlags::IS_IN_FIRST_LINE_SUBTREE);
296        }
297
298        if self.style.is_root_element {
299            self.style
300                .add_flags(ComputedValueFlags::IS_ROOT_ELEMENT_STYLE);
301        }
302
303        #[cfg(feature = "gecko")]
304        if box_style
305            .clone_effective_containment()
306            .contains(Contain::STYLE)
307        {
308            self.style
309                .add_flags(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_CONTAIN_STYLE);
310        }
311
312        if box_style.clone_container_type().is_size_container_type() {
313            self.style
314                .add_flags(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE);
315        }
316
317        #[cfg(feature = "servo")]
318        if self.style.get_parent_column().is_multicol() {
319            self.style.add_flags(ComputedValueFlags::CAN_BE_FRAGMENTED);
320        }
321    }
322
323    /// Adjust the style for text style.
324    ///
325    /// The adjustments here are a subset of the adjustments generally, because
326    /// text only inherits properties.
327    ///
328    /// Note that this, for Gecko, comes through Servo_ComputedValues_Inherit.
329    #[cfg(feature = "gecko")]
330    pub fn adjust_for_text(&mut self) {
331        debug_assert!(!self.style.is_root_element);
332        self.adjust_for_text_combine_upright();
333        self.adjust_for_text_in_ruby();
334        self.set_bits();
335    }
336
337    /// Change writing mode of the text frame for text-combine-upright.
338    ///
339    /// It is safe to look at our own style because we are looking at inherited
340    /// properties, and text is just plain inheritance.
341    ///
342    /// TODO(emilio): we should (Gecko too) revise these adjustments in presence
343    /// of display: contents.
344    ///
345    /// FIXME(emilio): How does this play with logical properties? Doesn't
346    /// mutating writing-mode change the potential physical sides chosen?
347    #[cfg(feature = "gecko")]
348    fn adjust_for_text_combine_upright(&mut self) {
349        use crate::computed_values::text_combine_upright::T as TextCombineUpright;
350        use crate::computed_values::writing_mode::T as WritingMode;
351        use crate::logical_geometry;
352
353        let writing_mode = self.style.get_inherited_box().clone_writing_mode();
354        let text_combine_upright = self.style.get_inherited_text().clone_text_combine_upright();
355
356        if matches!(
357            writing_mode,
358            WritingMode::VerticalRl | WritingMode::VerticalLr
359        ) && text_combine_upright == TextCombineUpright::All
360        {
361            self.style.add_flags(ComputedValueFlags::IS_TEXT_COMBINED);
362            self.style
363                .mutate_inherited_box()
364                .set_writing_mode(WritingMode::HorizontalTb);
365            self.style.writing_mode =
366                logical_geometry::WritingMode::new(self.style.get_inherited_box());
367        }
368    }
369
370    /// Unconditionally propagates the line break suppression flag to text, and
371    /// additionally it applies it if it is in any ruby box.
372    ///
373    /// This is necessary because its parent may not itself have the flag set
374    /// (e.g. ruby or ruby containers), thus we may not inherit the flag from
375    /// them.
376    #[cfg(feature = "gecko")]
377    fn adjust_for_text_in_ruby(&mut self) {
378        let parent_display = self.style.get_parent_box().clone_display();
379        if parent_display.is_ruby_type()
380            || self
381                .style
382                .get_parent_flags()
383                .contains(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK)
384        {
385            self.style
386                .add_flags(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK);
387        }
388    }
389
390    /// <https://drafts.csswg.org/css-writing-modes-3/#block-flow:>
391    ///
392    ///    If a box has a different writing-mode value than its containing
393    ///    block:
394    ///
395    ///        - If the box has a specified display of inline, its display
396    ///          computes to inline-block. [CSS21]
397    ///
398    /// This matches the adjustment that Gecko does, not exactly following
399    /// the spec. See also:
400    ///
401    /// <https://lists.w3.org/Archives/Public/www-style/2017Mar/0045.html>
402    /// <https://github.com/servo/servo/issues/15754>
403    fn adjust_for_writing_mode(&mut self, layout_parent_style: &ComputedValues) {
404        let our_writing_mode = self.style.get_inherited_box().clone_writing_mode();
405        let parent_writing_mode = layout_parent_style.get_inherited_box().clone_writing_mode();
406
407        if our_writing_mode != parent_writing_mode
408            && self.style.get_box().clone_display() == Display::Inline
409        {
410            // TODO(emilio): Figure out if we can just set the adjusted display
411            // on Gecko too and unify this code path.
412            if cfg!(feature = "servo") {
413                self.style
414                    .mutate_box()
415                    .set_adjusted_display(Display::InlineBlock, false);
416            } else {
417                self.style.mutate_box().set_display(Display::InlineBlock);
418            }
419        }
420    }
421
422    /// CSS overflow-x and overflow-y require some fixup as well in some cases.
423    /// https://drafts.csswg.org/css-overflow-3/#overflow-properties
424    /// "Computed value: as specified, except with `visible`/`clip` computing to
425    /// `auto`/`hidden` (respectively) if one of `overflow-x` or `overflow-y` is
426    /// neither `visible` nor `clip`."
427    fn adjust_for_overflow(&mut self) {
428        let overflow_x = self.style.get_box().clone_overflow_x();
429        let overflow_y = self.style.get_box().clone_overflow_y();
430        if overflow_x == overflow_y {
431            return; // optimization for the common case
432        }
433
434        if overflow_x.is_scrollable() != overflow_y.is_scrollable() {
435            let box_style = self.style.mutate_box();
436            box_style.set_overflow_x(overflow_x.to_scrollable());
437            box_style.set_overflow_y(overflow_y.to_scrollable());
438        }
439    }
440
441    #[cfg(feature = "gecko")]
442    fn adjust_for_contain(&mut self) {
443        let box_style = self.style.get_box();
444        let container_type = box_style.clone_container_type();
445        let content_visibility = box_style.clone_content_visibility();
446        if !container_type.is_size_container_type()
447            && content_visibility == ContentVisibility::Visible
448        {
449            debug_assert_eq!(
450                box_style.clone_contain(),
451                box_style.clone_effective_containment()
452            );
453            return;
454        }
455        let old_contain = box_style.clone_contain();
456        let mut new_contain = old_contain;
457        match content_visibility {
458            ContentVisibility::Visible => {},
459            // `content-visibility:auto` also applies size containment when content
460            // is not relevant (and therefore skipped). This is checked in
461            // nsIFrame::GetContainSizeAxes.
462            ContentVisibility::Auto => {
463                new_contain.insert(Contain::LAYOUT | Contain::PAINT | Contain::STYLE)
464            },
465            ContentVisibility::Hidden => new_contain
466                .insert(Contain::LAYOUT | Contain::PAINT | Contain::SIZE | Contain::STYLE),
467        }
468        if container_type.intersects(ContainerType::INLINE_SIZE) {
469            // https://drafts.csswg.org/css-contain-3/#valdef-container-type-inline-size:
470            //     Applies layout containment, style containment, and inline-size
471            //     containment to the principal box.
472            new_contain.insert(Contain::STYLE | Contain::INLINE_SIZE);
473        } else if container_type.intersects(ContainerType::SIZE) {
474            // https://drafts.csswg.org/css-contain-3/#valdef-container-type-size:
475            //     Applies layout containment, style containment, and size
476            //     containment to the principal box.
477            new_contain.insert(Contain::STYLE | Contain::SIZE);
478        }
479        if new_contain == old_contain {
480            debug_assert_eq!(
481                box_style.clone_contain(),
482                box_style.clone_effective_containment()
483            );
484            return;
485        }
486        self.style
487            .mutate_box()
488            .set_effective_containment(new_contain);
489    }
490
491    /// content-visibility: auto should force contain-intrinsic-size to gain
492    /// an auto value
493    ///
494    /// <https://github.com/w3c/csswg-drafts/issues/8407>
495    #[cfg(feature = "gecko")]
496    fn adjust_for_contain_intrinsic_size(&mut self) {
497        let content_visibility = self.style.get_box().clone_content_visibility();
498        if content_visibility != ContentVisibility::Auto {
499            return;
500        }
501
502        let pos = self.style.get_position();
503        let new_width = pos.clone_contain_intrinsic_width().add_auto_if_needed();
504        let new_height = pos.clone_contain_intrinsic_height().add_auto_if_needed();
505        if new_width.is_none() && new_height.is_none() {
506            return;
507        }
508
509        let pos = self.style.mutate_position();
510        if let Some(width) = new_width {
511            pos.set_contain_intrinsic_width(width);
512        }
513        if let Some(height) = new_height {
514            pos.set_contain_intrinsic_height(height);
515        }
516    }
517
518    /// Handles the relevant sections in:
519    ///
520    /// https://drafts.csswg.org/css-display/#unbox-html
521    ///
522    /// And forbidding display: contents in pseudo-elements, at least for now.
523    #[cfg(feature = "gecko")]
524    fn adjust_for_prohibited_display_contents<E>(&mut self, element: Option<E>)
525    where
526        E: TElement,
527    {
528        if self.style.get_box().clone_display() != Display::Contents {
529            return;
530        }
531
532        // FIXME(emilio): ::before and ::after should support display: contents, see bug 1418138.
533        if self.style.pseudo.is_some_and(|p| !p.is_element_backed()) {
534            self.style.mutate_box().set_display(Display::Inline);
535            return;
536        }
537
538        let element = match element {
539            Some(e) => e,
540            None => return,
541        };
542
543        if is_effective_display_none_for_display_contents(element) {
544            self.style.mutate_box().set_display(Display::None);
545        }
546    }
547
548    /// <textarea>'s editor root needs to inherit the overflow value from its
549    /// parent, but we need to make sure it's still scrollable.
550    #[cfg(feature = "gecko")]
551    fn adjust_for_text_control_editing_root(&mut self) {
552        use crate::properties::longhands::white_space_collapse::computed_value::T as WhiteSpaceCollapse;
553        use crate::selector_parser::PseudoElement;
554
555        if self.style.pseudo != Some(&PseudoElement::MozTextControlEditingRoot) {
556            return;
557        }
558
559        let old_collapse = self.style.get_inherited_text().clone_white_space_collapse();
560        let new_collapse = match old_collapse {
561            WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces => old_collapse,
562            WhiteSpaceCollapse::Collapse
563            | WhiteSpaceCollapse::PreserveSpaces
564            | WhiteSpaceCollapse::PreserveBreaks => WhiteSpaceCollapse::Preserve,
565        };
566        if new_collapse != old_collapse {
567            self.style
568                .mutate_inherited_text()
569                .set_white_space_collapse(new_collapse);
570        }
571
572        let box_style = self.style.get_box();
573        let overflow_x = box_style.clone_overflow_x();
574        let overflow_y = box_style.clone_overflow_y();
575
576        // If at least one is scrollable we'll adjust the other one in
577        // adjust_for_overflow if needed.
578        if overflow_x.is_scrollable() || overflow_y.is_scrollable() {
579            return;
580        }
581
582        let box_style = self.style.mutate_box();
583        box_style.set_overflow_x(Overflow::Auto);
584        box_style.set_overflow_y(Overflow::Auto);
585    }
586
587    /// If a <fieldset> has grid/flex display type, we need to inherit
588    /// this type into its ::-moz-fieldset-content anonymous box.
589    #[cfg(feature = "gecko")]
590    fn adjust_for_fieldset_content(&mut self) {
591        use crate::selector_parser::PseudoElement;
592        if self.style.pseudo != Some(&PseudoElement::FieldsetContent) {
593            return;
594        }
595        let parent_display = self.style.get_parent_box().clone_display();
596        debug_assert!(
597            !parent_display.is_contents(),
598            "How did we create a fieldset-content box with display: contents?"
599        );
600        let new_display = match parent_display {
601            Display::Flex | Display::InlineFlex => Some(Display::Flex),
602            Display::Grid | Display::InlineGrid => Some(Display::Grid),
603            _ => None,
604        };
605        if let Some(new_display) = new_display {
606            self.style.mutate_box().set_display(new_display);
607        }
608    }
609
610    /// -moz-center, -moz-left and -moz-right are used for HTML's alignment.
611    ///
612    /// This is covering the <div align="right"><table>...</table></div> case.
613    ///
614    /// In this case, we don't want to inherit the text alignment into the
615    /// table.
616    fn adjust_for_table_text_align(&mut self) {
617        use crate::properties::longhands::text_align::computed_value::T as TextAlign;
618        if self.style.get_box().clone_display() != Display::Table {
619            return;
620        }
621
622        match self.style.get_inherited_text().clone_text_align() {
623            TextAlign::MozLeft | TextAlign::MozCenter | TextAlign::MozRight => {},
624            _ => return,
625        }
626
627        self.style
628            .mutate_inherited_text()
629            .set_text_align(TextAlign::Start)
630    }
631
632    #[cfg(feature = "gecko")]
633    fn should_suppress_linebreak<E>(&self, element: Option<E>) -> bool
634    where
635        E: TElement,
636    {
637        // Line break suppression should only be propagated to in-flow children.
638        if self.style.is_floating() || self.style.is_absolutely_positioned() {
639            return false;
640        }
641        let parent_display = self.style.get_parent_box().clone_display();
642        if self
643            .style
644            .get_parent_flags()
645            .contains(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK)
646        {
647            // Line break suppression is propagated to any children of
648            // line participants, and across display: contents boundaries.
649            if parent_display.is_line_participant() || parent_display.is_contents() {
650                return true;
651            }
652        }
653        match self.style.get_box().clone_display() {
654            // Ruby base and text are always non-breakable.
655            Display::RubyBase | Display::RubyText => true,
656            // Ruby base container and text container are breakable.
657            // Non-HTML elements may not form ruby base / text container because
658            // they may not respect ruby-internal display values, so we can't
659            // make them escaped from line break suppression.
660            // Note that, when certain HTML tags, e.g. form controls, have ruby
661            // level container display type, they could also escape from the
662            // line break suppression flag while they shouldn't. However, it is
663            // generally fine as far as they can't break the line inside them.
664            Display::RubyBaseContainer | Display::RubyTextContainer
665                if element.map_or(true, |e| e.is_html_element()) =>
666            {
667                false
668            },
669            // Anything else is non-breakable if and only if its layout parent
670            // has a ruby display type, because any of the ruby boxes can be
671            // anonymous.
672            _ => parent_display.is_ruby_type(),
673        }
674    }
675
676    /// Do ruby-related style adjustments, which include:
677    /// * propagate the line break suppression flag,
678    /// * inlinify block descendants,
679    /// * suppress border and padding for ruby level containers,
680    /// * correct unicode-bidi.
681    #[cfg(feature = "gecko")]
682    fn adjust_for_ruby<E>(&mut self, element: Option<E>)
683    where
684        E: TElement,
685    {
686        use crate::properties::longhands::unicode_bidi::computed_value::T as UnicodeBidi;
687
688        let self_display = self.style.get_box().clone_display();
689        // Check whether line break should be suppressed for this element.
690        if self.should_suppress_linebreak(element) {
691            self.style
692                .add_flags(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK);
693            // Inlinify the display type if allowed.
694            if !self.skip_item_display_fixup(element) {
695                let inline_display = self_display.inlinify();
696                if self_display != inline_display {
697                    self.style
698                        .mutate_box()
699                        .set_adjusted_display(inline_display, false);
700                }
701            }
702        }
703        // Suppress border and padding for ruby level containers.
704        // This is actually not part of the spec. It is currently unspecified
705        // how border and padding should be handled for ruby level container,
706        // and suppressing them here make it easier for layout to handle.
707        if self_display.is_ruby_level_container() {
708            self.style.reset_border_struct();
709            self.style.reset_padding_struct();
710        }
711
712        // Force bidi isolation on all internal ruby boxes and ruby container
713        // per spec https://drafts.csswg.org/css-ruby-1/#bidi
714        if self_display.is_ruby_type() {
715            let new_value = match self.style.get_text().clone_unicode_bidi() {
716                UnicodeBidi::Normal | UnicodeBidi::Embed => Some(UnicodeBidi::Isolate),
717                UnicodeBidi::BidiOverride => Some(UnicodeBidi::IsolateOverride),
718                _ => None,
719            };
720            if let Some(new_value) = new_value {
721                self.style.mutate_text().set_unicode_bidi(new_value);
722            }
723        }
724    }
725
726    /// Computes the RELEVANT_LINK_VISITED flag based on the parent style and on
727    /// whether we're a relevant link.
728    ///
729    /// NOTE(emilio): We don't do this for text styles, which is... dubious, but
730    /// Gecko doesn't seem to do it either. It's extremely easy to do if needed
731    /// though.
732    ///
733    /// FIXME(emilio): This isn't technically a style adjustment thingie, could
734    /// it move somewhere else?
735    fn adjust_for_visited<E>(&mut self, element: Option<E>)
736    where
737        E: TElement,
738    {
739        if !self.style.has_visited_style() {
740            return;
741        }
742
743        let is_link_element = self.style.pseudo.is_none() && element.map_or(false, |e| e.is_link());
744
745        if !is_link_element {
746            return;
747        }
748
749        if element.unwrap().is_visited_link() {
750            self.style
751                .add_flags(ComputedValueFlags::IS_RELEVANT_LINK_VISITED);
752        } else {
753            // Need to remove to handle unvisited link inside visited.
754            self.style
755                .remove_flags(ComputedValueFlags::IS_RELEVANT_LINK_VISITED);
756        }
757    }
758
759    /// Resolves "justify-items: legacy" based on the inherited style if needed
760    /// to comply with:
761    ///
762    /// <https://drafts.csswg.org/css-align/#valdef-justify-items-legacy>
763    #[cfg(feature = "gecko")]
764    fn adjust_for_justify_items(&mut self) {
765        use crate::values::specified::align;
766        let justify_items = self.style.get_position().clone_justify_items();
767        if justify_items.specified != align::JustifyItems::legacy() {
768            return;
769        }
770
771        let parent_justify_items = self.style.get_parent_position().clone_justify_items();
772
773        if !parent_justify_items.computed.contains(AlignFlags::LEGACY) {
774            return;
775        }
776
777        if parent_justify_items.computed == justify_items.computed {
778            return;
779        }
780
781        self.style
782            .mutate_position()
783            .set_computed_justify_items(parent_justify_items.computed);
784    }
785
786    /// If '-webkit-appearance' is 'menulist' on a <select> element then
787    /// the computed value of 'line-height' is 'normal'.
788    ///
789    /// https://github.com/w3c/csswg-drafts/issues/3257
790    #[cfg(feature = "gecko")]
791    fn adjust_for_appearance<E>(&mut self, element: Option<E>)
792    where
793        E: TElement,
794    {
795        use crate::properties::longhands::appearance::computed_value::T as Appearance;
796        use crate::properties::longhands::line_height::computed_value::T as LineHeight;
797
798        let box_ = self.style.get_box();
799        let appearance = match box_.clone_appearance() {
800            Appearance::Auto => box_.clone__moz_default_appearance(),
801            a => a,
802        };
803
804        if appearance == Appearance::Menulist {
805            if self.style.get_font().clone_line_height() == LineHeight::normal() {
806                return;
807            }
808            if self.style.pseudo.is_some() {
809                return;
810            }
811            let is_html_select_element = element.map_or(false, |e| {
812                e.is_html_element() && e.local_name() == &*atom!("select")
813            });
814            if !is_html_select_element {
815                return;
816            }
817            self.style
818                .mutate_font()
819                .set_line_height(LineHeight::normal());
820        }
821    }
822
823    /// A legacy ::marker (i.e. no 'content') without an author-specified 'font-family'
824    /// and 'list-style-type:disc|circle|square|disclosure-closed|disclosure-open'
825    /// is assigned 'font-family:-moz-bullet-font'. (This is for <ul><li> etc.)
826    /// We don't want synthesized italic/bold for this font, so turn that off too.
827    /// Likewise for 'letter/word-spacing' -- unless the author specified it then reset
828    /// them to their initial value because traditionally we never added such spacing
829    /// between a legacy bullet and the list item's content, so we keep that behavior
830    /// for web-compat reasons.
831    /// We intentionally don't check 'list-style-image' below since we want it to use
832    /// the same font as its fallback ('list-style-type') in case it fails to load.
833    #[cfg(feature = "gecko")]
834    fn adjust_for_marker_pseudo(&mut self) {
835        use crate::values::computed::counters::Content;
836        use crate::values::computed::font::{FontFamily, FontSynthesis, FontSynthesisStyle};
837        use crate::values::computed::text::{LetterSpacing, WordSpacing};
838
839        let is_legacy_marker = self.style.pseudo.map_or(false, |p| p.is_marker())
840            && self.style.get_list().clone_list_style_type().is_bullet()
841            && self.style.get_counters().clone_content() == Content::Normal;
842        if !is_legacy_marker {
843            return;
844        }
845        let flags = self.style.flags.get();
846        if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_FAMILY) {
847            self.style
848                .mutate_font()
849                .set_font_family(FontFamily::moz_bullet().clone());
850
851            // FIXME(mats): We can remove this if support for font-synthesis is added to @font-face rules.
852            // Then we can add it to the @font-face rule in html.css instead.
853            // https://github.com/w3c/csswg-drafts/issues/6081
854            if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_WEIGHT) {
855                self.style
856                    .mutate_font()
857                    .set_font_synthesis_weight(FontSynthesis::None);
858            }
859            if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_STYLE) {
860                self.style
861                    .mutate_font()
862                    .set_font_synthesis_style(FontSynthesisStyle::None);
863            }
864        }
865        if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_LETTER_SPACING) {
866            self.style
867                .mutate_inherited_text()
868                .set_letter_spacing(LetterSpacing::normal());
869        }
870        if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_WORD_SPACING) {
871            self.style
872                .mutate_inherited_text()
873                .set_word_spacing(WordSpacing::normal());
874        }
875    }
876
877    /// Performs adjustments for position-try-fallbacks. The properties that need adjustments here
878    /// are luckily not affected by previous adjustments nor by other computed-value-time effects,
879    /// so we can just perform them here.
880    ///
881    /// NOTE(emilio): If we ever perform the interleaving dance, this could / should probably move
882    /// around to the specific properties' to_computed_value implementations, but that seems
883    /// overkill for now.
884    fn adjust_for_try_tactic(&mut self, tactic: &PositionTryFallbacksTryTactic) {
885        debug_assert!(!tactic.is_empty());
886        // TODO: This is supposed to use the containing block's WM (bug 1995256).
887        let wm = self.style.writing_mode;
888        // TODO: Flip inset / margin / sizes percentages and anchor lookup sides as necessary.
889        for tactic in tactic.iter() {
890            use PositionTryFallbacksTryTacticKeyword::*;
891            match tactic {
892                FlipBlock => {
893                    self.flip_self_alignment(/* block = */ true);
894                    self.flip_insets_and_margins(/* horizontal = */ wm.is_vertical());
895                },
896                FlipInline => {
897                    self.flip_self_alignment(/* block = */ false);
898                    self.flip_insets_and_margins(/* horizontal = */ wm.is_horizontal());
899                },
900                FlipX => {
901                    self.flip_self_alignment(/* block = */ wm.is_vertical());
902                    self.flip_insets_and_margins(/* horizontal = */ true);
903                },
904                FlipY => {
905                    self.flip_self_alignment(/* block = */ wm.is_horizontal());
906                    self.flip_insets_and_margins(/* horizontal = */ false);
907                },
908                FlipStart => {
909                    self.flip_start();
910                },
911            }
912            self.apply_position_area_tactic(*tactic);
913        }
914    }
915
916    fn apply_position_area_tactic(&mut self, tactic: PositionTryFallbacksTryTacticKeyword) {
917        let pos = self.style.get_position();
918        let old = pos.clone_position_area();
919        let wm = self.style.writing_mode;
920        let new = old.with_tactic(wm, tactic);
921        if new == old {
922            return;
923        }
924        let pos = self.style.mutate_position();
925        pos.set_position_area(new);
926    }
927
928    // TODO: Could avoid some clones here and below.
929    fn swap_insets(&mut self, a_side: PhysicalSide, b_side: PhysicalSide) {
930        debug_assert_ne!(a_side, b_side);
931        let pos = self.style.mutate_position();
932        let mut a = pos.get_inset(a_side).clone();
933        a.try_tactic_adjustment(a_side, b_side);
934        let mut b = pos.get_inset(b_side).clone();
935        b.try_tactic_adjustment(b_side, a_side);
936        pos.set_inset(a_side, b);
937        pos.set_inset(b_side, a);
938    }
939
940    fn swap_margins(&mut self, a_side: PhysicalSide, b_side: PhysicalSide) {
941        debug_assert_ne!(a_side, b_side);
942        let margin = self.style.get_margin();
943        let mut a = margin.get_margin(a_side).clone();
944        a.try_tactic_adjustment(a_side, b_side);
945        let mut b = margin.get_margin(b_side).clone();
946        b.try_tactic_adjustment(b_side, a_side);
947        let margin = self.style.mutate_margin();
948        margin.set_margin(a_side, b);
949        margin.set_margin(b_side, a);
950    }
951
952    fn swap_sizes(&mut self, block_start: PhysicalSide, inline_start: PhysicalSide) {
953        let pos = self.style.mutate_position();
954        let mut min_width = pos.clone_min_width();
955        min_width.try_tactic_adjustment(inline_start, block_start);
956        let mut max_width = pos.clone_max_width();
957        max_width.try_tactic_adjustment(inline_start, block_start);
958        let mut width = pos.clone_width();
959        width.try_tactic_adjustment(inline_start, block_start);
960
961        let mut min_height = pos.clone_min_height();
962        min_height.try_tactic_adjustment(block_start, inline_start);
963        let mut max_height = pos.clone_max_height();
964        max_height.try_tactic_adjustment(block_start, inline_start);
965        let mut height = pos.clone_height();
966        height.try_tactic_adjustment(block_start, inline_start);
967
968        let pos = self.style.mutate_position();
969        pos.set_width(height);
970        pos.set_height(width);
971        pos.set_max_width(max_height);
972        pos.set_max_height(max_width);
973        pos.set_min_width(min_height);
974        pos.set_min_height(min_width);
975    }
976
977    fn flip_start(&mut self) {
978        let wm = self.style.writing_mode;
979        let bs = wm.block_start_physical_side();
980        let is = wm.inline_start_physical_side();
981        let be = wm.block_end_physical_side();
982        let ie = wm.inline_end_physical_side();
983        self.swap_sizes(bs, is);
984        self.swap_insets(bs, is);
985        self.swap_insets(ie, be);
986        self.swap_margins(bs, is);
987        self.swap_margins(ie, be);
988        self.flip_alignment_start();
989    }
990
991    fn flip_insets_and_margins(&mut self, horizontal: bool) {
992        if horizontal {
993            self.swap_insets(PhysicalSide::Left, PhysicalSide::Right);
994            self.swap_margins(PhysicalSide::Left, PhysicalSide::Right);
995        } else {
996            self.swap_insets(PhysicalSide::Top, PhysicalSide::Bottom);
997            self.swap_margins(PhysicalSide::Top, PhysicalSide::Bottom);
998        }
999    }
1000
1001    fn flip_alignment_start(&mut self) {
1002        let pos = self.style.get_position();
1003        let align = pos.clone_align_self();
1004        let mut justify = pos.clone_justify_self();
1005        if align == justify {
1006            return;
1007        }
1008
1009        // Fix-up potential justify-self: {left, right} values which might end up as alignment
1010        // values.
1011        if matches!(justify.value(), AlignFlags::LEFT | AlignFlags::RIGHT) {
1012            let left = justify.value() == AlignFlags::LEFT;
1013            let ltr = self.style.writing_mode.is_bidi_ltr();
1014            justify = justify.with_value(if left == ltr {
1015                AlignFlags::SELF_START
1016            } else {
1017                AlignFlags::SELF_END
1018            });
1019        }
1020
1021        let pos = self.style.mutate_position();
1022        pos.set_align_self(justify);
1023        pos.set_justify_self(align);
1024    }
1025
1026    fn flip_self_alignment(&mut self, block: bool) {
1027        let pos = self.style.get_position();
1028        let cur = if block {
1029            pos.clone_align_self()
1030        } else {
1031            pos.clone_justify_self()
1032        };
1033        let flipped = cur.flip_position();
1034        if flipped == cur {
1035            return;
1036        }
1037        let pos = self.style.mutate_position();
1038        if block {
1039            pos.set_align_self(flipped);
1040        } else {
1041            pos.set_justify_self(flipped);
1042        }
1043    }
1044
1045    /// Adjusts the style to account for various fixups that don't fit naturally into the cascade.
1046    pub fn adjust<E>(
1047        &mut self,
1048        layout_parent_style: &ComputedValues,
1049        element: Option<E>,
1050        try_tactic: &PositionTryFallbacksTryTactic,
1051    ) where
1052        E: TElement,
1053    {
1054        if cfg!(debug_assertions) {
1055            if let Some(e) = element {
1056                if let Some(p) = e.implemented_pseudo_element() {
1057                    // It'd be nice to assert `self.style.pseudo == Some(&pseudo)`,
1058                    // but we do resolve ::-moz-list pseudos on ::before / ::after
1059                    // content, sigh.
1060                    debug_assert!(
1061                        self.style.pseudo.is_some(),
1062                        "Someone really messed up (no pseudo style for {e:?}, {p:?})"
1063                    );
1064                }
1065            }
1066        }
1067        // FIXME(emilio): The apply_declarations callsite in Servo's
1068        // animation, and the font stuff for Gecko
1069        // (Stylist::compute_for_declarations) should pass an element to
1070        // cascade(), then we can make this assertion hold everywhere.
1071        // debug_assert!(
1072        //     element.is_some() || self.style.pseudo.is_some(),
1073        //     "Should always have an element around for non-pseudo styles"
1074        // );
1075
1076        self.adjust_for_visited(element);
1077        #[cfg(feature = "gecko")]
1078        {
1079            self.adjust_for_prohibited_display_contents(element);
1080            self.adjust_for_fieldset_content();
1081            // NOTE: It's important that this happens before
1082            // adjust_for_overflow.
1083            self.adjust_for_text_control_editing_root();
1084        }
1085        self.adjust_for_top_layer();
1086        self.blockify_if_necessary(layout_parent_style, element);
1087        #[cfg(feature = "gecko")]
1088        self.adjust_for_webkit_line_clamp();
1089        self.adjust_for_position();
1090        self.adjust_for_overflow();
1091        #[cfg(feature = "gecko")]
1092        {
1093            self.adjust_for_contain();
1094            self.adjust_for_contain_intrinsic_size();
1095            self.adjust_for_justify_items();
1096        }
1097        self.adjust_for_table_text_align();
1098        self.adjust_for_writing_mode(layout_parent_style);
1099        #[cfg(feature = "gecko")]
1100        {
1101            self.adjust_for_ruby(element);
1102            self.adjust_for_appearance(element);
1103            self.adjust_for_marker_pseudo();
1104        }
1105        if !try_tactic.is_empty() {
1106            self.adjust_for_try_tactic(try_tactic);
1107        }
1108        self.set_bits();
1109    }
1110}