Skip to main content
KeyOS API Reference

gui_server_api/
lib.rs

1// SPDX-FileCopyrightText: 2024 Foundation Devices, Inc. <hello@foundation.xyz>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use {
5    num_derive::FromPrimitive,
6    num_traits::FromPrimitive,
7    server::{AsScalar, CheckedConn, CheckedPermissions, FromScalar, MessageAllowed},
8    xous::{MemoryRange, CID, PID, SID},
9};
10
11pub mod consts;
12pub mod error;
13pub mod msg;
14pub mod navigation;
15#[cfg(not(keyos))]
16pub mod simulator;
17pub mod touch;
18
19pub use error::GuiServerError;
20
21#[macro_export]
22macro_rules! use_api {
23    ($gui:path, $server:path) => {
24        mod gui_permissions {
25            use gui_server_api::msg::*;
26            pub use $gui as gui_server_api;
27            use $server as server;
28            #[derive(Clone, Default, server::Permissions)]
29            #[server_name = "os/gui-server"]
30            pub struct GuiPermissions;
31        }
32        type GuiApi = gui_permissions::gui_server_api::GuiApi<gui_permissions::GuiPermissions>;
33        type GuiApiLight = gui_permissions::gui_server_api::GuiApiLight<gui_permissions::GuiPermissions>;
34    };
35    () => {
36        gui_server_api::use_api!(gui_server_api, server);
37    };
38}
39
40pub type AppName = String;
41
42/// An RGB background color requested by an app for the collapsed Control Center.
43/// The Control Center chooses the foreground color; apps cannot control it.
44#[derive(Debug, Copy, Clone, PartialEq, Eq)]
45pub struct ControlCenterColor {
46    pub red: u8,
47    pub green: u8,
48    pub blue: u8,
49}
50
51impl ControlCenterColor {
52    pub const fn new(red: u8, green: u8, blue: u8) -> Self { Self { red, green, blue } }
53}
54
55#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
56pub struct RegisterApp {
57    pub cid: CID,
58    pub name: AppName,
59    pub height: usize,
60}
61
62#[derive(Debug, Copy, Clone, FromPrimitive, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Default)]
63pub enum ModalStyle {
64    /// A regular modal card that slides up from the bottom of the screen.
65    /// The user can drag it and dismiss it by dragging or clicking away.
66    #[default]
67    SlideUpDraggablePopup = 0,
68
69    /// A modal card that slides up from the bottom of the screen.
70    /// The user can't drag it and dismiss it by clicking away.
71    SlideUpFixedPopup,
72
73    /// A modal card that slides up from the bottom of the screen and takes the entire screen.
74    SlideUpFullscreen,
75
76    /// A modal that appears instantly with no animation.
77    Instant,
78}
79
80/// Reduced GUI API, usable by non-gui daemons
81#[derive(Clone, Debug, Default)]
82pub struct GuiApiLight<P: CheckedPermissions> {
83    conn: CheckedConn<P>,
84}
85
86/// Full GUI API, usable by GUI apps
87#[derive(Debug)]
88pub struct GuiApi<P: CheckedPermissions> {
89    inner: GuiApiLight<P>,
90    cid_self: CID,
91    sid: SID,
92}
93
94impl<P: CheckedPermissions> GuiApiLight<P> {
95    /// Blocking connect: waits until gui-server has registered its name. gui-server is a
96    /// mandatory system service, so callers wait for it instead of timing out and failing.
97    pub fn connect() -> Self { Self { conn: CheckedConn::default() } }
98
99    /// Switches the focus to the app window of the given PID and the app zoom-in start position.
100    /// Used by the app launcher, app switcher, and usb-debug protocol.
101    pub fn switch_to(&self, next_pid: PID, x: usize, y: usize) -> Result<(), GuiServerError>
102    where
103        P: MessageAllowed<msg::SwitchTo>,
104    {
105        self.conn.try_send_scalar(msg::SwitchTo { next_pid: next_pid.get() as usize, x, y })?;
106        Ok(())
107    }
108
109    /// Switches the focus to the launcher app window. Used by apps.
110    pub fn switch_to_launcher(&self) -> Result<bool, GuiServerError>
111    where
112        P: MessageAllowed<msg::SwitchToLauncher>,
113    {
114        Ok(self.conn.try_send_blocking_scalar(msg::SwitchToLauncher)?)
115    }
116
117    pub fn is_locked(&self) -> Result<bool, GuiServerError>
118    where
119        P: MessageAllowed<msg::IsLocked>,
120    {
121        Ok(self.conn.try_send_blocking_scalar(msg::IsLocked)?)
122    }
123
124    pub fn shutdown(&self) -> Result<(), GuiServerError>
125    where
126        P: MessageAllowed<msg::Shutdown>,
127    {
128        Ok(self.conn.try_send_blocking_scalar(msg::Shutdown { reboot: false })?)
129    }
130
131    pub fn reboot(&self) -> Result<(), GuiServerError>
132    where
133        P: MessageAllowed<msg::Shutdown>,
134    {
135        Ok(self.conn.try_send_blocking_scalar(msg::Shutdown { reboot: true })?)
136    }
137
138    /// Closes the app window of the given PID.
139    /// Used by the launcher, switcher, and usb-debug protocol to gracefully close apps.
140    pub fn close_app(&self, pid: PID) -> Result<(), GuiServerError>
141    where
142        P: MessageAllowed<msg::CloseApp>,
143    {
144        Ok(self.conn.try_send_blocking_scalar(msg::CloseApp { pid: pid.get() as usize })??)
145    }
146
147    /// Captures the current composited screen as raw pixel data.
148    /// Returns a `DropDeallocate` of length `FB_SIZE` (SCREEN_WIDTH * SCREEN_HEIGHT * 4)
149    /// that auto-unmaps on drop. Dereferences to `MemoryRange` / `&[u8]`.
150    pub fn capture_screen(&self) -> Result<xous::DropDeallocate, GuiServerError>
151    where
152        P: MessageAllowed<msg::CaptureScreen>,
153    {
154        let mem = xous::map_memory(None, None, consts::FB_SIZE, xous::MemoryFlags::W)?;
155        self.conn.lend_mut(msg::CaptureScreen(mem));
156        Ok(xous::DropDeallocate::new(mem))
157    }
158
159    /// Injects a touch event as if it came from the hardware touch controller.
160    pub fn inject_touch(&self, touch: touch::Touch) -> Result<(), GuiServerError>
161    where
162        P: MessageAllowed<msg::InjectTouch>,
163    {
164        self.conn.try_send_scalar(msg::InjectTouch(touch))?;
165        Ok(())
166    }
167
168    /// Injects a key press or release event into the active app.
169    pub fn inject_key(&self, is_pressed: bool, key: Key) -> Result<(), GuiServerError>
170    where
171        P: MessageAllowed<msg::InjectKey>,
172    {
173        self.conn.try_send_scalar(msg::InjectKey { is_pressed, key })?;
174        Ok(())
175    }
176
177    /// Injects a power button press or release into gui-server's power-button state machine.
178    pub fn inject_power_button(&self, is_pressed: bool) -> Result<(), GuiServerError>
179    where
180        P: MessageAllowed<msg::InjectPowerButton>,
181    {
182        self.conn.try_send_scalar(msg::InjectPowerButton(is_pressed))?;
183        Ok(())
184    }
185
186    pub fn update_kiosk_policy(&self, policy: msg::UpdateKioskPolicy) -> Result<(), GuiServerError>
187    where
188        P: MessageAllowed<msg::UpdateKioskPolicy>,
189    {
190        self.conn.try_send_scalar(policy)?;
191        Ok(())
192    }
193}
194
195impl<P: CheckedPermissions> GuiApi<P> {
196    /// Registers an ordinary app window.
197    pub fn register(name: &str, height: usize) -> Result<Self, GuiServerError>
198    where
199        P: MessageAllowed<msg::RegisterAppMessage>,
200    {
201        let (api, cid) = Self::register_inner()?;
202        api.inner.conn.send_blocking_archive(msg::RegisterAppMessage(RegisterApp {
203            cid,
204            name: name.into(),
205            height,
206        }))?;
207        Ok(api)
208    }
209
210    /// Requests a background color for the collapsed Control Center while this
211    /// app is visible.
212    ///
213    /// Passing `None` restores the system theme color
214    pub fn set_control_center_color(&self, color: Option<ControlCenterColor>) -> Result<(), GuiServerError>
215    where
216        P: MessageAllowed<msg::SetControlCenterColor>,
217    {
218        self.inner.conn.try_send_scalar(msg::SetControlCenterColor { color })?;
219        Ok(())
220    }
221
222    /// Registers as the control center, which gui-server tracks as a dedicated
223    /// overlay window rather than an ordinary app.
224    pub fn register_control_center(height: usize) -> Result<Self, GuiServerError>
225    where
226        P: MessageAllowed<msg::RegisterControlCenter>,
227    {
228        let (api, cid) = Self::register_inner()?;
229        api.inner.conn.send_blocking_archive(msg::RegisterControlCenter { cid, height })?;
230        Ok(api)
231    }
232
233    /// Registers as the keyboard, which gui-server tracks as a dedicated overlay
234    /// window rather than an ordinary app.
235    pub fn register_keyboard(height: usize) -> Result<Self, GuiServerError>
236    where
237        P: MessageAllowed<msg::RegisterKeyboard>,
238    {
239        let (api, cid) = Self::register_inner()?;
240        api.inner.conn.send_blocking_archive(msg::RegisterKeyboard { cid, height })?;
241        Ok(api)
242    }
243
244    /// Claims a privileged role, then registers an ordinary app window. The role is
245    /// granted per message type, so an app can only claim a role its manifest permits.
246    pub fn register_with_role<M>(name: &str, height: usize) -> Result<Self, GuiServerError>
247    where
248        M: msg::RoleClaim,
249        P: MessageAllowed<msg::RegisterAppMessage> + MessageAllowed<M>,
250    {
251        let (api, cid) = Self::register_inner()?;
252        api.inner.conn.send_blocking_scalar(M::default());
253        api.inner.conn.send_blocking_archive(msg::RegisterAppMessage(RegisterApp {
254            cid,
255            name: name.into(),
256            height,
257        }))?;
258        Ok(api)
259    }
260
261    fn register_inner() -> Result<(Self, CID), GuiServerError> {
262        let sid = xous::create_server()?;
263        let cid_self = xous::connect(sid)?;
264        let inner = GuiApiLight::connect();
265        let api = Self { inner, sid, cid_self };
266        let gui_server_pid = api.inner.conn.get_remote_pid();
267
268        let gui_server_cid = xous::connect_for_process(gui_server_pid, api.sid)?;
269        xous::allow_messages_on_connection(gui_server_pid, gui_server_cid, 0..64)?;
270
271        Ok((api, gui_server_cid))
272    }
273
274    pub fn sid(&self) -> SID { self.sid }
275
276    /// Submit a frame for display.
277    pub fn submit_frame(&self, buffer: MemoryRange) -> Result<(), GuiServerError>
278    where
279        P: MessageAllowed<msg::SubmitFrame>,
280    {
281        Ok(self.conn.try_send_move(msg::SubmitFrame { buffer })?)
282    }
283
284    pub fn show_camera(&self, y_pos: u16) -> Result<(), GuiServerError>
285    where
286        P: MessageAllowed<msg::ShowCamera>,
287    {
288        self.conn.try_send_scalar(msg::ShowCamera { y_pos })?;
289        Ok(())
290    }
291
292    pub fn hide_camera(&self) -> Result<(), GuiServerError>
293    where
294        P: MessageAllowed<msg::HideCamera>,
295    {
296        self.conn.try_send_scalar(msg::HideCamera)?;
297        Ok(())
298    }
299
300    pub fn update_keyboard(&self, msg: msg::UpdateKeyboard) -> Result<(), GuiServerError>
301    where
302        P: MessageAllowed<msg::UpdateKeyboard>,
303    {
304        self.conn.try_send_archive(msg)?;
305        Ok(())
306    }
307
308    pub fn hide_keyboard(&self) -> Result<(), GuiServerError>
309    where
310        P: MessageAllowed<msg::HideKeyboard>,
311    {
312        self.conn.try_send_scalar(msg::HideKeyboard)?;
313        Ok(())
314    }
315
316    pub fn notify_login_success(&self) -> Result<(), GuiServerError>
317    where
318        P: MessageAllowed<msg::LoginSuccess>,
319    {
320        self.conn.try_send_scalar(msg::LoginSuccess)?;
321        Ok(())
322    }
323
324    pub fn wake_event_loop(&self) {
325        let msg = xous::Message::new_scalar(InputMessage::Noop as usize, 0, 0, 0, 0);
326        if let Err(e) = xous::send_message(self.cid_self, msg) {
327            log::error!("Failed to send wake event to self: {e:?}");
328        }
329    }
330
331    pub fn request_redraw(&self) -> Result<(), GuiServerError>
332    where
333        P: MessageAllowed<msg::RequestRedraw>,
334    {
335        self.conn.try_send_scalar(msg::RequestRedraw)?;
336        Ok(())
337    }
338
339    pub fn try_receive_input(&self) -> Option<(InputMessage, xous::MessageEnvelope)> {
340        if let Ok(Some(msg)) = xous::try_receive_message(self.sid) {
341            let opcode = FromPrimitive::from_usize(msg.body.id());
342            return opcode.map(|opcode| (opcode, msg));
343        }
344
345        None
346    }
347
348    pub fn receive_input(&self) -> Result<(InputMessage, xous::MessageEnvelope), GuiServerError> {
349        xous::receive_message(self.sid)
350            .map(|msg| {
351                let opcode = FromPrimitive::from_usize(msg.body.id());
352                (opcode.expect("input opcode"), msg)
353            })
354            .map_err(Into::into)
355    }
356
357    pub fn key_pressed(&self, key: Key) -> Result<(), GuiServerError>
358    where
359        P: MessageAllowed<msg::KeyPressed>,
360    {
361        self.conn.try_send_scalar(msg::KeyPressed(Some(key)))?;
362        Ok(())
363    }
364
365    pub fn key_released(&self, key: Key) -> Result<(), GuiServerError>
366    where
367        P: MessageAllowed<msg::KeyReleased>,
368    {
369        self.conn.try_send_scalar(msg::KeyReleased(Some(key)))?;
370        Ok(())
371    }
372
373    pub fn animate_next_frame(&self, animation_kind: NextFrameAnimationKind) -> Result<(), GuiServerError>
374    where
375        P: MessageAllowed<msg::AnimateNextFrame>,
376    {
377        self.conn.try_send_scalar(msg::AnimateNextFrame { animation_kind })?;
378        Ok(())
379    }
380}
381
382impl<P: CheckedPermissions> std::ops::Deref for GuiApi<P> {
383    type Target = GuiApiLight<P>;
384
385    fn deref(&self) -> &Self::Target { &self.inner }
386}
387
388#[derive(Debug, PartialEq, num_derive::FromPrimitive, num_derive::ToPrimitive, Copy, Clone)]
389pub enum InputMessage {
390    Touch = 0,
391    KeyPress,
392    KeyRelease,
393
394    /// Another app has navigated to this app, and the app is now in modal focus.
395    /// This input message is a notification to check the `GuiApi` for a navigation event.
396    NavigationFocused,
397
398    /// The app is being navigated away from and is no longer in modal focus.
399    NavigationCancelled,
400
401    /// The apps that block on input can unblock themselves by sending this message to themselves.
402    Noop,
403
404    /// The app is brought into the foreground.
405    Visible,
406
407    /// The app is getting minimized and hidden in the background.
408    Hidden,
409
410    /// A new framebuffer the app can draw to.
411    /// Can be the same as a previous buffer or a completely new one.
412    FrameBuffer,
413
414    Custom1,
415    Custom2,
416    Custom3,
417    Custom4,
418
419    /// The app should exit gracefully after receiving this.
420    CloseRequested,
421
422    /// Mouse/trackpad scroll in the emulator (hosted mode only).
423    /// Scalar args: arg1 = x (physical px), arg2 = y (physical px),
424    ///              arg3 = delta_x (f32 bits), arg4 = delta_y (f32 bits).
425    Scroll,
426}
427
428#[derive(Debug, Copy, Clone)]
429pub enum Key {
430    Char(usize),
431    Backspace,
432    Delete,
433    CursorLeft,
434    CursorRight,
435    Enter,
436    Tab,
437}
438
439impl server::AsScalar<2> for Key {
440    fn as_scalar(&self) -> [u32; 2] {
441        match self {
442            Key::Char(c) => [0, *c as _],
443            Key::Backspace => [1, 0],
444            Key::Delete => [2, 0],
445            Key::CursorLeft => [3, 0],
446            Key::CursorRight => [4, 0],
447            Key::Enter => [5, 0],
448            Key::Tab => [6, 0],
449        }
450    }
451}
452
453impl server::FromScalar<2> for Key {
454    fn from_scalar(value: [u32; 2]) -> Self {
455        match value[0] {
456            1 => Key::Backspace,
457            2 => Key::Delete,
458            3 => Key::CursorLeft,
459            4 => Key::CursorRight,
460            5 => Key::Enter,
461            6 => Key::Tab,
462            _ => Key::Char(value[1] as _),
463        }
464    }
465}
466
467impl<P: CheckedPermissions> Drop for GuiApi<P> {
468    fn drop(&mut self) {
469        if let Err(e) = xous::destroy_server(self.sid) {
470            log::error!("Error destroying gui api event server: {e:?}");
471        }
472    }
473}
474
475#[derive(Debug, Copy, Clone, FromPrimitive, Default)]
476pub enum NextFrameAnimationKind {
477    #[default]
478    SlideInLeft = 0,
479    SlideInRight,
480    SlideOutLeft,
481    SlideOutRight,
482}
483
484#[derive(
485    Debug,
486    Copy,
487    Clone,
488    FromPrimitive,
489    Default,
490    PartialEq,
491    Eq,
492    rkyv::Archive,
493    rkyv::Serialize,
494    rkyv::Deserialize,
495)]
496#[rkyv(derive(Debug))]
497pub enum KeyboardKind {
498    #[default]
499    Alphanumeric = 0,
500    Password,
501    Numbers,
502    Decimal,
503    Email,
504}
505
506impl FromScalar<1> for KeyboardKind {
507    fn from_scalar([value]: [u32; 1]) -> Self { Self::from_u32(value).unwrap_or_default() }
508}
509
510impl AsScalar<1> for KeyboardKind {
511    fn as_scalar(&self) -> [u32; 1] { [*self as u32] }
512}
513
514impl From<&ArchivedKeyboardKind> for KeyboardKind {
515    fn from(archived: &ArchivedKeyboardKind) -> Self {
516        rkyv::deserialize::<_, rkyv::rancor::Error>(archived).unwrap()
517    }
518}