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}