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_color, compose_color_filters, resolve_layer_brush,
20 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>(
480 primitive: &DrawPrimitive,
481 layer_bounds: Rect,
482 layer: &GraphicsLayer,
483 clip: Option<Rect>,
484 sink: &mut S,
485 blend_mode: Option<BlendMode>,
486 motion_context_animated: bool,
487) {
488 match primitive {
489 DrawPrimitive::Content => {}
490 DrawPrimitive::Blend {
491 primitive,
492 blend_mode: nested,
493 } => emit_draw_primitive(
494 primitive,
495 layer_bounds,
496 layer,
497 clip,
498 sink,
499 blend_mode.or(Some(*nested)),
500 motion_context_animated,
501 ),
502 DrawPrimitive::Rect {
503 rect: local_rect,
504 brush,
505 stroke,
506 } => {
507 if let Some(params) = rect_shape_params(
508 *local_rect,
509 brush,
510 *stroke,
511 layer_bounds,
512 layer,
513 clip,
514 blend_mode.unwrap_or(BlendMode::SrcOver),
515 motion_context_animated,
516 ) {
517 sink.push_shape(params);
518 }
519 }
520 DrawPrimitive::RoundRect {
521 rect: local_rect,
522 brush,
523 radii,
524 stroke,
525 } => {
526 if let Some(params) = round_rect_shape_params(
527 *local_rect,
528 brush,
529 *radii,
530 *stroke,
531 layer_bounds,
532 layer,
533 clip,
534 blend_mode.unwrap_or(BlendMode::SrcOver),
535 motion_context_animated,
536 ) {
537 sink.push_shape(params);
538 }
539 }
540 DrawPrimitive::Arc {
541 rect: local_rect,
542 brush,
543 center,
544 radius,
545 start_angle,
546 sweep_angle,
547 stroke,
548 inner_radius,
549 } => {
550 if let Some(params) = arc_shape_params(
551 *local_rect,
552 brush,
553 *center,
554 *radius,
555 *start_angle,
556 *sweep_angle,
557 *stroke,
558 *inner_radius,
559 layer_bounds,
560 layer,
561 clip,
562 blend_mode.unwrap_or(BlendMode::SrcOver),
563 motion_context_animated,
564 ) {
565 sink.push_shape(params);
566 }
567 }
568 DrawPrimitive::Line {
569 brush,
570 start,
571 end,
572 stroke,
573 ..
574 } => {
575 if let Some(params) = line_shape_params(
576 brush,
577 *start,
578 *end,
579 *stroke,
580 layer_bounds,
581 layer,
582 clip,
583 blend_mode.unwrap_or(BlendMode::SrcOver),
584 motion_context_animated,
585 ) {
586 sink.push_shape(params);
587 }
588 }
589 DrawPrimitive::Trapezoid {
590 rect,
591 brush,
592 trapezoid,
593 } => {
594 if let Some(params) = trapezoid_shape_params(
595 *rect,
596 brush,
597 *trapezoid,
598 layer_bounds,
599 layer,
600 clip,
601 blend_mode.unwrap_or(BlendMode::SrcOver),
602 motion_context_animated,
603 ) {
604 sink.push_shape(params);
605 }
606 }
607 DrawPrimitive::Image {
608 rect: local_rect,
609 image,
610 alpha,
611 color_filter,
612 sampling,
613 src_rect,
614 } => {
615 let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
616 let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
617 let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
618 sink.push_image(ImageDrawParams {
619 rect: quad_bounds(quad),
620 local_rect,
621 quad,
622 image: image.clone(),
623 alpha: (alpha * layer.alpha).clamp(0.0, 1.0),
624 color_filter: compose_color_filters(*color_filter, layer.color_filter),
625 sampling: *sampling,
626 clip,
627 src_rect: *src_rect,
628 blend_mode: blend_mode.unwrap_or(BlendMode::SrcOver),
629 motion_context_animated,
630 });
631 }
632 DrawPrimitive::Text(text) => {
633 if let Some(params) = text_draw_params((**text).clone(), layer_bounds, layer, clip) {
634 sink.push_text(params);
635 }
636 }
637 DrawPrimitive::Shadow(shadow_primitive) => {
638 sink.push_shadow(shadow_primitive, layer_bounds, layer, clip);
639 }
640 }
641}
642
643fn text_draw_params(
644 text: TextPrimitive,
645 layer_bounds: Rect,
646 layer: &GraphicsLayer,
647 clip: Option<Rect>,
648) -> Option<TextDrawParams> {
649 if text.text.is_empty() {
650 return None;
651 }
652 let draw_rect = text.rect.translate(layer_bounds.x, layer_bounds.y);
653 let rect = apply_layer_to_rect(draw_rect, layer_bounds, layer);
654 if !(rect.width > 0.0 && rect.height > 0.0) {
655 return None;
656 }
657 let scale = layer_uniform_scale(layer);
658 if !scale.is_finite() || scale <= 0.0 {
659 return None;
660 }
661 let color = apply_layer_to_color(text.color, layer);
662 if color.3 <= 0.0 {
663 return None;
664 }
665
666 Some(TextDrawParams {
667 rect,
668 text: cranpose_ui::text::shared_plain_annotated_string(text.text.as_ref()),
669 render_text: cranpose_ui::text::shared_plain_render_string(text.text.as_ref()),
670 color,
671 text_style: text_style_for_draw_style(&text.style),
672 font_size: text.style.resolved_font_size(),
673 scale,
674 layout_options: TextLayoutOptions {
675 soft_wrap: false,
676 overflow: TextOverflow::Visible,
677 ..TextLayoutOptions::default()
678 },
679 clip,
680 })
681}
682
683#[cfg(test)]
684#[path = "tests/primitive_emit_tests.rs"]
685mod tests;