【xilem0.4基础语法学与练】第53课 to_do_mvc 官方示例代码深度解析

前言

源码来源: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,切换全局过滤规则。

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

四、本课核心知识点总结

  1. MVC架构在Xilem的实践:数据、视图、事件回调分离。
  2. 列表动态渲染: iter().enumerate().filter_map() 根据数据动态生成视图数组,是Xilem渲染列表的标准写法。
  3. 条件视图渲染: bool.then() ,条件成立才渲染组件。
  4. text_input文本输入控件,支持placeholder、回车事件、shift+enter换行。
  5. 原生控件: checkbox 复选框、 divider 分割线,支持虚线样式。
  6. std::mem::take :转移String所有权,高效清空输入框,减少内存拷贝。
  7. 闭包封装UI组件: filter_tasks ,快速批量生成同类型组件。

五、课后习题

练习1 填空

  1. 这个示例采用____架构,数据、视图、事件控制器分离。
  2. 动态筛选列表使用迭代器的____方法,满足条件返回视图,不满足返回None不渲染。
  3. bool.then() 的作用是________。
  4. std::mem::take 的作用是转移字符串____,避免clone。

参考答案

  1. MVC
  2. filter_map
  3. 布尔为true时才渲染视图,false不渲染
  4. 所有权

练习2 判断

  1. filter_map返回None时,不会渲染该条目的UI。(✅正确)
  2. 没有任务的时候,底部筛选footer仍然会渲染。(❌错误)
  3. text_input默认回车直接提交任务,Shift+Enter换行。(✅正确)
  4. 任务删除依靠任务名称查找,不需要索引。(❌错误)

练习3 简答

题目:Xilem渲染动态列表,为什么推荐filter_map而不是简单map?

参考答案

map会为每一条数据强制生成视图;filter_map可以一边遍历一边做条件判断,不满足条件直接返回None,跳过该条视图创建,非常适合列表筛选场景,代码简洁。

相关推荐
志尊宝2 小时前
Vue3 零基础每日笔记(021):class 与 style 动态绑定——对象、数组、三元表达式写法汇总
前端·vue.js·笔记·前端框架·html5
孙启超2 小时前
【AI开发之Rust】第 5 课:引用与生命周期 —— 借用能活多久?
人工智能·后端·rust·llm·ai应用开发
z23456d2 小时前
第四十三天学习心得
学习
彧azz2 小时前
Java学习语法篇:键盘录入
java·笔记·学习
喵喵锤锤你小可爱2 小时前
SerialHub:把串口变成 WebSocket 字节管道,浏览器和脚本直接读写(开源工具 SerialHub 实战)
websocket·rust·嵌入式·串口调试·开源工具
dadaobusi3 小时前
解码缓存的用途
学习
艾莉丝努力练剑3 小时前
【AI大模型接入SDK】LLMManager类架构与智能指针选型
网络·c++·人工智能·学习·面试·架构
传奇开心果编程4 小时前
【xilem0.4基础语法学与练】第48课 lists组件官方示例代码深度解析
学习·rust·前端框架
传奇开心果编程7 小时前
【Rust入门知识点学与练】第24课:Trait 基础
开发语言·学习·rust