Skip to main content

standard_plugin/
ui_runtime.rs

1//! UI plugins: the [`UiPlugin`] trait, what it is called with, and the
2//! runtime the `#[ui]` export forwards to.
3
4use alloc::string::String;
5use alloc::vec::Vec;
6use core::cell::{Cell, RefCell};
7
8use serde::de::DeserializeOwned;
9
10use crate::api::{self, Json};
11use crate::bindings::ui as wit;
12use crate::error::Result;
13use crate::geometry::Geometry;
14pub use crate::input::{Button, Key, KeyCode, KeyPhase, Modifiers, Pointer, PointerKind};
15
16/// A UI plugin: one instance runs in every viewer on the account.
17///
18/// It paints its [`crate::Surface`]s and reads account state; it is
19/// side-effect free. Anything that must happen once (a write, a
20/// notification, a request) is either claimed first ([`claims`](mod@crate::claims)) or
21/// asked of a companion daemon ([`calls`](mod@crate::calls)); [`crate::view::is_driving`]
22/// says whether the user is controlling this viewer.
23pub trait UiPlugin: Sized + 'static {
24    /// Called once after instantiation. Create surfaces and read the
25    /// configuration here.
26    fn activate(cx: &mut Context) -> Self;
27
28    /// Called when a paced frame is due and one of the plugin's surfaces is
29    /// visible. Paint, commit, and say when the next frame is wanted
30    /// ([`Frame::request_frame`], [`Frame::wake_at`]); asking for nothing
31    /// waits for an event.
32    fn frame(&mut self, frame: &Frame);
33
34    /// Everything else the host tells the plugin. Surfaces are already
35    /// rebound when a [`Event::Resize`] arrives.
36    fn event(&mut self, event: Event, cx: &mut Context) {
37        let _ = (event, cx);
38    }
39
40    /// Called before the instance is dropped.
41    fn deactivate(&mut self) {}
42}
43
44/// What a plugin is called with besides a frame: its configuration, and
45/// handles to every interface.
46#[derive(Debug, Default)]
47pub struct Context {
48    config: String,
49}
50
51impl Context {
52    pub const fn new() -> Self {
53        Self {
54            config: String::new(),
55        }
56    }
57
58    /// The configuration document `activate` was called with.
59    pub fn config_json(&self) -> Json {
60        Json(self.config.clone())
61    }
62
63    /// The configuration, deserialized (an empty document is `null`).
64    pub fn config<T: DeserializeOwned>(&self) -> Result<T> {
65        if self.config.trim().is_empty() {
66            return Json(String::from("null")).parse();
67        }
68        Json(self.config.clone()).parse()
69    }
70
71    pub fn values(&self) -> api::values::Values {
72        api::values()
73    }
74
75    pub fn live(&self) -> api::live::Live {
76        api::live()
77    }
78
79    pub fn events(&self) -> api::events::Events {
80        api::events()
81    }
82
83    pub fn calls(&self) -> api::calls::Calls {
84        api::calls()
85    }
86
87    pub fn claims(&self) -> api::claims::Claims {
88        api::claims()
89    }
90
91    pub fn is_driving(&self) -> bool {
92        api::view::is_driving()
93    }
94
95    pub fn capabilities(&self) -> api::capabilities::Capabilities {
96        api::capabilities::get()
97    }
98
99    /// Asks for a frame on the next paced frame, from an event handler
100    /// (inside [`UiPlugin::frame`], use [`Frame::request_frame`]). A commit
101    /// in `event` is otherwise sampled on the next frame something else
102    /// causes.
103    pub fn request_frame(&self) {
104        crate::host::request_frame();
105    }
106
107    /// Asks for a frame at `at_ms` on the viewer's clock.
108    pub fn wake_at(&self, at_ms: u64) {
109        crate::host::wake_at(at_ms);
110    }
111
112    /// Opens a `stage` surface. Only while handling a user gesture: a key,
113    /// a paste, a pointer press on one of the plugin's surfaces, or one of
114    /// its commands.
115    pub fn open(&self, surface: &str) -> crate::Result<()> {
116        crate::host::surface_open(surface)
117    }
118
119    /// Shows a pane in this viewer's workspace and gives it focus, as a
120    /// click on its sidebar row does. Only while handling a user gesture,
121    /// as [`Context::open`]; a pane the viewer does not know is ignored.
122    pub fn focus_pane(&self, pane: &str) -> crate::Result<()> {
123        crate::host::focus_pane(pane)
124    }
125
126    /// Opens an `https://` URL in the user's browser ([`crate::url::open`]):
127    /// while handling user input, or within a second after it.
128    pub fn open_url(&self, url: &str) -> crate::Result<()> {
129        crate::host::url_open(url)
130    }
131
132    /// Closes a surface [`Context::open`] opened.
133    pub fn close(&self, surface: &str) -> crate::Result<()> {
134        crate::host::surface_close(surface)
135    }
136
137    /// Asks for `surface` to be `cols` by `rows` cells (0: the viewer's
138    /// choice in that dimension) instead of the manifest's size. The
139    /// viewer clamps it where it places the surface and sends
140    /// [`Event::Resize`] when the size changes. A card grows to show more
141    /// rows; a popover fits its content.
142    pub fn request_size(&self, surface: &str, cols: u32, rows: u32) -> crate::Result<()> {
143        crate::host::surface_request_size(surface, cols, rows)
144    }
145
146    /// Sets a short label the viewer shows at the right end of `surface`'s
147    /// chrome title: a boxed card's top border, a pane's header, a column's
148    /// title row. `None` removes it. At most 48 characters with no control
149    /// characters.
150    pub fn set_label(&self, surface: &str, label: Option<&str>) -> crate::Result<()> {
151        crate::host::surface_set_label(surface, label)
152    }
153
154    /// Places the viewer's own text cursor at (`col`, `row`) of `surface`
155    /// while the surface takes keys (an open stage, popover or pane
156    /// overlay, or the surface a click or `open` gave input focus), so a
157    /// text field draws no cursor of its own; the cursor trail moves to it.
158    /// `None` removes it. Stays until set again.
159    pub fn set_caret(&self, surface: &str, caret: Option<(u32, u32)>) -> crate::Result<()> {
160        crate::host::surface_set_caret(surface, caret)
161    }
162}
163
164/// Whether the viewer's machine is saving power.
165#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
166pub enum Power {
167    #[default]
168    Mains,
169    SavePower,
170}
171
172impl From<wit::Power> for Power {
173    fn from(power: wit::Power) -> Self {
174        match power {
175            wit::Power::Mains => Self::Mains,
176            wit::Power::SavePower => Self::SavePower,
177        }
178    }
179}
180
181/// One frame callback.
182#[derive(Debug)]
183pub struct Frame {
184    now_ms: u64,
185    interval_ms: u32,
186    power: Power,
187    elapsed_ms: u64,
188    next: Cell<Option<u64>>,
189}
190
191impl Frame {
192    /// A frame at `now_ms` (for tests; the runtime makes the real ones).
193    pub fn new(now_ms: u64, interval_ms: u32, power: Power) -> Self {
194        Self {
195            now_ms,
196            interval_ms,
197            power,
198            elapsed_ms: 0,
199            next: Cell::new(None),
200        }
201    }
202
203    /// Milliseconds since the previous frame; zero on the first frame, and
204    /// on the first frame after the plugin's surfaces were all hidden (a
205    /// hidden period is not play time).
206    pub fn elapsed(&self) -> u64 {
207        self.elapsed_ms
208    }
209
210    /// The viewer's monotonic clock, in milliseconds.
211    pub fn now_ms(&self) -> u64 {
212        self.now_ms
213    }
214
215    /// The paced frame interval.
216    pub fn interval_ms(&self) -> u32 {
217        self.interval_ms
218    }
219
220    pub fn power(&self) -> Power {
221        self.power
222    }
223
224    /// Asks for the next paced frame (an animation).
225    pub fn request_frame(&self) {
226        self.wake_at(self.now_ms);
227    }
228
229    /// Asks for a frame at `at_ms` on the viewer's clock (a clock asks for
230    /// the next second). The earliest request wins.
231    pub fn wake_at(&self, at_ms: u64) {
232        let next = self.next.get().map_or(at_ms, |next| next.min(at_ms));
233        self.next.set(Some(next));
234    }
235
236    /// Asks for a frame `after_ms` from now.
237    pub fn wake_after(&self, after_ms: u64) {
238        self.wake_at(self.now_ms.saturating_add(after_ms));
239    }
240
241    /// When the plugin asked to be called next.
242    pub fn next_wake(&self) -> Option<u64> {
243        self.next.get()
244    }
245}
246
247/// What the host tells a plugin besides frames.
248#[derive(Clone, Debug, PartialEq, Eq)]
249#[non_exhaustive]
250pub enum Event {
251    /// A surface changed size. The SDK has rebound it; repaint it.
252    Resize { surface: String, geometry: Geometry },
253    /// A surface became visible or hidden.
254    Visibility { surface: String, visible: bool },
255    /// Graphics support, cell pixel size or frame rate changed
256    /// ([`capabilities`](mod@crate::capabilities)).
257    CapabilitiesChanged,
258    /// [`crate::view::is_driving`] changed.
259    Driving(bool),
260    /// A watched value changed ([`crate::values::Values::watch`]).
261    ValueChanged { key: String, value: Option<Json> },
262    /// A live message ([`crate::live::Live::subscribe`]).
263    Live { key: String, payload: Json },
264    /// A live key was withdrawn ([`crate::live::Live::delete`]).
265    LiveDeleted { key: String },
266    /// A plugin event ([`crate::events::Events::on`]).
267    Plugin { name: String, payload: Json },
268    /// The account changed ([`crate::account::watch`]).
269    AccountChanged,
270    /// A key on the surface with input focus.
271    Key(Key),
272    /// Text pasted into the surface with input focus.
273    Paste { surface: String, text: String },
274    /// The pointer over one of the plugin's surfaces.
275    Pointer(Pointer),
276    /// The viewer's theme changed ([`crate::view::theme`]).
277    ThemeChanged,
278    /// A surface gained or lost input focus.
279    Focus { surface: String, focused: bool },
280    /// The user ran one of the manifest's commands, by id.
281    Command(String),
282    /// A [`Calls::call_async`](crate::calls::Calls::call_async) finished:
283    /// the companion's answer, or why there is none (a timeout is
284    /// [`Error::Unavailable`](crate::Error::Unavailable)).
285    CallResult {
286        id: crate::api::calls::CallId,
287        result: Result<Json>,
288    },
289    /// Daemon plugins: files under a [`watch`](crate::daemon::watch)
290    /// changed; every path that changed since the last delivery.
291    FileChanged { watch: u32, paths: Vec<String> },
292    /// Daemon plugins: a watch stopped delivering (the watcher's error).
293    WatchFailed { watch: u32, error: String },
294    /// Daemon plugins: output from a child's piped stream.
295    ProcessOutput {
296        pid: u32,
297        stderr: bool,
298        bytes: Vec<u8>,
299    },
300    /// Daemon plugins: a child exited (a signal is negated), after the
301    /// last of its output.
302    ProcessExited { pid: u32, status: i32 },
303    /// Daemon plugins: a pane on the daemon's machine changed
304    /// ([`panes::subscribe`](crate::daemon::panes::subscribe)).
305    PaneChanged {
306        kind: crate::daemon::PaneChangeKind,
307        pane: crate::api::account::Pane,
308    },
309    /// Daemon plugins: which of the plugin's UI surfaces the account's
310    /// viewers show changed ([`Context::interest`](crate::daemon::Context::interest)).
311    /// Start reading an outside source when [`any`](crate::daemon::Interest::any)
312    /// turns true and stop when it turns false.
313    Interest(crate::daemon::Interest),
314}
315
316impl From<wit::Event> for Event {
317    fn from(event: wit::Event) -> Self {
318        match event {
319            wit::Event::Resize((surface, g)) => Self::Resize {
320                surface,
321                geometry: Geometry {
322                    cols: g.cols,
323                    rows: g.rows,
324                    px_w: g.px_w,
325                    px_h: g.px_h,
326                    cell_px_w: g.cell_px_w,
327                    cell_px_h: g.cell_px_h,
328                },
329            },
330            wit::Event::Visibility((surface, visible)) => Self::Visibility { surface, visible },
331            wit::Event::CapabilitiesChanged => Self::CapabilitiesChanged,
332            wit::Event::Driving(driving) => Self::Driving(driving),
333            wit::Event::ValueChanged((key, value)) => Self::ValueChanged {
334                key,
335                value: value.map(Json),
336            },
337            wit::Event::Live((key, payload)) => Self::Live {
338                key,
339                payload: Json(payload),
340            },
341            wit::Event::LiveDeleted(key) => Self::LiveDeleted { key },
342            wit::Event::PluginEvent((name, payload)) => Self::Plugin {
343                name,
344                payload: Json(payload),
345            },
346            wit::Event::AccountChanged => Self::AccountChanged,
347            wit::Event::Key(key) => Self::Key(Key {
348                surface: key.surface,
349                code: KeyCode::parse(&key.code),
350                text: key.text,
351                modifiers: Modifiers(key.modifiers),
352                phase: match key.phase {
353                    wit::standard::plugin::event_types::KeyPhase::Press => KeyPhase::Press,
354                    wit::standard::plugin::event_types::KeyPhase::Repeat => KeyPhase::Repeat,
355                    wit::standard::plugin::event_types::KeyPhase::Release => KeyPhase::Release,
356                },
357            }),
358            wit::Event::Paste((surface, text)) => Self::Paste { surface, text },
359            wit::Event::Pointer(pointer) => Self::Pointer(Pointer {
360                surface: pointer.surface,
361                x: pointer.x,
362                y: pointer.y,
363                col: pointer.col,
364                row: pointer.row,
365                button: Button::from_wire(pointer.button),
366                kind: match pointer.kind {
367                    wit::standard::plugin::event_types::PointerKind::Down => PointerKind::Down,
368                    wit::standard::plugin::event_types::PointerKind::Up => PointerKind::Up,
369                    wit::standard::plugin::event_types::PointerKind::Move => PointerKind::Move,
370                    wit::standard::plugin::event_types::PointerKind::Drag => PointerKind::Drag,
371                    wit::standard::plugin::event_types::PointerKind::Wheel => PointerKind::Wheel,
372                    wit::standard::plugin::event_types::PointerKind::Enter => PointerKind::Enter,
373                    wit::standard::plugin::event_types::PointerKind::Leave => PointerKind::Leave,
374                },
375                modifiers: Modifiers(pointer.modifiers),
376            }),
377            wit::Event::ThemeChanged => Self::ThemeChanged,
378            wit::Event::Focus((surface, focused)) => Self::Focus { surface, focused },
379            wit::Event::Command(id) => Self::Command(id),
380            wit::Event::CallResult(answer) => Self::CallResult {
381                id: crate::api::calls::CallId(answer.id),
382                result: answer.outcome.map(Json).map_err(Into::into),
383            },
384        }
385    }
386}
387
388/// The instance's plugin and context, driven by the exports.
389#[doc(hidden)]
390pub struct UiRuntime<P> {
391    plugin: RefCell<Option<P>>,
392    cx: RefCell<Context>,
393    /// The previous frame's clock; `None` at the start and after every
394    /// surface was hidden.
395    last_frame_ms: Cell<Option<u64>>,
396    /// Surfaces the viewer shows now.
397    visible: RefCell<alloc::collections::BTreeSet<String>>,
398}
399
400// SAFETY: a component instance runs on one thread and the host calls one
401// export at a time; the exports are the only users of the static runtime.
402// On other targets the static is never reached (tests drive their own
403// runtime through `testing::Harness`).
404unsafe impl<P> Sync for UiRuntime<P> {}
405
406impl<P> Default for UiRuntime<P> {
407    fn default() -> Self {
408        Self::new()
409    }
410}
411
412impl<P> UiRuntime<P> {
413    pub const fn new() -> Self {
414        Self {
415            plugin: RefCell::new(None),
416            cx: RefCell::new(Context::new()),
417            last_frame_ms: Cell::new(None),
418            visible: RefCell::new(alloc::collections::BTreeSet::new()),
419        }
420    }
421}
422
423impl<P: UiPlugin> UiRuntime<P> {
424    pub fn activate(&self, config: String) {
425        let mut cx = self.cx.borrow_mut();
426        cx.config = config;
427        let plugin = P::activate(&mut cx);
428        *self.plugin.borrow_mut() = Some(plugin);
429    }
430
431    pub fn frame(&self, now_ms: u64, interval_ms: u32, power: Power) -> Option<u64> {
432        let mut frame = Frame::new(now_ms, interval_ms, power);
433        frame.elapsed_ms = self
434            .last_frame_ms
435            .get()
436            .map_or(0, |last| now_ms.saturating_sub(last));
437        self.last_frame_ms.set(Some(now_ms));
438        if let Some(plugin) = self.plugin.borrow_mut().as_mut() {
439            plugin.frame(&frame);
440        }
441        frame.next_wake()
442    }
443
444    pub fn event(&self, event: Event) {
445        match &event {
446            Event::Resize { surface, .. } => crate::surface::rebind_surfaces(surface),
447            Event::Visibility { surface, visible } => {
448                let mut shown = self.visible.borrow_mut();
449                if *visible {
450                    shown.insert(surface.clone());
451                } else {
452                    shown.remove(surface);
453                    if shown.is_empty() {
454                        self.last_frame_ms.set(None);
455                    }
456                }
457            }
458            _ => {}
459        }
460        let mut cx = self.cx.borrow_mut();
461        if let Some(plugin) = self.plugin.borrow_mut().as_mut() {
462            plugin.event(event, &mut cx);
463        }
464    }
465
466    pub fn deactivate(&self) {
467        let plugin = self.plugin.borrow_mut().take();
468        if let Some(mut plugin) = plugin {
469            plugin.deactivate();
470        }
471    }
472
473    /// The plugin, for tests.
474    pub fn with_plugin<R>(&self, f: impl FnOnce(&mut P) -> R) -> Option<R> {
475        self.plugin.borrow_mut().as_mut().map(f)
476    }
477}