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