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