引言:为什么学 Xilem
Xilem 的核心哲学只有一句话:
UI = f(状态)
视图是状态的纯函数。你定义一个 Rust 结构体来存放状态,再写一个普通函数把状态"翻译"成 UI 描述。状态变了,函数重新执行,UI 自动更新。
没有 cx 上下文对象,没有信号,没有 use_state,没有 emit(Event)------就是最朴素的"函数接收数据,返回结果"。
环境准备
系统要求
-
Rust 版本 ≥ 1.92(需要 2024 edition 支持)
-
Linux 用户需额外安装系统依赖:
Debian/Ubuntu
sudo apt-get install clang libwayland-dev libxkbcommon-x11-dev libvulkan-dev
Fedora
sudo dnf install clang wayland-devel libxkbcommon-x11-devel libxcb-devel vulkan-loader-devel
创建项目
shell
cargo new xilem_lesson1
cd xilem_lesson1
cargo add xilem winit
核心概念:三层架构
Xilem 内部有三层,但你只需要关心最上面一层:
你写的代码(View 层) → "我想要一个按钮和一个标签"(蓝图)
↓ diff
框架自动处理(Element 层)→ "按钮没变,只更新标签文字"(施工调度)
↓
底层渲染(Widget 层) → 用 Vello GPU 引擎画到屏幕上(实际建造)
你只负责"画蓝图"------用纯 Rust 函数描述 UI 应该长什么样。状态变化时,框架自动 diff 并最小化更新底层 Widget。
第一个程序:计数器
将 src/main.rs 替换为以下代码:
rust
use winit::error::EventLoopError;
use xilem::view::{Axis, text_button, flex, label};
use xilem::{EventLoop, WindowOptions, WidgetView, Xilem};
// ① 定义应用状态------可以是任意 Rust 类型
#[derive(Default)]
struct Counter {
num: i32,
}
// ② 定义视图函数------状态 → UI 描述
fn app_logic(data: &mut Counter) -> impl WidgetView<Counter> + use<> {
flex(
Axis::Vertical, // 纵向排列
(
label(format!("{}", data.num)), // 显示数字
text_button("increment", |data: &mut Counter| data.num += 1), // 按钮
),
)
}
// ③ 启动应用
fn main() -> Result<(), EventLoopError> {
let app = Xilem::new_simple(
Counter::default(), // 初始状态
app_logic, // 视图函数
WindowOptions::new("我的第一个 Xilem 应用"), // 窗口配置
);
app.run_in(EventLoop::with_user_event())?;
Ok(())
}
运行:
shell
cargo run
点击按钮,数字就会 +1。就这么简单。
逐行拆解:三个关键语法点
语法点 1:状态是任意 Rust 类型
rust
#[derive(Default)]
struct Counter {
num: i32,
}
Xilem 不限制你的状态类型。可以是结构体、元组、枚举,甚至 i32。#derive(Default) 是为了方便创建初始值。
语法点 2:视图是状态的纯函数
rust
fn app_logic(data: &mut Counter) -> impl WidgetView<Counter> + use<> {
- 参数 data: &mut Counter:框架传入当前状态的可变引用
- 返回值 impl WidgetView:返回一个描述 UI 的 View 对象
-
- use<>:Rust 2024 edition 的精确捕获语法,表示闭包不捕获任何外部环境变量
语法点 3:声明式布局------函数 + 元组
rust
flex(
Axis::Vertical, // 第一个参数:排列方向
( // 第二个参数:子元素元组
label(...),
text_button(...),
),
)
- flex() 是 Flexbox 布局容器
- Axis::Vertical 表示纵向排列(还有 Axis::Horizontal)
- 子元素用元组 (a, b, c) 组合,最多 12 个元素
- 超过 12 个?嵌套 flex 即可
事件处理:闭包直接修改状态
rust
text_button("increment", |data: &mut Counter| data.num += 1)
- 第一个参数:按钮文字
- 第二个参数:回调闭包,接收 &mut Counter,直接修改状态
- 修改后,框架自动重新执行 app_logic,diff 后只更新变化的部分
💡 核心理解:
你不需要手动调用"刷新 UI"。修改状态 → 框架自动重新执行视图函数 → diff 算法只更新变化的 widget。这就是"响应式"。
试一试
练习一:加减计数器
在计数器的基础上,添加一个"decrement"按钮,实现加减计数。窗口中应该显示当前数字,以及两个按钮:一个 +1,一个 -1。
练习二:横向布局 + 多状态字段
将状态扩展为包含姓名(String)和分数(i32)两个字段,纵向显示姓名和分数,下方用横向布局放置两个按钮:一个"得分 +10",一个"重置"(分数归零、姓名恢复为"新玩家")。
练习三(进阶):条件渲染
在练习二的基础上,根据分数显示不同的提示文字:
- 分数 ≥ 100:显示"🏆 太棒了!"
- 分数 ≥ 50:显示"💪 继续加油!"
- 分数 < 50:显示"🎮 开始游戏吧"
提示:视图函数就是普通 Rust 函数,if/else、match 都可以直接用。
练习答案与知识点总结
以下是三个练习的正确答案:
练习一答案
rust
fn app_logic(data: &mut Counter) -> impl WidgetView<Counter> + use<> {
flex(
Axis::Vertical,
(
label(format!("{}", data.num)),
text_button("+1", |data: &mut Counter| data.num += 1),
text_button("-1", |data: &mut Counter| data.num -= 1),
),
)
}
知识点:
- 元组中可以放任意数量的子 View,每个 View 独立声明。多个 text_button 放在同一个元组里,就会按 flex 指定的方向依次排列。
- 每个按钮的闭包独立捕获 &mut Counter,各自修改同一个字段,互不干扰。
练习二答案
rust
struct Player {
name: String,
score: i32,
}
impl Default for Player {
fn default() -> Self {
Player {
name: "新玩家".to_string(),
score: 0,
}
}
}
fn app_logic(data: &mut Player) -> impl WidgetView<Player> + use<> {
flex(
Axis::Vertical,
(
label(format!("玩家: {}", data.name)),
label(format!("分数: {}", data.score)),
flex(
Axis::Horizontal,
(
text_button("得分 +10", |data: &mut Player| data.score += 10),
text_button("重置", |data: &mut Player| {
data.score = 0;
data.name = "新玩家".to_string();
}),
),
),
),
)
}
知识点:
- 当业务需要的默认值不是类型的零值时(比如 String 的零值是 "",但我们需要 "新玩家"),不能用 #derive(Default),必须手动实现 Default trait。
- 姓名和分数各自是一个 label,直接放在外层 flex(Axis::Vertical, (...)) 的元组中,自然纵向排列、各自独占一行。
- 两个按钮放在内层 flex(Axis::Horizontal, (...)) 中,实现横向并排。
- 外层纵向、内层横向------嵌套 flex 是 Xilem 实现混合布局的标准方式。
- 闭包中可以写多行逻辑,最后一个表达式的值作为闭包返回值。重置闭包中最后一条语句返回 (),即 unit 类型,正好匹配闭包的返回要求。
练习三答案
rust
fn app_logic(data: &mut Player) -> impl WidgetView<Player> + use<> {
let tip = if data.score >= 100 {
"🏆 太棒了!"
} else if data.score >= 50 {
"💪 继续加油!"
} else {
"🎮 开始游戏吧"
};
flex(
Axis::Vertical,
(
label(format!("玩家: {}", data.name)),
label(format!("分数: {}", data.score)),
label(tip.to_string()),
flex(
Axis::Horizontal,
(
text_button("得分 +10", |data: &mut Player| data.score += 10),
text_button("重置", |data: &mut Player| {
data.score = 0;
data.name = "新玩家".to_string();
}),
),
),
),
)
}
知识点:
- 视图函数就是普通 Rust 函数,if/else 等控制流可以直接用在 View 构建之前,先算好数据,再传给 View 组件。
- "在练习二的基础上"意味着保留所有已有功能(姓名、分数、得分按钮、重置按钮),只新增题目要求的条件渲染部分------在分数标签和按钮行之间插入一行 label(tip.to_string())。
- if/else 的三个分支都返回 &str,类型一致,所以可以赋值给同一个变量 tip。如果不同分支返回不同类型的 View,则需要用 one_of 做类型统一------这是后续课程会讲到的内容。
本课知识清单
状态:用 struct + Default 定义,任意 Rust 类型都行,框架通过 &mut 引用传入视图函数
视图函数:签名为 fn app_logic(data: &mut S) -> impl WidgetView ,是一个纯函数,输入状态、输出 UI 描述。
布局:用 flex(Axis, (child1, child2)) 函数 + 元组的方式声明式组合, Axis::Vertical 纵向、 Axis::Horizontal 横向。
事件: text_button("文字", |data| ...) 的闭包直接接收状态的可变引用,修改即触发刷新。
更新机制:View diff------你只管改状态,框架自动 diff View 树并最小化更新底层 Widget。
控制流: if/else 、 match 等 Rust 原生语法直接用在视图函数中,无需任何模板。
下课预告
第二课将覆盖:
- WidgetView trait 的更多方法(.on_click()、.enabled() 等)
- 列表渲染(for_each / ViewSequence)
- 组件拆分(将大视图拆成多个小函数)
- memoize 性能优化