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,
48                composer.ambient.design,
49                composer.ambient.permission,
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        // Focus asked for by the handlers, the composition or its effects goes out last,
193        // once every node it may name has been placed.
194        with_composer(Composer::flush_focus_requests);
195        // Whatever was written during this round, by the composition, an effect or a
196        // future, is the next frame's.
197        let pending = with_composer(|composer| {
198            !composer
199                .signal
200                .dirty
201                .lock()
202                .unwrap_or_else(|poison| poison.into_inner())
203                .is_empty()
204                || !composer.invalid.is_empty()
205        })
206        .unwrap_or(false);
207        if pending {
208            request_frame_from_worker();
209        }
210    }
211}
212
213/// The Host of a composable application: compose-rust's [`Host`] driving a
214/// [`Recomposer`].
215///
216/// The boundary builds one on the UI thread for an application launched with
217/// [`launch`]. Tests and benchmarks build one directly, which is why it is public; it
218/// derefs to the [`Host`], whose calls are the boundary's.
219pub struct ComposeHost(Host);
220
221impl ComposeHost {
222    /// The Host for `content`, in the theme the application launched with.
223    pub fn new(content: fn()) -> Self {
224        Self(Host::new(recomposer_for(content)))
225    }
226
227    /// The same, in the given theme.
228    pub fn with_theme(content: fn(), theme: Theme) -> Self {
229        Self(Host::with_theme(recomposer_for(content), theme))
230    }
231
232    /// The same Host with other window settings than the launch's, such as
233    /// `Window::new().with_windows()` for an application of several windows.
234    pub fn with_window(mut self, window: crate::schema::Window) -> Self {
235        self.0.set_window(window);
236        self
237    }
238
239    /// A Host for a closure rather than a function, for tests that compose something
240    /// built at run time. The closure is shared with the factory a resync calls, so it
241    /// has to be `Send` and `Sync` like any other root.
242    pub fn with_content(content: impl Fn() + Send + Sync + 'static) -> Self {
243        let content = Arc::new(content);
244        Self(Host::new(move || {
245            let content = Arc::clone(&content);
246            Box::new(Recomposer::new(move || content())) as Box<dyn Runtime>
247        }))
248    }
249}
250
251impl std::ops::Deref for ComposeHost {
252    type Target = Host;
253
254    fn deref(&self) -> &Host {
255        &self.0
256    }
257}
258
259impl std::ops::DerefMut for ComposeHost {
260    fn deref_mut(&mut self) -> &mut Host {
261        &mut self.0
262    }
263}
264
265/// The runtime factory for one composable root: what [`crate::LaunchBuilder::launch_runtime`]
266/// and the browser's start take.
267pub fn recomposer_for(content: fn()) -> impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static {
268    move || Box::new(Recomposer::new(content)) as Box<dyn Runtime>
269}
270
271impl crate::LaunchBuilder {
272    /// Runs a composable as the application. Does not return while it is running, and
273    /// ends the process with a failing status if the renderer loop could not run at all.
274    ///
275    /// ```ignore
276    /// #[Composable]
277    /// fn app() {
278    ///     Text("Hello");
279    /// }
280    ///
281    /// fn main() {
282    ///     compose_rust::LaunchBuilder::new().launch(app);
283    /// }
284    /// ```
285    pub fn launch(self, content: fn()) {
286        self.launch_runtime(recomposer_for(content));
287    }
288
289    /// [`crate::LaunchBuilder::launch`] without the exit: the status the renderer loop
290    /// ended with.
291    pub fn try_launch(self, content: fn()) -> i32 {
292        self.try_launch_runtime(recomposer_for(content))
293    }
294
295    /// The same as `launch`, under Compose Desktop's name for it.
296    pub fn application(self, content: fn()) {
297        self.launch(content);
298    }
299}
300
301/// Runs a composable as the application, with every launch choice left to its default.
302///
303/// ```ignore
304/// #[Composable]
305/// fn counter() {
306///     let count = remember(|| mutable_state_of(0));
307///     let more = count.clone();
308///     Button(format!("{}", count.get())).on_click(move || more.update(|count| *count += 1));
309/// }
310///
311/// fn main() {
312///     compose_rust::launch(counter);
313/// }
314/// ```
315pub fn launch(content: fn()) {
316    crate::LaunchBuilder::new().launch(content);
317}
318
319/// The same as [`launch`], under Compose Desktop's name for it.
320pub fn application(content: fn()) {
321    launch(content);
322}
323
324/// Calls a handler with an event's payload, converted to what the handler takes.
325fn deliver(callback: &EventCallback, payload: &EventPayload<'_>) -> Result<i64, ProtocolError> {
326    match (callback, payload) {
327        (
328            EventCallback::Unit(handler),
329            EventPayload::Clicked
330            | EventPayload::FocusLost
331            | EventPayload::FocusGained
332            | EventPayload::FilesEntered,
333        ) => (*handler.borrow_mut())(),
334        (EventCallback::Text(handler), EventPayload::TextChanged(text))
335        | (EventCallback::Text(handler), EventPayload::TextSubmitted(text)) => {
336            (*handler.borrow_mut())((*text).to_owned())
337        }
338        (
339            EventCallback::Key(handler),
340            EventPayload::KeyDown {
341                key,
342                shift_key,
343                ctrl_key,
344                alt_key,
345                meta_key,
346                repeat,
347                code,
348                text,
349            }
350            | EventPayload::KeyUp {
351                key,
352                shift_key,
353                ctrl_key,
354                alt_key,
355                meta_key,
356                repeat,
357                code,
358                text,
359            },
360        ) => {
361            let event = KeyEvent::new(*key, *shift_key, *ctrl_key, *alt_key, *meta_key)
362                .with_details(*repeat, *code, text);
363            (*handler.borrow_mut())(event.clone());
364            return Ok(i64::from(event.consumed()));
365        }
366        (
367            EventCallback::Pointer(handler),
368            EventPayload::Pointer {
369                kind,
370                pointer_id,
371                pointer_type,
372                x,
373                y,
374                buttons,
375                button,
376                modifiers,
377                click_count,
378                delta_x,
379                delta_y,
380                delta_mode,
381                timestamp_nanos,
382            },
383        ) => {
384            let event = crate::PointerEvent::new(
385                *kind,
386                *pointer_id,
387                *pointer_type,
388                *x,
389                *y,
390                *buttons,
391                *button,
392                *modifiers,
393                *click_count,
394                *delta_x,
395                *delta_y,
396                *delta_mode,
397                *timestamp_nanos,
398            );
399            (*handler.borrow_mut())(event.clone());
400            return Ok(i64::from(event.consumed()));
401        }
402        (EventCallback::Value(handler), EventPayload::ValueChanged(value)) => {
403            (*handler.borrow_mut())(*value)
404        }
405        (EventCallback::Value(handler), EventPayload::CompositionChanged { composing }) => {
406            (*handler.borrow_mut())(if *composing { 1.0 } else { 0.0 })
407        }
408        (EventCallback::Range(handler), EventPayload::RangeRequested { start, count }) => {
409            (*handler.borrow_mut())(RangeRequest::new(*start, *count))
410        }
411        (EventCallback::Files(handler), EventPayload::FilesDropped(paths)) => {
412            (*handler.borrow_mut())(crate::FileDrop::new(paths))
413        }
414        (EventCallback::Window(handler), payload) => {
415            let Some(event) = crate::windows::WindowEvent::from_payload(payload) else {
416                return Err(ProtocolError::InvalidValueKind(0));
417            };
418            (*handler.borrow_mut())(event)
419        }
420        _ => return Err(ProtocolError::InvalidValueKind(0)),
421    }
422    Ok(0)
423}
424
425impl Composer {
426    /// Drops any walk a panic left half done, so the next call starts from a clean stack.
427    pub(crate) fn abandon_composition(&mut self) {
428        self.clear_walk();
429    }
430}
431
432impl Runtime for Recomposer {
433    fn batch(&self) -> &Batch {
434        let composer = self.composition.composer.as_ptr();
435        // SAFETY: the composer is only borrowed inside this runtime's own calls, which take
436        // `&mut self`, so no borrow is alive while the Host holds this reference, and the
437        // reference borrows `self`, so no runtime call can start while it lives.
438        unsafe { (*composer).writer.batch() }
439    }
440
441    fn batch_mut(&mut self) -> &mut Batch {
442        let composer = self.composition.composer.as_ptr();
443        // SAFETY: as for `batch`, with `&mut self` making this the only reference.
444        unsafe { (*composer).writer.batch_mut() }
445    }
446
447    fn rebuild(&mut self) {
448        Recomposer::rebuild(self);
449    }
450
451    fn render(&mut self, frame_time_nanos: Option<u64>) {
452        Recomposer::render(self, frame_time_nanos);
453    }
454
455    fn handle_event(&mut self, event: &HostEvent<'_>) -> Result<i64, ProtocolError> {
456        Recomposer::handle_event(self, event)
457    }
458
459    fn size_token(&self, node_id: u32) -> Option<u32> {
460        Recomposer::size_token(self, node_id)
461    }
462
463    fn is_window(&self, node_id: u32) -> bool {
464        Recomposer::is_window(self, node_id)
465    }
466
467    fn run_in_context(&mut self, action: &mut dyn FnMut()) {
468        Recomposer::run_in_context(self, action);
469    }
470}