1use cranpose_core::NodeId;
4use cranpose_foundation::{PointerEvent, PointerEventKind};
5use cranpose_render_common::{HitTestTarget, RenderScene};
6use cranpose_ui_graphics::{Brush, Color, Point, Rect, RoundedCornerShape};
7use std::cell::RefCell;
8use std::collections::HashMap;
9use std::rc::Rc;
10
11#[derive(Clone)]
12pub enum ClickAction {
13 Simple(Rc<RefCell<dyn FnMut()>>),
14 WithPoint(Rc<dyn Fn(Point)>),
15}
16
17impl ClickAction {
18 fn invoke(&self, rect: Rect, x: f32, y: f32) {
19 match self {
20 ClickAction::Simple(handler) => (handler.borrow_mut())(),
21 ClickAction::WithPoint(handler) => handler(Point {
22 x: x - rect.x,
23 y: y - rect.y,
24 }),
25 }
26 }
27}
28
29#[derive(Clone)]
30pub struct DrawShape {
31 pub rect: Rect,
32 pub brush: Brush,
33 pub shape: Option<RoundedCornerShape>,
34 pub z_index: usize,
35 pub clip: Option<Rect>,
36}
37
38#[derive(Clone)]
39pub struct TextDraw {
40 pub node_id: NodeId,
41 pub rect: Rect,
42 pub text: Rc<str>,
43 pub color: Color,
44 pub font_size: f32,
45 pub scale: f32,
46 pub z_index: usize,
47 pub clip: Option<Rect>,
48}
49
50#[derive(Clone)]
51pub struct HitRegion {
52 pub node_id: NodeId,
53 pub rect: Rect,
54 pub shape: Option<RoundedCornerShape>,
55 pub click_actions: Vec<ClickAction>,
56 pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
57 pub z_index: usize,
58 pub hit_clip: Option<Rect>,
59}
60
61impl HitRegion {
62 fn contains(&self, x: f32, y: f32) -> bool {
63 if let Some(clip) = self.hit_clip {
64 if !clip.contains(x, y) {
65 return false;
66 }
67 }
68 if let Some(shape) = self.shape {
70 point_in_rounded_rect(x, y, self.rect, shape)
71 } else {
72 self.rect.contains(x, y)
73 }
74 }
75}
76
77impl HitTestTarget for HitRegion {
78 fn node_id(&self) -> NodeId {
79 self.node_id
80 }
81
82 fn dispatch(&self, event: PointerEvent) {
83 if event.is_consumed() {
84 return;
85 }
86 let x = event.global_position.x;
87 let y = event.global_position.y;
88 let kind = event.kind;
89 let local_position = Point {
90 x: x - self.rect.x,
91 y: y - self.rect.y,
92 };
93 let local_event = event.copy_with_local_position(local_position);
94 for handler in &self.pointer_inputs {
95 if local_event.is_consumed() {
96 break;
97 }
98 handler(local_event.clone());
99 }
100 if kind == PointerEventKind::Down && !local_event.is_consumed() {
101 for action in &self.click_actions {
102 action.invoke(self.rect, x, y);
103 }
104 }
105 }
106}
107
108pub struct Scene {
109 pub shapes: Vec<DrawShape>,
110 pub texts: Vec<TextDraw>,
111 pub hits: Vec<HitRegion>,
112 pub next_z: usize,
113 pub node_index: HashMap<NodeId, HitRegion>,
114}
115
116impl Scene {
117 pub fn new() -> Self {
118 Self {
119 shapes: Vec::new(),
120 texts: Vec::new(),
121 hits: Vec::new(),
122 next_z: 0,
123 node_index: HashMap::new(),
124 }
125 }
126
127 pub fn push_shape(
128 &mut self,
129 rect: Rect,
130 brush: Brush,
131 shape: Option<RoundedCornerShape>,
132 clip: Option<Rect>,
133 ) {
134 let z_index = self.next_z;
135 self.next_z += 1;
136 self.shapes.push(DrawShape {
137 rect,
138 brush,
139 shape,
140 z_index,
141 clip,
142 });
143 }
144
145 #[allow(clippy::too_many_arguments)]
146 pub fn push_text(
147 &mut self,
148 node_id: NodeId,
149 rect: Rect,
150 text: Rc<str>,
151 color: Color,
152 font_size: f32,
153 scale: f32,
154 clip: Option<Rect>,
155 ) {
156 let z_index = self.next_z;
157 self.next_z += 1;
158 self.texts.push(TextDraw {
159 node_id,
160 rect,
161 text,
162 color,
163 font_size,
164 scale,
165 z_index,
166 clip,
167 });
168 }
169
170 pub fn push_hit(
171 &mut self,
172 node_id: NodeId,
173 rect: Rect,
174 shape: Option<RoundedCornerShape>,
175 click_actions: Vec<ClickAction>,
176 pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
177 hit_clip: Option<Rect>,
178 ) {
179 if click_actions.is_empty() && pointer_inputs.is_empty() {
180 return;
181 }
182 let z_index = self.next_z;
183 self.next_z += 1;
184 let hit_region = HitRegion {
185 node_id,
186 rect,
187 shape,
188 click_actions,
189 pointer_inputs,
190 z_index,
191 hit_clip,
192 };
193 self.node_index.insert(node_id, hit_region.clone());
195 self.hits.push(hit_region);
196 }
197}
198
199impl Default for Scene {
200 fn default() -> Self {
201 Self::new()
202 }
203}
204
205impl RenderScene for Scene {
206 type HitTarget = HitRegion;
207
208 fn clear(&mut self) {
209 self.shapes.clear();
210 self.texts.clear();
211 self.hits.clear();
212 self.node_index.clear();
213 self.next_z = 0;
214 }
215
216 fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
217 let mut hits = self.hits.clone();
218 hits.retain(|hit| hit.contains(x, y));
219
220 hits.sort_by(|a, b| b.z_index.cmp(&a.z_index));
222 hits
223 }
224
225 fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
226 self.node_index.get(&node_id).cloned()
228 }
229}
230
231fn point_in_rounded_rect(x: f32, y: f32, rect: Rect, shape: RoundedCornerShape) -> bool {
233 if !rect.contains(x, y) {
234 return false;
235 }
236
237 let local_x = x - rect.x;
238 let local_y = y - rect.y;
239
240 let radii = shape.resolve(rect.width, rect.height);
242 let tl = radii.top_left;
243 let tr = radii.top_right;
244 let bl = radii.bottom_left;
245 let br = radii.bottom_right;
246
247 if local_x < tl && local_y < tl {
249 let dx = tl - local_x;
250 let dy = tl - local_y;
251 return dx * dx + dy * dy <= tl * tl;
252 }
253
254 if local_x > rect.width - tr && local_y < tr {
256 let dx = local_x - (rect.width - tr);
257 let dy = tr - local_y;
258 return dx * dx + dy * dy <= tr * tr;
259 }
260
261 if local_x < bl && local_y > rect.height - bl {
263 let dx = bl - local_x;
264 let dy = local_y - (rect.height - bl);
265 return dx * dx + dy * dy <= bl * bl;
266 }
267
268 if local_x > rect.width - br && local_y > rect.height - br {
270 let dx = local_x - (rect.width - br);
271 let dy = local_y - (rect.height - br);
272 return dx * dx + dy * dy <= br * br;
273 }
274
275 true
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use std::cell::{Cell, RefCell};
282 use std::rc::Rc;
283
284 fn make_handler(counter: Rc<Cell<u32>>, consume: bool) -> Rc<dyn Fn(PointerEvent)> {
285 Rc::new(move |event: PointerEvent| {
286 counter.set(counter.get() + 1);
287 if consume {
288 event.consume();
289 }
290 })
291 }
292
293 #[test]
294 fn hit_test_respects_hit_clip() {
295 let mut scene = Scene::new();
296 let rect = Rect {
297 x: 0.0,
298 y: 0.0,
299 width: 100.0,
300 height: 100.0,
301 };
302 let clip = Rect {
303 x: 0.0,
304 y: 0.0,
305 width: 40.0,
306 height: 40.0,
307 };
308 scene.push_hit(
309 1,
310 rect,
311 None,
312 Vec::new(),
313 vec![Rc::new(|_event: PointerEvent| {})],
314 Some(clip),
315 );
316
317 assert!(scene.hit_test(60.0, 20.0).is_empty());
318 assert_eq!(scene.hit_test(20.0, 20.0).len(), 1);
319 }
320
321 #[test]
322 fn dispatch_stops_after_event_consumed() {
323 let count_first = Rc::new(Cell::new(0));
324 let count_second = Rc::new(Cell::new(0));
325
326 let hit = HitRegion {
327 node_id: 1,
328 rect: Rect {
329 x: 0.0,
330 y: 0.0,
331 width: 50.0,
332 height: 50.0,
333 },
334 shape: None,
335 click_actions: Vec::new(),
336 pointer_inputs: vec![
337 make_handler(count_first.clone(), true),
338 make_handler(count_second.clone(), false),
339 ],
340 z_index: 0,
341 hit_clip: None,
342 };
343
344 let event = PointerEvent::new(
345 PointerEventKind::Down,
346 Point { x: 10.0, y: 10.0 },
347 Point { x: 10.0, y: 10.0 },
348 );
349 hit.dispatch(event);
350
351 assert_eq!(count_first.get(), 1);
352 assert_eq!(count_second.get(), 0);
353 }
354
355 #[test]
356 fn dispatch_triggers_click_action_on_down() {
357 let click_count = Rc::new(Cell::new(0));
358 let click_count_for_handler = Rc::clone(&click_count);
359 let click_action = ClickAction::Simple(Rc::new(RefCell::new(move || {
360 click_count_for_handler.set(click_count_for_handler.get() + 1);
361 })));
362
363 let hit = HitRegion {
364 node_id: 1,
365 rect: Rect {
366 x: 0.0,
367 y: 0.0,
368 width: 50.0,
369 height: 50.0,
370 },
371 shape: None,
372 click_actions: vec![click_action],
373 pointer_inputs: Vec::new(),
374 z_index: 0,
375 hit_clip: None,
376 };
377
378 hit.dispatch(PointerEvent::new(
379 PointerEventKind::Down,
380 Point { x: 10.0, y: 10.0 },
381 Point { x: 10.0, y: 10.0 },
382 ));
383 hit.dispatch(PointerEvent::new(
384 PointerEventKind::Move,
385 Point { x: 10.0, y: 10.0 },
386 Point { x: 12.0, y: 12.0 },
387 ));
388
389 assert_eq!(click_count.get(), 1);
390 }
391
392 #[test]
393 fn dispatch_does_not_trigger_click_action_when_consumed() {
394 let click_count = Rc::new(Cell::new(0));
395 let click_count_for_handler = Rc::clone(&click_count);
396 let click_action = ClickAction::Simple(Rc::new(RefCell::new(move || {
397 click_count_for_handler.set(click_count_for_handler.get() + 1);
398 })));
399
400 let hit = HitRegion {
401 node_id: 1,
402 rect: Rect {
403 x: 0.0,
404 y: 0.0,
405 width: 50.0,
406 height: 50.0,
407 },
408 shape: None,
409 click_actions: vec![click_action],
410 pointer_inputs: vec![Rc::new(|event: PointerEvent| event.consume())],
411 z_index: 0,
412 hit_clip: None,
413 };
414
415 hit.dispatch(PointerEvent::new(
416 PointerEventKind::Down,
417 Point { x: 10.0, y: 10.0 },
418 Point { x: 10.0, y: 10.0 },
419 ));
420
421 assert_eq!(click_count.get(), 0);
422 }
423}