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, SceneUpdates,
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 rebuild_from_applier(&mut self, applier: &MemoryApplier, root: NodeId) {
345 let previous = self.graph.take();
346 self.clear_hits();
347 let Some(graph) =
348 crate::scene_builder::rebuild_graph_from_applier(applier, root, 1.0, previous)
349 else {
350 return;
351 };
352 self.install_graph_with_hits(graph);
353 }
354
355 fn install_graph_with_hits(&mut self, graph: RenderGraph) {
356 crate::hit_graph::collect_hits_from_graph(
357 &graph.root,
358 ProjectiveTransform::identity(),
359 self,
360 None,
361 );
362 self.replace_graph(graph);
363 }
364
365 pub fn update_from_applier(
370 &mut self,
371 applier: &MemoryApplier,
372 root: NodeId,
373 updates: SceneUpdates<'_>,
374 refresh_hits: bool,
375 ) {
376 let report = if updates.is_empty() {
377 None
378 } else {
379 self.graph.as_mut().map(|graph| {
380 crate::scene_builder::update_graph_from_applier_report(applier, graph, updates, 1.0)
381 })
382 };
383 let Some(report) = report.filter(|report| report.applied()) else {
384 self.rebuild_from_applier(applier, root);
385 return;
386 };
387 if !refresh_hits && !report.hit_graph_dirty {
388 return;
389 }
390 self.clear_hits();
391 if let Some(graph) = self.graph.take() {
392 self.install_graph_with_hits(graph);
393 }
394 }
395
396 pub fn push_hit(
399 &mut self,
400 node_id: NodeId,
401 capture_path: &[NodeId],
402 geometry: HitGeometry<'_>,
403 target: HitTargetSpec<'_>,
404 ) {
405 let HitTargetSpec { shape, handlers } = target;
406 if handlers.pointer_inputs().is_empty() && handlers.pointer_icon().is_none() {
407 return;
408 }
409 let mut buffers = self.hit_buffers.pop().unwrap_or_default();
410 buffers.capture_path.extend_from_slice(capture_path);
411
412 let z_index = self.next_hit_z;
413 self.next_hit_z += 1;
414 let hit_index = self.hits.len();
415 self.hits.push(HitRegion::with_diagnostics(HitRegionInit {
416 node_id,
417 capture_path: buffers.capture_path,
418 geometry,
419 clip_buffer: buffers.hit_clips,
420 shape,
421 handlers: Rc::clone(handlers),
422 z_index,
423 diagnostics: Rc::clone(&self.diagnostics),
424 }));
425 self.node_index.insert(node_id, hit_index);
426 }
427
428 pub fn clear_hits(&mut self) {
430 self.hit_buffers.clear();
431 for hit in self.hits.drain(..) {
432 let mut buffers = HitBuffers {
433 hit_clips: hit.hit_clips,
434 capture_path: hit.capture_path,
435 };
436 buffers.hit_clips.clear();
437 buffers.capture_path.clear();
438 self.hit_buffers.push(buffers);
439 }
440 self.node_index.clear();
441 self.next_hit_z = 0;
442 }
443
444 pub fn replace_graph(&mut self, graph: RenderGraph) {
445 self.graph = Some(graph);
446 }
447}
448
449impl Default for Scene {
450 fn default() -> Self {
451 Self::new()
452 }
453}
454
455impl RenderScene for Scene {
456 type HitTarget = HitRegion;
457
458 fn clear(&mut self) {
459 self.graph = None;
460 self.clear_hits();
461 }
462
463 fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
464 let mut hit_indices: Vec<usize> = self
465 .hits
466 .iter()
467 .enumerate()
468 .filter_map(|(index, hit)| hit.contains(x, y).then_some(index))
469 .collect();
470
471 hit_indices.sort_by_key(|&index| Reverse(self.hits[index].z_index));
472 hit_indices
473 .into_iter()
474 .map(|index| self.hits[index].clone())
475 .collect()
476 }
477
478 fn hit_test_near(&self, x: f32, y: f32) -> Option<Self::HitTarget> {
479 self.hits
480 .iter()
481 .filter_map(|hit| hit.reach_distance(x, y).map(|distance| (distance, hit)))
482 .min_by(|(near, hit), (other_near, other)| {
483 near.total_cmp(other_near)
484 .then_with(|| other.z_index.cmp(&hit.z_index))
485 })
486 .map(|(_, hit)| hit.clone())
487 }
488
489 fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
490 self.node_index
491 .get(&node_id)
492 .and_then(|&index| self.hits.get(index))
493 .cloned()
494 }
495
496 fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) -> bool {
497 if let Some(graph) = &self.graph {
498 graph.collect_retained_visual_observation_nodes(nodes);
499 } else {
500 nodes.clear();
501 }
502 true
503 }
504}
505
506fn point_in_rounded_rect(point: Point, rect: Rect, shape: RoundedCornerShape) -> bool {
507 if !rect.contains(point.x, point.y) {
508 return false;
509 }
510
511 let local_x = point.x - rect.x;
512 let local_y = point.y - rect.y;
513 let radii = shape.resolve(rect.width, rect.height);
514 let tl = radii.top_left;
515 let tr = radii.top_right;
516 let bl = radii.bottom_left;
517 let br = radii.bottom_right;
518
519 if local_x < tl && local_y < tl {
520 let dx = tl - local_x;
521 let dy = tl - local_y;
522 return dx * dx + dy * dy <= tl * tl;
523 }
524
525 if local_x > rect.width - tr && local_y < tr {
526 let dx = local_x - (rect.width - tr);
527 let dy = tr - local_y;
528 return dx * dx + dy * dy <= tr * tr;
529 }
530
531 if local_x < bl && local_y > rect.height - bl {
532 let dx = bl - local_x;
533 let dy = local_y - (rect.height - bl);
534 return dx * dx + dy * dy <= bl * bl;
535 }
536
537 if local_x > rect.width - br && local_y > rect.height - br {
538 let dx = local_x - (rect.width - br);
539 let dy = local_y - (rect.height - br);
540 return dx * dx + dy * dy <= br * br;
541 }
542
543 true
544}
545
546fn point_in_quad(point: Point, quad: [[f32; 2]; 4]) -> bool {
547 point_in_triangle(point, quad[0], quad[1], quad[3])
548 || point_in_triangle(point, quad[0], quad[3], quad[2])
549}
550
551fn point_in_triangle(point: Point, a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
552 let d1 = triangle_sign(point, a, b);
553 let d2 = triangle_sign(point, b, c);
554 let d3 = triangle_sign(point, c, a);
555 let has_negative = d1 < -f32::EPSILON || d2 < -f32::EPSILON || d3 < -f32::EPSILON;
556 let has_positive = d1 > f32::EPSILON || d2 > f32::EPSILON || d3 > f32::EPSILON;
557 !(has_negative && has_positive)
558}
559
560fn triangle_sign(point: Point, a: [f32; 2], b: [f32; 2]) -> f32 {
561 (point.x - b[0]) * (a[1] - b[1]) - (a[0] - b[0]) * (point.y - b[1])
562}
563
564#[cfg(test)]
565#[path = "tests/graph_scene_tests.rs"]
566mod tests;