Implement background box geometry

This commit is contained in:
SGavrl committed 2026-08-03 16:18:16 +00:00
1 parent acde5c0fe4
commit ceb6fa52ca
3 files changed
+570 -241

No files matched your search

+24
View File
@@ -547,6 +547,25 @@ pub struct BackgroundPosition {
pub y: BackgroundPositionAxis, pub y: BackgroundPositionAxis,
} }
/// Box used to establish a background layer's positioning area.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum BackgroundOrigin {
BorderBox,
#[default]
PaddingBox,
ContentBox,
}
/// Box (or glyph shape) that limits background painting.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum BackgroundClip {
#[default]
BorderBox,
PaddingBox,
ContentBox,
Text,
}
impl BackgroundPosition { impl BackgroundPosition {
pub const fn new(x: BackgroundPositionAxis, y: BackgroundPositionAxis) -> Self { pub const fn new(x: BackgroundPositionAxis, y: BackgroundPositionAxis) -> Self {
Self { x, y } Self { x, y }
@@ -827,6 +846,11 @@ pub struct LayoutStyle {
/// Explicit `(repeat-x, repeat-y)` choice. `None` is the CSS initial /// Explicit `(repeat-x, repeat-y)` choice. `None` is the CSS initial
/// `repeat` in both axes. /// `repeat` in both axes.
pub background_repeat: Option<(bool, bool)>, pub background_repeat: Option<(bool, bool)>,
/// Geometry longhands retained independently: gradients and images are
/// authored in `background-origin`, while `background-clip` only limits
/// which portion becomes visible.
pub background_origin: BackgroundOrigin,
pub background_clip: BackgroundClip,
/// `background-clip: text` / `-webkit-background-clip: text`: the background /// `background-clip: text` / `-webkit-background-clip: text`: the background
/// paints only through the element's glyphs, not as a filled box. Combined /// paints only through the element's glyphs, not as a filled box. Combined
/// with a transparent text color this is the common gradient-text technique /// with a transparent text color this is the common gradient-text technique
File diff suppressed because it is too large. Load diff
+97 -6
View File
@@ -962,7 +962,13 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
style.background_size_fit = parse_background_size_fit(value); style.background_size_fit = parse_background_size_fit(value);
style.background_position = crate::BackgroundPosition::default(); style.background_position = crate::BackgroundPosition::default();
style.background_repeat = parse_image_repeat(value); style.background_repeat = parse_image_repeat(value);
style.background_clip_text = false; style.background_origin = crate::BackgroundOrigin::default();
style.background_clip = crate::BackgroundClip::default();
if let Some((origin, clip)) = parse_background_box_shorthand(value) {
style.background_origin = origin;
style.background_clip = clip;
}
style.background_clip_text = style.background_clip == crate::BackgroundClip::Text;
} }
} }
"background-image" => { "background-image" => {
@@ -979,6 +985,9 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
"background-repeat" => { "background-repeat" => {
style.background_repeat = parse_image_repeat(value); style.background_repeat = parse_image_repeat(value);
} }
"background-origin" => {
style.background_origin = parse_background_origin(value).unwrap_or_default();
}
// On replaced elements, an image-valued `content` replaces the // On replaced elements, an image-valued `content` replaces the
// ordinary source and participates in intrinsic sizing. String-valued // ordinary source and participates in intrinsic sizing. String-valued
// generated content is handled separately for pseudo-elements. // generated content is handled separately for pseudo-elements.
@@ -991,10 +1000,8 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
style.mask_repeat = parse_image_repeat(value); style.mask_repeat = parse_image_repeat(value);
} }
"background-clip" | "-webkit-background-clip" => { "background-clip" | "-webkit-background-clip" => {
// `text` clips the background to the element's glyphs (the gradient/ style.background_clip = parse_background_clip(value).unwrap_or_default();
// solid-color text technique). Any box value (border-box/padding-box/ style.background_clip_text = style.background_clip == crate::BackgroundClip::Text;
// content-box) is an ordinary background, so clear the flag.
style.background_clip_text = value.trim().eq_ignore_ascii_case("text");
} }
// Blink/WebKit gradient text commonly makes the glyph fill // Blink/WebKit gradient text commonly makes the glyph fill
// transparent through this inherited property while clipping a // transparent through this inherited property while clipping a
@@ -1801,6 +1808,7 @@ pub fn supports_declaration(name: &str, value: &str) -> bool {
| "background-size" | "background-size"
| "background-position" | "background-position"
| "background-repeat" | "background-repeat"
| "background-origin"
| "background-clip" | "background-clip"
| "-webkit-background-clip" | "-webkit-background-clip"
| "mask-image" | "mask-image"
@@ -2436,7 +2444,8 @@ fn supports_conservative_known_value(name: &str, value: &str) -> bool {
"background-size" | "mask-size" | "-webkit-mask-size" => { "background-size" | "mask-size" | "-webkit-mask-size" => {
matches!(lower.as_str(), "auto" | "cover" | "contain") || parse_background_size(value).is_some() matches!(lower.as_str(), "auto" | "cover" | "contain") || parse_background_size(value).is_some()
} }
"background-clip" | "-webkit-background-clip" => matches!(lower.as_str(), "border-box" | "padding-box" | "content-box" | "text"), "background-origin" => parse_background_origin(value).is_some(),
"background-clip" | "-webkit-background-clip" => parse_background_clip(value).is_some(),
"font-size" => is_font_size_token(value), "font-size" => is_font_size_token(value),
"font-weight" => specified_font_weight(value).is_some(), "font-weight" => specified_font_weight(value).is_some(),
"font-family" => !value.trim().is_empty(), "font-family" => !value.trim().is_empty(),
@@ -6700,6 +6709,49 @@ fn parse_image_repeat(value: &str) -> Option<(bool, bool)> {
} }
} }
fn parse_background_origin(value: &str) -> Option<crate::BackgroundOrigin> {
match value.trim().to_ascii_lowercase().as_str() {
"border-box" => Some(crate::BackgroundOrigin::BorderBox),
"padding-box" => Some(crate::BackgroundOrigin::PaddingBox),
"content-box" => Some(crate::BackgroundOrigin::ContentBox),
_ => None,
}
}
fn parse_background_clip(value: &str) -> Option<crate::BackgroundClip> {
match value.trim().to_ascii_lowercase().as_str() {
"border-box" => Some(crate::BackgroundClip::BorderBox),
"padding-box" => Some(crate::BackgroundClip::PaddingBox),
"content-box" => Some(crate::BackgroundClip::ContentBox),
"text" => Some(crate::BackgroundClip::Text),
_ => None,
}
}
/// One visual-box token in the shorthand sets both origin and clip; a second
/// token overrides only clip. Function arguments are excluded by
/// `split_ws_paren`, so a URL or gradient payload cannot be mistaken for box
/// geometry.
fn parse_background_box_shorthand(
value: &str,
) -> Option<(crate::BackgroundOrigin, crate::BackgroundClip)> {
let first_layer = split_top_level(value, ',').into_iter().next()?;
let boxes = split_ws_paren(first_layer)
.into_iter()
.filter_map(|token| parse_background_clip(token).map(|clip| (token, clip)))
.collect::<Vec<_>>();
match boxes.as_slice() {
[] => None,
[(_, crate::BackgroundClip::Text)] => Some((
crate::BackgroundOrigin::default(),
crate::BackgroundClip::Text,
)),
[(token, clip)] => Some((parse_background_origin(token)?, *clip)),
[(origin, _), (_, clip)] => Some((parse_background_origin(origin)?, *clip)),
_ => None,
}
}
fn background_size_expression(value: &str) -> Option<String> { fn background_size_expression(value: &str) -> Option<String> {
let (_, size) = value.rsplit_once('/')?; let (_, size) = value.rsplit_once('/')?;
let mut depth = 0i32; let mut depth = 0i32;
@@ -8420,6 +8472,8 @@ mod tests {
assert_eq!(s.background_size_expression, None); assert_eq!(s.background_size_expression, None);
assert_eq!(s.background_size_fit, None); assert_eq!(s.background_size_fit, None);
assert_eq!(s.background_position, crate::BackgroundPosition::default()); assert_eq!(s.background_position, crate::BackgroundPosition::default());
assert_eq!(s.background_origin, crate::BackgroundOrigin::PaddingBox);
assert_eq!(s.background_clip, crate::BackgroundClip::BorderBox);
assert!(!s.background_clip_text); assert!(!s.background_clip_text);
let cover = compute_style( let cover = compute_style(
@@ -8444,6 +8498,43 @@ mod tests {
); );
} }
#[test]
fn background_box_longhands_and_shorthand_retain_independent_geometry() {
let longhands = compute_style(
"div",
Some("background-origin:content-box;background-clip:padding-box"),
);
assert_eq!(longhands.background_origin, crate::BackgroundOrigin::ContentBox);
assert_eq!(longhands.background_clip, crate::BackgroundClip::PaddingBox);
assert!(!longhands.background_clip_text);
let shorthand = compute_style(
"div",
Some(
"background:linear-gradient(90deg,red,blue) content-box padding-box no-repeat",
),
);
assert_eq!(shorthand.background_origin, crate::BackgroundOrigin::ContentBox);
assert_eq!(shorthand.background_clip, crate::BackgroundClip::PaddingBox);
let one_box = compute_style("div", Some("background:red content-box"));
assert_eq!(one_box.background_origin, crate::BackgroundOrigin::ContentBox);
assert_eq!(one_box.background_clip, crate::BackgroundClip::ContentBox);
let text = compute_style(
"h1",
Some("background-origin:border-box;background-clip:text"),
);
assert_eq!(text.background_origin, crate::BackgroundOrigin::BorderBox);
assert_eq!(text.background_clip, crate::BackgroundClip::Text);
assert!(text.background_clip_text);
assert!(supports_declaration("background-origin", "border-box"));
assert!(supports_declaration("background-origin", "padding-box"));
assert!(supports_declaration("background-origin", "content-box"));
assert!(!supports_declaration("background-origin", "text"));
}
#[test] #[test]
fn background_gradients_keep_authored_layer_order_and_keyword_centers() { fn background_gradients_keep_authored_layer_order_and_keyword_centers() {
let style = compute_style( let style = compute_style(