Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions crates/csscascade/src/dom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -782,15 +782,15 @@ fn svg_presentation_hints(
if name.ns != markup5ever::ns!(svg) {
return None;
}
// On a gradient element the transform property's presentation attribute
// is `gradientTransform`, and the plain `transform` attribute is inert
// (measured: it changes no pixel in Chromium). Both spellings share one
// grammar and one measured rewrite; the computed value is applied about
// the raw origin in gradient space, identically for the attribute and an
// author `transform` declaration (measured with non-quarter rotations and
// scales — byte-identical).
// On a paint-server element the transform property's presentation
// attribute has the resource-specific spelling: `gradientTransform` or
// `patternTransform`. The plain `transform` attribute is inert on both
// families (measured in Chromium), while an author `transform`
// declaration still wins through the ordinary cascade. The special
// attributes share the transform grammar and measured rewrite below.
let transform_attribute = match name.local.as_ref() {
"linearGradient" | "radialGradient" => "gradientTransform",
"pattern" => "patternTransform",
_ => "transform",
};
let mut block = PropertyDeclarationBlock::new();
Expand Down
25 changes: 25 additions & 0 deletions crates/csscascade/tests/svg_presentation_hints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const STANDALONE: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" width="64"
#clip-style-attr-beats-rule { clip-path: url(#rule-clip); }
#clip-webkit-alias { -webkit-clip-path: url(#vendor-clip); }
#family-rule-beats-hint { font-family: monospace; }
#pattern-rule-beats-hint { transform: translate(30px, 0px); }
</style>
<rect id="hint-only" fill="#16a34a" width="8" height="8"/>
<rect id="named" fill="rebeccapurple" width="8" height="8"/>
Expand Down Expand Up @@ -96,6 +97,11 @@ const STANDALONE: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" width="64"
<g font-family="Ahem"><text id="family-inherited">X</text></g>
<linearGradient id="gradient-transform-hint" gradientTransform="translate(10 10)"/>
<linearGradient id="gradient-plain-transform-inert" transform="translate(10 10)"/>
<pattern id="pattern-transform-hint" patternTransform="translate(10 10)"/>
<pattern id="pattern-plain-transform-inert" transform="translate(10 10)"/>
<pattern id="pattern-rule-beats-hint" patternTransform="translate(10 10)"/>
<pattern id="pattern-style-beats-hint" patternTransform="translate(10 10)"
style="transform: translate(40px, 0px)"/>
</svg>"##;

#[test]
Expand Down Expand Up @@ -426,6 +432,25 @@ fn standalone_svg_presentation_hints_enter_below_author_rules() {
),
"none"
);
// Patterns use the sibling resource spelling. It enters the same
// transform longhand below author rules and style attributes; the plain
// transform attribute remains inert on the resource element.
assert_eq!(
property(root, "pattern-transform-hint", LonghandId::Transform),
"translate(10px, 10px)"
);
assert_eq!(
property(root, "pattern-plain-transform-inert", LonghandId::Transform),
"none"
);
assert_eq!(
property(root, "pattern-rule-beats-hint", LonghandId::Transform),
"translate(30px)"
);
assert_eq!(
property(root, "pattern-style-beats-hint", LonghandId::Transform),
"translate(40px)"
);
}

#[test]
Expand Down
39 changes: 39 additions & 0 deletions crates/n0/src/drawlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,31 @@ impl GlyphlessOwnerSlot {
}
}

/// One private source-neutral repeating vector program.
///
/// The nested drawlist is already compiled and preflighted. Its frame clip is
/// the tile cell `(0, 0, width, height)`; `transform` maps that tile-local
/// coordinate system into the consuming geometry's local space before both
/// axes repeat.
#[derive(Debug, Clone, PartialEq)]
pub struct ResolvedPattern {
pub(crate) width: f32,
pub(crate) height: f32,
pub(crate) transform: Affine,
pub(crate) program: Arc<DrawList<GlyphlessOwnerSlot>>,
pub(crate) opacity: f32,
}

/// Absolute geometry in the consuming node's local coordinate system.
/// Pattern items keep that origin in geometry instead of folding it into
/// `world`, because the pattern mapping is stated in the same local space.
#[derive(Debug, Clone, PartialEq)]
pub enum ResolvedPatternGeometry {
Rect { x: f32, y: f32, w: f32, h: f32 },
Oval { x: f32, y: f32, w: f32, h: f32 },
Path(Arc<ResolvedPathArtifact>),
}

/// One canonical, source-neutral dash phase carried by private stroke
/// material.
///
Expand Down Expand Up @@ -410,6 +435,20 @@ pub enum ItemKind {
filter: Arc<ResolvedFilter>,
},
EndFilter,
/// Fill absolute local geometry through one checked repeat program.
PatternFill {
geometry: ResolvedPatternGeometry,
pattern: Arc<ResolvedPattern>,
post_paint_opacity: PostPaintOpacity,
},
/// Stroke absolute local geometry through one checked repeat program.
PatternStroke {
geometry: ResolvedPatternGeometry,
pattern: Arc<ResolvedPattern>,
stroke: Stroke,
dash_phase: StrokeDashPhase,
post_paint_opacity: PostPaintOpacity,
},
RectFill {
w: f32,
h: f32,
Expand Down
8 changes: 8 additions & 0 deletions crates/n0/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,15 @@ impl From<crate::paint::GradientPreflightError> for FrameBuildError {
pub enum FrameExecutionError {
Environment(PaintEnvironmentMismatch),
Image(crate::paint::ImagePreflightError),
Pattern(crate::paint::PatternPreflightError),
}

impl std::fmt::Display for FrameExecutionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FrameExecutionError::Environment(error) => error.fmt(f),
FrameExecutionError::Image(error) => error.fmt(f),
FrameExecutionError::Pattern(error) => error.fmt(f),
}
}
}
Expand All @@ -164,6 +166,12 @@ impl From<crate::paint::ImagePreflightError> for FrameExecutionError {
}
}

impl From<crate::paint::PatternPreflightError> for FrameExecutionError {
fn from(error: crate::paint::PatternPreflightError) -> Self {
FrameExecutionError::Pattern(error)
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FrameError {
Build(FrameBuildError),
Expand Down
113 changes: 101 additions & 12 deletions crates/n0/src/glyphless.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
//!
//! [`rframe::Frame`] is the backend-free resolved contract. It carries no
//! authored n0 document, HTML/CSS/SVG syntax, parser binding, backend object,
//! I/O handle, or clock. This module admits its current solid- and
//! gradient-filled rectangle, ellipse, and path slice plus checked opacity,
//! clip, mask, and image-filter effects, compiles them into n0's one private
//! drawlist, and executes them through n0's one private painter.
//! I/O handle, or clock. This module admits its current solid-, gradient-, and
//! resolved-pattern-painted rectangle, ellipse, and path slice plus checked
//! opacity, clip, mask, and image-filter effects, compiles them into n0's one
//! private drawlist, and executes them through n0's one private painter.
//!
//! The resulting [`FrameProduct`] is intentionally separate from
//! [`crate::frame::FrameProduct`]. The latter owns an n0-model
Expand Down Expand Up @@ -35,7 +35,8 @@ use crate::drawlist::{
ResolvedFilterBlend, ResolvedFilterColorSpace, ResolvedFilterComposite,
ResolvedFilterConvolveEdgeMode, ResolvedFilterDisplacementChannel, ResolvedFilterInput,
ResolvedFilterLightSource, ResolvedFilterMorphology, ResolvedFilterNode,
ResolvedFilterPrimitive, ResolvedFilterTurbulenceKind, ResolvedMaskMode, StrokeDashPhase,
ResolvedFilterPrimitive, ResolvedFilterTurbulenceKind, ResolvedMaskMode, ResolvedPattern,
ResolvedPatternGeometry, StrokeDashPhase,
};
use crate::frame::FrameExecutionError;
use crate::paint::PaintCtx;
Expand Down Expand Up @@ -153,8 +154,10 @@ impl std::error::Error for BuildError {}
/// One immutable source-neutral frame, its private compiled material, and its
/// opaque provenance projection.
///
/// The admitted solid/geometry slice is resource-free, so this product neither
/// captures nor checks a [`crate::paint::PaintEnvironmentKey`].
/// Every admitted paint is already resolved and carries no resource handle, so
/// this product neither captures nor checks a
/// [`crate::paint::PaintEnvironmentKey`]. A repeating vector program is nested
/// immutable draw material, not a late resource lookup.
#[derive(Debug, Clone)]
pub struct FrameProduct {
resolved: Frame,
Expand All @@ -176,6 +179,7 @@ impl FrameProduct {
ctx: &PaintCtx,
) -> Result<(), FrameExecutionError> {
self.assert_provenance_complete();
crate::paint::preflight_patterns(&self.drawlist, ctx)?;
crate::paint::execute_unchecked(canvas, &self.drawlist, &to_affine(*view), ctx);
Ok(())
}
Expand All @@ -190,6 +194,7 @@ impl FrameProduct {
ctx: &PaintCtx,
) -> Result<Vec<u8>, FrameExecutionError> {
self.assert_provenance_complete();
crate::paint::preflight_patterns(&self.drawlist, ctx)?;
Ok(crate::paint::raster_to_bytes_unchecked(
&self.drawlist,
&to_affine(*view),
Expand Down Expand Up @@ -287,9 +292,9 @@ fn damage_input(product: &FrameProduct) -> FrameDamageInput<'_, VisualRef, (), G
/// slice is rectangles, ellipses (each carried as its local-space bounding
/// rectangle) and paths, the contract's admitted `cg` paints (solids, linear
/// and radial gradients — every gradient preflighted against its resolved
/// paint box before the product exists), a centred stroke over the fill,
/// isolated opacity scopes, resolved geometric clip scopes, and the
/// frame-bounds clip.
/// paint box before the product exists), checked repeating vector programs, a
/// centred stroke over the fill, isolated opacity scopes, resolved geometric
/// clip scopes, and the frame-bounds clip.
///
/// The contract's item stream is a checked type ([`rframe::FrameItems`]):
/// balance, non-emptiness, and bounded nesting were proven at construction,
Expand Down Expand Up @@ -591,6 +596,11 @@ pub fn compile(resolved: Frame) -> Result<FrameProduct, BuildError> {
_ => None,
};
let paints = compile_paints(&node.paints, unit_offset);
let fill_pattern = node
.paints
.pattern()
.map(|pattern| compile_pattern(pattern, node.owner))
.transpose()?;
let fill_post_paint_opacity =
PostPaintOpacity::from_resolved(node.paints.alpha_factor().get());
let owner = GlyphlessOwnerSlot::new(
Expand Down Expand Up @@ -645,7 +655,17 @@ pub fn compile(resolved: Frame) -> Result<FrameProduct, BuildError> {
Geometry::Path(path) => Some(compile_path(path)),
_ => None,
};
if !paints.is_empty() {
if let Some(pattern) = fill_pattern {
items.push(Item {
node: owner,
world: to_affine(node.transform),
kind: ItemKind::PatternFill {
geometry: compile_pattern_geometry(&node.geometry),
pattern,
post_paint_opacity: fill_post_paint_opacity,
},
});
} else if !paints.is_empty() {
let kind = match &node.geometry {
Geometry::Rect(_) => ItemKind::RectFill {
w,
Expand Down Expand Up @@ -679,6 +699,11 @@ pub fn compile(resolved: Frame) -> Result<FrameProduct, BuildError> {
// other in the same private drawlist, which is why a stroke needs no
// group scope.
if let Some(stroke) = &node.stroke {
let stroke_pattern = stroke
.paints()
.pattern()
.map(|pattern| compile_pattern(pattern, node.owner))
.transpose()?;
// A resolved dashed oval must preserve the exact local conic
// stream over which its producer resolved the dash facts. Skia's
// path measurement and dash traversal are f32
Expand All @@ -693,6 +718,20 @@ pub fn compile(resolved: Frame) -> Result<FrameProduct, BuildError> {
&& rect.height > 0.0
&& stroke.dash().is_some();
let (stroke, dash_phase, post_paint_opacity) = compile_stroke(stroke, unit_offset);
if let Some(pattern) = stroke_pattern {
items.push(Item {
node: owner,
world: to_affine(node.transform),
kind: ItemKind::PatternStroke {
geometry: compile_pattern_geometry(&node.geometry),
pattern,
stroke,
dash_phase,
post_paint_opacity,
},
});
continue;
}
let kind = match &node.geometry {
Geometry::Rect(_) => ItemKind::RectStroke {
w,
Expand Down Expand Up @@ -1224,6 +1263,50 @@ fn compile_gradient_transform(
affine
}

fn compile_pattern_geometry(geometry: &Geometry) -> ResolvedPatternGeometry {
match geometry {
Geometry::Rect(rect) => ResolvedPatternGeometry::Rect {
x: rect.x,
y: rect.y,
w: rect.width,
h: rect.height,
},
Geometry::Ellipse(rect) => ResolvedPatternGeometry::Oval {
x: rect.x,
y: rect.y,
w: rect.width,
h: rect.height,
},
Geometry::Path(path) => ResolvedPatternGeometry::Path(compile_path(path)),
}
}

/// Compile a checked nested frame program without issuing raster commands.
/// Recursive programs re-enter this same proving shell; `rframe` already
/// bounds their depth, and every nested gradient/effect receives the same
/// deterministic preflight as a top-level frame.
fn compile_pattern(
pattern: &rframe::PatternPaint,
owner: VisualRef,
) -> Result<Arc<ResolvedPattern>, BuildError> {
let nested = Frame {
owner: VisualRef::new(rframe::Identity::new(0), rframe::Provenance::new(0)),
bounds: math2::Rectangle::from_xywh(0.0, 0.0, pattern.width(), pattern.height()),
items: pattern.items().as_ref().clone(),
};
let product = compile(nested).map_err(|error| BuildError::Paint {
owner,
reason: format!("nested pattern program failed projection: {error}"),
})?;
Ok(Arc::new(ResolvedPattern {
width: pattern.width(),
height: pattern.height(),
transform: to_affine(pattern.transform()),
program: Arc::new(product.drawlist),
opacity: pattern.opacity(),
}))
}

fn compile_paints(paints: &PaintStack, unit_offset: Option<(f32, f32)>) -> Paints {
let mut compiled = Vec::with_capacity(paints.len());
for paint in paints.iter() {
Expand Down Expand Up @@ -1685,7 +1768,13 @@ mod tests {

fn post_paint_opacity(kind: &ItemKind) -> Option<PostPaintOpacity> {
match kind {
ItemKind::RectFill {
ItemKind::PatternFill {
post_paint_opacity, ..
}
| ItemKind::PatternStroke {
post_paint_opacity, ..
}
| ItemKind::RectFill {
post_paint_opacity, ..
}
| ItemKind::OvalFill {
Expand Down
Loading
Loading