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}