style: Implement parsing / serialization for container{,-type,-name} CSS properties

Two noteworthy details that may seem random otherwise:

 * Moving values around in nsStyleDisplay is needed so that the struct
   remains under the size limit that we have to avoid jumping allocator
   buckets.

 * All the test expectation churn is because tests depend on
   `container-type: size` parsing to run, and now they run. Tests for
   the relevant bits I implemented are passing, with the only exception
   of some `container-name-computed.html` failures which are
   https://github.com/w3c/csswg-drafts/issues/7181. Safari agrees with
   us there.

Other notes when looking at the spec and seeing how it matches the
implementation:

 * `container` syntax doesn't match spec, but matches tests and sanity:
   https://github.com/w3c/csswg-drafts/issues/7180

 * `container-type` syntax doesn't _quite_ match spec, but matches tests
   and I think it's a spec bug since the definition for the missing
   keyword is gone:
   https://github.com/w3c/csswg-drafts/issues/7179

Differential Revision: https://phabricator.services.mozilla.com/D142419
This commit is contained in:
Emilio Cobos Álvarez 2023-06-18 14:02:56 +02:00 committed by Martin Robinson
parent ee4e09359f
commit ec6099563e
9 changed files with 161 additions and 16 deletions

View file

@ -444,6 +444,7 @@ class Longhand(Property):
"ColumnCount", "ColumnCount",
"Contain", "Contain",
"ContentVisibility", "ContentVisibility",
"ContainerType",
"Display", "Display",
"FillRule", "FillRule",
"Float", "Float",

View file

@ -623,6 +623,26 @@ ${helpers.predefined_type(
animation_value_type="none", animation_value_type="none",
)} )}
${helpers.predefined_type(
"container-type",
"ContainerType",
"computed::ContainerType::NONE",
engines="gecko",
animation_value_type="none",
gecko_pref="layout.css.container-queries.enabled",
spec="https://drafts.csswg.org/css-contain-3/#container-type",
)}
${helpers.predefined_type(
"container-name",
"ContainerName",
"computed::ContainerName::none()",
engines="gecko",
animation_value_type="none",
gecko_pref="layout.css.container-queries.enabled",
spec="https://drafts.csswg.org/css-contain-3/#container-name",
)}
${helpers.predefined_type( ${helpers.predefined_type(
"appearance", "appearance",
"Appearance", "Appearance",

View file

@ -345,6 +345,45 @@ ${helpers.two_properties_shorthand(
spec="https://wicg.github.io/overscroll-behavior/#overscroll-behavior-properties", spec="https://wicg.github.io/overscroll-behavior/#overscroll-behavior-properties",
)} )}
<%helpers:shorthand
engines="gecko"
name="container"
sub_properties="container-type container-name"
gecko_pref="layout.css.container-queries.enabled",
spec="https://drafts.csswg.org/css-contain-3/#container-shorthand"
>
pub fn parse_value<'i>(
context: &ParserContext,
input: &mut Parser<'i, '_>,
) -> Result<Longhands, ParseError<'i>> {
use crate::parser::Parse;
use crate::values::specified::box_::{ContainerName, ContainerType};
// See https://github.com/w3c/csswg-drafts/issues/7180 for why we don't
// match the spec.
let container_type = ContainerType::parse(context, input)?;
let container_name = if input.try_parse(|input| input.expect_delim('/')).is_ok() {
ContainerName::parse(context, input)?
} else {
ContainerName::none()
};
Ok(expanded! {
container_type: container_type,
container_name: container_name,
})
}
impl<'a> ToCss for LonghandsToSerialize<'a> {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
self.container_type.to_css(dest)?;
if !self.container_name.is_none() {
dest.write_str(" / ")?;
self.container_name.to_css(dest)?;
}
Ok(())
}
}
</%helpers:shorthand>
<%helpers:shorthand <%helpers:shorthand
engines="gecko" engines="gecko"
name="page-break-before" name="page-break-before"

View file

@ -13,9 +13,10 @@ use crate::values::specified::box_ as specified;
pub use crate::values::specified::box_::{ pub use crate::values::specified::box_::{
AnimationName, AnimationTimeline, Appearance, BreakBetween, BreakWithin, AnimationName, AnimationTimeline, Appearance, BreakBetween, BreakWithin,
Clear as SpecifiedClear, Contain, ContentVisibility, Display, Float as SpecifiedFloat, Overflow, Clear as SpecifiedClear, Contain, ContainerName, ContainerType, ContentVisibility, Display,
OverflowAnchor, OverflowClipBox, OverscrollBehavior, ScrollSnapAlign, ScrollSnapAxis, Float as SpecifiedFloat, Overflow, OverflowAnchor, OverflowClipBox,
ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, TouchAction, TransitionProperty, WillChange, OverscrollBehavior, ScrollSnapAlign, ScrollSnapAxis, ScrollSnapStrictness,
ScrollSnapType, ScrollbarGutter, TouchAction, TransitionProperty, WillChange,
}; };
/// A computed value for the `vertical-align` property. /// A computed value for the `vertical-align` property.

View file

@ -44,7 +44,7 @@ pub use self::basic_shape::FillRule;
pub use self::border::{BorderCornerRadius, BorderRadius, BorderSpacing}; pub use self::border::{BorderCornerRadius, BorderRadius, BorderSpacing};
pub use self::border::{BorderImageRepeat, BorderImageSideWidth}; pub use self::border::{BorderImageRepeat, BorderImageSideWidth};
pub use self::border::{BorderImageSlice, BorderImageWidth}; pub use self::border::{BorderImageSlice, BorderImageWidth};
pub use self::box_::{AnimationIterationCount, AnimationName, AnimationTimeline, Contain}; pub use self::box_::{AnimationIterationCount, AnimationName, AnimationTimeline, Contain, ContainerName, ContainerType};
pub use self::box_::{Appearance, BreakBetween, BreakWithin, Clear, ContentVisibility, Float}; pub use self::box_::{Appearance, BreakBetween, BreakWithin, Clear, ContentVisibility, Float};
pub use self::box_::{Display, Overflow, OverflowAnchor, TransitionProperty}; pub use self::box_::{Display, Overflow, OverflowAnchor, TransitionProperty};
pub use self::box_::{OverflowClipBox, OverscrollBehavior, Perspective, Resize, ScrollbarGutter}; pub use self::box_::{OverflowClipBox, OverscrollBehavior, Perspective, Resize, ScrollbarGutter};

View file

@ -1288,6 +1288,61 @@ pub enum ContentVisibility {
Visible, Visible,
} }
bitflags! {
#[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToCss, Parse, ToResolvedValue, ToShmem)]
#[repr(C)]
#[allow(missing_docs)]
#[css(bitflags(single="none", mixed="style,size,inline-size", overlapping_bits))]
/// https://drafts.csswg.org/css-contain-3/#container-type
///
/// TODO: block-size is on the spec but it seems it was removed? WPTs don't
/// support it, see https://github.com/w3c/csswg-drafts/issues/7179.
pub struct ContainerType: u8 {
/// The `none` variant.
const NONE = 0;
/// The `style` variant.
const STYLE = 1 << 0;
/// The `inline-size` variant.
const INLINE_SIZE = 1 << 1;
/// The `size` variant, exclusive with `inline-size` (they sharing bits
/// guarantees this).
const SIZE = 1 << 2 | Self::INLINE_SIZE.bits;
}
}
/// https://drafts.csswg.org/css-contain-3/#container-name
#[repr(transparent)]
#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)]
pub struct ContainerName(#[css(iterable, if_empty = "none")] pub crate::OwnedSlice<CustomIdent>);
impl ContainerName {
/// Return the `none` value.
pub fn none() -> Self {
Self(Default::default())
}
/// Returns whether this is the `none` value.
pub fn is_none(&self) -> bool {
self.0.is_empty()
}
}
impl Parse for ContainerName {
fn parse<'i, 't>( _: &ParserContext, input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
let mut idents = vec![];
let location = input.current_source_location();
let first = input.expect_ident()?;
if first.eq_ignore_ascii_case("none") {
return Ok(Self::none())
}
idents.push(CustomIdent::from_ident(location, first, &["none"])?);
while let Ok(ident) = input.try_parse(|input| input.expect_ident_cloned()) {
idents.push(CustomIdent::from_ident(location, &ident, &["none"])?);
}
Ok(ContainerName(idents.into()))
}
}
/// A specified value for the `perspective` property. /// A specified value for the `perspective` property.
pub type Perspective = GenericPerspective<NonNegativeLength>; pub type Perspective = GenericPerspective<NonNegativeLength>;

View file

@ -37,7 +37,7 @@ pub use self::border::{BorderCornerRadius, BorderImageSlice, BorderImageWidth};
pub use self::border::{BorderImageRepeat, BorderImageSideWidth}; pub use self::border::{BorderImageRepeat, BorderImageSideWidth};
pub use self::border::{BorderRadius, BorderSideWidth, BorderSpacing, BorderStyle}; pub use self::border::{BorderRadius, BorderSideWidth, BorderSpacing, BorderStyle};
pub use self::box_::{AnimationIterationCount, AnimationName, AnimationTimeline, Contain, Display}; pub use self::box_::{AnimationIterationCount, AnimationName, AnimationTimeline, Contain, Display};
pub use self::box_::{Appearance, BreakBetween, BreakWithin}; pub use self::box_::{Appearance, BreakBetween, BreakWithin, ContainerName, ContainerType};
pub use self::box_::{Clear, ContentVisibility, Float, Overflow, OverflowAnchor}; pub use self::box_::{Clear, ContentVisibility, Float, Overflow, OverflowAnchor};
pub use self::box_::{OverflowClipBox, OverscrollBehavior, Perspective, Resize, ScrollbarGutter}; pub use self::box_::{OverflowClipBox, OverscrollBehavior, Perspective, Resize, ScrollbarGutter};
pub use self::box_::{ScrollSnapAlign, ScrollSnapAxis, ScrollSnapStrictness, ScrollSnapType}; pub use self::box_::{ScrollSnapAlign, ScrollSnapAxis, ScrollSnapStrictness, ScrollSnapType};

View file

@ -31,17 +31,35 @@ fn derive_bitflags(input: &syn::DeriveInput, bitflags: &CssBitflagAttrs) -> Toke
let mut has_any = false; let mut has_any = false;
}); });
if bitflags.overlapping_bits {
body.append_all(quote! {
let mut serialized = Self::empty();
});
}
for (rust_name, css_name) in bitflags.mixed_flags() { for (rust_name, css_name) in bitflags.mixed_flags() {
let rust_ident = Ident::new(&rust_name, Span::call_site()); let rust_ident = Ident::new(&rust_name, Span::call_site());
body.append_all(quote! { let serialize = quote! {
if self.intersects(Self::#rust_ident) { if has_any {
if has_any { dest.write_char(' ')?;
dest.write_char(' ')?;
}
has_any = true;
dest.write_str(#css_name)?;
} }
}); has_any = true;
dest.write_str(#css_name)?;
};
if bitflags.overlapping_bits {
body.append_all(quote! {
if self.contains(Self::#rust_ident) && !serialized.intersects(Self::#rust_ident) {
#serialize
serialized.insert(Self::#rust_ident);
}
});
} else {
body.append_all(quote! {
if self.intersects(Self::#rust_ident) {
#serialize
}
});
}
} }
body.append_all(quote! { body.append_all(quote! {
@ -319,6 +337,10 @@ pub struct CssBitflagAttrs {
/// Extra validation of the resulting mixed flags. /// Extra validation of the resulting mixed flags.
#[darling(default)] #[darling(default)]
pub validate_mixed: Option<Path>, pub validate_mixed: Option<Path>,
/// Whether there are overlapping bits we need to take care of when
/// serializing.
#[darling(default)]
pub overlapping_bits: bool,
} }
impl CssBitflagAttrs { impl CssBitflagAttrs {

View file

@ -44,9 +44,9 @@ use std::fmt::{self, Write};
/// * `#[css(represents_keyword)]` can be used on bool fields in order to /// * `#[css(represents_keyword)]` can be used on bool fields in order to
/// serialize the field name if the field is true, or nothing otherwise. It /// serialize the field name if the field is true, or nothing otherwise. It
/// also collects those keywords for `SpecifiedValueInfo`. /// also collects those keywords for `SpecifiedValueInfo`.
/// * `#[css(bitflags(single="", mixed="", validate="")]` can be used to derive /// * `#[css(bitflags(single="", mixed="", validate="", overlapping_bits)]` can
/// parse / serialize / etc on bitflags. The rules for parsing bitflags are /// be used to derive parse / serialize / etc on bitflags. The rules for parsing
/// the following: /// bitflags are the following:
/// ///
/// * `single` flags can only appear on their own. It's common that bitflags /// * `single` flags can only appear on their own. It's common that bitflags
/// properties at least have one such value like `none` or `auto`. /// properties at least have one such value like `none` or `auto`.
@ -66,6 +66,13 @@ use std::fmt::{self, Write};
/// ///
/// But `bar baz` will be valid, as they don't share bits, and so would /// But `bar baz` will be valid, as they don't share bits, and so would
/// `foo` with any other flag, or `bazz` on its own. /// `foo` with any other flag, or `bazz` on its own.
/// * `overlapping_bits` enables some tracking during serialization of mixed
/// flags to avoid serializing variants that can subsume other variants.
/// In the example above, you could do:
/// mixed="foo,bazz,bar,baz", overlapping_bits
/// to ensure that if bazz is serialized, bar and baz aren't, even though
/// their bits are set. Note that the serialization order is canonical,
/// and thus depends on the order you specify the flags in.
/// ///
/// * finally, one can put `#[css(derive_debug)]` on the whole type, to /// * finally, one can put `#[css(derive_debug)]` on the whole type, to
/// implement `Debug` by a single call to `ToCss::to_css`. /// implement `Debug` by a single call to `ToCss::to_css`.