vexide_slint/
lib.rs

1//! # Slint for vexide
2//!
3//! This crate exists to allow for the use of Slint-based UIs in [vexide]. It
4//! provides an implementation of the Slint `Platform` trait that uses the V5 brain
5//! to render the UI.
6//!
7//! [vexide]: https://vexide.dev
8//!
9//! ## Usage
10//!
11//! To use this crate, add it to your `Cargo.toml`:
12//!
13//! ```toml
14//! [dependencies]
15//! slint-vexide = "0.1.0"
16//! ```
17//!
18//! Then, you must call `slint_vexide::initialize_slint_platform()` before creating
19//! and running your Slint widget. This will set up Slint for software-rendering
20//! your UI on the brain's display.
21//!
22//! # Example
23//!
24//! ```no_run
25//! // Include the modules generated by Slint for your UI files.
26//! // You will need to configure your `build.rs` to do this; see below.
27//! slint::include_modules!();
28//!
29//! #[vexide::main]
30//! async fn main(peripherals: vexide::prelude::Peripherals) {
31//!     let robot = Robot {
32//!         // ...
33//!     };
34//!
35//!     // Since running the Slint UI is a blocking operation, we need to spawn the
36//!     // competition task as a separate task that will run concurrently.
37//!     // The Slint runtime internally polls all spawned futures.
38//!     vexide::task::spawn(robot.compete()).detach();
39//!
40//!     // Initialize the Slint platform with the V5 display-backed implementation.
41//!     vexide_slint::initialize_slint_platform(peripherals.display);
42//!     // Create and run the application. For more information on this, see the
43//!     // Slint documentation.
44//!     MyApplication::new()
45//!         .expect("Failed to create application")
46//!         .run()
47//!         .expect("Failed to run application");
48//!     // Since MyApplication::run() could return if the application is closed
49//!     // programmatically, we need to convince the compiler that the return type
50//!     // is `!` (never).
51//!     vexide::program::exit();
52//! }
53//! ```
54//!
55//! You'll need to compile your UI code separately from your main application code
56//! using a custom build script. Add the `slint-build` crate to your `Cargo.toml`:
57//!
58//! ```toml
59//! [build-dependencies]
60//! slint-build = "0.1.0"
61//! ```
62//!
63//! Then, create a `build.rs` file in your project root with the following content:
64//!
65//! ```no_run
66//! fn main() {
67//!     // Compile the Slint UI file with the appropriate configuration.
68//!     slint_build::compile_with_config(
69//!         "ui/YourFile.slint", // Path to your Slint UI file.
70//!         slint_build::CompilerConfiguration::new()
71//!             // Make sure to enable this configuration flag.
72//!             .embed_resources(slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer)
73//!             // Optionally, you can specify a style to use for the UI.
74//!             // Check the Slint documentation for more information.
75//!             .with_style("style-name".into()),
76//!     )
77//!     .expect("Slint build failed");
78//! }
79//! ```
80
81#![no_std]
82#![allow(clippy::needless_doctest_main)] // build script example should contain main function
83
84extern crate alloc;
85use alloc::{boxed::Box, rc::Rc};
86use core::cell::RefCell;
87
88use slint::{
89    platform::{
90        software_renderer::{MinimalSoftwareWindow, RepaintBufferType},
91        Platform, PointerEventButton, WindowEvent,
92    },
93    LogicalPosition, PhysicalPosition, PhysicalSize, Rgb8Pixel,
94};
95use vexide::devices::display::{Display, Rect, TouchEvent, TouchState};
96use vexide::time::Instant;
97
98/// A Slint platform implementation for the V5 Brain screen.
99///
100/// This struct is a wrapper around a [`Display`] and a [`MinimalSoftwareWindow`]
101/// and will handle updates to the screen through the [`Platform`] trait.
102pub struct V5Platform {
103    start: Instant,
104    window: Rc<MinimalSoftwareWindow>,
105    last_touch_event: RefCell<Option<TouchEvent>>,
106    display: RefCell<Display>,
107
108    buffer: RefCell<
109        [Rgb8Pixel;
110            Display::HORIZONTAL_RESOLUTION as usize * Display::VERTICAL_RESOLUTION as usize],
111    >,
112}
113impl V5Platform {
114    /// Create a new [`V5Platform`] from a [`Display`].
115    ///
116    /// This is used internally by [`initialize_slint_platform`] to create the
117    /// platform.
118    #[must_use]
119    pub fn new(display: Display) -> Self {
120        let window = MinimalSoftwareWindow::new(RepaintBufferType::NewBuffer);
121        window.set_size(PhysicalSize::new(
122            Display::HORIZONTAL_RESOLUTION as _,
123            Display::VERTICAL_RESOLUTION as _,
124        ));
125        Self {
126            start: Instant::now(),
127            window,
128            display: RefCell::new(display),
129            last_touch_event: RefCell::new(None),
130            #[allow(clippy::large_stack_arrays)] // we got plenty
131            buffer: RefCell::new(
132                [Rgb8Pixel::new(0, 0, 0);
133                    Display::HORIZONTAL_RESOLUTION as usize * Display::VERTICAL_RESOLUTION as usize],
134            ),
135        }
136    }
137
138    fn get_touch_event(&self) -> Option<WindowEvent> {
139        let event = self.display.borrow().touch_status();
140        // To avoid dispatching the same event multiple times
141        let mut last_event = self.last_touch_event.borrow_mut();
142        if Some(event) == *last_event || last_event.is_none() {
143            *last_event = Some(event);
144            return None;
145        }
146        let physical_pos = PhysicalPosition::new(event.x.into(), event.y.into());
147        let position = LogicalPosition::from_physical(physical_pos, 1.0);
148        let window_event = match event.state {
149            TouchState::Released => WindowEvent::PointerReleased {
150                position,
151                button: PointerEventButton::Left,
152            },
153            TouchState::Pressed => WindowEvent::PointerPressed {
154                position,
155                button: PointerEventButton::Left,
156            },
157            TouchState::Held => WindowEvent::PointerMoved { position },
158        };
159        *last_event = Some(event);
160        Some(window_event)
161    }
162}
163
164impl Platform for V5Platform {
165    fn create_window_adapter(
166        &self,
167    ) -> Result<alloc::rc::Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
168        Ok(self.window.clone())
169    }
170    fn duration_since_start(&self) -> core::time::Duration {
171        self.start.elapsed()
172    }
173    fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
174        loop {
175            // Update the Slint timers and animations
176            slint::platform::update_timers_and_animations();
177
178            self.window.draw_if_needed(|renderer| {
179                // Render the UI to our buffer
180                let mut buf = *self.buffer.borrow_mut();
181                renderer.render(&mut buf, Display::HORIZONTAL_RESOLUTION as _);
182
183                // Draw the buffer to the screen
184                self.display.borrow_mut().draw_buffer(
185                    Rect::from_dimensions(
186                        [0, 0],
187                        Display::HORIZONTAL_RESOLUTION as _,
188                        Display::VERTICAL_RESOLUTION as _,
189                    ),
190                    buf,
191                    Display::HORIZONTAL_RESOLUTION.into(),
192                );
193            });
194
195            // Connect V5 touch events to Slint
196            if let Some(event) = self.get_touch_event() {
197                self.window.dispatch_event(event);
198            }
199
200            // Hand the CPU back to the scheduler so that user code can run
201            // This used to not run if there were any animations running, but
202            // it seems to be necessary to run it regardless
203            vexide::runtime::block_on(vexide::time::sleep(Display::REFRESH_INTERVAL));
204        }
205    }
206}
207
208/// Sets the Slint platform to [`V5Platform`].
209///
210/// This function should be called before any other Slint functions are called
211/// and lets Slint know that it should use the V5 Brain screen as the platform.
212///
213/// # Panics
214///
215/// Panics if the Slint platform is already set (i.e., this function has already
216/// been called).
217pub fn initialize_slint_platform(display: Display) {
218    slint::platform::set_platform(Box::new(V5Platform::new(display)))
219        .expect("Slint platform already set!");
220}