【Xilem 0.4 基础语法学与练】第一课:从零到计数器

引言:为什么学 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 性能优化
相关推荐
David猪大卫2 小时前
【C++修炼】智能指针使用及原理
开发语言·c++·经验分享·笔记·学习·考研·面试
z23456d2 小时前
第四十一天学习心得
学习
sunflower#3 小时前
Go学习第五阶段:接口、泛型与包设计,实现可替换的通讯录存储层
学习·oracle·golang
dadaobusi3 小时前
RV设备透传:主线尚未支持
学习
xqqxqxxq4 小时前
AI Agent学习:主动工具发现(李博杰《深入理解 AI Agent》4.8观后总结)
人工智能·学习
爱吃苹果的日记本5 小时前
数据结构第一课
c语言·数据结构·数据库·学习·c#
孫治AllenSun5 小时前
【LangChain4J-09】开发学习知识点
spring boot·后端·学习
边境悍匪6 小时前
蜗牛学苑 Java 智能体学习 Day38|Spring AI Alibaba2 思维导图复盘
java·学习·spring