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, 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}
34
35impl Default for Batch {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl Batch {
42 /// Sized so that an ordinary screen's batches never grow it: once warm, writing a
43 /// frame allocates nothing.
44 pub fn new() -> Self {
45 Self {
46 encoder: BatchEncoder::with_capacity(16 * 1024, 4 * 1024, 256),
47 error: None,
48 }
49 }
50
51 /// The batch arena, so a Renderer that reads Host memory through a mapped view can be
52 /// handed one.
53 pub fn arena(&self) -> (*const u8, usize) {
54 self.encoder.arena()
55 }
56
57 /// Starts a new batch, discarding the previous one and any error it carried.
58 pub fn begin(&mut self) {
59 self.encoder.clear();
60 self.error = None;
61 }
62
63 /// Closes the batch and returns its bytes, or the first error written into it.
64 pub fn finish(&mut self) -> Result<&[u8], ProtocolError> {
65 if let Some(error) = self.error.take() {
66 return Err(error);
67 }
68 self.encoder.finish()
69 }
70
71 /// Writes one record. After an error nothing more is written: the batch is already
72 /// going to be refused.
73 pub fn write(&mut self, mutation: Mutation<'_>) {
74 if self.error.is_none() {
75 if let Err(error) = self.encoder.encode(&mutation) {
76 self.error = Some(error);
77 }
78 }
79 }
80
81 /// Records an error found while building the batch. `finish` returns it.
82 pub fn fail(&mut self, error: ProtocolError) {
83 self.error = Some(error);
84 }
85
86 /// The root theme record. Written once per rebuild, never per frame.
87 pub fn set_theme(&mut self, theme: Theme) {
88 self.write(Mutation::SetTheme(theme));
89 if let Some(overlay) = theme.overlay {
90 self.write(Mutation::SetOverlayTokens(*overlay));
91 }
92 }
93
94 /// Sets application-level overlay theme tokens for Dialog, Menu, Dropdown and Sheet.
95 pub fn set_overlay_tokens(&mut self, tokens: crate::schema::OverlayTokens) {
96 self.write(Mutation::SetOverlayTokens(tokens));
97 }
98
99 /// The root window record. Written once per rebuild, never per frame.
100 pub fn set_window(&mut self, window: Window) {
101 self.write(Mutation::SetWindow(window));
102 }
103
104 pub fn set_text_node(&mut self, node_id: u32, text: &str, selection: Option<Selection>) {
105 self.write(Mutation::SetText {
106 node_id,
107 text,
108 selection,
109 });
110 }
111
112 /// Copies one asset into the batch. The bytes ride behind the records, and the
113 /// Renderer takes its own copy inside the call that carries them.
114 pub fn register_asset(&mut self, asset_id: u32, kind: AssetKind, bytes: &[u8]) {
115 self.write(Mutation::RegisterAsset {
116 asset_id,
117 kind,
118 bytes,
119 });
120 }
121
122 pub fn release_asset(&mut self, asset_id: u32) {
123 self.write(Mutation::ReleaseAsset { asset_id });
124 }
125
126 /// Writes one transient message into the batch.
127 ///
128 /// It names no node, because a message is not in the tree: it is a sentence with a
129 /// lifetime, and that lifetime belongs to the Renderer.
130 pub fn show_message(
131 &mut self,
132 handler_id: u64,
133 text: &str,
134 action: &str,
135 duration: MessageDuration,
136 ) {
137 self.write(Mutation::ShowMessage {
138 handler_id,
139 text,
140 action,
141 duration,
142 });
143 }
144
145 /// Writes one notification command into the batch.
146 ///
147 /// Like a message it names no node: what it asks for happens outside the window, and
148 /// the Renderer is the side that owns the platform it happens on.
149 pub(crate) fn notification(&mut self, command: &crate::notification::NotificationCommand) {
150 use crate::notification::NotificationCommand;
151 match command {
152 NotificationCommand::Post(notification) => {
153 self.write(Mutation::PostNotification {
154 key: ¬ification.key,
155 title: ¬ification.title,
156 body: ¬ification.body,
157 channel: ¬ification.channel,
158 action_1: ¬ification.actions[0],
159 action_2: ¬ification.actions[1],
160 importance: notification.importance,
161 presentation: notification.presentation,
162 });
163 }
164 NotificationCommand::Withdraw(key) => {
165 self.write(Mutation::WithdrawNotification { key });
166 }
167 NotificationCommand::RequestPermission => {
168 self.write(Mutation::RequestNotificationPermission);
169 }
170 }
171 }
172
173 /// Writes one clipboard write. It names no node: the clipboard is the platform's.
174 pub fn set_clipboard_text(&mut self, text: &str) {
175 self.write(Mutation::SetClipboardText { text });
176 }
177
178 /// Writes one clipboard read request. It names no node either.
179 pub fn read_clipboard(&mut self, request: u32) {
180 self.write(Mutation::ReadClipboard { request });
181 }
182
183 /// Writes one operation on a file the Renderer holds a handle for.
184 pub(crate) fn file_call(&mut self, call: &crate::picked_file::Call) {
185 self.write(Mutation::FileCall {
186 request: call.request,
187 op: call.op,
188 create: call.create,
189 handle: call.handle,
190 name: &call.name,
191 data: &call.data,
192 });
193 }
194
195 /// Writes one storage operation for the Renderer to carry out. It names no node either.
196 pub(crate) fn storage_call(&mut self, call: &crate::storage::RendererCall) {
197 self.write(Mutation::StorageCall {
198 request: call.request,
199 op: call.op,
200 store: &call.store,
201 key: &call.key,
202 data: &call.data,
203 });
204 }
205
206 /// Lets the Renderer go of a file handle nothing on this side names any more.
207 pub(crate) fn release_file_handle(&mut self, handle: u32) {
208 self.write(Mutation::ReleaseFileHandle { handle });
209 }
210
211 /// Writes one request to scroll the container `node_id` to an offset in physical
212 /// pixels. An axis left `None` is not touched; a request that names neither axis is
213 /// refused when the batch is finished.
214 ///
215 /// A request, not a command: the Renderer owns the position. It keeps the offset
216 /// inside what the content allows, ignores the request while the user is dragging or
217 /// flinging, and says where the container ended up in `ScrollChanged`.
218 pub fn scroll_to(&mut self, node_id: u32, request: crate::schema::ScrollTo) {
219 use crate::schema::scroll_to_flags as flag;
220 let mut flags = 0;
221 if request.animate {
222 flags |= flag::ANIMATE;
223 }
224 if request.x_px.is_some() {
225 flags |= flag::X;
226 }
227 if request.y_px.is_some() {
228 flags |= flag::Y;
229 }
230 let x_px = request.x_px.unwrap_or(0.0);
231 let y_px = request.y_px.unwrap_or(0.0);
232 if flags & (flag::X | flag::Y) == 0 || !x_px.is_finite() || !y_px.is_finite() {
233 // The Renderer would refuse it with a protocol error after the batch had
234 // crossed. Refusing it here names the mistake where it was made.
235 self.fail(ProtocolError::InvalidValueKind(0));
236 return;
237 }
238 self.write(Mutation::ScrollTo {
239 node_id,
240 item: request.item,
241 flags,
242 x_px,
243 y_px,
244 });
245 }
246
247 /// Writes one request to bring a rectangle into view in the container `node_id`. One
248 /// request moves one container: to show something inside nested containers, write one
249 /// for each of them, innermost first.
250 pub fn scroll_into_view(&mut self, node_id: u32, request: crate::schema::ScrollIntoView) {
251 use crate::schema::{ScrollTarget, scroll_into_view_flags, scroll_target_kind};
252 let (target_kind, target, rect) = match request.target {
253 ScrollTarget::Content(rect) => (scroll_target_kind::CONTENT, 0, rect),
254 ScrollTarget::Node(node, rect) => (scroll_target_kind::NODE, node, rect),
255 ScrollTarget::Item(item, rect) => (scroll_target_kind::ITEM, item, rect),
256 };
257 let numbers = [rect.x_px, rect.y_px, rect.width_px, rect.height_px];
258 if numbers.iter().any(|value| !value.is_finite())
259 || rect.width_px < 0.0
260 || rect.height_px < 0.0
261 {
262 self.fail(ProtocolError::InvalidValueKind(0));
263 return;
264 }
265 self.write(Mutation::ScrollIntoView {
266 node_id,
267 target,
268 target_kind,
269 align_x: request.align_x as u16,
270 align_y: request.align_y as u16,
271 flags: if request.animate {
272 scroll_into_view_flags::ANIMATE
273 } else {
274 0
275 },
276 rect,
277 });
278 }
279
280 /// Writes one file dialog request. Like a notification it names no node: the dialog is
281 /// not in the tree, and `window` only says which window it is modal for.
282 pub(crate) fn file_dialog(&mut self, request: &crate::file_dialog::Request) {
283 self.write(Mutation::ShowFileDialog {
284 request: request.id,
285 window: request.window,
286 kind: request.kind,
287 default_filter: request.default_filter,
288 title: &request.title,
289 directory: &request.directory,
290 name: &request.name,
291 filters: &request.filters,
292 });
293 }
294
295 /// Writes one edit the application asked of a code editor: replace `range` of the
296 /// document as it stood at `base_version` with `text`. The Renderer moves it past what
297 /// the reader typed since, or refuses it and reports `request_id` back.
298 pub fn edit_code(
299 &mut self,
300 node_id: u32,
301 request_id: u32,
302 base_version: u32,
303 range: crate::code::CodeRange,
304 text: &str,
305 ) {
306 self.write(Mutation::EditCode {
307 node_id,
308 request_id,
309 base_version,
310 start_line: range.start.line,
311 start_column: range.start.column,
312 end_line: range.end.line,
313 end_column: range.end.column,
314 text,
315 });
316 }
317
318 /// Writes one request to put a code editor's selection between two places in the
319 /// document as it stood at `base_version`. The Renderer moves both ends past what the
320 /// reader typed since and drops the request, with a protocol error, where an end is not
321 /// a place in the document.
322 pub fn set_selection(
323 &mut self,
324 node_id: u32,
325 base_version: u32,
326 anchor: crate::code::Position,
327 active: crate::code::Position,
328 ) {
329 self.write(Mutation::SetSelection {
330 node_id,
331 base_version,
332 anchor_line: anchor.line,
333 anchor_column: anchor.column,
334 active_line: active.line,
335 active_column: active.column,
336 });
337 }
338
339 /// Writes one request to scroll a code editor until `range`, as the document stood at
340 /// `base_version`, is on screen. A range inside a folded stretch unfolds it.
341 pub fn reveal_range(
342 &mut self,
343 node_id: u32,
344 base_version: u32,
345 range: crate::code::CodeRange,
346 align: crate::schema::RevealAlign,
347 ) {
348 self.write(Mutation::RevealRange {
349 node_id,
350 base_version,
351 start_line: range.start.line,
352 start_column: range.start.column,
353 end_line: range.end.line,
354 end_column: range.end.column,
355 align,
356 });
357 }
358
359 /// Writes one request to fold or unfold the stretch of a code editor that starts on
360 /// `start_line`.
361 pub fn set_folded(&mut self, node_id: u32, start_line: u32, collapsed: bool) {
362 self.write(Mutation::SetFolded {
363 node_id,
364 start_line,
365 collapsed,
366 });
367 }
368
369 /// Writes one request to scroll a code editor so that `offset_px` physical pixels of its
370 /// document are above the top of its view. The offset is the unit `CodeScrolled`
371 /// reports, so what one editor reported can be given to another to keep the two level.
372 /// The Renderer keeps it inside what the document allows.
373 pub fn set_scroll_offset(&mut self, node_id: u32, offset_px: f64) {
374 self.write(Mutation::SetScrollOffset { node_id, offset_px });
375 }
376
377 /// Appends the streamed tail to a Text node without resending its whole value.
378 pub fn append_text_node(&mut self, node_id: u32, text: &str) {
379 self.write(Mutation::AppendText { node_id, text });
380 }
381
382 /// Requests input focus for `node_id`, or clears focus when `node_id` is 0.
383 pub fn request_focus(&mut self, node_id: u32) {
384 self.write(Mutation::RequestFocus { node_id });
385 }
386}
387
388/// What builds and changes the tree, seen from the boundary.
389///
390/// The Host owns everything that is not the tree and calls in here for the rest. Every
391/// method runs on the Renderer's UI thread, inside a boundary call, between a
392/// [`Batch::begin`] and a [`Batch::finish`] the Host makes on [`Runtime::batch_mut`], so a
393/// runtime writes its records and returns; it never opens or closes a batch itself.
394pub trait Runtime {
395 /// The batch this runtime writes the tree's records into. The Host writes its own
396 /// records into the same one, before and after the runtime's.
397 fn batch(&self) -> &Batch;
398
399 fn batch_mut(&mut self) -> &mut Batch;
400
401 /// Writes the whole tree. Called once on a fresh runtime, which is also what a resync
402 /// gets: the Host makes a new runtime and rebuilds it.
403 fn rebuild(&mut self);
404
405 /// Writes whatever changed since the last call: state a worker wrote, a scope that a
406 /// size class or a design system woke, the consequences of an event.
407 ///
408 /// `frame_time_nanos` is the time of the frame the Renderer is serving, when this call
409 /// is serving one, and `None` when it answers an event. A runtime whose animations and
410 /// timers run on the frame clock reads it here; one that has no clock of its own
411 /// ignores it.
412 fn render(&mut self, frame_time_nanos: Option<u64>);
413
414 /// Runs the handler an event names, and nothing else: the Host renders afterwards.
415 ///
416 /// Returns the value the boundary hands back as the call's result, which is how a key
417 /// handler says it consumed the key. An event naming a handler or a node this runtime
418 /// does not have is an error, and so is a payload that no handler can receive.
419 fn handle_event(&mut self, event: &HostEvent<'_>) -> Result<i64, ProtocolError>;
420
421 /// The application's name for a node it asked to have measured, if it asked.
422 fn size_token(&self, node_id: u32) -> Option<u32> {
423 let _ = node_id;
424 None
425 }
426
427 /// Whether `node_id` is a `Window` node. A window's size and zoom reports name
428 /// it, and nothing else, so a runtime without windows has none.
429 fn is_window(&self, node_id: u32) -> bool {
430 let _ = node_id;
431 false
432 }
433
434 /// Whether the runtime has work waiting that a frame should run. A runtime that is
435 /// woken by something outside a boundary call (a timer, a task it spawned) wakes the
436 /// context's waker, and the Host turns that into a frame request.
437 fn poll_work(&mut self, context: &mut Context<'_>) -> Poll<()> {
438 let _ = context;
439 Poll::Pending
440 }
441
442 /// Runs code the application handed to the Host, a message's action for one, inside
443 /// whatever context this runtime's state writes need.
444 fn run_in_context(&mut self, action: &mut dyn FnMut()) {
445 action();
446 }
447}