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::{self, 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    /// The initial value of border-*-width may be changed at computed value
411    /// time.
412    ///
413    /// This is moved to properties.rs for convenience.
414    fn adjust_for_border_width(&mut self) {
415        properties::adjust_border_width(self.style);
416    }
417
418    /// column-rule-style: none causes a computed column-rule-width of zero
419    /// at computed value time.
420    #[cfg(feature = "gecko")]
421    fn adjust_for_column_rule_width(&mut self) {
422        let column_style = self.style.get_column();
423        if !column_style.clone_column_rule_style().none_or_hidden() {
424            return;
425        }
426        if !column_style.column_rule_has_nonzero_width() {
427            return;
428        }
429        self.style
430            .mutate_column()
431            .set_column_rule_width(crate::Zero::zero());
432    }
433
434    /// outline-style: none causes a computed outline-width of zero at computed
435    /// value time.
436    fn adjust_for_outline_width(&mut self) {
437        let outline = self.style.get_outline();
438        if !outline.clone_outline_style().none_or_hidden() {
439            return;
440        }
441        if !outline.outline_has_nonzero_width() {
442            return;
443        }
444        self.style
445            .mutate_outline()
446            .set_outline_width(crate::Zero::zero());
447    }
448
449    /// CSS overflow-x and overflow-y require some fixup as well in some cases.
450    /// https://drafts.csswg.org/css-overflow-3/#overflow-properties
451    /// "Computed value: as specified, except with `visible`/`clip` computing to
452    /// `auto`/`hidden` (respectively) if one of `overflow-x` or `overflow-y` is
453    /// neither `visible` nor `clip`."
454    fn adjust_for_overflow(&mut self) {
455        let overflow_x = self.style.get_box().clone_overflow_x();
456        let overflow_y = self.style.get_box().clone_overflow_y();
457        if overflow_x == overflow_y {
458            return; // optimization for the common case
459        }
460
461        if overflow_x.is_scrollable() != overflow_y.is_scrollable() {
462            let box_style = self.style.mutate_box();
463            box_style.set_overflow_x(overflow_x.to_scrollable());
464            box_style.set_overflow_y(overflow_y.to_scrollable());
465        }
466    }
467
468    #[cfg(feature = "gecko")]
469    fn adjust_for_contain(&mut self) {
470        let box_style = self.style.get_box();
471        let container_type = box_style.clone_container_type();
472        let content_visibility = box_style.clone_content_visibility();
473        if !container_type.is_size_container_type()
474            && content_visibility == ContentVisibility::Visible
475        {
476            debug_assert_eq!(
477                box_style.clone_contain(),
478                box_style.clone_effective_containment()
479            );
480            return;
481        }
482        let old_contain = box_style.clone_contain();
483        let mut new_contain = old_contain;
484        match content_visibility {
485            ContentVisibility::Visible => {},
486            // `content-visibility:auto` also applies size containment when content
487            // is not relevant (and therefore skipped). This is checked in
488            // nsIFrame::GetContainSizeAxes.
489            ContentVisibility::Auto => {
490                new_contain.insert(Contain::LAYOUT | Contain::PAINT | Contain::STYLE)
491            },
492            ContentVisibility::Hidden => new_contain
493                .insert(Contain::LAYOUT | Contain::PAINT | Contain::SIZE | Contain::STYLE),
494        }
495        if container_type.intersects(ContainerType::INLINE_SIZE) {
496            // https://drafts.csswg.org/css-contain-3/#valdef-container-type-inline-size:
497            //     Applies layout containment, style containment, and inline-size
498            //     containment to the principal box.
499            new_contain.insert(Contain::STYLE | Contain::INLINE_SIZE);
500        } else if container_type.intersects(ContainerType::SIZE) {
501            // https://drafts.csswg.org/css-contain-3/#valdef-container-type-size:
502            //     Applies layout containment, style containment, and size
503            //     containment to the principal box.
504            new_contain.insert(Contain::STYLE | Contain::SIZE);
505        }
506        if new_contain == old_contain {
507            debug_assert_eq!(
508                box_style.clone_contain(),
509                box_style.clone_effective_containment()
510            );
511            return;
512        }
513        self.style
514            .mutate_box()
515            .set_effective_containment(new_contain);
516    }
517
518    /// content-visibility: auto should force contain-intrinsic-size to gain
519    /// an auto value
520    ///
521    /// <https://github.com/w3c/csswg-drafts/issues/8407>
522    #[cfg(feature = "gecko")]
523    fn adjust_for_contain_intrinsic_size(&mut self) {
524        let content_visibility = self.style.get_box().clone_content_visibility();
525        if content_visibility != ContentVisibility::Auto {
526            return;
527        }
528
529        let pos = self.style.get_position();
530        let new_width = pos.clone_contain_intrinsic_width().add_auto_if_needed();
531        let new_height = pos.clone_contain_intrinsic_height().add_auto_if_needed();
532        if new_width.is_none() && new_height.is_none() {
533            return;
534        }
535
536        let pos = self.style.mutate_position();
537        if let Some(width) = new_width {
538            pos.set_contain_intrinsic_width(width);
539        }
540        if let Some(height) = new_height {
541            pos.set_contain_intrinsic_height(height);
542        }
543    }
544
545    /// Handles the relevant sections in:
546    ///
547    /// https://drafts.csswg.org/css-display/#unbox-html
548    ///
549    /// And forbidding display: contents in pseudo-elements, at least for now.
550    #[cfg(feature = "gecko")]
551    fn adjust_for_prohibited_display_contents<E>(&mut self, element: Option<E>)
552    where
553        E: TElement,
554    {
555        if self.style.get_box().clone_display() != Display::Contents {
556            return;
557        }
558
559        // FIXME(emilio): ::before and ::after should support display: contents, see bug 1418138.
560        if self.style.pseudo.is_some_and(|p| !p.is_element_backed()) {
561            self.style.mutate_box().set_display(Display::Inline);
562            return;
563        }
564
565        let element = match element {
566            Some(e) => e,
567            None => return,
568        };
569
570        if is_effective_display_none_for_display_contents(element) {
571            self.style.mutate_box().set_display(Display::None);
572        }
573    }
574
575    /// <textarea>'s editor root needs to inherit the overflow value from its
576    /// parent, but we need to make sure it's still scrollable.
577    #[cfg(feature = "gecko")]
578    fn adjust_for_text_control_editing_root(&mut self) {
579        use crate::properties::longhands::white_space_collapse::computed_value::T as WhiteSpaceCollapse;
580        use crate::selector_parser::PseudoElement;
581
582        if self.style.pseudo != Some(&PseudoElement::MozTextControlEditingRoot) {
583            return;
584        }
585
586        let old_collapse = self.style.get_inherited_text().clone_white_space_collapse();
587        let new_collapse = match old_collapse {
588            WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces => old_collapse,
589            WhiteSpaceCollapse::Collapse
590            | WhiteSpaceCollapse::PreserveSpaces
591            | WhiteSpaceCollapse::PreserveBreaks => WhiteSpaceCollapse::Preserve,
592        };
593        if new_collapse != old_collapse {
594            self.style
595                .mutate_inherited_text()
596                .set_white_space_collapse(new_collapse);
597        }
598
599        let box_style = self.style.get_box();
600        let overflow_x = box_style.clone_overflow_x();
601        let overflow_y = box_style.clone_overflow_y();
602
603        // If at least one is scrollable we'll adjust the other one in
604        // adjust_for_overflow if needed.
605        if overflow_x.is_scrollable() || overflow_y.is_scrollable() {
606            return;
607        }
608
609        let box_style = self.style.mutate_box();
610        box_style.set_overflow_x(Overflow::Auto);
611        box_style.set_overflow_y(Overflow::Auto);
612    }
613
614    /// If a <fieldset> has grid/flex display type, we need to inherit
615    /// this type into its ::-moz-fieldset-content anonymous box.
616    ///
617    /// NOTE(emilio): We don't need to handle the display change for this case
618    /// in matching.rs because anonymous box restyling works separately to the
619    /// normal cascading process.
620    #[cfg(feature = "gecko")]
621    fn adjust_for_fieldset_content(&mut self, layout_parent_style: &ComputedValues) {
622        use crate::selector_parser::PseudoElement;
623
624        if self.style.pseudo != Some(&PseudoElement::FieldsetContent) {
625            return;
626        }
627
628        // TODO We actually want style from parent rather than layout
629        // parent, so that this fixup doesn't happen incorrectly when
630        // when <fieldset> has "display: contents".
631        let parent_display = layout_parent_style.get_box().clone_display();
632        let new_display = match parent_display {
633            Display::Flex | Display::InlineFlex => Some(Display::Flex),
634            Display::Grid | Display::InlineGrid => Some(Display::Grid),
635            _ => None,
636        };
637        if let Some(new_display) = new_display {
638            self.style.mutate_box().set_display(new_display);
639        }
640    }
641
642    /// -moz-center, -moz-left and -moz-right are used for HTML's alignment.
643    ///
644    /// This is covering the <div align="right"><table>...</table></div> case.
645    ///
646    /// In this case, we don't want to inherit the text alignment into the
647    /// table.
648    fn adjust_for_table_text_align(&mut self) {
649        use crate::properties::longhands::text_align::computed_value::T as TextAlign;
650        if self.style.get_box().clone_display() != Display::Table {
651            return;
652        }
653
654        match self.style.get_inherited_text().clone_text_align() {
655            TextAlign::MozLeft | TextAlign::MozCenter | TextAlign::MozRight => {},
656            _ => return,
657        }
658
659        self.style
660            .mutate_inherited_text()
661            .set_text_align(TextAlign::Start)
662    }
663
664    #[cfg(feature = "gecko")]
665    fn should_suppress_linebreak<E>(
666        &self,
667        layout_parent_style: &ComputedValues,
668        element: Option<E>,
669    ) -> bool
670    where
671        E: TElement,
672    {
673        // Line break suppression should only be propagated to in-flow children.
674        if self.style.is_floating() || self.style.is_absolutely_positioned() {
675            return false;
676        }
677        let parent_display = layout_parent_style.get_box().clone_display();
678        if layout_parent_style
679            .flags
680            .contains(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK)
681        {
682            // Line break suppression is propagated to any children of
683            // line participants.
684            if parent_display.is_line_participant() {
685                return true;
686            }
687        }
688        match self.style.get_box().clone_display() {
689            // Ruby base and text are always non-breakable.
690            Display::RubyBase | Display::RubyText => true,
691            // Ruby base container and text container are breakable.
692            // Non-HTML elements may not form ruby base / text container because
693            // they may not respect ruby-internal display values, so we can't
694            // make them escaped from line break suppression.
695            // Note that, when certain HTML tags, e.g. form controls, have ruby
696            // level container display type, they could also escape from the
697            // line break suppression flag while they shouldn't. However, it is
698            // generally fine as far as they can't break the line inside them.
699            Display::RubyBaseContainer | Display::RubyTextContainer
700                if element.map_or(true, |e| e.is_html_element()) =>
701            {
702                false
703            },
704            // Anything else is non-breakable if and only if its layout parent
705            // has a ruby display type, because any of the ruby boxes can be
706            // anonymous.
707            _ => parent_display.is_ruby_type(),
708        }
709    }
710
711    /// Do ruby-related style adjustments, which include:
712    /// * propagate the line break suppression flag,
713    /// * inlinify block descendants,
714    /// * suppress border and padding for ruby level containers,
715    /// * correct unicode-bidi.
716    #[cfg(feature = "gecko")]
717    fn adjust_for_ruby<E>(&mut self, layout_parent_style: &ComputedValues, element: Option<E>)
718    where
719        E: TElement,
720    {
721        use crate::properties::longhands::unicode_bidi::computed_value::T as UnicodeBidi;
722
723        let self_display = self.style.get_box().clone_display();
724        // Check whether line break should be suppressed for this element.
725        if self.should_suppress_linebreak(layout_parent_style, element) {
726            self.style
727                .add_flags(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK);
728            // Inlinify the display type if allowed.
729            if !self.skip_item_display_fixup(element) {
730                let inline_display = self_display.inlinify();
731                if self_display != inline_display {
732                    self.style
733                        .mutate_box()
734                        .set_adjusted_display(inline_display, false);
735                }
736            }
737        }
738        // Suppress border and padding for ruby level containers.
739        // This is actually not part of the spec. It is currently unspecified
740        // how border and padding should be handled for ruby level container,
741        // and suppressing them here make it easier for layout to handle.
742        if self_display.is_ruby_level_container() {
743            self.style.reset_border_struct();
744            self.style.reset_padding_struct();
745        }
746
747        // Force bidi isolation on all internal ruby boxes and ruby container
748        // per spec https://drafts.csswg.org/css-ruby-1/#bidi
749        if self_display.is_ruby_type() {
750            let new_value = match self.style.get_text().clone_unicode_bidi() {
751                UnicodeBidi::Normal | UnicodeBidi::Embed => Some(UnicodeBidi::Isolate),
752                UnicodeBidi::BidiOverride => Some(UnicodeBidi::IsolateOverride),
753                _ => None,
754            };
755            if let Some(new_value) = new_value {
756                self.style.mutate_text().set_unicode_bidi(new_value);
757            }
758        }
759    }
760
761    /// Computes the RELEVANT_LINK_VISITED flag based on the parent style and on
762    /// whether we're a relevant link.
763    ///
764    /// NOTE(emilio): We don't do this for text styles, which is... dubious, but
765    /// Gecko doesn't seem to do it either. It's extremely easy to do if needed
766    /// though.
767    ///
768    /// FIXME(emilio): This isn't technically a style adjustment thingie, could
769    /// it move somewhere else?
770    fn adjust_for_visited<E>(&mut self, element: Option<E>)
771    where
772        E: TElement,
773    {
774        if !self.style.has_visited_style() {
775            return;
776        }
777
778        let is_link_element = self.style.pseudo.is_none() && element.map_or(false, |e| e.is_link());
779
780        if !is_link_element {
781            return;
782        }
783
784        if element.unwrap().is_visited_link() {
785            self.style
786                .add_flags(ComputedValueFlags::IS_RELEVANT_LINK_VISITED);
787        } else {
788            // Need to remove to handle unvisited link inside visited.
789            self.style
790                .remove_flags(ComputedValueFlags::IS_RELEVANT_LINK_VISITED);
791        }
792    }
793
794    /// Resolves "justify-items: legacy" based on the inherited style if needed
795    /// to comply with:
796    ///
797    /// <https://drafts.csswg.org/css-align/#valdef-justify-items-legacy>
798    #[cfg(feature = "gecko")]
799    fn adjust_for_justify_items(&mut self) {
800        use crate::values::specified::align;
801        let justify_items = self.style.get_position().clone_justify_items();
802        if justify_items.specified != align::JustifyItems::legacy() {
803            return;
804        }
805
806        let parent_justify_items = self.style.get_parent_position().clone_justify_items();
807
808        if !parent_justify_items.computed.contains(AlignFlags::LEGACY) {
809            return;
810        }
811
812        if parent_justify_items.computed == justify_items.computed {
813            return;
814        }
815
816        self.style
817            .mutate_position()
818            .set_computed_justify_items(parent_justify_items.computed);
819    }
820
821    /// If '-webkit-appearance' is 'menulist' on a <select> element then
822    /// the computed value of 'line-height' is 'normal'.
823    ///
824    /// https://github.com/w3c/csswg-drafts/issues/3257
825    #[cfg(feature = "gecko")]
826    fn adjust_for_appearance<E>(&mut self, element: Option<E>)
827    where
828        E: TElement,
829    {
830        use crate::properties::longhands::appearance::computed_value::T as Appearance;
831        use crate::properties::longhands::line_height::computed_value::T as LineHeight;
832
833        let box_ = self.style.get_box();
834        let appearance = match box_.clone_appearance() {
835            Appearance::Auto => box_.clone__moz_default_appearance(),
836            a => a,
837        };
838
839        if appearance == Appearance::Menulist {
840            if self.style.get_font().clone_line_height() == LineHeight::normal() {
841                return;
842            }
843            if self.style.pseudo.is_some() {
844                return;
845            }
846            let is_html_select_element = element.map_or(false, |e| {
847                e.is_html_element() && e.local_name() == &*atom!("select")
848            });
849            if !is_html_select_element {
850                return;
851            }
852            self.style
853                .mutate_font()
854                .set_line_height(LineHeight::normal());
855        }
856    }
857
858    /// A legacy ::marker (i.e. no 'content') without an author-specified 'font-family'
859    /// and 'list-style-type:disc|circle|square|disclosure-closed|disclosure-open'
860    /// is assigned 'font-family:-moz-bullet-font'. (This is for <ul><li> etc.)
861    /// We don't want synthesized italic/bold for this font, so turn that off too.
862    /// Likewise for 'letter/word-spacing' -- unless the author specified it then reset
863    /// them to their initial value because traditionally we never added such spacing
864    /// between a legacy bullet and the list item's content, so we keep that behavior
865    /// for web-compat reasons.
866    /// We intentionally don't check 'list-style-image' below since we want it to use
867    /// the same font as its fallback ('list-style-type') in case it fails to load.
868    #[cfg(feature = "gecko")]
869    fn adjust_for_marker_pseudo(&mut self) {
870        use crate::values::computed::counters::Content;
871        use crate::values::computed::font::{FontFamily, FontSynthesis, FontSynthesisStyle};
872        use crate::values::computed::text::{LetterSpacing, WordSpacing};
873
874        let is_legacy_marker = self.style.pseudo.map_or(false, |p| p.is_marker())
875            && self.style.get_list().clone_list_style_type().is_bullet()
876            && self.style.get_counters().clone_content() == Content::Normal;
877        if !is_legacy_marker {
878            return;
879        }
880        let flags = self.style.flags.get();
881        if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_FAMILY) {
882            self.style
883                .mutate_font()
884                .set_font_family(FontFamily::moz_bullet().clone());
885
886            // FIXME(mats): We can remove this if support for font-synthesis is added to @font-face rules.
887            // Then we can add it to the @font-face rule in html.css instead.
888            // https://github.com/w3c/csswg-drafts/issues/6081
889            if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_WEIGHT) {
890                self.style
891                    .mutate_font()
892                    .set_font_synthesis_weight(FontSynthesis::None);
893            }
894            if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_STYLE) {
895                self.style
896                    .mutate_font()
897                    .set_font_synthesis_style(FontSynthesisStyle::None);
898            }
899        }
900        if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_LETTER_SPACING) {
901            self.style
902                .mutate_inherited_text()
903                .set_letter_spacing(LetterSpacing::normal());
904        }
905        if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_WORD_SPACING) {
906            self.style
907                .mutate_inherited_text()
908                .set_word_spacing(WordSpacing::normal());
909        }
910    }
911
912    /// Performs adjustments for position-try-fallbacks. The properties that need adjustments here
913    /// are luckily not affected by previous adjustments nor by other computed-value-time effects,
914    /// so we can just perform them here.
915    ///
916    /// NOTE(emilio): If we ever perform the interleaving dance, this could / should probably move
917    /// around to the specific properties' to_computed_value implementations, but that seems
918    /// overkill for now.
919    fn adjust_for_try_tactic(&mut self, tactic: &PositionTryFallbacksTryTactic) {
920        debug_assert!(!tactic.is_empty());
921        // TODO: This is supposed to use the containing block's WM (bug 1995256).
922        let wm = self.style.writing_mode;
923        // TODO: Flip inset / margin / sizes percentages and anchor lookup sides as necessary.
924        for tactic in tactic.iter() {
925            use PositionTryFallbacksTryTacticKeyword::*;
926            match tactic {
927                FlipBlock => {
928                    self.flip_self_alignment(/* block = */ true);
929                    self.flip_insets_and_margins(/* horizontal = */ wm.is_vertical());
930                },
931                FlipInline => {
932                    self.flip_self_alignment(/* block = */ false);
933                    self.flip_insets_and_margins(/* horizontal = */ wm.is_horizontal());
934                },
935                FlipX => {
936                    self.flip_self_alignment(/* block = */ wm.is_vertical());
937                    self.flip_insets_and_margins(/* horizontal = */ true);
938                },
939                FlipY => {
940                    self.flip_self_alignment(/* block = */ wm.is_horizontal());
941                    self.flip_insets_and_margins(/* horizontal = */ false);
942                },
943                FlipStart => {
944                    self.flip_start();
945                },
946            }
947            self.apply_position_area_tactic(*tactic);
948        }
949    }
950
951    fn apply_position_area_tactic(&mut self, tactic: PositionTryFallbacksTryTacticKeyword) {
952        let pos = self.style.get_position();
953        let old = pos.clone_position_area();
954        let wm = self.style.writing_mode;
955        let new = old.with_tactic(wm, tactic);
956        if new == old {
957            return;
958        }
959        let pos = self.style.mutate_position();
960        pos.set_position_area(new);
961    }
962
963    // TODO: Could avoid some clones here and below.
964    fn swap_insets(&mut self, a_side: PhysicalSide, b_side: PhysicalSide) {
965        debug_assert_ne!(a_side, b_side);
966        let pos = self.style.mutate_position();
967        let mut a = pos.get_inset(a_side).clone();
968        a.try_tactic_adjustment(a_side, b_side);
969        let mut b = pos.get_inset(b_side).clone();
970        b.try_tactic_adjustment(b_side, a_side);
971        pos.set_inset(a_side, b);
972        pos.set_inset(b_side, a);
973    }
974
975    fn swap_margins(&mut self, a_side: PhysicalSide, b_side: PhysicalSide) {
976        debug_assert_ne!(a_side, b_side);
977        let margin = self.style.get_margin();
978        let mut a = margin.get_margin(a_side).clone();
979        a.try_tactic_adjustment(a_side, b_side);
980        let mut b = margin.get_margin(b_side).clone();
981        b.try_tactic_adjustment(b_side, a_side);
982        let margin = self.style.mutate_margin();
983        margin.set_margin(a_side, b);
984        margin.set_margin(b_side, a);
985    }
986
987    fn swap_sizes(&mut self, block_start: PhysicalSide, inline_start: PhysicalSide) {
988        let pos = self.style.mutate_position();
989        let mut min_width = pos.clone_min_width();
990        min_width.try_tactic_adjustment(inline_start, block_start);
991        let mut max_width = pos.clone_max_width();
992        max_width.try_tactic_adjustment(inline_start, block_start);
993        let mut width = pos.clone_width();
994        width.try_tactic_adjustment(inline_start, block_start);
995
996        let mut min_height = pos.clone_min_height();
997        min_height.try_tactic_adjustment(block_start, inline_start);
998        let mut max_height = pos.clone_max_height();
999        max_height.try_tactic_adjustment(block_start, inline_start);
1000        let mut height = pos.clone_height();
1001        height.try_tactic_adjustment(block_start, inline_start);
1002
1003        let pos = self.style.mutate_position();
1004        pos.set_width(height);
1005        pos.set_height(width);
1006        pos.set_max_width(max_height);
1007        pos.set_max_height(max_width);
1008        pos.set_min_width(min_height);
1009        pos.set_min_height(min_width);
1010    }
1011
1012    fn flip_start(&mut self) {
1013        let wm = self.style.writing_mode;
1014        let bs = wm.block_start_physical_side();
1015        let is = wm.inline_start_physical_side();
1016        let be = wm.block_end_physical_side();
1017        let ie = wm.inline_end_physical_side();
1018        self.swap_sizes(bs, is);
1019        self.swap_insets(bs, is);
1020        self.swap_insets(ie, be);
1021        self.swap_margins(bs, is);
1022        self.swap_margins(ie, be);
1023        self.flip_alignment_start();
1024    }
1025
1026    fn flip_insets_and_margins(&mut self, horizontal: bool) {
1027        if horizontal {
1028            self.swap_insets(PhysicalSide::Left, PhysicalSide::Right);
1029            self.swap_margins(PhysicalSide::Left, PhysicalSide::Right);
1030        } else {
1031            self.swap_insets(PhysicalSide::Top, PhysicalSide::Bottom);
1032            self.swap_margins(PhysicalSide::Top, PhysicalSide::Bottom);
1033        }
1034    }
1035
1036    fn flip_alignment_start(&mut self) {
1037        let pos = self.style.get_position();
1038        let align = pos.clone_align_self();
1039        let mut justify = pos.clone_justify_self();
1040        if align == justify {
1041            return;
1042        }
1043
1044        // Fix-up potential justify-self: {left, right} values which might end up as alignment
1045        // values.
1046        if matches!(justify.value(), AlignFlags::LEFT | AlignFlags::RIGHT) {
1047            let left = justify.value() == AlignFlags::LEFT;
1048            let ltr = self.style.writing_mode.is_bidi_ltr();
1049            justify = justify.with_value(if left == ltr {
1050                AlignFlags::SELF_START
1051            } else {
1052                AlignFlags::SELF_END
1053            });
1054        }
1055
1056        let pos = self.style.mutate_position();
1057        pos.set_align_self(justify);
1058        pos.set_justify_self(align);
1059    }
1060
1061    fn flip_self_alignment(&mut self, block: bool) {
1062        let pos = self.style.get_position();
1063        let cur = if block {
1064            pos.clone_align_self()
1065        } else {
1066            pos.clone_justify_self()
1067        };
1068        let flipped = cur.flip_position();
1069        if flipped == cur {
1070            return;
1071        }
1072        let pos = self.style.mutate_position();
1073        if block {
1074            pos.set_align_self(flipped);
1075        } else {
1076            pos.set_justify_self(flipped);
1077        }
1078    }
1079
1080    /// Adjusts the style to account for various fixups that don't fit naturally into the cascade.
1081    pub fn adjust<E>(
1082        &mut self,
1083        layout_parent_style: &ComputedValues,
1084        element: Option<E>,
1085        try_tactic: &PositionTryFallbacksTryTactic,
1086    ) where
1087        E: TElement,
1088    {
1089        if cfg!(debug_assertions) {
1090            if let Some(e) = element {
1091                if let Some(p) = e.implemented_pseudo_element() {
1092                    // It'd be nice to assert `self.style.pseudo == Some(&pseudo)`,
1093                    // but we do resolve ::-moz-list pseudos on ::before / ::after
1094                    // content, sigh.
1095                    debug_assert!(
1096                        self.style.pseudo.is_some(),
1097                        "Someone really messed up (no pseudo style for {e:?}, {p:?})"
1098                    );
1099                }
1100            }
1101        }
1102        // FIXME(emilio): The apply_declarations callsite in Servo's
1103        // animation, and the font stuff for Gecko
1104        // (Stylist::compute_for_declarations) should pass an element to
1105        // cascade(), then we can make this assertion hold everywhere.
1106        // debug_assert!(
1107        //     element.is_some() || self.style.pseudo.is_some(),
1108        //     "Should always have an element around for non-pseudo styles"
1109        // );
1110
1111        self.adjust_for_visited(element);
1112        #[cfg(feature = "gecko")]
1113        {
1114            self.adjust_for_prohibited_display_contents(element);
1115            self.adjust_for_fieldset_content(layout_parent_style);
1116            // NOTE: It's important that this happens before
1117            // adjust_for_overflow.
1118            self.adjust_for_text_control_editing_root();
1119        }
1120        self.adjust_for_top_layer();
1121        self.blockify_if_necessary(layout_parent_style, element);
1122        #[cfg(feature = "gecko")]
1123        self.adjust_for_webkit_line_clamp();
1124        self.adjust_for_position();
1125        self.adjust_for_overflow();
1126        #[cfg(feature = "gecko")]
1127        {
1128            self.adjust_for_contain();
1129            self.adjust_for_contain_intrinsic_size();
1130            self.adjust_for_justify_items();
1131        }
1132        self.adjust_for_table_text_align();
1133        self.adjust_for_border_width();
1134        #[cfg(feature = "gecko")]
1135        self.adjust_for_column_rule_width();
1136        self.adjust_for_outline_width();
1137        self.adjust_for_writing_mode(layout_parent_style);
1138        #[cfg(feature = "gecko")]
1139        {
1140            self.adjust_for_ruby(layout_parent_style, element);
1141            self.adjust_for_appearance(element);
1142            self.adjust_for_marker_pseudo();
1143        }
1144        if !try_tactic.is_empty() {
1145            self.adjust_for_try_tactic(try_tactic);
1146        }
1147        self.set_bits();
1148    }
1149}