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