前言
源码来源:xilem/examples/to_do_mvc.rs
适配版本:Xilem 0.4 / Masonry 0.4
一、完整源码
rust
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
//! A to-do-list app, loosely inspired by todomvc.
use xilem::kurbo::Axis;
use xilem::masonry::layout::{AsUnit, Length};
use xilem::masonry::theme::{DEFAULT_GAP, ZYNC_800};
use xilem::masonry::widgets::DashFit;
use xilem::style::Style as _;
use xilem::view::{
FlexExt, FlexSpacer, MainAxisAlignment, button, checkbox, divider, divider_h, flex_col,
flex_row, label, text_button, text_input,
};
use xilem::winit::error::EventLoopError;
use xilem::{EventLoop, EventLoopBuilder, InsertNewline, WidgetView, WindowOptions, Xilem};
struct Task {
description: String,
done: bool,
}
#[derive(PartialEq, Eq, Copy, Clone)]
enum Filter {
All,
Active,
Completed,
}
struct TaskList {
next_task: String,
filter: Filter,
tasks: Vec<Task>,
}
impl TaskList {
fn add_task(&mut self) {
if self.next_task.is_empty() {
return;
}
self.tasks.push(Task {
description: std::mem::take(&mut self.next_task),
done: false,
});
}
}
fn app_logic(task_list: &mut TaskList) -> impl WidgetView<TaskList> + use<> {
let header_text = label("todos").text_size(80.);
let input_box = text_input(
task_list.next_task.clone(),
|task_list: &mut TaskList, new_value| {
task_list.next_task = new_value;
},
)
.text_size(16.)
.placeholder("What needs to be done?")
.insert_newline(InsertNewline::OnShiftEnter)
.on_enter(|task_list: &mut TaskList, _| {
task_list.add_task();
});
let input_line = flex_row((
input_box.flex(1.0),
button(
label("Add task".to_string()).text_size(16.),
|task_list: &mut TaskList| {
task_list.add_task();
},
),
));
let tasks = task_list
.tasks
.iter()
.enumerate()
.filter_map(|(i, task)| {
if (task_list.filter == Filter::Active && task.done)
|| (task_list.filter == Filter::Completed && !task.done)
{
None
} else {
let checkbox = checkbox(
task.description.clone(),
task.done,
move |data: &mut TaskList, checked| {
data.tasks[i].done = checked;
},
)
.text_size(16.);
let delete_button = text_button("Delete", move |data: &mut TaskList| {
data.tasks.remove(i);
})
.padding(5.px());
Some(
flex_row((checkbox, FlexSpacer::Flex(1.), delete_button))
.padding(DEFAULT_GAP)
.border(ZYNC_800, 1.px()),
)
}
})
.collect::<Vec<_>>();
let filter_tasks = |label, filter| {
// TODO: replace with combo-buttons
checkbox(
label,
task_list.filter == filter,
move |state: &mut TaskList, _| state.filter = filter,
)
};
let has_tasks = !task_list.tasks.is_empty();
let footer = has_tasks.then(|| {
flex_row((
filter_tasks("All", Filter::All),
filter_tasks("Active", Filter::Active),
filter_tasks("Completed", Filter::Completed),
))
.main_axis_alignment(MainAxisAlignment::Center)
});
flex_col((
header_text,
FlexSpacer::Fixed(DEFAULT_GAP),
input_line,
divider(Axis::Horizontal, label("Tasks")),
tasks,
FlexSpacer::Fixed(3.px()),
divider_h()
.thickness(2.px())
.dash_fit(DashFit::Stretch)
.dash_pattern(&[5.px(), 5.px()]),
FlexSpacer::Fixed(3.px()),
footer,
))
.gap(Length::px(4.))
.padding(50.px())
}
pub(crate) fn run(event_loop: EventLoopBuilder) -> Result<(), EventLoopError> {
let data = TaskList {
// Add a placeholder task for Android, whilst the
next_task: "My Next Task".into(),
filter: Filter::All,
tasks: vec![
Task {
description: "Buy milk".into(),
done: false,
},
Task {
description: "Buy eggs".into(),
done: true,
},
Task {
description: "Buy bread".into(),
done: false,
},
],
};
let app = Xilem::new_simple(data, app_logic, WindowOptions::new("To Do MVC"));
app.run_in(event_loop)
}
// Boilerplate code: Identical across all applications which support Android
fn main() -> Result<(), EventLoopError> {
run(EventLoop::with_user_event())
}
二、程序核心功能
本案例是TodoMVC风格待办清单(MVC架构示例),是Xilem综合实战:
✅ 文本输入框、回车添加任务
✅ 任务勾选标记完成/未完成
✅ 删除单条任务
✅ 任务筛选:全部 / 待完成(Active) / 已完成(Completed)
✅ 空任务时自动隐藏底部筛选栏
✅ 分割线、虚线分割线、布局间距、边框样式
本课核心主题:
集合列表渲染、数据过滤、条件视图、文本输入控件、MVC思想
- Model: TaskList + Task + Filter (纯数据)
- View: app_logic 构建UI视图
- Controller:按钮、输入框、复选框回调,修改Model
三、逐模块深度解析
1. 数据模型 Model
rust
struct Task {
description: String, // 任务描述
done: bool, // 是否完成
}
#[derive(PartialEq, Eq, Copy, Clone)]
enum Filter {
All,
Active,
Completed,
}
struct TaskList {
next_task: String, // 输入框临时文本
filter: Filter, // 当前筛选条件
tasks: Vec<Task>, // 任务数组
}
- TaskList::add_task() :把输入框内容移入tasks列表,清空输入框。 std::mem::take 高效转移字符串所有权,避免clone。
2. 输入框 text_input
rust
text_input(
task_list.next_task.clone(),
|task_list: &mut TaskList, new_value| {
task_list.next_task = new_value;
},
)
.text_size(16.)
.placeholder("What needs to be done?")
.insert_newline(InsertNewline::OnShiftEnter)
.on_enter(|task_list: &mut TaskList, _| {
task_list.add_task();
});
- 双向绑定:输入实时更新 next_task
- placeholder 占位提示文字
- .on_enter :按下回车执行添加任务
- InsertNewline::OnShiftEnter :Shift+Enter才换行,普通Enter提交任务
3. 列表渲染 + filter_map 动态生成任务UI(重点)
rust
let tasks = task_list
.tasks
.iter()
.enumerate()
.filter_map(|(i, task)| {
// 根据filter判断是否渲染当前任务
if (task_list.filter == Filter::Active && task.done)
|| (task_list.filter == Filter::Completed && !task.done)
{
None // 不渲染
} else {
// 生成一行任务UI:复选框 + 弹性占位 + 删除按钮
let checkbox = checkbox(...);
let delete_button = text_button("Delete", move |data: &mut TaskList| {
data.tasks.remove(i);
});
Some(
flex_row((checkbox, FlexSpacer::Flex(1.), delete_button))
.padding(DEFAULT_GAP)
.border(ZYNC_800, 1.px()),
)
}
})
.collect::<Vec<_>>();
- enumerate :拿到索引i,用于删除/修改对应task
- filter_map :遍历任务,按筛选条件动态决定是否生成视图,返回None就不渲染该条目
- checkbox :Xilem原生复选框组件,绑定勾选状态
- 每行任务flex_row:FlexSpacer把删除按钮推到最右侧
- 每条任务增加内边距+边框,视觉卡片效果
4. 筛选器封装函数 filter_tasks
rust
let filter_tasks = |label, filter| {
checkbox(
label,
task_list.filter == filter,
move |state: &mut TaskList, _| state.filter = filter,
)
};
用闭包快速生成筛选单选复选框;选中对应Filter,切换全局过滤规则。
5. 条件渲染 footer
rust
let has_tasks = !task_list.tasks.is_empty();
let footer = has_tasks.then(|| {
flex_row((
filter_tasks("All", Filter::All),
filter_tasks("Active", Filter::Active),
filter_tasks("Completed", Filter::Completed),
))
.main_axis_alignment(MainAxisAlignment::Center)
});
bool.then() :只有存在任务的时候,才渲染底部筛选栏;无任务自动隐藏footer。
6. 外层 flex_col 整体布局与分割线
rust
flex_col((
header_text,
FlexSpacer::Fixed(DEFAULT_GAP),
input_line,
divider(Axis::Horizontal, label("Tasks")),
tasks,
FlexSpacer::Fixed(3.px()),
divider_h()
.thickness(2.px())
.dash_fit(DashFit::Stretch)
.dash_pattern(&[5.px(), 5.px()]),
FlexSpacer::Fixed(3.px()),
footer,
))
.gap(Length::px(4.))
.padding(50.px())
- divider :带文字标题的分割线
- divider_h :纯水平虚线, dash_pattern 定义虚线样式
7. 数据流
7.1 初始加载预设3条任务
7.2 用户输入文本,实时更新 next_task
7.3 点击Add task / Enter,调用 add_task 追加任务
7.4 勾选复选框:修改 task.done
7.5 Delete按钮:按索引删除任务
7.6 切换筛选选项:修改 filter ,触发列表重新过滤渲染
7.7 状态变更 → app_logic重新执行,生成新视图树,Xilem diff更新UI
四、本课核心知识点总结
- MVC架构在Xilem的实践:数据、视图、事件回调分离。
- 列表动态渲染: iter().enumerate().filter_map() 根据数据动态生成视图数组,是Xilem渲染列表的标准写法。
- 条件视图渲染: bool.then() ,条件成立才渲染组件。
- text_input文本输入控件,支持placeholder、回车事件、shift+enter换行。
- 原生控件: checkbox 复选框、 divider 分割线,支持虚线样式。
- std::mem::take :转移String所有权,高效清空输入框,减少内存拷贝。
- 闭包封装UI组件: filter_tasks ,快速批量生成同类型组件。
五、课后习题
练习1 填空
- 这个示例采用____架构,数据、视图、事件控制器分离。
- 动态筛选列表使用迭代器的____方法,满足条件返回视图,不满足返回None不渲染。
- bool.then() 的作用是________。
- std::mem::take 的作用是转移字符串____,避免clone。
参考答案
- MVC
- filter_map
- 布尔为true时才渲染视图,false不渲染
- 所有权
练习2 判断
- filter_map返回None时,不会渲染该条目的UI。(✅正确)
- 没有任务的时候,底部筛选footer仍然会渲染。(❌错误)
- text_input默认回车直接提交任务,Shift+Enter换行。(✅正确)
- 任务删除依靠任务名称查找,不需要索引。(❌错误)
练习3 简答
题目:Xilem渲染动态列表,为什么推荐filter_map而不是简单map?
参考答案
map会为每一条数据强制生成视图;filter_map可以一边遍历一边做条件判断,不满足条件直接返回None,跳过该条视图创建,非常适合列表筛选场景,代码简洁。