ailloli_ui_runtime/app/runtime_handle.rs
1//! Shared UI-thread runtime state and tree-scoped orchestration handles.
2
3use super::diagnostics::InvalidationDiagnostics;
4use super::external_url::{ExternalUrl, ExternalUrlOpener, MemoryExternalUrlOpener, OpenUrlError};
5use super::state_store::StateStore;
6use super::{
7 FrameWorkPlan, Invalidation, InvalidationDiagnosticsSnapshot, InvalidationSource, UiWake,
8};
9use crate::app::{PresentationGeneration, PresentationWorkPlan};
10use crate::component::reactive::{
11 MountGeneration, ReactiveConsumer, ReactiveDependencyBatchResult, ReactiveDependencyGraph,
12 ReactiveDependencyUpdate, ReactiveReadSet, ReactiveRuntimeDiagnostics,
13 ReactiveRuntimeDiagnosticsSnapshot, ReactiveStage,
14};
15use crate::popup::{
16 ElementTreeId, PopupContent, PopupDismissReason, PopupId, PopupIntent, PopupPlacementSpec,
17 PopupPortal, PopupPortalError, PopupPortalOutcome, PopupRequest,
18};
19use ailloli_ui_core::geometry::{Point, Rect};
20use ailloli_ui_core::ids::{ElementId, LogicalWindowId};
21use std::cell::RefCell;
22use std::collections::{BTreeMap, HashMap, HashSet};
23use std::fmt;
24use std::rc::{Rc, Weak};
25use std::sync::Arc;
26use std::time::{Duration, Instant};
27
28/// Provider-neutral UTF-8 clipboard access.
29///
30/// Implementations may bridge a platform clipboard or use [`MemoryClipboard`]
31/// for deterministic/headless work. The trait is UI-thread-local (`Send` and
32/// `Sync` are not required). `None` means text is unavailable, which is distinct
33/// from `Some("")`; write errors are provider-defined displayable strings.
34///
35/// # Examples
36///
37/// ```
38/// use ailloli_ui_runtime::app::{ClipboardProvider, MemoryClipboard};
39/// let clipboard = MemoryClipboard::new();
40/// clipboard.write_text("copied")?;
41/// assert_eq!(clipboard.read_text().as_deref(), Some("copied"));
42/// # Ok::<(), String>(())
43/// ```
44pub trait ClipboardProvider {
45 /// Reads current UTF-8 text, or `None` when no text can be supplied.
46 ///
47 /// # Examples
48 ///
49 /// ```
50 /// use ailloli_ui_runtime::app::{ClipboardProvider, MemoryClipboard};
51 /// assert_eq!(MemoryClipboard::new().read_text(), Some(String::new()));
52 /// ```
53 fn read_text(&self) -> Option<String>;
54
55 /// Replaces clipboard text or returns a provider-defined failure message.
56 ///
57 /// Empty text is a valid value and is not equivalent to clearing provider
58 /// availability.
59 ///
60 /// # Errors
61 ///
62 /// Returns a provider-defined display string when the backend rejects or
63 /// cannot complete the write.
64 ///
65 /// # Examples
66 ///
67 /// ```
68 /// use ailloli_ui_runtime::app::{ClipboardProvider, MemoryClipboard};
69 /// let clipboard = MemoryClipboard::new();
70 /// clipboard.write_text("")?;
71 /// assert_eq!(clipboard.read_text().as_deref(), Some(""));
72 /// # Ok::<(), String>(())
73 /// ```
74 fn write_text(&self, text: &str) -> Result<(), String>;
75}
76
77/// In-memory clipboard for tests and headless runs.
78///
79/// Cloning is intentionally unsupported; share it behind an `Rc` when needed.
80/// Reads and writes use a `RefCell`, so reentrant conflicting access panics.
81/// New/default instances contain available empty text (`Some("")`).
82///
83/// # Examples
84///
85/// ```
86/// use ailloli_ui_runtime::app::{ClipboardProvider, MemoryClipboard};
87/// let clipboard = MemoryClipboard::default();
88/// assert_eq!(clipboard.read_text().unwrap(), "");
89/// ```
90#[derive(Default)]
91pub struct MemoryClipboard {
92 /// Current available UTF-8 contents.
93 text: RefCell<String>,
94}
95
96/// Constructors for the deterministic memory clipboard.
97impl MemoryClipboard {
98 /// Creates an available clipboard containing the empty string.
99 ///
100 /// # Examples
101 ///
102 /// ```
103 /// use ailloli_ui_runtime::app::{ClipboardProvider, MemoryClipboard};
104 /// assert_eq!(MemoryClipboard::new().read_text().as_deref(), Some(""));
105 /// ```
106 pub fn new() -> Self {
107 Self::default()
108 }
109}
110
111/// Provides infallible, UI-thread-local text storage.
112impl ClipboardProvider for MemoryClipboard {
113 /// Clones and returns the currently stored string as available text.
114 fn read_text(&self) -> Option<String> {
115 Some(self.text.borrow().clone())
116 }
117
118 /// Replaces stored text and always succeeds unless a conflicting borrow panics.
119 ///
120 /// # Errors
121 ///
122 /// This in-memory implementation currently never returns `Err`.
123 ///
124 /// # Panics
125 ///
126 /// Panics on a conflicting reentrant borrow of the clipboard text slot.
127 fn write_text(&self, text: &str) -> Result<(), String> {
128 *self.text.borrow_mut() = text.to_string();
129 Ok(())
130 }
131}
132
133/// Title-bar chrome operation queued for a logical window.
134///
135/// The runtime records requests but does not inspect native window state, so
136/// maximize is expressed as a toggle rather than an absolute value.
137///
138/// # Examples
139///
140/// ```
141/// use ailloli_ui_runtime::app::WindowChromeOp;
142/// assert_ne!(WindowChromeOp::Minimize, WindowChromeOp::ToggleMaximize);
143/// ```
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum WindowChromeOp {
146 /// Ask the host to minimize the addressed native window.
147 Minimize,
148 /// Ask the host to invert the addressed window's maximized state.
149 ToggleMaximize,
150}
151
152/// Cloneable handle to shared, UI-thread-local runtime state.
153///
154/// Clones share actions, tree-scoped dirty work, timers, clipboard/provider
155/// hooks, popup authority, and presentation queues. The `Rc<RefCell<_>>`
156/// backing means this type is neither `Send` nor `Sync`; ordinary APIs can panic
157/// on conflicting reentrant borrows. A standalone handle uses tree namespace
158/// zero until [`Runtime`](super::Runtime) allocates scoped handles.
159///
160/// # Examples
161///
162/// ```
163/// use ailloli_ui_runtime::app::RuntimeHandle;
164/// let handle = RuntimeHandle::<String>::new();
165/// let clone = handle.clone();
166/// clone.dispatch("save".into());
167/// assert_eq!(handle.take_actions(), ["save"]);
168/// ```
169pub struct RuntimeHandle<A> {
170 /// Shared mutable runtime storage.
171 pub(crate) inner: Rc<RefCell<RuntimeInner<A>>>,
172 /// Namespace applied to retained-tree-specific queues and state.
173 element_tree_id: ElementTreeId,
174}
175
176/// RAII lifetime for one UI-thread background-service registration.
177///
178/// Dropping the guard unregisters its tree-scoped weak callback when the
179/// runtime still exists. Keeping the guard alone does not keep either the
180/// callback or runtime alive.
181///
182/// # Examples
183///
184/// ```
185/// use std::rc::Rc;
186/// use ailloli_ui_runtime::app::RuntimeHandle;
187/// let runtime = RuntimeHandle::<()>::new();
188/// let callback: Rc<dyn Fn() -> bool> = Rc::new(|| true);
189/// let registration = runtime.register_ui_service(&callback);
190/// assert!(format!("{registration:?}").contains("UiServiceRegistration"));
191/// ```
192pub struct UiServiceRegistration<A> {
193 /// Wrapping registration ID within the shared runtime.
194 id: u64,
195 /// Tree namespace owning this registration.
196 element_tree_id: ElementTreeId,
197 /// Weak shared runtime used for best-effort cleanup on drop.
198 runtime: Weak<RefCell<RuntimeInner<A>>>,
199}
200
201/// Omits callback/runtime internals while exposing diagnostic IDs.
202impl<A> fmt::Debug for UiServiceRegistration<A> {
203 /// Formats registration/tree IDs while deliberately omitting runtime state.
204 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
205 formatter
206 .debug_struct("UiServiceRegistration")
207 .field("id", &self.id)
208 .field("element_tree_id", &self.element_tree_id)
209 .finish_non_exhaustive()
210 }
211}
212
213/// Unregisters the service if its runtime is still alive.
214impl<A> Drop for UiServiceRegistration<A> {
215 /// Performs best-effort weak-runtime unregistration.
216 fn drop(&mut self) {
217 if let Some(runtime) = self.runtime.upgrade() {
218 runtime
219 .borrow_mut()
220 .ui_services
221 .remove(&(self.element_tree_id, self.id));
222 }
223 }
224}
225
226/// Clones the shared storage and preserves the current tree namespace.
227impl<A> Clone for RuntimeHandle<A> {
228 /// Clones the `Rc` and copies the tree namespace.
229 fn clone(&self) -> Self {
230 Self {
231 inner: self.inner.clone(),
232 element_tree_id: self.element_tree_id,
233 }
234 }
235}
236
237/// Runtime state and tree-scope operations.
238impl<A> RuntimeHandle<A> {
239 /// Creates isolated runtime state in compatibility tree namespace zero.
240 ///
241 /// Queues and diagnostics start empty, the clipboard contains available
242 /// empty text, the URL opener is in-memory, and no wake is installed.
243 ///
244 /// # Examples
245 ///
246 /// ```
247 /// use ailloli_ui_runtime::app::RuntimeHandle;
248 /// let runtime = RuntimeHandle::<()>::new();
249 /// assert_eq!(runtime.element_tree_id().get(), 0);
250 /// assert!(!runtime.has_dirty_elements());
251 /// ```
252 pub fn new() -> Self {
253 Self {
254 inner: Rc::new(RefCell::new(RuntimeInner::new())),
255 element_tree_id: ElementTreeId::new(0),
256 }
257 }
258
259 /// Stable namespace of the retained element tree using this handle.
260 ///
261 /// A newly-created standalone handle uses the compatibility namespace
262 /// `0`. [`Runtime::new`](super::Runtime::new) reserves a unique namespace
263 /// from the shared handle for every retained tree it creates.
264 ///
265 /// # Examples
266 ///
267 /// ```
268 /// use ailloli_ui_runtime::app::RuntimeHandle;
269 /// assert_eq!(RuntimeHandle::<()>::new().element_tree_id().get(), 0);
270 /// ```
271 pub const fn element_tree_id(&self) -> ElementTreeId {
272 self.element_tree_id
273 }
274
275 /// Records the logical presentation currently hosting this retained tree.
276 ///
277 /// Widgets that publish popup geometry during paint do not have an
278 /// [`crate::input::EventMeta`]. This tree-scoped value lets them refresh
279 /// popup ownership after suspend/resume without falling back to a synthetic
280 /// headless owner. Hosts update it before routing or painting a tree.
281 /// Replacing a scope discards pending popup intents for the same logical
282 /// window whose generation is now stale.
283 ///
284 /// # Examples
285 ///
286 /// ```
287 /// use ailloli_ui_runtime::app::{PresentationGeneration, RuntimeHandle};
288 /// let runtime = RuntimeHandle::<()>::new();
289 /// runtime.set_presentation_scope("main", PresentationGeneration::new(4));
290 /// assert_eq!(runtime.presentation_scope().unwrap().0.as_str(), "main");
291 /// ```
292 pub fn set_presentation_scope(
293 &self,
294 logical_window_id: impl Into<LogicalWindowId>,
295 presentation_generation: PresentationGeneration,
296 ) {
297 let logical_window_id = logical_window_id.into();
298 let mut inner = self.inner.borrow_mut();
299 inner.pending_popup_intents.retain(|record| {
300 record.owner.element_tree_id() != self.element_tree_id
301 || record.owner.logical_window_id() != &logical_window_id
302 || record.owner.presentation_generation() == presentation_generation
303 });
304 inner.presentation_scopes.insert(
305 self.element_tree_id,
306 (logical_window_id, presentation_generation),
307 );
308 }
309
310 /// Returns the last presentation scope installed for this retained tree.
311 ///
312 /// `None` means no scope was installed or it was cleared/released. The
313 /// logical-window string is cloned into the returned tuple.
314 ///
315 /// # Examples
316 ///
317 /// ```
318 /// use ailloli_ui_runtime::app::RuntimeHandle;
319 /// assert!(RuntimeHandle::<()>::new().presentation_scope().is_none());
320 /// ```
321 pub fn presentation_scope(&self) -> Option<(LogicalWindowId, PresentationGeneration)> {
322 self.inner
323 .borrow()
324 .presentation_scopes
325 .get(&self.element_tree_id)
326 .cloned()
327 }
328
329 /// Removes the host presentation associated with this retained tree.
330 ///
331 /// Matching pending popup intents for that logical window/tree are also
332 /// removed. Calling it without a scope is a no-op.
333 ///
334 /// # Examples
335 ///
336 /// ```
337 /// use ailloli_ui_runtime::app::{PresentationGeneration, RuntimeHandle};
338 /// let runtime = RuntimeHandle::<()>::new();
339 /// runtime.set_presentation_scope("main", PresentationGeneration::INITIAL);
340 /// runtime.clear_presentation_scope();
341 /// assert!(runtime.presentation_scope().is_none());
342 /// ```
343 pub fn clear_presentation_scope(&self) {
344 let mut inner = self.inner.borrow_mut();
345 let removed = inner.presentation_scopes.remove(&self.element_tree_id);
346 if let Some((logical_window_id, _)) = removed {
347 inner.pending_popup_intents.retain(|record| {
348 record.owner.element_tree_id() != self.element_tree_id
349 || record.owner.logical_window_id() != &logical_window_id
350 });
351 }
352 }
353
354 /// Allocates the next wrapping-free `u64` tree namespace on shared state.
355 ///
356 /// Allocation starts at zero; this crate-visible operation is normally
357 /// reached through [`Runtime::new`](super::Runtime::new).
358 ///
359 /// # Panics
360 ///
361 /// Panics when the namespace counter is already `u64::MAX`.
362 ///
363 /// # Examples
364 ///
365 /// ```
366 /// use ailloli_ui_runtime::app::{Runtime, RuntimeHandle};
367 /// let shared = RuntimeHandle::<()>::new();
368 /// let first = Runtime::new(shared.clone());
369 /// let second = Runtime::new(shared);
370 /// assert_eq!((first.runtime.element_tree_id().get(), second.runtime.element_tree_id().get()), (0, 1));
371 /// ```
372 pub(crate) fn allocate_element_tree_scope(&self) -> Self {
373 let element_tree_id = {
374 let mut inner = self.inner.borrow_mut();
375 let next = inner.next_element_tree_id;
376 inner.next_element_tree_id = next
377 .checked_add(1)
378 .expect("element tree identifier space exhausted");
379 ElementTreeId::new(next)
380 };
381 Self {
382 inner: Rc::clone(&self.inner),
383 element_tree_id,
384 }
385 }
386
387 /// Releases all UI-local data owned by this retained element-tree
388 /// namespace.
389 ///
390 /// The operation is idempotent and deliberately leaves sibling tree
391 /// namespaces intact. [`Runtime`](super::Runtime) calls it from `Drop`, so
392 /// host-owned popup mounts cannot leave state slots, timers, focus
393 /// requests, popup registrations, or presentation effects behind.
394 ///
395 /// # Examples
396 ///
397 /// ```
398 /// use ailloli_ui_runtime::app::{Runtime, RuntimeHandle};
399 /// let shared = RuntimeHandle::<()>::new();
400 /// let runtime = Runtime::new(shared.clone());
401 /// drop(runtime); // releases only the allocated tree scope
402 /// assert!(shared.take_actions().is_empty());
403 /// ```
404 pub(crate) fn release_element_tree_scope(&self) {
405 let element_tree_id = self.element_tree_id;
406 let (states, popup_portal) = {
407 let inner = self.inner.borrow();
408 (Rc::clone(&inner.states), Rc::clone(&inner.popup_portal))
409 };
410 let removed_popup_ids: HashSet<PopupId> = popup_portal
411 .borrow_mut()
412 .release_element_tree(element_tree_id)
413 .into_iter()
414 .collect();
415
416 states.borrow_mut().remove_tree_scoped(element_tree_id);
417
418 let mut inner = self.inner.borrow_mut();
419 inner.reactive_dependencies.remove_tree(element_tree_id);
420 inner
421 .mounted_elements
422 .retain(|(tree_id, _), _| *tree_id != element_tree_id);
423 inner.dirty_elements.remove(&element_tree_id);
424 inner
425 .generation_bound_invalidations
426 .remove(&element_tree_id);
427 inner
428 .ui_services
429 .retain(|(tree_id, _), _| *tree_id != element_tree_id);
430 inner
431 .scheduled_repaints
432 .retain(|scheduled| scheduled.element_tree_id != element_tree_id);
433 inner.pending_focus_keys.remove(&element_tree_id);
434 inner.presentation_scopes.remove(&element_tree_id);
435 inner
436 .popup_ids_by_element
437 .retain(|(tree_id, _), _| *tree_id != element_tree_id);
438 inner.popup_intents.retain(|intent| {
439 !popup_intent_belongs_to_released_tree(intent, element_tree_id, &removed_popup_ids)
440 });
441 inner.pending_popup_intents.retain(|record| {
442 record.owner.element_tree_id() != element_tree_id
443 && record
444 .popup_id()
445 .is_none_or(|popup_id| !removed_popup_ids.contains(&popup_id))
446 });
447 }
448
449 /// Appends an application action to the shared FIFO action queue.
450 ///
451 /// Dispatch does not wake a host or request a frame by itself.
452 ///
453 /// # Examples
454 ///
455 /// ```
456 /// use ailloli_ui_runtime::app::RuntimeHandle;
457 /// let runtime = RuntimeHandle::new();
458 /// runtime.dispatch(1);
459 /// runtime.dispatch(2);
460 /// assert_eq!(runtime.take_actions(), [1, 2]);
461 /// ```
462 pub fn dispatch(&self, action: A) {
463 self.inner.borrow_mut().actions.push(action);
464 }
465
466 /// Requests the smallest retained unit of work required by `element_id`.
467 /// Repeated requests are coalesced to the strongest invalidation.
468 /// Requests are isolated by this handle's tree namespace and recorded with
469 /// [`InvalidationSource::Runtime`] provenance.
470 ///
471 /// # Examples
472 ///
473 /// ```
474 /// use ailloli_ui_core::ElementId;
475 /// use ailloli_ui_runtime::app::{Invalidation, RuntimeHandle};
476 /// let runtime = RuntimeHandle::<()>::new();
477 /// runtime.invalidate(ElementId(2), Invalidation::Paint);
478 /// runtime.invalidate(ElementId(2), Invalidation::Build);
479 /// assert!(runtime.frame_work_plan().needs_build());
480 /// ```
481 pub fn invalidate(&self, element_id: ElementId, invalidation: Invalidation) {
482 self.invalidate_from(element_id, invalidation, InvalidationSource::Runtime);
483 }
484
485 /// Requests retained work while preserving its diagnostic provenance.
486 ///
487 /// Repeated requests for the same tree/element merge to the strongest
488 /// invalidation, while every request still increments bounded diagnostics.
489 ///
490 /// # Examples
491 ///
492 /// ```
493 /// use ailloli_ui_core::ElementId;
494 /// use ailloli_ui_runtime::app::{Invalidation, InvalidationSource, RuntimeHandle};
495 /// let runtime = RuntimeHandle::<()>::new();
496 /// runtime.invalidate_from(ElementId(3), Invalidation::Layout, InvalidationSource::Host);
497 /// assert_eq!(runtime.invalidation_diagnostics().records[0].source(), InvalidationSource::Host);
498 /// ```
499 pub fn invalidate_from(
500 &self,
501 element_id: ElementId,
502 invalidation: Invalidation,
503 source: InvalidationSource,
504 ) {
505 let mut inner = self.inner.borrow_mut();
506 let generation_bound_pending = Self::generation_bound_invalidation_for_element(
507 &inner,
508 self.element_tree_id,
509 element_id,
510 )
511 .is_some();
512 let coalesced = {
513 let pending = inner
514 .dirty_elements
515 .entry(self.element_tree_id)
516 .or_default();
517 let coalesced = generation_bound_pending || pending.contains_key(&element_id);
518 pending
519 .entry(element_id)
520 .and_modify(|current| *current = current.merge(invalidation))
521 .or_insert(invalidation);
522 coalesced
523 };
524 inner.invalidation_diagnostics.record(
525 self.element_tree_id,
526 element_id,
527 invalidation,
528 source,
529 coalesced,
530 );
531 }
532
533 /// Queues one source-driven invalidation for an exact mounted generation.
534 ///
535 /// Generation-bound work remains separate from host/runtime work until the
536 /// frame drain. Replacing or removing the mount can therefore discard only
537 /// stale source notifications without losing an unrelated request for the
538 /// same stable element ID.
539 fn invalidate_generation_bound(
540 &self,
541 element_id: ElementId,
542 mount_generation: MountGeneration,
543 invalidation: Invalidation,
544 source: InvalidationSource,
545 count_consumer_notification: bool,
546 ) {
547 let mut inner = self.inner.borrow_mut();
548 if inner
549 .mounted_elements
550 .get(&(self.element_tree_id, element_id))
551 != Some(&mount_generation)
552 {
553 return;
554 }
555
556 if count_consumer_notification {
557 inner.reactive_runtime_diagnostics.consumer_notification();
558 }
559 let already_pending = inner
560 .dirty_elements
561 .get(&self.element_tree_id)
562 .is_some_and(|pending| pending.contains_key(&element_id));
563 let pending = inner
564 .generation_bound_invalidations
565 .entry(self.element_tree_id)
566 .or_default();
567 let coalesced = already_pending || pending.contains_key(&element_id);
568 pending
569 .entry(element_id)
570 .and_modify(|current| {
571 debug_assert_eq!(current.mount_generation, mount_generation);
572 if current.mount_generation == mount_generation {
573 current.invalidation = current.invalidation.merge(invalidation);
574 } else {
575 *current = GenerationBoundInvalidation {
576 mount_generation,
577 invalidation,
578 };
579 }
580 })
581 .or_insert(GenerationBoundInvalidation {
582 mount_generation,
583 invalidation,
584 });
585 inner.invalidation_diagnostics.record(
586 self.element_tree_id,
587 element_id,
588 invalidation,
589 source,
590 coalesced,
591 );
592 }
593
594 /// Removes only generation-bound work for one stable element identity.
595 fn remove_generation_bound_invalidation(
596 inner: &mut RuntimeInner<A>,
597 element_tree_id: ElementTreeId,
598 element_id: ElementId,
599 ) {
600 let remove_tree_entry = inner
601 .generation_bound_invalidations
602 .get_mut(&element_tree_id)
603 .is_some_and(|pending| {
604 pending.remove(&element_id);
605 pending.is_empty()
606 });
607 if remove_tree_entry {
608 inner
609 .generation_bound_invalidations
610 .remove(&element_tree_id);
611 }
612 }
613
614 /// Iterates current generation-bound work for one retained tree.
615 fn generation_bound_invalidations_for_tree(
616 inner: &RuntimeInner<A>,
617 element_tree_id: ElementTreeId,
618 ) -> impl Iterator<Item = (ElementId, Invalidation)> + '_ {
619 inner
620 .generation_bound_invalidations
621 .get(&element_tree_id)
622 .into_iter()
623 .flat_map(HashMap::iter)
624 .filter_map(move |(element_id, pending)| {
625 (inner.mounted_elements.get(&(element_tree_id, *element_id))
626 == Some(&pending.mount_generation))
627 .then_some((*element_id, pending.invalidation))
628 })
629 }
630
631 /// Returns current generation-bound work for one exact retained element.
632 fn generation_bound_invalidation_for_element(
633 inner: &RuntimeInner<A>,
634 element_tree_id: ElementTreeId,
635 element_id: ElementId,
636 ) -> Option<Invalidation> {
637 let pending = inner
638 .generation_bound_invalidations
639 .get(&element_tree_id)?
640 .get(&element_id)?;
641 (inner.mounted_elements.get(&(element_tree_id, element_id))
642 == Some(&pending.mount_generation))
643 .then_some(pending.invalidation)
644 }
645
646 /// Returns an owned snapshot of global invalidation diagnostics.
647 ///
648 /// Counters cover every tree sharing the handle, not only this namespace.
649 /// Aggregate counters saturate and the provenance ring is bounded.
650 ///
651 /// # Examples
652 ///
653 /// ```
654 /// use ailloli_ui_runtime::app::RuntimeHandle;
655 /// assert_eq!(RuntimeHandle::<()>::new().invalidation_diagnostics().requests, 0);
656 /// ```
657 pub fn invalidation_diagnostics(&self) -> InvalidationDiagnosticsSnapshot {
658 self.inner.borrow().invalidation_diagnostics.snapshot()
659 }
660
661 /// Compatibility alias for the historical rebuild invalidation.
662 #[deprecated(note = "use invalidate(element_id, Invalidation::Build)")]
663 ///
664 /// # Examples
665 ///
666 /// ```
667 /// #![allow(deprecated)]
668 /// use ailloli_ui_core::ElementId;
669 /// use ailloli_ui_runtime::app::RuntimeHandle;
670 /// let runtime = RuntimeHandle::<()>::new();
671 /// runtime.mark_dirty(ElementId(1));
672 /// assert!(runtime.frame_work_plan().needs_build());
673 /// ```
674 pub fn mark_dirty(&self, element_id: ElementId) {
675 self.invalidate(element_id, Invalidation::Build);
676 }
677
678 /// Requests paint-only work for one tree-local element.
679 ///
680 /// # Examples
681 ///
682 /// ```
683 /// use ailloli_ui_core::ElementId;
684 /// use ailloli_ui_runtime::app::RuntimeHandle;
685 /// let runtime = RuntimeHandle::<()>::new();
686 /// runtime.request_repaint(ElementId(1));
687 /// assert!(runtime.frame_work_plan().needs_paint());
688 /// ```
689 pub fn request_repaint(&self, element_id: ElementId) {
690 self.invalidate(element_id, Invalidation::Paint);
691 }
692
693 /// Requests layout-and-paint work for one tree-local element.
694 ///
695 /// # Examples
696 ///
697 /// ```
698 /// use ailloli_ui_core::ElementId;
699 /// use ailloli_ui_runtime::app::RuntimeHandle;
700 /// let runtime = RuntimeHandle::<()>::new();
701 /// runtime.request_layout(ElementId(1));
702 /// assert!(runtime.frame_work_plan().needs_layout());
703 /// ```
704 pub fn request_layout(&self, element_id: ElementId) {
705 self.invalidate(element_id, Invalidation::Layout);
706 }
707
708 /// Requests component-build, layout, and paint work for one element.
709 ///
710 /// # Examples
711 ///
712 /// ```
713 /// use ailloli_ui_core::ElementId;
714 /// use ailloli_ui_runtime::app::RuntimeHandle;
715 /// let runtime = RuntimeHandle::<()>::new();
716 /// runtime.request_build(ElementId(1));
717 /// assert!(runtime.frame_work_plan().needs_build());
718 /// ```
719 pub fn request_build(&self, element_id: ElementId) {
720 self.invalidate(element_id, Invalidation::Build);
721 }
722
723 /// Installs or replaces the payload-free host wake shared by UI services.
724 ///
725 /// Installation does not invoke the callback. The `Arc` may be used from
726 /// background threads even though this handle itself is UI-thread-local.
727 ///
728 /// # Examples
729 ///
730 /// ```
731 /// use std::sync::Arc;
732 /// use ailloli_ui_runtime::app::{RuntimeHandle, UiWake, UiWakeError};
733 /// struct Wake;
734 /// impl UiWake for Wake { fn wake(&self) -> Result<(), UiWakeError> { Ok(()) } }
735 /// let runtime = RuntimeHandle::<()>::new();
736 /// runtime.install_ui_wake(Arc::new(Wake));
737 /// assert!(runtime.ui_wake().unwrap().wake().is_ok());
738 /// ```
739 pub fn install_ui_wake(&self, wake: Arc<dyn UiWake>) {
740 self.inner.borrow_mut().ui_wake = Some(wake);
741 }
742
743 /// Returns a clone of the installed thread-safe host wake.
744 ///
745 /// `None` means none was installed. Removing the returned `Arc` does not
746 /// uninstall the runtime's copy.
747 ///
748 /// # Examples
749 ///
750 /// ```
751 /// use ailloli_ui_runtime::app::RuntimeHandle;
752 /// assert!(RuntimeHandle::<()>::new().ui_wake().is_none());
753 /// ```
754 pub fn ui_wake(&self) -> Option<Arc<dyn UiWake>> {
755 self.inner.borrow().ui_wake.clone()
756 }
757
758 /// Registers low-level UI-thread work after a payload-free host wake.
759 /// The registry owns only a weak target; the returned RAII guard and the
760 /// caller own the callback lifetime. The boolean result is observational:
761 /// this low-level registration does not infer an owner or enqueue retained
762 /// work. Component code should prefer
763 /// [`crate::component::Context::register_ui_service`], whose changed result
764 /// requests a build of that exact component owner.
765 /// IDs start at one and wrap on overflow; the `(tree, id)` key means a
766 /// theoretical wrap can replace an older still-live registration.
767 ///
768 /// # Examples
769 ///
770 /// ```
771 /// use std::{cell::Cell, rc::Rc};
772 /// use ailloli_ui_runtime::app::RuntimeHandle;
773 /// let runtime = RuntimeHandle::<()>::new();
774 /// let calls = Rc::new(Cell::new(0));
775 /// let seen = calls.clone();
776 /// let service: Rc<dyn Fn() -> bool> = Rc::new(move || { seen.set(seen.get() + 1); true });
777 /// let _registration = runtime.register_ui_service(&service);
778 /// assert!(runtime.service_ui_sources());
779 /// assert_eq!(calls.get(), 1);
780 /// ```
781 pub fn register_ui_service(&self, service: &Rc<dyn Fn() -> bool>) -> UiServiceRegistration<A> {
782 self.register_ui_service_entry(service, None)
783 }
784
785 /// Registers a component-owned UI service using the current mount generation.
786 pub(crate) fn register_owned_ui_service(
787 &self,
788 element_id: ElementId,
789 service: &Rc<dyn Fn() -> bool>,
790 ) -> UiServiceRegistration<A> {
791 let mount_generation = self
792 .inner
793 .borrow()
794 .mounted_elements
795 .get(&(self.element_tree_id, element_id))
796 .copied();
797 self.register_ui_service_entry(
798 service,
799 Some(UiServiceOwner {
800 element_id,
801 mount_generation,
802 }),
803 )
804 }
805
806 /// Installs one weak callback with optional retained-component ownership.
807 fn register_ui_service_entry(
808 &self,
809 service: &Rc<dyn Fn() -> bool>,
810 owner: Option<UiServiceOwner>,
811 ) -> UiServiceRegistration<A> {
812 let id = {
813 let mut inner = self.inner.borrow_mut();
814 let id = inner.next_ui_service_id;
815 inner.next_ui_service_id = id.wrapping_add(1);
816 inner.ui_services.insert(
817 (self.element_tree_id, id),
818 UiServiceEntry {
819 callback: Rc::downgrade(service),
820 owner,
821 },
822 );
823 id
824 };
825 UiServiceRegistration {
826 id,
827 element_tree_id: self.element_tree_id,
828 runtime: Rc::downgrade(&self.inner),
829 }
830 }
831
832 /// Services every live UI-local worker target. Callbacks run outside the
833 /// runtime borrow. A changed component-owned service requests `Build` for
834 /// its exact owner and mount generation; low-level registrations must
835 /// invalidate explicitly. Dead weak callbacks are pruned. Every live
836 /// callback runs once in unspecified hash-map order; the return value ORs
837 /// their boolean results.
838 ///
839 /// # Panics
840 ///
841 /// A callback panic propagates and later callbacks are not serviced.
842 ///
843 /// # Examples
844 ///
845 /// ```
846 /// use std::rc::Rc;
847 /// use ailloli_ui_runtime::app::RuntimeHandle;
848 /// let runtime = RuntimeHandle::<()>::new();
849 /// let service: Rc<dyn Fn() -> bool> = Rc::new(|| false);
850 /// let _registration = runtime.register_ui_service(&service);
851 /// assert!(!runtime.service_ui_sources());
852 /// ```
853 pub fn service_ui_sources(&self) -> bool {
854 let callbacks = {
855 let mut inner = self.inner.borrow_mut();
856 let callbacks = inner
857 .ui_services
858 .iter()
859 .filter_map(|((element_tree_id, _), entry)| {
860 entry
861 .callback
862 .upgrade()
863 .map(|callback| (callback, *element_tree_id, entry.owner))
864 })
865 .collect::<Vec<_>>();
866 inner
867 .ui_services
868 .retain(|_, entry| entry.callback.strong_count() != 0);
869 callbacks
870 };
871 let mut changed = false;
872 for (service, element_tree_id, owner) in callbacks {
873 let service_changed = service();
874 changed |= service_changed;
875 if !service_changed {
876 continue;
877 }
878 let Some(owner) = owner else {
879 continue;
880 };
881 let runtime = RuntimeHandle {
882 inner: Rc::clone(&self.inner),
883 element_tree_id,
884 };
885 if let Some(mount_generation) = owner.mount_generation {
886 runtime.invalidate_generation_bound(
887 owner.element_id,
888 mount_generation,
889 Invalidation::Build,
890 InvalidationSource::Context,
891 false,
892 );
893 } else {
894 runtime.invalidate_from(
895 owner.element_id,
896 Invalidation::Build,
897 InvalidationSource::Context,
898 );
899 }
900 }
901 changed
902 }
903
904 /// Builds a weak, model-provenance invalidator for a retained element.
905 ///
906 /// Invoking it after the shared runtime is dropped is a no-op. Public state
907 /// hooks use this helper so a signal does not keep the runtime alive.
908 ///
909 /// # Examples
910 ///
911 /// ```
912 /// use ailloli_ui_core::ElementId;
913 /// use ailloli_ui_runtime::app::{Invalidation, InvalidationSource, RuntimeHandle};
914 /// let runtime = RuntimeHandle::<()>::new();
915 /// // State-hook invalidators have the same externally visible queue effect.
916 /// runtime.invalidate_from(ElementId(1), Invalidation::Build, InvalidationSource::Model);
917 /// assert_eq!(runtime.invalidation_diagnostics().records[0].source(), InvalidationSource::Model);
918 /// ```
919 pub(crate) fn weak_invalidator(
920 &self,
921 element_id: ElementId,
922 invalidation: Invalidation,
923 ) -> Rc<dyn Fn()>
924 where
925 A: 'static,
926 {
927 let inner: Weak<RefCell<RuntimeInner<A>>> = Rc::downgrade(&self.inner);
928 let element_tree_id = self.element_tree_id;
929 Rc::new(move || {
930 let Some(inner) = inner.upgrade() else {
931 return;
932 };
933 let handle = RuntimeHandle {
934 inner,
935 element_tree_id,
936 };
937 handle.invalidate_from(element_id, invalidation, InvalidationSource::Model);
938 })
939 }
940
941 /// Builds a weak historical invalidator retaining signal provenance.
942 pub(crate) fn weak_signal_invalidator(
943 &self,
944 element_id: ElementId,
945 invalidation: Invalidation,
946 ) -> Rc<dyn Fn()>
947 where
948 A: 'static,
949 {
950 let inner: Weak<RefCell<RuntimeInner<A>>> = Rc::downgrade(&self.inner);
951 let element_tree_id = self.element_tree_id;
952 let mount_generation = self
953 .inner
954 .borrow()
955 .mounted_elements
956 .get(&(element_tree_id, element_id))
957 .copied();
958 Rc::new(move || {
959 let Some(inner) = inner.upgrade() else {
960 return;
961 };
962 if let Some(mount_generation) = mount_generation {
963 RuntimeHandle {
964 inner,
965 element_tree_id,
966 }
967 .invalidate_generation_bound(
968 element_id,
969 mount_generation,
970 invalidation,
971 InvalidationSource::Signal,
972 false,
973 );
974 return;
975 }
976 RuntimeHandle {
977 inner,
978 element_tree_id,
979 }
980 .invalidate_from(element_id, invalidation, InvalidationSource::Signal);
981 })
982 }
983
984 /// Registers the exact payload generation currently mounted at an element.
985 pub(crate) fn register_element_mount(
986 &self,
987 element_id: ElementId,
988 mount_generation: MountGeneration,
989 ) {
990 let mut inner = self.inner.borrow_mut();
991 let previous = inner
992 .mounted_elements
993 .insert((self.element_tree_id, element_id), mount_generation);
994 if let Some(previous) = previous.filter(|previous| *previous != mount_generation) {
995 let probes = inner.reactive_dependencies.remove_mount(
996 self.element_tree_id,
997 element_id,
998 previous,
999 );
1000 inner
1001 .reactive_runtime_diagnostics
1002 .mount_cleanup_key_probes(probes);
1003 Self::remove_generation_bound_invalidation(
1004 &mut inner,
1005 self.element_tree_id,
1006 element_id,
1007 );
1008 }
1009 }
1010
1011 /// Removes one mount and all Build/Layout/Paint dependency edges it owns.
1012 pub(crate) fn unregister_element_mount(&self, element_id: ElementId) {
1013 let mut inner = self.inner.borrow_mut();
1014 let mount_generation = inner
1015 .mounted_elements
1016 .remove(&(self.element_tree_id, element_id));
1017 if let Some(mount_generation) = mount_generation {
1018 let probes = inner.reactive_dependencies.remove_mount(
1019 self.element_tree_id,
1020 element_id,
1021 mount_generation,
1022 );
1023 inner
1024 .reactive_runtime_diagnostics
1025 .mount_cleanup_key_probes(probes);
1026 }
1027 Self::remove_generation_bound_invalidation(&mut inner, self.element_tree_id, element_id);
1028 }
1029
1030 /// Replaces one mounted consumer's direct dependency set after success.
1031 #[doc(hidden)]
1032 pub fn replace_reactive_dependencies(
1033 &self,
1034 element_id: ElementId,
1035 mount_generation: MountGeneration,
1036 stage: ReactiveStage,
1037 reads: &ReactiveReadSet,
1038 ) -> bool
1039 where
1040 A: 'static,
1041 {
1042 let consumer = ReactiveConsumer {
1043 element_tree_id: self.element_tree_id,
1044 element_id,
1045 mount_generation,
1046 stage,
1047 };
1048 let mut inner = self.inner.borrow_mut();
1049 let is_current = inner
1050 .mounted_elements
1051 .get(&(consumer.element_tree_id, consumer.element_id))
1052 == Some(&consumer.mount_generation);
1053 if !is_current {
1054 return false;
1055 }
1056
1057 let renewed = inner.reactive_dependencies.replace(consumer, reads, || {
1058 Self::reactive_consumer_invalidator_from(Rc::downgrade(&self.inner), consumer)
1059 });
1060 inner
1061 .reactive_runtime_diagnostics
1062 .publication(reads.len(), renewed);
1063 renewed
1064 }
1065
1066 /// Publishes a prevalidated batch of exact retained dependency replacements.
1067 ///
1068 /// The complete batch is rejected before mutation when any mount generation
1069 /// is stale. Applying a valid batch invokes no user callbacks. The return
1070 /// verdict distinguishes a valid no-op from an atomically rejected stale
1071 /// generation batch.
1072 #[doc(hidden)]
1073 pub fn replace_reactive_dependencies_batch(
1074 &self,
1075 updates: &[ReactiveDependencyUpdate],
1076 ) -> ReactiveDependencyBatchResult
1077 where
1078 A: 'static,
1079 {
1080 let mut inner = self.inner.borrow_mut();
1081 let all_current = updates.iter().all(|update| {
1082 inner
1083 .mounted_elements
1084 .get(&(self.element_tree_id, update.element_id))
1085 == Some(&update.mount_generation)
1086 });
1087 if !all_current {
1088 return ReactiveDependencyBatchResult::Stale;
1089 }
1090
1091 let mut changed = false;
1092 for update in updates {
1093 let consumer = ReactiveConsumer {
1094 element_tree_id: self.element_tree_id,
1095 element_id: update.element_id,
1096 mount_generation: update.mount_generation,
1097 stage: update.stage,
1098 };
1099 let renewed = inner
1100 .reactive_dependencies
1101 .replace(consumer, &update.reads, || {
1102 Self::reactive_consumer_invalidator_from(Rc::downgrade(&self.inner), consumer)
1103 });
1104 inner
1105 .reactive_runtime_diagnostics
1106 .publication(update.reads.len(), renewed);
1107 changed |= renewed;
1108 }
1109 ReactiveDependencyBatchResult::Accepted { renewed: changed }
1110 }
1111
1112 /// Removes one exact mounted callback stage from the dependency graph.
1113 #[doc(hidden)]
1114 pub fn clear_reactive_dependencies(
1115 &self,
1116 element_id: ElementId,
1117 mount_generation: MountGeneration,
1118 stage: ReactiveStage,
1119 ) -> bool {
1120 let consumer = ReactiveConsumer {
1121 element_tree_id: self.element_tree_id,
1122 element_id,
1123 mount_generation,
1124 stage,
1125 };
1126 self.inner
1127 .borrow_mut()
1128 .reactive_dependencies
1129 .remove_consumer(consumer)
1130 }
1131
1132 /// Returns the current exact-consumer count for internal diagnostics/tests.
1133 #[doc(hidden)]
1134 pub fn reactive_dependency_consumer_count(&self) -> usize {
1135 self.inner.borrow().reactive_dependencies.len()
1136 }
1137
1138 /// Returns non-resetting counters for retained reactive work.
1139 #[doc(hidden)]
1140 pub fn reactive_runtime_diagnostics(&self) -> ReactiveRuntimeDiagnosticsSnapshot {
1141 let inner = self.inner.borrow();
1142 inner
1143 .reactive_runtime_diagnostics
1144 .snapshot(inner.reactive_dependencies.len())
1145 }
1146
1147 /// Records one abandoned runtime-owned layout attempt.
1148 pub(crate) fn record_abandoned_layout_transaction(&self) {
1149 self.inner
1150 .borrow_mut()
1151 .reactive_runtime_diagnostics
1152 .abandoned_layout_transaction();
1153 }
1154
1155 /// Records unique stale units found by one paint traversal.
1156 pub(crate) fn record_stale_paint_feedback(&self, count: usize) {
1157 self.inner
1158 .borrow_mut()
1159 .reactive_runtime_diagnostics
1160 .stale_paint_feedback(count);
1161 }
1162
1163 /// Creates a weak, generation-checked invalidator for one retained consumer.
1164 fn reactive_consumer_invalidator_from(
1165 inner: Weak<RefCell<RuntimeInner<A>>>,
1166 consumer: ReactiveConsumer,
1167 ) -> Rc<dyn Fn()>
1168 where
1169 A: 'static,
1170 {
1171 Rc::new(move || {
1172 let Some(inner) = inner.upgrade() else {
1173 return;
1174 };
1175 RuntimeHandle {
1176 inner,
1177 element_tree_id: consumer.element_tree_id,
1178 }
1179 .invalidate_generation_bound(
1180 consumer.element_id,
1181 consumer.mount_generation,
1182 consumer.stage.invalidation(),
1183 InvalidationSource::Signal,
1184 true,
1185 );
1186 })
1187 }
1188
1189 /// Replaces the pending focus request for this tree with a view-key string.
1190 ///
1191 /// Empty strings are valid keys. The request is not itself an invalidation
1192 /// and remains pending until taken or the tree scope is released.
1193 ///
1194 /// # Examples
1195 ///
1196 /// ```
1197 /// use ailloli_ui_runtime::app::RuntimeHandle;
1198 /// let runtime = RuntimeHandle::<()>::new();
1199 /// runtime.request_focus_key("search");
1200 /// assert_eq!(runtime.take_focus_key_request().as_deref(), Some("search"));
1201 /// ```
1202 pub fn request_focus_key(&self, key: impl Into<String>) {
1203 self.inner
1204 .borrow_mut()
1205 .pending_focus_keys
1206 .insert(self.element_tree_id, key.into());
1207 }
1208
1209 /// Takes this tree's pending focus key, leaving no request.
1210 ///
1211 /// `None` means no request is pending; `Some("")` is a real empty key.
1212 ///
1213 /// # Examples
1214 ///
1215 /// ```
1216 /// use ailloli_ui_runtime::app::RuntimeHandle;
1217 /// let runtime = RuntimeHandle::<()>::new();
1218 /// assert_eq!(runtime.take_focus_key_request(), None);
1219 /// runtime.request_focus_key("");
1220 /// assert_eq!(runtime.take_focus_key_request(), Some(String::new()));
1221 /// ```
1222 pub fn take_focus_key_request(&self) -> Option<String> {
1223 self.inner
1224 .borrow_mut()
1225 .pending_focus_keys
1226 .remove(&self.element_tree_id)
1227 }
1228
1229 /// Schedules paint invalidation after a monotonic delay.
1230 ///
1231 /// Equal tree/element/strength timers coalesce to their earliest deadline.
1232 /// A zero delay is due immediately but must still be promoted or taken.
1233 ///
1234 /// # Examples
1235 ///
1236 /// ```
1237 /// use std::time::{Duration, Instant};
1238 /// use ailloli_ui_core::ElementId;
1239 /// use ailloli_ui_runtime::app::RuntimeHandle;
1240 /// let runtime = RuntimeHandle::<()>::new();
1241 /// runtime.request_repaint_after(ElementId(1), Duration::ZERO);
1242 /// assert_eq!(runtime.take_due_scheduled_repaints(Instant::now()), [ElementId(1)]);
1243 /// ```
1244 pub fn request_repaint_after(&self, element_id: ElementId, delay: Duration) {
1245 self.invalidate_after(element_id, Invalidation::Paint, delay);
1246 }
1247
1248 /// Schedules layout invalidation after a monotonic delay.
1249 ///
1250 /// # Examples
1251 ///
1252 /// ```
1253 /// use std::time::{Duration, Instant};
1254 /// use ailloli_ui_core::ElementId;
1255 /// use ailloli_ui_runtime::app::RuntimeHandle;
1256 /// let runtime = RuntimeHandle::<()>::new();
1257 /// runtime.request_layout_after(ElementId(2), Duration::ZERO);
1258 /// assert_eq!(runtime.promote_due_scheduled_repaints(Instant::now()), 1);
1259 /// assert!(runtime.frame_work_plan().needs_layout());
1260 /// ```
1261 pub fn request_layout_after(&self, element_id: ElementId, delay: Duration) {
1262 self.invalidate_after(element_id, Invalidation::Layout, delay);
1263 }
1264
1265 /// Schedules component-build invalidation after a monotonic delay.
1266 ///
1267 /// # Examples
1268 ///
1269 /// ```
1270 /// use std::time::{Duration, Instant};
1271 /// use ailloli_ui_core::ElementId;
1272 /// use ailloli_ui_runtime::app::RuntimeHandle;
1273 /// let runtime = RuntimeHandle::<()>::new();
1274 /// runtime.request_build_after(ElementId(3), Duration::ZERO);
1275 /// runtime.promote_due_scheduled_repaints(Instant::now());
1276 /// assert!(runtime.frame_work_plan().needs_build());
1277 /// ```
1278 pub fn request_build_after(&self, element_id: ElementId, delay: Duration) {
1279 self.invalidate_after(element_id, Invalidation::Build, delay);
1280 }
1281
1282 /// Schedules an exact invalidation strength for a monotonic deadline.
1283 ///
1284 /// The deadline is computed as `Instant::now() + delay`. Entries coalesce
1285 /// only when tree, element, and invalidation strength all match; the earlier
1286 /// deadline wins. Different strengths remain separate timers and later
1287 /// merge when promoted.
1288 ///
1289 /// # Panics
1290 ///
1291 /// `Instant` addition can panic if `delay` exceeds the platform clock range.
1292 ///
1293 /// # Examples
1294 ///
1295 /// ```
1296 /// use std::time::{Duration, Instant};
1297 /// use ailloli_ui_core::ElementId;
1298 /// use ailloli_ui_runtime::app::{Invalidation, RuntimeHandle};
1299 /// let runtime = RuntimeHandle::<()>::new();
1300 /// runtime.invalidate_after(ElementId(4), Invalidation::Paint, Duration::ZERO);
1301 /// assert_eq!(runtime.take_due_scheduled_repaints(Instant::now()), [ElementId(4)]);
1302 /// ```
1303 pub fn invalidate_after(
1304 &self,
1305 element_id: ElementId,
1306 invalidation: Invalidation,
1307 delay: Duration,
1308 ) {
1309 let due = Instant::now() + delay;
1310 let mut inner = self.inner.borrow_mut();
1311 if let Some(current_due) = inner
1312 .scheduled_repaints
1313 .iter_mut()
1314 .find(|scheduled| {
1315 scheduled.element_tree_id == self.element_tree_id
1316 && scheduled.element_id == element_id
1317 && scheduled.invalidation == invalidation
1318 })
1319 .map(|scheduled| &mut scheduled.due)
1320 {
1321 if due < *current_due {
1322 *current_due = due;
1323 }
1324 return;
1325 }
1326 inner.scheduled_repaints.push(ScheduledInvalidation {
1327 element_tree_id: self.element_tree_id,
1328 element_id,
1329 invalidation,
1330 due,
1331 });
1332 }
1333
1334 /// Removes and returns due timer element IDs for this tree only.
1335 ///
1336 /// Despite the historical name, timers of every invalidation strength are
1337 /// removed, their strengths are discarded, and no dirty work is enqueued.
1338 /// Results preserve timer-vector order and may contain the same element more
1339 /// than once when different strengths were scheduled. Other trees and
1340 /// deadlines strictly later than `now` remain queued.
1341 ///
1342 /// # Examples
1343 ///
1344 /// ```
1345 /// use std::time::{Duration, Instant};
1346 /// use ailloli_ui_core::ElementId;
1347 /// use ailloli_ui_runtime::app::{Invalidation, RuntimeHandle};
1348 /// let runtime = RuntimeHandle::<()>::new();
1349 /// runtime.invalidate_after(ElementId(1), Invalidation::Paint, Duration::ZERO);
1350 /// runtime.invalidate_after(ElementId(1), Invalidation::Layout, Duration::ZERO);
1351 /// assert_eq!(runtime.take_due_scheduled_repaints(Instant::now()), [ElementId(1), ElementId(1)]);
1352 /// assert!(!runtime.has_dirty_elements());
1353 /// ```
1354 pub fn take_due_scheduled_repaints(&self, now: Instant) -> Vec<ElementId> {
1355 let mut inner = self.inner.borrow_mut();
1356 let mut due = Vec::new();
1357 let mut pending = Vec::with_capacity(inner.scheduled_repaints.len());
1358 for scheduled in inner.scheduled_repaints.drain(..) {
1359 if scheduled.element_tree_id == self.element_tree_id && scheduled.due <= now {
1360 due.push(scheduled.element_id);
1361 } else {
1362 pending.push(scheduled);
1363 }
1364 }
1365 inner.scheduled_repaints = pending;
1366 due
1367 }
1368
1369 /// Promotes all due repaint timers to their owning tree's dirty queue.
1370 ///
1371 /// Hosts with multiple windows use this at their global scheduling
1372 /// boundary. The return value is the number of promoted timer entries,
1373 /// including entries that coalesce into already-pending work. This operation
1374 /// spans all retained trees sharing the handle and records timer provenance.
1375 ///
1376 /// # Examples
1377 ///
1378 /// ```
1379 /// use std::time::{Duration, Instant};
1380 /// use ailloli_ui_core::ElementId;
1381 /// use ailloli_ui_runtime::app::RuntimeHandle;
1382 /// let runtime = RuntimeHandle::<()>::new();
1383 /// runtime.request_repaint_after(ElementId(5), Duration::ZERO);
1384 /// assert_eq!(runtime.promote_due_scheduled_repaints(Instant::now()), 1);
1385 /// assert!(runtime.has_dirty_elements());
1386 /// ```
1387 pub fn promote_due_scheduled_repaints(&self, now: Instant) -> usize {
1388 let mut inner = self.inner.borrow_mut();
1389 let scheduled = std::mem::take(&mut inner.scheduled_repaints);
1390 let mut promoted = 0;
1391 for scheduled in scheduled {
1392 if scheduled.due <= now {
1393 let generation_bound_pending = Self::generation_bound_invalidation_for_element(
1394 &inner,
1395 scheduled.element_tree_id,
1396 scheduled.element_id,
1397 )
1398 .is_some();
1399 let pending = inner
1400 .dirty_elements
1401 .entry(scheduled.element_tree_id)
1402 .or_default();
1403 let coalesced =
1404 generation_bound_pending || pending.contains_key(&scheduled.element_id);
1405 pending
1406 .entry(scheduled.element_id)
1407 .and_modify(|current| {
1408 *current = current.merge(scheduled.invalidation);
1409 })
1410 .or_insert(scheduled.invalidation);
1411 inner.invalidation_diagnostics.record(
1412 scheduled.element_tree_id,
1413 scheduled.element_id,
1414 scheduled.invalidation,
1415 InvalidationSource::Timer,
1416 coalesced,
1417 );
1418 promoted += 1;
1419 } else {
1420 inner.scheduled_repaints.push(scheduled);
1421 }
1422 }
1423 promoted
1424 }
1425
1426 /// Returns this tree's earliest timer deadline without removing it.
1427 ///
1428 /// `None` means this tree has no scheduled invalidation of any strength.
1429 ///
1430 /// # Examples
1431 ///
1432 /// ```
1433 /// use std::time::Duration;
1434 /// use ailloli_ui_core::ElementId;
1435 /// use ailloli_ui_runtime::app::RuntimeHandle;
1436 /// let runtime = RuntimeHandle::<()>::new();
1437 /// assert!(runtime.next_scheduled_repaint_due().is_none());
1438 /// runtime.request_repaint_after(ElementId(1), Duration::from_secs(1));
1439 /// assert!(runtime.next_scheduled_repaint_due().is_some());
1440 /// ```
1441 pub fn next_scheduled_repaint_due(&self) -> Option<Instant> {
1442 self.inner
1443 .borrow()
1444 .scheduled_repaints
1445 .iter()
1446 .filter(|scheduled| scheduled.element_tree_id == self.element_tree_id)
1447 .map(|scheduled| scheduled.due)
1448 .min()
1449 }
1450
1451 /// Returns the earliest scheduled invalidation across all retained trees.
1452 ///
1453 /// `None` means the shared runtime has no timer of any invalidation strength.
1454 ///
1455 /// # Examples
1456 ///
1457 /// ```
1458 /// use std::time::Duration;
1459 /// use ailloli_ui_core::ElementId;
1460 /// use ailloli_ui_runtime::app::RuntimeHandle;
1461 /// let runtime = RuntimeHandle::<()>::new();
1462 /// runtime.request_layout_after(ElementId(1), Duration::from_millis(2));
1463 /// assert_eq!(runtime.next_scheduled_repaint_due_global(), runtime.next_scheduled_repaint_due());
1464 /// ```
1465 pub fn next_scheduled_repaint_due_global(&self) -> Option<Instant> {
1466 self.inner
1467 .borrow()
1468 .scheduled_repaints
1469 .iter()
1470 .map(|scheduled| scheduled.due)
1471 .min()
1472 }
1473
1474 /// Reports whether this tree has at least one pending invalidation.
1475 ///
1476 /// Scheduled-but-unpromoted timers do not count.
1477 ///
1478 /// # Examples
1479 ///
1480 /// ```
1481 /// use ailloli_ui_core::ElementId;
1482 /// use ailloli_ui_runtime::app::RuntimeHandle;
1483 /// let runtime = RuntimeHandle::<()>::new();
1484 /// assert!(!runtime.has_dirty_elements());
1485 /// runtime.request_repaint(ElementId(1));
1486 /// assert!(runtime.has_dirty_elements());
1487 /// ```
1488 pub fn has_dirty_elements(&self) -> bool {
1489 let inner = self.inner.borrow();
1490 inner
1491 .dirty_elements
1492 .get(&self.element_tree_id)
1493 .is_some_and(|elements| !elements.is_empty())
1494 || Self::generation_bound_invalidations_for_tree(&inner, self.element_tree_id)
1495 .next()
1496 .is_some()
1497 }
1498
1499 /// Returns the retained invalidation already queued for one exact element.
1500 ///
1501 /// This crate-private observation neither drains nor promotes work. Paint
1502 /// uses it to fail closed before callbacks can consume fresh state while a
1503 /// build or layout affecting the same retained subtree is still pending.
1504 pub(crate) fn pending_invalidation_for_element(
1505 &self,
1506 element_id: ElementId,
1507 ) -> Option<Invalidation> {
1508 let inner = self.inner.borrow();
1509 let ordinary = inner
1510 .dirty_elements
1511 .get(&self.element_tree_id)
1512 .and_then(|elements| elements.get(&element_id).copied());
1513 let generation_bound = Self::generation_bound_invalidation_for_element(
1514 &inner,
1515 self.element_tree_id,
1516 element_id,
1517 );
1518 match (ordinary, generation_bound) {
1519 (Some(ordinary), Some(generation_bound)) => Some(ordinary.merge(generation_bound)),
1520 (Some(invalidation), None) | (None, Some(invalidation)) => Some(invalidation),
1521 (None, None) => None,
1522 }
1523 }
1524
1525 /// Drains this tree's pending invalidations and returns sorted element IDs.
1526 ///
1527 /// Invalidation strengths are discarded. Each element occurs once because
1528 /// its requests were previously coalesced; ascending numeric ID order makes
1529 /// the result deterministic. Other tree namespaces are untouched.
1530 ///
1531 /// # Examples
1532 ///
1533 /// ```
1534 /// use ailloli_ui_core::ElementId;
1535 /// use ailloli_ui_runtime::app::RuntimeHandle;
1536 /// let runtime = RuntimeHandle::<()>::new();
1537 /// runtime.request_repaint(ElementId(9));
1538 /// runtime.request_layout(ElementId(2));
1539 /// assert_eq!(runtime.take_dirty_elements(), [ElementId(2), ElementId(9)]);
1540 /// assert!(!runtime.has_dirty_elements());
1541 /// ```
1542 pub fn take_dirty_elements(&self) -> Vec<ElementId> {
1543 let mut elements = self.take_invalidations().into_keys().collect::<Vec<_>>();
1544 elements.sort_by_key(|element_id| element_id.0);
1545 elements
1546 }
1547
1548 /// Returns and clears exact invalidations for this retained tree.
1549 ///
1550 /// This crate-visible form preserves each coalesced strength for
1551 /// [`Runtime::prepare_frame`](super::Runtime::prepare_frame).
1552 ///
1553 /// # Examples
1554 ///
1555 /// ```
1556 /// use ailloli_ui_core::ElementId;
1557 /// use ailloli_ui_runtime::app::{Invalidation, RuntimeHandle};
1558 /// let runtime = RuntimeHandle::<()>::new();
1559 /// runtime.invalidate(ElementId(1), Invalidation::Layout);
1560 /// // Public callers can observe the same retained strength through the plan.
1561 /// assert!(runtime.frame_work_plan().needs_layout());
1562 /// ```
1563 pub(crate) fn take_invalidations(&self) -> HashMap<ElementId, Invalidation> {
1564 let mut inner = self.inner.borrow_mut();
1565 let mut invalidations = inner
1566 .dirty_elements
1567 .remove(&self.element_tree_id)
1568 .unwrap_or_default();
1569 let generation_bound = inner
1570 .generation_bound_invalidations
1571 .remove(&self.element_tree_id)
1572 .unwrap_or_default();
1573 for (element_id, pending) in generation_bound {
1574 if inner
1575 .mounted_elements
1576 .get(&(self.element_tree_id, element_id))
1577 != Some(&pending.mount_generation)
1578 {
1579 continue;
1580 }
1581 invalidations
1582 .entry(element_id)
1583 .and_modify(|current| *current = current.merge(pending.invalidation))
1584 .or_insert(pending.invalidation);
1585 }
1586 invalidations
1587 }
1588
1589 /// Aggregate work currently pending for this tree without draining it.
1590 ///
1591 /// Empty queues return [`FrameWorkPlan::none`]. All element invalidations
1592 /// are merged to the strongest required frame stages.
1593 ///
1594 /// # Examples
1595 ///
1596 /// ```
1597 /// use ailloli_ui_core::ElementId;
1598 /// use ailloli_ui_runtime::app::RuntimeHandle;
1599 /// let runtime = RuntimeHandle::<()>::new();
1600 /// assert!(runtime.frame_work_plan().is_empty());
1601 /// runtime.request_layout(ElementId(1));
1602 /// assert!(runtime.frame_work_plan().needs_layout());
1603 /// ```
1604 pub fn frame_work_plan(&self) -> FrameWorkPlan {
1605 let inner = self.inner.borrow();
1606 let ordinary = inner
1607 .dirty_elements
1608 .get(&self.element_tree_id)
1609 .into_iter()
1610 .flat_map(|pending| pending.values().copied())
1611 .fold(FrameWorkPlan::none(), |plan, invalidation| {
1612 plan.merge(FrameWorkPlan::from_invalidation(invalidation))
1613 });
1614 Self::generation_bound_invalidations_for_tree(&inner, self.element_tree_id)
1615 .fold(ordinary, |plan, (_, invalidation)| {
1616 plan.merge(FrameWorkPlan::from_invalidation(invalidation))
1617 })
1618 }
1619
1620 /// Returns deterministic aggregate work for every dirty presentation.
1621 ///
1622 /// Trees without an installed presentation scope remain pending but are
1623 /// absent from the result. Trees sharing the same logical window and exact
1624 /// generation are merged to one plan. The method never drains retained
1625 /// invalidations and never exposes element-tree identifiers or dirty roots.
1626 ///
1627 /// This provider-neutral API is public only for host adapters and is not
1628 /// re-exported through the `ailloli_ui` facade.
1629 #[doc(hidden)]
1630 pub fn pending_presentation_work(&self) -> Vec<PresentationWorkPlan> {
1631 let inner = self.inner.borrow();
1632 let mut tree_plans = HashMap::<ElementTreeId, FrameWorkPlan>::new();
1633 for (element_tree_id, dirty_elements) in &inner.dirty_elements {
1634 let tree_plan = dirty_elements
1635 .values()
1636 .copied()
1637 .fold(FrameWorkPlan::none(), |plan, invalidation| {
1638 plan.merge(FrameWorkPlan::from_invalidation(invalidation))
1639 });
1640 if !tree_plan.is_empty() {
1641 tree_plans.insert(*element_tree_id, tree_plan);
1642 }
1643 }
1644 for element_tree_id in inner.generation_bound_invalidations.keys().copied() {
1645 let tree_plan = Self::generation_bound_invalidations_for_tree(&inner, element_tree_id)
1646 .fold(FrameWorkPlan::none(), |plan, (_, invalidation)| {
1647 plan.merge(FrameWorkPlan::from_invalidation(invalidation))
1648 });
1649 if !tree_plan.is_empty() {
1650 tree_plans
1651 .entry(element_tree_id)
1652 .and_modify(|plan| *plan = plan.merge(tree_plan))
1653 .or_insert(tree_plan);
1654 }
1655 }
1656 let mut pending =
1657 BTreeMap::<(LogicalWindowId, PresentationGeneration), FrameWorkPlan>::new();
1658
1659 for (element_tree_id, tree_plan) in tree_plans {
1660 let Some((logical_window_id, presentation_generation)) =
1661 inner.presentation_scopes.get(&element_tree_id)
1662 else {
1663 continue;
1664 };
1665 pending
1666 .entry((logical_window_id.clone(), *presentation_generation))
1667 .and_modify(|plan| *plan = plan.merge(tree_plan))
1668 .or_insert(tree_plan);
1669 }
1670
1671 pending
1672 .into_iter()
1673 .map(
1674 |((logical_window_id, presentation_generation), frame_work_plan)| {
1675 PresentationWorkPlan::new(
1676 logical_window_id,
1677 presentation_generation,
1678 frame_work_plan,
1679 )
1680 },
1681 )
1682 .collect()
1683 }
1684
1685 /// Drains and returns all shared application actions in FIFO order.
1686 ///
1687 /// The action queue is global to clones/tree scopes; an empty queue returns
1688 /// an empty vector without allocation reuse guarantees.
1689 ///
1690 /// # Examples
1691 ///
1692 /// ```
1693 /// use ailloli_ui_runtime::app::RuntimeHandle;
1694 /// let runtime = RuntimeHandle::new();
1695 /// runtime.dispatch("a");
1696 /// runtime.dispatch("b");
1697 /// assert_eq!(runtime.take_actions(), ["a", "b"]);
1698 /// assert!(runtime.take_actions().is_empty());
1699 /// ```
1700 pub fn take_actions(&self) -> Vec<A> {
1701 std::mem::take(&mut self.inner.borrow_mut().actions)
1702 }
1703
1704 /// Clones the shared retained state store handle.
1705 ///
1706 /// Tree scoping is part of each internal state key. Holding the returned
1707 /// `Rc` can keep state alive after the runtime handle is dropped; conflicting
1708 /// `RefCell` borrows panic.
1709 ///
1710 /// # Examples
1711 ///
1712 /// ```
1713 /// use std::rc::Rc;
1714 /// use ailloli_ui_runtime::app::RuntimeHandle;
1715 /// let runtime = RuntimeHandle::<()>::new();
1716 /// let states = runtime.states();
1717 /// assert!(Rc::ptr_eq(&states, &runtime.states()));
1718 /// ```
1719 pub fn states(&self) -> Rc<RefCell<StateStore>> {
1720 self.inner.borrow().states.clone()
1721 }
1722
1723 /// Replaces the shared clipboard provider without migrating its contents.
1724 ///
1725 /// # Examples
1726 ///
1727 /// ```
1728 /// use std::rc::Rc;
1729 /// use ailloli_ui_runtime::app::{MemoryClipboard, RuntimeHandle};
1730 /// let runtime = RuntimeHandle::<()>::new();
1731 /// runtime.set_clipboard_provider(Rc::new(MemoryClipboard::new()));
1732 /// assert_eq!(runtime.read_clipboard_text().as_deref(), Some(""));
1733 /// ```
1734 pub fn set_clipboard_provider(&self, provider: Rc<dyn ClipboardProvider>) {
1735 self.inner.borrow_mut().clipboard = provider;
1736 }
1737
1738 /// Reads text through the current provider.
1739 ///
1740 /// `None` and `Some("")` retain the distinction defined by
1741 /// [`ClipboardProvider::read_text`].
1742 ///
1743 /// # Examples
1744 ///
1745 /// ```
1746 /// use ailloli_ui_runtime::app::RuntimeHandle;
1747 /// assert_eq!(RuntimeHandle::<()>::new().read_clipboard_text(), Some(String::new()));
1748 /// ```
1749 pub fn read_clipboard_text(&self) -> Option<String> {
1750 self.inner.borrow().clipboard.read_text()
1751 }
1752
1753 /// Writes UTF-8 text through the current provider.
1754 ///
1755 /// Errors are returned verbatim and are not recorded by the runtime.
1756 ///
1757 /// # Errors
1758 ///
1759 /// Returns the provider-defined display string when the configured
1760 /// clipboard rejects or cannot complete the write.
1761 ///
1762 /// # Examples
1763 ///
1764 /// ```
1765 /// use ailloli_ui_runtime::app::RuntimeHandle;
1766 /// let runtime = RuntimeHandle::<()>::new();
1767 /// runtime.write_clipboard_text("hello")?;
1768 /// assert_eq!(runtime.read_clipboard_text().as_deref(), Some("hello"));
1769 /// # Ok::<(), String>(())
1770 /// ```
1771 pub fn write_clipboard_text(&self, text: &str) -> Result<(), String> {
1772 self.inner.borrow().clipboard.write_text(text)
1773 }
1774
1775 /// Replaces the shared validated-URL opener.
1776 ///
1777 /// Previously recorded errors remain queued and opener state is not
1778 /// migrated.
1779 ///
1780 /// # Examples
1781 ///
1782 /// ```
1783 /// use std::rc::Rc;
1784 /// use ailloli_ui_runtime::app::{MemoryExternalUrlOpener, RuntimeHandle};
1785 /// let runtime = RuntimeHandle::<()>::new();
1786 /// runtime.set_external_url_opener(Rc::new(MemoryExternalUrlOpener::new()));
1787 /// assert!(runtime.take_open_url_errors().is_empty());
1788 /// ```
1789 pub fn set_external_url_opener(&self, opener: Rc<dyn ExternalUrlOpener>) {
1790 self.inner.borrow_mut().external_url_opener = opener;
1791 }
1792
1793 /// Opens an already validated URL and records non-fatal provider errors.
1794 ///
1795 /// Validation must have occurred through [`ExternalUrl::parse`]. Successful
1796 /// calls are not recorded here; failures are both returned and cloned into
1797 /// the FIFO error queue.
1798 ///
1799 /// # Errors
1800 ///
1801 /// Returns the active provider's [`OpenUrlError`].
1802 ///
1803 /// # Examples
1804 ///
1805 /// ```
1806 /// use ailloli_ui_runtime::app::{ExternalUrl, RuntimeHandle};
1807 /// let runtime = RuntimeHandle::<()>::new();
1808 /// let url = ExternalUrl::parse("https://example.com")?;
1809 /// runtime.open_external_url(&url)?;
1810 /// assert!(runtime.take_open_url_errors().is_empty());
1811 /// # Ok::<(), Box<dyn std::error::Error>>(())
1812 /// ```
1813 pub fn open_external_url(&self, url: &ExternalUrl) -> Result<(), OpenUrlError> {
1814 let opener = self.inner.borrow().external_url_opener.clone();
1815 let result = opener.open(url);
1816 if let Err(error) = &result {
1817 self.inner.borrow_mut().open_url_errors.push(error.clone());
1818 }
1819 result
1820 }
1821
1822 /// Drains provider failures recorded by [`Self::open_external_url`].
1823 ///
1824 /// Errors preserve call order and successful opens add nothing.
1825 ///
1826 /// # Examples
1827 ///
1828 /// ```
1829 /// use ailloli_ui_runtime::app::RuntimeHandle;
1830 /// let runtime = RuntimeHandle::<()>::new();
1831 /// assert!(runtime.take_open_url_errors().is_empty());
1832 /// ```
1833 pub fn take_open_url_errors(&self) -> Vec<OpenUrlError> {
1834 std::mem::take(&mut self.inner.borrow_mut().open_url_errors)
1835 }
1836
1837 /// Requests application exit (like `Command::Quit` without a user action).
1838 /// The flag is idempotent and shared across all handle clones/tree scopes.
1839 ///
1840 /// # Examples
1841 ///
1842 /// ```
1843 /// use ailloli_ui_runtime::app::RuntimeHandle;
1844 /// let runtime = RuntimeHandle::<()>::new();
1845 /// runtime.request_close();
1846 /// assert!(runtime.take_close_requested());
1847 /// ```
1848 pub fn request_close(&self) {
1849 self.inner.borrow_mut().close_requested = true;
1850 }
1851
1852 /// Returns and clears the shared close-request flag.
1853 ///
1854 /// # Examples
1855 ///
1856 /// ```
1857 /// use ailloli_ui_runtime::app::RuntimeHandle;
1858 /// let runtime = RuntimeHandle::<()>::new();
1859 /// assert!(!runtime.take_close_requested());
1860 /// runtime.request_close();
1861 /// assert!(runtime.take_close_requested());
1862 /// assert!(!runtime.take_close_requested());
1863 /// ```
1864 pub fn take_close_requested(&self) -> bool {
1865 std::mem::replace(&mut self.inner.borrow_mut().close_requested, false)
1866 }
1867
1868 /// Queues a minimize request for one logical window.
1869 ///
1870 /// Empty logical IDs are stored verbatim; the runtime does not check that a
1871 /// native presentation exists or coalesce duplicate operations.
1872 ///
1873 /// # Examples
1874 ///
1875 /// ```
1876 /// use ailloli_ui_runtime::app::{RuntimeHandle, WindowChromeOp};
1877 /// let runtime = RuntimeHandle::<()>::new();
1878 /// runtime.request_window_minimize("main");
1879 /// assert_eq!(runtime.take_window_chrome_ops()[0].1, WindowChromeOp::Minimize);
1880 /// ```
1881 pub fn request_window_minimize(&self, logical_window_id: impl Into<LogicalWindowId>) {
1882 self.inner
1883 .borrow_mut()
1884 .window_chrome_ops
1885 .push((logical_window_id.into(), WindowChromeOp::Minimize));
1886 }
1887
1888 /// Queues a maximize-toggle request for one logical window.
1889 ///
1890 /// Repeated requests remain repeated and may cancel at the host depending on
1891 /// the native state observed when each is applied.
1892 ///
1893 /// # Examples
1894 ///
1895 /// ```
1896 /// use ailloli_ui_runtime::app::{RuntimeHandle, WindowChromeOp};
1897 /// let runtime = RuntimeHandle::<()>::new();
1898 /// runtime.request_window_toggle_maximize("main");
1899 /// assert_eq!(runtime.take_window_chrome_ops()[0].1, WindowChromeOp::ToggleMaximize);
1900 /// ```
1901 pub fn request_window_toggle_maximize(&self, logical_window_id: impl Into<LogicalWindowId>) {
1902 self.inner
1903 .borrow_mut()
1904 .window_chrome_ops
1905 .push((logical_window_id.into(), WindowChromeOp::ToggleMaximize));
1906 }
1907
1908 /// Drains all logical-window chrome operations in FIFO order.
1909 ///
1910 /// This queue is shared globally rather than filtered by tree namespace.
1911 ///
1912 /// # Examples
1913 ///
1914 /// ```
1915 /// use ailloli_ui_runtime::app::RuntimeHandle;
1916 /// let runtime = RuntimeHandle::<()>::new();
1917 /// runtime.request_window_minimize("one");
1918 /// runtime.request_window_toggle_maximize("two");
1919 /// let operations = runtime.take_window_chrome_ops();
1920 /// assert_eq!((operations[0].0.as_str(), operations[1].0.as_str()), ("one", "two"));
1921 /// ```
1922 pub fn take_window_chrome_ops(&self) -> Vec<(LogicalWindowId, WindowChromeOp)> {
1923 std::mem::take(&mut self.inner.borrow_mut().window_chrome_ops)
1924 }
1925
1926 /// Returns the UI-local popup authority shared by this runtime.
1927 ///
1928 /// Prefer the typed helpers below when the resulting host intents or
1929 /// errors must remain observable. Direct access is primarily intended for
1930 /// provider-neutral hosts and deterministic inspection.
1931 /// The returned `Rc<RefCell<_>>` shares authority with every handle clone;
1932 /// conflicting borrows panic and callers must remain on the UI thread.
1933 ///
1934 /// # Examples
1935 ///
1936 /// ```
1937 /// use ailloli_ui_runtime::app::RuntimeHandle;
1938 /// let runtime = RuntimeHandle::<()>::new();
1939 /// assert_eq!(runtime.popup_portal().borrow().open_ids().count(), 0);
1940 /// ```
1941 pub fn popup_portal(&self) -> Rc<RefCell<PopupPortal<A>>> {
1942 Rc::clone(&self.inner.borrow().popup_portal)
1943 }
1944
1945 /// Returns the stable popup id reserved for one retained popup owner.
1946 ///
1947 /// The mapping deliberately follows the owner element rather than a
1948 /// native presentation, so a popup keeps its identity across surface
1949 /// detach/reattach and component reconciliation.
1950 /// Equal element IDs in different tree namespaces do not alias. IDs are
1951 /// allocated lazily and a successful mapping remains until tree release.
1952 ///
1953 /// # Errors
1954 ///
1955 /// Returns and records [`PopupPortalError::IdExhausted`] if the portal's
1956 /// `u64` identifier space is exhausted.
1957 ///
1958 /// # Examples
1959 ///
1960 /// ```
1961 /// use ailloli_ui_core::ElementId;
1962 /// use ailloli_ui_runtime::app::RuntimeHandle;
1963 /// let runtime = RuntimeHandle::<()>::new();
1964 /// let first = runtime.popup_id_for_element(ElementId(8))?;
1965 /// assert_eq!(runtime.popup_id_for_element(ElementId(8))?, first);
1966 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
1967 /// ```
1968 pub fn popup_id_for_element(&self, owner: ElementId) -> Result<PopupId, PopupPortalError> {
1969 let owner_key = (self.element_tree_id, owner);
1970 if let Some(id) = self.inner.borrow().popup_ids_by_element.get(&owner_key) {
1971 return Ok(*id);
1972 }
1973
1974 let id = match self.popup_portal().borrow_mut().allocate_id() {
1975 Ok(id) => id,
1976 Err(error) => {
1977 self.record_popup_error(error.clone());
1978 return Err(error);
1979 }
1980 };
1981 self.inner
1982 .borrow_mut()
1983 .popup_ids_by_element
1984 .insert(owner_key, id);
1985 Ok(id)
1986 }
1987
1988 /// Registers a closed popup and records any portal error.
1989 ///
1990 /// The request is moved into shared portal storage. Registration does not
1991 /// open, position, build, or emit presentation intents.
1992 ///
1993 /// # Errors
1994 ///
1995 /// Propagates duplicate-ID, parent, presentation, or identifier-exhaustion
1996 /// errors from [`PopupPortal::register`] and clones them into the error queue.
1997 ///
1998 /// # Examples
1999 ///
2000 /// ```
2001 /// use ailloli_ui_core::ElementId;
2002 /// use ailloli_ui_runtime::{app::{PresentationGeneration, RuntimeHandle}, component::View, popup::{ElementTreeId, PopupContent, PopupId, PopupOwner, PopupRequest}};
2003 /// let runtime = RuntimeHandle::<()>::new();
2004 /// let request = PopupRequest::new(PopupId::new(1), PopupOwner::new("main", PresentationGeneration::INITIAL, ElementTreeId::new(0), ElementId(2)), PopupContent::new(View::empty));
2005 /// runtime.register_popup(request)?;
2006 /// assert!(!runtime.popup_is_open(PopupId::new(1)));
2007 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
2008 /// ```
2009 pub fn register_popup(&self, request: PopupRequest<A>) -> Result<(), PopupPortalError> {
2010 let result = self.popup_portal().borrow_mut().register(request);
2011 if let Err(error) = &result {
2012 self.record_popup_error(error.clone());
2013 }
2014 result
2015 }
2016
2017 /// Refreshes retained popup content for an already registered owner.
2018 ///
2019 /// Visibility, z-order, ownership, semantics, and resolved geometry remain
2020 /// unchanged. The new content factory is not called by this method.
2021 ///
2022 /// # Errors
2023 ///
2024 /// Returns and records [`PopupPortalError::UnknownPopup`] for an unregistered
2025 /// ID.
2026 ///
2027 /// # Examples
2028 ///
2029 /// ```
2030 /// use ailloli_ui_core::ElementId;
2031 /// use ailloli_ui_runtime::{app::{PresentationGeneration, RuntimeHandle}, component::View, popup::{ElementTreeId, PopupContent, PopupId, PopupOwner, PopupRequest}};
2032 /// let runtime = RuntimeHandle::<()>::new();
2033 /// let id = PopupId::new(1);
2034 /// runtime.register_popup(PopupRequest::new(id, PopupOwner::new("main", PresentationGeneration::INITIAL, ElementTreeId::new(0), ElementId(1)), PopupContent::new(View::empty)))?;
2035 /// runtime.set_popup_content(id, PopupContent::new(|| View::empty().key("updated")))?;
2036 /// assert_eq!(runtime.popup_portal().borrow().build_content(id).unwrap().key_ref(), Some("updated"));
2037 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
2038 /// ```
2039 pub fn set_popup_content(
2040 &self,
2041 popup_id: PopupId,
2042 content: PopupContent<A>,
2043 ) -> Result<(), PopupPortalError> {
2044 let result = self
2045 .popup_portal()
2046 .borrow_mut()
2047 .set_content(popup_id, content);
2048 if let Err(error) = &result {
2049 self.record_popup_error(error.clone());
2050 }
2051 result
2052 }
2053
2054 /// Updates procedural backend geometry and opens or raises a popup.
2055 ///
2056 /// `anchor` and `bounds` are global logical-pixel rectangles. Both must be
2057 /// finite with non-negative dimensions. Anchor update, bounds update, and
2058 /// open occur in order but are not transactional: a later failure can leave
2059 /// an earlier update committed. Successful lifecycle intents are recorded.
2060 ///
2061 /// # Errors
2062 ///
2063 /// Returns and records portal validation, unknown-ID, or parent-open errors.
2064 ///
2065 /// # Examples
2066 ///
2067 /// ```
2068 /// use ailloli_ui_core::{ElementId, Rect};
2069 /// use ailloli_ui_runtime::{app::{PresentationGeneration, RuntimeHandle}, component::View, popup::{ElementTreeId, PopupContent, PopupId, PopupOwner, PopupRequest}};
2070 /// let runtime = RuntimeHandle::<()>::new();
2071 /// let id = PopupId::new(1);
2072 /// runtime.register_popup(PopupRequest::new(id, PopupOwner::new("main", PresentationGeneration::INITIAL, ElementTreeId::new(0), ElementId(1)), PopupContent::new(View::empty)))?;
2073 /// assert!(runtime.open_popup(id, Rect::new(5.0, 5.0, 10.0, 4.0), Rect::new(5.0, 9.0, 40.0, 20.0))?.handled());
2074 /// assert!(runtime.popup_is_open(id));
2075 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
2076 /// ```
2077 pub fn open_popup(
2078 &self,
2079 popup_id: PopupId,
2080 anchor: Rect,
2081 bounds: Rect,
2082 ) -> Result<PopupPortalOutcome, PopupPortalError> {
2083 let portal = self.popup_portal();
2084 let result = (|| {
2085 let mut portal = portal.borrow_mut();
2086 portal.set_anchor(popup_id, Some(anchor))?;
2087 portal.set_bounds(popup_id, bounds)?;
2088 portal.open(popup_id)
2089 })();
2090 self.record_popup_result(result)
2091 }
2092
2093 /// Publishes semantic placement inputs and opens (or raises) a popup.
2094 ///
2095 /// Unlike [`Self::open_popup`], this path does not accept backend-resolved
2096 /// bounds. A changed request clears previous backend geometry so the
2097 /// active host resolves it against its current viewport; an identical
2098 /// repaint preserves geometry already resolved by that host.
2099 /// All dimensions use logical pixels.
2100 ///
2101 /// # Errors
2102 ///
2103 /// Returns and records invalid placement geometry, unknown-ID, or
2104 /// parent-open errors.
2105 ///
2106 /// # Examples
2107 ///
2108 /// ```
2109 /// use ailloli_ui_core::{ElementId, Rect, Size};
2110 /// use ailloli_ui_runtime::{app::{PresentationGeneration, RuntimeHandle}, component::View, popup::{ElementTreeId, PopupContent, PopupId, PopupOwner, PopupPlacementSpec, PopupRequest}};
2111 /// let runtime = RuntimeHandle::<()>::new();
2112 /// let id = PopupId::new(1);
2113 /// runtime.register_popup(PopupRequest::new(id, PopupOwner::new("main", PresentationGeneration::INITIAL, ElementTreeId::new(0), ElementId(1)), PopupContent::new(View::empty)))?;
2114 /// let spec = PopupPlacementSpec::new(Rect::new(0.0, 0.0, 10.0, 4.0), Size::new(30.0, 20.0));
2115 /// runtime.open_popup_placed(id, spec)?;
2116 /// assert_eq!(runtime.popup_portal().borrow().request(id).unwrap().desired_size(), Some(Size::new(30.0, 20.0)));
2117 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
2118 /// ```
2119 pub fn open_popup_placed(
2120 &self,
2121 popup_id: PopupId,
2122 placement: PopupPlacementSpec,
2123 ) -> Result<PopupPortalOutcome, PopupPortalError> {
2124 let portal = self.popup_portal();
2125 let result = (|| {
2126 let mut portal = portal.borrow_mut();
2127 portal.set_placement_request(popup_id, placement)?;
2128 portal.open(popup_id)
2129 })();
2130 self.record_popup_result(result)
2131 }
2132
2133 /// Opens a registered popup before its backend has committed geometry.
2134 ///
2135 /// This is used for declarative `default_open`/bound-open state. The
2136 /// overlay backend supplies anchor and bounds at its next interaction or
2137 /// paint pass without delaying semantic visibility.
2138 ///
2139 /// # Errors
2140 ///
2141 /// Returns and records unknown-ID or parent-open errors.
2142 ///
2143 /// # Examples
2144 ///
2145 /// ```
2146 /// use ailloli_ui_core::ElementId;
2147 /// use ailloli_ui_runtime::{app::{PresentationGeneration, RuntimeHandle}, component::View, popup::{ElementTreeId, PopupContent, PopupId, PopupOwner, PopupRequest}};
2148 /// let runtime = RuntimeHandle::<()>::new();
2149 /// let id = PopupId::new(1);
2150 /// runtime.register_popup(PopupRequest::new(id, PopupOwner::new("main", PresentationGeneration::INITIAL, ElementTreeId::new(0), ElementId(1)), PopupContent::new(View::empty)))?;
2151 /// runtime.open_popup_unpositioned(id)?;
2152 /// assert!(runtime.popup_is_open(id));
2153 /// assert_eq!(runtime.popup_portal().borrow().bounds(id), None);
2154 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
2155 /// ```
2156 pub fn open_popup_unpositioned(
2157 &self,
2158 popup_id: PopupId,
2159 ) -> Result<PopupPortalOutcome, PopupPortalError> {
2160 let result = self.popup_portal().borrow_mut().open(popup_id);
2161 self.record_popup_result(result)
2162 }
2163
2164 /// Closes an open popup subtree for a stated reason and records its intents.
2165 ///
2166 /// Unknown or already-closed IDs produce an unhandled empty outcome rather
2167 /// than an error. Descendants close before their parent; focus restoration
2168 /// can add a final intent.
2169 ///
2170 /// # Examples
2171 ///
2172 /// ```
2173 /// use ailloli_ui_core::ElementId;
2174 /// use ailloli_ui_runtime::{app::{PresentationGeneration, RuntimeHandle}, component::View, popup::{ElementTreeId, PopupContent, PopupDismissReason, PopupId, PopupOwner, PopupRequest}};
2175 /// let runtime = RuntimeHandle::<()>::new();
2176 /// let id = PopupId::new(1);
2177 /// runtime.register_popup(PopupRequest::new(id, PopupOwner::new("main", PresentationGeneration::INITIAL, ElementTreeId::new(0), ElementId(1)), PopupContent::new(View::empty)))?;
2178 /// runtime.open_popup_unpositioned(id)?;
2179 /// assert!(runtime.close_popup(id, PopupDismissReason::Programmatic).handled());
2180 /// assert!(!runtime.popup_is_open(id));
2181 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
2182 /// ```
2183 pub fn close_popup(&self, popup_id: PopupId, reason: PopupDismissReason) -> PopupPortalOutcome {
2184 let outcome = self
2185 .popup_portal()
2186 .borrow_mut()
2187 .close_with_reason(popup_id, reason);
2188 self.record_popup_intents(outcome.intents());
2189 outcome
2190 }
2191
2192 /// Closes and removes a popup registration, recording resulting intents.
2193 ///
2194 /// An owner snapshot preserves focus-restoration data after the portal entry
2195 /// disappears. Unknown IDs return an unhandled outcome. Any element-to-ID
2196 /// reservation remains until its tree scope is released.
2197 ///
2198 /// # Examples
2199 ///
2200 /// ```
2201 /// use ailloli_ui_core::ElementId;
2202 /// use ailloli_ui_runtime::{app::{PresentationGeneration, RuntimeHandle}, component::View, popup::{ElementTreeId, PopupContent, PopupId, PopupOwner, PopupRequest}};
2203 /// let runtime = RuntimeHandle::<()>::new();
2204 /// let id = PopupId::new(1);
2205 /// runtime.register_popup(PopupRequest::new(id, PopupOwner::new("main", PresentationGeneration::INITIAL, ElementTreeId::new(0), ElementId(1)), PopupContent::new(View::empty)))?;
2206 /// assert!(runtime.unregister_popup(id).handled());
2207 /// assert!(!runtime.popup_portal().borrow().contains(id));
2208 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
2209 /// ```
2210 pub fn unregister_popup(&self, popup_id: PopupId) -> PopupPortalOutcome {
2211 let owners = self.open_popup_owner_snapshot();
2212 let outcome = self.popup_portal().borrow_mut().unregister(popup_id);
2213 self.record_popup_intents_with_owners(outcome.intents(), &owners);
2214 outcome
2215 }
2216
2217 /// Reports whether a registered popup is currently open.
2218 ///
2219 /// Unknown IDs and registered-but-closed entries both return `false`.
2220 ///
2221 /// # Examples
2222 ///
2223 /// ```
2224 /// use ailloli_ui_runtime::{app::RuntimeHandle, popup::PopupId};
2225 /// assert!(!RuntimeHandle::<()>::new().popup_is_open(PopupId::new(404)));
2226 /// ```
2227 pub fn popup_is_open(&self, popup_id: PopupId) -> bool {
2228 self.popup_portal().borrow().is_open(popup_id)
2229 }
2230
2231 /// Routes a pointer press through the popup z-order authority.
2232 ///
2233 /// The returned outcome is also recorded for backend processing. Input
2234 /// routers use it to prevent an outside-dismiss press from activating
2235 /// content behind the popup.
2236 /// `point` is in global logical pixels. Only popups belonging to the exact
2237 /// logical window and presentation generation participate.
2238 ///
2239 /// # Examples
2240 ///
2241 /// ```
2242 /// use ailloli_ui_core::{ElementId, LogicalWindowId, Point, Rect};
2243 /// use ailloli_ui_runtime::{app::{PresentationGeneration, RuntimeHandle}, component::View, popup::{ElementTreeId, PopupContent, PopupId, PopupOwner, PopupRequest}};
2244 /// let runtime = RuntimeHandle::<()>::new();
2245 /// let window = LogicalWindowId::new("main");
2246 /// let id = PopupId::new(1);
2247 /// runtime.register_popup(PopupRequest::new(id, PopupOwner::new(window.clone(), PresentationGeneration::INITIAL, ElementTreeId::new(0), ElementId(1)), PopupContent::new(View::empty)))?;
2248 /// runtime.open_popup(id, Rect::new(0.0, 0.0, 1.0, 1.0), Rect::new(0.0, 0.0, 10.0, 10.0))?;
2249 /// let outcome = runtime.route_popup_pointer_press(&window, PresentationGeneration::INITIAL, Point::new(20.0, 20.0));
2250 /// assert!(outcome.handled() && !runtime.popup_is_open(id));
2251 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
2252 /// ```
2253 pub fn route_popup_pointer_press(
2254 &self,
2255 logical_window_id: &LogicalWindowId,
2256 presentation_generation: PresentationGeneration,
2257 point: Point,
2258 ) -> PopupPortalOutcome {
2259 self.route_popup_pointer_press_with_backend_hit(
2260 logical_window_id,
2261 presentation_generation,
2262 point,
2263 None,
2264 )
2265 }
2266
2267 /// Same as [`Self::route_popup_pointer_press`], with a popup hit confirmed
2268 /// by the active presentation backend.
2269 ///
2270 /// `backend_hit` is trusted only when it names an open popup in the exact
2271 /// presentation. `None` falls back to retained bounds; an unpositioned popup
2272 /// can therefore still be recognized by `Some(id)`.
2273 ///
2274 /// # Examples
2275 ///
2276 /// ```
2277 /// use ailloli_ui_core::{ElementId, LogicalWindowId, Point};
2278 /// use ailloli_ui_runtime::{app::{PresentationGeneration, RuntimeHandle}, component::View, popup::{ElementTreeId, PopupContent, PopupId, PopupOwner, PopupRequest}};
2279 /// let runtime = RuntimeHandle::<()>::new();
2280 /// let window = LogicalWindowId::new("main");
2281 /// let id = PopupId::new(1);
2282 /// runtime.register_popup(PopupRequest::new(id, PopupOwner::new(window.clone(), PresentationGeneration::INITIAL, ElementTreeId::new(0), ElementId(1)), PopupContent::new(View::empty)))?;
2283 /// runtime.open_popup_unpositioned(id)?;
2284 /// assert!(runtime.route_popup_pointer_press_with_backend_hit(&window, PresentationGeneration::INITIAL, Point::new(500.0, 500.0), Some(id)).handled());
2285 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
2286 /// ```
2287 pub fn route_popup_pointer_press_with_backend_hit(
2288 &self,
2289 logical_window_id: &LogicalWindowId,
2290 presentation_generation: PresentationGeneration,
2291 point: Point,
2292 backend_hit: Option<PopupId>,
2293 ) -> PopupPortalOutcome {
2294 let outcome = self
2295 .popup_portal()
2296 .borrow_mut()
2297 .handle_pointer_press_with_backend_hit(
2298 logical_window_id,
2299 presentation_generation,
2300 point,
2301 backend_hit,
2302 );
2303 self.record_popup_intents(outcome.intents());
2304 outcome
2305 }
2306
2307 /// Routes Escape to the topmost eligible popup.
2308 ///
2309 /// Only the exact logical window/generation participates. The returned
2310 /// outcome is recorded; an eligible popup may close and restore focus.
2311 ///
2312 /// # Examples
2313 ///
2314 /// ```
2315 /// use ailloli_ui_core::{ElementId, LogicalWindowId};
2316 /// use ailloli_ui_runtime::{app::{PresentationGeneration, RuntimeHandle}, component::View, popup::{ElementTreeId, PopupContent, PopupId, PopupOwner, PopupRequest}};
2317 /// let runtime = RuntimeHandle::<()>::new();
2318 /// let window = LogicalWindowId::new("main");
2319 /// let id = PopupId::new(1);
2320 /// runtime.register_popup(PopupRequest::new(id, PopupOwner::new(window.clone(), PresentationGeneration::INITIAL, ElementTreeId::new(0), ElementId(1)), PopupContent::new(View::empty)))?;
2321 /// runtime.open_popup_unpositioned(id)?;
2322 /// assert!(runtime.route_popup_escape(&window, PresentationGeneration::INITIAL).handled());
2323 /// assert!(!runtime.popup_is_open(id));
2324 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
2325 /// ```
2326 pub fn route_popup_escape(
2327 &self,
2328 logical_window_id: &LogicalWindowId,
2329 presentation_generation: PresentationGeneration,
2330 ) -> PopupPortalOutcome {
2331 let outcome = self
2332 .popup_portal()
2333 .borrow_mut()
2334 .handle_escape(logical_window_id, presentation_generation);
2335 self.record_popup_intents(outcome.intents());
2336 outcome
2337 }
2338
2339 /// Closes registrations attached to obsolete native presentation
2340 /// generations and records the resulting backend intents.
2341 /// Registrations for other logical windows or for the supplied current
2342 /// generation remain open.
2343 ///
2344 /// # Examples
2345 ///
2346 /// ```
2347 /// use ailloli_ui_core::{ElementId, LogicalWindowId};
2348 /// use ailloli_ui_runtime::{app::{PresentationGeneration, RuntimeHandle}, component::View, popup::{ElementTreeId, PopupContent, PopupId, PopupOwner, PopupRequest}};
2349 /// let runtime = RuntimeHandle::<()>::new();
2350 /// let window = LogicalWindowId::new("main");
2351 /// let id = PopupId::new(1);
2352 /// runtime.register_popup(PopupRequest::new(id, PopupOwner::new(window.clone(), PresentationGeneration::new(1), ElementTreeId::new(0), ElementId(1)), PopupContent::new(View::empty)))?;
2353 /// runtime.open_popup_unpositioned(id)?;
2354 /// assert!(runtime.close_stale_popup_presentations(&window, PresentationGeneration::new(2)).handled());
2355 /// assert!(!runtime.popup_is_open(id));
2356 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
2357 /// ```
2358 pub fn close_stale_popup_presentations(
2359 &self,
2360 logical_window_id: &LogicalWindowId,
2361 presentation_generation: PresentationGeneration,
2362 ) -> PopupPortalOutcome {
2363 let owners = self.open_popup_owner_snapshot();
2364 let outcome = self
2365 .popup_portal()
2366 .borrow_mut()
2367 .close_stale_presentations(logical_window_id, presentation_generation);
2368 self.record_popup_intents_with_owners(outcome.intents(), &owners);
2369 outcome
2370 }
2371
2372 /// Prunes missing popup owners in this handle's retained-tree namespace.
2373 ///
2374 /// The callback receives tree-local element IDs for registrations in this
2375 /// scope. Missing owners and their descendants are closed/unregistered;
2376 /// sibling tree namespaces are untouched. Resulting intents are recorded.
2377 ///
2378 /// # Examples
2379 ///
2380 /// ```
2381 /// use ailloli_ui_core::ElementId;
2382 /// use ailloli_ui_runtime::{app::{PresentationGeneration, RuntimeHandle}, component::View, popup::{ElementTreeId, PopupContent, PopupId, PopupOwner, PopupRequest}};
2383 /// let runtime = RuntimeHandle::<()>::new();
2384 /// let id = PopupId::new(1);
2385 /// runtime.register_popup(PopupRequest::new(id, PopupOwner::new("main", PresentationGeneration::INITIAL, ElementTreeId::new(0), ElementId(9)), PopupContent::new(View::empty)))?;
2386 /// let outcome = runtime.prune_stale_popup_owners(|element| element != ElementId(9));
2387 /// assert!(outcome.handled());
2388 /// assert!(!runtime.popup_portal().borrow().contains(id));
2389 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
2390 /// ```
2391 pub fn prune_stale_popup_owners(
2392 &self,
2393 element_is_alive: impl FnMut(ElementId) -> bool,
2394 ) -> PopupPortalOutcome {
2395 let owners = self.open_popup_owner_snapshot();
2396 let outcome = self
2397 .popup_portal()
2398 .borrow_mut()
2399 .prune_stale_owners_in_tree(self.element_tree_id, element_is_alive);
2400 self.record_popup_intents_with_owners(outcome.intents(), &owners);
2401 outcome
2402 }
2403
2404 /// Drains all backend-facing popup intents in production order.
2405 ///
2406 /// This queue is global across tree namespaces/presentations. It is distinct
2407 /// from the internally filtered pending-input queue, so draining it does not
2408 /// consume focus work awaited by an input router.
2409 ///
2410 /// # Examples
2411 ///
2412 /// ```
2413 /// use ailloli_ui_core::ElementId;
2414 /// use ailloli_ui_runtime::{app::{PresentationGeneration, RuntimeHandle}, component::View, popup::{ElementTreeId, PopupContent, PopupId, PopupIntent, PopupOwner, PopupRequest}};
2415 /// let runtime = RuntimeHandle::<()>::new();
2416 /// let id = PopupId::new(1);
2417 /// runtime.register_popup(PopupRequest::new(id, PopupOwner::new("main", PresentationGeneration::INITIAL, ElementTreeId::new(0), ElementId(1)), PopupContent::new(View::empty)))?;
2418 /// runtime.open_popup_unpositioned(id)?;
2419 /// assert!(matches!(runtime.take_popup_intents().as_slice(), [PopupIntent::Present { popup_id }] if *popup_id == id));
2420 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
2421 /// ```
2422 pub fn take_popup_intents(&self) -> Vec<PopupIntent> {
2423 std::mem::take(&mut self.inner.borrow_mut().popup_intents)
2424 }
2425
2426 /// Drains only the pending popup intents owned by one exact retained tree
2427 /// and presentation.
2428 ///
2429 /// A runtime handle can be shared by several windows and popup subtrees.
2430 /// Keeping unmatched records queued prevents one input router from
2431 /// consuming focus/dismissal work that belongs to another presentation.
2432 /// Records for the requested tree/window but a stale generation are
2433 /// discarded, while records for other scopes remain queued.
2434 ///
2435 /// # Examples
2436 ///
2437 /// ```
2438 /// use ailloli_ui_runtime::app::RuntimeHandle;
2439 /// // Public hosts drain backend intents separately; input routing performs
2440 /// // exact tree/presentation filtering internally.
2441 /// assert!(RuntimeHandle::<()>::new().take_popup_intents().is_empty());
2442 /// ```
2443 pub(crate) fn take_pending_popup_intents_for(
2444 &self,
2445 element_tree_id: ElementTreeId,
2446 logical_window_id: &LogicalWindowId,
2447 presentation_generation: PresentationGeneration,
2448 ) -> Vec<PopupIntent> {
2449 let mut inner = self.inner.borrow_mut();
2450 let pending = std::mem::take(&mut inner.pending_popup_intents);
2451 let mut matched = Vec::new();
2452 let mut remaining = Vec::new();
2453 for record in pending {
2454 if record.owner.element_tree_id() == element_tree_id
2455 && record.owner.logical_window_id() == logical_window_id
2456 {
2457 if record.owner.presentation_generation() == presentation_generation {
2458 matched.push(record.intent);
2459 }
2460 } else {
2461 remaining.push(record);
2462 }
2463 }
2464 inner.pending_popup_intents = remaining;
2465 matched
2466 }
2467
2468 /// Drains popup-operation errors in occurrence order.
2469 ///
2470 /// Only handle helpers record here; errors produced through direct
2471 /// [`Self::popup_portal`] access are not observed.
2472 ///
2473 /// # Examples
2474 ///
2475 /// ```
2476 /// use ailloli_ui_core::ElementId;
2477 /// use ailloli_ui_runtime::{app::{PresentationGeneration, RuntimeHandle}, component::View, popup::{ElementTreeId, PopupContent, PopupId, PopupOwner, PopupPortalError, PopupRequest}};
2478 /// let runtime = RuntimeHandle::<()>::new();
2479 /// let request = || PopupRequest::new(PopupId::new(1), PopupOwner::new("main", PresentationGeneration::INITIAL, ElementTreeId::new(0), ElementId(1)), PopupContent::new(View::empty));
2480 /// runtime.register_popup(request())?;
2481 /// assert_eq!(runtime.register_popup(request()), Err(PopupPortalError::DuplicateId));
2482 /// assert_eq!(runtime.take_popup_errors(), [PopupPortalError::DuplicateId]);
2483 /// # Ok::<(), ailloli_ui_runtime::popup::PopupPortalError>(())
2484 /// ```
2485 pub fn take_popup_errors(&self) -> Vec<PopupPortalError> {
2486 std::mem::take(&mut self.inner.borrow_mut().popup_errors)
2487 }
2488
2489 /// Records either lifecycle intents or a cloned portal error, preserving the result.
2490 ///
2491 /// # Errors
2492 ///
2493 /// Returns the same [`PopupPortalError`] supplied in `result`, after cloning
2494 /// it into the runtime's diagnostic error queue.
2495 fn record_popup_result(
2496 &self,
2497 result: Result<PopupPortalOutcome, PopupPortalError>,
2498 ) -> Result<PopupPortalOutcome, PopupPortalError> {
2499 match result {
2500 Ok(outcome) => {
2501 self.record_popup_intents(outcome.intents());
2502 Ok(outcome)
2503 }
2504 Err(error) => {
2505 self.record_popup_error(error.clone());
2506 Err(error)
2507 }
2508 }
2509 }
2510
2511 /// Records backend intents and resolves current owners from the portal.
2512 fn record_popup_intents(&self, intents: &[PopupIntent]) {
2513 self.record_popup_intents_with_owners(intents, &HashMap::new());
2514 }
2515
2516 /// Records global intents plus input-pending intents with owner snapshots.
2517 fn record_popup_intents_with_owners(
2518 &self,
2519 intents: &[PopupIntent],
2520 owners: &HashMap<PopupId, crate::popup::PopupOwner>,
2521 ) {
2522 let portal = self.popup_portal();
2523 let portal = portal.borrow();
2524 let pending: Vec<PendingPopupIntent> = intents
2525 .iter()
2526 .filter_map(|intent| {
2527 let owner = match intent {
2528 PopupIntent::RestoreFocus { owner } => Some(owner.clone()),
2529 PopupIntent::Present { popup_id }
2530 | PopupIntent::MoveFocusInto { popup_id, .. }
2531 | PopupIntent::Dismiss { popup_id, .. } => {
2532 owners.get(popup_id).cloned().or_else(|| {
2533 portal
2534 .request(*popup_id)
2535 .map(|request| request.owner().clone())
2536 })
2537 }
2538 }?;
2539 Some(PendingPopupIntent {
2540 intent: intent.clone(),
2541 owner,
2542 })
2543 })
2544 .collect();
2545 drop(portal);
2546
2547 let mut inner = self.inner.borrow_mut();
2548 let pending: Vec<PendingPopupIntent> = pending
2549 .into_iter()
2550 .filter(|record| {
2551 inner
2552 .presentation_scopes
2553 .get(&record.owner.element_tree_id())
2554 .is_none_or(|(logical_window_id, generation)| {
2555 logical_window_id != record.owner.logical_window_id()
2556 || *generation == record.owner.presentation_generation()
2557 })
2558 })
2559 .collect();
2560 let wake_targets = pending
2561 .iter()
2562 .filter_map(|record| {
2563 inner
2564 .presentation_scopes
2565 .get(&record.owner.element_tree_id())
2566 .is_some_and(|(logical_window_id, generation)| {
2567 logical_window_id == record.owner.logical_window_id()
2568 && *generation == record.owner.presentation_generation()
2569 })
2570 .then_some((record.owner.element_tree_id(), record.owner.element_id()))
2571 })
2572 .collect::<HashSet<_>>();
2573 inner.popup_intents.extend_from_slice(intents);
2574 inner.pending_popup_intents.extend(pending);
2575 drop(inner);
2576
2577 // Opening/closing outside input routing still needs one owner frame so
2578 // the host can mount, hide, or reposition the popup. Only lifecycle
2579 // operations emit intents; repeatedly opening an already-open popup is
2580 // therefore a no-op and cannot create a redraw loop during paint.
2581 for (element_tree_id, element_id) in wake_targets {
2582 RuntimeHandle {
2583 inner: Rc::clone(&self.inner),
2584 element_tree_id,
2585 }
2586 .invalidate_from(
2587 element_id,
2588 Invalidation::Paint,
2589 InvalidationSource::Runtime,
2590 );
2591 }
2592 }
2593
2594 /// Snapshots owners for every currently open popup in z-order.
2595 fn open_popup_owner_snapshot(&self) -> HashMap<PopupId, crate::popup::PopupOwner> {
2596 let portal = self.popup_portal();
2597 let portal = portal.borrow();
2598 portal
2599 .open_ids()
2600 .filter_map(|popup_id| {
2601 portal
2602 .request(popup_id)
2603 .map(|request| (popup_id, request.owner().clone()))
2604 })
2605 .collect()
2606 }
2607
2608 /// Appends one non-fatal popup error to the shared FIFO queue.
2609 fn record_popup_error(&self, error: PopupPortalError) {
2610 self.inner.borrow_mut().popup_errors.push(error);
2611 }
2612}
2613
2614/// Creates the same isolated compatibility-scope state as [`RuntimeHandle::new`].
2615impl<A> Default for RuntimeHandle<A> {
2616 /// Returns [`RuntimeHandle::new`].
2617 fn default() -> Self {
2618 Self::new()
2619 }
2620}
2621
2622/// Shared mutable storage behind every [`RuntimeHandle`] clone.
2623///
2624/// This low-level type is public for provider integrations but is normally
2625/// accessed through handle methods. Public collections span every retained-tree
2626/// namespace sharing the value. The type is UI-thread-local and intended to
2627/// remain behind `Rc<RefCell<_>>`; direct mutation can bypass coalescing,
2628/// diagnostics, error recording, and cleanup invariants.
2629///
2630/// # Examples
2631///
2632/// ```
2633/// use ailloli_ui_runtime::app::RuntimeInner;
2634/// let inner = RuntimeInner::<String>::new();
2635/// assert!(inner.actions.is_empty());
2636/// assert!(!inner.close_requested);
2637/// ```
2638pub struct RuntimeInner<A> {
2639 /// Shared type-erased retained component-state slots.
2640 pub states: Rc<RefCell<StateStore>>,
2641 /// Global FIFO application actions awaiting a host drain.
2642 pub actions: Vec<A>,
2643 /// Optional thread-safe, payload-free host notification callback.
2644 ui_wake: Option<Arc<dyn UiWake>>,
2645 /// Weak UI-thread service callbacks keyed by tree and wrapping ID.
2646 ui_services: HashMap<(ElementTreeId, u64), UiServiceEntry>,
2647 /// Next wrapping UI-service ID; initialized to one.
2648 next_ui_service_id: u64,
2649 /// Coalesced tree-local invalidations keyed by retained element.
2650 pub dirty_elements: HashMap<ElementTreeId, HashMap<ElementId, Invalidation>>,
2651 /// Source-driven invalidations retaining the exact mounted generation.
2652 generation_bound_invalidations:
2653 HashMap<ElementTreeId, HashMap<ElementId, GenerationBoundInvalidation>>,
2654 /// Exact retained payload generation currently mounted at each element.
2655 mounted_elements: HashMap<(ElementTreeId, ElementId), MountGeneration>,
2656 /// Exact source edges retained by successful Build/Layout/Paint callbacks.
2657 reactive_dependencies: ReactiveDependencyGraph,
2658 /// Hidden cumulative counters for dependency and transaction behavior.
2659 reactive_runtime_diagnostics: ReactiveRuntimeDiagnostics,
2660 /// Global bounded invalidation counters and provenance.
2661 invalidation_diagnostics: InvalidationDiagnostics,
2662 /// Active shared clipboard provider.
2663 pub clipboard: Rc<dyn ClipboardProvider>,
2664 /// Active shared validated external-URL opener.
2665 pub external_url_opener: Rc<dyn ExternalUrlOpener>,
2666 /// FIFO URL-opening failures not yet drained.
2667 pub open_url_errors: Vec<OpenUrlError>,
2668 /// Close requested; consumed by winit via `take_close_requested`.
2669 pub close_requested: bool,
2670 /// Pending minimize/maximize per logical window id (`Window::new("main")`).
2671 pub window_chrome_ops: Vec<(LogicalWindowId, WindowChromeOp)>,
2672 /// Timed invalidations for every retained-tree namespace.
2673 scheduled_repaints: Vec<ScheduledInvalidation>,
2674 /// Last pending focus key per retained-tree namespace.
2675 pub pending_focus_keys: HashMap<ElementTreeId, String>,
2676 /// Current logical window/generation per retained-tree namespace.
2677 pub presentation_scopes: HashMap<ElementTreeId, (LogicalWindowId, PresentationGeneration)>,
2678 /// Shared popup registry and z-order authority.
2679 pub popup_portal: Rc<RefCell<PopupPortal<A>>>,
2680 /// Stable popup ID reservation per tree-local owner element.
2681 pub popup_ids_by_element: HashMap<(ElementTreeId, ElementId), PopupId>,
2682 /// Global FIFO backend-facing popup intents.
2683 pub popup_intents: Vec<PopupIntent>,
2684 /// Owner-tagged popup intents awaiting scoped input routing.
2685 pub(crate) pending_popup_intents: Vec<PendingPopupIntent>,
2686 /// FIFO errors recorded by handle popup helpers.
2687 pub popup_errors: Vec<PopupPortalError>,
2688 /// Next checked tree-namespace ID; initialized to zero.
2689 next_element_tree_id: u64,
2690}
2691
2692/// One exact tree/element/strength monotonic invalidation deadline.
2693#[derive(Debug, Clone, Copy)]
2694struct ScheduledInvalidation {
2695 /// Owning retained-tree namespace.
2696 element_tree_id: ElementTreeId,
2697 /// Tree-local invalidation target.
2698 element_id: ElementId,
2699 /// Exact strength, used as part of the timer coalescing key.
2700 invalidation: Invalidation,
2701 /// Monotonic deadline at or after which the entry is due.
2702 due: Instant,
2703}
2704
2705/// One coalesced source notification that remains valid for one mount only.
2706#[derive(Debug, Clone, Copy)]
2707struct GenerationBoundInvalidation {
2708 /// Payload generation observed by the notifying subscription.
2709 mount_generation: MountGeneration,
2710 /// Strongest retained work requested by that generation.
2711 invalidation: Invalidation,
2712}
2713
2714/// Weak callback plus optional exact component owner for one UI service.
2715struct UiServiceEntry {
2716 /// Callback retained by component state rather than the runtime.
2717 callback: Weak<dyn Fn() -> bool>,
2718 /// Component invalidated when the callback reports model-visible changes.
2719 owner: Option<UiServiceOwner>,
2720}
2721
2722/// Retained owner captured when a component registers a UI service.
2723#[derive(Debug, Clone, Copy)]
2724struct UiServiceOwner {
2725 /// Tree-local component identity.
2726 element_id: ElementId,
2727 /// Exact payload generation, absent only for compatibility contexts that
2728 /// are not currently mounted in a retained tree.
2729 mount_generation: Option<MountGeneration>,
2730}
2731
2732/// Popup intent paired with the complete owner identity used for scoped drains.
2733///
2734/// # Examples
2735///
2736/// ```
2737/// use ailloli_ui_runtime::app::RuntimeHandle;
2738/// // The runtime starts with no public or internally pending popup intents.
2739/// assert!(RuntimeHandle::<()>::new().take_popup_intents().is_empty());
2740/// ```
2741#[derive(Debug, Clone)]
2742pub(crate) struct PendingPopupIntent {
2743 /// Backend/focus operation.
2744 intent: PopupIntent,
2745 /// Owner identity captured before an unregister can remove it.
2746 owner: crate::popup::PopupOwner,
2747}
2748
2749/// Helpers for owner-tagged pending popup intents.
2750impl PendingPopupIntent {
2751 /// Returns the addressed popup ID, or `None` for owner-only focus restore.
2752 fn popup_id(&self) -> Option<PopupId> {
2753 match &self.intent {
2754 PopupIntent::Present { popup_id }
2755 | PopupIntent::MoveFocusInto { popup_id, .. }
2756 | PopupIntent::Dismiss { popup_id, .. } => Some(*popup_id),
2757 PopupIntent::RestoreFocus { .. } => None,
2758 }
2759 }
2760}
2761
2762/// Tests whether an intent must be purged when a tree scope is released.
2763///
2764/// Popup-addressed intents match the released portal IDs; restore-focus intents
2765/// match the owner's tree namespace directly.
2766fn popup_intent_belongs_to_released_tree(
2767 intent: &PopupIntent,
2768 element_tree_id: ElementTreeId,
2769 removed_popup_ids: &HashSet<PopupId>,
2770) -> bool {
2771 match intent {
2772 PopupIntent::Present { popup_id }
2773 | PopupIntent::MoveFocusInto { popup_id, .. }
2774 | PopupIntent::Dismiss { popup_id, .. } => removed_popup_ids.contains(popup_id),
2775 PopupIntent::RestoreFocus { owner } => owner.element_tree_id() == element_tree_id,
2776 }
2777}
2778
2779/// Constructors for raw shared runtime storage.
2780impl<A> RuntimeInner<A> {
2781 /// Creates empty shared state with deterministic in-memory providers.
2782 ///
2783 /// Service IDs start at one, popup IDs start at one inside the portal, and
2784 /// allocated tree namespaces start at zero. No capacity limit is imposed on
2785 /// the runtime-owned queues/maps apart from the bounded diagnostics ring.
2786 ///
2787 /// # Examples
2788 ///
2789 /// ```
2790 /// use ailloli_ui_runtime::app::{ClipboardProvider, RuntimeInner};
2791 /// let inner = RuntimeInner::<()>::new();
2792 /// assert_eq!(inner.clipboard.read_text().as_deref(), Some(""));
2793 /// assert!(inner.dirty_elements.is_empty() && inner.popup_errors.is_empty());
2794 /// ```
2795 pub fn new() -> Self {
2796 Self {
2797 states: Rc::new(RefCell::new(StateStore::default())),
2798 actions: Vec::new(),
2799 ui_wake: None,
2800 ui_services: HashMap::new(),
2801 next_ui_service_id: 1,
2802 dirty_elements: HashMap::new(),
2803 generation_bound_invalidations: HashMap::new(),
2804 mounted_elements: HashMap::new(),
2805 reactive_dependencies: ReactiveDependencyGraph::default(),
2806 reactive_runtime_diagnostics: ReactiveRuntimeDiagnostics::default(),
2807 invalidation_diagnostics: InvalidationDiagnostics::default(),
2808 clipboard: Rc::new(MemoryClipboard::new()),
2809 external_url_opener: Rc::new(MemoryExternalUrlOpener::new()),
2810 open_url_errors: Vec::new(),
2811 close_requested: false,
2812 window_chrome_ops: Vec::new(),
2813 scheduled_repaints: Vec::new(),
2814 pending_focus_keys: HashMap::new(),
2815 presentation_scopes: HashMap::new(),
2816 popup_portal: Rc::new(RefCell::new(PopupPortal::new())),
2817 popup_ids_by_element: HashMap::new(),
2818 popup_intents: Vec::new(),
2819 pending_popup_intents: Vec::new(),
2820 popup_errors: Vec::new(),
2821 next_element_tree_id: 0,
2822 }
2823 }
2824}
2825
2826/// Delegates to [`RuntimeInner::new`].
2827impl<A> Default for RuntimeInner<A> {
2828 /// Returns fresh empty shared runtime state.
2829 fn default() -> Self {
2830 Self::new()
2831 }
2832}
2833
2834// Future: DirtyLayout / DirtyPaint / DirtyInput / DirtyText
2835
2836#[cfg(test)]
2837/// Runtime-handle queue, timer, provider, and tree-isolation regression tests.
2838mod tests {
2839 use super::*;
2840 use crate::app::Runtime;
2841 use crate::component::View;
2842 use crate::popup::{PopupContent, PopupOwner};
2843
2844 #[test]
2845 fn owned_ui_service_rejects_a_replaced_mount_generation() {
2846 let runtime = RuntimeHandle::<()>::new();
2847 let owner = ElementId(70);
2848 runtime.register_element_mount(owner, MountGeneration::INITIAL);
2849 let service: Rc<dyn Fn() -> bool> = Rc::new(|| true);
2850 let _registration = runtime.register_owned_ui_service(owner, &service);
2851
2852 assert!(runtime.service_ui_sources());
2853 assert!(runtime.frame_work_plan().needs_build());
2854 assert_eq!(runtime.take_dirty_elements(), [owner]);
2855
2856 runtime.register_element_mount(owner, MountGeneration::INITIAL.next());
2857 assert!(runtime.service_ui_sources());
2858 assert!(runtime.pending_presentation_work().is_empty());
2859 assert!(!runtime.has_dirty_elements());
2860 }
2861
2862 #[test]
2863 /// A future timer remains scheduled and does not dirty its target early.
2864 fn scheduled_repaint_not_due_marks_nothing() {
2865 let runtime = RuntimeHandle::<()>::new();
2866 let id = ElementId(1);
2867 runtime.request_repaint_after(id, Duration::from_millis(50));
2868
2869 assert!(runtime
2870 .take_due_scheduled_repaints(Instant::now())
2871 .is_empty());
2872 assert!(!runtime.has_dirty_elements());
2873 assert!(runtime.next_scheduled_repaint_due().is_some());
2874 }
2875
2876 #[test]
2877 /// A due timer can be taken without directly marking retained work.
2878 fn scheduled_repaint_due_can_mark_dirty() {
2879 let runtime = RuntimeHandle::<()>::new();
2880 let id = ElementId(2);
2881 runtime.request_repaint_after(id, Duration::from_millis(1));
2882
2883 let due = runtime.take_due_scheduled_repaints(Instant::now() + Duration::from_secs(1));
2884 assert_eq!(due, vec![id]);
2885 for element_id in due {
2886 runtime.request_build(element_id);
2887 }
2888 assert_eq!(runtime.take_dirty_elements(), vec![id]);
2889 assert!(runtime.next_scheduled_repaint_due().is_none());
2890 }
2891
2892 #[test]
2893 /// Equal-strength timers keep only their earliest monotonic deadline.
2894 fn scheduled_repaint_deduplicates_by_earliest_due() {
2895 let runtime = RuntimeHandle::<()>::new();
2896 let id = ElementId(3);
2897 runtime.request_repaint_after(id, Duration::from_millis(100));
2898 runtime.request_repaint_after(id, Duration::from_millis(10));
2899 runtime.request_repaint_after(id, Duration::from_millis(200));
2900
2901 assert!(runtime
2902 .take_due_scheduled_repaints(Instant::now() + Duration::from_millis(50))
2903 .contains(&id));
2904 assert!(runtime.next_scheduled_repaint_due().is_none());
2905 }
2906
2907 #[test]
2908 /// Injected URL opener failures are returned and retained for host inspection.
2909 fn external_url_opener_is_injected_and_errors_are_non_fatal() {
2910 let runtime = RuntimeHandle::<()>::new();
2911 let opener = MemoryExternalUrlOpener::new();
2912 runtime.set_external_url_opener(Rc::new(opener.clone()));
2913 let url = ExternalUrl::parse("https://example.com/docs?q=1#api").unwrap();
2914
2915 runtime.open_external_url(&url).unwrap();
2916 assert_eq!(opener.opened_urls(), [url.as_str()]);
2917 assert!(runtime.take_open_url_errors().is_empty());
2918
2919 opener.fail_next(OpenUrlError::LaunchFailed);
2920 assert!(runtime.open_external_url(&url).is_err());
2921 assert_eq!(runtime.take_open_url_errors().len(), 1);
2922 assert!(runtime.take_open_url_errors().is_empty());
2923 }
2924
2925 #[test]
2926 /// Presentation scopes and pending popup work remain tree-namespace isolated.
2927 fn presentation_scope_and_popup_intents_are_isolated_per_tree() {
2928 let shared = RuntimeHandle::<()>::new();
2929 let first = Runtime::new(shared.clone());
2930 let second = Runtime::new(shared);
2931 let first_handle = first.runtime.clone();
2932 let second_handle = second.runtime.clone();
2933 let first_window = LogicalWindowId::new("first");
2934 let second_window = LogicalWindowId::new("second");
2935 let first_generation = PresentationGeneration::new(3);
2936 let second_generation = PresentationGeneration::new(7);
2937
2938 first_handle.set_presentation_scope(first_window.clone(), first_generation);
2939 second_handle.set_presentation_scope(second_window.clone(), second_generation);
2940 assert_eq!(
2941 first_handle.presentation_scope(),
2942 Some((first_window.clone(), first_generation))
2943 );
2944 assert_eq!(
2945 second_handle.presentation_scope(),
2946 Some((second_window.clone(), second_generation))
2947 );
2948 second_handle.clear_presentation_scope();
2949 assert_eq!(second_handle.presentation_scope(), None);
2950
2951 let popup_id = PopupId::new(41);
2952 first_handle
2953 .register_popup(PopupRequest::new(
2954 popup_id,
2955 PopupOwner::new(
2956 first_window.clone(),
2957 first_generation,
2958 first_handle.element_tree_id(),
2959 ElementId(9),
2960 ),
2961 PopupContent::new(View::empty),
2962 ))
2963 .unwrap();
2964 first_handle.open_popup_unpositioned(popup_id).unwrap();
2965
2966 assert!(second_handle
2967 .take_pending_popup_intents_for(
2968 second_handle.element_tree_id(),
2969 &second_window,
2970 second_generation,
2971 )
2972 .is_empty());
2973 assert!(matches!(
2974 first_handle
2975 .take_pending_popup_intents_for(
2976 first_handle.element_tree_id(),
2977 &first_window,
2978 first_generation,
2979 )
2980 .as_slice(),
2981 [PopupIntent::Present { popup_id: id }] if *id == popup_id
2982 ));
2983
2984 first_handle.unregister_popup(popup_id);
2985 assert!(second_handle
2986 .take_pending_popup_intents_for(
2987 second_handle.element_tree_id(),
2988 &second_window,
2989 second_generation,
2990 )
2991 .is_empty());
2992 assert!(matches!(
2993 first_handle
2994 .take_pending_popup_intents_for(
2995 first_handle.element_tree_id(),
2996 &first_window,
2997 first_generation,
2998 )
2999 .as_slice(),
3000 [PopupIntent::Dismiss { popup_id: id, .. }] if *id == popup_id
3001 ));
3002 }
3003
3004 #[test]
3005 /// A scoped drain discards stale same-window generations but preserves peers.
3006 fn pending_popup_intents_drop_only_stale_generation_for_requested_scope() {
3007 let shared = RuntimeHandle::<()>::new();
3008 let first = Runtime::new(shared.clone());
3009 let sibling = Runtime::new(shared);
3010 let first_handle = first.runtime.clone();
3011 let sibling_handle = sibling.runtime.clone();
3012 let main_window = LogicalWindowId::new("main");
3013 let sibling_window = LogicalWindowId::new("sibling");
3014 let other_window = LogicalWindowId::new("other");
3015 let generation_one = PresentationGeneration::new(1);
3016 let generation_two = PresentationGeneration::new(2);
3017 let stale = PopupId::new(51);
3018 let current = PopupId::new(52);
3019 let sibling_popup = PopupId::new(53);
3020 let other_window_popup = PopupId::new(54);
3021
3022 first_handle.set_presentation_scope(main_window.clone(), generation_one);
3023 sibling_handle.set_presentation_scope(sibling_window.clone(), generation_one);
3024 for (popup_id, owner) in [
3025 (
3026 stale,
3027 PopupOwner::new(
3028 main_window.clone(),
3029 generation_one,
3030 first_handle.element_tree_id(),
3031 ElementId(1),
3032 ),
3033 ),
3034 (
3035 current,
3036 PopupOwner::new(
3037 main_window.clone(),
3038 generation_two,
3039 first_handle.element_tree_id(),
3040 ElementId(2),
3041 ),
3042 ),
3043 (
3044 sibling_popup,
3045 PopupOwner::new(
3046 sibling_window.clone(),
3047 generation_one,
3048 sibling_handle.element_tree_id(),
3049 ElementId(3),
3050 ),
3051 ),
3052 (
3053 other_window_popup,
3054 PopupOwner::new(
3055 other_window.clone(),
3056 generation_one,
3057 first_handle.element_tree_id(),
3058 ElementId(4),
3059 ),
3060 ),
3061 ] {
3062 first_handle
3063 .register_popup(PopupRequest::new(
3064 popup_id,
3065 owner,
3066 PopupContent::new(View::empty),
3067 ))
3068 .unwrap();
3069 }
3070 first_handle.open_popup_unpositioned(stale).unwrap();
3071 sibling_handle
3072 .open_popup_unpositioned(sibling_popup)
3073 .unwrap();
3074 first_handle
3075 .open_popup_unpositioned(other_window_popup)
3076 .unwrap();
3077
3078 first_handle.set_presentation_scope(main_window.clone(), generation_two);
3079 first_handle.close_stale_popup_presentations(&main_window, generation_two);
3080 first_handle.open_popup_unpositioned(current).unwrap();
3081
3082 assert!(matches!(
3083 first_handle
3084 .take_pending_popup_intents_for(
3085 first_handle.element_tree_id(),
3086 &main_window,
3087 generation_two,
3088 )
3089 .as_slice(),
3090 [PopupIntent::Present { popup_id }] if *popup_id == current
3091 ));
3092 assert!(matches!(
3093 sibling_handle
3094 .take_pending_popup_intents_for(
3095 sibling_handle.element_tree_id(),
3096 &sibling_window,
3097 generation_one,
3098 )
3099 .as_slice(),
3100 [PopupIntent::Present { popup_id }] if *popup_id == sibling_popup
3101 ));
3102 assert!(matches!(
3103 first_handle
3104 .take_pending_popup_intents_for(
3105 first_handle.element_tree_id(),
3106 &other_window,
3107 generation_one,
3108 )
3109 .as_slice(),
3110 [PopupIntent::Present { popup_id }] if *popup_id == other_window_popup
3111 ));
3112 assert!(first_handle.inner.borrow().pending_popup_intents.is_empty());
3113 }
3114
3115 #[test]
3116 /// Tree release purges only that scope's owner-tagged pending popup records.
3117 fn releasing_tree_scope_purges_its_pending_popup_records_only() {
3118 let shared = RuntimeHandle::<()>::new();
3119 let first = Runtime::new(shared.clone());
3120 let sibling = Runtime::new(shared);
3121 let first_handle = first.runtime.clone();
3122 let sibling_handle = sibling.runtime.clone();
3123 let first_window = LogicalWindowId::new("first");
3124 let sibling_window = LogicalWindowId::new("sibling");
3125 let generation = PresentationGeneration::new(4);
3126 let first_popup = PopupId::new(61);
3127 let sibling_popup = PopupId::new(62);
3128
3129 for (handle, popup_id, window, element) in [
3130 (
3131 &first_handle,
3132 first_popup,
3133 first_window.clone(),
3134 ElementId(1),
3135 ),
3136 (
3137 &sibling_handle,
3138 sibling_popup,
3139 sibling_window.clone(),
3140 ElementId(2),
3141 ),
3142 ] {
3143 handle
3144 .register_popup(PopupRequest::new(
3145 popup_id,
3146 PopupOwner::new(window, generation, handle.element_tree_id(), element),
3147 PopupContent::new(View::empty),
3148 ))
3149 .unwrap();
3150 handle.open_popup_unpositioned(popup_id).unwrap();
3151 }
3152
3153 first_handle.release_element_tree_scope();
3154 first_handle.release_element_tree_scope();
3155
3156 assert!(!first_handle.popup_portal().borrow().contains(first_popup));
3157 assert!(sibling_handle
3158 .popup_portal()
3159 .borrow()
3160 .contains(sibling_popup));
3161 assert!(first_handle
3162 .take_pending_popup_intents_for(
3163 first_handle.element_tree_id(),
3164 &first_window,
3165 generation,
3166 )
3167 .is_empty());
3168 assert!(matches!(
3169 sibling_handle
3170 .take_pending_popup_intents_for(
3171 sibling_handle.element_tree_id(),
3172 &sibling_window,
3173 generation,
3174 )
3175 .as_slice(),
3176 [PopupIntent::Present { popup_id }] if *popup_id == sibling_popup
3177 ));
3178 assert!(first_handle.inner.borrow().pending_popup_intents.is_empty());
3179 }
3180}