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