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)]
25pub enum PrimitiveClipSpace {
26 Local,
27 LayerTransformed,
28}
29
30pub struct ShapeDrawParams {
31 pub rect: Rect,
32 pub local_rect: Rect,
33 pub quad: [[f32; 2]; 4],
34 pub brush: ResolvedBrush,
38 pub shape: Option<RoundedCornerShape>,
39 pub stroke: Option<Stroke>,
44 pub arc: Option<ArcGeometry>,
47 pub line: Option<LineGeometry>,
50 pub trapezoid: Option<Trapezoid>,
54 pub clip: Option<Rect>,
55 pub blend_mode: BlendMode,
56 pub motion_context_animated: bool,
57}
58
59fn stroked_draw_rect(
60 local_rect: Rect,
61 stroke: Option<Stroke>,
62 layer_bounds: Rect,
63 layer: &GraphicsLayer,
64) -> Option<(Rect, Option<Stroke>)> {
65 let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
66 let Some(stroke) = stroke else {
67 return Some((draw_rect, None));
68 };
69 if !stroke.is_visible() {
70 return None;
71 }
72 let outset = stroke.half_width();
73 Some((
74 inflate_rect(draw_rect, outset),
75 Some(stroke.scaled(layer_uniform_scale(layer))),
76 ))
77}
78
79pub struct ImageDrawParams {
80 pub rect: Rect,
81 pub local_rect: Rect,
82 pub quad: [[f32; 2]; 4],
83 pub image: ImageBitmap,
84 pub alpha: f32,
85 pub color_filter: Option<ColorFilter>,
86 pub sampling: ImageSampling,
87 pub clip: Option<Rect>,
88 pub src_rect: Option<Rect>,
89 pub blend_mode: BlendMode,
90 pub motion_context_animated: bool,
91}
92
93pub struct TextDrawParams {
100 pub rect: Rect,
103 pub text: Rc<AnnotatedString>,
104 pub render_text: std::sync::Arc<cranpose_ui::text::RenderString>,
106 pub color: Color,
107 pub text_style: TextStyle,
108 pub font_size: f32,
109 pub scale: f32,
111 pub layout_options: TextLayoutOptions,
112 pub clip: Option<Rect>,
113}
114
115pub trait DrawPrimitiveSink {
116 fn push_shape(&mut self, params: ShapeDrawParams);
117
118 fn push_image(&mut self, params: ImageDrawParams);
119
120 fn push_shadow(
122 &mut self,
123 shadow_primitive: &ShadowPrimitive,
124 layer_bounds: Rect,
125 layer: &GraphicsLayer,
126 clip: Option<Rect>,
127 );
128
129 fn push_text(&mut self, params: TextDrawParams) {
133 let _ = params;
134 }
135}
136
137pub fn draw_shape_params_for_primitive(
139 primitive: &DrawPrimitive,
140 layer_bounds: Rect,
141 layer: &GraphicsLayer,
142 clip: Option<Rect>,
143 blend_mode: BlendMode,
144) -> Option<ShapeDrawParams> {
145 struct SingleShapeSink {
146 shape: Option<ShapeDrawParams>,
147 }
148
149 impl DrawPrimitiveSink for SingleShapeSink {
150 fn push_shape(&mut self, params: ShapeDrawParams) {
151 if self.shape.is_none() {
152 self.shape = Some(params);
153 }
154 }
155
156 fn push_image(&mut self, _params: ImageDrawParams) {}
157
158 fn push_shadow(
159 &mut self,
160 _shadow_primitive: &ShadowPrimitive,
161 _layer_bounds: Rect,
162 _layer: &GraphicsLayer,
163 _clip: Option<Rect>,
164 ) {
165 }
166 }
167
168 let mut sink = SingleShapeSink { shape: None };
169 emit_draw_primitive(
170 primitive,
171 layer_bounds,
172 layer,
173 clip,
174 &mut sink,
175 Some(blend_mode),
176 false,
177 );
178 sink.shape
179}
180
181pub fn resolve_clip(parent_clip: Option<Rect>, requested_clip: Option<Rect>) -> Option<Rect> {
186 match (parent_clip, requested_clip) {
187 (Some(parent), Some(current)) => Some(parent.intersect(current).unwrap_or(Rect::EMPTY)),
188 (Some(parent), None) => Some(parent),
189 (None, Some(current)) => Some(current),
190 (None, None) => None,
191 }
192}
193
194pub fn resolve_primitive_clip(
195 local_clip: Option<Rect>,
196 layer_bounds: Rect,
197 layer: &GraphicsLayer,
198 parent_clip: Option<Rect>,
199 clip_space: PrimitiveClipSpace,
200) -> Option<Rect> {
201 let Some(local_clip) = local_clip else {
202 return parent_clip;
203 };
204 let clip_rect = Rect {
205 x: layer_bounds.x + local_clip.x,
206 y: layer_bounds.y + local_clip.y,
207 width: local_clip.width,
208 height: local_clip.height,
209 };
210 let requested_clip = match clip_space {
211 PrimitiveClipSpace::Local => clip_rect,
212 PrimitiveClipSpace::LayerTransformed => apply_layer_to_rect(clip_rect, layer_bounds, layer),
213 };
214 resolve_clip(parent_clip, Some(requested_clip))
215}
216
217#[expect(clippy::too_many_arguments)]
222pub fn rect_shape_params(
223 local_rect: Rect,
224 brush: &Brush,
225 stroke: Option<Stroke>,
226 layer_bounds: Rect,
227 layer: &GraphicsLayer,
228 clip: Option<Rect>,
229 blend_mode: BlendMode,
230 motion_context_animated: bool,
231) -> Option<ShapeDrawParams> {
232 let (draw_rect, stroke) = stroked_draw_rect(local_rect, stroke, layer_bounds, layer)?;
233 let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
234 let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
235 Some(ShapeDrawParams {
236 rect: quad_bounds(quad),
237 local_rect,
238 quad,
239 brush: resolve_layer_brush(brush, layer),
240 shape: None,
241 stroke,
242 arc: None,
243 line: None,
244 trapezoid: None,
245 clip,
246 blend_mode,
247 motion_context_animated,
248 })
249}
250
251#[expect(clippy::too_many_arguments)]
254pub fn round_rect_shape_params(
255 local_rect: Rect,
256 brush: &Brush,
257 radii: CornerRadii,
258 stroke: Option<Stroke>,
259 layer_bounds: Rect,
260 layer: &GraphicsLayer,
261 clip: Option<Rect>,
262 blend_mode: BlendMode,
263 motion_context_animated: bool,
264) -> Option<ShapeDrawParams> {
265 let (draw_rect, stroke) = stroked_draw_rect(local_rect, stroke, layer_bounds, layer)?;
266 let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
267 let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
268 let shape =
269 RoundedCornerShape::with_radii(scale_corner_radii(radii, layer_uniform_scale(layer)));
270 Some(ShapeDrawParams {
271 rect: quad_bounds(quad),
272 local_rect,
273 quad,
274 brush: resolve_layer_brush(brush, layer),
275 shape: Some(shape),
276 stroke,
277 arc: None,
278 line: None,
279 trapezoid: None,
280 clip,
281 blend_mode,
282 motion_context_animated,
283 })
284}
285
286#[expect(clippy::too_many_arguments)]
289pub fn arc_shape_params(
290 local_rect: Rect,
291 brush: &Brush,
292 center: Point,
293 radius: f32,
294 start_angle: f32,
295 sweep_angle: f32,
296 stroke: Option<Stroke>,
297 inner_radius: f32,
298 layer_bounds: Rect,
299 layer: &GraphicsLayer,
300 clip: Option<Rect>,
301 blend_mode: BlendMode,
302 motion_context_animated: bool,
303) -> Option<ShapeDrawParams> {
304 let (band_inner, band_outer, cap) = arc_band(radius, inner_radius, stroke);
305 let arc = ArcGeometry::new(
306 center,
307 band_inner,
308 band_outer,
309 start_angle,
310 sweep_angle,
311 cap,
312 );
313 if arc.is_degenerate() {
314 return None;
315 }
316 let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
317 let out_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
318 let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
319 let scale = layer_uniform_scale(layer);
320 let arc_center = apply_layer_affine_to_point(
321 Point::new(center.x + layer_bounds.x, center.y + layer_bounds.y),
322 layer_bounds,
323 layer,
324 );
325 Some(ShapeDrawParams {
326 rect: quad_bounds(quad),
327 local_rect: out_rect,
328 quad,
329 brush: resolve_layer_brush(brush, layer),
330 shape: None,
331 stroke: None,
332 arc: Some(arc.scaled_about(arc_center, scale)),
333 line: None,
334 trapezoid: None,
335 clip,
336 blend_mode,
337 motion_context_animated,
338 })
339}
340
341#[expect(clippy::too_many_arguments)]
345pub fn line_shape_params(
346 brush: &Brush,
347 start: Point,
348 end: Point,
349 stroke: Stroke,
350 layer_bounds: Rect,
351 layer: &GraphicsLayer,
352 clip: Option<Rect>,
353 blend_mode: BlendMode,
354 motion_context_animated: bool,
355) -> Option<ShapeDrawParams> {
356 let line = LineGeometry::new(start, end, stroke);
357 if line.is_degenerate() {
358 return None;
359 }
360 let place = |point: Point| {
361 apply_layer_affine_to_point(
362 Point::new(point.x + layer_bounds.x, point.y + layer_bounds.y),
363 layer_bounds,
364 layer,
365 )
366 };
367 let draw_rect = line.bounds().translate(layer_bounds.x, layer_bounds.y);
368 let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
369 Some(ShapeDrawParams {
370 rect: quad_bounds(quad),
371 local_rect: apply_layer_affine_to_rect(draw_rect, layer_bounds, layer),
372 quad,
373 brush: resolve_layer_brush(brush, layer),
374 shape: None,
375 stroke: None,
376 arc: None,
377 line: Some(line.placed(place(start), place(end), layer_uniform_scale(layer))),
378 trapezoid: None,
379 clip,
380 blend_mode,
381 motion_context_animated,
382 })
383}
384
385#[expect(clippy::too_many_arguments)]
389pub fn trapezoid_shape_params(
390 brush_rect: Rect,
391 brush: &Brush,
392 trapezoid: Trapezoid,
393 layer_bounds: Rect,
394 layer: &GraphicsLayer,
395 clip: Option<Rect>,
396 blend_mode: BlendMode,
397 motion_context_animated: bool,
398) -> Option<ShapeDrawParams> {
399 if trapezoid.right <= trapezoid.left {
400 return None;
401 }
402 let place = |x: f32, y: f32| {
403 apply_layer_affine_to_point(
404 Point::new(x + layer_bounds.x, y + layer_bounds.y),
405 layer_bounds,
406 layer,
407 )
408 };
409 let [top_left, top_right, bottom_left, bottom_right] = [
410 place(trapezoid.left, trapezoid.top[0]),
411 place(trapezoid.right, trapezoid.top[1]),
412 place(trapezoid.left, trapezoid.bottom[0]),
413 place(trapezoid.right, trapezoid.bottom[1]),
414 ];
415 let draw_rect = brush_rect.translate(layer_bounds.x, layer_bounds.y);
416 let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
417 Some(ShapeDrawParams {
418 rect: quad_bounds(quad),
419 local_rect: apply_layer_affine_to_rect(draw_rect, layer_bounds, layer),
420 quad,
421 brush: resolve_layer_brush(brush, layer),
422 shape: None,
423 stroke: None,
424 arc: None,
425 line: None,
426 trapezoid: Some(Trapezoid {
427 left: top_left.x,
428 right: top_right.x,
429 top: [top_left.y, top_right.y],
430 bottom: [bottom_left.y, bottom_right.y],
431 ..trapezoid
432 }),
433 clip,
434 blend_mode,
435 motion_context_animated,
436 })
437}
438
439pub fn emit_draw_primitive<S: DrawPrimitiveSink>(
440 primitive: &DrawPrimitive,
441 layer_bounds: Rect,
442 layer: &GraphicsLayer,
443 clip: Option<Rect>,
444 sink: &mut S,
445 blend_mode: Option<BlendMode>,
446 motion_context_animated: bool,
447) {
448 match primitive {
449 DrawPrimitive::Content => {}
450 DrawPrimitive::Blend {
451 primitive,
452 blend_mode: nested,
453 } => emit_draw_primitive(
454 primitive,
455 layer_bounds,
456 layer,
457 clip,
458 sink,
459 blend_mode.or(Some(*nested)),
460 motion_context_animated,
461 ),
462 DrawPrimitive::Rect {
463 rect: local_rect,
464 brush,
465 stroke,
466 } => {
467 if let Some(params) = rect_shape_params(
468 *local_rect,
469 brush,
470 *stroke,
471 layer_bounds,
472 layer,
473 clip,
474 blend_mode.unwrap_or(BlendMode::SrcOver),
475 motion_context_animated,
476 ) {
477 sink.push_shape(params);
478 }
479 }
480 DrawPrimitive::RoundRect {
481 rect: local_rect,
482 brush,
483 radii,
484 stroke,
485 } => {
486 if let Some(params) = round_rect_shape_params(
487 *local_rect,
488 brush,
489 *radii,
490 *stroke,
491 layer_bounds,
492 layer,
493 clip,
494 blend_mode.unwrap_or(BlendMode::SrcOver),
495 motion_context_animated,
496 ) {
497 sink.push_shape(params);
498 }
499 }
500 DrawPrimitive::Arc {
501 rect: local_rect,
502 brush,
503 center,
504 radius,
505 start_angle,
506 sweep_angle,
507 stroke,
508 inner_radius,
509 } => {
510 if let Some(params) = arc_shape_params(
511 *local_rect,
512 brush,
513 *center,
514 *radius,
515 *start_angle,
516 *sweep_angle,
517 *stroke,
518 *inner_radius,
519 layer_bounds,
520 layer,
521 clip,
522 blend_mode.unwrap_or(BlendMode::SrcOver),
523 motion_context_animated,
524 ) {
525 sink.push_shape(params);
526 }
527 }
528 DrawPrimitive::Line {
529 brush,
530 start,
531 end,
532 stroke,
533 ..
534 } => {
535 if let Some(params) = line_shape_params(
536 brush,
537 *start,
538 *end,
539 *stroke,
540 layer_bounds,
541 layer,
542 clip,
543 blend_mode.unwrap_or(BlendMode::SrcOver),
544 motion_context_animated,
545 ) {
546 sink.push_shape(params);
547 }
548 }
549 DrawPrimitive::Trapezoid {
550 rect,
551 brush,
552 trapezoid,
553 } => {
554 if let Some(params) = trapezoid_shape_params(
555 *rect,
556 brush,
557 *trapezoid,
558 layer_bounds,
559 layer,
560 clip,
561 blend_mode.unwrap_or(BlendMode::SrcOver),
562 motion_context_animated,
563 ) {
564 sink.push_shape(params);
565 }
566 }
567 DrawPrimitive::Image {
568 rect: local_rect,
569 image,
570 alpha,
571 color_filter,
572 sampling,
573 src_rect,
574 } => {
575 let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
576 let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
577 let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
578 sink.push_image(ImageDrawParams {
579 rect: quad_bounds(quad),
580 local_rect,
581 quad,
582 image: image.clone(),
583 alpha: (alpha * layer.alpha).clamp(0.0, 1.0),
584 color_filter: compose_color_filters(*color_filter, layer.color_filter),
585 sampling: *sampling,
586 clip,
587 src_rect: *src_rect,
588 blend_mode: blend_mode.unwrap_or(BlendMode::SrcOver),
589 motion_context_animated,
590 });
591 }
592 DrawPrimitive::Text(text) => {
593 if let Some(params) = text_draw_params((**text).clone(), layer_bounds, layer, clip) {
594 sink.push_text(params);
595 }
596 }
597 DrawPrimitive::Shadow(shadow_primitive) => {
598 sink.push_shadow(shadow_primitive, layer_bounds, layer, clip);
599 }
600 }
601}
602
603fn text_draw_params(
604 text: TextPrimitive,
605 layer_bounds: Rect,
606 layer: &GraphicsLayer,
607 clip: Option<Rect>,
608) -> Option<TextDrawParams> {
609 if text.text.is_empty() {
610 return None;
611 }
612 let draw_rect = text.rect.translate(layer_bounds.x, layer_bounds.y);
613 let rect = apply_layer_to_rect(draw_rect, layer_bounds, layer);
614 if !(rect.width > 0.0 && rect.height > 0.0) {
615 return None;
616 }
617 let scale = layer_uniform_scale(layer);
618 if !scale.is_finite() || scale <= 0.0 {
619 return None;
620 }
621 let color = apply_layer_to_color(text.color, layer);
622 if color.3 <= 0.0 {
623 return None;
624 }
625
626 Some(TextDrawParams {
627 rect,
628 text: cranpose_ui::text::shared_plain_annotated_string(text.text.as_ref()),
629 render_text: cranpose_ui::text::shared_plain_render_string(text.text.as_ref()),
630 color,
631 text_style: text_style_for_draw_style(&text.style),
632 font_size: text.style.resolved_font_size(),
633 scale,
634 layout_options: TextLayoutOptions {
635 soft_wrap: false,
636 overflow: TextOverflow::Visible,
637 ..TextLayoutOptions::default()
638 },
639 clip,
640 })
641}
642
643#[cfg(test)]
644#[path = "tests/primitive_emit_tests.rs"]
645mod tests;