Slint-UI移植到任意平台-概述-Rust

前言

Slint官方架构图
image

本文仅为笔者记忆,个人经验写着玩,目前1.3.2版本。

注:本文尚未完成。
注:本文尚未完成。
注:本文尚未完成。

本人目前想要移植一种贴近前端技术的GUI到裸机上,但是裸机不支持UNIX环境,所以绝大部分框架都用不了(如Flutter/skia/GLFW/Cairo等),最后发现Slint最合适。

Slint有三种渲染器{femtovg/OpenGL ,skia ,software },由于本文移植目标是裸机,所以采用CPU/MCU完成这个过程,选用软件渲染software

Slint官方文档

  1. Slint模板语法,类似于 HTML
    https://slint.dev/releases/1.3.2/docs/slint/
  2. MCU移植指南(重要):https://slint.dev/releases/1.3.2/docs/rust/slint/docs/mcu/
  3. MCU示例工程(SPI/IO等),主要侧重于使用Slint:
    https://github.com/slint-ui/slint-mcu-rust-template
  4. MCU移植示例,侧重于移植Slint
    https://github.com/slint-ui/slint/tree/master/examples/printerdemo_mcu

本文研究的目标

我们就研究
https://github.com/slint-ui/slint/tree/master/examples/printerdemo_mcu
及其依赖的 mcu-board-support = { path = "../mcu-board-support" }
image

首先我们去它的目录 ../mcu-board-support看看,选择我们熟悉的芯片平台来看即可,我选择stm32
image

这里传递了 StmBackend,由于Rust没有c那种先声明才能使用的规定,所以继续看下面。看看StmBackend的声明和定义。

image
image
PS:源码里的RNG我们先不管,RNG硬件随机数发生器,是STM32的片上外设硬件。

可以看到,StmBackend实现了两个行为(Rust里面叫Trait:定义共同行为; Cpp和Java叫做继承,Golang叫接口实现)

我们只需要实现这些行为,即可完成移植slint的大部分工作(即完成HAL层),剩下的底层就是纯裸机Bringup开发了。

impl slint::platform::Platform for StmBackend 实现如下:

impl slint::platform::Platform for StmBackend {
    fn create_window_adapter(
        &self,
    ) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
        let window = slint::platform::software_renderer::MinimalSoftwareWindow::new(
            slint::platform::software_renderer::RepaintBufferType::SwappedBuffers,
        );
        self.window.replace(Some(window.clone()));
        Ok(window)
    }

    fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
        let inner = &mut *self.inner.borrow_mut();

        let mut ft5336 =
            ft5336::Ft5336::new(&mut inner.touch_i2c, 0x70 >> 1, &mut inner.delay).unwrap();
        ft5336.init(&mut inner.touch_i2c);

        // Safety: The Refcell at the beginning of `run_event_loop` prevents re-entrancy and thus multiple mutable references to FB1/FB2.
        let (fb1, fb2) = unsafe { (&mut FB1, &mut FB2) };

        let mut displayed_fb: &mut [TargetPixel] = fb1;
        let mut work_fb: &mut [TargetPixel] = fb2;

        let mut last_touch = None;
        self.window
            .borrow()
            .as_ref()
            .unwrap()
            .set_size(slint::PhysicalSize::new(DISPLAY_WIDTH as u32, DISPLAY_HEIGHT as u32));
        loop {
            slint::platform::update_timers_and_animations();

            if let Some(window) = self.window.borrow().clone() {
                window.draw_if_needed(|renderer| {
                    while inner.layer.is_swap_pending() {}
                    renderer.render(work_fb, DISPLAY_WIDTH);
                    inner.scb.clean_dcache_by_slice(work_fb);
                    // Safety: the frame buffer has the right size
                    unsafe { inner.layer.swap_framebuffer(work_fb.as_ptr() as *const u8) };
                    // Swap the buffer pointer so we will work now on the second buffer
                    core::mem::swap::<&mut [_]>(&mut work_fb, &mut displayed_fb);
                });

                // handle touch event
                let touch = ft5336.detect_touch(&mut inner.touch_i2c).unwrap();
                let button = slint::platform::PointerEventButton::Left;
                let event = if touch > 0 {
                    let state = ft5336.get_touch(&mut inner.touch_i2c, 1).unwrap();
                    let position = slint::PhysicalPosition::new(state.y as i32, state.x as i32)
                        .to_logical(window.scale_factor());
                    Some(match last_touch.replace(position) {
                        Some(_) => slint::platform::WindowEvent::PointerMoved { position },
                        None => slint::platform::WindowEvent::PointerPressed { position, button },
                    })
                } else {
                    last_touch.take().map(|position| {
                        slint::platform::WindowEvent::PointerReleased { position, button }
                    })
                };

                if let Some(event) = event {
                    let is_pointer_release_event =
                        matches!(event, slint::platform::WindowEvent::PointerReleased { .. });

                    window.dispatch_event(event);

                    // removes hover state on widgets
                    if is_pointer_release_event {
                        window.dispatch_event(slint::platform::WindowEvent::PointerExited);
                    }
                }
            }

            // FIXME: cortex_m::asm::wfe();
        }
    }

    fn duration_since_start(&self) -> core::time::Duration {
        // FIXME! the timer can overflow
        let val = self.timer.counter() / 10;
        core::time::Duration::from_millis(val.into())
    }

    fn debug_log(&self, arguments: core::fmt::Arguments) {
        use alloc::string::ToString;
        defmt::println!("{=str}", arguments.to_string());
    }
}


HAL层的过程是这样的:

  1. create_window_adapter就是配置一下使用software render
  2. run_event_loop完成每一次事件处理(包括屏幕刷新/触摸输入等事件)
  • Slint上层software renderer渲染器完成渲染,通过FB1和FB2双缓冲机制将图像传递给 可触摸屏幕主控FT5336(I2C通讯),完成显示。也接收触摸屏事件,完成输入响应。注:rust里的loop{..}就是其他语言的while(1){...}
    image
  1. debug_log完成日志输出接口,其实就是print
  2. duration_since_start是运行时间,通过定时器实现即可。

Slint的官方宣言!

译文:
image

本文未完成。

看完这个STM32的,再去看其他芯片(例如ESP32)的Slint移植代码,会发现其实都差不多。
本来想着给Slint-UI适配UEFI的,结果发现被人抢先一步呜呜呜,Platform的trait被适配了,所以我就给它加了个鼠标支持:

https://github.com/slint-ui/slint/tree/e321ef70a9e8f21a50a20502f3cc15c3108bcd09/examples/uefi-demo
鼠标图片是examples/uefi-demo/resource/cursor.png,你可以随意替换成带透明通道的png图标,然后重新编译,这个我是做了支持的。漂亮的鼠标图标文件可以从这里自己在线制作:https://icons8.com/icon/11945/cursor

posted @ 2024-01-28 18:43  蓝天上的云℡  阅读(143)  评论(0编辑  收藏  举报