5个版本

0.2.0 2024年3月9日
0.1.3 2024年2月11日
0.1.2 2023年8月15日
0.1.1 2023年8月9日
0.1.0 2023年8月8日

#92 in 科学

Download history 23/week @ 2024-04-02

每月 124次下载

ISC许可

48KB
899

Lunk是一个事件图处理库。

预期用例是用户界面,其中用户事件(点击、输入)可以触发其他更改和计算的级联。具体来说,是WASM/web用户界面(因为它单线程),但也合理地可以与其他单线程UI库(如Gtk)一起使用。

大多数Web UI框架都包括自己的事件图处理工具:Sycamore有信号,Dioxus和其他有类似功能。这是一个独立工具,可以在没有框架的情况下使用(即与Gloo和其他按需工具一起使用)。

与其他工具相比,这个库侧重于易用性、灵活性和与常用语言结构的组合,而不是性能(尽管它看起来仍然相当快)。

  • 你可以表示完整的图,包括循环
  • 数据具有简单、无生命周期的类型(例如Prim<i32>List<i32>等),这使得它在结构中传递和存储变得简单。
  • 在图处理过程中处理图修改 - 新节点将在其依赖项之后处理
  • 动画/缓动属性更改

目前只处理同步图处理。不处理异步事件。

状态:MVP

用法

基本用法

  1. 创建一个EventGraph eg
  2. 调用eg.event(|pc| { })进行设置。
  3. 在事件上下文中,使用 lunk::Prim::new()lunk::List::new() 创建值。使用 lunk::link!() 创建链接以处理输入变化。
  4. 当用户输入发生时,调用 eg.event 并使用相关方法修改数据值。

示例

fn main() {
    let eg = lunk::EventGraph::new();
    let (_input_a, _input_b, output, _link) = eg.event(|pc| {
        let input_a = lunk::Prim::new(pc, 0i32);
        let input_b = lunk::Prim::new(pc, 1f32);
        let output = lunk::Prim::new(pc, 0f32);
        let _link =
            lunk::link!(
                (ctx = pc),
                (input_a = input_a.clone(), input_b = input_b.clone()),
                (output = output.clone()),
                () {
                    output.set(ctx, input_a.get() as f32 * input_b.get() + 5.);
                }
            );
        input_a.set(pc, 46);
        return (input_a, input_b, output, _link);
    });
    assert_eq!(output.get(), 51.);
}

a 被修改时,b 将被更新为 a 的值加 5。

有关详细解释,请参阅 link! 文档。链接可以手动(无需宏)定义,但有一些样板代码。

所有权

值对依赖链接有弱引用,不对写入它们的链接有引用。这意味着在图形结构本身中没有 rc 泄漏循环。

链接必须在您希望它们触发的持续时间内保持活动状态。如果删除链接,它将在事件期间停止激活。

动画

要动画化原始值

  1. 创建一个 Animator

    由于您可能希望传递它,将其创建为 Rc<RefCell<Animator>> 是一个不错的解决方案。

  2. 在原始值上调用 set_ease 而不是 set

    set_ease 需要一个缓动函数 - ezing 提供了一套相当完整的缓动函数。

    my_prim.set_ease(&mut animator, 44.3, 0.3, ezing::linear_inout);
    

    set_ease 应该为任何实现 Mult<f32> 和其自身类型的 AddSubPrim 自动实现。

  3. 定期(至少直到所有动画完成)在 Animator 上调用 update()

    animator.update(&eg, delta_s);
    

    此步骤更新动画(delta_s 是上次更新以来的秒数)并返回 true 如果仍有正在进行的动画。

除了 set_ease 之外,您还可以通过实现 PrimAnimation 并调用 animator.start(MyPrimAnimation{...}) 来创建自定义动画。

网络上的惯用法

这有点棘手,但我认为在 WASM 中使用 request_animation_frame(通过 Gloo)的惯用方法是当前这样。

let eg = lunk::EventGraph::new();
let anim = Rc::new(RefCell::new(Animator::new()));
anim.as_ref().borrow_mut().set_start_cb({
    let anim = anim.clone();
    let eg = eg.clone();
    let running = Rc::new(RefCell::new(None));

    fn one_more_frame(
        running: Rc<RefCell<Option<AnimationFrame>>>,
        eg: EventGraph,
        anim: Rc<RefCell<Animator>>,
    ) {
        *running.borrow_mut() = Some(request_animation_frame({
            let running = running.clone();
            move |delta| {
                if anim.as_ref().borrow_mut().update(&eg, delta) {
                    one_more_frame(running, eg, anim);
                } else {
                    *running.borrow_mut() = None;
                }
            }
        }));
    }

    move || {
        if running.borrow().is_some() {
            return;
        }
        one_more_frame(running.clone(), eg.clone(), anim.clone());
    }
});

基本上,需要递归调用 request_animation_frame 来开始下一帧,但每个的返回值都需要存储,直到实际调用。

为此,我创建了一个共享的 Option 来存储返回值,该值由 anim 所拥有的回调保持活动状态。

故障排除

类型不匹配;预期 fn 指针

向下阅读时,应显示类似“如果闭包没有捕获任何变量,则只能强制转换为 fn 类型”的内容。这不是关于错误类型的问题,而是关于隐式捕获。

使用 link! 创建的链接中所有的捕获都需要位于起始的 () 之一。

在 VS Code 中,我需要点击链接才能看到完整的错误信息,在此之前它不会显示哪个值被隐式捕获。

不可达语句

这发生在您的回调中有一个无条件 return 时。为了支持短路,通过 ? 返回 Optionlink! 在函数体末尾添加一个 return None。如果您也返回,这个 return None 就变成了不可达的,因此会有警告。

我的回调没有触发

可能的原因

  • 回调链接被丢弃,或者被弱引用捕获的输入/输出值被丢弃,或者在该节点路径中的某个地方被丢弃了。

    前向引用是弱引用,所以每个依赖链接对象需要保留,直到回调相关。

  • 您设置了值,但 PartialEq 判断它等于当前值

    如果值相同,则不会发生更新。这是为了防止由于大量更改而消除未修改的路径时产生不必要的操作,但如果您实现了 PartialEq 不精确,它可能会阻止合法事件被处理。

  • 您将捕获组搞混了,输入被解释为其他东西(输出、其他捕获)。

    如果您在错误的宏 KV 组中输入,它们将不会被识别为图连接,因此对依赖项的更改不会触发回调。

我的回调正在触发(泄露)

可能的原因

  • 回调捕获了拥有它的项。例如,您执行了 link! 并捕获了一个由 link! 修改的 HTML 元素。您应该将捕获更改为弱引用。

为什么优先考虑灵活性而不是性能

这些库的主要好处来自帮助您避免昂贵的操作。越灵活,它能帮助您避免的工作就越多。

对于它无法避免的工作,它仍然很快:它是用 Rust 编写的,与 FFI 调用、样式、布局和 Web 环境中的渲染相比,图处理的时间微不足道。

尽管性能不是重点,但它实际上非常快!我尝试将其集成到 https://github.com/krausest/js-framework-benchmark 中,并得到了良好的性能:746ms 比 Sycamore 的 796ms 更快(!)

(我并不完全相信这比 Sycamore 快:)

  • 基准测试中可能存在一些随机性,毕竞它是一个浏览器。
  • 其中一些差异可能是由非事件图的东西引起的,比如元素创建(直接 rooting 操作与 Sycamore 的 JSX 类系统相比)。

设计决策

您通常传递数据,以便其他系统可以附加自己的监听器。如果值是图计算的结果,它需要包含计算所需的全部输入的引用,这在一般情况下意味着您需要为每个计算创建一个新类型。通过将数据与链接分开,数据类型可以保持简单,而复杂性则保持在链接中。

这还支持许多只有少量函数/宏的配置:手动触发而不是计算得出的值、从其他值计算得出的值,以及不输出值的计算。

在宏中要求用户指定输出类型

宏缺乏在需要的地方检测输出类型的能力。

我本可以使用模板魔法来推断类型,但这样会让没有宏的手动链接实现需要更多的样板代码,所以我决定不这样做。类型相当简单,所以我认为这不是一个很大的缺点。

作为单独的结构进行动画

在真正的点菜式哲学中,我认为有些人可能不想使用动画,而且将其完全分开并不困难。

我认为将动画器与处理上下文捆绑在一起不应该太难。

如果有其他类似的扩展,拥有允许外部扩展的解决方案很重要(也许这不会发生,那么我可能会将其集成)。

依赖关系