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