Rust Slint實現炫酷鎖屏源碼分享

  • 一、源碼分享
  • 1、效果展示
  • 2、源碼分享
  • 2.1、工程搭建
  • 2.2、工程結構
  • 2.3、main.rs
  • 2.4、main.slint
  • 2.5、models.slint
  • 2.6、Cargo.toml
  • 2.7、源碼資源下載
  • 二、Slint詳解
  • 1、核心特性
  • 2、架構模式 (MVU)
  • 3、優勢
  • 4、適用場景
  • 5、總結

一、源碼分享

1、效果展示

【Rust日報】2022-02-22 Slint - 為桌面和嵌入式設備創建一個新的GUI框架-_sed


【Rust日報】2022-02-22 Slint - 為桌面和嵌入式設備創建一個新的GUI框架-_#開發語言_02


【Rust日報】2022-02-22 Slint - 為桌面和嵌入式設備創建一個新的GUI框架-_ci_03


【Rust日報】2022-02-22 Slint - 為桌面和嵌入式設備創建一個新的GUI框架-_#rust_04

2、源碼分享

2.1、工程搭建

參考我這篇博文:【Rust 使用Slint庫開發UI界面工程搭建詳細教程】

2.2、工程結構

【Rust日報】2022-02-22 Slint - 為桌面和嵌入式設備創建一個新的GUI框架-_#rust_05

2.3、main.rs

use slint::{PlatformError};
slint::include_modules!();


fn main() ->Result<(), PlatformError>{

    let app: MainWindow  = MainWindow::new()?;
    let weak: slint::Weak<MainWindow> = app.as_weak();


    app.global::<DataAdapter>().on_btn_clicked({
        let weak: slint::Weak<MainWindow> = weak.clone();
        move ||{
            if let Some(strong) = weak.upgrade(){
                let _adapter: DataAdapter<'_> = strong.global::<DataAdapter>();
            }
        }
    });


    let _ = app.run();


    Ok(())
}

2.4、main.slint

import { AboutSlint, VerticalBox, LineEdit, HorizontalBox, Button, GroupBox, GridBox, 
    ComboBox, Spinner, Slider, ListView, Palette, ProgressIndicator, CheckBox, Switch } from "std-widgets.slint";
import { DataAdapter} from "models.slint";
export { DataAdapter}



export global DialState {
    out property <int> totalLights: 60;
    out property <angle> degreesFilledWithLights: 360deg - (startAngle - endAngle);
    out property <angle> startAngle: 104deg;
    out property <angle> endAngle: -startAngle;
    in-out property <length> elementRadius: 120px;
}

component Dial {
    pure public function normalizeAngle(angle: angle) -> angle {
        return (angle + 360deg).mod(360deg);
    }

    in property <bool> interactive: true;
    property <bool> moving: ta.firstTouch;
    in-out property <angle> dialAngle: DialState.startAngle;
    out property <int> volume: ((dialAngle - DialState.startAngle) / DialState.degreesFilledWithLights) * DialState.totalLights;

    width: 212px;
    height: 213px;
    knob := Rectangle {
        base := Rectangle {
            Image {
                x: 0px;
                y: 9px;
                source: @image-url("images/dial-frame.png");
            }

            Image {
                source: @image-url("images/lines.png");
                colorize: #000;
                transform-rotation: root.dialAngle;
                width: self.source.width * 0.55 * 1px;
                height: self.source.height * 0.55 * 1px;
                opacity: 0.03;
            }

            ta := TouchArea {
                property <length> centerX: self.width / 2;
                property <length> centerY: self.height / 2;
                property <length> relativeX;
                property <length> relativeY;
                property <angle> newAngle;
                property <angle> deltaDegrees;
                property <bool> firstTouch: false;
                width: parent.width;
                height: parent.height;
                enabled: root.interactive;

                changed pressed => {
                    if !self.pressed {
                        firstTouch = false;
                    }
                }

                moved => {
                    relativeX = ta.mouse-x - centerX;
                    relativeY = ta.mouse-y - centerY;
                    newAngle = normalizeAngle(atan2(relativeY / 1px, relativeX / 1px));
                    if !firstTouch {
                        firstTouch = true;
                        deltaDegrees = normalizeAngle(root.dialAngle - newAngle);
                    } else {
                        root.dialAngle = normalizeAngle(deltaDegrees + newAngle).clamp(DialState.startAngle, 260deg);
                    }
                }
            }
        }
    }

    Rectangle {
        width: 1px;
        height: 1px;
        x: 106px;
        y: 105px;
        Rectangle {
            width: 0px;
            height: 0px;
            x: 55px * root.dialAngle.cos();
            y: 55px * root.dialAngle.sin();
            Image {
                source: @image-url("images/notch.png");
            }
        }
    }
}

component Light {

    function pulseAnimation(duration: duration) -> float {
        return 1 * (1 - abs(sin(360deg * animation-tick() / duration)));
    }

    in property <int> index;
    in property <int> volume;
    property <angle> gap: (360deg - (DialState.startAngle - DialState.endAngle)) / DialState.totalLights;
    property <angle> angle: (index * gap) + DialState.startAngle;
    property <bool> lightOn: index <= volume;
    property <float> pulse:   index == 0 && lightOn && volume <= 1 ? pulseAnimation(5s) : 1.0;

    x: DialState.elementRadius * angle.cos();
    y: DialState.elementRadius * angle.sin();
    width: 0;
    height: 0;

    states [
        lightOff when !root.lightOn: {
            dialLed.opacity: 0;
        }
        lightOn when root.lightOn: {
            dialLed.opacity: pulse;
            in {
                animate dialLed.opacity {
                    duration: 100ms;
                    easing: ease-in-sine;
                }
            }
            out {
                animate dialLed.opacity {
                    duration: 600ms;
                    easing: ease-out-sine;
                }
            }
        }
    ]
    Rectangle {

        Rectangle {
            width: 5px;
            height: self.width;
            border-radius: self.width / 2;
            background: #00331a;
            opacity: 0.1;
        }

        dialLed := Image {
            source:  @image-url("images/led-dark.png");
            width: self.source.width * 0.5 * 1px;
            height: self.source.height * 0.5 * 1px;
        }
    }
}

component DialLights {
    width: 212px;
    height: 213px;
    in property <int> volume;

    Rectangle {
        width: 1px;
        height: 1px;
        x: 106px;
        y: 105px;
        lightHolder := Rectangle {
            x: 0px;
            y: 1px;
            for i in DialState.totalLights + 1: Light {
                index: i;
                volume: root.volume;
            }
        }
    }
}
enum DoorState { closed, open }
component Doors {
    property <brush> notch-border-color: #0000005d;
    in-out property <bool> demo-locked: true;
    in-out property <DoorState> initial-door-state: closed;
    property <DoorState> target-door-state: initial-door-state;

    callback unlockDemo();
    callback doorsOpened();
    callback doorsOpening();
    callback doorsClosed();
    width: 100%;
    height: 100%;

    unlockDemo => {
        demo-locked = false;
        target-door-state = DoorState.open;
        doorsOpening();
    }

    Timer {
        interval: 1ms;
        triggered => {
            if initial-door-state == DoorState.open && demo-locked {
                target-door-state = DoorState.closed;
                initial-door-state = DoorState.closed;
            }
            self.running = false;
        }
    }

    touch-catcher := Rectangle {
        TouchArea { }
    }

    leftDoor := Rectangle {
        x: -30px;
        width: parent.width / 2 + 70px;
        height: 100%;
        background: @linear-gradient(180deg, #34373F, #1D2026);
        border-width: 5px;
        border-color: notch-border-color;
        border-radius: 30px;
        clip: true;
        changed x => {
            if root.initial-door-state == DoorState.closed && self.x <= -leftDoor.width {
                root.doorsOpened();
            }
            if self.x == -60px {
                root.doorsClosed();
            }
        }

        Image {
            x: 0;
            y: 0;
            width: self.source.width * 2 * 1px;
            height: self.source.height * 2 * 1px;
            source: @image-url("images/logo-fragment.png");
            opacity: 0.02;
        }

        DialLights {
            x: parent.width - 125px;
            volume: dial.volume;
        }
    }

    states [
        doorsOpen when target-door-state == DoorState.open: {
            leftDoor.x: -leftDoor.width;
            rightDoor.x: root.width + 85px;
            dial.dialAngle: DialState.startAngle;
            in {
                animate rightDoor.x, leftDoor.x {
                    duration: 800ms;
                    easing: ease-in-expo;
                }
            }
        }
        doorsClosed when target-door-state == DoorState.closed: {
            leftDoor.x: -30px;
            rightDoor.x: root.width / 2;
            in {
                animate rightDoor.x, leftDoor.x {
                    duration: 800ms;
                    easing: ease-in-expo;
                }
            }
        }
    ]

    rightDoor := Rectangle {
        property <length> notch-width: 220px;
        property <length> notch-border: 4px;
        property <length> notch-indent: 20px;

        x: parent.width / 2;
        width: parent.width / 2 + 30px;
        height: 100%;
        Rectangle {
            background: @linear-gradient(180deg, #34373F, #1D2026);
            clip: true;
            border-radius: 30px;
            Image {
                x: parent.width - self.width;
                y: parent.height - self.height;
                width: self.source.width * 2 * 1px;
                height: self.source.height * 2 * 1px;
                source: @image-url("images/logo-fragment.png");
                transform-rotation: 180deg;
                opacity: 0.08;
            }
        }

        Rectangle {
            border-width: notch-border;
            border-color: notch-border-color;
            border-radius: 30px;
        }

        Rectangle {
            x: -(notch-width / 2) + notch-border;
            width: (notch-width / 2);
            // - notch-indent ;
            height: notch-width;
            clip: true;
            Rectangle {
                x: notch-indent;
                y: 0;
                width: notch-width;
                height: self.width;
                Rectangle {
                    width: notch-width;
                    height: self.width;
                    border-radius: self.width / 2;
                    background: @linear-gradient(180deg, #2e3037, #25272c);
                    border-width: notch-border;
                    border-color: notch-border-color;
                }
            }
        }

        Image {
            x: 20px;
            y: (parent.height / 2) - 125px;
            source: @image-url("images/open-lock.svg");
            colorize:  #000;
            width: 15px;
            height: self.width;
            opacity: 0.4;
        }

        dial := Dial {
            x: -82px;
            y: (parent.height - self.height) / 2 - 1px;
            interactive: root.demo-locked;
            changed volume => {
                if self.volume >= 60 {
                    root.unlockDemo()
                }
            }
        }
    }
}

export component MainWindow inherits Window {

    width: 800px;
    height: 600px;
    background: #6d05e4;
    in-out property <bool> door-component-loaded: false;
    in-out property <DoorState> initial-door-state: open;
    property <bool> demo-locked: true;


    Button {
        width: 150px;
        height: 50px;
        icon: @image-url("images/lock.svg");
        text: "鎖屏";
        icon-size: 30px;
        clicked => {
            door-component-loaded = true;
        }
    }

    if door-component-loaded: door := Doors{
        doorsOpened => {
            door-component-loaded = false;
            initial-door-state = DoorState.open;
        }
        demo-locked: demo-locked;
        initial-door-state: initial-door-state;
        
    }



}

2.5、models.slint

export global DataAdapter {


    callback btn_clicked();
}

2.6、Cargo.toml

[package]
name = "ttt"
version = "0.1.0"
edition = "2024"
author = "<peng.xu@sf-express.com>"

[dependencies]
slint = "1.14.1"
[build-dependencies] 
slint-build = "1.14.1"

2.7、源碼資源下載

文章頂部下載。

二、Slint詳解

Slint 是一個用於創建本地用户界面的聲明式框架,特別為 Rust 設計,但也支持 C++。它的目標是提供一個輕量級高效易於使用的解決方案,用於構建具有現代外觀和感覺的桌面和嵌入式應用程序的 UI。Slint 的核心思想是使用一種聲明式的語言來描述用户界面,並將其與 Rust 的業務邏輯代碼緊密結合。

1、核心特性

  1. 聲明式 UI 語言 (.slint):
  • Slint 定義了自己的領域特定語言(DSL),通常寫在 .slint 文件中。
  • 這種語言語法簡潔,專注於描述 UI 的結構、佈局、屬性、狀態和交互。
  • 示例 .slint 片段:
import { Button } from "std-widgets.slint";

export component MainWindow {
    width: 300px;
    height: 200px;

    VerticalLayout {
        Button {
            text: "Click Me!";
            clicked => { // 處理點擊事件的邏輯 }
        }
        Text {
            text: "Hello Slint!";
        }
    }
}
  1. 響應式數據綁定:
  • Slint 支持聲明式響應式編程模型。
  • 使用 := 運算符綁定屬性值,當依賴項(如其他屬性或狀態變量)發生變化時,綁定會自動更新。
  • 例如:
export component MainWindow {
    in property <int> count: 0; // 輸入屬性
    out property <string> label_text: "Count: " + count; // 輸出屬性,綁定到 count
    ...
}
  • count 改變時,label_text 會自動更新。
  1. 狀態管理:
  • 使用 state 關鍵字定義組件的內部狀態。
  • 狀態變化會觸發 UI 的重新渲染。
  • 例如:
export component ToggleButton {
    out property <bool> checked;
    state property <bool> is_checked: false;

    // 點擊切換狀態
    clicked => {
        is_checked = !is_checked;
        checked = is_checked;
    }

    // UI 根據狀態變化
    background: is_checked ? @linear-gradient(...) : @solid-color(...);
    ...
}
  1. 與 Rust 集成:
  • 使用 slint-buildslint-interpreter 庫將 .slint 文件編譯或解釋成 Rust 代碼。
  • 編譯後,UI 組件在 Rust 中變成結構體,其屬性、回調函數和函數都可以在 Rust 代碼中訪問和操作。
  • 在 Rust 中實例化和運行 UI 的典型代碼:
use slint::ComponentHandle;

slint::slint! { // 這裏可以內聯 .slint 代碼,或使用路徑指向文件
    // ... .slint 內容 ...
}

fn main() {
    let ui = MainWindow::new();
    // 設置 Rust 回調
    let ui_weak = ui.as_weak();
    ui.on_button_clicked(move || {
        let ui = ui_weak.unwrap();
        // 處理點擊事件,更新 UI 狀態等
        ui.set_some_property(new_value);
    });
    ui.run();
}
  1. 跨平台:
  • Slint 支持 Windows, macOS, Linux (X11/Wayland), 以及嵌入式平台(如通過 OpenGL ES 或軟件渲染)。
  • 其渲染後端是可插拔的。
  1. 輕量級與高性能:
  • 設計之初就考慮了資源受限的環境(如嵌入式設備)。
  • 渲染引擎高效,避免了不必要的重繪。
  • 生成的代碼量相對較小。
  1. 內置組件庫:
  • 提供了一套基礎組件(如 Button, Slider, TextInput, ListView 等),稱為 “std-widgets”。
  • 用户可以基於這些基礎組件構建自己的自定義組件。

2、架構模式 (MVU)

Slint 鼓勵一種類似於 Model-View-Update (MVU)Model-View-ViewModel (MVVM) 的模式:

  • View:.slint 文件中聲明式定義。
  • Model/ViewModel/State:.slint 文件中通過 property, state 和回調函數定義,或者在 Rust 中定義並通過綁定暴露給 UI。
  • Update/Logic: 在 Rust 中實現,處理用户交互、業務邏輯,並更新 Model/State,進而通過綁定驅動 View 更新。

3、優勢

  • 開發效率: 聲明式語法簡化了 UI 構建和佈局。.slint 文件清晰地將 UI 描述與邏輯分離。
  • 性能: 高效渲染和輕量級設計。
  • 跨平台: 一套代碼支持多個目標平台。
  • Rust 集成: 與 Rust 語言深度集成,利用 Rust 的安全性和性能。
  • 熱重載 (部分支持): 在開發過程中,某些工具鏈支持修改 .slint 文件後無需重新編譯整個 Rust 項目即可看到 UI 變化。

4、適用場景

  • 桌面應用程序 (Windows, macOS, Linux)
  • 嵌入式設備 GUI
  • 需要輕量級、原生 UI 的 Rust 應用

5、總結

Slint 為 Rust 開發者提供了一個現代、聲明式且高效的 UI 框架。它通過自定義的 .slint 語言描述界面,結合強大的響應式數據綁定和狀態管理,並與 Rust 代碼無縫集成。其輕量級和高性能的特點使其成為桌面和嵌入式應用 UI 開發的強有力候選方案。如果你正在尋找一個 Rust 生態下的原生 GUI 解決方案,Slint 絕對值得一試。

建議查閲 Slint 官方文檔 和示例以獲取更詳細的信息和上手教程。