云计算百科
云计算领域专业知识百科平台

Rust+Slint 实现智能家居控制面板源码分享

在这里插入图片描述

Rust+Slint 实现智能家居控制面板源码分享

  • 一、效果展示
  • 二、源码分享
    • 1、工程结构
    • 2、main.rs
    • 3、build.rs
    • 4、main.slint
    • 5、Cargo.toml
  • 三、实现原理
    • 1、整体架构
    • 2、数据模型与绑定机制
    • 3、回调机制与状态同步
    • 4、组件化设计与图形绘制
    • 5、动画与交互反馈
    • 6、构建流程

一、效果展示

在这里插入图片描述 在这里插入图片描述在这里插入图片描述

二、源码分享

1、工程结构

在这里插入图片描述

2、main.rs

use std::cell::Cell;
use std::rc::Rc;
use slint::{Model, ModelRc, SharedString, VecModel, Timer};

slint::include_modules!();

fn main() -> Result<(), slint::PlatformError> {
let ui = SmartHomeWindow::new()?;

// ── 初始化 6 盏灯 ──
let rooms: Vec<(&str, i32)> = vec![
("客厅", 0), ("卧室", 1), ("厨房", 2),
("卫生间", 3), ("书房", 4), ("阳台", 5),
];

let lights: Vec<LightData> = rooms
.iter()
.enumerate()
.map(|(i, (name, icon_type))| LightData {
id: i as i32,
name: SharedString::from(*name),
icon_type: *icon_type,
is_on: i < 2,
brightness: if i == 0 { 80 } else if i == 1 { 60 } else { 0 },
})
.collect();

let lights_model = Rc::new(VecModel::from(lights.clone()));
ui.set_lights(ModelRc::from(lights_model.clone()));

// ── 初始化 7 天温度 ──
let days: Vec<(&str, i32, i32, i32)> = vec![
("周一", 32, 24, 0), ("周二", 30, 23, 1),
("周三", 28, 22, 2), ("周四", 31, 25, 0),
("周五", 33, 26, 0), ("周六", 29, 21, 1),
("周日", 27, 20, 2),
];

let temps: Vec<DayTemp> = days
.iter()
.map(|(name, high, low, wtype)| DayTemp {
day_name: SharedString::from(*name),
temp_high: *high,
temp_low: *low,
weather_type: *wtype,
})
.collect();
ui.set_temperatures(ModelRc::new(VecModel::from(temps)));
ui.set_temp_max(33);
ui.set_temp_min(20);

// ── 默认选中第一盏灯 ──
ui.set_selected_light_id(0);
ui.set_slider_brightness(80);

// ── 宠物初始状态 ──
ui.set_pet_state(PetState {
is_speaking: false,
speech_text: SharedString::from(""),
is_blinking: false,
});

// ── 用 Cell 包装可变状态 ──
let lights_vec = Rc::new(Cell::new(lights));

// ── 回调:切换灯开关 ──
let lights_ref = lights_vec.clone();
let lights_model_ref = lights_model.clone();
ui.on_toggle_light(move |id| {
let mut current: Vec<LightData> = lights_ref.take();
let idx = id as usize;
current[idx].is_on = !current[idx].is_on;
lights_model_ref.set_row_data(idx, current[idx].clone());
lights_ref.set(current);
});

// ── 回调:选中灯(用于调光) ──
let ui_weak = ui.as_weak();
let lights_ref = lights_vec.clone();
ui.on_select_light(move |id| {
let current: Vec<LightData> = lights_ref.take();
lights_ref.set(current.clone());
if let Some(ui) = ui_weak.upgrade() {
ui.set_selected_light_id(id);
ui.set_slider_brightness(current[id as usize].brightness);
}
});

// ── 回调:设置亮度 ──
let lights_ref = lights_vec.clone();
let lights_model_ref = lights_model.clone();
ui.on_set_brightness(move |id, brightness| {
let mut current: Vec<LightData> = lights_ref.take();
let idx = id as usize;
current[idx].brightness = brightness;
lights_model_ref.set_row_data(idx, current[idx].clone());
lights_ref.set(current);
});

// ── 回调:点击宠物 ──
let ui_weak = ui.as_weak();
let speak_index = Rc::new(Cell::new(0usize));
let phrases: Vec<&str> = vec![
"你好!欢迎回家",
"今天天气不错哦",
"客厅灯已打开",
"温度适宜,适合休息",
"所有设备运行正常",
"晚安,好梦",
];

ui.on_click_pet(move || {
if let Some(ui) = ui_weak.upgrade() {
let idx = speak_index.get();
let phrase = phrases[idx % phrases.len()];
speak_index.set(idx + 1);

ui.set_pet_state(PetState {
is_speaking: true,
speech_text: SharedString::from(phrase),
is_blinking: false,
});

let ui_weak2 = ui.as_weak();
Timer::single_shot(
std::time::Duration::from_millis(5000),
move || {
if let Some(ui) = ui_weak2.upgrade() {
ui.set_pet_state(PetState {
is_speaking: false,
speech_text: SharedString::from(""),
is_blinking: false,
});
}
},
);
}
});

ui.run()
}

3、build.rs

fn main(){
slint_build::compile("ui/main.slint")
.unwrap()
}

4、main.slint

// ── 数据结构 ──
export struct LightData {
id: int,
name: string,
icontype: int, // 0=沙发 1=床 2=厨房 3=浴 4=书 5=草
ison: bool,
brightness: int,
}

export struct DayTemp {
dayname: string,
temphigh: int,
templow: int,
weathertype: int, // 0=晴 1=多云 2=雨
}

export struct PetState {
isspeaking: bool,
speechtext: string,
isblinking: bool,
}

// ── 小组件 ──

component ToggleSwitch inherits Rectangle {
inout property <bool> ison: false;
callback toggled();
width: 32px; height: 18px; borderradius: 9px;
background: ison ? #ff9f43 : #2a2f45;
borderwidth: 1px;
bordercolor: ison ? #ffb347 : #3a3f55;
animate background { duration: 200ms; }
TouchArea {
mousecursor: pointer;
clicked => { ison = !ison; root.toggled(); }
}
Rectangle {
y: 2px;
x: ison ? parent.width 16px : 2px;
width: 14px; height: 14px; borderradius: 7px;
background: white;
animate x { duration: 200ms; easing: easeinout; }
}
}

// ── 房间图标(图形绘制)──
// 0=沙发 1=床 2=厨房 3=浴 4=书 5=草
component RoomIcon inherits Rectangle {
in property <int> roomtype;
in property <bool> active: false;
width: 20px; height: 20px;

// 0: 沙发
if roomtype == 0: Rectangle {
width: 18px; height: 10px; borderradius: 3px;
x: 1px; y: 5px;
background: active ? #ff9f43 : #6b7a99;
Rectangle { width: 5px; height: 12px; borderradius: 2px; x: 1px; y: 2px; background: active ? #ff9f43cc : #6b7a99cc; }
Rectangle { width: 5px; height: 12px; borderradius: 2px; x: 14px; y: 2px; background: active ? #ff9f43cc : #6b7a99cc; }
}
// 1: 床
if roomtype == 1: Rectangle {
width: 18px; height: 12px; borderradius: 2px;
x: 1px; y: 4px;
background: active ? #ff9f43 : #6b7a99;
Rectangle { width: 7px; height: 6px; borderradius: 2px; x: 1px; y: 3px; background: active ? #ffb347 : #8892a8; }
}
// 2: 厨房(锅)
if roomtype == 2: Rectangle {
width: 14px; height: 10px; borderradius: 5px;
x: 3px; y: 6px;
background: active ? #ff9f43 : #6b7a99;
Rectangle { width: 6px; height: 2px; x: 3px; y: 3px; background: active ? #ff9f43 : #6b7a99; }
}
// 3: 浴(水滴)
if roomtype == 3: Rectangle {
width: 10px; height: 14px; borderradius: 5px;
x: 5px; y: 3px;
background: active ? #4ecdc4 : #6b7a99;
}
// 4: 书
if roomtype == 4: Rectangle {
width: 14px; height: 16px; borderradius: 1px;
x: 3px; y: 2px;
background: active ? #ff9f43 : #6b7a99;
Rectangle { width: 1px; height: 12px; x: 3px; y: 2px; background: active ? #1a2040 : #0e1225; }
}
// 5: 草(叶子)
if roomtype == 5: Rectangle {
width: 12px; height: 12px; borderradius: 6px;
x: 4px; y: 2px;
background: active ? #4ecdc4 : #6b7a99;
Rectangle { width: 2px; height: 8px; x: 5px; y: 8px; background: active ? #4ecdc4cc : #6b7a99cc; }
}
}

// ── 天气图标(SVG图片)──
// 0=晴 1=多云 2=雨
component WeatherIcon inherits Rectangle {
in property <int> weathertype;
// width: 24px; height: 24px;

if weathertype == 0: Image {
source: @imageurl("../images/weather-sunny.svg");
// width: 100%; height: 100%;
}
if weathertype == 1: Image {
source: @imageurl("../images/weather-cloudy.svg");
// width: 100%; height: 100%;
}
if weathertype == 2: Image {
source: @imageurl("../images/weather-rainy.svg");
// width: 100%; height: 100%;
}
}

component LightCard inherits Rectangle {
in property <LightData> lightdata;
callback toggled(int);
callback selected(int);
borderradius: 12px;
background: lightdata.ison
? @lineargradient(135deg, #1a2040 0%, #1e2850 100%)
: @lineargradient(135deg, #0e1225 0%, #121830 100%);
borderwidth: 1px;
bordercolor: lightdata.ison ? #ff9f4344 : #ffffff0d;
animate background { duration: 250ms; }
animate bordercolor { duration: 250ms; }
TouchArea {
mousecursor: pointer;
clicked => { root.selected(lightdata.id); }
}
VerticalLayout {
padding: 10px; spacing: 4px;
HorizontalLayout {
spacing: 6px;
RoomIcon { roomtype: lightdata.icontype; active: lightdata.ison; }
Text { text: lightdata.name; color: #e8e0d4; fontsize: 11px; fontweight: 600; verticalalignment: center; horizontalstretch: 1; overflow: elide; }
}
// 亮度条区域始终保留,避免开关切换时高度变化
if lightdata.ison : Rectangle {
height: 4px; borderradius: 2px; background: #ffffff12;
Rectangle {
width: parent.width * lightdata.brightness / 100;
height: 100%; borderradius: 2px;
background: lightdata.ison ? @lineargradient(90deg, #ff9f43, #ffb347) : transparent;
animate width { duration: 150ms; }
}
}
if !lightdata.ison : Rectangle {
height: 4px; borderradius: 2px; background: transparent;
}
Text {
text: lightdata.brightness + "%";
color: #ff9f43; fontsize: 9px;
opacity: lightdata.ison ? 1 : 0;
}
Rectangle { verticalstretch: 1; }
HorizontalLayout {
alignment: end;
ToggleSwitch {
ison: lightdata.ison;
toggled => { root.toggled(lightdata.id); }
}
}
}
}

component TempCard inherits Rectangle {
in property <DayTemp> day;
in property <int> globalmaxtemp;
in property <int> globalmintemp;
borderradius: 8px;
background: @lineargradient(180deg, #161c38 0%, #0e1225 100%);
borderwidth: 1px; bordercolor: #ffffff0a;
HorizontalLayout {
paddingleft: 20px;
paddingright: 20px;
spacing: 12px;
WeatherIcon {
horizontalstretch: 0.5;
weathertype: day.weathertype;

}
VerticalLayout {
horizontalstretch: 1;
padding: 6px; spacing: 3px;
Text { text: day.dayname; color: #e8e0d4; fontsize: 10px; fontweight: 600; horizontalalignment: center; }
Rectangle {
verticalstretch: 1;
HorizontalLayout {
alignment: center;
Rectangle {
width: 6px; borderradius: 3px; background: #ffffff0a;
Rectangle {
y: parent.height self.height;
width: 100%; borderradius: 3px;
height: parent.height * (day.temphigh globalmintemp) / max(1, globalmaxtemp globalmintemp);
background: day.temphigh >= 35 ? #ff6b6b : day.temphigh >= 25 ? #ff9f43 : #4ecdc4;
animate height { duration: 300ms; }
}
}
}
}
Text { text: day.temphigh + "°"; color: #ff9f43; fontsize: 10px; fontweight: 600; horizontalalignment: center; }
Text { text: day.templow + "°"; color: #4ecdc4; fontsize: 9px; horizontalalignment: center; }
}
}

}

component RobotPet inherits Rectangle {
in property <PetState> pet;
callback clickedpet();
background: transparent;
pettouch := TouchArea { mousecursor: pointer; clicked => { root.clickedpet(); } }
VerticalLayout {
alignment: center;
if pet.isspeaking: Rectangle {
height: 36px;
//width: parent.parent.width;
borderradius: 8px; background: #1e2850ee;
borderwidth: 1px; bordercolor: #4ecdc444;
Text { text: pet.speechtext; color: #e8e0d4; fontsize: 10px; horizontalalignment: center; verticalalignment: center; overflow: elide; }
}
Rectangle {
height: 5px;
}
Rectangle {
x:parent.width/2 self.width/2;
width:8px;height: self.width;
borderradius: self.width/2;
background: #ff6b6b;
}
Rectangle {
x:parent.width/2 self.width/2;
width:3px;height: 10px;
background: #4ecdc4;
}
Rectangle {
x:parent.width/2 self.width/2;
width:parent.width/2;height: self.width;
borderradius: self.width/2;
background: @lineargradient(180deg, #1e2850 0%, #161c38 100%);

// 眼睛
Rectangle { x: parent.width/3 self.width/2; y: parent.height/4; width: pet.isblinking ? parent.width/9 : parent.width/10; height: pet.isblinking ? parent.height/9 : parent.height/10; borderradius: 4px; background: #4ecdc4; animate height { duration: 100ms; } }
Rectangle { x: parent.width/3*2 self.width/2; y: parent.height/4; width: pet.isblinking ? parent.width/9 : parent.width/10; height: pet.isblinking ? parent.height/9 : parent.height/10; borderradius: 4px; background: #4ecdc4; animate height { duration: 100ms; } }
// 脸颊
Rectangle { x: parent.width/4 self.width/2; y: parent.height/2.0; width: parent.width/6; height: 8px; borderradius: self.height/2; background: pet.isspeaking ? #ff6b6b88 : #ff6b6b44; }
Rectangle { x: parent.width/4 *3 self.width/2; y: parent.height/2.0; width: parent.width/6; height: 8px; borderradius: self.height/2; background: pet.isspeaking ? #ff6b6b88 : #ff6b6b44; }
// 嘴巴
Rectangle { x: parent.width / 2 self.width/2; y: parent.height/1.4; width: parent.width/5; height: pet.isspeaking ? 8px : 5px; borderradius: 2px; background: #ff9f43; animate height { duration: 150ms; } }

}
Rectangle {height: 5px;}
// 身体
Rectangle {
x: parent.width / 2 self.width/2;
width: parent.width/2.7; height: self.width/2; borderradius: 8px;
background: @lineargradient(180deg, #1a2040 0%, #14182e 100%);
borderwidth: 1px; bordercolor: #4ecdc433;
Rectangle { x: parent.width / 2 self.width/2; y: 8px; width: parent.width/8; height: parent.width/8; borderradius: self.width/2; background: pet.isspeaking ? #4ecdc4 : #4ecdc444; animate background { duration: 200ms; } }
}
Text { width: 100%; text: "点击说话"; color: #5a6380; fontsize: 8px; horizontalalignment: center; }
}
}

// 分段式调光滑块(10段,每段10%,纯int操作)
component DimmingSlider inherits Rectangle {
in property <string> currentlightname;
in property <bool> currentlighton;
inout property <int> brightness;
callback brightnesschanged(int);
height: 42px; borderradius: 10px;
background: @lineargradient(90deg, #121830 0%, #161c38 100%);
borderwidth: 1px; bordercolor: #ffffff0d;
HorizontalLayout {
paddingleft: 10px; paddingright: 10px; spacing: 8px;
VerticalLayout {
width: 60px; alignment: start;
Text { text: currentlightname; color: #e8e0d4; fontsize: 10px; fontweight: 600; overflow: elide; }
Text { text: currentlighton ? brightness + "%" : "已关闭"; color: currentlighton ? #ff9f43 : #6b7a99; fontsize: 9px; }
}
// 分段滑块条
HorizontalLayout {
verticalstretch: 1; spacing: 2px;
for seg[idx] in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] : Rectangle {
horizontalstretch: 1;
borderradius: 2px;
background: (seg * 10) <= root.brightness
? @lineargradient(180deg, #ff9f43 0%, #ffb347 100%)
: #1a1f35;
animate background { duration: 100ms; }
TouchArea {
mousecursor: pointer;
clicked => {
root.brightness = seg * 10;
root.brightnesschanged(seg * 10);
}
}
}
}
// 数值显示
Rectangle {
width: 34px; height: 20px; borderradius: 5px; background: #ff9f4318;
Text { text: brightness + "%"; color: #ff9f43; fontsize: 10px; fontweight: 600; horizontalalignment: center; verticalalignment: center; }
}
}
}

// ── 主窗口 ──
export component SmartHomeWindow inherits Window {
title: "智能家居";
preferredwidth: 800px; preferredheight: 480px;
minwidth: 800px; minheight: 480px;
background: @lineargradient(180deg, #0a0e1a 0%, #121830 100%);

in property <[LightData]> lights;
in property <[DayTemp]> temperatures;
inout property <int> selectedlightid;
inout property <int> sliderbrightness;
in property <PetState> petstate;
in property <int> tempmax;
in property <int> tempmin;

callback togglelight(int);
callback selectlight(int);
callback setbrightness(int, int);
callback clickpet();

VerticalLayout {
padding: 8px; spacing: 6px;

// ═══ 顶部栏 ═══
Rectangle {
height: 32px; borderradius: 8px; background: #ffffff06;
HorizontalLayout {
paddingleft: 10px; paddingright: 10px; spacing: 8px;
Text { text: "\\u{1F3E0} 智能家居"; color: #e8e0d4; fontsize: 13px; fontweight: 700; verticalalignment: center; }
Rectangle { y:parent.height/2 self.height/2;horizontalstretch: 1; }
Rectangle { y:parent.height/2 self.height/2;width: 6px; height: 6px; borderradius: 3px; background: #4ecdc4; }
Text { text: "在线"; color: #4ecdc4; fontsize: 9px; verticalalignment: center; }
}
}

// ═══ 灯光网格 + 宠物 ═══
HorizontalLayout {
verticalstretch: 1; spacing: 6px;

// 灯光网格 3×2
Rectangle {
horizontalstretch: 3;
borderradius: 10px; background: #ffffff04;
VerticalLayout {
x: 6px; y: 6px;
width: parent.width 12px;
height: parent.height 12px;
spacing: 6px;
// 第一行: 灯 0, 1, 2
HorizontalLayout {
spacing: 6px;
LightCard {
horizontalstretch: 1;
lightdata: root.lights[0];
toggled(id) => { root.togglelight(0); }
selected(id) => { root.selectlight(0); }
}
LightCard {
horizontalstretch: 1;
lightdata: root.lights[1];
toggled(id) => { root.togglelight(1); }
selected(id) => { root.selectlight(1); }
}
LightCard {
horizontalstretch: 1;
lightdata: root.lights[2];
toggled(id) => { root.togglelight(2); }
selected(id) => { root.selectlight(2); }
}
}
// 第二行: 灯 3, 4, 5
HorizontalLayout {
spacing: 6px;
LightCard {
horizontalstretch: 1;
lightdata: root.lights[3];
toggled(id) => { root.togglelight(3); }
selected(id) => { root.selectlight(3); }
}
LightCard {
horizontalstretch: 1;
lightdata: root.lights[4];
toggled(id) => { root.togglelight(4); }
selected(id) => { root.selectlight(4); }
}
LightCard {
horizontalstretch: 1;
lightdata: root.lights[5];
toggled(id) => { root.togglelight(5); }
selected(id) => { root.selectlight(5); }
}
}
}
}

// 宠物
Rectangle {
width: 180px;
RobotPet {
width: 100%;
// height: 100%;
pet: root.petstate;
clickedpet => { root.clickpet(); }
}
}
}

// ═══ 温度预报 ═══
Rectangle {
height: 80px; borderradius: 10px; background: #ffffff04;
HorizontalLayout {
padding: 6px; spacing: 4px;
VerticalLayout {
width: 80px; spacing: 1px;
Text { text: "7天"; color: #e8e0d4; fontsize: 14px; fontweight: 600; }
Text { text: "温度"; color: #8892a8; fontsize: 13px; }
}
for day in root.temperatures : Rectangle {
horizontalstretch: 1;
TempCard {
width: parent.width; height: parent.height;
day: day;
globalmaxtemp: root.tempmax;
globalmintemp: root.tempmin;
}
}
}
}

// ═══ 调光控制 ═══
DimmingSlider {
currentlightname: root.lights[root.selectedlightid].name;
currentlighton: root.lights[root.selectedlightid].ison;
brightness <=> root.sliderbrightness;
brightnesschanged(val) => {
root.setbrightness(root.selectedlightid, val);
}
}
}
}

5、Cargo.toml

[package]
name = "slint-smarthome"
version = "0.1.0"
edition = "2021"
build = "build.rs"

[dependencies]
slint = "1.17"

[builddependencies]
slintbuild = "1.17"

三、实现原理

1、整体架构

本项目采用 Rust + Slint 的前后端分离架构:Rust 负责业务逻辑与数据管理,Slint 负责界面声明与渲染。两者通过 Slint 自动生成的类型安全接口进行通信,无需手动编写 FFI 绑定。

#mermaid-svg-A1yihBpigPPxqZDw{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-A1yihBpigPPxqZDw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-A1yihBpigPPxqZDw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-A1yihBpigPPxqZDw .error-icon{fill:#552222;}#mermaid-svg-A1yihBpigPPxqZDw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-A1yihBpigPPxqZDw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-A1yihBpigPPxqZDw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-A1yihBpigPPxqZDw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-A1yihBpigPPxqZDw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-A1yihBpigPPxqZDw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-A1yihBpigPPxqZDw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-A1yihBpigPPxqZDw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-A1yihBpigPPxqZDw .marker.cross{stroke:#333333;}#mermaid-svg-A1yihBpigPPxqZDw svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-A1yihBpigPPxqZDw p{margin:0;}#mermaid-svg-A1yihBpigPPxqZDw .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-A1yihBpigPPxqZDw .cluster-label text{fill:#333;}#mermaid-svg-A1yihBpigPPxqZDw .cluster-label span{color:#333;}#mermaid-svg-A1yihBpigPPxqZDw .cluster-label span p{background-color:transparent;}#mermaid-svg-A1yihBpigPPxqZDw .label text,#mermaid-svg-A1yihBpigPPxqZDw span{fill:#333;color:#333;}#mermaid-svg-A1yihBpigPPxqZDw .node rect,#mermaid-svg-A1yihBpigPPxqZDw .node circle,#mermaid-svg-A1yihBpigPPxqZDw .node ellipse,#mermaid-svg-A1yihBpigPPxqZDw .node polygon,#mermaid-svg-A1yihBpigPPxqZDw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-A1yihBpigPPxqZDw .rough-node .label text,#mermaid-svg-A1yihBpigPPxqZDw .node .label text,#mermaid-svg-A1yihBpigPPxqZDw .image-shape .label,#mermaid-svg-A1yihBpigPPxqZDw .icon-shape .label{text-anchor:middle;}#mermaid-svg-A1yihBpigPPxqZDw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-A1yihBpigPPxqZDw .rough-node .label,#mermaid-svg-A1yihBpigPPxqZDw .node .label,#mermaid-svg-A1yihBpigPPxqZDw .image-shape .label,#mermaid-svg-A1yihBpigPPxqZDw .icon-shape .label{text-align:center;}#mermaid-svg-A1yihBpigPPxqZDw .node.clickable{cursor:pointer;}#mermaid-svg-A1yihBpigPPxqZDw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-A1yihBpigPPxqZDw .arrowheadPath{fill:#333333;}#mermaid-svg-A1yihBpigPPxqZDw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-A1yihBpigPPxqZDw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-A1yihBpigPPxqZDw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-A1yihBpigPPxqZDw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-A1yihBpigPPxqZDw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-A1yihBpigPPxqZDw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-A1yihBpigPPxqZDw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-A1yihBpigPPxqZDw .cluster text{fill:#333;}#mermaid-svg-A1yihBpigPPxqZDw .cluster span{color:#333;}#mermaid-svg-A1yihBpigPPxqZDw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-A1yihBpigPPxqZDw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-A1yihBpigPPxqZDw rect.text{fill:none;stroke-width:0;}#mermaid-svg-A1yihBpigPPxqZDw .icon-shape,#mermaid-svg-A1yihBpigPPxqZDw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-A1yihBpigPPxqZDw .icon-shape p,#mermaid-svg-A1yihBpigPPxqZDw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-A1yihBpigPPxqZDw .icon-shape .label rect,#mermaid-svg-A1yihBpigPPxqZDw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-A1yihBpigPPxqZDw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-A1yihBpigPPxqZDw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-A1yihBpigPPxqZDw :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

main.rs 业务逻辑

slint::include_modules! 生成接口

SmartHomeWindow 界面

LightCard 灯光卡片

TempCard 温度卡片

RobotPet 宠物组件

DimmingSlider 调光滑块

回调 toggle-light / select-light

数据绑定 temperatures

回调 click-pet

回调 set-brightness

2、数据模型与绑定机制

Slint 使用声明式 UI 语言,通过 export struct 定义跨语言数据结构。Rust 侧通过 slint::include_modules!() 宏引入这些结构,实现类型安全的双向绑定。

// main.slint 中定义
export struct LightData {
id: int,
name: string,
icontype: int,
ison: bool,
brightness: int,
}

Rust 侧创建 VecModel 作为数据源,通过 ModelRc 传递给 UI:

let lights_model = Rc::new(VecModel::from(lights.clone()));
ui.set_lights(ModelRc::from(lights_model.clone()));

当 UI 中修改数据时,通过 set_row_data 更新模型,Slint 自动触发界面刷新,无需手动操作 DOM。

3、回调机制与状态同步

Slint 的 callback 机制实现了 UI 事件到 Rust 逻辑的传递。以灯光开关为例:

// main.slint 中声明
callback togglelight(int);

// main.rs 中注册
ui.on_toggle_light(move |id| {
let mut current: Vec<LightData> = lights_ref.take();
let idx = id as usize;
current[idx].is_on = !current[idx].is_on;
lights_model_ref.set_row_data(idx, current[idx].clone());
lights_ref.set(current);
});

这里使用 Rc<Cell<Vec<LightData>>> 包装可变状态,配合 take() / set() 实现借用检查器友好的状态更新。每次修改后调用 set_row_data 通知模型更新,Slint 自动重绘对应组件。

4、组件化设计与图形绘制

Slint 支持组件化开发,本项目将界面拆分为多个可复用组件:

  • LightCard:灯光卡片,包含图标、名称、亮度条和开关
  • TempCard:温度卡片,包含天气图标和温度柱状图
  • RobotPet:宠物组件,包含表情、身体和对话气泡
  • DimmingSlider:分段式调光滑块

图标采用纯图形绘制而非图片资源,例如沙发图标:

if room-type == 0: Rectangle {
width: 18px; height: 10px; border-radius: 3px;
x: 1px; y: 5px;
background: active ? #ff9f43 : #6b7a99;
Rectangle { width: 5px; height: 12px; border-radius: 2px; x: -1px; y: -2px; background: active ? #ff9f43cc : #6b7a99cc; }
Rectangle { width: 5px; height: 12px; border-radius: 2px; x: 14px; y: -2px; background: active ? #ff9f43cc : #6b7a99cc; }
}

通过 if 条件渲染不同图形,配合 active 属性实现开关状态的视觉反馈。

5、动画与交互反馈

Slint 内置动画系统,通过 animate 关键字声明属性过渡:

animate background { duration: 200ms; }
animate x { duration: 200ms; easing: ease-in-out; }

宠物对话功能使用 Timer::single_shot 实现定时恢复:

Timer::single_shot(
std::time::Duration::from_millis(5000),
move || {
if let Some(ui) = ui_weak2.upgrade() {
ui.set_pet_state(PetState {
is_speaking: false,
speech_text: SharedString::from(""),
is_blinking: false,
});
}
},
);

6、构建流程

build.rs 在编译时调用 slint_build::compile 将 .slint 文件编译为 Rust 代码:

fn main(){
slint_build::compile("ui/main.slint")
.unwrap()
}

编译产物通过 include_modules!() 宏在运行时引入,实现 UI 定义与业务逻辑的完全解耦。

在这里插入图片描述

赞(0)
未经允许不得转载:网硕互联帮助中心 » Rust+Slint 实现智能家居控制面板源码分享
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!