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    /// Requests closing the app window of the given PID, without waiting for the outcome.
148    /// Callers gui-server itself may block on must use this instead of [`Self::close_app`].
149    pub fn request_app_close(&self, pid: PID) -> Result<(), GuiServerError>
150    where
151        P: MessageAllowed<msg::RequestAppClose>,
152    {
153        self.conn.try_send_scalar(msg::RequestAppClose { pid: pid.get() as usize })?;
154        Ok(())
155    }
156
157    /// Captures the current composited screen as raw pixel data.
158    /// Returns a `DropDeallocate` of length `FB_SIZE` (SCREEN_WIDTH * SCREEN_HEIGHT * 4)
159    /// that auto-unmaps on drop. Dereferences to `MemoryRange` / `&[u8]`.
160    pub fn capture_screen(&self) -> Result<xous::DropDeallocate, GuiServerError>
161    where
162        P: MessageAllowed<msg::CaptureScreen>,
163    {
164        let mem = xous::map_memory(None, None, consts::FB_SIZE, xous::MemoryFlags::W)?;
165        self.conn.lend_mut(msg::CaptureScreen(mem));
166        Ok(xous::DropDeallocate::new(mem))
167    }
168
169    /// Injects a touch event as if it came from the hardware touch controller.
170    pub fn inject_touch(&self, touch: touch::Touch) -> Result<(), GuiServerError>
171    where
172        P: MessageAllowed<msg::InjectTouch>,
173    {
174        self.conn.try_send_scalar(msg::InjectTouch(touch))?;
175        Ok(())
176    }
177
178    /// Injects a key press or release event into the active app.
179    pub fn inject_key(&self, is_pressed: bool, key: Key) -> Result<(), GuiServerError>
180    where
181        P: MessageAllowed<msg::InjectKey>,
182    {
183        self.conn.try_send_scalar(msg::InjectKey { is_pressed, key })?;
184        Ok(())
185    }
186
187    /// Injects a power button press or release into gui-server's power-button state machine.
188    pub fn inject_power_button(&self, is_pressed: bool) -> Result<(), GuiServerError>
189    where
190        P: MessageAllowed<msg::InjectPowerButton>,
191    {
192        self.conn.try_send_scalar(msg::InjectPowerButton(is_pressed))?;
193        Ok(())
194    }
195
196    pub fn update_kiosk_policy(&self, policy: msg::UpdateKioskPolicy) -> Result<(), GuiServerError>
197    where
198        P: MessageAllowed<msg::UpdateKioskPolicy>,
199    {
200        self.conn.try_send_scalar(policy)?;
201        Ok(())
202    }
203}
204
205impl<P: CheckedPermissions> GuiApi<P> {
206    /// Registers an ordinary app window.
207    pub fn register(name: &str, height: usize) -> Result<Self, GuiServerError>
208    where
209        P: MessageAllowed<msg::RegisterAppMessage>,
210    {
211        let (api, cid) = Self::register_inner()?;
212        api.inner.conn.send_blocking_archive(msg::RegisterAppMessage(RegisterApp {
213            cid,
214            name: name.into(),
215            height,
216        }))?;
217        Ok(api)
218    }
219
220    /// Requests a background color for the collapsed Control Center while this
221    /// app is visible.
222    ///
223    /// Passing `None` restores the system theme color
224    pub fn set_control_center_color(&self, color: Option<ControlCenterColor>) -> Result<(), GuiServerError>
225    where
226        P: MessageAllowed<msg::SetControlCenterColor>,
227    {
228        self.inner.conn.try_send_scalar(msg::SetControlCenterColor { color })?;
229        Ok(())
230    }
231
232    /// Registers as the control center, which gui-server tracks as a dedicated
233    /// overlay window rather than an ordinary app.
234    pub fn register_control_center(height: usize) -> Result<Self, GuiServerError>
235    where
236        P: MessageAllowed<msg::RegisterControlCenter>,
237    {
238        let (api, cid) = Self::register_inner()?;
239        api.inner.conn.send_blocking_archive(msg::RegisterControlCenter { cid, height })?;
240        Ok(api)
241    }
242
243    /// Registers as the keyboard, which gui-server tracks as a dedicated overlay
244    /// window rather than an ordinary app.
245    pub fn register_keyboard(height: usize) -> Result<Self, GuiServerError>
246    where
247        P: MessageAllowed<msg::RegisterKeyboard>,
248    {
249        let (api, cid) = Self::register_inner()?;
250        api.inner.conn.send_blocking_archive(msg::RegisterKeyboard { cid, height })?;
251        Ok(api)
252    }
253
254    /// Claims a privileged role, then registers an ordinary app window. The role is
255    /// granted per message type, so an app can only claim a role its manifest permits.
256    pub fn register_with_role<M>(name: &str, height: usize) -> Result<Self, GuiServerError>
257    where
258        M: msg::RoleClaim,
259        P: MessageAllowed<msg::RegisterAppMessage> + MessageAllowed<M>,
260    {
261        let (api, cid) = Self::register_inner()?;
262        api.inner.conn.send_blocking_scalar(M::default());
263        api.inner.conn.send_blocking_archive(msg::RegisterAppMessage(RegisterApp {
264            cid,
265            name: name.into(),
266            height,
267        }))?;
268        Ok(api)
269    }
270
271    fn register_inner() -> Result<(Self, CID), GuiServerError> {
272        let sid = xous::create_server()?;
273        let cid_self = xous::connect(sid)?;
274        let inner = GuiApiLight::connect();
275        let api = Self { inner, sid, cid_self };
276        let gui_server_pid = api.inner.conn.get_remote_pid();
277
278        let gui_server_cid = xous::connect_for_process(gui_server_pid, api.sid)?;
279        xous::allow_messages_on_connection(gui_server_pid, gui_server_cid, 0..64)?;
280
281        Ok((api, gui_server_cid))
282    }
283
284    pub fn sid(&self) -> SID { self.sid }
285
286    /// Submit a frame for display.
287    pub fn submit_frame(&self, buffer: MemoryRange) -> Result<(), GuiServerError>
288    where
289        P: MessageAllowed<msg::SubmitFrame>,
290    {
291        Ok(self.conn.try_send_move(msg::SubmitFrame { buffer })?)
292    }
293
294    pub fn show_camera(&self, y_pos: u16) -> Result<(), GuiServerError>
295    where
296        P: MessageAllowed<msg::ShowCamera>,
297    {
298        self.conn.try_send_scalar(msg::ShowCamera { y_pos })?;
299        Ok(())
300    }
301
302    pub fn hide_camera(&self) -> Result<(), GuiServerError>
303    where
304        P: MessageAllowed<msg::HideCamera>,
305    {
306        self.conn.try_send_scalar(msg::HideCamera)?;
307        Ok(())
308    }
309
310    pub fn update_keyboard(&self, msg: msg::UpdateKeyboard) -> Result<(), GuiServerError>
311    where
312        P: MessageAllowed<msg::UpdateKeyboard>,
313    {
314        self.conn.try_send_archive(msg)?;
315        Ok(())
316    }
317
318    pub fn hide_keyboard(&self) -> Result<(), GuiServerError>
319    where
320        P: MessageAllowed<msg::HideKeyboard>,
321    {
322        self.conn.try_send_scalar(msg::HideKeyboard)?;
323        Ok(())
324    }
325
326    pub fn notify_login_success(&self) -> Result<(), GuiServerError>
327    where
328        P: MessageAllowed<msg::LoginSuccess>,
329    {
330        self.conn.try_send_scalar(msg::LoginSuccess)?;
331        Ok(())
332    }
333
334    pub fn wake_event_loop(&self) {
335        let msg = xous::Message::new_scalar(InputMessage::Noop as usize, 0, 0, 0, 0);
336        if let Err(e) = xous::send_message(self.cid_self, msg) {
337            log::error!("Failed to send wake event to self: {e:?}");
338        }
339    }
340
341    pub fn request_redraw(&self) -> Result<(), GuiServerError>
342    where
343        P: MessageAllowed<msg::RequestRedraw>,
344    {
345        self.conn.try_send_scalar(msg::RequestRedraw)?;
346        Ok(())
347    }
348
349    pub fn try_receive_input(&self) -> Option<(InputMessage, xous::MessageEnvelope)> {
350        if let Ok(Some(msg)) = xous::try_receive_message(self.sid) {
351            let opcode = FromPrimitive::from_usize(msg.body.id());
352            return opcode.map(|opcode| (opcode, msg));
353        }
354
355        None
356    }
357
358    pub fn receive_input(&self) -> Result<(InputMessage, xous::MessageEnvelope), GuiServerError> {
359        xous::receive_message(self.sid)
360            .map(|msg| {
361                let opcode = FromPrimitive::from_usize(msg.body.id());
362                (opcode.expect("input opcode"), msg)
363            })
364            .map_err(Into::into)
365    }
366
367    pub fn key_pressed(&self, key: Key) -> Result<(), GuiServerError>
368    where
369        P: MessageAllowed<msg::KeyPressed>,
370    {
371        self.conn.try_send_scalar(msg::KeyPressed(Some(key)))?;
372        Ok(())
373    }
374
375    pub fn key_released(&self, key: Key) -> Result<(), GuiServerError>
376    where
377        P: MessageAllowed<msg::KeyReleased>,
378    {
379        self.conn.try_send_scalar(msg::KeyReleased(Some(key)))?;
380        Ok(())
381    }
382
383    pub fn animate_next_frame(&self, animation_kind: NextFrameAnimationKind) -> Result<(), GuiServerError>
384    where
385        P: MessageAllowed<msg::AnimateNextFrame>,
386    {
387        self.conn.try_send_scalar(msg::AnimateNextFrame { animation_kind })?;
388        Ok(())
389    }
390}
391
392impl<P: CheckedPermissions> std::ops::Deref for GuiApi<P> {
393    type Target = GuiApiLight<P>;
394
395    fn deref(&self) -> &Self::Target { &self.inner }
396}
397
398#[derive(Debug, PartialEq, num_derive::FromPrimitive, num_derive::ToPrimitive, Copy, Clone)]
399pub enum InputMessage {
400    Touch = 0,
401    KeyPress,
402    KeyRelease,
403
404    /// Another app has navigated to this app, and the app is now in modal focus.
405    /// This input message is a notification to check the `GuiApi` for a navigation event.
406    NavigationFocused,
407
408    /// The app is being navigated away from and is no longer in modal focus.
409    NavigationCancelled,
410
411    /// The apps that block on input can unblock themselves by sending this message to themselves.
412    Noop,
413
414    /// The app is brought into the foreground.
415    Visible,
416
417    /// The app is getting minimized and hidden in the background.
418    Hidden,
419
420    /// A new framebuffer the app can draw to.
421    /// Can be the same as a previous buffer or a completely new one.
422    FrameBuffer,
423
424    Custom1,
425    Custom2,
426    Custom3,
427    Custom4,
428
429    /// The app should exit gracefully after receiving this.
430    CloseRequested,
431
432    /// Mouse/trackpad scroll in the emulator (hosted mode only).
433    /// Scalar args: arg1 = x (physical px), arg2 = y (physical px),
434    ///              arg3 = delta_x (f32 bits), arg4 = delta_y (f32 bits).
435    Scroll,
436}
437
438#[derive(Debug, Copy, Clone)]
439pub enum Key {
440    Char(usize),
441    Backspace,
442    Delete,
443    CursorLeft,
444    CursorRight,
445    Enter,
446    Tab,
447}
448
449impl server::AsScalar<2> for Key {
450    fn as_scalar(&self) -> [u32; 2] {
451        match self {
452            Key::Char(c) => [0, *c as _],
453            Key::Backspace => [1, 0],
454            Key::Delete => [2, 0],
455            Key::CursorLeft => [3, 0],
456            Key::CursorRight => [4, 0],
457            Key::Enter => [5, 0],
458            Key::Tab => [6, 0],
459        }
460    }
461}
462
463impl server::FromScalar<2> for Key {
464    fn from_scalar(value: [u32; 2]) -> Self {
465        match value[0] {
466            1 => Key::Backspace,
467            2 => Key::Delete,
468            3 => Key::CursorLeft,
469            4 => Key::CursorRight,
470            5 => Key::Enter,
471            6 => Key::Tab,
472            _ => Key::Char(value[1] as _),
473        }
474    }
475}
476
477impl<P: CheckedPermissions> Drop for GuiApi<P> {
478    fn drop(&mut self) {
479        if let Err(e) = xous::destroy_server(self.sid) {
480            log::error!("Error destroying gui api event server: {e:?}");
481        }
482    }
483}
484
485#[derive(Debug, Copy, Clone, FromPrimitive, Default)]
486pub enum NextFrameAnimationKind {
487    #[default]
488    SlideInLeft = 0,
489    SlideInRight,
490    SlideOutLeft,
491    SlideOutRight,
492}
493
494#[derive(
495    Debug,
496    Copy,
497    Clone,
498    FromPrimitive,
499    Default,
500    PartialEq,
501    Eq,
502    rkyv::Archive,
503    rkyv::Serialize,
504    rkyv::Deserialize,
505)]
506#[rkyv(derive(Debug))]
507pub enum KeyboardKind {
508    #[default]
509    Alphanumeric = 0,
510    Password,
511    Numbers,
512    Decimal,
513    Email,
514}
515
516impl FromScalar<1> for KeyboardKind {
517    fn from_scalar([value]: [u32; 1]) -> Self { Self::from_u32(value).unwrap_or_default() }
518}
519
520impl AsScalar<1> for KeyboardKind {
521    fn as_scalar(&self) -> [u32; 1] { [*self as u32] }
522}
523
524impl From<&ArchivedKeyboardKind> for KeyboardKind {
525    fn from(archived: &ArchivedKeyboardKind) -> Self {
526        rkyv::deserialize::<_, rkyv::rancor::Error>(archived).unwrap()
527    }
528}