ailloli_ui_runtime/component/
context.rs

1//! Component context for state, services, invalidation, and runtime access.
2
3use crate::app::runtime_handle::RuntimeHandle;
4use crate::app::{Invalidation, InvalidationSource, UiServiceRegistration};
5use crate::component::signal::Signal;
6use crate::component::state::State;
7use ailloli_ui_core::ids::ElementId;
8use std::rc::Rc;
9use std::time::Duration;
10
11/// Capabilities and hook-slot cursor for one synchronous component build.
12///
13/// A fresh context starts at signal slot zero. Components must call state/signal
14/// constructors in stable order and with a stable type per slot across builds;
15/// changing that order/type violates the type-erased [`crate::app::StateStore`]
16/// invariant and can panic. The context and its runtime handle are UI-thread
17/// local (`Rc`/`RefCell`), not `Send` or `Sync`.
18///
19/// # Examples
20///
21/// ```
22/// use ailloli_ui_core::ElementId;
23/// use ailloli_ui_runtime::app::RuntimeHandle;
24/// use ailloli_ui_runtime::component::Context;
25/// let ctx = Context::<()>::new(ElementId(1), RuntimeHandle::new());
26/// assert_eq!(ctx.element_id(), ElementId(1));
27/// ```
28pub struct Context<A> {
29    /// Retained element currently being built.
30    element_id: ElementId,
31    /// UI-local runtime used for hooks, actions, and invalidation.
32    runtime: RuntimeHandle<A>,
33    /// Zero-based hook slot assigned to the next state lookup in this build.
34    next_signal_slot: usize,
35}
36
37/// Provides the operations defined for `Context<A>`.
38impl<A: 'static> Context<A> {
39    /// Creates a build context for `element_id` at hook slot zero.
40    ///
41    /// The ID is stored verbatim and need not currently exist in a tree. The
42    /// runtime clone determines the element-tree namespace used for state and
43    /// invalidation.
44    ///
45    /// # Examples
46    ///
47    /// ```
48    /// use ailloli_ui_core::ElementId;
49    /// use ailloli_ui_runtime::app::RuntimeHandle;
50    /// use ailloli_ui_runtime::component::Context;
51    /// let ctx = Context::<()>::new(ElementId(5), RuntimeHandle::new());
52    /// assert_eq!(ctx.element_id(), ElementId(5));
53    /// ```
54    pub fn new(element_id: ElementId, runtime: RuntimeHandle<A>) -> Self {
55        Self {
56            element_id,
57            runtime,
58            next_signal_slot: 0,
59        }
60    }
61
62    /// Returns the retained component element targeted by this context.
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use ailloli_ui_core::ElementId;
68    /// use ailloli_ui_runtime::app::RuntimeHandle;
69    /// use ailloli_ui_runtime::component::Context;
70    /// assert_eq!(Context::<()>::new(ElementId(9), RuntimeHandle::new()).element_id(), ElementId(9));
71    /// ```
72    pub fn element_id(&self) -> ElementId {
73        self.element_id
74    }
75
76    /// Returns the signal in the next hook slot, creating it from `initial` once.
77    ///
78    /// Writes request build invalidation. On later builds, an existing slot
79    /// keeps its current value and ignores `initial`. Calls advance the slot
80    /// cursor, so order and type must remain stable.
81    ///
82    /// # Panics
83    ///
84    /// Panics if an existing slot contains a different Rust type or if internal
85    /// `RefCell` borrows conflict. Slot-index overflow follows build overflow
86    /// settings and is practically unreachable.
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use ailloli_ui_core::ElementId;
92    /// use ailloli_ui_runtime::app::RuntimeHandle;
93    /// use ailloli_ui_runtime::component::Context;
94    /// let runtime = RuntimeHandle::<()>::new();
95    /// let mut ctx = Context::new(ElementId(1), runtime.clone());
96    /// let value = ctx.signal(3_u8);
97    /// value.set(4);
98    /// assert_eq!(value.read(), 4);
99    /// assert!(runtime.frame_work_plan().needs_build());
100    /// ```
101    pub fn signal<T: 'static>(&mut self, initial: T) -> Signal<T> {
102        self.signal_with_invalidation(initial, Invalidation::Build)
103    }
104
105    /// Lazily initializes the signal in the next build hook slot.
106    ///
107    /// The factory runs only when that tree/element/slot has no stored signal;
108    /// existing state skips it. Writes request build invalidation.
109    ///
110    /// # Panics
111    ///
112    /// Has the same stable-order/type and borrow requirements as [`Self::signal`].
113    /// Factory panics propagate without installing a value.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use std::cell::Cell;
119    /// use ailloli_ui_core::ElementId;
120    /// use ailloli_ui_runtime::app::RuntimeHandle;
121    /// use ailloli_ui_runtime::component::Context;
122    /// let calls = Cell::new(0);
123    /// let mut ctx = Context::<()>::new(ElementId(1), RuntimeHandle::new());
124    /// let signal = ctx.signal_with(|| { calls.set(calls.get() + 1); 7 });
125    /// assert_eq!(signal.read(), 7);
126    /// assert_eq!(calls.get(), 1);
127    /// ```
128    pub fn signal_with<T: 'static>(&mut self, initial: impl FnOnce() -> T) -> Signal<T> {
129        self.signal_with_invalidation_factory(initial, Invalidation::Build)
130    }
131
132    /// Creates a signal whose writes request the declared retained work.
133    ///
134    /// `Build` matches the historical [`Self::signal`] behavior. Widget-local
135    /// state that does not alter the declarative subtree can opt into `Layout`
136    /// or `Paint` without rebuilding sibling components.
137    ///
138    /// Existing state ignores `initial`, but the returned signal uses the
139    /// invalidator installed when the slot was originally created. Reusing a
140    /// slot with a different invalidation level does not replace that callback.
141    ///
142    /// # Panics
143    ///
144    /// Has the same stable-order/type and borrow requirements as [`Self::signal`].
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use ailloli_ui_core::ElementId;
150    /// use ailloli_ui_runtime::app::{Invalidation, RuntimeHandle};
151    /// use ailloli_ui_runtime::component::Context;
152    /// let runtime = RuntimeHandle::<()>::new();
153    /// let mut ctx = Context::new(ElementId(2), runtime.clone());
154    /// ctx.signal_with_invalidation(false, Invalidation::Paint).set(true);
155    /// let plan = runtime.frame_work_plan();
156    /// assert!(plan.needs_paint() && !plan.needs_layout());
157    /// ```
158    pub fn signal_with_invalidation<T: 'static>(
159        &mut self,
160        initial: T,
161        invalidation: Invalidation,
162    ) -> Signal<T> {
163        self.signal_with_invalidation_factory(|| initial, invalidation)
164    }
165
166    /// Lazily initializes a signal with a chosen write-invalidation level.
167    ///
168    /// The next slot index is reserved before the factory runs. Existing slots
169    /// do not invoke `initial` and retain their originally installed invalidator.
170    ///
171    /// # Panics
172    ///
173    /// Panics on a stored type mismatch, conflicting interior borrow, or
174    /// propagated factory panic. Hook-order changes can therefore fail during
175    /// component reconciliation.
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use ailloli_ui_core::ElementId;
181    /// use ailloli_ui_runtime::app::{Invalidation, RuntimeHandle};
182    /// use ailloli_ui_runtime::component::Context;
183    /// let runtime = RuntimeHandle::<()>::new();
184    /// let mut ctx = Context::new(ElementId(2), runtime.clone());
185    /// let signal = ctx.signal_with_invalidation_factory(|| String::from("ready"), Invalidation::Layout);
186    /// signal.set(String::from("changed"));
187    /// assert!(runtime.frame_work_plan().needs_layout());
188    /// ```
189    pub fn signal_with_invalidation_factory<T: 'static>(
190        &mut self,
191        initial: impl FnOnce() -> T,
192        invalidation: Invalidation,
193    ) -> Signal<T> {
194        let slot_index = self.next_signal_slot;
195        self.next_signal_slot += 1;
196
197        let element_id = self.element_id;
198        let invalidate = self
199            .runtime
200            .weak_signal_invalidator(element_id, invalidation);
201
202        let element_tree_id = self.runtime.element_tree_id();
203        self.runtime
204            .inner
205            .borrow_mut()
206            .states
207            .borrow_mut()
208            .signal_scoped_with(
209                element_tree_id,
210                self.element_id,
211                slot_index,
212                initial,
213                invalidate,
214            )
215    }
216
217    /// Returns [`State`] backed by the next build-invalidating signal slot.
218    ///
219    /// # Panics
220    ///
221    /// Has the same stable-order/type and borrow requirements as [`Self::signal`].
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// use ailloli_ui_core::ElementId;
227    /// use ailloli_ui_runtime::app::RuntimeHandle;
228    /// use ailloli_ui_runtime::component::Context;
229    /// let mut ctx = Context::<()>::new(ElementId(1), RuntimeHandle::new());
230    /// let state = ctx.state(10);
231    /// state.update(|value| *value += 2);
232    /// assert_eq!(state.read(), 12);
233    /// ```
234    pub fn state<T: 'static>(&mut self, initial: T) -> State<T> {
235        State::from_signal(self.signal(initial))
236    }
237
238    /// Queues one application action in FIFO order.
239    ///
240    /// # Examples
241    ///
242    /// ```
243    /// use ailloli_ui_core::ElementId;
244    /// use ailloli_ui_runtime::app::RuntimeHandle;
245    /// use ailloli_ui_runtime::component::Context;
246    /// let runtime = RuntimeHandle::<u8>::new();
247    /// Context::new(ElementId(1), runtime.clone()).dispatch(6);
248    /// assert_eq!(runtime.take_actions(), vec![6]);
249    /// ```
250    pub fn dispatch(&self, action: A) {
251        self.runtime.dispatch(action);
252    }
253
254    /// Marks this component element with explicit context-origin invalidation.
255    ///
256    /// Repeated requests coalesce to the strongest work level and diagnostics
257    /// preserve the request provenance.
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// use ailloli_ui_core::ElementId;
263    /// use ailloli_ui_runtime::app::{Invalidation, RuntimeHandle};
264    /// use ailloli_ui_runtime::component::Context;
265    /// let runtime = RuntimeHandle::<()>::new();
266    /// Context::new(ElementId(4), runtime.clone()).invalidate(Invalidation::Layout);
267    /// assert!(runtime.frame_work_plan().needs_layout());
268    /// ```
269    pub fn invalidate(&self, invalidation: Invalidation) {
270        self.runtime
271            .invalidate_from(self.element_id, invalidation, InvalidationSource::Context);
272    }
273
274    /// Requests paint work for this component element.
275    ///
276    /// # Examples
277    ///
278    /// ```
279    /// use ailloli_ui_core::ElementId;
280    /// use ailloli_ui_runtime::app::RuntimeHandle;
281    /// use ailloli_ui_runtime::component::Context;
282    /// let runtime = RuntimeHandle::<()>::new();
283    /// Context::new(ElementId(1), runtime.clone()).request_repaint();
284    /// assert!(runtime.frame_work_plan().needs_paint());
285    /// ```
286    pub fn request_repaint(&self) {
287        self.invalidate(Invalidation::Paint);
288    }
289
290    /// Requests layout and paint work for this component element.
291    ///
292    /// # Examples
293    ///
294    /// ```
295    /// use ailloli_ui_core::ElementId;
296    /// use ailloli_ui_runtime::app::RuntimeHandle;
297    /// use ailloli_ui_runtime::component::Context;
298    /// let runtime = RuntimeHandle::<()>::new();
299    /// Context::new(ElementId(1), runtime.clone()).request_layout();
300    /// assert!(runtime.frame_work_plan().needs_layout());
301    /// ```
302    pub fn request_layout(&self) {
303        self.invalidate(Invalidation::Layout);
304    }
305
306    /// Requests build, layout, and paint work for this component element.
307    ///
308    /// # Examples
309    ///
310    /// ```
311    /// use ailloli_ui_core::ElementId;
312    /// use ailloli_ui_runtime::app::RuntimeHandle;
313    /// use ailloli_ui_runtime::component::Context;
314    /// let runtime = RuntimeHandle::<()>::new();
315    /// Context::new(ElementId(1), runtime.clone()).request_build();
316    /// assert!(runtime.frame_work_plan().needs_build());
317    /// ```
318    pub fn request_build(&self) {
319        self.invalidate(Invalidation::Build);
320    }
321
322    /// Weak callback suitable for retained model subscriptions. The callback
323    /// never keeps the runtime or its widget tree alive.
324    ///
325    /// While the runtime exists, invocation records `InvalidationSource::Model`
326    /// through the handle's weak invalidator. After all strong handles are
327    /// dropped, invocation is a harmless no-op.
328    ///
329    /// # Examples
330    ///
331    /// ```
332    /// use ailloli_ui_core::ElementId;
333    /// use ailloli_ui_runtime::app::{Invalidation, RuntimeHandle};
334    /// use ailloli_ui_runtime::component::Context;
335    /// let runtime = RuntimeHandle::<()>::new();
336    /// let callback = Context::new(ElementId(3), runtime.clone()).invalidation_target(Invalidation::Paint);
337    /// callback();
338    /// assert_eq!(runtime.take_dirty_elements(), vec![ElementId(3)]);
339    /// ```
340    pub fn invalidation_target(&self, invalidation: Invalidation) -> Rc<dyn Fn()> {
341        self.runtime.weak_invalidator(self.element_id, invalidation)
342    }
343
344    /// Schedules paint invalidation no earlier than `delay` from now.
345    ///
346    /// Timers are tree-scoped and the runtime deduplicates compatible requests
347    /// by earliest due instant.
348    ///
349    /// # Examples
350    ///
351    /// ```
352    /// use std::time::Duration;
353    /// use ailloli_ui_core::ElementId;
354    /// use ailloli_ui_runtime::app::RuntimeHandle;
355    /// use ailloli_ui_runtime::component::Context;
356    /// let runtime = RuntimeHandle::<()>::new();
357    /// Context::new(ElementId(1), runtime.clone()).request_repaint_after(Duration::from_millis(1));
358    /// assert!(runtime.next_scheduled_repaint_due().is_some());
359    /// ```
360    pub fn request_repaint_after(&self, delay: Duration) {
361        self.runtime.request_repaint_after(self.element_id, delay);
362    }
363
364    /// Schedules layout invalidation no earlier than `delay` from now.
365    ///
366    /// # Examples
367    ///
368    /// ```
369    /// use std::time::Duration;
370    /// use ailloli_ui_core::ElementId;
371    /// use ailloli_ui_runtime::app::RuntimeHandle;
372    /// use ailloli_ui_runtime::component::Context;
373    /// let runtime = RuntimeHandle::<()>::new();
374    /// Context::new(ElementId(1), runtime.clone()).request_layout_after(Duration::from_millis(1));
375    /// assert!(runtime.next_scheduled_repaint_due().is_some());
376    /// ```
377    pub fn request_layout_after(&self, delay: Duration) {
378        self.runtime.request_layout_after(self.element_id, delay);
379    }
380
381    /// Schedules build invalidation no earlier than `delay` from now.
382    ///
383    /// # Examples
384    ///
385    /// ```
386    /// use std::time::Duration;
387    /// use ailloli_ui_core::ElementId;
388    /// use ailloli_ui_runtime::app::RuntimeHandle;
389    /// use ailloli_ui_runtime::component::Context;
390    /// let runtime = RuntimeHandle::<()>::new();
391    /// Context::new(ElementId(1), runtime.clone()).request_build_after(Duration::from_millis(1));
392    /// assert!(runtime.next_scheduled_repaint_due().is_some());
393    /// ```
394    pub fn request_build_after(&self, delay: Duration) {
395        self.runtime.request_build_after(self.element_id, delay);
396    }
397
398    /// Clones the runtime handle while preserving this tree namespace.
399    ///
400    /// # Examples
401    ///
402    /// ```
403    /// use ailloli_ui_core::ElementId;
404    /// use ailloli_ui_runtime::app::RuntimeHandle;
405    /// use ailloli_ui_runtime::component::Context;
406    /// let ctx = Context::<()>::new(ElementId(1), RuntimeHandle::new());
407    /// assert_eq!(ctx.runtime().element_tree_id(), ctx.runtime().element_tree_id());
408    /// ```
409    pub fn runtime(&self) -> RuntimeHandle<A> {
410        self.runtime.clone()
411    }
412
413    /// Registers a polled UI service and returns its lifetime guard.
414    ///
415    /// The callback is invoked by [`RuntimeHandle::service_ui_sources`] and
416    /// returns whether it changed component-visible state. A `true` result
417    /// requests `Build` for this exact component owner and mount generation;
418    /// `false` queues no retained work. Dropping the registration removes only
419    /// that tree-scoped service. Callback panics propagate from servicing.
420    ///
421    /// # Examples
422    ///
423    /// ```
424    /// use std::rc::Rc;
425    /// use ailloli_ui_core::ElementId;
426    /// use ailloli_ui_runtime::app::RuntimeHandle;
427    /// use ailloli_ui_runtime::component::Context;
428    /// let runtime = RuntimeHandle::<()>::new();
429    /// let service: Rc<dyn Fn() -> bool> = Rc::new(|| true);
430    /// let registration = Context::new(ElementId(1), runtime.clone()).register_ui_service(&service);
431    /// assert!(runtime.service_ui_sources());
432    /// drop(registration);
433    /// assert!(!runtime.service_ui_sources());
434    /// ```
435    pub fn register_ui_service(&self, service: &Rc<dyn Fn() -> bool>) -> UiServiceRegistration<A> {
436        self.runtime
437            .register_owned_ui_service(self.element_id, service)
438    }
439}