1use std::rc::Rc;
2
3use cranpose_ui::text::{
4 AnnotatedString, TextLayoutOptions, TextOverflow, TextStyle, text_style_for_draw_style,
5};
6use cranpose_ui_graphics::{
7 ArcGeometry, BlendMode, Brush, Color, ColorFilter, CornerRadii, DrawPrimitive, GraphicsLayer,
8 ImageBitmap, ImageSampling, LineGeometry, Point, Rect, RoundedCornerShape, ShadowPrimitive,
9 Stroke, TextPrimitive, Trapezoid, arc_band, inflate_rect,
10};
11
12use crate::{
13 graph::quad_bounds,
14 layer_transform::{
15 apply_layer_affine_to_point, apply_layer_affine_to_rect, apply_layer_to_quad,
16 apply_layer_to_rect, layer_uniform_scale,
17 },
18 style_shared::{
19 ResolvedBrush, apply_layer_to_brush, apply_layer_to_color, compose_color_filters,
20 resolve_layer_brush, scale_corner_radii,
21 },
22};
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum PrimitiveClipSpace {
27 Local,
29 LayerTransformed,
31}
32
33pub struct ShapeDrawParams {
35 pub rect: Rect,
37 pub local_rect: Rect,
39 pub quad: [[f32; 2]; 4],
41 pub brush: ResolvedBrush,
45 pub shape: Option<RoundedCornerShape>,
47 pub stroke: Option<Stroke>,
52 pub arc: Option<ArcGeometry>,
55 pub line: Option<LineGeometry>,
58 pub trapezoid: Option<Trapezoid>,
62 pub clip: Option<Rect>,
64 pub blend_mode: BlendMode,
66 pub motion_context_animated: bool,
69}
70
71fn stroked_draw_rect(
72 local_rect: Rect,
73 stroke: Option<Stroke>,
74 layer_bounds: Rect,
75 layer: &GraphicsLayer,
76) -> Option<(Rect, Option<Stroke>)> {
77 let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
78 let Some(stroke) = stroke else {
79 return Some((draw_rect, None));
80 };
81 if !stroke.is_visible() {
82 return None;
83 }
84 let outset = stroke.half_width();
85 Some((
86 inflate_rect(draw_rect, outset),
87 Some(stroke.scaled(layer_uniform_scale(layer))),
88 ))
89}
90
91pub struct ImageDrawParams {
93 pub rect: Rect,
95 pub local_rect: Rect,
97 pub quad: [[f32; 2]; 4],
99 pub image: ImageBitmap,
101 pub alpha: f32,
103 pub color_filter: Option<ColorFilter>,
105 pub sampling: ImageSampling,
107 pub clip: Option<Rect>,
109 pub src_rect: Option<Rect>,
111 pub blend_mode: BlendMode,
113 pub motion_context_animated: bool,
116}
117
118pub struct TextDrawParams {
125 pub rect: Rect,
128 pub text: Rc<AnnotatedString>,
130 pub render_text: std::sync::Arc<cranpose_ui::text::RenderString>,
132 pub color: Color,
134 pub text_style: TextStyle,
136 pub font_size: f32,
138 pub scale: f32,
140 pub layout_options: TextLayoutOptions,
142 pub clip: Option<Rect>,
144}
145
146pub trait DrawPrimitiveSink {
148 fn push_shape(&mut self, params: ShapeDrawParams);
150
151 fn push_image(&mut self, params: ImageDrawParams);
153
154 fn push_shadow(
156 &mut self,
157 shadow_primitive: &ShadowPrimitive,
158 layer_bounds: Rect,
159 layer: &GraphicsLayer,
160 clip: Option<Rect>,
161 );
162
163 fn push_text(&mut self, params: TextDrawParams) {
167 let _ = params;
168 }
169}
170
171pub fn draw_shape_params_for_primitive(
173 primitive: &DrawPrimitive,
174 layer_bounds: Rect,
175 layer: &GraphicsLayer,
176 clip: Option<Rect>,
177 blend_mode: BlendMode,
178) -> Option<ShapeDrawParams> {
179 struct SingleShapeSink {
180 shape: Option<ShapeDrawParams>,
181 }
182
183 impl DrawPrimitiveSink for SingleShapeSink {
184 fn push_shape(&mut self, params: ShapeDrawParams) {
185 if self.shape.is_none() {
186 self.shape = Some(params);
187 }
188 }
189
190 fn push_image(&mut self, _params: ImageDrawParams) {}
191
192 fn push_shadow(
193 &mut self,
194 _shadow_primitive: &ShadowPrimitive,
195 _layer_bounds: Rect,
196 _layer: &GraphicsLayer,
197 _clip: Option<Rect>,
198 ) {
199 }
200 }
201
202 let mut sink = SingleShapeSink { shape: None };
203 emit_draw_primitive(
204 primitive,
205 layer_bounds,
206 layer,
207 clip,
208 &mut sink,
209 Some(blend_mode),
210 false,
211 );
212 sink.shape
213}
214
215pub fn resolve_clip(parent_clip: Option<Rect>, requested_clip: Option<Rect>) -> Option<Rect> {
220 match (parent_clip, requested_clip) {
221 (Some(parent), Some(current)) => Some(parent.intersect(current).unwrap_or(Rect::EMPTY)),
222 (Some(parent), None) => Some(parent),
223 (None, Some(current)) => Some(current),
224 (None, None) => None,
225 }
226}
227
228pub fn resolve_primitive_clip(
232 local_clip: Option<Rect>,
233 layer_bounds: Rect,
234 layer: &GraphicsLayer,
235 parent_clip: Option<Rect>,
236 clip_space: PrimitiveClipSpace,
237) -> Option<Rect> {
238 let Some(local_clip) = local_clip else {
239 return parent_clip;
240 };
241 let clip_rect = Rect {
242 x: layer_bounds.x + local_clip.x,
243 y: layer_bounds.y + local_clip.y,
244 width: local_clip.width,
245 height: local_clip.height,
246 };
247 let requested_clip = match clip_space {
248 PrimitiveClipSpace::Local => clip_rect,
249 PrimitiveClipSpace::LayerTransformed => apply_layer_to_rect(clip_rect, layer_bounds, layer),
250 };
251 resolve_clip(parent_clip, Some(requested_clip))
252}
253
254#[expect(clippy::too_many_arguments)]
259pub fn rect_shape_params(
260 local_rect: Rect,
261 brush: &Brush,
262 stroke: Option<Stroke>,
263 layer_bounds: Rect,
264 layer: &GraphicsLayer,
265 clip: Option<Rect>,
266 blend_mode: BlendMode,
267 motion_context_animated: bool,
268) -> Option<ShapeDrawParams> {
269 let (draw_rect, stroke) = stroked_draw_rect(local_rect, stroke, layer_bounds, layer)?;
270 let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
271 let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
272 Some(ShapeDrawParams {
273 rect: quad_bounds(quad),
274 local_rect,
275 quad,
276 brush: resolve_layer_brush(brush, layer),
277 shape: None,
278 stroke,
279 arc: None,
280 line: None,
281 trapezoid: None,
282 clip,
283 blend_mode,
284 motion_context_animated,
285 })
286}
287
288#[expect(clippy::too_many_arguments)]
291pub fn round_rect_shape_params(
292 local_rect: Rect,
293 brush: &Brush,
294 radii: CornerRadii,
295 stroke: Option<Stroke>,
296 layer_bounds: Rect,
297 layer: &GraphicsLayer,
298 clip: Option<Rect>,
299 blend_mode: BlendMode,
300 motion_context_animated: bool,
301) -> Option<ShapeDrawParams> {
302 let (draw_rect, stroke) = stroked_draw_rect(local_rect, stroke, layer_bounds, layer)?;
303 let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
304 let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
305 let shape =
306 RoundedCornerShape::with_radii(scale_corner_radii(radii, layer_uniform_scale(layer)));
307 Some(ShapeDrawParams {
308 rect: quad_bounds(quad),
309 local_rect,
310 quad,
311 brush: resolve_layer_brush(brush, layer),
312 shape: Some(shape),
313 stroke,
314 arc: None,
315 line: None,
316 trapezoid: None,
317 clip,
318 blend_mode,
319 motion_context_animated,
320 })
321}
322
323#[expect(clippy::too_many_arguments)]
326pub fn arc_shape_params(
327 local_rect: Rect,
328 brush: &Brush,
329 center: Point,
330 radius: f32,
331 start_angle: f32,
332 sweep_angle: f32,
333 stroke: Option<Stroke>,
334 inner_radius: f32,
335 layer_bounds: Rect,
336 layer: &GraphicsLayer,
337 clip: Option<Rect>,
338 blend_mode: BlendMode,
339 motion_context_animated: bool,
340) -> Option<ShapeDrawParams> {
341 let (band_inner, band_outer, cap) = arc_band(radius, inner_radius, stroke);
342 let arc = ArcGeometry::new(
343 center,
344 band_inner,
345 band_outer,
346 start_angle,
347 sweep_angle,
348 cap,
349 );
350 if arc.is_degenerate() {
351 return None;
352 }
353 let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
354 let out_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
355 let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
356 let scale = layer_uniform_scale(layer);
357 let arc_center = apply_layer_affine_to_point(
358 Point::new(center.x + layer_bounds.x, center.y + layer_bounds.y),
359 layer_bounds,
360 layer,
361 );
362 Some(ShapeDrawParams {
363 rect: quad_bounds(quad),
364 local_rect: out_rect,
365 quad,
366 brush: resolve_layer_brush(brush, layer),
367 shape: None,
368 stroke: None,
369 arc: Some(arc.scaled_about(arc_center, scale)),
370 line: None,
371 trapezoid: None,
372 clip,
373 blend_mode,
374 motion_context_animated,
375 })
376}
377
378#[expect(clippy::too_many_arguments)]
382pub fn line_shape_params(
383 brush: &Brush,
384 start: Point,
385 end: Point,
386 stroke: Stroke,
387 layer_bounds: Rect,
388 layer: &GraphicsLayer,
389 clip: Option<Rect>,
390 blend_mode: BlendMode,
391 motion_context_animated: bool,
392) -> Option<ShapeDrawParams> {
393 let line = LineGeometry::new(start, end, stroke);
394 if line.is_degenerate() {
395 return None;
396 }
397 let place = |point: Point| {
398 apply_layer_affine_to_point(
399 Point::new(point.x + layer_bounds.x, point.y + layer_bounds.y),
400 layer_bounds,
401 layer,
402 )
403 };
404 let draw_rect = line.bounds().translate(layer_bounds.x, layer_bounds.y);
405 let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
406 Some(ShapeDrawParams {
407 rect: quad_bounds(quad),
408 local_rect: apply_layer_affine_to_rect(draw_rect, layer_bounds, layer),
409 quad,
410 brush: resolve_layer_brush(brush, layer),
411 shape: None,
412 stroke: None,
413 arc: None,
414 line: Some(line.placed(place(start), place(end), layer_uniform_scale(layer))),
415 trapezoid: None,
416 clip,
417 blend_mode,
418 motion_context_animated,
419 })
420}
421
422#[expect(clippy::too_many_arguments)]
426pub fn trapezoid_shape_params(
427 brush_rect: Rect,
428 brush: &Brush,
429 trapezoid: Trapezoid,
430 layer_bounds: Rect,
431 layer: &GraphicsLayer,
432 clip: Option<Rect>,
433 blend_mode: BlendMode,
434 motion_context_animated: bool,
435) -> Option<ShapeDrawParams> {
436 if trapezoid.right <= trapezoid.left {
437 return None;
438 }
439 let place = |x: f32, y: f32| {
440 apply_layer_affine_to_point(
441 Point::new(x + layer_bounds.x, y + layer_bounds.y),
442 layer_bounds,
443 layer,
444 )
445 };
446 let [top_left, top_right, bottom_left, bottom_right] = [
447 place(trapezoid.left, trapezoid.top[0]),
448 place(trapezoid.right, trapezoid.top[1]),
449 place(trapezoid.left, trapezoid.bottom[0]),
450 place(trapezoid.right, trapezoid.bottom[1]),
451 ];
452 let draw_rect = brush_rect.translate(layer_bounds.x, layer_bounds.y);
453 let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
454 Some(ShapeDrawParams {
455 rect: quad_bounds(quad),
456 local_rect: apply_layer_affine_to_rect(draw_rect, layer_bounds, layer),
457 quad,
458 brush: resolve_layer_brush(brush, layer),
459 shape: None,
460 stroke: None,
461 arc: None,
462 line: None,
463 trapezoid: Some(Trapezoid {
464 left: top_left.x,
465 right: top_right.x,
466 top: [top_left.y, top_right.y],
467 bottom: [bottom_left.y, bottom_right.y],
468 ..trapezoid
469 }),
470 clip,
471 blend_mode,
472 motion_context_animated,
473 })
474}
475
476pub fn emit_draw_primitive<S: DrawPrimitiveSink>(
481 primitive: &DrawPrimitive,
482 layer_bounds: Rect,
483 layer: &GraphicsLayer,
484 clip: Option<Rect>,
485 sink: &mut S,
486 blend_mode: Option<BlendMode>,
487 motion_context_animated: bool,
488) {
489 match primitive {
490 DrawPrimitive::Content | DrawPrimitive::Group(_) => {}
491 DrawPrimitive::Blend {
492 primitive,
493 blend_mode: nested,
494 } => emit_draw_primitive(
495 primitive,
496 layer_bounds,
497 layer,
498 clip,
499 sink,
500 blend_mode.or(Some(*nested)),
501 motion_context_animated,
502 ),
503 DrawPrimitive::Rect {
504 rect: local_rect,
505 brush,
506 stroke,
507 } => {
508 if let Some(params) = rect_shape_params(
509 *local_rect,
510 brush,
511 *stroke,
512 layer_bounds,
513 layer,
514 clip,
515 blend_mode.unwrap_or(BlendMode::SrcOver),
516 motion_context_animated,
517 ) {
518 sink.push_shape(params);
519 }
520 }
521 DrawPrimitive::RoundRect {
522 rect: local_rect,
523 brush,
524 radii,
525 stroke,
526 } => {
527 if let Some(params) = round_rect_shape_params(
528 *local_rect,
529 brush,
530 *radii,
531 *stroke,
532 layer_bounds,
533 layer,
534 clip,
535 blend_mode.unwrap_or(BlendMode::SrcOver),
536 motion_context_animated,
537 ) {
538 sink.push_shape(params);
539 }
540 }
541 DrawPrimitive::Arc {
542 rect: local_rect,
543 brush,
544 center,
545 radius,
546 start_angle,
547 sweep_angle,
548 stroke,
549 inner_radius,
550 } => {
551 if let Some(params) = arc_shape_params(
552 *local_rect,
553 brush,
554 *center,
555 *radius,
556 *start_angle,
557 *sweep_angle,
558 *stroke,
559 *inner_radius,
560 layer_bounds,
561 layer,
562 clip,
563 blend_mode.unwrap_or(BlendMode::SrcOver),
564 motion_context_animated,
565 ) {
566 sink.push_shape(params);
567 }
568 }
569 DrawPrimitive::Line {
570 brush,
571 start,
572 end,
573 stroke,
574 ..
575 } => {
576 if let Some(params) = line_shape_params(
577 brush,
578 *start,
579 *end,
580 *stroke,
581 layer_bounds,
582 layer,
583 clip,
584 blend_mode.unwrap_or(BlendMode::SrcOver),
585 motion_context_animated,
586 ) {
587 sink.push_shape(params);
588 }
589 }
590 DrawPrimitive::Trapezoid {
591 rect,
592 brush,
593 trapezoid,
594 } => {
595 if let Some(params) = trapezoid_shape_params(
596 *rect,
597 brush,
598 *trapezoid,
599 layer_bounds,
600 layer,
601 clip,
602 blend_mode.unwrap_or(BlendMode::SrcOver),
603 motion_context_animated,
604 ) {
605 sink.push_shape(params);
606 }
607 }
608 DrawPrimitive::Image {
609 rect: local_rect,
610 image,
611 alpha,
612 color_filter,
613 sampling,
614 src_rect,
615 } => {
616 let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
617 let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
618 let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
619 sink.push_image(ImageDrawParams {
620 rect: quad_bounds(quad),
621 local_rect,
622 quad,
623 image: image.clone(),
624 alpha: (alpha * layer.alpha).clamp(0.0, 1.0),
625 color_filter: compose_color_filters(
626 color_filter.as_ref(),
627 layer.color_filter.as_ref(),
628 ),
629 sampling: *sampling,
630 clip,
631 src_rect: *src_rect,
632 blend_mode: blend_mode.unwrap_or(BlendMode::SrcOver),
633 motion_context_animated,
634 });
635 }
636 DrawPrimitive::Text(text) => {
637 if let Some(params) = text_draw_params((**text).clone(), layer_bounds, layer, clip) {
638 sink.push_text(params);
639 }
640 }
641 DrawPrimitive::Shadow(shadow_primitive) => {
642 sink.push_shadow(shadow_primitive, layer_bounds, layer, clip);
643 }
644 }
645}
646
647fn text_draw_params(
648 text: TextPrimitive,
649 layer_bounds: Rect,
650 layer: &GraphicsLayer,
651 clip: Option<Rect>,
652) -> Option<TextDrawParams> {
653 if text.text.is_empty() {
654 return None;
655 }
656 let draw_rect = text.rect.translate(layer_bounds.x, layer_bounds.y);
657 let rect = apply_layer_to_rect(draw_rect, layer_bounds, layer);
658 if !(rect.width > 0.0 && rect.height > 0.0) {
659 return None;
660 }
661 let scale = layer_uniform_scale(layer);
662 if !scale.is_finite() || scale <= 0.0 {
663 return None;
664 }
665 let mut text_style = text_style_for_draw_style(&text.style);
666 let color = match text.brush {
669 Brush::Solid(color) => apply_layer_to_color(color, layer),
670 gradient => {
671 let gradient = apply_layer_to_brush(gradient, layer);
672 let first = gradient
673 .colors()
674 .first()
675 .copied()
676 .unwrap_or(Color::TRANSPARENT);
677 text_style.span_style.brush = Some(gradient);
678 first
679 }
680 };
681 if !text_style
682 .span_style
683 .brush
684 .as_ref()
685 .map_or(color.3 > 0.0, Brush::paints)
686 {
687 return None;
688 }
689
690 Some(TextDrawParams {
691 rect,
692 text: cranpose_ui::text::shared_plain_annotated_string(text.text.as_ref()),
693 render_text: cranpose_ui::text::shared_plain_render_string(text.text.as_ref()),
694 color,
695 text_style,
696 font_size: text.style.resolved_font_size(),
697 scale,
698 layout_options: TextLayoutOptions {
699 soft_wrap: false,
700 overflow: TextOverflow::Visible,
701 ..TextLayoutOptions::default()
702 },
703 clip,
704 })
705}
706
707#[cfg(test)]
708#[path = "tests/primitive_emit_tests.rs"]
709mod tests;