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 {
22 reported_warnings: RefCell<HashSet<&'static str>>,
23 live_modifier_slice_lookup_miss_count: Cell<usize>,
24}
25
26impl RenderDiagnostics {
27 pub fn new() -> Self {
29 Self {
30 reported_warnings: RefCell::new(HashSet::default()),
31 live_modifier_slice_lookup_miss_count: Cell::new(0),
32 }
33 }
34
35 pub fn claim_warning_once(&self, key: &'static str) -> bool {
37 self.reported_warnings.borrow_mut().insert(key)
38 }
39
40 pub fn record_live_modifier_slice_lookup_miss(&self) {
43 self.live_modifier_slice_lookup_miss_count.set(
44 self.live_modifier_slice_lookup_miss_count
45 .get()
46 .saturating_add(1),
47 );
48 }
49
50 pub fn live_modifier_slice_lookup_miss_count(&self) -> usize {
53 self.live_modifier_slice_lookup_miss_count.get()
54 }
55}
56
57impl Default for RenderDiagnostics {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63#[derive(Clone, Copy, Debug, PartialEq)]
65pub struct HitClip {
66 pub quad: [[f32; 2]; 4],
68 pub bounds: Rect,
70}
71
72#[derive(Clone, Copy)]
74pub struct HitGeometry<'a> {
75 pub rect: Rect,
77 pub quad: [[f32; 2]; 4],
79 pub local_bounds: Rect,
81 pub world_to_local: ProjectiveTransform,
83 pub hit_clip_bounds: Option<Rect>,
85 pub hit_clips: &'a [HitClip],
87}
88
89pub struct HitTargetSpec<'a> {
93 pub shape: Option<RoundedCornerShape>,
96 pub handlers: &'a Rc<ModifierNodeSlices>,
98}
99
100#[derive(Clone)]
102pub struct HitRegion {
103 pub node_id: NodeId,
105 pub capture_path: Vec<NodeId>,
108 pub rect: Rect,
110 pub quad: [[f32; 2]; 4],
112 pub local_bounds: Rect,
114 pub world_to_local: ProjectiveTransform,
116 pub shape: Option<RoundedCornerShape>,
118 pub handlers: Rc<ModifierNodeSlices>,
121 pub z_index: usize,
123 pub hit_clip_bounds: Option<Rect>,
125 pub hit_clips: Vec<HitClip>,
127 diagnostics: Rc<RenderDiagnostics>,
128}
129
130struct HitRegionInit<'a> {
131 node_id: NodeId,
132 capture_path: Vec<NodeId>,
133 geometry: HitGeometry<'a>,
134 clip_buffer: Vec<HitClip>,
135 shape: Option<RoundedCornerShape>,
136 handlers: Rc<ModifierNodeSlices>,
137 z_index: usize,
138 diagnostics: Rc<RenderDiagnostics>,
139}
140
141impl Default for HitRegionInit<'_> {
142 fn default() -> Self {
143 Self {
144 node_id: 0,
145 capture_path: Vec::new(),
146 geometry: HitGeometry {
147 rect: Rect {
148 x: 0.0,
149 y: 0.0,
150 width: 0.0,
151 height: 0.0,
152 },
153 quad: [[0.0, 0.0]; 4],
154 local_bounds: Rect {
155 x: 0.0,
156 y: 0.0,
157 width: 0.0,
158 height: 0.0,
159 },
160 world_to_local: ProjectiveTransform::identity(),
161 hit_clip_bounds: None,
162 hit_clips: &[],
163 },
164 clip_buffer: Vec::new(),
165 shape: None,
166 handlers: Rc::default(),
167 z_index: 0,
168 diagnostics: Rc::new(RenderDiagnostics::new()),
169 }
170 }
171}
172
173impl HitRegion {
174 fn with_diagnostics(init: HitRegionInit<'_>) -> Self {
175 let HitRegionInit {
176 node_id,
177 capture_path,
178 geometry,
179 clip_buffer: mut hit_clips,
180 shape,
181 handlers,
182 z_index,
183 diagnostics,
184 } = init;
185 let HitGeometry {
186 rect,
187 quad,
188 local_bounds,
189 world_to_local,
190 hit_clip_bounds,
191 hit_clips: clips,
192 } = geometry;
193 hit_clips.extend_from_slice(clips);
194 Self {
195 node_id,
196 capture_path,
197 rect,
198 quad,
199 local_bounds,
200 world_to_local,
201 shape,
202 handlers,
203 z_index,
204 hit_clip_bounds,
205 hit_clips,
206 diagnostics,
207 }
208 }
209
210 fn contains(&self, x: f32, y: f32) -> bool {
211 if !self.rect.contains(x, y) {
212 return false;
213 }
214
215 if let Some(clip_bounds) = self.hit_clip_bounds
216 && !clip_bounds.contains(x, y)
217 {
218 return false;
219 }
220
221 let point = Point { x, y };
222 if !point_in_quad(point, self.quad) {
223 return false;
224 }
225
226 for clip in &self.hit_clips {
227 if !point_in_quad(point, clip.quad) {
228 return false;
229 }
230 }
231
232 let local_point = self.world_to_local.map_point(point);
233 if let Some(shape) = self.shape {
234 point_in_rounded_rect(local_point, self.local_bounds, shape)
235 } else {
236 self.local_bounds.contains(local_point.x, local_point.y)
237 }
238 }
239
240 fn reach_distance(&self, x: f32, y: f32) -> Option<f32> {
245 if self.handlers.pointer_inputs().is_empty() {
246 return None;
247 }
248 if let Some(clip_bounds) = self.hit_clip_bounds
249 && !clip_bounds.contains(x, y)
250 {
251 return None;
252 }
253 let grow_x = ((MINIMUM_TOUCH_TARGET_SIZE - self.rect.width) / 2.0).max(0.0);
254 let grow_y = ((MINIMUM_TOUCH_TARGET_SIZE - self.rect.height) / 2.0).max(0.0);
255 if grow_x <= 0.0 && grow_y <= 0.0 {
256 return None;
257 }
258 let right = self.rect.x + self.rect.width;
259 let bottom = self.rect.y + self.rect.height;
260 let in_reach = x >= self.rect.x - grow_x
261 && x <= right + grow_x
262 && y >= self.rect.y - grow_y
263 && y <= bottom + grow_y;
264 if !in_reach {
265 return None;
266 }
267 let dx = (self.rect.x - x).max(x - right).max(0.0);
268 let dy = (self.rect.y - y).max(y - bottom).max(0.0);
269 Some(dx * dx + dy * dy)
270 }
271
272 fn localize_event(&self, event: &PointerEvent) -> PointerEvent {
273 let local = self.world_to_local.map_point(event.global_position);
274 event.copy_with_local_position(Point {
275 x: local.x - self.local_bounds.x,
276 y: local.y - self.local_bounds.y,
277 })
278 }
279
280 fn dispatch_modifier_slices(&self, modifier_slices: &ModifierNodeSlices, event: PointerEvent) {
281 if should_skip_consumed_event(&event) {
282 return;
283 }
284
285 modifier_slices.dispatch_pointer_event(self.localize_event(&event));
286 }
287
288 fn live_modifier_slices(&self, applier: &mut MemoryApplier) -> Option<Rc<ModifierNodeSlices>> {
289 if let Ok(modifier_slices) =
290 applier.with_node::<LayoutNode, _>(self.node_id, |node| node.modifier_slices_snapshot())
291 {
292 return Some(modifier_slices);
293 }
294
295 applier
296 .with_node::<SubcomposeLayoutNode, _>(self.node_id, |node| {
297 node.modifier_slices_snapshot()
298 })
299 .ok()
300 }
301}
302
303fn is_terminal_pointer_event(kind: PointerEventKind) -> bool {
304 matches!(kind, PointerEventKind::Up | PointerEventKind::Cancel)
305}
306
307fn should_skip_consumed_event(event: &PointerEvent) -> bool {
308 event.is_consumed() && !is_terminal_pointer_event(event.kind)
309}
310
311impl HitTestTarget for HitRegion {
312 fn node_id(&self) -> NodeId {
313 self.node_id
314 }
315
316 fn pointer_icon(&self) -> Option<PointerIcon> {
317 self.handlers.pointer_icon().cloned()
318 }
319
320 fn capture_path(&self) -> Vec<NodeId> {
321 self.capture_path.clone()
322 }
323
324 fn dispatch(&self, event: PointerEvent) {
325 self.dispatch_modifier_slices(&self.handlers, event);
326 }
327
328 fn dispatch_with_applier(&self, applier: &mut MemoryApplier, event: PointerEvent) {
329 if let Some(modifier_slices) = self.live_modifier_slices(applier) {
330 self.dispatch_modifier_slices(modifier_slices.as_ref(), event);
331 return;
332 }
333
334 self.diagnostics.record_live_modifier_slice_lookup_miss();
335 self.dispatch_modifier_slices(&self.handlers, event);
336 }
337}
338
339#[derive(Default)]
340struct HitBuffers {
341 hit_clips: Vec<HitClip>,
342 capture_path: Vec<NodeId>,
343}
344
345pub struct Scene {
347 pub graph: Option<RenderGraph>,
349 pub hits: Vec<HitRegion>,
351 hit_buffers: Vec<HitBuffers>,
352 pub next_hit_z: usize,
354 pub node_index: HashMap<NodeId, usize>,
356 diagnostics: Rc<RenderDiagnostics>,
357}
358
359impl Scene {
360 pub fn new() -> Self {
362 Self {
363 graph: None,
364 hits: Vec::new(),
365 hit_buffers: Vec::new(),
366 next_hit_z: 0,
367 node_index: HashMap::default(),
368 diagnostics: Rc::new(RenderDiagnostics::new()),
369 }
370 }
371
372 pub fn diagnostics(&self) -> &RenderDiagnostics {
374 self.diagnostics.as_ref()
375 }
376
377 pub fn rebuild_from_applier(&mut self, applier: &MemoryApplier, root: NodeId) {
380 let previous = self.graph.take();
381 self.clear_hits();
382 let Some(graph) =
383 crate::scene_builder::rebuild_graph_from_applier(applier, root, 1.0, previous)
384 else {
385 return;
386 };
387 self.install_graph_with_hits(graph);
388 }
389
390 fn install_graph_with_hits(&mut self, graph: RenderGraph) {
391 crate::hit_graph::collect_hits_from_graph(
392 &graph.root,
393 ProjectiveTransform::identity(),
394 self,
395 None,
396 );
397 self.replace_graph(graph);
398 }
399
400 pub fn update_from_applier(
405 &mut self,
406 applier: &MemoryApplier,
407 root: NodeId,
408 updates: SceneUpdates<'_>,
409 refresh_hits: bool,
410 ) {
411 let report = if updates.is_empty() {
412 None
413 } else {
414 self.graph.as_mut().map(|graph| {
415 crate::scene_builder::update_graph_from_applier_report(applier, graph, updates, 1.0)
416 })
417 };
418 let Some(report) = report.filter(|report| report.applied()) else {
419 self.rebuild_from_applier(applier, root);
420 return;
421 };
422 if !refresh_hits && !report.hit_graph_dirty {
423 return;
424 }
425 self.clear_hits();
426 if let Some(graph) = self.graph.take() {
427 self.install_graph_with_hits(graph);
428 }
429 }
430
431 pub fn push_hit(
434 &mut self,
435 node_id: NodeId,
436 capture_path: &[NodeId],
437 geometry: HitGeometry<'_>,
438 target: HitTargetSpec<'_>,
439 ) {
440 let HitTargetSpec { shape, handlers } = target;
441 if handlers.pointer_inputs().is_empty() && handlers.pointer_icon().is_none() {
442 return;
443 }
444 let mut buffers = self.hit_buffers.pop().unwrap_or_default();
445 buffers.capture_path.extend_from_slice(capture_path);
446
447 let z_index = self.next_hit_z;
448 self.next_hit_z += 1;
449 let hit_index = self.hits.len();
450 self.hits.push(HitRegion::with_diagnostics(HitRegionInit {
451 node_id,
452 capture_path: buffers.capture_path,
453 geometry,
454 clip_buffer: buffers.hit_clips,
455 shape,
456 handlers: Rc::clone(handlers),
457 z_index,
458 diagnostics: Rc::clone(&self.diagnostics),
459 }));
460 self.node_index.insert(node_id, hit_index);
461 }
462
463 pub fn clear_hits(&mut self) {
465 self.hit_buffers.clear();
466 for hit in self.hits.drain(..) {
467 let mut buffers = HitBuffers {
468 hit_clips: hit.hit_clips,
469 capture_path: hit.capture_path,
470 };
471 buffers.hit_clips.clear();
472 buffers.capture_path.clear();
473 self.hit_buffers.push(buffers);
474 }
475 self.node_index.clear();
476 self.next_hit_z = 0;
477 }
478
479 pub fn replace_graph(&mut self, graph: RenderGraph) {
481 self.graph = Some(graph);
482 }
483}
484
485impl Default for Scene {
486 fn default() -> Self {
487 Self::new()
488 }
489}
490
491impl RenderScene for Scene {
492 type HitTarget = HitRegion;
493
494 fn clear(&mut self) {
495 self.graph = None;
496 self.clear_hits();
497 }
498
499 fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
500 let mut hit_indices: Vec<usize> = self
501 .hits
502 .iter()
503 .enumerate()
504 .filter_map(|(index, hit)| hit.contains(x, y).then_some(index))
505 .collect();
506
507 hit_indices.sort_by_key(|&index| Reverse(self.hits[index].z_index));
508 hit_indices
509 .into_iter()
510 .map(|index| self.hits[index].clone())
511 .collect()
512 }
513
514 fn hit_test_near(&self, x: f32, y: f32) -> Option<Self::HitTarget> {
515 self.hits
516 .iter()
517 .filter_map(|hit| hit.reach_distance(x, y).map(|distance| (distance, hit)))
518 .min_by(|(near, hit), (other_near, other)| {
519 near.total_cmp(other_near)
520 .then_with(|| other.z_index.cmp(&hit.z_index))
521 })
522 .map(|(_, hit)| hit.clone())
523 }
524
525 fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
526 self.node_index
527 .get(&node_id)
528 .and_then(|&index| self.hits.get(index))
529 .cloned()
530 }
531
532 fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) -> bool {
533 if let Some(graph) = &self.graph {
534 graph.collect_retained_visual_observation_nodes(nodes);
535 } else {
536 nodes.clear();
537 }
538 true
539 }
540}
541
542fn point_in_rounded_rect(point: Point, rect: Rect, shape: RoundedCornerShape) -> bool {
543 if !rect.contains(point.x, point.y) {
544 return false;
545 }
546
547 let local_x = point.x - rect.x;
548 let local_y = point.y - rect.y;
549 let radii = shape.resolve(rect.width, rect.height);
550 let tl = radii.top_left;
551 let tr = radii.top_right;
552 let bl = radii.bottom_left;
553 let br = radii.bottom_right;
554
555 if local_x < tl && local_y < tl {
556 let dx = tl - local_x;
557 let dy = tl - local_y;
558 return dx * dx + dy * dy <= tl * tl;
559 }
560
561 if local_x > rect.width - tr && local_y < tr {
562 let dx = local_x - (rect.width - tr);
563 let dy = tr - local_y;
564 return dx * dx + dy * dy <= tr * tr;
565 }
566
567 if local_x < bl && local_y > rect.height - bl {
568 let dx = bl - local_x;
569 let dy = local_y - (rect.height - bl);
570 return dx * dx + dy * dy <= bl * bl;
571 }
572
573 if local_x > rect.width - br && local_y > rect.height - br {
574 let dx = local_x - (rect.width - br);
575 let dy = local_y - (rect.height - br);
576 return dx * dx + dy * dy <= br * br;
577 }
578
579 true
580}
581
582fn point_in_quad(point: Point, quad: [[f32; 2]; 4]) -> bool {
583 point_in_triangle(point, quad[0], quad[1], quad[3])
584 || point_in_triangle(point, quad[0], quad[3], quad[2])
585}
586
587fn point_in_triangle(point: Point, a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
588 let d1 = triangle_sign(point, a, b);
589 let d2 = triangle_sign(point, b, c);
590 let d3 = triangle_sign(point, c, a);
591 let has_negative = d1 < -f32::EPSILON || d2 < -f32::EPSILON || d3 < -f32::EPSILON;
592 let has_positive = d1 > f32::EPSILON || d2 > f32::EPSILON || d3 > f32::EPSILON;
593 !(has_negative && has_positive)
594}
595
596fn triangle_sign(point: Point, a: [f32; 2], b: [f32; 2]) -> f32 {
597 (point.x - b[0]) * (a[1] - b[1]) - (a[0] - b[0]) * (point.y - b[1])
598}
599
600#[cfg(test)]
601#[path = "tests/graph_scene_tests.rs"]
602mod tests;