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