Skip to main content

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