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