/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! A struct to encapsulate all the style fixups and flags propagations //! a computed style needs in order for it to adhere to the CSS spec. use crate::computed_value_flags::ComputedValueFlags; use crate::dom::TElement; use crate::properties::longhands::display::computed_value::T as Display; use crate::properties::longhands::float::computed_value::T as Float; use crate::properties::longhands::overflow_x::computed_value::T as Overflow; use crate::properties::longhands::position::computed_value::T as Position; use crate::properties::{self, ComputedValues, StyleBuilder}; /// A struct that implements all the adjustment methods. /// /// NOTE(emilio): If new adjustments are introduced that depend on reset /// properties of the parent, you may need tweaking the /// `ChildCascadeRequirement` code in `matching.rs`. /// /// NOTE(emilio): Also, if new adjustments are introduced that break the /// following invariant: /// /// Given same tag name, namespace, rules and parent style, two elements would /// end up with exactly the same style. /// /// Then you need to adjust the lookup_by_rules conditions in the sharing cache. pub struct StyleAdjuster<'a, 'b: 'a> { style: &'a mut StyleBuilder<'b>, } #[cfg(feature = "gecko")] fn is_topmost_svg_svg_element(e: E) -> bool where E: TElement, { debug_assert!(e.is_svg_element()); if e.local_name() != &*atom!("svg") { return false; } let parent = match e.traversal_parent() { Some(n) => n, None => return true, }; if !parent.is_svg_element() { return true; } parent.local_name() == &*atom!("foreignObject") } // https://drafts.csswg.org/css-display/#unbox #[cfg(feature = "gecko")] fn is_effective_display_none_for_display_contents(element: E) -> bool where E: TElement, { use crate::Atom; const SPECIAL_HTML_ELEMENTS: [Atom; 16] = [ atom!("br"), atom!("wbr"), atom!("meter"), atom!("progress"), atom!("canvas"), atom!("embed"), atom!("object"), atom!("audio"), atom!("iframe"), atom!("img"), atom!("video"), atom!("frame"), atom!("frameset"), atom!("input"), atom!("textarea"), atom!("select"), ]; // https://drafts.csswg.org/css-display/#unbox-svg // // There's a note about "Unknown elements", but there's not a good way to // know what that means, or to get that information from here, and no other // UA implements this either. const SPECIAL_SVG_ELEMENTS: [Atom; 6] = [ atom!("svg"), atom!("a"), atom!("g"), atom!("use"), atom!("tspan"), atom!("textPath"), ]; // https://drafts.csswg.org/css-display/#unbox-html if element.is_html_element() { let local_name = element.local_name(); return SPECIAL_HTML_ELEMENTS .iter() .any(|name| &**name == local_name); } // https://drafts.csswg.org/css-display/#unbox-svg if element.is_svg_element() { if is_topmost_svg_svg_element(element) { return true; } let local_name = element.local_name(); return !SPECIAL_SVG_ELEMENTS .iter() .any(|name| &**name == local_name); } // https://drafts.csswg.org/css-display/#unbox-mathml // // We always treat XUL as display: none. We don't use display: // contents in XUL anyway, so should be fine to be consistent with // MathML unless there's a use case for it. if element.is_mathml_element() || element.is_xul_element() { return true; } false } impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> { /// Trivially constructs a new StyleAdjuster. #[inline] pub fn new(style: &'a mut StyleBuilder<'b>) -> Self { StyleAdjuster { style } } /// /// /// Any position value other than 'absolute' and 'fixed' are /// computed to 'absolute' if the element is in a top layer. /// fn adjust_for_top_layer(&mut self) { if !self.style.is_absolutely_positioned() && self.style.in_top_layer() { self.style.mutate_box().set_position(Position::Absolute); } } /// CSS 2.1 section 9.7: /// /// If 'position' has the value 'absolute' or 'fixed', [...] the computed /// value of 'float' is 'none'. /// fn adjust_for_position(&mut self) { if self.style.is_absolutely_positioned() && self.style.is_floating() { self.style.mutate_box().set_float(Float::None); } } /// https://html.spec.whatwg.org/multipage/interaction.html#inert-subtrees /// /// If -moz-inert is applied then add: /// -moz-user-focus: none; /// -moz-user-input: none; /// -moz-user-modify: read-only; /// user-select: none; /// pointer-events: none; /// cursor: default; /// /// NOTE: dialog:-moz-topmost-modal-dialog is used to override above /// rules to remove the inertness for the topmost modal dialog. /// /// NOTE: If this or the pointer-events tweak is removed, then /// minimal-xul.css and the scrollbar style caching need to be tweaked. fn adjust_for_inert(&mut self) { use crate::values::specified::ui::CursorKind; use crate::values::specified::ui::UserSelect; use properties::longhands::_moz_inert::computed_value::T as Inert; use properties::longhands::_moz_user_focus::computed_value::T as UserFocus; use properties::longhands::_moz_user_input::computed_value::T as UserInput; use properties::longhands::_moz_user_modify::computed_value::T as UserModify; use properties::longhands::cursor::computed_value::T as Cursor; use properties::longhands::pointer_events::computed_value::T as PointerEvents; let needs_update = { let ui = self.style.get_inherited_ui(); if ui.clone__moz_inert() == Inert::None { return; } ui.clone__moz_user_focus() != UserFocus::None || ui.clone__moz_user_input() != UserInput::None || ui.clone__moz_user_modify() != UserModify::ReadOnly || ui.clone_pointer_events() != PointerEvents::None || ui.clone_cursor().keyword != CursorKind::Default || ui.clone_cursor().images != Default::default() }; if needs_update { let ui = self.style.mutate_inherited_ui(); ui.set__moz_user_focus(UserFocus::None); ui.set__moz_user_input(UserInput::None); ui.set__moz_user_modify(UserModify::ReadOnly); ui.set_pointer_events(PointerEvents::None); ui.set_cursor(Cursor { images: Default::default(), keyword: CursorKind::Default, }); } if self.style.get_ui().clone_user_select() != UserSelect::None { self.style.mutate_ui().set_user_select(UserSelect::None); } } /// Whether we should skip any item-based display property blockification on /// this element. fn skip_item_display_fixup(&self, element: Option) -> bool where E: TElement, { if let Some(pseudo) = self.style.pseudo { return pseudo.skip_item_display_fixup(); } element.map_or(false, |e| e.skip_item_display_fixup()) } /// Apply the blockification rules based on the table in CSS 2.2 section 9.7. /// /// A ::marker pseudo-element with 'list-style-position:outside' needs to /// have its 'display' blockified, unless the ::marker is for an inline /// list-item (for which 'list-style-position:outside' behaves as 'inside'). /// https://drafts.csswg.org/css-lists-3/#list-style-position-property fn blockify_if_necessary(&mut self, layout_parent_style: &ComputedValues, element: Option) where E: TElement, { #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] use crate::computed_values::list_style_position::T as ListStylePosition; let mut blockify = false; macro_rules! blockify_if { ($if_what:expr) => { if !blockify { blockify = $if_what; } }; } blockify_if!(self.style.is_root_element); if !self.skip_item_display_fixup(element) { let parent_display = layout_parent_style.get_box().clone_display(); blockify_if!(parent_display.is_item_container()); } let is_item_or_root = blockify; blockify_if!(self.style.is_floating()); blockify_if!(self.style.is_absolutely_positioned()); #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] blockify_if!( self.style.pseudo.map_or(false, |p| p.is_marker()) && self.style.get_parent_list().clone_list_style_position() == ListStylePosition::Outside && !layout_parent_style .get_box() .clone_display() .is_inline_flow() ); if !blockify { return; } let display = self.style.get_box().clone_display(); let blockified_display = display.equivalent_block_display(self.style.is_root_element); if display != blockified_display { self.style .mutate_box() .set_adjusted_display(blockified_display, is_item_or_root); } } /// Compute a few common flags for both text and element's style. fn set_bits(&mut self) { let display = self.style.get_box().clone_display(); if !display.is_contents() { if !self .style .get_text() .clone_text_decoration_line() .is_empty() { self.style .add_flags(ComputedValueFlags::HAS_TEXT_DECORATION_LINES); } if self.style.get_effects().clone_opacity() == 0. { self.style .add_flags(ComputedValueFlags::IS_IN_OPACITY_ZERO_SUBTREE); } } if self.style.is_pseudo_element() { self.style .add_flags(ComputedValueFlags::IS_IN_PSEUDO_ELEMENT_SUBTREE); } if self.style.is_root_element { self.style .add_flags(ComputedValueFlags::IS_ROOT_ELEMENT_STYLE); } #[cfg(feature = "servo-layout-2013")] { if self.style.get_parent_column().is_multicol() { self.style.add_flags(ComputedValueFlags::CAN_BE_FRAGMENTED); } } } /// Adjust the style for text style. /// /// The adjustments here are a subset of the adjustments generally, because /// text only inherits properties. /// /// Note that this, for Gecko, comes through Servo_ComputedValues_Inherit. #[cfg(feature = "gecko")] pub fn adjust_for_text(&mut self) { debug_assert!(!self.style.is_root_element); self.adjust_for_text_combine_upright(); self.adjust_for_text_in_ruby(); self.set_bits(); } /// Change writing mode of the text frame for text-combine-upright. /// /// It is safe to look at our own style because we are looking at inherited /// properties, and text is just plain inheritance. /// /// TODO(emilio): we should (Gecko too) revise these adjustments in presence /// of display: contents. /// /// FIXME(emilio): How does this play with logical properties? Doesn't /// mutating writing-mode change the potential physical sides chosen? #[cfg(feature = "gecko")] fn adjust_for_text_combine_upright(&mut self) { use crate::computed_values::text_combine_upright::T as TextCombineUpright; use crate::computed_values::writing_mode::T as WritingMode; use crate::logical_geometry; let writing_mode = self.style.get_inherited_box().clone_writing_mode(); let text_combine_upright = self.style.get_inherited_text().clone_text_combine_upright(); if matches!( writing_mode, WritingMode::VerticalRl | WritingMode::VerticalLr ) && text_combine_upright == TextCombineUpright::All { self.style.add_flags(ComputedValueFlags::IS_TEXT_COMBINED); self.style .mutate_inherited_box() .set_writing_mode(WritingMode::HorizontalTb); self.style.writing_mode = logical_geometry::WritingMode::new(self.style.get_inherited_box()); } } /// Unconditionally propagates the line break suppression flag to text, and /// additionally it applies it if it is in any ruby box. /// /// This is necessary because its parent may not itself have the flag set /// (e.g. ruby or ruby containers), thus we may not inherit the flag from /// them. #[cfg(feature = "gecko")] fn adjust_for_text_in_ruby(&mut self) { let parent_display = self.style.get_parent_box().clone_display(); if parent_display.is_ruby_type() || self.style .get_parent_flags() .contains(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK) { self.style .add_flags(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK); } } /// /// /// If a box has a different writing-mode value than its containing /// block: /// /// - If the box has a specified display of inline, its display /// computes to inline-block. [CSS21] /// /// This matches the adjustment that Gecko does, not exactly following /// the spec. See also: /// /// /// fn adjust_for_writing_mode(&mut self, layout_parent_style: &ComputedValues) { let our_writing_mode = self.style.get_inherited_box().clone_writing_mode(); let parent_writing_mode = layout_parent_style.get_inherited_box().clone_writing_mode(); if our_writing_mode != parent_writing_mode && self.style.get_box().clone_display() == Display::Inline { // TODO(emilio): Figure out if we can just set the adjusted display // on Gecko too and unify this code path. if cfg!(feature = "servo") { self.style .mutate_box() .set_adjusted_display(Display::InlineBlock, false); } else { self.style.mutate_box().set_display(Display::InlineBlock); } } } /// When mathvariant is not "none", font-weight and font-style are /// both forced to "normal". #[cfg(feature = "gecko")] fn adjust_for_mathvariant(&mut self) { use crate::properties::longhands::_moz_math_variant::computed_value::T as MozMathVariant; use crate::properties::longhands::font_weight::computed_value::T as FontWeight; use crate::values::generics::font::FontStyle; if self.style.get_font().clone__moz_math_variant() != MozMathVariant::None { let font_style = self.style.mutate_font(); font_style.set_font_weight(FontWeight::normal()); font_style.set_font_style(FontStyle::Normal); } } /// This implements an out-of-date spec. The new spec moves the handling of /// this to layout, which Gecko implements but Servo doesn't. /// /// See https://github.com/servo/servo/issues/15229 #[cfg(feature = "servo")] fn adjust_for_alignment(&mut self, layout_parent_style: &ComputedValues) { use crate::computed_values::align_items::T as AlignItems; use crate::computed_values::align_self::T as AlignSelf; if self.style.get_position().clone_align_self() == AlignSelf::Auto && !self.style.is_absolutely_positioned() { let self_align = match layout_parent_style.get_position().clone_align_items() { AlignItems::Stretch => AlignSelf::Stretch, AlignItems::Baseline => AlignSelf::Baseline, AlignItems::FlexStart => AlignSelf::FlexStart, AlignItems::FlexEnd => AlignSelf::FlexEnd, AlignItems::Center => AlignSelf::Center, }; self.style.mutate_position().set_align_self(self_align); } } /// The initial value of border-*-width may be changed at computed value /// time. /// /// This is moved to properties.rs for convenience. fn adjust_for_border_width(&mut self) { properties::adjust_border_width(self.style); } /// The initial value of outline-width may be changed at computed value time. fn adjust_for_outline(&mut self) { if self .style .get_outline() .clone_outline_style() .none_or_hidden() && self.style.get_outline().outline_has_nonzero_width() { self.style .mutate_outline() .set_outline_width(crate::Zero::zero()); } } /// CSS overflow-x and overflow-y require some fixup as well in some cases. /// https://drafts.csswg.org/css-overflow-3/#overflow-properties /// "Computed value: as specified, except with `visible`/`clip` computing to /// `auto`/`hidden` (respectively) if one of `overflow-x` or `overflow-y` is /// neither `visible` nor `clip`." fn adjust_for_overflow(&mut self) { let overflow_x = self.style.get_box().clone_overflow_x(); let overflow_y = self.style.get_box().clone_overflow_y(); if overflow_x == overflow_y { return; // optimization for the common case } if overflow_x.is_scrollable() != overflow_y.is_scrollable() { let box_style = self.style.mutate_box(); box_style.set_overflow_x(overflow_x.to_scrollable()); box_style.set_overflow_y(overflow_y.to_scrollable()); } } /// Handles the relevant sections in: /// /// https://drafts.csswg.org/css-display/#unbox-html /// /// And forbidding display: contents in pseudo-elements, at least for now. #[cfg(feature = "gecko")] fn adjust_for_prohibited_display_contents(&mut self, element: Option) where E: TElement, { if self.style.get_box().clone_display() != Display::Contents { return; } // FIXME(emilio): ::before and ::after should support display: contents, // see bug 1418138. if self.style.pseudo.is_some() { self.style.mutate_box().set_display(Display::Inline); return; } let element = match element { Some(e) => e, None => return, }; if is_effective_display_none_for_display_contents(element) { self.style.mutate_box().set_display(Display::None); } } ///