ailloli_ui_runtime/input/
event_ctx.rs

1//! Event-dispatch context for actions, invalidation, focus, and external URLs.
2
3use crate::app::runtime_handle::RuntimeHandle;
4use crate::app::{ExternalUrl, Invalidation, InvalidationSource, OpenUrlError};
5use ailloli_ui_core::ids::{ElementId, LogicalWindowId};
6use std::sync::Arc;
7use std::time::Duration;
8
9use super::EventMeta;
10
11/// Mutable capabilities exposed while routing one event to one element.
12///
13/// The context clones a single-threaded [`RuntimeHandle`], stores the strict
14/// target ID, and starts with propagation enabled. Directly constructed
15/// contexts have no host event metadata.
16///
17/// # Examples
18///
19/// ```
20/// use ailloli_ui_core::ElementId;
21/// use ailloli_ui_runtime::app::RuntimeHandle;
22/// use ailloli_ui_runtime::input::EventContext;
23/// let ctx = EventContext::<()>::new(RuntimeHandle::new(), ElementId(4));
24/// assert_eq!(ctx.target(), ElementId(4));
25/// assert!(ctx.event_meta().is_none());
26/// ```
27pub struct EventContext<A> {
28    /// UI-local runtime receiving dispatched actions and host requests.
29    runtime: RuntimeHandle<A>,
30    /// Element to which the current event was routed.
31    target: ElementId,
32    /// Optional immutable provider metadata shared across propagation stages.
33    event_meta: Option<Arc<EventMeta>>,
34    /// Whether this handler stopped further propagation of the current event.
35    propagation_stopped: bool,
36}
37
38/// Short alias for [`EventContext`].
39///
40/// # Examples
41///
42/// ```
43/// use ailloli_ui_core::ElementId;
44/// use ailloli_ui_runtime::app::RuntimeHandle;
45/// use ailloli_ui_runtime::input::EventCtx;
46/// let ctx = EventCtx::<()>::new(RuntimeHandle::new(), ElementId(1));
47/// assert_eq!(ctx.target(), ElementId(1));
48/// ```
49pub type EventCtx<A> = EventContext<A>;
50
51/// Provides the operations defined for `EventContext<A>`.
52impl<A> EventContext<A> {
53    /// Creates a metadata-free context for `target`.
54    ///
55    /// This is primarily useful for direct/legacy dispatch and tests; host event
56    /// routing uses an internal constructor that attaches [`EventMeta`].
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use ailloli_ui_core::ElementId;
62    /// use ailloli_ui_runtime::app::RuntimeHandle;
63    /// use ailloli_ui_runtime::input::EventCtx;
64    /// let ctx = EventCtx::<u8>::new(RuntimeHandle::new(), ElementId(2));
65    /// assert!(!ctx.is_propagation_stopped());
66    /// ```
67    pub fn new(runtime: RuntimeHandle<A>, target: ElementId) -> Self {
68        Self {
69            runtime,
70            target,
71            event_meta: None,
72            propagation_stopped: false,
73        }
74    }
75
76    /// Creates a routed context that shares immutable host metadata.
77    ///
78    /// The `Arc` avoids copying logical window IDs while an event bubbles.
79    ///
80    /// # Examples
81    ///
82    /// Public callers construct metadata-free contexts; the router uses this
83    /// internal variant to make [`Self::event_meta`] return `Some`.
84    ///
85    /// ```
86    /// use ailloli_ui_core::ElementId;
87    /// use ailloli_ui_runtime::app::RuntimeHandle;
88    /// use ailloli_ui_runtime::input::EventContext;
89    /// let ctx = EventContext::<()>::new(RuntimeHandle::new(), ElementId(7));
90    /// assert!(ctx.event_meta().is_none());
91    /// ```
92    pub(crate) fn new_with_event_meta(
93        runtime: RuntimeHandle<A>,
94        target: ElementId,
95        event_meta: Arc<EventMeta>,
96    ) -> Self {
97        Self {
98            runtime,
99            target,
100            event_meta: Some(event_meta),
101            propagation_stopped: false,
102        }
103    }
104
105    /// Returns the strict element that originally received the event.
106    ///
107    /// This remains unchanged while the same context bubbles to ancestors.
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use ailloli_ui_core::ElementId;
113    /// use ailloli_ui_runtime::app::RuntimeHandle;
114    /// use ailloli_ui_runtime::input::EventCtx;
115    /// let ctx = EventCtx::<()>::new(RuntimeHandle::new(), ElementId(8));
116    /// assert_eq!(ctx.target(), ElementId(8));
117    /// ```
118    pub fn target(&self) -> ElementId {
119        self.target
120    }
121
122    /// Metadata for the currently routed event. Legacy direct dispatches do not
123    /// have host metadata and return `None`.
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use ailloli_ui_core::ElementId;
129    /// use ailloli_ui_runtime::app::RuntimeHandle;
130    /// use ailloli_ui_runtime::input::EventCtx;
131    /// assert!(EventCtx::<()>::new(RuntimeHandle::new(), ElementId(1)).event_meta().is_none());
132    /// ```
133    pub fn event_meta(&self) -> Option<&EventMeta> {
134        self.event_meta.as_deref()
135    }
136
137    /// Queues one application action on the shared runtime handle.
138    ///
139    /// Actions are retained in FIFO order until the host drains them.
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use ailloli_ui_core::ElementId;
145    /// use ailloli_ui_runtime::app::RuntimeHandle;
146    /// use ailloli_ui_runtime::input::EventCtx;
147    /// let runtime = RuntimeHandle::<u8>::new();
148    /// let mut ctx = EventCtx::new(runtime.clone(), ElementId(1));
149    /// ctx.dispatch(9);
150    /// assert_eq!(runtime.take_actions(), vec![9]);
151    /// ```
152    pub fn dispatch(&mut self, action: A) {
153        self.runtime.dispatch(action);
154    }
155
156    /// Clones the shared runtime handle for longer-lived UI operations.
157    ///
158    /// The clone shares queues and state but retains this tree namespace.
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use ailloli_ui_core::ElementId;
164    /// use ailloli_ui_runtime::app::RuntimeHandle;
165    /// use ailloli_ui_runtime::input::EventCtx;
166    /// let ctx = EventCtx::<()>::new(RuntimeHandle::new(), ElementId(1));
167    /// assert_eq!(ctx.runtime().element_tree_id(), ctx.runtime().element_tree_id());
168    /// ```
169    pub fn runtime(&self) -> RuntimeHandle<A> {
170        self.runtime.clone()
171    }
172
173    /// Requests application close; repeated requests coalesce to one flag.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use ailloli_ui_core::ElementId;
179    /// use ailloli_ui_runtime::app::RuntimeHandle;
180    /// use ailloli_ui_runtime::input::EventCtx;
181    /// let runtime = RuntimeHandle::<()>::new();
182    /// EventCtx::new(runtime.clone(), ElementId(1)).request_close();
183    /// assert!(runtime.take_close_requested());
184    /// ```
185    pub fn request_close(&self) {
186        self.runtime.request_close();
187    }
188
189    /// Queues a minimize operation for a logical window ID.
190    ///
191    /// Requests preserve FIFO order and are not deduplicated.
192    ///
193    /// # Examples
194    ///
195    /// ```
196    /// use ailloli_ui_core::ElementId;
197    /// use ailloli_ui_runtime::app::{RuntimeHandle, WindowChromeOp};
198    /// use ailloli_ui_runtime::input::EventCtx;
199    /// let runtime = RuntimeHandle::<()>::new();
200    /// EventCtx::new(runtime.clone(), ElementId(1)).request_minimize_window("main");
201    /// assert_eq!(runtime.take_window_chrome_ops()[0].1, WindowChromeOp::Minimize);
202    /// ```
203    pub fn request_minimize_window(&self, logical_window_id: impl Into<LogicalWindowId>) {
204        self.runtime.request_window_minimize(logical_window_id);
205    }
206
207    /// Queues a maximize/restore toggle for a logical window ID.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use ailloli_ui_core::ElementId;
213    /// use ailloli_ui_runtime::app::{RuntimeHandle, WindowChromeOp};
214    /// use ailloli_ui_runtime::input::EventCtx;
215    /// let runtime = RuntimeHandle::<()>::new();
216    /// EventCtx::new(runtime.clone(), ElementId(1)).request_toggle_maximize_window("main");
217    /// assert_eq!(runtime.take_window_chrome_ops()[0].1, WindowChromeOp::ToggleMaximize);
218    /// ```
219    pub fn request_toggle_maximize_window(&self, logical_window_id: impl Into<LogicalWindowId>) {
220        self.runtime
221            .request_window_toggle_maximize(logical_window_id);
222    }
223
224    /// Stops bubbling after the current widget callback returns.
225    ///
226    /// Calling this repeatedly is idempotent and does not undo work already
227    /// performed by the target callback.
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// use ailloli_ui_core::ElementId;
233    /// use ailloli_ui_runtime::app::RuntimeHandle;
234    /// use ailloli_ui_runtime::input::EventCtx;
235    /// let mut ctx = EventCtx::<()>::new(RuntimeHandle::new(), ElementId(1));
236    /// ctx.stop_propagation();
237    /// assert!(ctx.is_propagation_stopped());
238    /// ```
239    pub fn stop_propagation(&mut self) {
240        self.propagation_stopped = true;
241    }
242
243    /// Returns whether an event handler requested that bubbling stop.
244    ///
245    /// # Examples
246    ///
247    /// ```
248    /// use ailloli_ui_core::ElementId;
249    /// use ailloli_ui_runtime::app::RuntimeHandle;
250    /// use ailloli_ui_runtime::input::EventCtx;
251    /// let ctx = EventCtx::<()>::new(RuntimeHandle::new(), ElementId(1));
252    /// assert!(!ctx.is_propagation_stopped());
253    /// ```
254    pub fn is_propagation_stopped(&self) -> bool {
255        self.propagation_stopped
256    }
257
258    /// Marks the target for paint work with event provenance.
259    ///
260    /// # Examples
261    ///
262    /// ```
263    /// use ailloli_ui_core::ElementId;
264    /// use ailloli_ui_runtime::app::RuntimeHandle;
265    /// use ailloli_ui_runtime::input::EventCtx;
266    /// let runtime = RuntimeHandle::<()>::new();
267    /// EventCtx::new(runtime.clone(), ElementId(3)).request_repaint();
268    /// assert_eq!(runtime.take_dirty_elements(), vec![ElementId(3)]);
269    /// ```
270    pub fn request_repaint(&mut self) {
271        self.invalidate(Invalidation::Paint);
272    }
273
274    /// Schedules target paint invalidation no earlier than `delay` from now.
275    ///
276    /// Equal target/kind timers are deduplicated by the runtime's earliest due
277    /// instant. Duration-to-instant overflow behavior is delegated to it.
278    ///
279    /// # Examples
280    ///
281    /// ```
282    /// use std::time::Duration;
283    /// use ailloli_ui_core::ElementId;
284    /// use ailloli_ui_runtime::app::RuntimeHandle;
285    /// use ailloli_ui_runtime::input::EventCtx;
286    /// let runtime = RuntimeHandle::<()>::new();
287    /// EventCtx::new(runtime.clone(), ElementId(1)).request_repaint_after(Duration::from_millis(5));
288    /// assert!(runtime.next_scheduled_repaint_due().is_some());
289    /// ```
290    pub fn request_repaint_after(&mut self, delay: Duration) {
291        self.runtime.request_repaint_after(self.target, delay);
292    }
293
294    /// Marks the target with an explicit invalidation and event provenance.
295    ///
296    /// Repeated requests coalesce to the strongest level: build, layout, then
297    /// paint. Diagnostics retain whether a request was coalesced.
298    ///
299    /// # Examples
300    ///
301    /// ```
302    /// use ailloli_ui_core::ElementId;
303    /// use ailloli_ui_runtime::app::{Invalidation, RuntimeHandle};
304    /// use ailloli_ui_runtime::input::EventCtx;
305    /// let runtime = RuntimeHandle::<()>::new();
306    /// EventCtx::new(runtime.clone(), ElementId(2)).invalidate(Invalidation::Layout);
307    /// assert!(runtime.frame_work_plan().needs_layout());
308    /// ```
309    pub fn invalidate(&mut self, invalidation: Invalidation) {
310        self.runtime
311            .invalidate_from(self.target, invalidation, InvalidationSource::Event);
312    }
313
314    /// Marks the target for layout and paint work with event provenance.
315    ///
316    /// # Examples
317    ///
318    /// ```
319    /// use ailloli_ui_core::ElementId;
320    /// use ailloli_ui_runtime::app::RuntimeHandle;
321    /// use ailloli_ui_runtime::input::EventCtx;
322    /// let runtime = RuntimeHandle::<()>::new();
323    /// EventCtx::new(runtime.clone(), ElementId(2)).request_layout();
324    /// assert!(runtime.frame_work_plan().needs_layout());
325    /// ```
326    pub fn request_layout(&mut self) {
327        self.invalidate(Invalidation::Layout);
328    }
329
330    /// Marks the target for build, layout, and paint work with event provenance.
331    ///
332    /// # Examples
333    ///
334    /// ```
335    /// use ailloli_ui_core::ElementId;
336    /// use ailloli_ui_runtime::app::RuntimeHandle;
337    /// use ailloli_ui_runtime::input::EventCtx;
338    /// let runtime = RuntimeHandle::<()>::new();
339    /// EventCtx::new(runtime.clone(), ElementId(2)).request_build();
340    /// assert!(runtime.frame_work_plan().needs_build());
341    /// ```
342    pub fn request_build(&mut self) {
343        self.invalidate(Invalidation::Build);
344    }
345
346    /// Schedules target layout invalidation after `delay`.
347    ///
348    /// # Examples
349    ///
350    /// ```
351    /// use std::time::Duration;
352    /// use ailloli_ui_core::ElementId;
353    /// use ailloli_ui_runtime::app::RuntimeHandle;
354    /// use ailloli_ui_runtime::input::EventCtx;
355    /// let runtime = RuntimeHandle::<()>::new();
356    /// EventCtx::new(runtime.clone(), ElementId(1)).request_layout_after(Duration::from_millis(1));
357    /// assert!(runtime.next_scheduled_repaint_due().is_some());
358    /// ```
359    pub fn request_layout_after(&mut self, delay: Duration) {
360        self.runtime.request_layout_after(self.target, delay);
361    }
362
363    /// Schedules target build invalidation after `delay`.
364    ///
365    /// # Examples
366    ///
367    /// ```
368    /// use std::time::Duration;
369    /// use ailloli_ui_core::ElementId;
370    /// use ailloli_ui_runtime::app::RuntimeHandle;
371    /// use ailloli_ui_runtime::input::EventCtx;
372    /// let runtime = RuntimeHandle::<()>::new();
373    /// EventCtx::new(runtime.clone(), ElementId(1)).request_build_after(Duration::from_millis(1));
374    /// assert!(runtime.next_scheduled_repaint_due().is_some());
375    /// ```
376    pub fn request_build_after(&mut self, delay: Duration) {
377        self.runtime.request_build_after(self.target, delay);
378    }
379
380    /// Replaces the pending declarative focus-key request for this tree.
381    ///
382    /// The latest request wins; an empty string is stored as a real key rather
383    /// than interpreted as `None`.
384    ///
385    /// # Examples
386    ///
387    /// ```
388    /// use ailloli_ui_core::ElementId;
389    /// use ailloli_ui_runtime::app::RuntimeHandle;
390    /// use ailloli_ui_runtime::input::EventCtx;
391    /// let runtime = RuntimeHandle::<()>::new();
392    /// EventCtx::new(runtime.clone(), ElementId(1)).request_focus_key("search");
393    /// assert_eq!(runtime.take_focus_key_request().as_deref(), Some("search"));
394    /// ```
395    pub fn request_focus_key(&self, key: impl Into<String>) {
396        self.runtime.request_focus_key(key);
397    }
398
399    /// Reads text from the configured clipboard provider.
400    ///
401    /// `None` means no text is available; provider-specific failures are also
402    /// represented as `None` by this interface. The built-in memory provider
403    /// starts with an available empty string, so it returns `Some("")`.
404    ///
405    /// # Examples
406    ///
407    /// ```
408    /// use ailloli_ui_core::ElementId;
409    /// use ailloli_ui_runtime::app::RuntimeHandle;
410    /// use ailloli_ui_runtime::input::EventCtx;
411    /// let ctx = EventCtx::<()>::new(RuntimeHandle::new(), ElementId(1));
412    /// assert_eq!(ctx.read_clipboard_text().as_deref(), Some(""));
413    /// ```
414    pub fn read_clipboard_text(&self) -> Option<String> {
415        self.runtime.read_clipboard_text()
416    }
417
418    /// Writes text through the configured clipboard provider.
419    ///
420    /// Provider errors are returned as strings. The built-in memory provider
421    /// accepts empty and arbitrary UTF-8 strings.
422    ///
423    /// # Errors
424    ///
425    /// Returns the configured clipboard provider's display string when it
426    /// rejects or cannot complete the write.
427    ///
428    /// # Examples
429    ///
430    /// ```
431    /// use ailloli_ui_core::ElementId;
432    /// use ailloli_ui_runtime::app::RuntimeHandle;
433    /// use ailloli_ui_runtime::input::EventCtx;
434    /// let runtime = RuntimeHandle::<()>::new();
435    /// let ctx = EventCtx::new(runtime, ElementId(1));
436    /// ctx.write_clipboard_text("copied").unwrap();
437    /// assert_eq!(ctx.read_clipboard_text().as_deref(), Some("copied"));
438    /// ```
439    pub fn write_clipboard_text(&self, text: &str) -> Result<(), String> {
440        self.runtime.write_clipboard_text(text)
441    }
442
443    /// Opens an already validated HTTP(S) URL through the configured provider.
444    ///
445    /// Provider failure is returned and also recorded by the runtime handle as
446    /// a non-fatal error. No parsing, shell escaping, or scheme validation is
447    /// performed here; construct URLs with [`ExternalUrl::parse`].
448    ///
449    /// # Errors
450    ///
451    /// Returns [`OpenUrlError::Unavailable`] when no opener is available or
452    /// [`OpenUrlError::LaunchFailed`] when the configured opener rejects the
453    /// request. The runtime also records the same non-fatal error.
454    ///
455    /// # Examples
456    ///
457    /// ```
458    /// use ailloli_ui_core::ElementId;
459    /// use ailloli_ui_runtime::app::{ExternalUrl, RuntimeHandle};
460    /// use ailloli_ui_runtime::input::EventCtx;
461    /// let ctx = EventCtx::<()>::new(RuntimeHandle::new(), ElementId(1));
462    /// let url = ExternalUrl::parse("https://example.com/docs").unwrap();
463    /// assert!(ctx.open_external_url(&url).is_ok());
464    /// ```
465    pub fn open_external_url(&self, url: &ExternalUrl) -> Result<(), OpenUrlError> {
466        self.runtime.open_external_url(url)
467    }
468}