Skip to main content

compose_rust/runtime/
seam.rs

1//! The seam between the boundary and whatever builds the tree.
2//!
3//! The boundary entry points, the batch arena, the theme and window records, assets,
4//! messages, notifications, streaming appends and the frame request all belong to this
5//! crate and behave the same whatever authoring model produced the tree. What differs is
6//! how a tree is built and changed: a Dioxus `VirtualDom` diffs one, a recomposition
7//! runtime re-runs the scopes that read a changed state. That part is a [`Runtime`].
8//!
9//! An application registers one with [`LaunchBuilder::launch_runtime`], and every
10//! `compose_rust_host_*` call is answered by a [`Host`] that drives it. Nothing about
11//! the boundary changes with the runtime: the same five entry points, the same records,
12//! the same batch.
13//!
14//! [`LaunchBuilder::launch_runtime`]: crate::LaunchBuilder::launch_runtime
15//! [`Host`]: crate::Host
16
17use crate::protocol::{BatchEncoder, HostEvent, Mutation, ProtocolError};
18use crate::schema::{AssetKind, MessageDuration, MessagePlacement, Selection, Theme, Window};
19use std::task::{Context, Poll};
20
21/// One batch, written by the runtime and by the Host into the same arena.
22///
23/// The Host opens it with [`Batch::begin`], writes the records that are its own (the theme,
24/// the window, assets, messages, notifications, streamed tails) and has the runtime write
25/// the tree's records between them, then closes it with [`Batch::finish`]. The bytes that
26/// come back are what the boundary hands the Renderer.
27///
28/// A runtime has no fallible way to report a bad record from the middle of a diff, so
29/// [`Batch::fail`] keeps the error and `finish` returns it in place of the batch.
30pub struct Batch {
31    encoder: BatchEncoder,
32    error: Option<ProtocolError>,
33    /// Where a grouped edit is encoded before it is copied into the arena. Kept, so a warm
34    /// batch encodes one without allocating.
35    edit_group: Vec<u8>,
36}
37
38impl Default for Batch {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl Batch {
45    /// Sized so that an ordinary screen's batches never grow it: once warm, writing a
46    /// frame allocates nothing.
47    pub fn new() -> Self {
48        Self {
49            encoder: BatchEncoder::with_capacity(16 * 1024, 4 * 1024, 256),
50            error: None,
51            edit_group: Vec::new(),
52        }
53    }
54
55    /// The batch arena, so a Renderer that reads Host memory through a mapped view can be
56    /// handed one.
57    pub fn arena(&self) -> (*const u8, usize) {
58        self.encoder.arena()
59    }
60
61    /// Starts a new batch, discarding the previous one and any error it carried.
62    pub fn begin(&mut self) {
63        self.encoder.clear();
64        self.error = None;
65    }
66
67    /// Closes the batch and returns its bytes, or the first error written into it.
68    pub fn finish(&mut self) -> Result<&[u8], ProtocolError> {
69        if let Some(error) = self.error.take() {
70            return Err(error);
71        }
72        self.encoder.finish()
73    }
74
75    /// Writes one record. After an error nothing more is written: the batch is already
76    /// going to be refused.
77    pub fn write(&mut self, mutation: Mutation<'_>) {
78        if self.error.is_none() {
79            if let Err(error) = self.encoder.encode(&mutation) {
80                self.error = Some(error);
81            }
82        }
83    }
84
85    /// Records an error found while building the batch. `finish` returns it.
86    pub fn fail(&mut self, error: ProtocolError) {
87        self.error = Some(error);
88    }
89
90    /// The root theme record. Written once per rebuild, never per frame.
91    pub fn set_theme(&mut self, theme: Theme) {
92        self.write(Mutation::SetTheme(theme));
93        if let Some(overlay) = theme.overlay {
94            self.write(Mutation::SetOverlayTokens(*overlay));
95        }
96    }
97
98    /// Sets application-level overlay theme tokens for Dialog, Menu, Dropdown and Sheet.
99    pub fn set_overlay_tokens(&mut self, tokens: crate::schema::OverlayTokens) {
100        self.write(Mutation::SetOverlayTokens(tokens));
101    }
102
103    /// The root window record. Written once per rebuild, never per frame.
104    pub fn set_window(&mut self, window: Window) {
105        self.write(Mutation::SetWindow(window));
106    }
107
108    pub fn set_text_node(&mut self, node_id: u32, text: &str, selection: Option<Selection>) {
109        self.write(Mutation::SetText {
110            node_id,
111            text,
112            selection,
113        });
114    }
115
116    /// Copies one asset into the batch. The bytes ride behind the records, and the
117    /// Renderer takes its own copy inside the call that carries them.
118    pub fn register_asset(&mut self, asset_id: u32, kind: AssetKind, bytes: &[u8]) {
119        self.write(Mutation::RegisterAsset {
120            asset_id,
121            kind,
122            bytes,
123        });
124    }
125
126    pub fn release_asset(&mut self, asset_id: u32) {
127        self.write(Mutation::ReleaseAsset { asset_id });
128    }
129
130    /// Writes one transient message into the batch.
131    ///
132    /// It names no node, because a message is not in the tree: it is a sentence with a
133    /// lifetime, and that lifetime belongs to the Renderer.
134    pub fn show_message(
135        &mut self,
136        handler_id: u64,
137        text: &str,
138        action: &str,
139        duration: MessageDuration,
140    ) {
141        self.show_placed_message(handler_id, text, action, duration, None, None);
142    }
143
144    /// Writes one transient message that names where it is drawn: along which edge and at
145    /// which end of it, and how far from that edge in dp. `None` for either leaves it to
146    /// the design system, which is what [`Batch::show_message`] does for both.
147    pub fn show_placed_message(
148        &mut self,
149        handler_id: u64,
150        text: &str,
151        action: &str,
152        duration: MessageDuration,
153        placement: Option<MessagePlacement>,
154        offset_dp: Option<f32>,
155    ) {
156        self.write(Mutation::ShowMessage {
157            handler_id,
158            text,
159            action,
160            duration,
161            placement,
162            offset_dp,
163        });
164    }
165
166    /// Writes one notification command into the batch.
167    ///
168    /// Like a message it names no node: what it asks for happens outside the window, and
169    /// the Renderer is the side that owns the platform it happens on.
170    pub(crate) fn notification(&mut self, command: &crate::notification::NotificationCommand) {
171        use crate::notification::NotificationCommand;
172        match command {
173            NotificationCommand::Post(notification) => {
174                self.write(Mutation::PostNotification {
175                    key: &notification.key,
176                    title: &notification.title,
177                    body: &notification.body,
178                    channel: &notification.channel,
179                    action_1: &notification.actions[0],
180                    action_2: &notification.actions[1],
181                    importance: notification.importance,
182                    presentation: notification.presentation,
183                });
184            }
185            NotificationCommand::Withdraw(key) => {
186                self.write(Mutation::WithdrawNotification { key });
187            }
188            NotificationCommand::RequestPermission => {
189                self.write(Mutation::RequestNotificationPermission);
190            }
191        }
192    }
193
194    /// Writes one clipboard write. It names no node: the clipboard is the platform's.
195    pub fn set_clipboard_text(&mut self, text: &str) {
196        self.write(Mutation::SetClipboardText { text });
197    }
198
199    /// Writes one clipboard read request. It names no node either.
200    pub fn read_clipboard(&mut self, request: u32) {
201        self.write(Mutation::ReadClipboard { request });
202    }
203
204    /// Writes one operation on a file the Renderer holds a handle for.
205    pub(crate) fn file_call(&mut self, call: &crate::picked_file::Call) {
206        self.write(Mutation::FileCall {
207            request: call.request,
208            op: call.op,
209            create: call.create,
210            handle: call.handle,
211            name: &call.name,
212            data: &call.data,
213        });
214    }
215
216    /// Writes one storage operation for the Renderer to carry out. It names no node either.
217    pub(crate) fn storage_call(&mut self, call: &crate::storage::RendererCall) {
218        self.write(Mutation::StorageCall {
219            request: call.request,
220            op: call.op,
221            store: &call.store,
222            key: &call.key,
223            data: &call.data,
224        });
225    }
226
227    /// Lets the Renderer go of a file handle nothing on this side names any more.
228    pub(crate) fn release_file_handle(&mut self, handle: u32) {
229        self.write(Mutation::ReleaseFileHandle { handle });
230    }
231
232    /// Writes one request to scroll the container `node_id` to an offset in physical
233    /// pixels. An axis left `None` is not touched; a request that names neither axis is
234    /// refused when the batch is finished.
235    ///
236    /// A request, not a command: the Renderer owns the position. It keeps the offset
237    /// inside what the content allows, ignores the request while the user is dragging or
238    /// flinging, and says where the container ended up in `ScrollChanged`.
239    pub fn scroll_to(&mut self, node_id: u32, request: crate::schema::ScrollTo) {
240        use crate::schema::scroll_to_flags as flag;
241        let mut flags = 0;
242        if request.animate {
243            flags |= flag::ANIMATE;
244        }
245        if request.x_px.is_some() {
246            flags |= flag::X;
247        }
248        if request.y_px.is_some() {
249            flags |= flag::Y;
250        }
251        let x_px = request.x_px.unwrap_or(0.0);
252        let y_px = request.y_px.unwrap_or(0.0);
253        if flags & (flag::X | flag::Y) == 0 || !x_px.is_finite() || !y_px.is_finite() {
254            // The Renderer would refuse it with a protocol error after the batch had
255            // crossed. Refusing it here names the mistake where it was made.
256            self.fail(ProtocolError::InvalidValueKind(0));
257            return;
258        }
259        self.write(Mutation::ScrollTo {
260            node_id,
261            item: request.item,
262            flags,
263            x_px,
264            y_px,
265        });
266    }
267
268    /// Writes one request to bring a rectangle into view in the container `node_id`. One
269    /// request moves one container: to show something inside nested containers, write one
270    /// for each of them, innermost first.
271    pub fn scroll_into_view(&mut self, node_id: u32, request: crate::schema::ScrollIntoView) {
272        use crate::schema::{ScrollTarget, scroll_into_view_flags, scroll_target_kind};
273        let (target_kind, target, rect) = match request.target {
274            ScrollTarget::Content(rect) => (scroll_target_kind::CONTENT, 0, rect),
275            ScrollTarget::Node(node, rect) => (scroll_target_kind::NODE, node, rect),
276            ScrollTarget::Item(item, rect) => (scroll_target_kind::ITEM, item, rect),
277        };
278        let numbers = [rect.x_px, rect.y_px, rect.width_px, rect.height_px];
279        if numbers.iter().any(|value| !value.is_finite())
280            || rect.width_px < 0.0
281            || rect.height_px < 0.0
282        {
283            self.fail(ProtocolError::InvalidValueKind(0));
284            return;
285        }
286        self.write(Mutation::ScrollIntoView {
287            node_id,
288            target,
289            target_kind,
290            align_x: request.align_x as u16,
291            align_y: request.align_y as u16,
292            flags: if request.animate {
293                scroll_into_view_flags::ANIMATE
294            } else {
295                0
296            },
297            rect,
298        });
299    }
300
301    /// Writes one file dialog request. Like a notification it names no node: the dialog is
302    /// not in the tree, and `window` only says which window it is modal for.
303    pub(crate) fn file_dialog(&mut self, request: &crate::file_dialog::Request) {
304        self.write(Mutation::ShowFileDialog {
305            request: request.id,
306            window: request.window,
307            kind: request.kind,
308            default_filter: request.default_filter,
309            title: &request.title,
310            directory: &request.directory,
311            name: &request.name,
312            filters: &request.filters,
313        });
314    }
315
316    /// Writes one edit the application asked of a code editor: replace `range` of the
317    /// document as it stood at `base_version` with `text`. The Renderer moves it past what
318    /// the reader typed since, or refuses it and reports `request_id` back.
319    pub fn edit_code(
320        &mut self,
321        node_id: u32,
322        request_id: u32,
323        base_version: u32,
324        range: crate::code::CodeRange,
325        text: &str,
326    ) {
327        self.write(Mutation::EditCode {
328            node_id,
329            request_id,
330            base_version,
331            start_line: range.start.line,
332            start_column: range.start.column,
333            end_line: range.end.line,
334            end_column: range.end.column,
335            text,
336        });
337    }
338
339    /// Writes one grouped edit the application asked of a code editor: replace every range
340    /// in `edits` with its text, all of them or none.
341    ///
342    /// Every range is in the document as it stood at `base_version`, so an edit never moves
343    /// the ranges of the others, and the edits may be listed in any order. They must not
344    /// overlap; two insertions at the same place go in in the order they are listed. Where
345    /// the reader has typed since, the Renderer moves every range past what they typed, and
346    /// refuses the whole group where any range meets it. It answers the group with exactly
347    /// one `CodeEditGroupResult` carrying `request_id`, and the whole group undoes as one
348    /// step. A group with no edits writes nothing at all.
349    pub fn edit_group(
350        &mut self,
351        node_id: u32,
352        request_id: u32,
353        base_version: u32,
354        edits: &[(crate::code::CodeRange, &str)],
355    ) {
356        if edits.is_empty() {
357            return;
358        }
359        let mut blob = std::mem::take(&mut self.edit_group);
360        blob.clear();
361        match crate::code::encode_edit_group(edits, &mut blob) {
362            Ok(()) => self.write(Mutation::EditCodeGroup {
363                node_id,
364                request_id,
365                base_version,
366                edits: &blob,
367            }),
368            Err(error) => self.fail(error),
369        }
370        self.edit_group = blob;
371    }
372
373    /// Writes one request to put a code editor's selection between two places in the
374    /// document as it stood at `base_version`. The Renderer moves both ends past what the
375    /// reader typed since and drops the request, with a protocol error, where an end is not
376    /// a place in the document.
377    pub fn set_selection(
378        &mut self,
379        node_id: u32,
380        base_version: u32,
381        anchor: crate::code::Position,
382        active: crate::code::Position,
383    ) {
384        self.write(Mutation::SetSelection {
385            node_id,
386            base_version,
387            anchor_line: anchor.line,
388            anchor_column: anchor.column,
389            active_line: active.line,
390            active_column: active.column,
391        });
392    }
393
394    /// Writes one request to scroll a code editor until `range`, as the document stood at
395    /// `base_version`, is on screen. A range inside a folded stretch unfolds it.
396    pub fn reveal_range(
397        &mut self,
398        node_id: u32,
399        base_version: u32,
400        range: crate::code::CodeRange,
401        align: crate::schema::RevealAlign,
402    ) {
403        self.write(Mutation::RevealRange {
404            node_id,
405            base_version,
406            start_line: range.start.line,
407            start_column: range.start.column,
408            end_line: range.end.line,
409            end_column: range.end.column,
410            align,
411        });
412    }
413
414    /// Writes one request to fold or unfold the stretch of a code editor that starts on
415    /// `start_line`.
416    pub fn set_folded(&mut self, node_id: u32, start_line: u32, collapsed: bool) {
417        self.write(Mutation::SetFolded {
418            node_id,
419            start_line,
420            collapsed,
421        });
422    }
423
424    /// Writes one request to scroll a code editor so that `offset_px` physical pixels of its
425    /// document are above the top of its view. The offset is the unit `CodeScrolled`
426    /// reports, so what one editor reported can be given to another to keep the two level.
427    /// The Renderer keeps it inside what the document allows.
428    pub fn set_scroll_offset(&mut self, node_id: u32, offset_px: f64) {
429        self.write(Mutation::SetScrollOffset { node_id, offset_px });
430    }
431
432    /// Writes one question to a code editor: where would a caret at `position` be drawn.
433    /// `request` is the number [`crate::code::position_query`] gave the callback that waits
434    /// for the answer. The Renderer answers it exactly once, with a rectangle in the
435    /// window's dp, or with none where the position is not on screen.
436    pub fn query_position_rect(
437        &mut self,
438        node_id: u32,
439        request: u32,
440        position: crate::code::Position,
441    ) {
442        self.write(Mutation::QueryPositionRect {
443            node_id,
444            request,
445            line: position.line,
446            column: position.column,
447        });
448    }
449
450    /// Writes one request to set every selection of a code editor, each an anchor and an
451    /// active end in the document as it stood at `base_version`, the first the primary one.
452    /// The Renderer moves every end past what the reader typed since, and drops the request,
453    /// with a protocol error, where an end is not a place in the document or the list is
454    /// empty. Typing, deleting and pasting then happen at every selection as one step, and
455    /// Escape goes back to the primary one.
456    pub fn set_selections(
457        &mut self,
458        node_id: u32,
459        base_version: u32,
460        selections: &[(crate::code::Position, crate::code::Position)],
461    ) {
462        let mut blob = std::mem::take(&mut self.edit_group);
463        blob.clear();
464        crate::code::encode_selections(selections, &mut blob);
465        self.write(Mutation::SetSelections {
466            node_id,
467            base_version,
468            selections: &blob,
469        });
470        self.edit_group = blob;
471    }
472
473    /// Writes one request to replace `range` of a code editor's document, as it stood at
474    /// `base_version`, with `snippet`, and to open its tab stops.
475    ///
476    /// The snippet is in the subset of VS Code's syntax the editor reads: `$1`,
477    /// `${1:placeholder}`, `${1|one,two|}`, `$0`, and `\$` for a dollar sign
478    /// ([`crate::code::escape_snippet_text`] escapes plain text). The caret selects the first
479    /// stop; Tab and Shift+Tab move between the stops, the places with the same number change
480    /// together, and a stop with choices opens a list. The session ends at `$0`, on Escape, or
481    /// when the caret leaves the snippet. The text it leaves arrives as ordinary changes and
482    /// undoes as one step. Where the reader has typed over the range since `base_version`,
483    /// the Renderer drops the request with a protocol error.
484    pub fn insert_snippet(
485        &mut self,
486        node_id: u32,
487        base_version: u32,
488        range: crate::code::CodeRange,
489        snippet: &str,
490    ) {
491        self.write(Mutation::InsertSnippet {
492            node_id,
493            base_version,
494            start_line: range.start.line,
495            start_column: range.start.column,
496            end_line: range.end.line,
497            end_column: range.end.column,
498            snippet,
499        });
500    }
501
502    /// Appends the streamed tail to a Text node without resending its whole value.
503    pub fn append_text_node(&mut self, node_id: u32, text: &str) {
504        self.write(Mutation::AppendText { node_id, text });
505    }
506
507    /// Requests input focus for `node_id`, or clears focus when `node_id` is 0.
508    pub fn request_focus(&mut self, node_id: u32) {
509        self.write(Mutation::RequestFocus { node_id });
510    }
511}
512
513/// What builds and changes the tree, seen from the boundary.
514///
515/// The Host owns everything that is not the tree and calls in here for the rest. Every
516/// method runs on the Renderer's UI thread, inside a boundary call, between a
517/// [`Batch::begin`] and a [`Batch::finish`] the Host makes on [`Runtime::batch_mut`], so a
518/// runtime writes its records and returns; it never opens or closes a batch itself.
519pub trait Runtime {
520    /// The batch this runtime writes the tree's records into. The Host writes its own
521    /// records into the same one, before and after the runtime's.
522    fn batch(&self) -> &Batch;
523
524    fn batch_mut(&mut self) -> &mut Batch;
525
526    /// Writes the whole tree. Called once on a fresh runtime, which is also what a resync
527    /// gets: the Host makes a new runtime and rebuilds it.
528    fn rebuild(&mut self);
529
530    /// Writes whatever changed since the last call: state a worker wrote, a scope that a
531    /// size class or a design system woke, the consequences of an event.
532    ///
533    /// `frame_time_nanos` is the time of the frame the Renderer is serving, when this call
534    /// is serving one, and `None` when it answers an event. A runtime whose animations and
535    /// timers run on the frame clock reads it here; one that has no clock of its own
536    /// ignores it.
537    fn render(&mut self, frame_time_nanos: Option<u64>);
538
539    /// Runs the handler an event names, and nothing else: the Host renders afterwards.
540    ///
541    /// Returns the value the boundary hands back as the call's result, which is how a key
542    /// handler says it consumed the key. An event naming a handler or a node this runtime
543    /// does not have is an error, and so is a payload that no handler can receive.
544    fn handle_event(&mut self, event: &HostEvent<'_>) -> Result<i64, ProtocolError>;
545
546    /// The application's name for a node it asked to have measured, if it asked.
547    fn size_token(&self, node_id: u32) -> Option<u32> {
548        let _ = node_id;
549        None
550    }
551
552    /// Whether `node_id` is a `Window` node. A window's size and zoom reports name
553    /// it, and nothing else, so a runtime without windows has none.
554    fn is_window(&self, node_id: u32) -> bool {
555        let _ = node_id;
556        false
557    }
558
559    /// Whether the runtime has work waiting that a frame should run. A runtime that is
560    /// woken by something outside a boundary call (a timer, a task it spawned) wakes the
561    /// context's waker, and the Host turns that into a frame request.
562    fn poll_work(&mut self, context: &mut Context<'_>) -> Poll<()> {
563        let _ = context;
564        Poll::Pending
565    }
566
567    /// Runs code the application handed to the Host, a message's action for one, inside
568    /// whatever context this runtime's state writes need.
569    fn run_in_context(&mut self, action: &mut dyn FnMut()) {
570        action();
571    }
572}