blitz-pdf 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ use super::ElementCx;
2
+ use crate::color::{Color, ToColorColor as _};
3
+ use anyrender::PaintScene;
4
+ use kurbo::{Rect, Vec2};
5
+ use peniko::{Compose, Fill, Mix};
6
+
7
+ impl ElementCx<'_, '_> {
8
+ pub(super) fn draw_outset_box_shadow(&self, scene: &mut impl PaintScene) {
9
+ let box_shadow = &self.style.get_effects().box_shadow.0;
10
+ let has_outset_shadow = box_shadow.iter().any(|s| !s.inset);
11
+ if !has_outset_shadow {
12
+ return;
13
+ }
14
+
15
+ let current_color = self.style.clone_color();
16
+ let opacity = self.style.get_effects().opacity;
17
+ let bg_color = self
18
+ .style
19
+ .get_background()
20
+ .background_color
21
+ .resolve_to_absolute(&current_color)
22
+ .as_srgb_color();
23
+ let bg_is_opaque = bg_color.components[3] >= 1.0;
24
+ let needs_clip = opacity < 1.0 || !bg_is_opaque;
25
+
26
+ let max_shadow_rect = box_shadow.iter().fold(Rect::ZERO, |prev, shadow| {
27
+ let x = shadow.base.horizontal.px() as f64 * self.scale;
28
+ let y = shadow.base.vertical.px() as f64 * self.scale;
29
+ let blur = shadow.base.blur.px() as f64 * self.scale;
30
+ let spread = shadow.spread.px() as f64 * self.scale;
31
+ let offset = spread + blur * 2.5;
32
+
33
+ let rect = self.frame.border_box.inflate(offset, offset) + Vec2::new(x, y);
34
+
35
+ prev.union(rect)
36
+ });
37
+
38
+ self.context.layer_manager.maybe_with_layer(
39
+ scene,
40
+ needs_clip,
41
+ 1.0,
42
+ self.transform,
43
+ &self.frame.shadow_clip(max_shadow_rect),
44
+ None,
45
+ None,
46
+ |scene| {
47
+ for shadow in box_shadow.iter().filter(|s| !s.inset).rev() {
48
+ let shadow_color = shadow
49
+ .base
50
+ .color
51
+ .resolve_to_absolute(&current_color)
52
+ .as_srgb_color();
53
+
54
+ let alpha = shadow_color.components[3];
55
+ if alpha != 0.0 {
56
+ let transform = self.transform.then_translate(Vec2 {
57
+ x: shadow.base.horizontal.px() as f64 * self.scale,
58
+ y: shadow.base.vertical.px() as f64 * self.scale,
59
+ });
60
+
61
+ let spread = shadow.spread.px() as f64 * self.scale;
62
+ let blur = shadow.base.blur.px() as f64;
63
+
64
+ // Without blur or spread the shadow is exactly the element's shape,
65
+ // so fill the border box path directly (preserving individual radii)
66
+ if blur == 0.0 && spread == 0.0 {
67
+ scene.fill(
68
+ Fill::NonZero,
69
+ transform,
70
+ shadow_color,
71
+ None,
72
+ &self.frame.border_box_path(),
73
+ );
74
+ continue;
75
+ }
76
+
77
+ // TODO draw shadows with matching individual radii instead of averaging
78
+ let radius = self.frame.border_radii.average();
79
+ let rect = self.frame.border_box.inflate(spread, spread);
80
+
81
+ // Fill the color
82
+ scene.draw_box_shadow(transform, rect, shadow_color, radius, blur);
83
+ }
84
+ }
85
+ },
86
+ )
87
+ }
88
+
89
+ pub(super) fn draw_inset_box_shadow(&self, scene: &mut impl PaintScene) {
90
+ let current_color = self.style.clone_color();
91
+ let box_shadow = &self.style.get_effects().box_shadow.0;
92
+ let has_inset_shadow = box_shadow.iter().any(|s| s.inset);
93
+ if !has_inset_shadow {
94
+ return;
95
+ }
96
+
97
+ let padding_box = self.frame.padding_box_path();
98
+
99
+ for shadow in box_shadow.iter().filter(|s| s.inset) {
100
+ let shadow_color = shadow
101
+ .base
102
+ .color
103
+ .resolve_to_absolute(&current_color)
104
+ .as_srgb_color();
105
+ if shadow_color == Color::TRANSPARENT {
106
+ return;
107
+ }
108
+
109
+ // TODO draw shadows with matching individual radii instead of averaging
110
+ let radius = self.frame.border_radii.average();
111
+ let transform = self.transform.then_translate(Vec2 {
112
+ x: shadow.base.horizontal.px() as f64,
113
+ y: shadow.base.vertical.px() as f64,
114
+ });
115
+
116
+ scene.push_layer(
117
+ Fill::NonZero,
118
+ Mix::Normal,
119
+ 1.0,
120
+ self.transform,
121
+ &padding_box,
122
+ None,
123
+ None,
124
+ );
125
+ scene.fill(
126
+ Fill::NonZero,
127
+ self.transform,
128
+ shadow_color,
129
+ None,
130
+ &padding_box,
131
+ );
132
+
133
+ scene.push_layer(
134
+ Fill::NonZero,
135
+ Compose::DestOut,
136
+ 1.0,
137
+ self.transform,
138
+ &padding_box,
139
+ None,
140
+ None,
141
+ );
142
+ // The unshadowed area is the padding box shrunk by the spread radius
143
+ let spread = shadow.spread.px() as f64 * self.scale;
144
+ let hole = self.frame.padding_box.inflate(-spread, -spread);
145
+ scene.draw_box_shadow(
146
+ transform,
147
+ hole,
148
+ Color::WHITE,
149
+ radius,
150
+ shadow.base.blur.px() as f64 * self.scale,
151
+ );
152
+
153
+ scene.pop_layer();
154
+ scene.pop_layer();
155
+ }
156
+ }
157
+ }
@@ -0,0 +1,492 @@
1
+ use super::ElementCx;
2
+ use kurbo::{BezPath, Circle, Ellipse, Point, Rect, Shape, SvgArc, Vec2};
3
+ use style::values::computed::basic_shape::{BasicShape, ClipPath};
4
+ use style::values::computed::{Angle, CSSPixelLength, LengthPercentage};
5
+ use style::values::generics::basic_shape::{
6
+ ArcSize, ArcSweep, AxisEndPoint, AxisPosition, CommandEndPoint, ControlPoint, ControlReference,
7
+ GenericBasicShape, GenericPathOrShapeFunction, GenericShapeCommand, GenericShapeRadius,
8
+ ShapeBox, ShapeGeometryBox,
9
+ };
10
+ use style::values::generics::position::{GenericPosition, GenericPositionOrAuto};
11
+
12
+ impl ElementCx<'_, '_> {
13
+ /// Compute the rectangle (in scaled pixels, relative to the border box origin) described by
14
+ /// the CSS 2 `clip` property, if it applies. `clip` only applies to absolutely positioned
15
+ /// elements; `auto` components resolve to the corresponding border box edge.
16
+ pub(super) fn css_clip_rect(&self) -> Option<Rect> {
17
+ use style::computed_values::position::T as Position;
18
+ use style::values::computed::LengthOrAuto;
19
+ use style::values::generics::ClipRectOrAuto;
20
+
21
+ if !matches!(
22
+ self.style.get_box().position,
23
+ Position::Absolute | Position::Fixed
24
+ ) {
25
+ return None;
26
+ }
27
+ let ClipRectOrAuto::Rect(rect) = &self.style.get_effects().clip else {
28
+ return None;
29
+ };
30
+
31
+ let border_box = self.frame.border_box;
32
+ let resolve = |component: &LengthOrAuto, auto: f64| match component {
33
+ LengthOrAuto::Auto => auto,
34
+ LengthOrAuto::LengthPercentage(length) => length.px() as f64 * self.scale,
35
+ };
36
+ let x0 = resolve(&rect.left, 0.0);
37
+ let y0 = resolve(&rect.top, 0.0);
38
+ let x1 = resolve(&rect.right, border_box.width());
39
+ let y1 = resolve(&rect.bottom, border_box.height());
40
+
41
+ Some(Rect::new(x0, y0, x1.max(x0), y1.max(y0)))
42
+ }
43
+
44
+ /// Compute the clip-path BezPath (if any) for this element.
45
+ /// Returns `None` if clip-path is `none` or unsupported.
46
+ pub(super) fn clip_path_shape(&self) -> Option<BezPath> {
47
+ let clip_path = self.style.clone_clip_path();
48
+ match clip_path {
49
+ ClipPath::None => None,
50
+ ClipPath::Url(_) => {
51
+ // URL references (e.g. `clip-path: url(#myClip)`) are not yet supported
52
+ None
53
+ }
54
+ ClipPath::Shape(basic_shape, geometry_box) => {
55
+ let reference_box = self.resolve_geometry_box(&geometry_box);
56
+ self.basic_shape_to_path(&basic_shape, reference_box)
57
+ }
58
+ ClipPath::Box(geometry_box) => {
59
+ let reference_box = self.resolve_geometry_box(&geometry_box);
60
+ Some(Rect::from(reference_box).into_path(0.1))
61
+ }
62
+ }
63
+ }
64
+
65
+ /// Resolve a ShapeGeometryBox to a concrete rectangle (x, y, width, height) in scaled pixels
66
+ ///
67
+ /// For SVG elements without associated CSS layout box, the used value for content-box and padding-box is fill-box and for border-box and margin-box is stroke-box.
68
+ /// For elements with associated CSS layout box, the used value for fill-box is content-box and for stroke-box and view-box is border-box.
69
+ fn resolve_geometry_box(&self, geometry_box: &ShapeGeometryBox) -> ReferenceBox {
70
+ match geometry_box {
71
+ ShapeGeometryBox::ElementDependent
72
+ | ShapeGeometryBox::StrokeBox
73
+ | ShapeGeometryBox::ViewBox
74
+ | ShapeGeometryBox::ShapeBox(ShapeBox::BorderBox) => ReferenceBox {
75
+ x: 0.0,
76
+ y: 0.0,
77
+ width: self.frame.border_box.width() / self.scale,
78
+ height: self.frame.border_box.height() / self.scale,
79
+ },
80
+ ShapeGeometryBox::ShapeBox(ShapeBox::PaddingBox) => ReferenceBox {
81
+ x: self.frame.border_width.x0 / self.scale,
82
+ y: self.frame.border_width.y0 / self.scale,
83
+ width: self.frame.padding_box.width() / self.scale,
84
+ height: self.frame.padding_box.height() / self.scale,
85
+ },
86
+ ShapeGeometryBox::FillBox | ShapeGeometryBox::ShapeBox(ShapeBox::ContentBox) => {
87
+ ReferenceBox {
88
+ x: (self.frame.border_width.x0 + self.frame.padding_width.x0) / self.scale,
89
+ y: (self.frame.border_width.y0 + self.frame.padding_width.y0) / self.scale,
90
+ width: self.frame.content_box.width() / self.scale,
91
+ height: self.frame.content_box.height() / self.scale,
92
+ }
93
+ }
94
+ ShapeGeometryBox::ShapeBox(ShapeBox::MarginBox) => {
95
+ // Margin box is not tracked in CssBox, fall back to border box
96
+ ReferenceBox {
97
+ x: 0.0,
98
+ y: 0.0,
99
+ width: self.frame.border_box.width() / self.scale,
100
+ height: self.frame.border_box.height() / self.scale,
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ /// Convert a CSS basic-shape to a kurbo BezPath
107
+ fn basic_shape_to_path(
108
+ &self,
109
+ shape: &BasicShape,
110
+ reference_box: ReferenceBox,
111
+ ) -> Option<BezPath> {
112
+ let w = reference_box.width;
113
+ let h = reference_box.height;
114
+ let ox = reference_box.x;
115
+ let oy = reference_box.y;
116
+
117
+ match shape {
118
+ GenericBasicShape::Circle(circle) => {
119
+ let (cx, cy) = resolve_position(&circle.position, w, h, ox, oy);
120
+ let r = resolve_shape_radius(&circle.radius, w, h, cx - ox, cy - oy);
121
+ Some(Circle::new(Point::new(cx, cy), r).into_path(0.1))
122
+ }
123
+ GenericBasicShape::Ellipse(ellipse) => {
124
+ let (cx, cy) = resolve_position(&ellipse.position, w, h, ox, oy);
125
+ let rx = resolve_shape_radius(&ellipse.semiaxis_x, w, h, cx - ox, cy - oy);
126
+ let ry = resolve_shape_radius(&ellipse.semiaxis_y, h, w, cy - oy, cx - ox);
127
+ Some(Ellipse::new(Point::new(cx, cy), (rx, ry), 0.0).into_path(0.1))
128
+ }
129
+ GenericBasicShape::Polygon(polygon) => {
130
+ let mut path = BezPath::new();
131
+ let _fill = &polygon.fill;
132
+ let coords = &polygon.coordinates;
133
+
134
+ if coords.is_empty() {
135
+ return None;
136
+ }
137
+
138
+ for (i, coord) in coords.iter().enumerate() {
139
+ let x = ox + resolve_lp(&coord.0, w);
140
+ let y = oy + resolve_lp(&coord.1, h);
141
+ if i == 0 {
142
+ path.move_to(Point::new(x, y));
143
+ } else {
144
+ path.line_to(Point::new(x, y));
145
+ }
146
+ }
147
+ path.close_path();
148
+
149
+ Some(path)
150
+ }
151
+ GenericBasicShape::Rect(inset_rect) => {
152
+ // inset() function: inset(top right bottom left round border-radius)
153
+ let top = resolve_lp(&inset_rect.rect.0, h);
154
+ let right = resolve_lp(&inset_rect.rect.1, w);
155
+ let bottom = resolve_lp(&inset_rect.rect.2, h);
156
+ let left = resolve_lp(&inset_rect.rect.3, w);
157
+
158
+ let x0 = ox + left;
159
+ let y0 = oy + top;
160
+ let x1 = ox + w - right;
161
+ let y1 = oy + h - bottom;
162
+
163
+ if x1 <= x0 || y1 <= y0 {
164
+ return None;
165
+ }
166
+
167
+ // TODO: Support border-radius on inset()
168
+ Some(kurbo::Rect::new(x0, y0, x1, y1).into_path(0.1))
169
+ }
170
+ GenericBasicShape::PathOrShape(path_or_shape) => match path_or_shape {
171
+ GenericPathOrShapeFunction::Path(path) => svg_path_to_bezpath(
172
+ path.commands(),
173
+ w,
174
+ h,
175
+ |v| *v as f64,
176
+ |v| *v as f64,
177
+ |v| *v as f64,
178
+ )
179
+ .map(|mut p| {
180
+ p.apply_affine(kurbo::Affine::translate((ox, oy)));
181
+ p
182
+ }),
183
+ GenericPathOrShapeFunction::Shape(shape) => svg_path_to_bezpath(
184
+ &shape.commands,
185
+ w,
186
+ h,
187
+ move |v: &LengthPercentage| {
188
+ v.resolve(CSSPixelLength::new(w as f32)).px() as f64
189
+ },
190
+ move |v: &LengthPercentage| {
191
+ v.resolve(CSSPixelLength::new(h as f32)).px() as f64
192
+ },
193
+ move |v: &Angle| v.degrees() as f64,
194
+ )
195
+ .map(|mut p| {
196
+ p.apply_affine(kurbo::Affine::translate((ox, oy)));
197
+ p
198
+ }),
199
+ },
200
+ }
201
+ }
202
+ }
203
+
204
+ /// A resolved reference box in scaled pixel coordinates
205
+ #[derive(Clone, Copy)]
206
+ struct ReferenceBox {
207
+ x: f64,
208
+ y: f64,
209
+ width: f64,
210
+ height: f64,
211
+ }
212
+
213
+ impl From<ReferenceBox> for kurbo::Rect {
214
+ fn from(reference_box: ReferenceBox) -> Self {
215
+ kurbo::Rect::new(
216
+ reference_box.x,
217
+ reference_box.y,
218
+ reference_box.x + reference_box.width,
219
+ reference_box.y + reference_box.height,
220
+ )
221
+ }
222
+ }
223
+
224
+ /// Resolve a LengthPercentage value against a basis length (already scaled)
225
+ fn resolve_lp(lp: &LengthPercentage, basis: f64) -> f64 {
226
+ let basis_css = CSSPixelLength::new(basis as f32);
227
+ lp.resolve(basis_css).px() as f64
228
+ }
229
+
230
+ /// Resolve a position (or auto => center) to absolute coordinates
231
+ fn resolve_position(
232
+ position: &GenericPositionOrAuto<style::values::computed::Position>,
233
+ w: f64,
234
+ h: f64,
235
+ ox: f64,
236
+ oy: f64,
237
+ ) -> (f64, f64) {
238
+ match position {
239
+ GenericPositionOrAuto::Auto => (ox + w / 2.0, oy + h / 2.0),
240
+ GenericPositionOrAuto::Position(pos) => {
241
+ let x = ox + resolve_lp(&pos.horizontal, w);
242
+ let y = oy + resolve_lp(&pos.vertical, h);
243
+ (x, y)
244
+ }
245
+ }
246
+ }
247
+
248
+ /// Resolve a shape radius keyword or length value
249
+ fn resolve_shape_radius(
250
+ radius: &GenericShapeRadius<LengthPercentage>,
251
+ primary_size: f64,
252
+ secondary_size: f64,
253
+ center_offset_primary: f64,
254
+ center_offset_secondary: f64,
255
+ ) -> f64 {
256
+ match radius {
257
+ GenericShapeRadius::Length(lp) => resolve_lp(&lp.0, primary_size),
258
+ GenericShapeRadius::ClosestSide => center_offset_primary
259
+ .min(primary_size - center_offset_primary)
260
+ .min(center_offset_secondary)
261
+ .min(secondary_size - center_offset_secondary)
262
+ .max(0.0),
263
+ GenericShapeRadius::FarthestSide => center_offset_primary
264
+ .max(primary_size - center_offset_primary)
265
+ .max(center_offset_secondary)
266
+ .max(secondary_size - center_offset_secondary),
267
+
268
+ GenericShapeRadius::FarthestCorner => {
269
+ let dp = center_offset_primary.max(primary_size - center_offset_primary);
270
+ let ds = center_offset_secondary.max(secondary_size - center_offset_secondary);
271
+ dp.hypot(ds)
272
+ }
273
+ GenericShapeRadius::ClosestCorner => {
274
+ let dp = center_offset_primary
275
+ .min(primary_size - center_offset_primary)
276
+ .max(0.0);
277
+ let ds = center_offset_secondary
278
+ .min(secondary_size - center_offset_secondary)
279
+ .max(0.0);
280
+ dp.hypot(ds)
281
+ }
282
+ }
283
+ }
284
+
285
+ type GenericPathCommand<Angle, N> = GenericShapeCommand<Angle, GenericPosition<N, N>, N>;
286
+
287
+ /// Convert an SVG path() to a kurbo BezPath.
288
+ /// The returned path is in the path's own coordinate system (origin at 0,0).
289
+ /// The caller is responsible for translating it to the reference box origin.
290
+ fn svg_path_to_bezpath<Angle: Copy, N>(
291
+ commands: &[GenericPathCommand<Angle, N>],
292
+ w: f64,
293
+ h: f64,
294
+ resolve_x: impl Fn(&N) -> f64,
295
+ resolve_y: impl Fn(&N) -> f64,
296
+ resolve_angle: impl Fn(&Angle) -> f64,
297
+ ) -> Option<BezPath> {
298
+ if commands.is_empty() {
299
+ return None;
300
+ }
301
+
302
+ let mut bez = BezPath::new();
303
+ let mut cur = Point::ZERO;
304
+ let mut subpath_start = cur;
305
+ // Tracks the last control point for smooth continuation.
306
+ let mut last_control: Option<Point> = None;
307
+
308
+ for cmd in commands {
309
+ match &cmd {
310
+ GenericShapeCommand::Close => {
311
+ bez.close_path();
312
+ cur = subpath_start;
313
+ last_control = None;
314
+ }
315
+ GenericShapeCommand::Move { point } => {
316
+ let p = resolve_endpoint(point, cur, &resolve_x, &resolve_y);
317
+ bez.move_to(p);
318
+ cur = p;
319
+ subpath_start = p;
320
+ last_control = None;
321
+ }
322
+ GenericShapeCommand::Line { point } => {
323
+ let p = resolve_endpoint(point, cur, &resolve_x, &resolve_y);
324
+ bez.line_to(p);
325
+ cur = p;
326
+ last_control = None;
327
+ }
328
+ GenericShapeCommand::HLine { x } => {
329
+ cur.x = resolve_axis_endpoint(x, cur.x, w, &resolve_x);
330
+ bez.line_to(cur);
331
+ last_control = None;
332
+ }
333
+ GenericShapeCommand::VLine { y } => {
334
+ cur.y = resolve_axis_endpoint(y, cur.y, h, &resolve_y);
335
+ bez.line_to(cur);
336
+ last_control = None;
337
+ }
338
+ GenericShapeCommand::CubicCurve {
339
+ point,
340
+ control1,
341
+ control2,
342
+ } => {
343
+ let p = resolve_endpoint(point, cur, &resolve_x, &resolve_y);
344
+ let c1 = resolve_control_point(control1, cur, p, &resolve_x, &resolve_y);
345
+ let c2 = resolve_control_point(control2, cur, p, &resolve_x, &resolve_y);
346
+ bez.curve_to(c1, c2, p);
347
+ last_control = Some(c2);
348
+ cur = p;
349
+ }
350
+ GenericShapeCommand::QuadCurve { point, control1 } => {
351
+ let p = resolve_endpoint(point, cur, &resolve_x, &resolve_y);
352
+ let c1 = resolve_control_point(control1, cur, p, &resolve_x, &resolve_y);
353
+ bez.quad_to(c1, p);
354
+ last_control = Some(c1);
355
+ cur = p;
356
+ }
357
+ GenericShapeCommand::SmoothCubic { point, control2 } => {
358
+ let p = resolve_endpoint(point, cur, &resolve_x, &resolve_y);
359
+ let c2 = resolve_control_point(control2, cur, p, &resolve_x, &resolve_y);
360
+ let c1 = reflect_point(last_control, cur);
361
+ bez.curve_to(c1, c2, p);
362
+ last_control = Some(c2);
363
+ cur = p;
364
+ }
365
+ GenericShapeCommand::SmoothQuad { point } => {
366
+ let p = resolve_endpoint(point, cur, &resolve_x, &resolve_y);
367
+ let c1 = reflect_point(last_control, cur);
368
+ bez.quad_to(c1, p);
369
+ last_control = Some(c1);
370
+ cur = p;
371
+ }
372
+ GenericShapeCommand::Arc {
373
+ point,
374
+ radii,
375
+ arc_sweep,
376
+ arc_size,
377
+ rotate,
378
+ } => {
379
+ // SVG arc commands are complex; approximate as a line for now
380
+ let p = resolve_endpoint(point, cur, &resolve_x, &resolve_y);
381
+ let svg_arc = SvgArc {
382
+ from: cur,
383
+ to: p,
384
+ radii: Vec2 {
385
+ x: resolve_x(&radii.rx),
386
+ y: resolve_y(radii.ry.as_ref().unwrap_or(&radii.rx)),
387
+ },
388
+ x_rotation: resolve_angle(rotate),
389
+ large_arc: match arc_size {
390
+ ArcSize::Large => true,
391
+ ArcSize::Small => false,
392
+ },
393
+ // TODO: check mapping is correct
394
+ sweep: match arc_sweep {
395
+ ArcSweep::Ccw => true,
396
+ ArcSweep::Cw => false,
397
+ },
398
+ };
399
+ let arc = kurbo::Arc::from_svg_arc(&svg_arc)?;
400
+ for el in arc.append_iter(0.1) {
401
+ bez.push(el);
402
+ }
403
+ last_control = None;
404
+ cur = p;
405
+ }
406
+ }
407
+ }
408
+
409
+ Some(bez)
410
+ }
411
+
412
+ /// Resolve a CommandEndPoint to an absolute Point.
413
+ /// `ToPosition` is absolute; `ByCoordinate` is relative to `cur`.
414
+ fn resolve_endpoint<N>(
415
+ endpoint: &CommandEndPoint<GenericPosition<N, N>, N>,
416
+ cur: Point,
417
+ resolve_x: impl Fn(&N) -> f64,
418
+ resolve_y: impl Fn(&N) -> f64,
419
+ ) -> Point {
420
+ match endpoint {
421
+ CommandEndPoint::ToPosition(pos) => {
422
+ Point::new(resolve_x(&pos.horizontal), resolve_y(&pos.vertical))
423
+ }
424
+ CommandEndPoint::ByCoordinate(coord) => {
425
+ Point::new(cur.x + resolve_x(&coord.x), cur.y + resolve_y(&coord.y))
426
+ }
427
+ }
428
+ }
429
+
430
+ /// Resolve a ControlPoint to an absolute Point.
431
+ /// - `Absolute`: uses the position directly.
432
+ /// - `Relative`: the coordinate pair is offset from a base point determined by `ControlReference`:
433
+ /// - `Start` (default): relative to the command's starting point (`cur`).
434
+ /// - `End`: relative to the command's end point (`end`).
435
+ /// - `Origin`: relative to the reference box origin (0,0 in path coordinates).
436
+ fn resolve_control_point<N>(
437
+ control_point: &ControlPoint<GenericPosition<N, N>, N>,
438
+ cur: Point,
439
+ end: Point,
440
+ resolve_x: impl Fn(&N) -> f64,
441
+ resolve_y: impl Fn(&N) -> f64,
442
+ ) -> Point {
443
+ match control_point {
444
+ ControlPoint::Absolute(pos) => {
445
+ Point::new(resolve_x(&pos.horizontal), resolve_y(&pos.vertical))
446
+ }
447
+ ControlPoint::Relative(rel) => {
448
+ let base = match rel.reference {
449
+ ControlReference::Start => cur,
450
+ ControlReference::End => end,
451
+ ControlReference::Origin => Point::ZERO,
452
+ };
453
+ Point::new(
454
+ base.x + resolve_x(&rel.coord.x),
455
+ base.y + resolve_y(&rel.coord.y),
456
+ )
457
+ }
458
+ }
459
+ }
460
+
461
+ /// Reflect a previous control point around the current point.
462
+ /// If there is no previous control point (previous command wasn't the matching curve type),
463
+ /// returns the current point itself.
464
+ fn reflect_point(last_control: Option<Point>, cur: Point) -> Point {
465
+ match last_control {
466
+ Some(ctrl) => Point::new(2.0 * cur.x - ctrl.x, 2.0 * cur.y - ctrl.y),
467
+ None => cur,
468
+ }
469
+ }
470
+
471
+ /// Resolve an AxisEndPoint to an absolute value.
472
+ /// `ToPosition` is absolute; `ByCoordinate` is relative to `cur_val`.
473
+ /// `w` and `h` are the reference box dimensions so keywords resolve against the correct axis.
474
+ fn resolve_axis_endpoint<N>(
475
+ endpoint: &AxisEndPoint<N>,
476
+ cur_val: f64,
477
+ basis: f64,
478
+ resolve: impl Fn(&N) -> f64,
479
+ ) -> f64 {
480
+ use style::values::generics::basic_shape::AxisPositionKeyword;
481
+ match endpoint {
482
+ AxisEndPoint::ToPosition(AxisPosition::LengthPercent(lp)) => resolve(lp),
483
+ AxisEndPoint::ToPosition(AxisPosition::Keyword(kw)) => match kw {
484
+ AxisPositionKeyword::Left | AxisPositionKeyword::XStart => 0.0,
485
+ AxisPositionKeyword::Right | AxisPositionKeyword::XEnd => basis,
486
+ AxisPositionKeyword::Top | AxisPositionKeyword::YStart => 0.0,
487
+ AxisPositionKeyword::Bottom | AxisPositionKeyword::YEnd => basis,
488
+ AxisPositionKeyword::Center => basis / 2.0,
489
+ },
490
+ AxisEndPoint::ByCoordinate(val) => cur_val + resolve(val),
491
+ }
492
+ }