1use cranpose_core::NodeId;
4use cranpose_foundation::{PointerEvent, PointerEventKind};
5use cranpose_render_common::{HitTestTarget, RenderScene};
6use cranpose_ui_graphics::{
7 BlendMode, Brush, Color, ColorFilter, ImageBitmap, Point, Rect, RenderEffect,
8 RoundedCornerShape,
9};
10use std::cell::RefCell;
11use std::collections::HashMap;
12use std::rc::Rc;
13
14#[derive(Clone)]
15pub enum ClickAction {
16 Simple(Rc<RefCell<dyn FnMut()>>),
17 WithPoint(Rc<dyn Fn(Point)>),
18}
19
20impl ClickAction {
21 fn invoke(&self, rect: Rect, x: f32, y: f32) {
22 match self {
23 ClickAction::Simple(handler) => (handler.borrow_mut())(),
24 ClickAction::WithPoint(handler) => handler(Point {
25 x: x - rect.x,
26 y: y - rect.y,
27 }),
28 }
29 }
30}
31
32#[derive(Clone)]
33pub struct DrawShape {
34 pub rect: Rect,
35 pub local_rect: Rect,
36 pub quad: [[f32; 2]; 4],
37 pub brush: Brush,
38 pub shape: Option<RoundedCornerShape>,
39 pub z_index: usize,
40 pub clip: Option<Rect>,
41 pub blend_mode: BlendMode,
42}
43
44#[derive(Clone)]
45pub struct TextDraw {
46 pub node_id: NodeId,
47 pub rect: Rect,
48 pub text: Rc<str>,
49 pub color: Color,
50 pub font_size: f32,
51 pub scale: f32,
52 pub z_index: usize,
53 pub clip: Option<Rect>,
54}
55
56#[derive(Clone)]
57pub struct ImageDraw {
58 pub rect: Rect,
59 pub local_rect: Rect,
60 pub quad: [[f32; 2]; 4],
61 pub image: ImageBitmap,
62 pub alpha: f32,
63 pub color_filter: Option<ColorFilter>,
64 pub z_index: usize,
65 pub clip: Option<Rect>,
66 pub blend_mode: BlendMode,
67 pub src_rect: Option<Rect>,
69}
70
71#[derive(Clone)]
72pub struct HitRegion {
73 pub node_id: NodeId,
74 pub rect: Rect,
75 pub shape: Option<RoundedCornerShape>,
76 pub click_actions: Vec<ClickAction>,
77 pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
78 pub z_index: usize,
79 pub hit_clip: Option<Rect>,
80}
81
82impl HitRegion {
83 fn contains(&self, x: f32, y: f32) -> bool {
84 if let Some(clip) = self.hit_clip {
85 if !clip.contains(x, y) {
86 return false;
87 }
88 }
89 if let Some(shape) = self.shape {
91 point_in_rounded_rect(x, y, self.rect, shape)
92 } else {
93 self.rect.contains(x, y)
94 }
95 }
96}
97
98impl HitTestTarget for HitRegion {
99 fn node_id(&self) -> NodeId {
100 self.node_id
101 }
102
103 fn dispatch(&self, event: PointerEvent) {
104 if event.is_consumed() {
105 return;
106 }
107 let x = event.global_position.x;
108 let y = event.global_position.y;
109 let kind = event.kind;
110 let local_position = Point {
111 x: x - self.rect.x,
112 y: y - self.rect.y,
113 };
114 let local_event = event.copy_with_local_position(local_position);
115 for handler in &self.pointer_inputs {
116 if local_event.is_consumed() {
117 break;
118 }
119 handler(local_event.clone());
120 }
121 if kind == PointerEventKind::Down && !local_event.is_consumed() {
122 for action in &self.click_actions {
123 action.invoke(self.rect, x, y);
124 }
125 }
126 }
127}
128
129#[derive(Clone)]
131pub struct ShadowDraw {
132 pub shapes: Vec<(DrawShape, BlendMode)>,
135 pub blur_radius: f32,
137 pub clip: Option<Rect>,
139 pub z_index: usize,
141}
142
143#[derive(Clone)]
145pub struct EffectLayer {
146 pub rect: Rect,
147 pub clip: Option<Rect>,
148 pub effect: Option<RenderEffect>,
151 pub blend_mode: BlendMode,
153 pub composite_alpha: f32,
155 pub z_start: usize,
157 pub z_end: usize,
159}
160
161#[derive(Clone)]
163pub struct BackdropLayer {
164 pub rect: Rect,
165 pub clip: Option<Rect>,
166 pub effect: RenderEffect,
167 pub z_index: usize,
169}
170
171pub struct Scene {
172 pub shapes: Vec<DrawShape>,
173 pub images: Vec<ImageDraw>,
174 pub texts: Vec<TextDraw>,
175 pub shadow_draws: Vec<ShadowDraw>,
176 pub hits: Vec<HitRegion>,
177 pub effect_layers: Vec<EffectLayer>,
178 pub backdrop_layers: Vec<BackdropLayer>,
179 pub next_z: usize,
180 pub node_index: HashMap<NodeId, HitRegion>,
181}
182
183impl Scene {
184 pub fn new() -> Self {
185 Self {
186 shapes: Vec::new(),
187 images: Vec::new(),
188 texts: Vec::new(),
189 shadow_draws: Vec::new(),
190 hits: Vec::new(),
191 effect_layers: Vec::new(),
192 backdrop_layers: Vec::new(),
193 next_z: 0,
194 node_index: HashMap::new(),
195 }
196 }
197
198 pub fn push_shape(
199 &mut self,
200 rect: Rect,
201 brush: Brush,
202 shape: Option<RoundedCornerShape>,
203 clip: Option<Rect>,
204 blend_mode: BlendMode,
205 ) {
206 self.push_shape_with_geometry(
207 rect,
208 rect,
209 rect_to_quad(rect),
210 brush,
211 shape,
212 clip,
213 blend_mode,
214 );
215 }
216
217 #[allow(clippy::too_many_arguments)]
218 pub fn push_shape_with_geometry(
219 &mut self,
220 rect: Rect,
221 local_rect: Rect,
222 quad: [[f32; 2]; 4],
223 brush: Brush,
224 shape: Option<RoundedCornerShape>,
225 clip: Option<Rect>,
226 blend_mode: BlendMode,
227 ) {
228 let z_index = self.next_z;
229 self.next_z += 1;
230 self.shapes.push(DrawShape {
231 rect,
232 local_rect,
233 quad,
234 brush,
235 shape,
236 z_index,
237 clip,
238 blend_mode,
239 });
240 }
241
242 #[allow(clippy::too_many_arguments)]
243 pub fn push_image(
244 &mut self,
245 rect: Rect,
246 image: ImageBitmap,
247 alpha: f32,
248 color_filter: Option<ColorFilter>,
249 clip: Option<Rect>,
250 src_rect: Option<Rect>,
251 blend_mode: BlendMode,
252 ) {
253 self.push_image_with_geometry(
254 rect,
255 rect,
256 rect_to_quad(rect),
257 image,
258 alpha,
259 color_filter,
260 clip,
261 src_rect,
262 blend_mode,
263 );
264 }
265
266 #[allow(clippy::too_many_arguments)]
267 pub fn push_image_with_geometry(
268 &mut self,
269 rect: Rect,
270 local_rect: Rect,
271 quad: [[f32; 2]; 4],
272 image: ImageBitmap,
273 alpha: f32,
274 color_filter: Option<ColorFilter>,
275 clip: Option<Rect>,
276 src_rect: Option<Rect>,
277 blend_mode: BlendMode,
278 ) {
279 let z_index = self.next_z;
280 self.next_z += 1;
281 self.images.push(ImageDraw {
282 rect,
283 local_rect,
284 quad,
285 image,
286 alpha: alpha.clamp(0.0, 1.0),
287 color_filter,
288 z_index,
289 clip,
290 blend_mode,
291 src_rect,
292 });
293 }
294
295 #[allow(clippy::too_many_arguments)]
296 pub fn push_text(
297 &mut self,
298 node_id: NodeId,
299 rect: Rect,
300 text: Rc<str>,
301 color: Color,
302 font_size: f32,
303 scale: f32,
304 clip: Option<Rect>,
305 ) {
306 let z_index = self.next_z;
307 self.next_z += 1;
308 self.texts.push(TextDraw {
309 node_id,
310 rect,
311 text,
312 color,
313 font_size,
314 scale,
315 z_index,
316 clip,
317 });
318 }
319
320 pub fn push_hit(
321 &mut self,
322 node_id: NodeId,
323 rect: Rect,
324 shape: Option<RoundedCornerShape>,
325 click_actions: Vec<ClickAction>,
326 pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
327 hit_clip: Option<Rect>,
328 ) {
329 if click_actions.is_empty() && pointer_inputs.is_empty() {
330 return;
331 }
332 let z_index = self.next_z;
333 self.next_z += 1;
334 let hit_region = HitRegion {
335 node_id,
336 rect,
337 shape,
338 click_actions,
339 pointer_inputs,
340 z_index,
341 hit_clip,
342 };
343 self.node_index.insert(node_id, hit_region.clone());
345 self.hits.push(hit_region);
346 }
347
348 pub fn push_shadow_draw(&mut self, mut draw: ShadowDraw) {
349 let z_index = self.next_z;
350 self.next_z += 1;
351 draw.z_index = z_index;
352 self.shadow_draws.push(draw);
353 }
354}
355
356impl Default for Scene {
357 fn default() -> Self {
358 Self::new()
359 }
360}
361
362fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
363 [
364 [rect.x, rect.y],
365 [rect.x + rect.width, rect.y],
366 [rect.x, rect.y + rect.height],
367 [rect.x + rect.width, rect.y + rect.height],
368 ]
369}
370
371impl RenderScene for Scene {
372 type HitTarget = HitRegion;
373
374 fn clear(&mut self) {
375 self.shapes.clear();
376 self.images.clear();
377 self.texts.clear();
378 self.shadow_draws.clear();
379 self.hits.clear();
380 self.effect_layers.clear();
381 self.backdrop_layers.clear();
382 self.node_index.clear();
383 self.next_z = 0;
384 }
385
386 fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
387 let mut hits = self.hits.clone();
388 hits.retain(|hit| hit.contains(x, y));
389
390 hits.sort_by(|a, b| b.z_index.cmp(&a.z_index));
392 hits
393 }
394
395 fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
396 self.node_index.get(&node_id).cloned()
398 }
399}
400
401fn point_in_rounded_rect(x: f32, y: f32, rect: Rect, shape: RoundedCornerShape) -> bool {
403 if !rect.contains(x, y) {
404 return false;
405 }
406
407 let local_x = x - rect.x;
408 let local_y = y - rect.y;
409
410 let radii = shape.resolve(rect.width, rect.height);
412 let tl = radii.top_left;
413 let tr = radii.top_right;
414 let bl = radii.bottom_left;
415 let br = radii.bottom_right;
416
417 if local_x < tl && local_y < tl {
419 let dx = tl - local_x;
420 let dy = tl - local_y;
421 return dx * dx + dy * dy <= tl * tl;
422 }
423
424 if local_x > rect.width - tr && local_y < tr {
426 let dx = local_x - (rect.width - tr);
427 let dy = tr - local_y;
428 return dx * dx + dy * dy <= tr * tr;
429 }
430
431 if local_x < bl && local_y > rect.height - bl {
433 let dx = bl - local_x;
434 let dy = local_y - (rect.height - bl);
435 return dx * dx + dy * dy <= bl * bl;
436 }
437
438 if local_x > rect.width - br && local_y > rect.height - br {
440 let dx = local_x - (rect.width - br);
441 let dy = local_y - (rect.height - br);
442 return dx * dx + dy * dy <= br * br;
443 }
444
445 true
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451 use std::cell::{Cell, RefCell};
452 use std::rc::Rc;
453
454 fn make_handler(counter: Rc<Cell<u32>>, consume: bool) -> Rc<dyn Fn(PointerEvent)> {
455 Rc::new(move |event: PointerEvent| {
456 counter.set(counter.get() + 1);
457 if consume {
458 event.consume();
459 }
460 })
461 }
462
463 #[test]
464 fn hit_test_respects_hit_clip() {
465 let mut scene = Scene::new();
466 let rect = Rect {
467 x: 0.0,
468 y: 0.0,
469 width: 100.0,
470 height: 100.0,
471 };
472 let clip = Rect {
473 x: 0.0,
474 y: 0.0,
475 width: 40.0,
476 height: 40.0,
477 };
478 scene.push_hit(
479 1,
480 rect,
481 None,
482 Vec::new(),
483 vec![Rc::new(|_event: PointerEvent| {})],
484 Some(clip),
485 );
486
487 assert!(scene.hit_test(60.0, 20.0).is_empty());
488 assert_eq!(scene.hit_test(20.0, 20.0).len(), 1);
489 }
490
491 #[test]
492 fn dispatch_stops_after_event_consumed() {
493 let count_first = Rc::new(Cell::new(0));
494 let count_second = Rc::new(Cell::new(0));
495
496 let hit = HitRegion {
497 node_id: 1,
498 rect: Rect {
499 x: 0.0,
500 y: 0.0,
501 width: 50.0,
502 height: 50.0,
503 },
504 shape: None,
505 click_actions: Vec::new(),
506 pointer_inputs: vec![
507 make_handler(count_first.clone(), true),
508 make_handler(count_second.clone(), false),
509 ],
510 z_index: 0,
511 hit_clip: None,
512 };
513
514 let event = PointerEvent::new(
515 PointerEventKind::Down,
516 Point { x: 10.0, y: 10.0 },
517 Point { x: 10.0, y: 10.0 },
518 );
519 hit.dispatch(event);
520
521 assert_eq!(count_first.get(), 1);
522 assert_eq!(count_second.get(), 0);
523 }
524
525 #[test]
526 fn dispatch_triggers_click_action_on_down() {
527 let click_count = Rc::new(Cell::new(0));
528 let click_count_for_handler = Rc::clone(&click_count);
529 let click_action = ClickAction::Simple(Rc::new(RefCell::new(move || {
530 click_count_for_handler.set(click_count_for_handler.get() + 1);
531 })));
532
533 let hit = HitRegion {
534 node_id: 1,
535 rect: Rect {
536 x: 0.0,
537 y: 0.0,
538 width: 50.0,
539 height: 50.0,
540 },
541 shape: None,
542 click_actions: vec![click_action],
543 pointer_inputs: Vec::new(),
544 z_index: 0,
545 hit_clip: None,
546 };
547
548 hit.dispatch(PointerEvent::new(
549 PointerEventKind::Down,
550 Point { x: 10.0, y: 10.0 },
551 Point { x: 10.0, y: 10.0 },
552 ));
553 hit.dispatch(PointerEvent::new(
554 PointerEventKind::Move,
555 Point { x: 10.0, y: 10.0 },
556 Point { x: 12.0, y: 12.0 },
557 ));
558
559 assert_eq!(click_count.get(), 1);
560 }
561
562 #[test]
563 fn dispatch_does_not_trigger_click_action_when_consumed() {
564 let click_count = Rc::new(Cell::new(0));
565 let click_count_for_handler = Rc::clone(&click_count);
566 let click_action = ClickAction::Simple(Rc::new(RefCell::new(move || {
567 click_count_for_handler.set(click_count_for_handler.get() + 1);
568 })));
569
570 let hit = HitRegion {
571 node_id: 1,
572 rect: Rect {
573 x: 0.0,
574 y: 0.0,
575 width: 50.0,
576 height: 50.0,
577 },
578 shape: None,
579 click_actions: vec![click_action],
580 pointer_inputs: vec![Rc::new(|event: PointerEvent| event.consume())],
581 z_index: 0,
582 hit_clip: None,
583 };
584
585 hit.dispatch(PointerEvent::new(
586 PointerEventKind::Down,
587 Point { x: 10.0, y: 10.0 },
588 Point { x: 10.0, y: 10.0 },
589 ));
590
591 assert_eq!(click_count.get(), 0);
592 }
593}