本文记录学习 IcedRust 的声明式 GUI 框架第一天的收获主题是 pane_grid 的交互流程。贯穿全文的一条主线UI 不是被直接操作的而是由 State 驱动、在每次交互后重新生成的。按「三个概念 → 布局模型 → UI 生成 → 交互流程 → 总结」的顺序展开主要回答这几个问题文章里到底有几个 Pane—— 三个都叫 Pane 的概念怎么区分一个 StatePane 为什么能管理多个 Pane—— 布局树如何组织拖拽分隔条后发生了什么—— ResizeEvent 如何一步步传到 update()view() 里的闭包为什么执行多次—— 每个 Pane 都会被调用一次。一、分清三个 Pane刚开始学 pane_grid最大的坑就是示例代码里出现了三个都叫 Pane 的东西写法相似、职责完全不同。① 你自己的 enum Pane —— 业务数据这个 Pane 是什么 ② pane_grid::Pane —— ID / 句柄它是第几个 Pane ③ StatePane —— 布局状态整个布局长什么样1.1 你自己的 enum Pane —— 业务数据最开始容易误解enum Pane { MainPane, SidePane, }它并不是一个真正的 GUI 控件它只是每个 Pane 中保存的业务数据类型。例如enum Pane { Editor, Terminal, FileExplorer, }那么它表达的是这个 Pane 是 Editor 这个 Pane 是 Terminal 这个 Pane 是 FileExplorer它本身不负责resizelayoutsplitrendering因此 Pane::MainPane 没有 resize() 方法完全正常——它只是一份数据不是控件。1.2 pane_grid::Pane —— ID句柄这是 Iced 内部用来标识布局树中某个 Pane 的 ID。它的实现非常简单pub struct Pane(u64); // 内部就是一个编号它不保存任何业务数据只回答一个问题你是第几个 Pane 比如第一个 Pane 的 ID 就是 0Pane(0)。之所以要把 ID 交给你是因为之后所有操作都要凭 ID 指定操作哪个 Pane// 在 main 上切一刀生成 SidePane let (split, side_pane) panes.split( pane_grid::Axis::Vertical, main, // ← 传入 ID Pane::SidePane, // ← 新 Pane 的业务数据 );1.3 pane_grid::StatePane —— 布局状态真正负责维护 PaneGrid 布局的是 pane_grid::StatePane可以把它理解成 PaneGrid 的状态模型 / 布局树。例如StatePane │ ▼ Split / \ Pane Pane │ │ Main Side它维护的信息包括当前有哪些 PanePane 之间如何组织哪些 Pane 被 SplitSplit 的方向Split 的 ratioPane 的布局关系Pane 的 ID所以可以粗略类比StatePane ≈ 控件的内部状态 布局模型但不要严格把它当成实际绘制出来的控件真正产生 UI 的是 pane_grid(self.panes, ...)。1.4 源码实锤State::new 里两个 Pane 同时出现如果还分不清前两个 Pane直接看 State::new 的完整源码就清楚了pub fn new(first_pane_state: T) - (Self, Pane) { ( Self::with_configuration(Configuration::Pane(first_pane_state)), Pane(0), ) }这个函数里同时出现了两个不同的 Panefirst_pane_state: T —— 参数就是你的 enum Pane业务数据会被存进布局树的叶子节点 Configuration::Pane(...) 里Pane(0) —— 返回值是 pane_grid::PaneIced 内部的 ID / 句柄0 只是编号不携带任何业务数据所以写 State::new(Pane::MainPane) 时两个 Pane 写法相同、含义却不同State::new(Pane::MainPane) │ │ │ └── ① 你的 enum Pane业务数据→ 存进 State 内部 │ └── 返回 Pane(0) → ② pane_grid::PaneID→ 留给你以后操作它用而 let (mut panes, main) State::new(Pane::MainPane) 中panes 是布局状态main 是 ID。一句话State::new 收的是数据你的 enum还给你的却是IDpane_grid::Pane。1.5 记忆口诀你的 Pane ↓ 我是谁 pane_grid::Pane ↓ 我的 ID 是什么 StatePane ↓ 整个布局是什么样二、布局模型State、Split 与布局树2.1 为什么一个 StatePane 能拥有多个 Pane刚开始let (panes, _) pane_grid::State::new(Pane::MainPane);得到State └── MainPane因此只有一个 Pane。如果想增加第二个 Panelet (mut panes, main) pane_grid::State::new(Pane::MainPane); panes.split( pane_grid::Axis::Vertical, main, Pane::SidePane, );结果State │ Split / \ / \ MainPane SidePane视觉上┌────────────────────┬────────────────────┐ │ │ │ │ Main │ Side │ │ │ │ └────────────────────┴────────────────────┘所以不是StatePane StatePane而是StatePane一个 State 内部维护多个 Pane。2.2 Split 是什么—— 理解 resize 的关键这是理解 resize 的关键Pane 本身不能 resize。 真正被拖动的是两个 Pane 之间的 Split。例如┌──────────────────┬──────────────────┐ │ │ │ │ Pane A │ Pane B │ │ │ │ └──────────────────┴──────────────────┘ ↑ Split用户拖动←────→ ┌──────────────────┼──────────────────┐ ↑ Split改变的是 Split 的位置。例如 ratio 0.5 变成 50% | 50%拖动之后 ratio 0.3 变成 30% | 70%。因此self.panes.resize( event.split, event.ratio, );真正的含义是修改某个 Split 的 ratio而不是找到某个 Pane然后调用 Pane 的 resize。2.3 pane_grid 的布局本质上是一棵树例如三个 PaneSplit / \ Main Split / \ Editor Terminal对应 UI┌────────────────────┬────────────────────┐ │ │ │ │ │ Editor │ │ Main │ │ │ ├────────────────────┤ │ │ │ │ │ Terminal │ │ │ │ └────────────────────┴────────────────────┘所以 StatePane 更准确地说是维护了一棵 Pane/Split 布局树。这也是为什么 resize() 属于 State——因为Pane ↓ 只知道自己是什么 State ↓ 知道 Pane 之间怎么组织 ↓ 知道 Split 在哪里 ↓ 知道 Split 的 ratio三、view()状态如何生成 UI3.1 闭包为什么会执行多次代码fn view(self) - Element_, Message { pane_grid(self.panes, |_pane, state, _is_maximized| { pane_grid::Content::new( match state { Pane::MainPane text(Main), Pane::SidePane text(Side), } ) }) .into() }这里的闭包不是只处理一个 Pane而是 PaneGrid 遍历当前 State 中的每一个 Pane 时分别调用这个闭包。例如State ├── MainPane └── SidePane那么大致可以理解成pane_grid(...) │ ├── 调用闭包(MainPane) │ ↓ │ Content(Main) │ └── 调用闭包(SidePane) ↓ Content(Side)最终 PaneGrid 根据布局树同时显示两个 Pane。所以后一个 Pane 不会覆盖前一个 Pane因为闭包是针对每一个 Pane 分别生成 Content。3.2 三个参数pane / state / is_maximizedpane_grid(self.panes, |pane, state, is_maximized| {三个参数可以理解成pane ↓ Iced 内部的 Pane ID② pane_grid::Pane state ↓ 你放进 State 的 Pane 数据① 你的 enum Pane is_maximized ↓ 当前 Pane 是否最大化例如Pane ID 1 → Pane::MainPane Pane ID 2 → Pane::SidePane遍历时第一次 pane Pane ID 1 state Pane::MainPane 第二次 pane Pane ID 2 state Pane::SidePane所以match state { Pane::MainPane text(Main), Pane::SidePane text(Side), }是在决定当前这个 Pane 应该显示什么内容。四、Resize 交互流程4.1 完整流程从鼠标到 Message首先pane_grid(...) .on_resize(10, Message::PaneResized)这里告诉 PaneGrid如果用户拖动 Split就把 resize 事件转换成 Message::PaneResized。用户拖动分隔条后PaneGrid 内部检测到哪个 Split新的 ratio 是多少然后生成ResizeEvent { split: ..., ratio: 0.35, }再通过 Message::PaneResized(event) 交给应用。4.2 ResizeEvent 是谁创建的不是用户创建也不是我们创建是 PaneGrid 内部产生的。我们只声明enum Message { PaneResized(pane_grid::ResizeEvent), }意思是我的应用允许收到一个携带 ResizeEvent 的 Message。然后.on_resize(10, Message::PaneResized)相当于告诉 Iced当你产生 ResizeEvent 时请使用 Message::PaneResized 把它包装成我的 Message。因此PaneGrid │ │ 创建 ▼ ResizeEvent │ │ 包装 ▼ Message::PaneResized(event) │ ▼ update(message)4.3 event 为什么可以直接出现在 match 里这是 Rust 模式匹配match message { Message::PaneResized(event) { ... } }这里的 event 只是把 Message::PaneResized 中携带的值绑定到一个局部变量。类似enum Message { Hello(String), } match message { Message::Hello(value) { println!({}, value); } }所以 Message::PaneResized(event) 中的 event 就是 pane_grid::ResizeEvent。4.4 update() 最终修改 State收到 Message::PaneResized(event) 之后match message { Message::PaneResized(event) { self.panes.resize( event.split, event.ratio, ); } }event.split 告诉 State 修改哪一个 Splitevent.ratio 告诉 State 修改成什么比例。例如原来 ┌──────────────┼──────────────────────┐ 30% 70% 拖动后 ┌────────────────────┼────────────────┐ 60% 40%本质上就是Split ratio: 0.3 ↓ Split ratio: 0.64.5 我自己的理解对照修正我当时是这样理解的当我用鼠标触发 resize 事件pane_grid 会自动生成一个 Message::ResizePane(event)其中 event 是具体的 resize 数据然后系统将这个 msg 推送给 updateupdate 通过 self.panes 找到当前的 pane并调用他的 resize 方法调整实际的 pane 大小。这个理解大方向对但有三处细节要修正我的原话更准确的说法自动生成一个 Message::ResizePane(event)生成的是 ResizeEvent再经由 .on_resize(10, Message::PaneResized) 这个回调映射成 Message::PaneResized(event)注意是 PaneResized不是 ResizePane系统将这个 msg 推送给 update是 Iced 的事件循环把 Message 交给 update()Elm 架构的一部分不是系统自动update 通过 self.panes 找到当前的 pane并调用他的 resize 方法self.panes 里没有当前的 Pane——resize() 收到的是 event.split哪个 Split event.ratio什么比例修改的是 Split 的 ratio而不是某个 Pane 的 resize 方法Pane 本身没有 resize最后一点恰恰是第 2 章强调过的拖的不是 Pane是 Split改的不是 Pane是 ratio。完整链条修正版鼠标拖动 Split ↓ PaneGrid 内部检测 → 生成 ResizeEvent { split, ratio } ↓ .on_resize(10, Message::PaneResized) 包装成 Message ↓ Iced 事件循环 → update(Message::PaneResized(event)) ↓ self.panes.resize(event.split, event.ratio) // 修改 Split 的 ratio ↓ State 变化 → view() 重新生成 UI4.6 为什么修改 State 后 UI 会变化这是 Iced 的核心思想State ↓ View(State) ↓ UI所以用户操作 ↓ Message ↓ update() ↓ 修改 State ↓ view() ↓ PaneGrid 重新根据 State 构建 UI例如 ratio 0.3┌───────────┬─────────────────────────┐ │ Main │ Side │ │ 30% │ 70% │ └───────────┴─────────────────────────┘修改为 ratio 0.6┌────────────────────┬────────────────┐ │ Main │ Side │ │ 60% │ 40% │ └────────────────────┴────────────────┘所以我们没有直接操作屏幕上的 Pane而是修改状态 → 根据新状态重新生成 UI。五、总结5.1 最终关系图这是今天最值得保存的一张图┌───────────────┐ │ User │ │ 鼠标拖 Split │ └───────┬───────┘ │ ▼ ┌───────────────┐ │ PaneGrid │ │ 捕获鼠标事件 │ └───────┬───────┘ │ │ 创建 ▼ ┌───────────────┐ │ ResizeEvent │ │ │ │ split │ │ ratio │ └───────┬───────┘ │ │ ▼ Message::PaneResized(event) │ ▼ ┌───────────────┐ │ update() │ └───────┬───────┘ │ │ 修改 ▼ ┌──────────────────────┐ │ pane_grid::StatePane│ │ │ │ Split │ │ / \ │ │ Pane Pane │ │ │ │ │ │ Main Side │ │ │ │ ratio 0.3 → 0.6 │ └──────────┬───────────┘ │ │ ▼ view() │ ▼ ┌──────────────────────┐ │ pane_grid() │ │ │ │ 遍历每一个 Pane │ │ ↓ │ │ 多次调用闭包 │ └──────────┬───────────┘ │ ┌──────────┴──────────┐ ▼ ▼ Content(Main) Content(Side) │ │ └──────────┬──────────┘ ▼ Renderer │ ▼ 屏幕 UI5.2 一句话记忆如果以后忘了可以只记这句话Pane 决定显示什么StatePane 决定怎么布局Split 决定怎么分割ResizeEvent 描述用户拖成什么样Message 把事件带进应用update() 修改 Stateview() 再根据新的 State 生成 UI。最终形成用户 ↓ Event ↓ Message ↓ update ↓ State ↓ view ↓ UI这就是今天通过 pane_grid 实际理解到的 Iced 的状态驱动 GUI 模型。