WIP: Add Safety Critical UI Demo

This commit is contained in:
Simon Hausmann 2025-12-09 17:21:21 +01:00
parent 8a95bc557a
commit 5df82f052d
11 changed files with 731 additions and 1 deletions

View file

@ -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"

1
examples/safe-ui/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

View file

@ -0,0 +1,32 @@
# Copyright © SixtyFPS GmbH <info@slint.dev>
# 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
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/src>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
)

View file

@ -0,0 +1,39 @@
# Copyright © SixtyFPS GmbH <info@slint.dev>
# 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"] }

View file

@ -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()`)

25
examples/safe-ui/build.rs Normal file
View file

@ -0,0 +1,25 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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();
}

View file

@ -0,0 +1,21 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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();
}

View file

@ -0,0 +1,293 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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<MinimalSoftwareWindow>,
//event_queue: Queue,
}
impl slint::platform::Platform for Platform {
fn create_window_adapter(
&self,
) -> Result<alloc::rc::Rc<dyn slint::platform::WindowAdapter>, 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<Box<dyn slint::platform::EventLoopProxy>> {
// Some(Box::new(self.event_queue.clone()) as Box<dyn slint::platform::EventLoopProxy>)
//}
#[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: Fn(&mut [Bgra8888Pixel], usize)>(f: &F) {
let user_data = f as *const _ as *const core::ffi::c_void;
unsafe extern "C" fn c_render_wrap<F: Fn(&mut [Bgra8888Pixel], usize)>(
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::<Bgra8888Pixel>(),
)
};
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::<F>)) }
}
#[repr(transparent)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Bgra8888Pixel(pub u32);
impl From<Bgra8888Pixel> 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<slint::platform::software_renderer::PremultipliedRgbaColor> 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<dyn FnOnce() + Send>),
//}
//
//#[derive(Clone)]
//struct Queue(alloc::sync::Arc<critical_section::Mutex<RefCell<Vec<Event>>>>);
//
//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<dyn FnOnce() + Send>,
// ) -> 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::<usize>() {
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::<usize>() {
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;
}

View file

@ -0,0 +1,105 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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<std::thread::Thread> = OnceLock::new();
static PIXEL_CHANNEL: OnceLock<smol::channel::Sender<Vec<Bgra8888Pixel>>> = 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> 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::Rgb8Pixel> =
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();
}

View file

@ -0,0 +1,79 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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

View file

@ -0,0 +1,53 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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 <int> 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)];
}
}