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