1use cranpose_core::{run_in_mutable_snapshot, 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 struct DrawShape {
13 pub rect: Rect,
14 pub brush: Brush,
15 pub shape: Option<RoundedCornerShape>,
16 pub z_index: usize,
17 pub clip: Option<Rect>,
18}
19
20#[derive(Clone)]
21pub struct TextDraw {
22 pub node_id: NodeId,
23 pub rect: Rect,
24 pub text: Rc<str>,
25 pub color: Color,
26 pub scale: f32,
27 pub z_index: usize,
28 pub clip: Option<Rect>,
29}
30
31#[derive(Clone)]
32pub enum ClickAction {
33 Simple(Rc<RefCell<dyn FnMut()>>),
34 WithPoint(Rc<dyn Fn(Point)>),
35}
36
37impl ClickAction {
38 pub(crate) fn invoke(&self, rect: Rect, x: f32, y: f32) {
39 match self {
40 ClickAction::Simple(handler) => (handler.borrow_mut())(),
41 ClickAction::WithPoint(handler) => handler(Point {
42 x: x - rect.x,
43 y: y - rect.y,
44 }),
45 }
46 }
47}
48
49#[derive(Clone)]
50pub struct HitRegion {
51 pub node_id: NodeId,
52 pub rect: Rect,
53 pub shape: Option<RoundedCornerShape>,
54 pub click_actions: Vec<ClickAction>,
55 pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
56 pub z_index: usize,
57 pub hit_clip: Option<Rect>,
58}
59
60impl HitTestTarget for HitRegion {
61 fn dispatch(&self, event: PointerEvent) {
62 let x = event.global_position.x;
63 let y = event.global_position.y;
64 let kind = event.kind;
65
66 let local = Point {
67 x: x - self.rect.x,
68 y: y - self.rect.y,
69 };
70
71 let local_event = event.copy_with_local_position(local);
72
73 let has_pointer_inputs = !self.pointer_inputs.is_empty();
74 let has_click_actions = kind == PointerEventKind::Down && !self.click_actions.is_empty();
75
76 if !has_pointer_inputs && !has_click_actions {
77 return;
78 }
79
80 if let Err(err) = run_in_mutable_snapshot(|| {
81 for handler in self.pointer_inputs.iter() {
82 if local_event.is_consumed() {
84 break;
85 }
86 handler(local_event.clone());
87 }
88
89 if kind == PointerEventKind::Down && !local_event.is_consumed() {
91 for action in &self.click_actions {
92 action.invoke(self.rect, x, y);
93 }
94 }
95 }) {
96 log::error!(
97 "failed to apply mutable snapshot for pointer event {:?} at ({}, {}): {}",
98 kind,
99 x,
100 y,
101 err
102 );
103 }
104 }
105
106 fn node_id(&self) -> NodeId {
107 self.node_id
108 }
109}
110
111impl HitRegion {
112 pub fn contains(&self, x: f32, y: f32) -> bool {
113 if let Some(clip) = self.hit_clip {
114 if !clip.contains(x, y) {
115 return false;
116 }
117 }
118 if let Some(shape) = self.shape {
119 point_in_rounded_rect(x, y, self.rect, shape)
120 } else {
121 self.rect.contains(x, y)
122 }
123 }
124}
125
126pub struct Scene {
127 pub shapes: Vec<DrawShape>,
128 pub texts: Vec<TextDraw>,
129 pub hits: Vec<HitRegion>,
130 node_index: HashMap<NodeId, HitRegion>,
132 next_z: usize,
133}
134
135impl Scene {
136 pub fn new() -> Self {
137 Self {
138 shapes: Vec::new(),
139 texts: Vec::new(),
140 hits: Vec::new(),
141 node_index: HashMap::new(),
142 next_z: 0,
143 }
144 }
145
146 pub fn push_shape(
147 &mut self,
148 rect: Rect,
149 brush: Brush,
150 shape: Option<RoundedCornerShape>,
151 clip: Option<Rect>,
152 ) {
153 let z_index = self.next_z;
154 self.next_z += 1;
155 self.shapes.push(DrawShape {
156 rect,
157 brush,
158 shape,
159 z_index,
160 clip,
161 });
162 }
163
164 pub fn push_text(
165 &mut self,
166 node_id: NodeId,
167 rect: Rect,
168 text: Rc<str>,
169 color: Color,
170 scale: f32,
171 clip: Option<Rect>,
172 ) {
173 let z_index = self.next_z;
174 self.next_z += 1;
175 self.texts.push(TextDraw {
176 node_id,
177 rect,
178 text,
179 color,
180 scale,
181 z_index,
182 clip,
183 });
184 }
185
186 pub fn push_hit(
187 &mut self,
188 node_id: NodeId,
189 rect: Rect,
190 shape: Option<RoundedCornerShape>,
191 click_actions: Vec<ClickAction>,
192 pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
193 hit_clip: Option<Rect>,
194 ) {
195 if click_actions.is_empty() && pointer_inputs.is_empty() {
196 return;
197 }
198 let z_index = self.next_z;
199 self.next_z += 1;
200 let hit_region = HitRegion {
201 node_id,
202 rect,
203 shape,
204 click_actions,
205 pointer_inputs,
206 z_index,
207 hit_clip,
208 };
209 self.node_index.insert(node_id, hit_region.clone());
211 self.hits.push(hit_region);
212 }
213}
214
215impl Default for Scene {
216 fn default() -> Self {
217 Self::new()
218 }
219}
220
221impl RenderScene for Scene {
222 type HitTarget = HitRegion;
223
224 fn clear(&mut self) {
225 self.shapes.clear();
226 self.texts.clear();
227 self.hits.clear();
228 self.node_index.clear();
229 self.next_z = 0;
230 }
231
232 fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
233 let mut hits: Vec<_> = self
234 .hits
235 .iter()
236 .filter(|hit| hit.contains(x, y))
237 .cloned()
238 .collect();
239
240 hits.sort_by(|a, b| b.z_index.cmp(&a.z_index));
242 hits
243 }
244
245 fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
246 self.node_index.get(&node_id).cloned()
248 }
249}
250
251fn point_in_rounded_rect(x: f32, y: f32, rect: Rect, shape: RoundedCornerShape) -> bool {
253 if !rect.contains(x, y) {
254 return false;
255 }
256
257 let local_x = x - rect.x;
258 let local_y = y - rect.y;
259
260 let radii = shape.resolve(rect.width, rect.height);
262 let tl = radii.top_left;
263 let tr = radii.top_right;
264 let bl = radii.bottom_left;
265 let br = radii.bottom_right;
266
267 if local_x < tl && local_y < tl {
269 let dx = tl - local_x;
270 let dy = tl - local_y;
271 return dx * dx + dy * dy <= tl * tl;
272 }
273
274 if local_x > rect.width - tr && local_y < tr {
276 let dx = local_x - (rect.width - tr);
277 let dy = tr - local_y;
278 return dx * dx + dy * dy <= tr * tr;
279 }
280
281 if local_x < bl && local_y > rect.height - bl {
283 let dx = bl - local_x;
284 let dy = local_y - (rect.height - bl);
285 return dx * dx + dy * dy <= bl * bl;
286 }
287
288 if local_x > rect.width - br && local_y > rect.height - br {
290 let dx = local_x - (rect.width - br);
291 let dy = local_y - (rect.height - br);
292 return dx * dx + dy * dy <= br * br;
293 }
294
295 true
296}