style/
style_adjuster.rs

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