3 个版本
0.1.3 | 2023 年 7 月 24 日 |
---|---|
0.1.2 | 2023 年 7 月 24 日 |
0.1.1 | 2023 年 7 月 23 日 |
0.1.0 |
|
#355 在 GUI 中
18KB
275 行
gflux
gflux 是一个为 Rust 设计的小型实验性响应式组件系统,旨在使 GTK 更易于管理。
gflux
- 大约有 300 行代码
- 不包含宏
- 不依赖于任何特定的 GUI 库
- 跟踪哪些组件已对其模型进行了修改
- 与任何模型差异代码正交,后者用于优化更新
为什么 Rust 中的 GTK 难以使用
让我们看看一个 GTK 按钮的例子。您可以使用此方法注册一个回调来处理按钮点击
fn connect_clicked<F: Fn(&Self) + 'static>(&self, f: F) -> SignalHandlerId
'static
生命周期限制意味着提供的回调函数只能捕获静态引用。这很有意义,因为 GTK 按钮是一个引用计数对象,可能存在于当前的堆栈帧之外。但 我认为这是 Rust 中 GTK 难以使用的最大原因。
通常有三种方法来处理这种情况
- 将您的应用程序状态包裹在
Rc<RefCell<T>>
中。这适用于您的应用程序状态简单的情况。对于复杂的应用程序,您不希望每个小部件都意识到您整个应用程序状态。这意味着您最终会在应用程序状态的所有地方都使用Rc<RefCell<T>>
。这使得应用程序变得难以管理。 - 向队列发送消息。许多现有的 Rust GTK 组件框架选择这种方法。这可行。但有时,您可能想要提供一个基于其返回值的回调来阻止操作。例如,GTK 中窗口的删除事件可以返回 true 或 false。发送消息需要在不访问您的应用程序状态的情况下选择返回值。
- 创建自定义小部件,并将您的应用程序状态放在其中。这是 GTK 的方式。如果不在乎将应用程序状态放在面向对象的组件中而不是维护分离,这可行。
gflux 组件
gflux 通过构建组件树工作。每个组件在创建时都会获得一个“透镜”函数。从每个组件的透镜函数链一起工作,始终能够从全局应用程序状态到个别组件关心的状态。
当创建组件时,会提供一个透镜函数。但首先,让我们看看一个简单的任务组件示例。
use crate::{AppState, Task};
use glib::clone;
use gtk::{prelude::*, Align};
use gflux::{Component, ComponentCtx};
pub struct TaskComponent {
hbox: gtk::Box,
label: gtk::Label,
}
impl Component for TaskComponent {
type GlobalModel = AppState;
type Model = Task;
type Widget = gtk::Box;
type Params = ();
// The root widget
fn widget(&self) -> Self::Widget {
self.hbox.clone()
}
// Called when the component is constructed
fn build(ctx: ComponentCtx<Self>, params: ()) -> Self {
let checkbox = gtk::CheckButton::new();
checkbox.connect_toggled(clone!(@strong ctx => move |cb| {
ctx.with_model_mut(|task| task.done = cb.is_active());
}));
let label = gtk::Label::new(None);
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 8);
hbox.append(&checkbox);
hbox.append(&label);
// rebuild will be called immediately afterwards
Self { hbox, label }
}
// Called after a change in state is detected
fn rebuild(&mut self, ctx: ComponentCtx<Self>) {
let name = ctx.with_model(|task| task.name.clone());
if ctx.with_model(|task| task.done) {
// If the task is done, make it strikethrough
let markup = format!("<s>{}</s>", glib::markup_escape_text(&name));
self.label.set_markup(&markup);
} else {
self.label.set_text(&name);
}
}
}
当调用 rebuild
时,组件需要确保小部件与组件模型中的 Task
结构相匹配。
ComponentCtx<Self>
会为你处理所有组件的账目工作。最重要的是,它提供了对组件模型的访问,这是一个简单的 Task
结构体。它为此提供了两种方法:with_model
和 with_model_mut
。
with_model_mut
将组件标记为脏,以便随后尽快调用重建。
初始化组件树
// Create the global application state
let global = Rc::new(RefCell::new(AppState { ... }));
// Create the root of the component tree
let mut ctree = ComponentTree::new(global);
创建组件
给定一个 "透镜" 函数和参数,你可以创建新的组件。在 ComponentCtx 上也存在类似的函数来创建现有组件的子组件。
let task_comp: ComponentHandle<TaskComponent> = ctree.new_component(|app_state| app_state.get_task_mut(), ());
变更跟踪
组件树提供了两种方法
on_first_change
来注册一个回调,每当组件树从完全干净的状态变为脏状态时都会调用。rebuild_changed
来调用所有被with_model_mut
标记为脏的组件及其所有祖先组件的rebuild
方法,从最旧的到最新的。
使用这两种方法,我们可以注册 GTK 主循环,以始终重建任何模型已更改的组件
// When the tree first moves from clean to dirty, use `idle_add_local_once`
// to make sure that `ctree.rebuild_changed()` later gets called from the gtk
// main loop
ctree.on_first_change(clone!(@strong ctree => move || {
glib::source::idle_add_local_once(clone!(@strong ctree => move || ctree.rebuild_changed()));
}));
享受愉快的指导原则
- 创建组件返回一个 ComponentHandle。如果你的组件仍然存在,请保持这些对象存活。
- 对
with_model
和with_model_mut
的调用应保持简短,并且不应在其中调用任何 GTK 函数。在调用 GTK 函数之前,请复制模型的部分内容。 - 避免调用递归调用主循环的 GTK 函数,例如
dialog.run()