diff --git a/.mise/tasks.toml b/.mise/tasks.toml index ab1b3e3991..21b19ff0f5 100644 --- a/.mise/tasks.toml +++ b/.mise/tasks.toml @@ -38,8 +38,13 @@ description = "Run cargo fmt --all" run = "cargo fmt --all" dir = "examples/servo" +["fix:rust:format:safeui"] +description = "Run cargo fmt --all" +run = "cargo fmt --all" +dir = "examples/safe-ui" + ["fix:rust:format:all"] -depends = ["fix:rust:format:root", "fix:rust:format:bevy", "fix:rust:format:servo"] +depends = ["fix:rust:format:root", "fix:rust:format:bevy", "fix:rust:format:servo", "fix:rust:format:safeui"] ["fix:toml:format"] description = "Run taplo format" diff --git a/examples/safe-ui/.gitignore b/examples/safe-ui/.gitignore new file mode 100644 index 0000000000..2f7896d1d1 --- /dev/null +++ b/examples/safe-ui/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/examples/safe-ui/CMakeLists.txt b/examples/safe-ui/CMakeLists.txt new file mode 100644 index 0000000000..13472dea30 --- /dev/null +++ b/examples/safe-ui/CMakeLists.txt @@ -0,0 +1,32 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: MIT + +cmake_minimum_required(VERSION 3.21) +project(SlintSafeUI LANGUAGES C CXX VERSION 1.0) + +include(FetchContent) +FetchContent_Declare( + Corrosion + GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git + GIT_TAG v0.6.0 +) +FetchContent_MakeAvailable(Corrosion) + +set(Rust_CARGO_TARGET_LINK_NATIVE_LIBS "" CACHE INTERNAL "") + +corrosion_import_crate(MANIFEST_PATH "${CMAKE_CURRENT_SOURCE_DIR}/Cargo.toml" + CRATES slint-safeui CRATE_TYPES staticlib) + +set_property( + TARGET slint_safeui_lib + PROPERTY CORROSION_NO_DEFAULT_FEATURES + ON +) + +add_library(SlintSafeUi INTERFACE) +target_link_libraries(SlintSafeUi INTERFACE slint_safeui_lib) + +target_include_directories(SlintSafeUi INTERFACE + $ + $ +) diff --git a/examples/safe-ui/Cargo.toml b/examples/safe-ui/Cargo.toml new file mode 100644 index 0000000000..e81f05d36a --- /dev/null +++ b/examples/safe-ui/Cargo.toml @@ -0,0 +1,39 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: MIT + +[workspace] + +[package] +name = "slint-safeui" +version = "1.15.0" +edition = "2024" +build = "build.rs" + +[lib] +path = "src/lib.rs" +crate-type = ["rlib", "staticlib"] +name = "slint_safeui_lib" + +[[bin]] +name = "slint_safeui" +path = "src/simulator.rs" + +[features] +simulator = ["slint/backend-winit", "dep:smol"] +default = ["simulator"] + +[dependencies] +bytemuck = "1.24.0" +slint = { version = "1.14.1", default-features = false, features = ["compat-1-2", "renderer-software", "libm", "unsafe-single-threaded"] } +smol = { version = "2.0.0", optional = true } + +[profile.release] +panic = "abort" +opt-level = "s" + +[profile.dev] +panic = "abort" + +[build-dependencies] +bindgen = "0.72.1" +slint-build = { version = "1.14.1", features = ["sdf-fonts"] } diff --git a/examples/safe-ui/README.md b/examples/safe-ui/README.md new file mode 100644 index 0000000000..cdb1d28913 --- /dev/null +++ b/examples/safe-ui/README.md @@ -0,0 +1,77 @@ +# Slint Safety Critical UI Demo + +We aim to make Slint suitable in environments that require reliable display of safety-critical UI, such as vehicles of any kind, medical devices, or industrial tools and machines. + +This example serves as a starting point for a setup where strict separation of domains into a safety domain and an application domain is implemented either by hardware or system software: + +- The application domain is for example a Slint based application running on Linux, rendering into some kind of surface that only indirectly makes it to the physical output screen. +- The safety domain could be implemented by means of hardware or software. This domain is restricted and would be subject to a device specific safety certification. We aim to demonstrate + that Slint is suitable for this use-case. + +The safety domain is assumed to be split into two parts again: + + - A system or hardware specific layer. + - The Rust-based Slint and application safety layer. + + This directory contains the Slint safety layer scaffolding and interface. The interface to the system layer is based on a few low-level C functions. The application specific + safety critical UI is implemented in Slint and Rust. + + The reference device used for developing the example is the Toradex NXP i.MX 95 Verdin https://www.toradex.com/computer-on-modules/verdin-arm-family/nxp-imx95-evaluation-kit#explore + with NXP's SafeAssure framework. + +The following video shows this demo in action, with Linux booting underneath a Slint based rectangular overlay. + +The Linux based underlay starts the gallery demo, rendering with OpenGL on a Mali GPU with Skia and Slint's LinuxKMS backend. + +https://github.com/user-attachments/assets/077790db-b325-49d2-9d10-1e1be7c5a660 + +The overlay is rendered on the Cortex-M7 running FreeRTOS and NXP's SafeAssure framework, to handle driving the Display Processing Unit (DPU) for blending, and to run Slint's event loop. +The Slint scene rendered can be found in [appwindow.slint](./ui/app-window.slint). +The application entry point is [./src/lib.rs](./src/lib.rs); + +## Build System Integration + +Integration of this example into an existing safety domain build system works by means of CMake. In your existing `CMakeLists.txt` for your target +that produces the final binary, use `FetchContent` to pull in the `SlintSafeUi` target: + +```cmake +set(Rust_CARGO_TARGET "thumbv7em-none-eabihf" CACHE STRING "") +include(FetchContent) +FetchContent_Declare( + SlintSafeUi + GIT_REPOSITORY https://github.com/slint-ui/slint.git + GIT_TAG master + SOURCE_SUBDIR examples/safe-ui +) +FetchContent_MakeAvailable(SlintSafeUi) +``` + +Link against it in your firmware target, to ensure linkage and access to the C system interface headers: + +```cmake +target_link_libraries(my_firmware PRIVATE SlintSafeUi) +``` + +## C System Interface + +The basic C system interface is documented in [./src/slint-safeui-platform-interface.h](./src/slint-safeui-platform-interface.h). This header file is also part of the `INTERFACE` +of the `SlintSafeUi` CMake target. Implement these functions in your firmware. + +Once you've started your UI task, invoke `slint_app_main()` to start the Slint event loop and the UI safety layer. + +## Simulation + +For convenience, this example provides a "simulator" feature in [./src/simulator.rs](./src/simulator.rs), so that you can just run this on a desktop system with + +``` +cargo run +``` + +The "simulator" implements the same C system interface and runs the Slint UI safety layer example in a secondary thread. + +## Known Limitations + +- Partial rendering is not implemented. While this is technically possible, we aim to exclude the partial renderer from the safety certification process for now. +- The pixel format is hard-coded to BGRA8888. This is relatively easy to change, if necessary. +- `slint::invoke_from_event_loop()` (and `slint_safeui_platform_wake` in the interface) isn't fully implemented yet. This is partly due to missing abstractions + (mutexes) as well as missing support to distinguish between waking up from an interrupt handler vs. being invoked from another task (`vTaskNotifyGiveFromISR()` vs `xTaskNotifyGive()`) diff --git a/examples/safe-ui/build.rs b/examples/safe-ui/build.rs new file mode 100644 index 0000000000..3b9415ec85 --- /dev/null +++ b/examples/safe-ui/build.rs @@ -0,0 +1,25 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +use std::env; +use std::path::PathBuf; + +fn main() { + let bindings = bindgen::Builder::default() + .header("src/slint-safeui-platform-interface.h") + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .use_core() + .generate() + .expect("Unable to generate bindings"); + + // Write the bindings to the $OUT_DIR/bindings.rs file. + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings.write_to_file(out_path.join("bindings.rs")).expect("Couldn't write bindings!"); + + let config = slint_build::CompilerConfiguration::new() + .with_style("fluent-light".into()) + .with_sdf_fonts(true) + .embed_resources(slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer) + .with_scale_factor(2.); + slint_build::compile_with_config("ui/app-window.slint", config).unwrap(); +} diff --git a/examples/safe-ui/src/lib.rs b/examples/safe-ui/src/lib.rs new file mode 100644 index 0000000000..0b4611c974 --- /dev/null +++ b/examples/safe-ui/src/lib.rs @@ -0,0 +1,21 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +#![no_std] + +extern crate alloc; + +pub mod platform; + +slint::include_modules!(); + +#[unsafe(no_mangle)] +pub extern "C" fn slint_app_main() { + platform::slint_init_safeui_platform(); + + let app = MainWindow::new().unwrap(); + + app.show().unwrap(); + + app.run().unwrap(); +} diff --git a/examples/safe-ui/src/platform.rs b/examples/safe-ui/src/platform.rs new file mode 100644 index 0000000000..5e1fe69ca9 --- /dev/null +++ b/examples/safe-ui/src/platform.rs @@ -0,0 +1,293 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +extern crate alloc; +use alloc::boxed::Box; +use alloc::rc::Rc; +//use alloc::vec::Vec; +//use core::cell::RefCell; + +use slint::platform::software_renderer::MinimalSoftwareWindow; + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + +struct Platform { + window: Rc, + //event_queue: Queue, +} + +impl slint::platform::Platform for Platform { + fn create_window_adapter( + &self, + ) -> Result, slint::PlatformError> { + Ok(self.window.clone()) + } + + fn run_event_loop(&self) -> Result<(), slint::PlatformError> { + self.window + .dispatch_event(slint::platform::WindowEvent::ScaleFactorChanged { scale_factor: 2.0 }); + + let mut width: u32 = 0; + let mut height: u32 = 0; + unsafe { + slint_safeui_platform_get_screen_size(&mut width as *mut _, &mut height as *mut _); + } + + self.window.set_size(slint::WindowSize::Physical(slint::PhysicalSize::new(width, height))); + self.window.request_redraw(); + + loop { + slint::platform::update_timers_and_animations(); + + // let events_to_process = + // critical_section::with(|cs| self.event_queue.0.borrow(cs).take()); + // for event in events_to_process.into_iter() { + // match event { + // Event::Quit => return Ok(()), + // Event::Event(f) => f(), + // } + // } + + self.window.draw_if_needed(|renderer| { + render_wrapper(&|buffer, pixel_stride| { + renderer.render(buffer, pixel_stride); + }) + }); + + let mut next_timeout = slint::platform::duration_until_next_timer_update(); + + if self.window.has_active_animations() { + let frame_duration = core::time::Duration::from_millis(16); + next_timeout = Some(match next_timeout { + Some(x) => x.min(frame_duration), + None => frame_duration, + }) + } + + unsafe { + slint_safeui_platform_wait_for_events( + next_timeout.map_or(-1, |dur| dur.as_millis() as i32), + ) + }; + } + } + + //#[cfg(not(feature = "simulator"))] + //fn new_event_loop_proxy(&self) -> Option> { + // Some(Box::new(self.event_queue.clone()) as Box) + //} + + #[cfg(not(feature = "simulator"))] + fn duration_since_start(&self) -> core::time::Duration { + core::time::Duration::from_millis(unsafe { + slint_safeui_platform_duration_since_start() as u64 + }) + } +} + +fn render_wrapper(f: &F) { + let user_data = f as *const _ as *const core::ffi::c_void; + + unsafe extern "C" fn c_render_wrap( + user_data: *const core::ffi::c_void, + buffer: *mut core::ffi::c_char, + byte_size: core::ffi::c_uint, + pixel_stride: core::ffi::c_uint, + ) { + let buffer = unsafe { + core::slice::from_raw_parts_mut( + buffer as *mut Bgra8888Pixel, + byte_size as usize / core::mem::size_of::(), + ) + }; + let f = unsafe { &*(user_data as *const F) }; + f(buffer, pixel_stride as usize) + } + + unsafe { slint_safeui_platform_render(user_data, Some(c_render_wrap::)) } +} + +#[repr(transparent)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Bgra8888Pixel(pub u32); + +impl From for slint::platform::software_renderer::PremultipliedRgbaColor { + #[inline] + fn from(pixel: Bgra8888Pixel) -> Self { + let v = pixel.0; + slint::platform::software_renderer::PremultipliedRgbaColor { + blue: (v >> 0) as u8, + green: (v >> 8) as u8, + red: (v >> 16) as u8, + alpha: (v >> 24) as u8, + } + } +} + +impl From for Bgra8888Pixel { + #[inline] + fn from(pixel: slint::platform::software_renderer::PremultipliedRgbaColor) -> Self { + Self( + (pixel.alpha as u32) << 24 + | ((pixel.red as u32) << 16) + | ((pixel.green as u32) << 8) + | (pixel.blue as u32), + ) + } +} + +impl slint::platform::software_renderer::TargetPixel for Bgra8888Pixel { + fn blend(&mut self, color: slint::platform::software_renderer::PremultipliedRgbaColor) { + let mut x = slint::platform::software_renderer::PremultipliedRgbaColor::from(*self); + x.blend(color); + *self = x.into(); + } + fn from_rgb(r: u8, g: u8, b: u8) -> Self { + Self(0xff000000 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)) + } + fn background() -> Self { + Self(0) + } +} + +pub fn slint_init_safeui_platform() { + let platform = Platform { + window: slint::platform::software_renderer::MinimalSoftwareWindow::new( + slint::platform::software_renderer::RepaintBufferType::NewBuffer, + ), + //event_queue: Queue(critical_section::Mutex::new(RefCell::new(Vec::new())).into()), + }; + + slint::platform::set_platform(Box::new(platform)).unwrap(); +} + +//enum Event { +// Quit, +// Event(Box), +//} +// +//#[derive(Clone)] +//struct Queue(alloc::sync::Arc>>>); +// +//impl slint::platform::EventLoopProxy for Queue { +// fn quit_event_loop(&self) -> Result<(), slint::EventLoopError> { +// critical_section::with(|cs| { +// self.0.borrow_ref_mut(cs).push(Event::Quit); +// }); +// +// unsafe { slint_safeui_platform_wake() }; +// Ok(()) +// } +// +// fn invoke_from_event_loop( +// &self, +// event: Box, +// ) -> Result<(), slint::EventLoopError> { +// critical_section::with(|cs| { +// self.0.borrow_ref_mut(cs).push(Event::Event(event)); +// }); +// unsafe { slint_safeui_platform_wake() }; +// Ok(()) +// } +//} + +#[cfg_attr(not(feature = "simulator"), panic_handler)] +#[cfg(not(feature = "simulator"))] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::ffi::CStr; + use core::fmt::{self, Write}; + + pub struct FixedBuf<'a> { + buf: &'a mut [u8], + pos: usize, + } + + impl<'a> FixedBuf<'a> { + pub fn new(storage: &'a mut [u8]) -> Self { + Self { buf: storage, pos: 0 } + } + + pub fn as_cstr(&mut self) -> &CStr { + let cap = self.buf.len(); + let end = core::cmp::min(self.pos, cap.saturating_sub(1)); + self.buf[end] = 0; + unsafe { CStr::from_bytes_with_nul_unchecked(&self.buf[..=end]) } + } + } + + impl Write for FixedBuf<'_> { + fn write_str(&mut self, s: &str) -> fmt::Result { + let bytes = s.as_bytes(); + let cap = self.buf.len(); + + if self.pos >= cap { + return Ok(()); + } + + // Leave room for terminating null + let remaining = cap - self.pos - 1; + let to_copy = remaining.min(bytes.len()); + + let dst = &mut self.buf[self.pos..self.pos + to_copy]; + dst.copy_from_slice(&bytes[..to_copy]); + + self.pos += to_copy; + Ok(()) + } + } + + unsafe extern "C" { + pub fn slint_log_error(msg: *const core::ffi::c_char); + } + + let mut STORAGE: [u8; 256] = [0; 256]; + + unsafe { + let mut w = FixedBuf::new(&mut STORAGE); + write!(&mut w, "Rust PANIC: {:?}", info).ok(); + slint_log_error(w.as_cstr().as_ptr()); + }; + + loop {} +} + +#[cfg(not(feature = "simulator"))] +mod allocator { + use core::alloc::Layout; + use core::ffi::c_void; + unsafe extern "C" { + pub fn free(p: *mut c_void); + pub fn malloc(size: usize) -> *mut c_void; + } + + struct CAlloc; + unsafe impl core::alloc::GlobalAlloc for CAlloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let align = layout.align(); + if align <= core::mem::size_of::() { + unsafe { malloc(layout.size()) as *mut u8 } + } else { + // Ideally we'd use aligned_alloc, but that function caused heap corruption with esp-idf + let ptr = unsafe { malloc(layout.size() + align) as *mut u8 }; + let shift = align - (ptr as usize % align); + let ptr = ptr.add(shift); + core::ptr::write(ptr.sub(1), shift as u8); + ptr + } + } + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + unsafe { + let align = layout.align(); + if align <= core::mem::size_of::() { + free(ptr as *mut c_void); + } else { + let shift = core::ptr::read(ptr.sub(1)) as usize; + free(ptr.sub(shift) as *mut c_void); + } + } + } + } + + #[global_allocator] + static ALLOCATOR: CAlloc = CAlloc; +} diff --git a/examples/safe-ui/src/simulator.rs b/examples/safe-ui/src/simulator.rs new file mode 100644 index 0000000000..98b6ec1428 --- /dev/null +++ b/examples/safe-ui/src/simulator.rs @@ -0,0 +1,105 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +use std::sync::OnceLock; + +use slint_safeui_lib::platform::Bgra8888Pixel; + +const WIDTH_PIXELS: u32 = 640; +const HEIGHT_PIXELS: u32 = 480; +const PIXEL_STRIDE: u32 = WIDTH_PIXELS; + +static SIM_THREAD: OnceLock = OnceLock::new(); +static PIXEL_CHANNEL: OnceLock>> = OnceLock::new(); + +#[unsafe(no_mangle)] +extern "C" fn slint_safeui_platform_wait_for_events(max_wait_milliseconds: i32) { + if max_wait_milliseconds > 0 { + std::thread::park_timeout(std::time::Duration::from_millis(max_wait_milliseconds as u64)) + } else { + std::thread::park(); + } +} + +#[unsafe(no_mangle)] +extern "C" fn slint_safeui_platform_wake() { + if let Some(thread) = SIM_THREAD.get() { + thread.unpark(); + } +} + +#[unsafe(no_mangle)] +extern "C" fn slint_safeui_platform_render( + user_data: *mut (), + render_fn: extern "C" fn( + *mut (), + *mut core::ffi::c_char, + buffer_size_bytes: u32, + pixel_stride: u32, + ), +) { + let mut pixels = Vec::new(); + pixels.resize(PIXEL_STRIDE as usize * HEIGHT_PIXELS as usize, Bgra8888Pixel(0)); + let pixel_bytes: &mut [u8] = bytemuck::cast_slice_mut(&mut pixels); + render_fn( + user_data, + pixel_bytes.as_mut_ptr() as *mut core::ffi::c_char, + pixel_bytes.len() as u32, + PIXEL_STRIDE, + ); + + PIXEL_CHANNEL.get().unwrap().send_blocking(pixels).unwrap(); +} + +#[unsafe(no_mangle)] +extern "C" fn slint_safeui_platform_get_screen_size(width: *mut u32, height: *mut u32) { + unsafe { + *width = WIDTH_PIXELS; + *height = HEIGHT_PIXELS; + } +} + +slint::slint! {import { AboutSlint, VerticalBox } from "std-widgets.slint"; + +export component MainWindow inherits Window { + in property image <=> screen.source; + screen := Image { } +} +} + +fn main() { + let (pixel_sender, pixel_receiver) = smol::channel::unbounded(); + + PIXEL_CHANNEL.set(pixel_sender).unwrap(); + + let _thr = std::thread::spawn(|| { + SIM_THREAD.set(std::thread::current()).unwrap(); + slint_safeui_lib::slint_app_main() + }); + + let window = MainWindow::new().unwrap(); + + let window_weak = window.as_weak(); + + slint::spawn_local(async move { + loop { + if let Ok(source_pixels) = pixel_receiver.recv().await + && let Some(window) = window_weak.upgrade() + { + let mut pixel_buf: slint::SharedPixelBuffer = + slint::SharedPixelBuffer::new(WIDTH_PIXELS, HEIGHT_PIXELS); + let pixel_dest = pixel_buf.make_mut_slice(); + for i in 0..(WIDTH_PIXELS * HEIGHT_PIXELS) as usize { + let src = slint::platform::software_renderer::PremultipliedRgbaColor::from( + source_pixels[i], + ); + pixel_dest[i] = slint::Rgb8Pixel { r: src.red, g: src.green, b: src.blue }; + } + window.set_image(slint::Image::from_rgb8(pixel_buf)); + } + } + }) + .unwrap(); + + window.run().unwrap(); +} diff --git a/examples/safe-ui/src/slint-safeui-platform-interface.h b/examples/safe-ui/src/slint-safeui-platform-interface.h new file mode 100644 index 0000000000..d5f3f9902c --- /dev/null +++ b/examples/safe-ui/src/slint-safeui-platform-interface.h @@ -0,0 +1,79 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +#ifndef SLINT_SAFEUI_PLATFORM_INTERFACE +#define SLINT_SAFEUI_PLATFORM_INTERFACE + +/** + * Implement this function to suspend the current task. The function should return if one of the two + * conditions are met: + * + * 1. If `max_wait_milliseconds` is positive and `max_wait_milliseconds` have elapsed since the + * invocation, wake up and return. + * 2. If `slint_safeui_platform_wake()` was invoked. + * + * In practice, this function marks to FreeRTOS' TaskNotifyTake function(s), like this: + * + * ```cpp + * TickType_t ticks_to_wait = portMAX_DELAY; + * if (max_wait_milliseconds >= 0) { + * ticks_to_wait = pdMS_TO_TICKS(max_wait_milliseconds); + * } + * ulTaskNotifyTake(pdTRUE, ticks_to_wait); + * ``` + */ +void slint_safeui_platform_wait_for_events(int max_wait_milliseconds); + +/** + * Implement this function to wake up the suspend slint task. + * + * With FreeRTOS, this typically maps to `vTaskNotifyGiveFromISR()`. + */ +void slint_safeui_platform_wake(void); + +/** + * Implement this function to provide Slint with temporary access to the framebuffer, for rendering. + * + * The framebuffer is expected to be in BGRA8888 format (blue in the lower 8 bit, alpha in the upper + * most, etc.) + * + * The implementation is typically three-fold: + * + * 1. Obtain a pointer to the framebuffer to render into. + * 2. Invoke `render_fn()` with the provided `user_data()`, as well as a pointer to the frame + * buffer, the size of the buffer in bytes, as well as the number of pixels per line. Slint is + * expected to write to all bytes of the buffer. + * 3. Flush the framebuffer to the display. + */ +void slint_safeui_platform_render(const void *user_data, + void (*render_fn)(const void *user_data, char *frame_buffer, + unsigned int buffer_size_bytes, + unsigned int pixel_stride)); + +/** + * Implement this function to provide Slint with a "sense of time". This is used to driver + * animations as well as timers. + * + * A FreeRTOS-based implementation is typically a two-liner: + * + * ```cpp + * TickType_t ticks = xTaskGetTickCount(); + * return ticks * portTICK_PERIOD_MS; + * ``` + */ +int slint_safeui_platform_duration_since_start(void); + +/** + * Implement this function to provide Slint with the dimensions of the frame buffer in pixels. + * + * This function is called only once. Resizing of the frame buffer is not implemented right now. + */ +void slint_safeui_platform_get_screen_size(unsigned int *width, unsigned int *height); + +/** + * This function is provided by the `SlintSafeUi` CMake target. It's implemented in + * [./lib.rs](./lib.rs); Invoke this from your UI task to spin the Slint event loop. + */ +void slint_app_main(void); + +#endif diff --git a/examples/safe-ui/ui/app-window.slint b/examples/safe-ui/ui/app-window.slint new file mode 100644 index 0000000000..1ea15a99d1 --- /dev/null +++ b/examples/safe-ui/ui/app-window.slint @@ -0,0 +1,53 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +import { AboutSlint, VerticalBox } from "std-widgets.slint"; + +export component MainWindow inherits Window { + width: 320px; + height: 240px; + background: #ff000047; + VerticalBox { + AboutSlint { + preferred-height: 150px; + } + } + + property <[color]> colors: [Colors.green, Colors.orange, Colors.red]; + property idx: 0; + + Timer { + running: true; + interval: 1s; + triggered => { + idx = (idx + 1).mod(colors.length); + } + } + + first := Rectangle { + x: 0px; + y: 0px; + width: 40px; + height: self.width; + border-radius: self.width / 2; + background: colors[idx.mod(colors.length)]; + } + + second := Rectangle { + x: 50px; + y: 0px; + width: 40px; + height: self.width; + border-radius: self.width / 2; + background: colors[(idx + 1).mod(colors.length)]; + } + + third := Rectangle { + x: 100px; + y: 0px; + width: 40px; + height: self.width; + border-radius: self.width / 2; + background: colors[(idx + 2).mod(colors.length)]; + } +}