#hook #egui #gamedev

egui_hooks

类似 React Hooks 的 API,用于增强 egui 的易用性

6 个版本 (3 个重大变更)

0.4.0 2024年5月26日
0.3.0 2024年5月26日
0.2.0 2024年5月26日
0.1.2 2024年1月8日

#135 in GUI

MIT/Apache

67KB
1.5K SLoC

egui_hooks

GitHub MIT/Apache 2.0 Crates.io docs.rs

类似 React Hooks 的 API,用于增强 egui::Memory 的易用性

egui 版本 egui_hooks 版本
[email protected] 0.1.2
[email protected] 0.2.0
[email protected] 0.3.0
[email protected] 0.4.0

概述

此 crate 提供了类似 React Hooks 的 API,用于 egui。

尽管这个项目最初是一个玩具项目,但我最终发现它非常有用,它可以成为部件开发的基石,也可以用于应用开发。

查看示例,运行 cargo run --example <example-name> 以查看演示。

特性

  • 无资源泄露:与直接使用 egui::Memory 相反,当部件不再显示时,状态会自动从 HashMap 中释放。这是基于此 crate 中定义的 TwoFrameMap (2f kv)。
  • 无锁定或回调:您可以在不使用 ui.data(|| { ... }) 的情况下管理状态。这是因为 hooks 封装了底层的 RwLock 操作。
  • 依赖跟踪:Hooks 有依赖项,如 use_state(|| user_id.clone(), user_id)use_effect(|| log(input), input),因此您可以精确跟踪依赖关系,而无需手动编写状态更改时的 if 语句。
  • 可组合:Hooks 是可组合的,您可以在自定义 Hooks 中调用现有 Hooks。
  • 熟悉API:钩子设计得与React Hooks API相似,因此您可以轻松学习如何使用它们。在最近的UI开发场景中,管理UI状态是关键,但内置的egui::Memory是一个相对底层的API,对应用程序开发不够友好,而egui_hooks提供了一个更高级的API,但控制更加精确。

工作原理

如果您在一个小部件中使用use_state(|| 0usize, dep).into_var(),以下事情会发生

  1. 在第一次调用use_state时,它会在egui::Memory中创建一个具有默认值的Arc<ArcSwap<usize>>
  2. 如果自上一帧以来dep已更改,则将默认值存储到现有的ArcSwap中。
  3. 返回一个Var<usize>给调用者。
  4. 调用者可以在其小部件代码中DerefDerefMut Var
  5. Var被丢弃时,它将更新后的值存储到ArcSwap中。
  6. 当小部件不再显示时,从egui::Memory中删除ArcSwap

这是egui_hooks中钩子的典型生命周期。

此外,还有一个持久版本的use_state,称为use_persisted_state。它执行类似的事情,但它使用持久化方法将状态的副本存储到egui::Memory中。与未持久化的一样,当小部件不再显示时,持久化状态会被释放。您需要persistence功能才能使用持久化钩子。

预期用例

  • use_state用于特定小部件的状态(例如动画状态、滚动位置)
  • use_stateinto_var()一起使用,将变量就地提供给Window::openTextEdit::singleline
  • use_memouse_cache用于缓存昂贵的计算
  • use_effectuse_future用于副作用(例如日志记录、网络请求)
  • use_global用于全局设置(例如主题、区域设置)
  • use_kv用于在多个小部件之间共享状态(例如获取特定小部件的位置)
  • use_ephemeral_kv用于在当前帧中存储事件(例如在自定义小部件上提供自定义响应)
  • use_previous_measurement用于使用上一帧的结果进行布局
  • use_measurement用于计算和缓存小部件的大小以进行布局

状态

  • use_memo
  • use_effect
  • use_effect_with_cleanup
  • use_stateuse_persisted_state
  • state.into_var() 将状态用作变量
  • use_kvuse_persisted_kv
  • use_2f_kvuse_persisted_2f_kv
  • 使用临时kv
  • use_globaluse_persisted_global,和 use_ephemeral_global
  • use_cache(egui::Memory 中缓存的轻量级包装器)
  • 使用之前的测量
  • use_measurement(无需担心2^N 问题计算小部件的大小。)
  • use_future(需要 tokio 功能)
  • use_throttleuse_debounce
  • 使用拖拽起始点
  • use_two_path(这是一个玩笑,但真的想实现这个功能)

用法

  1. 使用状态
// You can reset the initial state by changing the dependency part.
let count = ui.use_state(|| 0usize, ());
ui.label(format!("Count: {}", count));
if ui.button("Increment").clicked() {
    count.set_next(*count + 1);
}
  1. use_memo
let count = ui.use_state(|| 0usize, ());
let memo = ui.use_memo(
    || {
        println!("Calculating memoized value");
        count.pow(2)
    },
    count.clone(),
);
ui.label(format!("Memo: {}", memo));
if ui.button("Increment").clicked() {
    count.set_next(*count + 1);
}

自定义钩子

您可以通过两种方式创建自己的钩子。

  1. 为钩子创建一个函数

这是创建自定义钩子的最简单和推荐的方式。

fn use_search(ui: &mut Ui, backend: Backend) -> Option<SearchResults> {
    let text = ui.use_state(|| String::default(), ()).into_var();
    ui.text_edit_singleline(&mut *name);
    ui.use_future(async {
        backend.search(name.get()).await
    }, name.state())
}
  1. 实现 Hook 特性

所有内置钩子都是通过这种方式实现的。这允许您创建一个具有完全控制的钩子,但这种方式有点冗长。

impl<D> Hook<D> for MyHook {
    type Backend = ()
    type Output = usize;

    fn init(
        &mut self,
        _index: usize,
        _deps: &D,
        _backend: Option<Self::Backend>,
        _ui: &mut egui::Ui,
    ) -> Self::Backend {
    }

    fn hook(self, backend: &mut Self::Backend, ui: &mut egui::Ui) -> Self::Output {
        let count = ui.use_state(0usize, ());
        ui.label(format!("Count: {}", count));
        if ui.button("Increment").clicked() {
            count.set_next(*count + 1);
        }
        count
    }
}

依赖关系

~4.5–10MB
~87K SLoC