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}