1use std::{
2 cell::{Cell, RefCell},
3 cmp::Reverse,
4 collections::HashMap,
5 rc::Rc,
6};
7
8use cranpose_core::{MemoryApplier, NodeId, collections::map::HashSet};
9use cranpose_foundation::{MINIMUM_TOUCH_TARGET_SIZE, PointerEvent, PointerEventKind};
10use cranpose_ui::{LayoutNode, ModifierNodeSlices, SubcomposeLayoutNode};
11use cranpose_ui_graphics::{Point, PointerIcon, Rect, RoundedCornerShape};
12
13use crate::{
14 HitTestTarget, RenderScene,
15 graph::{ProjectiveTransform, RenderGraph},
16};
17
18pub struct RenderDiagnostics {
19 reported_warnings: RefCell<HashSet<&'static str>>,
20 live_modifier_slice_lookup_miss_count: Cell<usize>,
21}
22
23impl RenderDiagnostics {
24 pub fn new() -> Self {
25 Self {
26 reported_warnings: RefCell::new(HashSet::default()),
27 live_modifier_slice_lookup_miss_count: Cell::new(0),
28 }
29 }
30
31 pub fn claim_warning_once(&self, key: &'static str) -> bool {
32 self.reported_warnings.borrow_mut().insert(key)
33 }
34
35 pub fn record_live_modifier_slice_lookup_miss(&self) {
36 self.live_modifier_slice_lookup_miss_count.set(
37 self.live_modifier_slice_lookup_miss_count
38 .get()
39 .saturating_add(1),
40 );
41 }
42
43 pub fn live_modifier_slice_lookup_miss_count(&self) -> usize {
44 self.live_modifier_slice_lookup_miss_count.get()
45 }
46}
47
48impl Default for RenderDiagnostics {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq)]
55pub struct HitClip {
56 pub quad: [[f32; 2]; 4],
57 pub bounds: Rect,
58}
59
60#[derive(Clone, Copy)]
62pub struct HitGeometry<'a> {
63 pub rect: Rect,
64 pub quad: [[f32; 2]; 4],
65 pub local_bounds: Rect,
66 pub world_to_local: ProjectiveTransform,
67 pub hit_clip_bounds: Option<Rect>,
68 pub hit_clips: &'a [HitClip],
69}
70
71pub struct HitTargetSpec<'a> {
75 pub shape: Option<RoundedCornerShape>,
78 pub handlers: &'a Rc<ModifierNodeSlices>,
80}
81
82#[derive(Clone)]
83pub struct HitRegion {
84 pub node_id: NodeId,
85 pub capture_path: Vec<NodeId>,
86 pub rect: Rect,
87 pub quad: [[f32; 2]; 4],
88 pub local_bounds: Rect,
89 pub world_to_local: ProjectiveTransform,
90 pub shape: Option<RoundedCornerShape>,
91 pub handlers: Rc<ModifierNodeSlices>,
94 pub z_index: usize,
95 pub hit_clip_bounds: Option<Rect>,
96 pub hit_clips: Vec<HitClip>,
97 diagnostics: Rc<RenderDiagnostics>,
98}
99
100struct HitRegionInit<'a> {
101 node_id: NodeId,
102 capture_path: Vec<NodeId>,
103 geometry: HitGeometry<'a>,
104 clip_buffer: Vec<HitClip>,
105 shape: Option<RoundedCornerShape>,
106 handlers: Rc<ModifierNodeSlices>,
107 z_index: usize,
108 diagnostics: Rc<RenderDiagnostics>,
109}
110
111impl Default for HitRegionInit<'_> {
112 fn default() -> Self {
113 Self {
114 node_id: 0,
115 capture_path: Vec::new(),
116 geometry: HitGeometry {
117 rect: Rect {
118 x: 0.0,
119 y: 0.0,
120 width: 0.0,
121 height: 0.0,
122 },
123 quad: [[0.0, 0.0]; 4],
124 local_bounds: Rect {
125 x: 0.0,
126 y: 0.0,
127 width: 0.0,
128 height: 0.0,
129 },
130 world_to_local: ProjectiveTransform::identity(),
131 hit_clip_bounds: None,
132 hit_clips: &[],
133 },
134 clip_buffer: Vec::new(),
135 shape: None,
136 handlers: Rc::default(),
137 z_index: 0,
138 diagnostics: Rc::new(RenderDiagnostics::new()),
139 }
140 }
141}
142
143impl HitRegion {
144 fn with_diagnostics(init: HitRegionInit<'_>) -> Self {
145 let HitRegionInit {
146 node_id,
147 capture_path,
148 geometry,
149 clip_buffer: mut hit_clips,
150 shape,
151 handlers,
152 z_index,
153 diagnostics,
154 } = init;
155 let HitGeometry {
156 rect,
157 quad,
158 local_bounds,
159 world_to_local,
160 hit_clip_bounds,
161 hit_clips: clips,
162 } = geometry;
163 hit_clips.extend_from_slice(clips);
164 Self {
165 node_id,
166 capture_path,
167 rect,
168 quad,
169 local_bounds,
170 world_to_local,
171 shape,
172 handlers,
173 z_index,
174 hit_clip_bounds,
175 hit_clips,
176 diagnostics,
177 }
178 }
179
180 fn contains(&self, x: f32, y: f32) -> bool {
181 if !self.rect.contains(x, y) {
182 return false;
183 }
184
185 if let Some(clip_bounds) = self.hit_clip_bounds
186 && !clip_bounds.contains(x, y)
187 {
188 return false;
189 }
190
191 let point = Point { x, y };
192 if !point_in_quad(point, self.quad) {
193 return false;
194 }
195
196 for clip in &self.hit_clips {
197 if !point_in_quad(point, clip.quad) {
198 return false;
199 }
200 }
201
202 let local_point = self.world_to_local.map_point(point);
203 if let Some(shape) = self.shape {
204 point_in_rounded_rect(local_point, self.local_bounds, shape)
205 } else {
206 self.local_bounds.contains(local_point.x, local_point.y)
207 }
208 }
209
210 fn reach_distance(&self, x: f32, y: f32) -> Option<f32> {
215 if self.handlers.pointer_inputs().is_empty() {
216 return None;
217 }
218 if let Some(clip_bounds) = self.hit_clip_bounds
219 && !clip_bounds.contains(x, y)
220 {
221 return None;
222 }
223 let grow_x = ((MINIMUM_TOUCH_TARGET_SIZE - self.rect.width) / 2.0).max(0.0);
224 let grow_y = ((MINIMUM_TOUCH_TARGET_SIZE - self.rect.height) / 2.0).max(0.0);
225 if grow_x <= 0.0 && grow_y <= 0.0 {
226 return None;
227 }
228 let right = self.rect.x + self.rect.width;
229 let bottom = self.rect.y + self.rect.height;
230 let in_reach = x >= self.rect.x - grow_x
231 && x <= right + grow_x
232 && y >= self.rect.y - grow_y
233 && y <= bottom + grow_y;
234 if !in_reach {
235 return None;
236 }
237 let dx = (self.rect.x - x).max(x - right).max(0.0);
238 let dy = (self.rect.y - y).max(y - bottom).max(0.0);
239 Some(dx * dx + dy * dy)
240 }
241
242 fn localize_event(&self, event: &PointerEvent) -> PointerEvent {
243 let local = self.world_to_local.map_point(event.global_position);
244 event.copy_with_local_position(Point {
245 x: local.x - self.local_bounds.x,
246 y: local.y - self.local_bounds.y,
247 })
248 }
249
250 fn dispatch_modifier_slices(&self, modifier_slices: &ModifierNodeSlices, event: PointerEvent) {
251 if should_skip_consumed_event(&event) {
252 return;
253 }
254
255 modifier_slices.dispatch_pointer_event(self.localize_event(&event));
256 }
257
258 fn live_modifier_slices(&self, applier: &mut MemoryApplier) -> Option<Rc<ModifierNodeSlices>> {
259 if let Ok(modifier_slices) =
260 applier.with_node::<LayoutNode, _>(self.node_id, |node| node.modifier_slices_snapshot())
261 {
262 return Some(modifier_slices);
263 }
264
265 applier
266 .with_node::<SubcomposeLayoutNode, _>(self.node_id, |node| {
267 node.modifier_slices_snapshot()
268 })
269 .ok()
270 }
271}
272
273fn is_terminal_pointer_event(kind: PointerEventKind) -> bool {
274 matches!(kind, PointerEventKind::Up | PointerEventKind::Cancel)
275}
276
277fn should_skip_consumed_event(event: &PointerEvent) -> bool {
278 event.is_consumed() && !is_terminal_pointer_event(event.kind)
279}
280
281impl HitTestTarget for HitRegion {
282 fn node_id(&self) -> NodeId {
283 self.node_id
284 }
285
286 fn pointer_icon(&self) -> Option<PointerIcon> {
287 self.handlers.pointer_icon().cloned()
288 }
289
290 fn capture_path(&self) -> Vec<NodeId> {
291 self.capture_path.clone()
292 }
293
294 fn dispatch(&self, event: PointerEvent) {
295 self.dispatch_modifier_slices(&self.handlers, event);
296 }
297
298 fn dispatch_with_applier(&self, applier: &mut MemoryApplier, event: PointerEvent) {
299 if let Some(modifier_slices) = self.live_modifier_slices(applier) {
300 self.dispatch_modifier_slices(modifier_slices.as_ref(), event);
301 return;
302 }
303
304 self.diagnostics.record_live_modifier_slice_lookup_miss();
305 self.dispatch_modifier_slices(&self.handlers, event);
306 }
307}
308
309#[derive(Default)]
310struct HitBuffers {
311 hit_clips: Vec<HitClip>,
312 capture_path: Vec<NodeId>,
313}
314
315pub struct Scene {
316 pub graph: Option<RenderGraph>,
317 pub hits: Vec<HitRegion>,
318 hit_buffers: Vec<HitBuffers>,
319 pub next_hit_z: usize,
320 pub node_index: HashMap<NodeId, usize>,
321 diagnostics: Rc<RenderDiagnostics>,
322}
323
324impl Scene {
325 pub fn new() -> Self {
326 Self {
327 graph: None,
328 hits: Vec::new(),
329 hit_buffers: Vec::new(),
330 next_hit_z: 0,
331 node_index: HashMap::new(),
332 diagnostics: Rc::new(RenderDiagnostics::new()),
333 }
334 }
335
336 pub fn diagnostics(&self) -> &RenderDiagnostics {
337 self.diagnostics.as_ref()
338 }
339
340 pub fn push_hit(
343 &mut self,
344 node_id: NodeId,
345 capture_path: &[NodeId],
346 geometry: HitGeometry<'_>,
347 target: HitTargetSpec<'_>,
348 ) {
349 let HitTargetSpec { shape, handlers } = target;
350 if handlers.pointer_inputs().is_empty() && handlers.pointer_icon().is_none() {
351 return;
352 }
353 let mut buffers = self.hit_buffers.pop().unwrap_or_default();
354 buffers.capture_path.extend_from_slice(capture_path);
355
356 let z_index = self.next_hit_z;
357 self.next_hit_z += 1;
358 let hit_index = self.hits.len();
359 self.hits.push(HitRegion::with_diagnostics(HitRegionInit {
360 node_id,
361 capture_path: buffers.capture_path,
362 geometry,
363 clip_buffer: buffers.hit_clips,
364 shape,
365 handlers: Rc::clone(handlers),
366 z_index,
367 diagnostics: Rc::clone(&self.diagnostics),
368 }));
369 self.node_index.insert(node_id, hit_index);
370 }
371
372 pub fn clear_hits(&mut self) {
374 self.hit_buffers.clear();
375 for hit in self.hits.drain(..) {
376 let mut buffers = HitBuffers {
377 hit_clips: hit.hit_clips,
378 capture_path: hit.capture_path,
379 };
380 buffers.hit_clips.clear();
381 buffers.capture_path.clear();
382 self.hit_buffers.push(buffers);
383 }
384 self.node_index.clear();
385 self.next_hit_z = 0;
386 }
387
388 pub fn replace_graph(&mut self, graph: RenderGraph) {
389 self.graph = Some(graph);
390 }
391}
392
393impl Default for Scene {
394 fn default() -> Self {
395 Self::new()
396 }
397}
398
399impl RenderScene for Scene {
400 type HitTarget = HitRegion;
401
402 fn clear(&mut self) {
403 self.graph = None;
404 self.clear_hits();
405 }
406
407 fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
408 let mut hit_indices: Vec<usize> = self
409 .hits
410 .iter()
411 .enumerate()
412 .filter_map(|(index, hit)| hit.contains(x, y).then_some(index))
413 .collect();
414
415 hit_indices.sort_by_key(|&index| Reverse(self.hits[index].z_index));
416 hit_indices
417 .into_iter()
418 .map(|index| self.hits[index].clone())
419 .collect()
420 }
421
422 fn hit_test_near(&self, x: f32, y: f32) -> Option<Self::HitTarget> {
423 self.hits
424 .iter()
425 .filter_map(|hit| hit.reach_distance(x, y).map(|distance| (distance, hit)))
426 .min_by(|(near, hit), (other_near, other)| {
427 near.total_cmp(other_near)
428 .then_with(|| other.z_index.cmp(&hit.z_index))
429 })
430 .map(|(_, hit)| hit.clone())
431 }
432
433 fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
434 self.node_index
435 .get(&node_id)
436 .and_then(|&index| self.hits.get(index))
437 .cloned()
438 }
439
440 fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) -> bool {
441 if let Some(graph) = &self.graph {
442 graph.collect_retained_visual_observation_nodes(nodes);
443 } else {
444 nodes.clear();
445 }
446 true
447 }
448}
449
450fn point_in_rounded_rect(point: Point, rect: Rect, shape: RoundedCornerShape) -> bool {
451 if !rect.contains(point.x, point.y) {
452 return false;
453 }
454
455 let local_x = point.x - rect.x;
456 let local_y = point.y - rect.y;
457 let radii = shape.resolve(rect.width, rect.height);
458 let tl = radii.top_left;
459 let tr = radii.top_right;
460 let bl = radii.bottom_left;
461 let br = radii.bottom_right;
462
463 if local_x < tl && local_y < tl {
464 let dx = tl - local_x;
465 let dy = tl - local_y;
466 return dx * dx + dy * dy <= tl * tl;
467 }
468
469 if local_x > rect.width - tr && local_y < tr {
470 let dx = local_x - (rect.width - tr);
471 let dy = tr - local_y;
472 return dx * dx + dy * dy <= tr * tr;
473 }
474
475 if local_x < bl && local_y > rect.height - bl {
476 let dx = bl - local_x;
477 let dy = local_y - (rect.height - bl);
478 return dx * dx + dy * dy <= bl * bl;
479 }
480
481 if local_x > rect.width - br && local_y > rect.height - br {
482 let dx = local_x - (rect.width - br);
483 let dy = local_y - (rect.height - br);
484 return dx * dx + dy * dy <= br * br;
485 }
486
487 true
488}
489
490fn point_in_quad(point: Point, quad: [[f32; 2]; 4]) -> bool {
491 point_in_triangle(point, quad[0], quad[1], quad[3])
492 || point_in_triangle(point, quad[0], quad[3], quad[2])
493}
494
495fn point_in_triangle(point: Point, a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
496 let d1 = triangle_sign(point, a, b);
497 let d2 = triangle_sign(point, b, c);
498 let d3 = triangle_sign(point, c, a);
499 let has_negative = d1 < -f32::EPSILON || d2 < -f32::EPSILON || d3 < -f32::EPSILON;
500 let has_positive = d1 > f32::EPSILON || d2 > f32::EPSILON || d3 > f32::EPSILON;
501 !(has_negative && has_positive)
502}
503
504fn triangle_sign(point: Point, a: [f32; 2], b: [f32; 2]) -> f32 {
505 (point.x - b[0]) * (a[1] - b[1]) - (a[0] - b[0]) * (point.y - b[1])
506}
507
508#[cfg(test)]
509#[path = "tests/graph_scene_tests.rs"]
510mod tests;