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