1use crate::api::{
4 Event, Layer, LocalProperty, ViewportProximityChange, ViewportProximityEnter,
5 ViewportProximityExit, ViewportProximitySnapshot,
6};
7use crate::constants::*;
8use crate::scene_geometry::SpatialIndex;
9use crate::{ExpandedNode, ExpandedNodeIdentifier, RuntimeContext};
10use kurbo::{Affine, Rect, Shape};
11use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
12use std::rc::Rc;
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub struct ViewportProximityStats {
17 pub index_updates: u64,
19 pub domain_queries: u64,
21 pub candidates: u64,
23 pub samples: u64,
25 pub events: u64,
27}
28
29struct Registration {
30 generation: u64,
31 chain: Vec<u32>,
32}
33struct Dependency {
34 users: usize,
35 _effect: LocalProperty<()>,
36 parent: Option<u32>,
37 owner: Option<u32>,
38 scrolls: bool,
39 canvas: bool,
40 escapes: bool,
41 unclipped_scroll: bool,
42}
43
44#[derive(Default)]
45pub(crate) struct ViewportProximityState {
46 registrations: BTreeMap<u32, Registration>,
47 dependencies: HashMap<u32, Dependency>,
48 indices: HashMap<Option<u32>, SpatialIndex>,
49 indexed: HashMap<u32, (Option<u32>, Rect)>,
50 dirty: BTreeSet<u32>,
51 pending_bindings: BTreeSet<u32>,
52 active: BTreeMap<u32, ViewportProximitySnapshot>,
53 escaped: BTreeSet<u32>,
54 pending: bool,
55 rebind: bool,
56 escape_dirty: bool,
57 generation: u64,
58 stats: ViewportProximityStats,
59}
60
61fn has_handlers(node: &ExpandedNode, key: &str) -> bool {
62 node.instance_node
63 .borrow()
64 .base()
65 .get_handler_registry()
66 .is_some_and(|registry| {
67 registry
68 .borrow()
69 .handlers
70 .get(key)
71 .is_some_and(|handlers| !handlers.is_empty())
72 })
73}
74
75impl RuntimeContext {
76 pub fn viewport_proximity_stats(&self) -> ViewportProximityStats {
78 self.viewport_proximity.borrow().stats
79 }
80
81 pub(crate) fn viewport_dependency(&self, id: u32) -> bool {
82 self.viewport_proximity
83 .borrow()
84 .dependencies
85 .contains_key(&id)
86 }
87
88 pub(crate) fn invalidate_viewport_presentation(&self) {
89 let mut state = self.viewport_proximity.borrow_mut();
90 if !state.registrations.is_empty() {
91 state.pending = true;
92 }
93 }
94
95 pub(crate) fn viewport_structure_changed(&self) {
96 let mut state = self.viewport_proximity.borrow_mut();
97 if !state.registrations.is_empty() {
98 state.rebind = true;
99 state.pending = true;
100 }
101 }
102
103 fn invalidate_viewport_dependency(&self, id: u32) {
104 let mut state = self.viewport_proximity.borrow_mut();
105 if state.dependencies.contains_key(&id) {
106 state.dirty.insert(id);
107 state.pending = true;
108 drop(state);
109 self.invalidate_scene_geometry(id);
110 }
111 }
112
113 pub(crate) fn register_viewport_proximity(self: &Rc<Self>, node: &Rc<ExpandedNode>) {
114 if node.is_render_source() {
116 return;
117 }
118 if ![
119 VIEWPORT_PROXIMITY_ENTER_HANDLERS,
120 VIEWPORT_PROXIMITY_CHANGE_HANDLERS,
121 VIEWPORT_PROXIMITY_EXIT_HANDLERS,
122 ]
123 .iter()
124 .any(|key| has_handlers(node, key))
125 {
126 return;
127 }
128 let id = node.id.to_u32();
129 if self
130 .viewport_proximity
131 .borrow()
132 .registrations
133 .contains_key(&id)
134 {
135 return;
136 }
137 let chain = Vec::new();
138 let mut state = self.viewport_proximity.borrow_mut();
139 state.generation += 1;
140 let generation = state.generation;
141 state.pending_bindings.insert(id);
142 state.escape_dirty = true;
143 state
144 .registrations
145 .insert(id, Registration { generation, chain });
146 state.dirty.insert(id);
147 state.pending = true;
148 }
149
150 fn acquire_viewport_chain(self: &Rc<Self>, target: &Rc<ExpandedNode>) -> Vec<u32> {
151 let mut nodes = vec![target.clone()];
152 let mut parent = target.render_parent_node();
153 while let Some(node) = parent {
154 parent = node.render_parent_node();
155 nodes.push(node);
156 }
157 for node in nodes.iter().rev() {
158 let id = node.id.to_u32();
159 {
160 let mut state = self.viewport_proximity.borrow_mut();
161 if let Some(dependency) = state.dependencies.get_mut(&id) {
162 dependency.users += 1;
163 continue;
164 }
165 }
166 let parent = node.render_parent_node().map(|p| p.id.to_u32());
167 let owner = parent.and_then(|parent| {
168 let state = self.viewport_proximity.borrow();
169 let dependency = &state.dependencies[&parent];
170 if dependency.scrolls {
171 Some(parent)
172 } else {
173 dependency.owner
174 }
175 });
176 let instance = node.instance_node.borrow();
177 let scrolls = instance.scrolls_content(node);
178 let canvas = instance.base().flags().layer == Layer::Canvas;
179 let mut dependencies: Vec<_> = node
180 .properties_scope
181 .borrow()
182 .iter()
183 .filter(|(name, _)| instance.property_requires_occlusion_recompute(name))
184 .map(|(_, value)| value.get_untyped_property().clone())
185 .collect();
186 drop(instance);
187 dependencies.extend([
188 node.transform_and_bounds.untyped(),
189 node.parent_frame.untyped(),
190 node.occlusion.untyped(),
191 node.suspended.untyped(),
192 node.get_common_properties().borrow().unclippable.untyped(),
193 ]);
194 let weak_context = Rc::downgrade(self);
195 let effect = self.register_node_effect(node.id, &dependencies, move || {
196 if let Some(context) = weak_context.upgrade() {
197 context.invalidate_viewport_dependency(id);
198 }
199 });
200 let mut state = self.viewport_proximity.borrow_mut();
201 state.dependencies.insert(
202 id,
203 Dependency {
204 users: 1,
205 _effect: effect,
206 parent,
207 owner,
208 scrolls,
209 canvas,
210 escapes: false,
211 unclipped_scroll: false,
212 },
213 );
214 state.dirty.insert(id);
215 }
216 nodes.iter().map(|n| n.id.to_u32()).collect()
217 }
218
219 fn release_viewport_chain(&self, chain: Vec<u32>) {
220 let mut remove_geometry = Vec::new();
221 let mut state = self.viewport_proximity.borrow_mut();
222 for id in chain {
223 let Some(dependency) = state.dependencies.get_mut(&id) else {
224 continue;
225 };
226 dependency.users -= 1;
227 if dependency.users != 0 {
228 continue;
229 }
230 let dependency = state.dependencies.remove(&id).unwrap();
231 if let Some((owner, _)) = state.indexed.remove(&id) {
232 if let Some(index) = state.indices.get_mut(&owner) {
233 index.remove(id);
234 }
235 }
236 if dependency.scrolls {
237 state.indices.remove(&Some(id));
238 }
239 state.dirty.remove(&id);
240 if !dependency.canvas {
241 remove_geometry.push(id);
242 }
243 }
244 drop(state);
245 for id in remove_geometry {
246 self.remove_observation_geometry(id);
247 }
248 }
249
250 pub(crate) fn unregister_viewport_proximity(&self, id: u32) {
251 let registration = {
252 let mut state = self.viewport_proximity.borrow_mut();
253 state.active.remove(&id);
254 state.pending_bindings.remove(&id);
255 state.escaped.remove(&id);
256 state.registrations.remove(&id)
257 };
258 if let Some(registration) = registration {
259 self.release_viewport_chain(registration.chain);
260 let mut state = self.viewport_proximity.borrow_mut();
262 if state.dependencies.get(&id).is_some_and(|d| !d.scrolls) {
263 if let Some((owner, _)) = state.indexed.remove(&id) {
264 if let Some(index) = state.indices.get_mut(&owner) {
265 index.remove(id);
266 }
267 }
268 }
269 }
270 }
271
272 fn refresh_viewport_index(self: &Rc<Self>) {
273 let rebind = std::mem::take(&mut self.viewport_proximity.borrow_mut().rebind);
274 if rebind {
275 let ids: Vec<_> = self
276 .viewport_proximity
277 .borrow()
278 .registrations
279 .keys()
280 .copied()
281 .collect();
282 for &id in &ids {
283 let chain = std::mem::take(
284 &mut self
285 .viewport_proximity
286 .borrow_mut()
287 .registrations
288 .get_mut(&id)
289 .unwrap()
290 .chain,
291 );
292 self.release_viewport_chain(chain);
293 }
294 for id in ids {
295 if let Some(node) = self.get_expanded_node_by_eid(ExpandedNodeIdentifier(id)) {
296 let chain = self.acquire_viewport_chain(&node);
297 self.viewport_proximity
298 .borrow_mut()
299 .registrations
300 .get_mut(&id)
301 .unwrap()
302 .chain = chain;
303 }
304 }
305 self.viewport_proximity
306 .borrow_mut()
307 .pending_bindings
308 .clear();
309 } else {
310 let pending =
311 std::mem::take(&mut self.viewport_proximity.borrow_mut().pending_bindings);
312 for id in pending {
313 if let Some(node) = self.get_expanded_node_by_eid(ExpandedNodeIdentifier(id)) {
314 let chain = self.acquire_viewport_chain(&node);
315 if let Some(registration) = self
316 .viewport_proximity
317 .borrow_mut()
318 .registrations
319 .get_mut(&id)
320 {
321 registration.chain = chain;
322 }
323 }
324 }
325 }
326 self.drain_node_effects();
327 self.viewport_proximity.borrow_mut().pending = false;
328 let dirty = std::mem::take(&mut self.viewport_proximity.borrow_mut().dirty);
329 let mut escapes_changed =
330 rebind || std::mem::take(&mut self.viewport_proximity.borrow_mut().escape_dirty);
331 for id in dirty {
332 let (owner, indexed) = {
333 let state = self.viewport_proximity.borrow();
334 let Some(dependency) = state.dependencies.get(&id) else {
335 continue;
336 };
337 (
338 dependency.owner,
339 dependency.scrolls || state.registrations.contains_key(&id),
340 )
341 };
342 let Some(node) = self.get_expanded_node_by_eid(ExpandedNodeIdentifier(id)) else {
343 continue;
344 };
345 let escapes = node
346 .get_common_properties()
347 .borrow()
348 .unclippable
349 .get()
350 .unwrap_or(false);
351 let unclipped_scroll = {
352 let instance = node.instance_node.borrow();
353 instance.scrolls_content(&node) && !instance.clips_content(&node)
354 };
355 let geometry = self.canvas_geometry_for_node(&node);
356 let owner_transform = owner
357 .and_then(|owner| self.get_expanded_node_by_eid(ExpandedNodeIdentifier(owner)))
358 .map(|owner| self.canvas_geometry_for_node(&owner).world_transform)
359 .unwrap_or(Affine::IDENTITY);
360 let bounds = (owner_transform.inverse() * geometry.world_transform)
361 .transform_rect_bbox(Rect::new(0.0, 0.0, geometry.bounds.0, geometry.bounds.1));
362 let mut state = self.viewport_proximity.borrow_mut();
363 let dependency = state.dependencies.get_mut(&id).unwrap();
364 escapes_changed |=
365 dependency.escapes != escapes || dependency.unclipped_scroll != unclipped_scroll;
366 dependency.unclipped_scroll = unclipped_scroll;
367 dependency.escapes = escapes;
368 if indexed && state.indexed.get(&id) != Some(&(owner, bounds)) {
369 if let Some((old_owner, _)) = state.indexed.remove(&id) {
370 if let Some(index) = state.indices.get_mut(&old_owner) {
371 index.remove(id);
372 }
373 }
374 state
375 .indices
376 .entry(owner)
377 .or_default()
378 .insert(id, Some(bounds));
379 state.indexed.insert(id, (owner, bounds));
380 state.stats.index_updates += 1;
381 }
382 }
383 if escapes_changed {
384 let mut state = self.viewport_proximity.borrow_mut();
385 state.escaped = state
388 .registrations
389 .iter()
390 .filter_map(|(&id, registration)| {
391 registration
392 .chain
393 .iter()
394 .any(|id| {
395 state
396 .dependencies
397 .get(id)
398 .is_some_and(|d| d.escapes || d.unclipped_scroll)
399 })
400 .then_some(id)
401 })
402 .collect();
403 }
404 }
405
406 pub(crate) fn dispatch_viewport_proximity(self: &Rc<Self>) {
408 {
409 let mut state = self.viewport_proximity.borrow_mut();
410 if !state.pending || state.registrations.is_empty() {
411 return;
412 }
413 state.pending = false;
414 }
415 self.refresh_viewport_index();
416 let viewport = self.viewport_observation_rect();
417 let mut frames = HashMap::new();
418 let mut candidates = BTreeSet::new();
419 self.query_viewport_domain(None, viewport, &mut frames, &mut candidates);
420 {
421 let state = self.viewport_proximity.borrow();
422 candidates.extend(state.active.keys());
423 candidates.extend(&state.escaped);
424 }
425 let mut batch = Vec::new();
426 for id in candidates {
427 if !self
428 .viewport_proximity
429 .borrow()
430 .registrations
431 .contains_key(&id)
432 {
433 continue;
434 }
435 let Some(node) = self.get_expanded_node_by_eid(ExpandedNodeIdentifier(id)) else {
436 continue;
437 };
438 if node.suspended.get() {
439 let mut state = self.viewport_proximity.borrow_mut();
440 state.active.remove(&id);
441 continue;
442 }
443 let Some(frame) = self.viewport_frame(id, viewport, &mut frames) else {
444 continue;
445 };
446 let geometry = self.canvas_geometry_for_node(&node);
447 let transform = frame.inherited_scroll * geometry.world_transform;
448 let bounds = transform.transform_rect_bbox(Rect::new(
449 0.0,
450 0.0,
451 geometry.bounds.0,
452 geometry.bounds.1,
453 ));
454 let has_area = geometry.bounds.0 > 0.0
456 && geometry.bounds.1 > 0.0
457 && transform.determinant().is_finite()
458 && transform.determinant() != 0.0
459 && [bounds.x0, bounds.y0, bounds.x1, bounds.y1]
460 .iter()
461 .all(|v| v.is_finite());
462 let origin = kurbo::Vec2::new(-viewport.x0, -viewport.y0);
463 let current = ViewportProximitySnapshot {
464 in_proximity: has_area && intersect(Some(bounds), frame.near).is_some(),
465 bounds: bounds + origin,
466 viewport_intersection: has_area
467 .then(|| intersect(Some(bounds), frame.visible))
468 .flatten()
469 .map(|r| r + origin),
470 };
471 let mut state = self.viewport_proximity.borrow_mut();
472 state.stats.samples += 1;
473 let previous = state.active.get(&id).copied();
474 let generation = state.registrations[&id].generation;
475 if current.in_proximity {
476 state.active.insert(id, current);
477 } else {
478 state.active.remove(&id);
479 }
480 if previous == Some(current) || (previous.is_none() && !current.in_proximity) {
481 continue;
482 }
483 if previous.is_none() {
484 batch.push((
485 id,
486 generation,
487 Delivery::Enter(ViewportProximityEnter { current }),
488 ));
489 }
490 batch.push((
491 id,
492 generation,
493 Delivery::Change(ViewportProximityChange { previous, current }),
494 ));
495 if !current.in_proximity {
496 batch.push((
497 id,
498 generation,
499 Delivery::Exit(ViewportProximityExit {
500 previous: previous.unwrap(),
501 current,
502 }),
503 ));
504 }
505 }
506 for (id, generation, delivery) in batch {
509 if !self
510 .viewport_proximity
511 .borrow()
512 .registrations
513 .get(&id)
514 .is_some_and(|registration| registration.generation == generation)
515 {
516 continue;
517 }
518 let Some(node) = self.get_expanded_node_by_eid(ExpandedNodeIdentifier(id)) else {
519 continue;
520 };
521 if node.suspended.get() {
522 continue;
523 }
524 let key = delivery.key();
525 if !has_handlers(&node, key) {
526 continue;
527 }
528 self.viewport_proximity.borrow_mut().stats.events += 1;
529 match delivery {
530 Delivery::Enter(args) => {
531 node.dispatch_viewport_proximity_enter(Event::new(args), self)
532 }
533 Delivery::Change(args) => {
534 node.dispatch_viewport_proximity_change(Event::new(args), self)
535 }
536 Delivery::Exit(args) => {
537 node.dispatch_viewport_proximity_exit(Event::new(args), self)
538 }
539 }
540 }
541 }
542
543 fn viewport_observation_rect(&self) -> Rect {
544 if self.get_root_scroller_id().is_some() {
545 if let Some(visual) = self.get_visual_viewport_state() {
546 let x = visual.page_scroll_x + visual.offset_x;
547 let y = visual.page_scroll_y + visual.offset_y;
548 return Rect::new(x, y, x + visual.width, y + visual.height);
549 }
550 }
551 let (width, height) = self.globals().viewport.get().bounds;
552 Rect::new(0.0, 0.0, width, height)
553 }
554
555 fn query_viewport_domain(
556 &self,
557 owner: Option<u32>,
558 viewport: Rect,
559 frames: &mut HashMap<u32, ViewportFrame>,
560 targets: &mut BTreeSet<u32>,
561 ) {
562 let (region, transform) = if let Some(owner) = owner {
563 let Some(frame) = self.viewport_frame(owner, viewport, frames) else {
564 return;
565 };
566 let Some(node) = self.get_expanded_node_by_eid(ExpandedNodeIdentifier(owner)) else {
567 return;
568 };
569 (
570 frame.child_near,
571 frame.child_scroll * self.canvas_geometry_for_node(&node).world_transform,
572 )
573 } else {
574 (
575 Some(viewport.inflate(viewport.width(), viewport.height())),
576 Affine::IDENTITY,
577 )
578 };
579 let Some(region) = region else {
580 return;
581 };
582 let query = transform.inverse().transform_rect_bbox(region);
583 let mut candidates = {
584 let mut state = self.viewport_proximity.borrow_mut();
585 let Some(index) = state.indices.get(&owner) else {
586 return;
587 };
588 let mut candidates = HashSet::new();
589 index.candidates(query, &mut candidates);
590 state.stats.domain_queries += 1;
591 state.stats.candidates += candidates.len() as u64;
592 candidates
593 };
594 if owner.is_none() && self.get_visual_viewport_state().is_some() {
597 if let Some(root) = self.get_root_scroller_id() {
598 if self.viewport_dependency(root) {
599 candidates.insert(root);
600 }
601 }
602 }
603 for id in candidates {
604 let (target, scrolls) = {
605 let state = self.viewport_proximity.borrow();
606 (
607 state.registrations.contains_key(&id),
608 state.dependencies.get(&id).is_some_and(|d| d.scrolls),
609 )
610 };
611 if target {
612 targets.insert(id);
613 }
614 if scrolls {
615 self.query_viewport_domain(Some(id), viewport, frames, targets);
616 }
617 }
618 }
619
620 fn viewport_frame(
621 &self,
622 id: u32,
623 viewport: Rect,
624 frames: &mut HashMap<u32, ViewportFrame>,
625 ) -> Option<ViewportFrame> {
626 if let Some(frame) = frames.get(&id) {
627 return Some(*frame);
628 }
629 let (parent, scrolls, escapes) = {
630 let state = self.viewport_proximity.borrow();
631 let dependency = state.dependencies.get(&id)?;
632 (dependency.parent, dependency.scrolls, dependency.escapes)
633 };
634 let parent = parent.and_then(|parent| self.viewport_frame(parent, viewport, frames));
635 let inherited_scroll = parent.map_or(Affine::IDENTITY, |p| p.child_scroll);
636 let root_near = Some(viewport.inflate(viewport.width(), viewport.height()));
637 let visible = if escapes {
638 Some(viewport)
639 } else {
640 parent.map_or(Some(viewport), |p| p.child_visible)
641 };
642 let near = if escapes {
643 root_near
644 } else {
645 parent.map_or(root_near, |p| p.child_near)
646 };
647 let node = self.get_expanded_node_by_eid(ExpandedNodeIdentifier(id))?;
648 let geometry = self.canvas_geometry_for_node(&node);
649 let mut child_visible = visible;
650 let mut child_near = near;
651 let instance = node.instance_node.borrow();
652 let page_root =
653 self.get_root_scroller_id() == Some(id) && self.get_visual_viewport_state().is_some();
654 let clip = instance
655 .resolve_effect_clip_path(&node)
656 .map(|path| (inherited_scroll * path).bounding_box());
657 if scrolls && instance.clips_content(&node) {
658 let bounds = if page_root {
659 viewport
660 } else {
661 (inherited_scroll * geometry.world_transform).transform_rect_bbox(Rect::new(
662 0.0,
663 0.0,
664 geometry.bounds.0,
665 geometry.bounds.1,
666 ))
667 };
668 child_visible = intersect(child_visible, Some(bounds));
669 child_near = intersect(
670 child_near,
671 Some(bounds.inflate(viewport.width(), viewport.height())),
672 );
673 }
674 if let Some(clip) = clip.filter(|_| !page_root) {
675 child_visible = intersect(child_visible, Some(clip));
676 child_near = intersect(
677 child_near,
678 Some(if scrolls {
679 clip.inflate(viewport.width(), viewport.height())
680 } else {
681 clip
682 }),
683 );
684 }
685 drop(instance);
686 let child_scroll = if scrolls {
687 inherited_scroll * self.scroller_content_presentation_transform(&node)
688 } else {
689 inherited_scroll
690 };
691 let frame = ViewportFrame {
692 inherited_scroll,
693 child_scroll,
694 visible,
695 near,
696 child_visible,
697 child_near,
698 };
699 frames.insert(id, frame);
700 Some(frame)
701 }
702}
703
704#[derive(Clone, Copy)]
705struct ViewportFrame {
706 inherited_scroll: Affine,
707 child_scroll: Affine,
708 visible: Option<Rect>,
709 near: Option<Rect>,
710 child_visible: Option<Rect>,
711 child_near: Option<Rect>,
712}
713
714fn intersect(a: Option<Rect>, b: Option<Rect>) -> Option<Rect> {
715 let rect = a?.intersect(b?);
716 (rect.width() > 0.0
717 && rect.height() > 0.0
718 && [rect.x0, rect.y0, rect.x1, rect.y1]
719 .iter()
720 .all(|x| x.is_finite()))
721 .then_some(rect)
722}
723
724enum Delivery {
725 Enter(ViewportProximityEnter),
726 Change(ViewportProximityChange),
727 Exit(ViewportProximityExit),
728}
729impl Delivery {
730 fn key(&self) -> &'static str {
731 match self {
732 Self::Enter(_) => VIEWPORT_PROXIMITY_ENTER_HANDLERS,
733 Self::Change(_) => VIEWPORT_PROXIMITY_CHANGE_HANDLERS,
734 Self::Exit(_) => VIEWPORT_PROXIMITY_EXIT_HANDLERS,
735 }
736 }
737}