diff --git a/.github/workflows/build_docs.yaml b/.github/workflows/build_docs.yaml index 9a4a3cd3c65..c1f19495272 100644 --- a/.github/workflows/build_docs.yaml +++ b/.github/workflows/build_docs.yaml @@ -180,7 +180,7 @@ jobs: api/node/docs docs/site - name: "Check for docs warnings in internal crates" - run: cargo doc --workspace --no-deps --all-features --exclude slint-node --exclude pyslint --exclude mcu-board-support --exclude printerdemo_mcu --exclude carousel --exclude test-* --exclude plotter --exclude uefi-demo --exclude ffmpeg --exclude gstreamer-player --exclude slint-cpp --exclude slint-python + run: cargo doc --workspace --no-deps --all-features --exclude slint-node --exclude pyslint --exclude mcu-board-support --exclude mcu-embassy --exclude printerdemo_mcu --exclude carousel --exclude test-* --exclude plotter --exclude uefi-demo --exclude ffmpeg --exclude gstreamer-player --exclude slint-cpp --exclude slint-python - name: Clean cache # Don't cache docs to avoid them including removed classes being published run: | rm -rf target/doc target/cppdocs api/node/docs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e2b3294a2c4..78a0b5c1633 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -86,12 +86,12 @@ jobs: cargo update -p home --precise 0.5.9 fi - name: Run tests (not qt) - run: cargo test --verbose --all-features --workspace ${{ matrix.extra_args }} --exclude slint-node --exclude pyslint --exclude test-driver-node --exclude slint-node --exclude test-driver-nodejs --exclude test-driver-cpp --exclude mcu-board-support --exclude printerdemo_mcu --exclude uefi-demo --exclude slint-cpp --exclude slint-python -- --skip=_qt::t + run: cargo test --verbose --all-features --workspace ${{ matrix.extra_args }} --exclude slint-node --exclude pyslint --exclude test-driver-node --exclude slint-node --exclude test-driver-nodejs --exclude test-driver-cpp --exclude mcu-board-support --exclude mcu-embassy --exclude printerdemo_mcu --exclude uefi-demo --exclude slint-cpp --exclude slint-python -- --skip=_qt::t env: SLINT_CREATE_SCREENSHOTS: 1 shell: bash - name: Run tests (qt) - run: cargo test --verbose --all-features --workspace ${{ matrix.extra_args }} --exclude slint-node --exclude pyslint --exclude test-driver-node --exclude slint-node --exclude test-driver-nodejs --exclude test-driver-cpp --exclude mcu-board-support --exclude printerdemo_mcu --exclude uefi-demo --exclude slint-cpp --exclude slint-python --bin test-driver-rust -- _qt --test-threads=1 + run: cargo test --verbose --all-features --workspace ${{ matrix.extra_args }} --exclude slint-node --exclude pyslint --exclude test-driver-node --exclude slint-node --exclude test-driver-nodejs --exclude test-driver-cpp --exclude mcu-board-support --exclude mcu-embassy --exclude printerdemo_mcu --exclude uefi-demo --exclude slint-cpp --exclude slint-python --bin test-driver-rust -- _qt --test-threads=1 shell: bash - name: Archive screenshots after failed tests if: ${{ failure() }} @@ -375,6 +375,23 @@ jobs: - name: Check run: cargo check --target=${{matrix.target}} -p printerdemo_mcu --no-default-features --features=mcu-board-support/${{matrix.feature}} --release + # test to compile the mcu backend for the arm target (no_std) using embassy + mcu-embassy: + env: + SLINT_FONT_SIZES: 8,11,10,12,13,14,15,16,18,20,22,24,32 + RUSTFLAGS: --cfg slint_int_coord -D warnings + CARGO_PROFILE_DEV_DEBUG: 0 + CARGO_PROFILE_RELEASE_OPT_LEVEL: s + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-rust + with: + target: thumbv8m.main-none-eabihf + - name: Check + working-directory: examples/mcu-embassy + run: cargo check --bin ui_mcu --target=thumbv8m.main-none-eabihf --no-default-features --features="mcu-embassy/mcu" --release + mcu_esp: env: RUSTFLAGS: -D warnings diff --git a/examples/mcu-embassy/.cargo/config.toml b/examples/mcu-embassy/.cargo/config.toml new file mode 100644 index 00000000000..1784a38657a --- /dev/null +++ b/examples/mcu-embassy/.cargo/config.toml @@ -0,0 +1,20 @@ +# Copyright © 2025 David Haig +# SPDX-License-Identifier: MIT + +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +rustflags = [ + "-C", + "link-arg=--nmagic", + "-C", + "link-arg=-Tlink.x", + "-C", + "link-arg=-Tdefmt.x", +] +runner = "probe-rs run --chip STM32U5G9ZJTxQ" + +[build] +target = "thumbv8m.main-none-eabihf" + +[env] +DEFMT_LOG = "info" +RUST_LOG = "info" diff --git a/examples/mcu-embassy/.gitignore b/examples/mcu-embassy/.gitignore new file mode 100644 index 00000000000..cd781f59248 --- /dev/null +++ b/examples/mcu-embassy/.gitignore @@ -0,0 +1,11 @@ +Cargo.lock +/target + +# Ignore all package-lock.json files +**/package-lock.json +# But keep these specific ones +!editors/vscode/package-lock.json + +.env +.envrc +__pycache__ diff --git a/examples/mcu-embassy/.vscode/settings.json b/examples/mcu-embassy/.vscode/settings.json new file mode 100644 index 00000000000..17f909cac35 --- /dev/null +++ b/examples/mcu-embassy/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + // uncomment this to have rust-analyzer work with mcu related code + "rust-analyzer.cargo.features": [ + "mcu", + //"simulator", + ], + "rust-analyzer.check.allTargets": false, + "rust-analyzer.showUnlinkedFileNotification": false, +} \ No newline at end of file diff --git a/examples/mcu-embassy/Cargo.toml b/examples/mcu-embassy/Cargo.toml new file mode 100644 index 00000000000..393691dfb45 --- /dev/null +++ b/examples/mcu-embassy/Cargo.toml @@ -0,0 +1,119 @@ +# Copyright © 2025 David Haig +# SPDX-License-Identifier: MIT + +# A work around for an embassy-time-driver conflict with esp32-hal's embassy dependency. +# Remove this workspace and add 'examples/mcu-embassy' as a member to the slint root Cargo.toml once this issue has been resolved +[workspace] + +[package] +name = "mcu-embassy" +version = "0.1.0" +edition = "2021" +authors = ["David Haig "] +readme = "README.md" +license = "MIT" +resolver = "2" +publish = false +repository = "https://github.com/slint-ui/slint" +homepage = "https://slint.dev" + +[dependencies] +log = "0.4" +defmt = { version = "0.3", optional = true, features = ["alloc"] } +defmt-rtt = { version = "0.4", optional = true } +panic-probe = { version = "0.3", optional = true, features = ["print-defmt"] } +cortex-m = { version = "0.7.7", optional = true, features = [ + "inline-asm", + "critical-section-single-core", +] } +cortex-m-rt = { version = "0.7.3", optional = true } +slint-generated = { path = "./slint_generated" } +embedded-alloc = { version = "0.5", optional = true } +heapless = { version = "0.8", default-features = false, features = [ + "defmt-03", +] } +tinybmp = { version = "0.5" } +static_cell = { version = "2", optional = true } + +#slint = { version = "1.9.1", default-features = false, features = [ +# "compat-1-2", +# "unsafe-single-threaded", +# "libm", +#] } + +slint = { path = "../../api/rs/slint", default-features = false, features = [ + "compat-1-2", + "unsafe-single-threaded", + "libm", +] } + +embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", rev = "72976fe", optional = true, features = [ + "stm32u5g9zj", + "time-driver-tim2", + "exti", + "memory-x", + "unstable-pac", + "chrono", + "time", + "defmt", +] } +embassy-sync = { git = "https://github.com/embassy-rs/embassy", rev = "72976fe" } +embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "72976fe", features = [ + "task-arena-size-32768", + "executor-thread", + "integrated-timers", +] } +embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "72976fe" } +embassy-futures = { git = "https://github.com/embassy-rs/embassy", rev = "72976fe" } + +env_logger = { version = "0.9.0", optional = true } +sdl2 = { git = "https://github.com/Rust-SDL2/rust-sdl2.git", rev = "400e033", optional = true } +object-pool = { version = "0.6.0", optional = true } +embedded-hal = { version = "1.0.0", optional = true } +gt911 = { version = "0.1", features = ["defmt"], optional = true } + +[features] +default = ["mcu"] +mcu = [ + "defmt", + "defmt-rtt", + "panic-probe", + "cortex-m", + "cortex-m-rt", + "embassy-stm32", + "embassy-sync/defmt", + "embassy-executor/arch-cortex-m", + "embassy-executor/executor-interrupt", + "embassy-executor/defmt", + "embassy-time/tick-hz-32_768", + "embassy-time/defmt", + "embassy-time/defmt-timestamp-uptime", + "embedded-alloc", + "embedded-hal", + "gt911", +] + +simulator = [ + "slint/renderer-software", + "slint/backend-winit", + "slint/std", + "embassy-executor/arch-std", + "embassy-time/std", + "env_logger", + "sdl2", + "object-pool", + "static_cell", +] + +[profile.release] +debug = true # required for decent panic messages and log line locations +opt-level = "s" +lto = "thin" + +[[bin]] +name = "ui_mcu" +required-features = ["mcu"] + +[[bin]] +name = "ui_simulator" +required-features = ["simulator"] diff --git a/examples/mcu-embassy/README.md b/examples/mcu-embassy/README.md new file mode 100644 index 00000000000..09668e45831 --- /dev/null +++ b/examples/mcu-embassy/README.md @@ -0,0 +1,96 @@ + + +# Embassy Slint stm32u5g9j-dk2 Demo + +An embedded async Slint GUI demo using Embassy and an stm32u5g9j-dk2 development kit. This demo was written to run on a resource constrained device, not a PC or laptop. +The simulator can run on a PC if you do not have the dev kit on hand but it is not meant to be a reference design for an async GUI implementation on a PC. + +The stm32u5g9j-dk2 was chosen because of its availability and price point and has enough onboard ram (3MB) and flash (4MB) to run Slint without external psram and flash, reducing setup complexity. +It comes with a 5" 800x480 IPS touchscreen display. Async is useful for building more complex UIs because you don't have to hand code your own state machines. + +Things that are demonstrated here: +- Sending rendered display buffer to LCD screen asynchronously freeing up the mcu to do other things +- Responding to hardware events (pressing the USER button on the DK2 changes the colour of the grey circle to blue) +- Touchscreen actions setting physical hardware (toggling the switch on the touchscreen to turn on the green led on the DK2) +- Cooperative multitasking (red led continues to flash on a separate task regardless of UI actions) +- UI animations work +- The application can be simulated on a PC without having to download to the DK2 every time you want to test something + +# Installation instructions + +Install the cross compilation target for the mcu: + +```bash + rustup target add thumbv8m.main-none-eabihf +``` + +You need software to be able to flash the firmware to the dev kit. + +```bash +cargo install --force --locked probe-rs-tools +``` + +# Running the application + +Plug a usbc cable into the ST-LINK port on the dk2 and run the following: + +```bash +cargo run --bin ui_mcu --release --features=mcu +``` + +Troubleshooting: + +If you are getting some complication errors from cortex-m like "error: invalid register `r1`: unknown register" make sure that you are cross compiling for the correct cpu target: + +You can specify the target in the cargo run command in the following file: + +In `.cargo/Cargo.toml` +```toml +[build] +target = "thumbv8m.main-none-eabihf" +``` + +If using vscode then make sure `rust-analyzer.cargo.features` is set to `mcu` in `.vscode/settings.json` + +You may be wondering why you get the following message in the logs: `invalid location: defmt frame-index` +In the Slint workspace `Cargo.toml` file overrides the `Cargo.toml` file in this crate so make sure the release profile is as follows in that workspace file: +```toml +[profile.release] +debug = true # required for decent panic messages and log line locations +opt-level = "s" +lto = "thin" +``` + +# Running the simulator + +Of course you can use Slint's vscode plugin to preview slint files but you may want to actually run your application and simulate the hardware interactions. +The simulator runs Embassy on the host machine (instead of on an mcu) and renders to the screen using the sdl2 library. +Hardware like leds and buttons are emulated in the hardware module. + +To install SDL2 follow the instructions here: https://github.com/Rust-SDL2/rust-sdl2 + +To run the simulator on a pc: +```bash +# for linux +cargo run --bin ui_simulator --release --no-default-features --features=simulator --target x86_64-unknown-linux-gnu +# for windows +cargo run --bin ui_simulator --release --no-default-features --features=simulator --target x86_64-pc-windows-msvc +# for mac +cargo run --bin ui_simulator --release --no-default-features --features=simulator --target x86_64-apple-darwin +``` + +Note: Instead of specifying a target you can simply remove the arm target in .cargo/config.toml and cargo will use the host by default + +Troubleshooting: + +If you are getting some compilation errors from arrayvec like "error: requires `sized` lang_item" make sure you are NOT targeting the mcu when building for your pc. + +Set the target correctly in the command line or comment out the following: + +In `.cargo/Cargo.toml` +```toml +#[build] +#target = "thumbv8m.main-none-eabihf" +``` + +If using vscode then make sure `rust-analyzer.cargo.features` is set to `simulator` in `.vscode/settings.json` diff --git a/examples/mcu-embassy/slint_generated/Cargo.toml b/examples/mcu-embassy/slint_generated/Cargo.toml new file mode 100644 index 00000000000..87023b33dd5 --- /dev/null +++ b/examples/mcu-embassy/slint_generated/Cargo.toml @@ -0,0 +1,39 @@ +# Copyright © 2025 David Haig +# SPDX-License-Identifier: MIT + +[package] +name = "slint-generated" +version = "0.1.0" +edition = "2021" +build = "build.rs" +authors = ["David Haig "] +readme = "README.md" +resolver = "2" +license = "MIT" +publish = false +repository = "https://github.com/slint-ui/slint" +homepage = "https://slint.dev" + +[dependencies] +slint = { path = "../../../api/rs/slint", default-features = false, features = [ + "compat-1-2", + "unsafe-single-threaded", + "libm", + "renderer-software", +] } +i-slint-core-macros = { path = "../../../internal/core-macros" } + +[build-dependencies] +slint-build = { path = "../../../api/rs/build" } + +#[dependencies] +#slint = { version = "1.9.1", default-features = false, features = [ +# "compat-1-2", +# "unsafe-single-threaded", +# "libm", +# "renderer-software", +#] } +#i-slint-core-macros = { version = "1.9.1" } +# +#[build-dependencies] +#slint-build = { version = "1.9.1" } diff --git a/examples/mcu-embassy/slint_generated/README.md b/examples/mcu-embassy/slint_generated/README.md new file mode 100644 index 00000000000..d52d53d676d --- /dev/null +++ b/examples/mcu-embassy/slint_generated/README.md @@ -0,0 +1,5 @@ + + +# Generated + +This crate is here to separate the `.slint` file compilation from the main application. \ No newline at end of file diff --git a/examples/mcu-embassy/slint_generated/build.rs b/examples/mcu-embassy/slint_generated/build.rs new file mode 100644 index 00000000000..3d3994dd66b --- /dev/null +++ b/examples/mcu-embassy/slint_generated/build.rs @@ -0,0 +1,9 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +fn main() { + let config = slint_build::CompilerConfiguration::new() + .embed_resources(slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer); + slint_build::compile_with_config("../ui/main.slint", config).unwrap(); + slint_build::print_rustc_flags().unwrap(); +} diff --git a/examples/mcu-embassy/slint_generated/src/lib.rs b/examples/mcu-embassy/slint_generated/src/lib.rs new file mode 100644 index 00000000000..86b23c5a7ba --- /dev/null +++ b/examples/mcu-embassy/slint_generated/src/lib.rs @@ -0,0 +1,6 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +#![no_std] + +slint::include_modules!(); diff --git a/examples/mcu-embassy/src/bin/ui_mcu.rs b/examples/mcu-embassy/src/bin/ui_mcu.rs new file mode 100644 index 00000000000..063082b20f8 --- /dev/null +++ b/examples/mcu-embassy/src/bin/ui_mcu.rs @@ -0,0 +1,327 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +// A demo for stm32u5g9j-dk2 +// The application renders a simple Slint screen to the display and the user can interact with it +// by toggling the green led on and off as well as pushing the blue button on the dk which should +// turn the grey circle next to "Hardware User Button" blue. +// The hello world button demonstrates animations. More details in the readme. + +#![no_std] +#![no_main] +#![macro_use] +#![allow(static_mut_refs)] + +extern crate alloc; + +use alloc::{boxed::Box, rc::Rc}; +use defmt::{info, unwrap}; +use embassy_executor::Spawner; +use embassy_futures::select::{select, Either}; +use embassy_stm32::{ + bind_interrupts, + exti::ExtiInput, + gpio::{Level, Output, Pull, Speed}, + i2c::{self, I2c}, + ltdc::{ + self, Ltdc, LtdcConfiguration, LtdcLayer, LtdcLayerConfig, PolarityActive, PolarityEdge, + }, + mode::{self}, + peripherals, + time::Hertz, +}; +use embassy_time::{Duration, Timer}; +use gt911::Gt911; +use mcu_embassy::{ + controller::{self, Action, Controller}, + mcu::{double_buffer::DoubleBuffer, hardware::HardwareMcu, rcc_setup, ALLOCATOR}, + slint_backend::{StmBackend, TargetPixelType, DISPLAY_HEIGHT, DISPLAY_WIDTH}, +}; +use slint::{ + platform::{ + software_renderer::{MinimalSoftwareWindow, RepaintBufferType, Rgb565Pixel}, + PointerEventButton, WindowEvent, + }, + ComponentHandle, +}; +use slint_generated::MainWindow; +use {defmt_rtt as _, panic_probe as _}; + +const MY_TASK_POOL_SIZE: usize = 2; +const HEAP_SIZE: usize = 200 * 1024; +static mut HEAP: [u8; HEAP_SIZE] = [0; HEAP_SIZE]; + +// the following two display buffers consume 1536000 bytes that just about fits into the ram found on the mcu +static mut FB1: [TargetPixelType; DISPLAY_WIDTH * DISPLAY_HEIGHT] = + [Rgb565Pixel(0); DISPLAY_WIDTH * DISPLAY_HEIGHT]; +static mut FB2: [TargetPixelType; DISPLAY_WIDTH * DISPLAY_HEIGHT] = + [Rgb565Pixel(0); DISPLAY_WIDTH * DISPLAY_HEIGHT]; + +bind_interrupts!(struct Irqs { + LTDC => ltdc::InterruptHandler; + I2C2_EV => i2c::EventInterruptHandler; + I2C2_ER => i2c::ErrorInterruptHandler; +}); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let p = rcc_setup::stm32u5g9zj_init(); + + // setup an allocator + unsafe { ALLOCATOR.init(&mut HEAP as *const u8 as usize, core::mem::size_of_val(&HEAP)) } + + // enable instruction cache + embassy_stm32::pac::ICACHE.cr().write(|w| { + w.set_en(true); + }); + + // enable data cache 1 + embassy_stm32::pac::DCACHE1.cr().write(|w| { + w.set_en(true); + }); + + // enable data cache 2 + embassy_stm32::pac::DCACHE2.cr().write(|w| { + w.set_en(true); + }); + + // used for the touch events + let i2c = I2c::new( + p.I2C2, + p.PF1, + p.PF0, + Irqs, + p.GPDMA1_CH0, + p.GPDMA1_CH1, + Hertz(100_000), + Default::default(), + ); + + // TASK: blink the red led on another task + let red_led = Output::new(p.PD2, Level::High, Speed::Low); + unwrap!(spawner.spawn(led_task(red_led))); + + // TASK: wait for hardware user button press + let user_btn = ExtiInput::new(p.PC13, p.EXTI13, Pull::Down); + unwrap!(spawner.spawn(user_btn_task(user_btn))); + + // set up the LTDC peripheral to send data to the LCD screen + // numbers from STM32U5G9J-DK2.ioc + const RK050HR18H_HSYNC: u16 = 5; // Horizontal synchronization + const RK050HR18H_HBP: u16 = 8; // Horizontal back porch + const RK050HR18H_HFP: u16 = 8; // Horizontal front porch + const RK050HR18H_VSYNC: u16 = 5; // Vertical synchronization + const RK050HR18H_VBP: u16 = 8; // Vertical back porch + const RK050HR18H_VFP: u16 = 8; // Vertical front porch + + // NOTE: all polarities have to be reversed with respect to the STM32U5G9J-DK2 CubeMX parametrization + let ltdc_config = LtdcConfiguration { + active_width: DISPLAY_WIDTH as _, + active_height: DISPLAY_HEIGHT as _, + h_back_porch: RK050HR18H_HBP, + h_front_porch: RK050HR18H_HFP, + v_back_porch: RK050HR18H_VBP, + v_front_porch: RK050HR18H_VFP, + h_sync: RK050HR18H_HSYNC, + v_sync: RK050HR18H_VSYNC, + h_sync_polarity: PolarityActive::ActiveHigh, + v_sync_polarity: PolarityActive::ActiveHigh, + data_enable_polarity: PolarityActive::ActiveHigh, + pixel_clock_polarity: PolarityEdge::RisingEdge, + }; + + info!("init ltdc"); + let mut ltdc_de = Output::new(p.PD6, Level::Low, Speed::High); + let mut ltdc_disp_ctrl = Output::new(p.PE4, Level::Low, Speed::High); + let mut ltdc_bl_ctrl = Output::new(p.PE6, Level::Low, Speed::High); + let mut ltdc = Ltdc::new_with_pins( + p.LTDC, // PERIPHERAL + Irqs, // IRQS + p.PD3, // CLK + p.PE0, // HSYNC + p.PD13, // VSYNC + p.PB9, // B0 + p.PB2, // B1 + p.PD14, // B2 + p.PD15, // B3 + p.PD0, // B4 + p.PD1, // B5 + p.PE7, // B6 + p.PE8, // B7 + p.PC8, // G0 + p.PC9, // G1 + p.PE9, // G2 + p.PE10, // G3 + p.PE11, // G4 + p.PE12, // G5 + p.PE13, // G6 + p.PE14, // G7 + p.PC6, // R0 + p.PC7, // R1 + p.PE15, // R2 + p.PD8, // R3 + p.PD9, // R4 + p.PD10, // R5 + p.PD11, // R6 + p.PD12, // R7 + ); + ltdc.init(<dc_config); + ltdc_de.set_low(); + ltdc_bl_ctrl.set_high(); + ltdc_disp_ctrl.set_high(); + + // we only need to draw on one layer for this example (not to be confused with the double buffer) + info!("enable bottom layer"); + let layer_config = LtdcLayerConfig { + pixel_format: ltdc::PixelFormat::RGB565, // 2 bytes per pixel + layer: LtdcLayer::Layer1, + window_x0: 0, + window_x1: DISPLAY_WIDTH as _, + window_y0: 0, + window_y1: DISPLAY_HEIGHT as _, + }; + + // enable the bottom layer + ltdc.init_layer(&layer_config, None); + + // Safety: the DoubleBuffer controls access to the statically allocated frame buffers + // and it is the only thing that mutates their content + let double_buffer = + DoubleBuffer::new(unsafe { FB1.as_mut() }, unsafe { FB2.as_mut() }, layer_config); + + // create a slint window and register it with slint + let window = MinimalSoftwareWindow::new(RepaintBufferType::SwappedBuffers); + window.set_size(slint::PhysicalSize::new(DISPLAY_WIDTH as u32, DISPLAY_HEIGHT as u32)); + let backend = Box::new(StmBackend::new(window.clone())); + slint::platform::set_platform(backend).expect("backend already initialized"); + info!("slint gui setup complete"); + + // TASK: run the gui render loop + unwrap!(spawner.spawn(render_loop(window, double_buffer, ltdc, i2c))); + + let main_window = MainWindow::new().unwrap(); + main_window.show().expect("unable to show main window"); + + let green_led = Output::new(p.PD4, Level::High, Speed::Low); + let hardware = HardwareMcu { green_led }; + + // run the controller event loop + let mut controller = Controller::new(&main_window, hardware); + controller.run().await; +} + +#[embassy_executor::task(pool_size = MY_TASK_POOL_SIZE)] +async fn led_task(mut led: Output<'static>) { + loop { + // on + led.set_low(); + Timer::after(Duration::from_millis(50)).await; + + // off + led.set_high(); + Timer::after(Duration::from_millis(450)).await; + } +} + +// low latency button press with debounce and toggle state recovery (for data races) +#[embassy_executor::task(pool_size = MY_TASK_POOL_SIZE)] +async fn user_btn_task(mut user_btn: ExtiInput<'static>) { + let mut is_high = false; + info!("Press the USER button..."); + + loop { + let any_edge = user_btn.wait_for_any_edge(); + let timeout = Timer::after(Duration::from_millis(1000)); + + // the timeout is here in case of a data race between the last button check + // and beginning the wait for an edge change + match select(any_edge, timeout).await { + Either::First(_) => {} + Either::Second(_) => {} + }; + + if user_btn.is_high() != is_high { + is_high = !is_high; + info!("Button is pressed: {}", is_high); + controller::send_action(Action::HardwareUserBtnPressed(is_high)); + + // debounce + Timer::after(Duration::from_millis(50)).await; + } + + // check button state again as the button may have been + // released (and remained released) within the debounce period + if user_btn.is_high() != is_high { + is_high = !is_high; + info!("Button is pressed: {}", is_high); + controller::send_action(Action::HardwareUserBtnPressed(is_high)); + } + } +} + +#[embassy_executor::task()] +pub async fn render_loop( + window: Rc, + mut double_buffer: DoubleBuffer, + mut ltdc: Ltdc<'static, peripherals::LTDC>, + mut i2c: I2c<'static, mode::Async>, +) { + let mut last_touch: Option = None; + let touch = Gt911::default(); + touch.init(&mut i2c).await.unwrap(); + + loop { + slint::platform::update_timers_and_animations(); + + // process touchscreen events + process_touch(&touch, &mut i2c, &mut last_touch, window.clone()).await; + + // blocking render + let is_dirty = window.draw_if_needed(|renderer| { + let buffer = double_buffer.current(); + renderer.render(buffer, DISPLAY_WIDTH); + }); + + if is_dirty { + // async transfer of frame buffer to lcd + double_buffer.swap(&mut ltdc).await.unwrap(); + } else { + Timer::after(Duration::from_millis(10)).await; + } + } +} + +async fn process_touch( + touch: &Gt911>, + i2c: &mut I2c<'static, mode::Async>, + last_touch: &mut Option, + window: Rc, +) { + // process touchscreen touch events + if let Ok(point) = touch.get_touch(i2c).await { + let button = PointerEventButton::Left; + let event = match point { + Some(point) => { + let position = slint::PhysicalPosition::new(point.x as i32, point.y as i32) + .to_logical(window.scale_factor()); + Some(match last_touch.replace(position) { + Some(_) => WindowEvent::PointerMoved { position }, + None => WindowEvent::PointerPressed { position, button }, + }) + } + None => { + last_touch.take().map(|position| WindowEvent::PointerReleased { position, button }) + } + }; + + if let Some(event) = event { + let is_pointer_release_event = matches!(event, WindowEvent::PointerReleased { .. }); + window.dispatch_event(event); + + // removes hover state on widgets + if is_pointer_release_event { + window.dispatch_event(WindowEvent::PointerExited); + } + } + } +} diff --git a/examples/mcu-embassy/src/bin/ui_simulator.rs b/examples/mcu-embassy/src/bin/ui_simulator.rs new file mode 100644 index 00000000000..708fe6eb22a --- /dev/null +++ b/examples/mcu-embassy/src/bin/ui_simulator.rs @@ -0,0 +1,255 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +// A simulator for the stm32u5g9j-dk2 +// This uses the cross platform sdl2 library to render the application on a PC (see readme for installation instructions) +// The LEFTSHIFT key can be used in place of the blue push button on the dk2 (Hardware User Button) +// Then the Hardware Green Led TOGGLE button is pressed in the UI, the state of the led is logged to the console +// +// How it works: This demo still uses Embassy as the async runtime as well as Slint's software renderer. +// However, sdl2 is used to render the bitmap generated by Slint to a window at 60fps. Sdl2 is also used to +// emulate the touchscreen with the mouse and to capture keyboard events in place of the hardware button on the dk2. +// +// To run: `cargo run --bin ui_simulator --release --features=simulator` + +use std::{ + rc::Rc, + slice, + sync::mpsc::{self, Receiver, Sender}, + thread::{self}, + vec::Vec, +}; + +use embassy_executor::{Executor, Spawner}; +use embassy_time::{Duration, Timer}; +use log::*; +use mcu_embassy::{ + controller::{self, Action, Controller}, + simulator::hardware::HardwareSim, + slint_backend::{StmBackend, TargetPixelType, DISPLAY_HEIGHT, DISPLAY_WIDTH}, +}; +use object_pool::{Pool, Reusable}; +use sdl2::{ + event::Event, keyboard::Keycode, mouse::MouseButton, pixels::PixelFormatEnum, rect::Rect, +}; +use slint::{ + platform::{ + software_renderer::{MinimalSoftwareWindow, RepaintBufferType}, + PointerEventButton, WindowAdapter, WindowEvent, + }, + ComponentHandle, +}; +use slint_generated::MainWindow; +use static_cell::StaticCell; + +static EXECUTOR: StaticCell = StaticCell::new(); +static POOL: StaticCell>> = StaticCell::new(); + +fn main() { + env_logger::builder().filter_level(log::LevelFilter::Debug).format_timestamp_nanos().init(); + + thread::scope(|scope| { + let (tx_render, rx_render) = mpsc::channel(); + let (tx_event, rx_event) = mpsc::channel(); + + let pool = POOL.init(Pool::new(4, || { + vec![TargetPixelType::default(); DISPLAY_WIDTH * DISPLAY_HEIGHT] + })); + + scope.spawn(move || sdl2_render_loop(rx_render, tx_event).unwrap()); + let executor = EXECUTOR.init(Executor::new()); + executor.run(|spawner| { + spawner.spawn(main_task(spawner, tx_render, rx_event, pool)).unwrap(); + }); + }); +} + +fn sdl2_render_loop( + rx_render: Receiver>>, + tx_event: Sender, +) -> Result<(), String> { + let sdl_context = sdl2::init()?; + let video_subsystem = sdl_context.video()?; + + let window = video_subsystem + .window("Demo", DISPLAY_WIDTH as _, DISPLAY_HEIGHT as _) + .position_centered() + .opengl() + .build() + .map_err(|e| e.to_string())?; + + let mut canvas = window.into_canvas().build().map_err(|e| e.to_string())?; + let texture_creator = canvas.texture_creator(); + + let mut texture = texture_creator + .create_texture_streaming(PixelFormatEnum::RGB565, DISPLAY_WIDTH as _, DISPLAY_HEIGHT as _) + .map_err(|e| e.to_string())?; + + canvas.clear(); + canvas.copy(&texture, None, Some(Rect::new(0, 0, DISPLAY_WIDTH as _, DISPLAY_HEIGHT as _)))?; + canvas.present(); + + let mut event_pump = sdl_context.event_pump()?; + + loop { + for event in event_pump.poll_iter() { + match event { + Event::Quit { .. } | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => { + std::process::exit(0) + } + Event::KeyDown { keycode: Some(Keycode::LSHIFT), .. } => { + controller::send_action(Action::HardwareUserBtnPressed(true)) + } + Event::KeyUp { keycode: Some(Keycode::LSHIFT), .. } => { + controller::send_action(Action::HardwareUserBtnPressed(false)) + } + Event::MouseButtonDown { + timestamp: _timestamp, + window_id: _window_id, + which: _which, + mouse_btn, + clicks: _clicks, + x, + y, + } => { + if mouse_btn == MouseButton::Left { + let button = PointerEventButton::Left; + let position = slint::PhysicalPosition::new(x, y).to_logical(1.0); + let event = WindowEvent::PointerPressed { position, button }; + tx_event.send(event).unwrap(); + } + } + Event::MouseButtonUp { + timestamp: _timestamp, + window_id: _window_id, + which: _which, + mouse_btn, + clicks: _clicks, + x, + y, + } => { + if mouse_btn == MouseButton::Left { + let button = PointerEventButton::Left; + let position = slint::PhysicalPosition::new(x, y).to_logical(1.0); + let event = WindowEvent::PointerReleased { position, button }; + tx_event.send(event).unwrap(); + } + } + Event::MouseMotion { + timestamp: _timestamp, + window_id: _window_id, + which: _which, + mousestate, + x, + y, + xrel: _xrel, + yrel: _yrel, + } => { + if mousestate.is_mouse_button_pressed(MouseButton::Left) { + let position = slint::PhysicalPosition::new(x, y).to_logical(1.0); + let event = WindowEvent::PointerMoved { position }; + tx_event.send(event).unwrap(); + } + } + + _ => {} + } + } + + 'render_buffers: loop { + match rx_render.try_recv() { + Ok(buf) => { + texture.with_lock(None, |buffer: &mut [u8], _pitch: usize| { + let buf_ptr = buf.as_ptr() as *const u8; + let buf_slice = unsafe { slice::from_raw_parts(buf_ptr, buf.len() * 2) }; + buffer.copy_from_slice(buf_slice); + drop(buf); // returns buffer to pool + })?; + canvas.clear(); + canvas.copy_ex( + &texture, + None, + Some(Rect::new(0, 0, DISPLAY_WIDTH as _, DISPLAY_HEIGHT as _)), + 0.0, + None, + false, + false, + )?; + canvas.present(); + } + _ => { + // ignore + break 'render_buffers; + } + } + } + } +} + +#[embassy_executor::task] +async fn main_task( + spawner: Spawner, + tx_render: Sender>>, + rx_event: Receiver, + pool: &'static Pool>, +) { + let window = MinimalSoftwareWindow::new(RepaintBufferType::SwappedBuffers); + window.set_size(slint::PhysicalSize::new(DISPLAY_WIDTH as u32, DISPLAY_HEIGHT as u32)); + let backend = Box::new(StmBackend::new(window.clone())); + slint::platform::set_platform(backend).expect("backend already initialized"); + info!("slint gui setup complete"); + + spawner.spawn(embassy_render_loop(window, tx_render, rx_event, pool)).unwrap(); + + // give the render loop time to come up (otherwise it will draw a blank screen) + Timer::after(Duration::from_millis(200)).await; + let main_window = MainWindow::new().unwrap(); + main_window.show().expect("unable to show main window"); + + info!("press LEFT SHIFT to simulate a hardware button press"); + + let hardware = HardwareSim {}; + + // run the gui controller loop + let mut controller = Controller::new(&main_window, hardware); + controller.run().await; +} + +#[embassy_executor::task] +async fn embassy_render_loop( + window: Rc, + tx_render: Sender>>, + rx_event: Receiver, + pool: &'static Pool>, +) { + info!("embassy_render_loop"); + + loop { + slint::platform::update_timers_and_animations(); + + 'event: loop { + match rx_event.try_recv() { + Ok(e) => { + window.dispatch_event(e); + } + Err(_) => break 'event, + } + } + + // redraw the entire window (otherwise we get partial redraws which are more complicated to deal with) + window.request_redraw(); + + let _is_dirty = window.draw_if_needed(|renderer| match pool.try_pull() { + Some(mut buffer) => { + renderer.render(&mut buffer, DISPLAY_WIDTH as _); + tx_render.send(buffer).ok(); + } + None => { + // this happens when the MainWindow hasn't yet been created or if it has been closed by the user + } + }); + + // for approx 60fps + Timer::after(Duration::from_millis(16)).await; + } +} diff --git a/examples/mcu-embassy/src/controller.rs b/examples/mcu-embassy/src/controller.rs new file mode 100644 index 00000000000..e0a5b240d4b --- /dev/null +++ b/examples/mcu-embassy/src/controller.rs @@ -0,0 +1,100 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +use embassy_sync::channel::Channel; +use slint::ComponentHandle; +use slint_generated::{Globals, MainWindow}; + +use crate::{error, warn}; + +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[derive(Debug, Clone)] +pub enum Action { + HardwareUserBtnPressed(bool), + TouchscreenToggleBtn(bool), +} + +#[cfg(feature = "mcu")] +type ActionChannelType = Channel; + +#[cfg(feature = "simulator")] +type ActionChannelType = + Channel; + +pub static ACTION: ActionChannelType = Channel::new(); + +// see mcu::hardware or simulator::hardware modules for impl +// depending on features used +pub trait Hardware { + fn green_led_set_high(&mut self) {} + + fn green_led_set_low(&mut self) {} +} + +pub struct Controller<'a, Hardware> { + main_window: &'a MainWindow, + hardware: Hardware, +} + +impl<'a, H> Controller<'a, H> +where + H: Hardware, +{ + pub fn new(main_window: &'a MainWindow, hardware: H) -> Self { + Self { main_window, hardware } + } + + pub async fn run(&mut self) { + self.set_action_event_handlers(); + + loop { + let action = ACTION.receive().await; + + match self.process_action(action).await { + Ok(()) => { + // all good + } + Err(e) => { + error!("process action: {:?}", e); + } + } + } + } + + pub async fn process_action(&mut self, action: Action) -> Result<(), ()> { + let globals = self.main_window.global::(); + + match action { + Action::HardwareUserBtnPressed(is_pressed) => { + globals.set_hardware_user_btn_pressed(is_pressed); + } + Action::TouchscreenToggleBtn(on) => { + if on { + self.hardware.green_led_set_low(); + } else { + self.hardware.green_led_set_high() + } + } + } + Ok(()) + } + + // user initiated action event handlers + fn set_action_event_handlers(&self) { + let globals = self.main_window.global::(); + globals.on_toggle_btn(|on| send_action(Action::TouchscreenToggleBtn(on))); + } +} + +pub fn send_action(a: Action) { + // use non-blocking try_send here because this function needs is called from sync code (the gui callbacks) + match ACTION.try_send(a) { + Ok(_) => { + // see loop in `fn run()` for dequeue + } + Err(a) => { + // this could happen because the controller is slow to respond or we are making too many requests + warn!("user action queue full, could not add: {:?}", a) + } + } +} diff --git a/examples/mcu-embassy/src/lib.rs b/examples/mcu-embassy/src/lib.rs new file mode 100644 index 00000000000..e77432bda91 --- /dev/null +++ b/examples/mcu-embassy/src/lib.rs @@ -0,0 +1,21 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +#![cfg_attr(feature = "mcu", no_std)] + +extern crate alloc; + +pub mod controller; +pub mod slint_backend; + +#[cfg(feature = "mcu")] +pub mod mcu; + +#[cfg(feature = "mcu")] +pub use defmt::{debug, error, info, trace, warn}; + +#[cfg(feature = "simulator")] +pub mod simulator; + +#[cfg(feature = "simulator")] +pub use log::{debug, error, info, trace, warn}; diff --git a/examples/mcu-embassy/src/mcu/double_buffer.rs b/examples/mcu-embassy/src/mcu/double_buffer.rs new file mode 100644 index 00000000000..dd354f8959a --- /dev/null +++ b/examples/mcu-embassy/src/mcu/double_buffer.rs @@ -0,0 +1,57 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +use embassy_stm32::ltdc::{self, Ltdc, LtdcLayerConfig}; +use slint::platform::software_renderer::Rgb565Pixel; + +use crate::slint_backend::TargetPixelType; + +// A simple double buffer +pub struct DoubleBuffer { + buf0: &'static mut [TargetPixelType], + buf1: &'static mut [TargetPixelType], + is_buf0: bool, + layer_config: LtdcLayerConfig, +} + +impl DoubleBuffer { + pub fn new( + buf0: &'static mut [TargetPixelType], + buf1: &'static mut [TargetPixelType], + layer_config: LtdcLayerConfig, + ) -> Self { + Self { buf0, buf1, is_buf0: true, layer_config } + } + + pub fn current(&mut self) -> &mut [TargetPixelType] { + if self.is_buf0 { + self.buf0 + } else { + self.buf1 + } + } + + pub fn swap_temp(&mut self) { + self.is_buf0 = !self.is_buf0; + } + + pub async fn swap( + &mut self, + ltdc: &mut Ltdc<'_, T>, + ) -> Result<(), ltdc::Error> { + let buf = self.current(); + let frame_buffer = buf.as_ptr(); + self.is_buf0 = !self.is_buf0; + ltdc.set_buffer(self.layer_config.layer, frame_buffer as *const _).await + } + + // Clears the buffer + pub fn clear(&mut self) { + let buf = self.current(); + let solid_black = Rgb565Pixel::default(); + + for a in buf.iter_mut() { + *a = solid_black; + } + } +} diff --git a/examples/mcu-embassy/src/mcu/hardware.rs b/examples/mcu-embassy/src/mcu/hardware.rs new file mode 100644 index 00000000000..a232e4cc2a9 --- /dev/null +++ b/examples/mcu-embassy/src/mcu/hardware.rs @@ -0,0 +1,18 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +use crate::controller::Hardware; + +pub struct HardwareMcu { + pub green_led: embassy_stm32::gpio::Output<'static>, +} + +impl Hardware for HardwareMcu { + fn green_led_set_high(&mut self) { + self.green_led.set_high(); + } + + fn green_led_set_low(&mut self) { + self.green_led.set_low(); + } +} diff --git a/examples/mcu-embassy/src/mcu/mod.rs b/examples/mcu-embassy/src/mcu/mod.rs new file mode 100644 index 00000000000..ef8c204616f --- /dev/null +++ b/examples/mcu-embassy/src/mcu/mod.rs @@ -0,0 +1,11 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +pub mod double_buffer; +pub mod hardware; +pub mod rcc_setup; + +use embedded_alloc::Heap; + +#[global_allocator] +pub static ALLOCATOR: Heap = Heap::empty(); diff --git a/examples/mcu-embassy/src/mcu/rcc_setup.rs b/examples/mcu-embassy/src/mcu/rcc_setup.rs new file mode 100644 index 00000000000..a4ae61f6d20 --- /dev/null +++ b/examples/mcu-embassy/src/mcu/rcc_setup.rs @@ -0,0 +1,32 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +use embassy_stm32::time::Hertz; +use embassy_stm32::{rcc, Config, Peripherals}; + +/// Sets up clocks for the stm32u5g9zj mcu +/// change this if you plan to use a different microcontroller +pub fn stm32u5g9zj_init() -> Peripherals { + // setup power and clocks for an STM32U5G9J-DK2 run from an external 16 Mhz external oscillator + let mut config = Config::default(); + config.rcc.hse = Some(rcc::Hse { freq: Hertz(16_000_000), mode: rcc::HseMode::Oscillator }); + config.rcc.pll1 = Some(rcc::Pll { + source: rcc::PllSource::HSE, + prediv: rcc::PllPreDiv::DIV1, + mul: rcc::PllMul::MUL10, + divp: None, + divq: None, + divr: Some(rcc::PllDiv::DIV1), + }); + config.rcc.sys = rcc::Sysclk::PLL1_R; // 160 Mhz + config.rcc.pll3 = Some(rcc::Pll { + source: rcc::PllSource::HSE, + prediv: rcc::PllPreDiv::DIV4, // PLL_M + mul: rcc::PllMul::MUL125, // PLL_N + divp: None, + divq: None, + divr: Some(rcc::PllDiv::DIV20), + }); + config.rcc.mux.ltdcsel = rcc::mux::Ltdcsel::PLL3_R; // 25 MHz + embassy_stm32::init(config) +} diff --git a/examples/mcu-embassy/src/simulator/hardware.rs b/examples/mcu-embassy/src/simulator/hardware.rs new file mode 100644 index 00000000000..3ac17665189 --- /dev/null +++ b/examples/mcu-embassy/src/simulator/hardware.rs @@ -0,0 +1,17 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +use crate::controller::Hardware; +use crate::info; + +pub struct HardwareSim {} + +impl Hardware for HardwareSim { + fn green_led_set_high(&mut self) { + info!("green led OFF"); + } + + fn green_led_set_low(&mut self) { + info!("green led ON"); + } +} diff --git a/examples/mcu-embassy/src/simulator/mod.rs b/examples/mcu-embassy/src/simulator/mod.rs new file mode 100644 index 00000000000..66c47e35c68 --- /dev/null +++ b/examples/mcu-embassy/src/simulator/mod.rs @@ -0,0 +1,4 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +pub mod hardware; diff --git a/examples/mcu-embassy/src/slint_backend.rs b/examples/mcu-embassy/src/slint_backend.rs new file mode 100644 index 00000000000..329e4bbafa8 --- /dev/null +++ b/examples/mcu-embassy/src/slint_backend.rs @@ -0,0 +1,38 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +use alloc::rc::Rc; +use embassy_time::Instant; +use slint::{ + platform::{ + software_renderer::{self, MinimalSoftwareWindow}, + Platform, WindowAdapter, + }, + PlatformError, +}; + +pub const DISPLAY_WIDTH: usize = 800; +pub const DISPLAY_HEIGHT: usize = 480; +pub type TargetPixelType = software_renderer::Rgb565Pixel; + +pub struct StmBackend { + window: Rc, +} + +impl StmBackend { + pub fn new(window: Rc) -> Self { + Self { window } + } +} + +impl Platform for StmBackend { + fn create_window_adapter(&self) -> Result, PlatformError> { + let window = self.window.clone(); + crate::info!("create_window_adapter called"); + Ok(window) + } + + fn duration_since_start(&self) -> core::time::Duration { + Instant::now().duration_since(Instant::from_secs(0)).into() + } +} diff --git a/examples/mcu-embassy/ui/common.slint b/examples/mcu-embassy/ui/common.slint new file mode 100644 index 00000000000..92a5c1b02b6 --- /dev/null +++ b/examples/mcu-embassy/ui/common.slint @@ -0,0 +1,111 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +export global Globals { + in property hardware-user-btn-pressed; + callback toggle-btn(bool); +} + +global Palette { + out property neutralSecondaryAlt: #8a8886; + out property neutralLight: #edebe9; + out property white: #ffffff; + out property black: #000000; + out property neutralDark: #201f1e; +} + +export global Theme { + out property page-background-color: Palette.white; + out property text-foreground-color: Palette.black; + out property font-size-standard: 24px; + out property page-width: 800px; + out property page-height: 480px; +} + +export component Button { + callback clicked; + in property text <=> text.text; + out property pressed: touch.pressed; + in property checkable; + in-out property checked; + in property font-size <=> text.font-size; + in property background: Palette.white; + Rectangle { + border-width: 1px; + border-radius: 2px; + border-color: Palette.neutralSecondaryAlt; + background: (touch.pressed || root.checked) ? Palette.neutralLight : root.background; + } + + horizontal-stretch: 0; + vertical-stretch: 0; + min-height: max(32px, l.min-height); + l := HorizontalLayout { + padding-left: 10px; + padding-right: 10px; + padding-top: 3px; + padding-bottom: 3px; + text := Text { + color: Palette.neutralDark; + horizontal-alignment: center; + vertical-alignment: center; + font-size: Theme.font-size-standard; + } + } + + touch := TouchArea { + clicked => { + if (root.checkable) { + root.checked = !root.checked; + } + root.clicked(); + } + } + + @children +} + +export component Toggle inherits Rectangle { + callback clicked(); + in-out property on; + width: 100px; + height: 40px; + + Rectangle { + width: 100px; + height: 40px; + background: on ? blue : gray; + animate background { + duration: 100ms; + easing: ease; + } + border-radius: 20px; + + Text { + text: on ? "On" : "Off"; + x: on ? 8px : parent.width - 50px; + color: white; + font-size: Theme.font-size-standard; + } + + Rectangle { + width: parent.height - 4px; + height: parent.height - 4px; + x: on ? parent.width - (parent.height - 2px) : 2px; + animate x { + duration: 100ms; + easing: ease; + } + y: 2px; + background: white; + border-radius: (parent.height - 4px) / 2; + } + } + + TouchArea { + clicked => { + on = !on; + root.clicked(); + } + } +} diff --git a/examples/mcu-embassy/ui/main.slint b/examples/mcu-embassy/ui/main.slint new file mode 100644 index 00000000000..494ee1d8075 --- /dev/null +++ b/examples/mcu-embassy/ui/main.slint @@ -0,0 +1,65 @@ +// Copyright © 2025 David Haig +// SPDX-License-Identifier: MIT + +import { Globals, Button, Theme, Toggle } from "common.slint"; + +export { Globals } + +export component MainWindow inherits Window { + width: 800px; + height: 480px; + + HorizontalLayout { + alignment: center; + VerticalLayout { + alignment: center; + spacing: 50px; + + Button { + text: "Hello, World"; + font-size: Theme.font-size-standard; + height: 50px; + animate height { + duration: 100ms; + easing: ease-in; + } + states [ + left-aligned when self.pressed: { + height: 80px; + } + ] + } + + HorizontalLayout { + Text { + width: 300px; + text: "Hardware Green Led"; + font-size: Theme.font-size-standard; + } + + Toggle { + width: 100px; + clicked => { + Globals.toggle-btn(self.on); + } + } + } + + HorizontalLayout { + Text { + width: 300px; + vertical-alignment: center; + text: "Hardware User Button"; + font-size: Theme.font-size-standard; + } + + Rectangle { + width: 100px; + height: 100px; + background: Globals.hardware-user-btn-pressed ? blue : lightgray; + border-radius: 100px; + } + } + } + } +}