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: ¬ification.key,
176 title: ¬ification.title,
177 body: ¬ification.body,
178 channel: ¬ification.channel,
179 action_1: ¬ification.actions[0],
180 action_2: ¬ification.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}