#gtk #component #reactive #ui

gflux

gflux 是一个为 Rust 设计的小型实验性响应式组件系统,旨在使 GTK 更易于管理

3 个版本

0.1.3 2023 年 7 月 24 日
0.1.2 2023 年 7 月 24 日
0.1.1 2023 年 7 月 23 日
0.1.0 2023 年 7 月 23 日

#355GUI

MIT 许可证

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_modelwith_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_modelwith_model_mut 的调用应保持简短,并且不应在其中调用任何 GTK 函数。在调用 GTK 函数之前,请复制模型的部分内容。
  • 避免调用递归调用主循环的 GTK 函数,例如 dialog.run()

灵感

无运行时依赖