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}