Skip to main content

compose_rust/runtime/
host.rs

1//! The slot table runtime as the boundary sees it, in two halves.
2//!
3//! [`Recomposer`] is the part that builds and changes the tree: it composes the root,
4//! recomposes what a state change invalidated, and runs the handler an event names. It
5//! writes the tree's records and nothing else, so it can sit behind any Host that owns
6//! the rest of a batch. That is the same division the Dioxus adapter has, where a
7//! `VirtualDom` builds the tree and the Host owns the theme, the window, assets, messages,
8//! notifications and the frame clock.
9//!
10//! That rest is compose-rust's own [`Host`], which drives any [`Runtime`]; the Recomposer
11//! is one. The window, the design system and the notification permission reach the
12//! composition the way they reach a Dioxus hook, by subscribing to the modules the Host
13//! publishes them to. [`ComposeHost`] is the Host with a Recomposer in it.
14
15use super::Composition;
16use super::composer::{Composer, EventCallback, ROOT};
17use super::seam::{Batch, Runtime};
18use super::{drain_graveyard, effects, state, with_composer};
19use crate::boundary::{Host, request_frame_from_worker};
20use crate::protocol::{HostEvent, ProtocolError};
21use crate::schema::{EventPayload, Theme};
22use crate::{KeyEvent, RangeRequest};
23use std::any::Any;
24use std::rc::Rc;
25use std::sync::Arc;
26
27/// The slot table runtime for one root: what composes the tree and runs its handlers.
28///
29/// Every method enters the composition for its own length, so it may be called from any
30/// Host call on the UI thread. The tree's records go into [`Recomposer::writer`]'s batch,
31/// between whatever the Host writes before and after them.
32pub struct Recomposer {
33    content: Rc<dyn Fn()>,
34    composition: Rc<Composition>,
35    /// What the Renderer reports, followed the way a Dioxus hook follows it, for as long
36    /// as this composition lives.
37    _subscriptions: Vec<Box<dyn Any>>,
38}
39
40impl Recomposer {
41    /// A runtime for `content`, composed by [`Recomposer::rebuild`].
42    pub fn new(content: impl Fn() + 'static) -> Self {
43        let composition = Composition::new();
44        let (window, design, permission) = {
45            let composer = composition.composer.borrow();
46            (
47                composer.ambient.window.clone(),
48                composer.ambient.design.clone(),
49                composer.ambient.permission.clone(),
50            )
51        };
52        // The values themselves stay in the modules the Host publishes them to. A state
53        // per value is what lets a composable read one and be run again when it moves,
54        // and the subscription is what moves it.
55        let subscriptions: Vec<Box<dyn Any>> = vec![
56            Box::new(crate::window::subscribe(Arc::new(move || {
57                window.set(crate::window::window_size())
58            }))),
59            Box::new(crate::design::subscribe(Arc::new(move || {
60                design.set(crate::design::design_system())
61            }))),
62            Box::new(crate::notification::subscribe_permission(Arc::new(
63                move || permission.set(crate::notification::notification_permission()),
64            ))),
65        ];
66        Self {
67            content: Rc::new(content),
68            composition,
69            _subscriptions: subscriptions,
70        }
71    }
72
73    /// The batch arena.
74    pub fn arena(&self) -> (*const u8, usize) {
75        self.composition.composer.borrow().writer.arena()
76    }
77
78    /// Whether `node_id` is a `Window` node.
79    pub fn is_window(&self, node_id: u32) -> bool {
80        self.composition.composer.borrow().writer.is_window(node_id)
81    }
82
83    /// The application's name for a node it asked to have measured, if it asked.
84    pub fn size_token(&self, node_id: u32) -> Option<u32> {
85        self.composition
86            .composer
87            .borrow()
88            .writer
89            .size_token(node_id)
90    }
91
92    /// Composes the whole tree. Called once, on a fresh runtime.
93    pub fn rebuild(&mut self) {
94        let _composition = self.composition.enter();
95        with_composer(|composer| {
96            composer.abandon_composition();
97            composer.begin_root();
98        });
99        (self.content)();
100        with_composer(Composer::end_root);
101        self.after_composition();
102    }
103
104    /// Writes whatever changed since the last call: a frame's time to whatever waits for
105    /// one, the futures that woke, and the scopes the states written since invalidated.
106    pub fn render(&mut self, frame_time_nanos: Option<u64>) {
107        let _composition = self.composition.enter();
108        if let Some(now) = frame_time_nanos {
109            with_composer(|composer| composer.executor.deliver_frame(now));
110        }
111        effects::poll_woken();
112        self.recompose();
113    }
114
115    /// Runs the handler an event names, and nothing else; the caller renders afterwards.
116    ///
117    /// Returns what the boundary hands back as the call's result, which is how a key
118    /// handler says it consumed the key. An event naming a handler or node this
119    /// composition does not have is an error, and so is a payload no handler can take.
120    pub fn handle_event(&mut self, event: &HostEvent<'_>) -> Result<i64, ProtocolError> {
121        let _composition = self.composition.enter();
122        let Some((node, callback)) = with_composer(|composer| {
123            composer
124                .handlers
125                .get(&event.handler_id)
126                .map(|entry| (entry.node, entry.callback.clone()))
127        })
128        .flatten() else {
129            return Err(ProtocolError::InvalidValueKind(0));
130        };
131        if node != event.node_id {
132            return Err(ProtocolError::InvalidValueKind(0));
133        }
134        // A call to `window_size()` or `zoom()` from the handler means the window the
135        // handler's node is in.
136        let window = with_composer(|composer| composer.writer.window_of(node)).unwrap_or(0);
137        let _window = crate::windows::DispatchWindow::enter(window);
138        deliver(&callback, &event.payload)
139    }
140
141    /// Runs code the application handed to the Host, a message's action for one, with
142    /// this composition current, so the states it writes are picked up by the next
143    /// render.
144    pub fn run_in_context(&mut self, action: &mut dyn FnMut()) {
145        let _composition = self.composition.enter();
146        action();
147    }
148
149    /// One round of recomposition: the states written since the last round mark their
150    /// readers, and the readers run, outermost first.
151    ///
152    /// A state written while this round composes is left for the next frame, which the
153    /// write asks for: composing describes the screen, and a description that changes
154    /// what it describes is a loop the frame clock should pace, not this call.
155    fn recompose(&mut self) {
156        with_composer(Composer::abandon_composition);
157        state::apply_state_changes();
158        let root = with_composer(|composer| composer.root_invalid()).unwrap_or(false);
159        if root {
160            with_composer(Composer::begin_root);
161            (self.content)();
162            with_composer(Composer::end_root);
163        }
164        let invalid = with_composer(Composer::take_invalid).unwrap_or_default();
165        for group in invalid {
166            if group == ROOT {
167                continue;
168            }
169            let restart = with_composer(|composer| composer.prepare_restart(group)).flatten();
170            if let Some(restart) = restart {
171                restart();
172                with_composer(Composer::finish_restart);
173            }
174        }
175        self.after_composition();
176    }
177
178    fn after_composition(&mut self) {
179        // What the composition kept for a window that closed goes with it. The entries are
180        // buried and dropped below, with the composer free.
181        with_composer(|composer| {
182            for window in composer.writer.take_forgotten_windows() {
183                if let Some(entry) = composer.ambient.windows.remove(&window) {
184                    composer.graveyard.push(Box::new(entry));
185                }
186            }
187        });
188        drain_graveyard();
189        effects::apply_effects();
190        drain_graveyard();
191        effects::poll_woken();
192        // Whatever was written during this round, by the composition, an effect or a
193        // future, is the next frame's.
194        let pending = with_composer(|composer| {
195            !composer
196                .signal
197                .dirty
198                .lock()
199                .unwrap_or_else(|poison| poison.into_inner())
200                .is_empty()
201                || !composer.invalid.is_empty()
202        })
203        .unwrap_or(false);
204        if pending {
205            request_frame_from_worker();
206        }
207    }
208}
209
210/// The Host of a composable application: compose-rust's [`Host`] driving a
211/// [`Recomposer`].
212///
213/// The boundary builds one on the UI thread for an application launched with
214/// [`launch`]. Tests and benchmarks build one directly, which is why it is public; it
215/// derefs to the [`Host`], whose calls are the boundary's.
216pub struct ComposeHost(Host);
217
218impl ComposeHost {
219    /// The Host for `content`, in the theme the application launched with.
220    pub fn new(content: fn()) -> Self {
221        Self(Host::new(recomposer_for(content)))
222    }
223
224    /// The same, in the given theme.
225    pub fn with_theme(content: fn(), theme: Theme) -> Self {
226        Self(Host::with_theme(recomposer_for(content), theme))
227    }
228
229    /// The same Host with other window settings than the launch's, such as
230    /// `Window::new().with_windows()` for an application of several windows.
231    pub fn with_window(mut self, window: crate::schema::Window) -> Self {
232        self.0.set_window(window);
233        self
234    }
235
236    /// A Host for a closure rather than a function, for tests that compose something
237    /// built at run time. The closure is shared with the factory a resync calls, so it
238    /// has to be `Send` and `Sync` like any other root.
239    pub fn with_content(content: impl Fn() + Send + Sync + 'static) -> Self {
240        let content = Arc::new(content);
241        Self(Host::new(move || {
242            let content = Arc::clone(&content);
243            Box::new(Recomposer::new(move || content())) as Box<dyn Runtime>
244        }))
245    }
246}
247
248impl std::ops::Deref for ComposeHost {
249    type Target = Host;
250
251    fn deref(&self) -> &Host {
252        &self.0
253    }
254}
255
256impl std::ops::DerefMut for ComposeHost {
257    fn deref_mut(&mut self) -> &mut Host {
258        &mut self.0
259    }
260}
261
262/// The runtime factory for one composable root: what [`crate::LaunchBuilder::launch_runtime`]
263/// and the browser's start take.
264pub fn recomposer_for(content: fn()) -> impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static {
265    move || Box::new(Recomposer::new(content)) as Box<dyn Runtime>
266}
267
268impl crate::LaunchBuilder {
269    /// Runs a composable as the application. Does not return while it is running, and
270    /// ends the process with a failing status if the renderer loop could not run at all.
271    ///
272    /// ```ignore
273    /// #[composable]
274    /// fn app() {
275    ///     Text("Hello");
276    /// }
277    ///
278    /// fn main() {
279    ///     compose_rust::LaunchBuilder::new().launch(app);
280    /// }
281    /// ```
282    pub fn launch(self, content: fn()) {
283        self.launch_runtime(recomposer_for(content));
284    }
285
286    /// [`crate::LaunchBuilder::launch`] without the exit: the status the renderer loop
287    /// ended with.
288    pub fn try_launch(self, content: fn()) -> i32 {
289        self.try_launch_runtime(recomposer_for(content))
290    }
291
292    /// The same as `launch`, under Compose Desktop's name for it.
293    pub fn application(self, content: fn()) {
294        self.launch(content);
295    }
296}
297
298/// Runs a composable as the application, with every launch choice left to its default.
299///
300/// ```ignore
301/// #[composable]
302/// fn counter() {
303///     let count = remember(|| mutable_state_of(0));
304///     let more = count.clone();
305///     Button(format!("{}", count.get())).on_click(move || more.update(|count| *count += 1));
306/// }
307///
308/// fn main() {
309///     compose_rust::launch(counter);
310/// }
311/// ```
312pub fn launch(content: fn()) {
313    crate::LaunchBuilder::new().launch(content);
314}
315
316/// The same as [`launch`], under Compose Desktop's name for it.
317pub fn application(content: fn()) {
318    launch(content);
319}
320
321/// Calls a handler with an event's payload, converted to what the handler takes.
322fn deliver(callback: &EventCallback, payload: &EventPayload<'_>) -> Result<i64, ProtocolError> {
323    match (callback, payload) {
324        (
325            EventCallback::Unit(handler),
326            EventPayload::Clicked | EventPayload::FocusLost | EventPayload::FilesEntered,
327        ) => (*handler.borrow_mut())(),
328        (EventCallback::Text(handler), EventPayload::TextChanged(text))
329        | (EventCallback::Text(handler), EventPayload::TextSubmitted(text)) => {
330            (*handler.borrow_mut())((*text).to_owned())
331        }
332        (
333            EventCallback::Key(handler),
334            EventPayload::KeyDown {
335                key,
336                shift_key,
337                ctrl_key,
338                alt_key,
339                meta_key,
340            },
341        ) => {
342            let event = KeyEvent::new(*key, *shift_key, *ctrl_key, *alt_key, *meta_key);
343            (*handler.borrow_mut())(event.clone());
344            return Ok(i64::from(event.consumed()));
345        }
346        (
347            EventCallback::Pointer(handler),
348            EventPayload::Pointer {
349                kind,
350                pointer_id,
351                pointer_type,
352                x,
353                y,
354                buttons,
355                button,
356                modifiers,
357                click_count,
358                delta_x,
359                delta_y,
360                delta_mode,
361                timestamp_nanos,
362            },
363        ) => {
364            let event = crate::PointerEvent::new(
365                *kind,
366                *pointer_id,
367                *pointer_type,
368                *x,
369                *y,
370                *buttons,
371                *button,
372                *modifiers,
373                *click_count,
374                *delta_x,
375                *delta_y,
376                *delta_mode,
377                *timestamp_nanos,
378            );
379            (*handler.borrow_mut())(event.clone());
380            return Ok(i64::from(event.consumed()));
381        }
382        (EventCallback::Value(handler), EventPayload::ValueChanged(value)) => {
383            (*handler.borrow_mut())(*value)
384        }
385        (EventCallback::Range(handler), EventPayload::RangeRequested { start, count }) => {
386            (*handler.borrow_mut())(RangeRequest::new(*start, *count))
387        }
388        (EventCallback::Files(handler), EventPayload::FilesDropped(paths)) => {
389            (*handler.borrow_mut())(crate::FileDrop::new(paths))
390        }
391        (EventCallback::Window(handler), payload) => {
392            let Some(event) = crate::windows::WindowEvent::from_payload(payload) else {
393                return Err(ProtocolError::InvalidValueKind(0));
394            };
395            (*handler.borrow_mut())(event)
396        }
397        _ => return Err(ProtocolError::InvalidValueKind(0)),
398    }
399    Ok(0)
400}
401
402impl Composer {
403    /// Drops any walk a panic left half done, so the next call starts from a clean stack.
404    pub(crate) fn abandon_composition(&mut self) {
405        self.clear_walk();
406    }
407}
408
409impl Runtime for Recomposer {
410    fn batch(&self) -> &Batch {
411        let composer = self.composition.composer.as_ptr();
412        // SAFETY: the composer is only borrowed inside this runtime's own calls, which take
413        // `&mut self`, so no borrow is alive while the Host holds this reference, and the
414        // reference borrows `self`, so no runtime call can start while it lives.
415        unsafe { (*composer).writer.batch() }
416    }
417
418    fn batch_mut(&mut self) -> &mut Batch {
419        let composer = self.composition.composer.as_ptr();
420        // SAFETY: as for `batch`, with `&mut self` making this the only reference.
421        unsafe { (*composer).writer.batch_mut() }
422    }
423
424    fn rebuild(&mut self) {
425        Recomposer::rebuild(self);
426    }
427
428    fn render(&mut self, frame_time_nanos: Option<u64>) {
429        Recomposer::render(self, frame_time_nanos);
430    }
431
432    fn handle_event(&mut self, event: &HostEvent<'_>) -> Result<i64, ProtocolError> {
433        Recomposer::handle_event(self, event)
434    }
435
436    fn size_token(&self, node_id: u32) -> Option<u32> {
437        Recomposer::size_token(self, node_id)
438    }
439
440    fn is_window(&self, node_id: u32) -> bool {
441        Recomposer::is_window(self, node_id)
442    }
443
444    fn run_in_context(&mut self, action: &mut dyn FnMut()) {
445        Recomposer::run_in_context(self, action);
446    }
447}