gpui step by step 3. 消息传递 EventEmitter

写在前面

gpui 不同组件之间的通信有多种方式,比较直观的一种是 EventEmitter,本次将基于该机制,结合上一节的 Button,实现一个点击计数器,左键点击+1,右键点击-1,按下滚轮清零

事件发生到处理的流程大致如下

  1. BaseView 订阅 Button 将要发出的事件,注册一个处理函数
  2. 点击 Button
  3. Button 发出事件
  4. BaseView 响应并处理该事件

结合上述流程,代码如下,先赋予 Button 发出 Event 的能力

components/button.rs

rust 复制代码
use gpui::{
    AppContext, ClickEvent, Context, CursorStyle, EventEmitter, InteractiveElement, IntoElement,
    MouseButton, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window,
    div, rgb,
};

pub struct Button {
    text: SharedString,
}

// 这里我们通过一个枚举用来代表事件,可以在其中添加额外的信息,不过简单起见就不添加了
pub enum ClickButtonEvent {
    LeftClick,
    RightClick,
    MiddleClick,
}

// 想要让 Button 能够发送事件,其必须实现 EventEmitter trait,并在后者的范型中指定可发出的事件
// 当然可以实现多个 EventEmitter<T> ,以达到发送多种类型事件的目的
impl EventEmitter<ClickButtonEvent> for Button {}

impl Button {
    pub fn new(text: &str) -> Button {
        Button {
            text: SharedString::from(text.to_owned()),
        }
    }
}

impl Render for Button {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        // 实现了 EventEmitter<T> 的类型,其关联的 Context<T> 可调用 emit 方法发送事件
        // 这里我们先通过 Context<Button> 获取其对应的 Entity 实例,方便后续使用,原因见下面的代码
        // 注:后续会介绍优化的版本,届时将无需如此
        let self_entity = cx.entity();
        div()
            .id(self.text.clone())
            .flex()
            .justify_center()
            .items_center()
            .w_16()
            .h_8()
            .bg(rgb(0x8F98BD))
            .text_color(rgb(0xffffff))
            .rounded_sm()
            .hover(|style| style.cursor(CursorStyle::PointingHand).bg(rgb(0x615F73)))
            .child(self.text.clone())
            .on_click(move |event, _window, cx| match event {
                // 这里我们只关注鼠标点击的操作
                ClickEvent::Mouse(mouse_click_evnet) => {
                    // 承接上文,因为 on_click 回调函数的第三个入参类型为 &mut App,我们没法通过
                    // cx 直接 .emit 发送事件,但是可以通过其 update_* 系列方法,这里是 update_entity
                    // 指定为上文获取到 self_entity,即 Entity<Button>,在其回调中获取到的 cx
                    // 即为 Context<Button>,可以通过它发出 ClickButtonEvent 事件了
                    cx.update_entity(&self_entity, |_button, cx| {
                        match mouse_click_evnet.down.button {
                            MouseButton::Left => cx.emit(ClickButtonEvent::LeftClick),
                            MouseButton::Right => cx.emit(ClickButtonEvent::RightClick),
                            MouseButton::Middle => cx.emit(ClickButtonEvent::MiddleClick),
                            _ => {}
                        }
                    })
                }
                ClickEvent::Keyboard(..) => {}
            })
    }
}

上面代码中先获取 self_entity 的操作是一种常见的处理方式,尤其是在需要相关 Entity 的 Context,但是内部闭包只提供了 &mut App 的场景

接下来在BaseView中订阅该事件

rust 复制代码
use gpui::{
    AppContext, Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription, Window,
    div, rgb,
};

use crate::components::button::{Button, ClickButtonEvent};

pub struct BaseView {
    button: Entity<Button>,
    count: isize,
    #[allow(dead_code)]
    subscription: Subscription,
}
impl BaseView {
    // 有人会注意到,这里的入参换成了 Context<BaseView> 而不是 &mut App,
    // 有关 Context,Entity,App 等相关内容,会在补充章节中讲解
    pub fn new(cx: &mut Context<Self>) -> BaseView {
        let button = cx.new(|_| Button::new("click me"));
        // 我们使用 Context<BaseView> 订阅 button 发出的事件,并进行处理;需要注意的是,订阅也要
        // 保存起来,否则会失效,这里选择直接保存在 BaseView 中
        let subscription = cx.subscribe(
            &button,
            |base_view, _button, event: &ClickButtonEvent, cx| {
                match event {
                    ClickButtonEvent::LeftClick => {
                        base_view.count = base_view.count.saturating_add(1)
                    }
                    ClickButtonEvent::RightClick => {
                        base_view.count = base_view.count.saturating_sub(1)
                    }
                    ClickButtonEvent::MiddleClick => base_view.count = 0,
                };
                cx.notify(); // 这里的 cx 类型也为 &mut Context<BaseView>,通过 notify,
                             // 我们告知 gpui,当前 Context 对应的 Entity 需要重新绘制
            },
        );
        BaseView {
            button,
            count: 0,
            subscription,
        }
    }
}

impl Render for BaseView {
    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        div()
            .flex()
            .flex_col()
            .gap_2()
            .size_full()
            .items_center()
            .justify_center()
            .bg(rgb(0xffffff))
            .child(format!("当前计数 {}", self.count))
            .child(self.button.clone())
    }
}

出现的问题

左键点击确实增加了,但是右键和滚轮按下后并没有出现预期的减少与归零

这是因为 gpui 当前的 on_click 只处理了鼠标左键的点击,其他部分如右键,滚轮等均无法捕获处理;对于这个问题,可以使用 on_mouse_down 来解决,如下是修复该问题,与优化后的代码

components/button.rs

rust 复制代码
use gpui::{
    Context, CursorStyle, EventEmitter, InteractiveElement, IntoElement, MouseButton,
    ParentElement, Render, SharedString, Styled, Window, div, rgb,
};

pub struct Button {
    text: SharedString,
}

pub enum ClickButtonEvent {
    LeftClick,
    RightClick,
    MiddleClick,
}

impl EventEmitter<ClickButtonEvent> for Button {}

impl Button {
    pub fn new(text: &str) -> Button {
        Button {
            text: SharedString::from(text.to_owned()),
        }
    }
}

impl Render for Button {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        div()
             // .id(self.text.clone()) // on_mouse_down 方法可以直接在 Div 上执行,无需调用 id 转换成 Stateful<Div>
            .flex()
            .justify_center()
            .items_center()
            .w_16()
            .h_8()
            .bg(rgb(0x8F98BD))
            .text_color(rgb(0xffffff))
            .rounded_sm()
            .hover(|style| style.cursor(CursorStyle::PointingHand).bg(rgb(0x615F73)))
            .child(self.text.clone())
            .on_mouse_down( // 直接使用 on_mouse_down
                MouseButton::Left,
                // 这里我们使用一个辅助函数,它所需的闭包的第四个参数即是 Context<Button>,无需我们手动获取 Entity 再传入了
                cx.listener(|_this, _event, _window, cx| {
                    cx.emit(ClickButtonEvent::LeftClick);
                }),
            )
            .on_mouse_down(
                MouseButton::Right,
                cx.listener(|_this, _event, _window, cx| {
                    cx.emit(ClickButtonEvent::RightClick);
                }),
            )
            .on_mouse_down(
                MouseButton::Middle,
                cx.listener(|_this, _event, _window, cx| {
                    cx.emit(ClickButtonEvent::MiddleClick);
                }),
            )
    }
}

运行结果如下

相关推荐
特立独行的猫a2 小时前
我用 Rust + Tauri 2 复刻了 N_m3u8DL-CLI:一个 m3u8 多线程下载器
开发语言·后端·rust·m3u8·视频下载器·m3u8dl-cli
对象存储与RustFS2 小时前
大文件传到 48 GiB 就断:S3 分片上传的参数怎么算,以及那些没人清的残片
后端·rust·开源
程序员爱钓鱼4 小时前
Rust 生命周期案例详解:从编译错误到真实业务场景
前端·后端·rust
Albart5751 天前
【玩转 AtomCode】替代Claude Code!开源多模型免费AI编码Agent深度实测教程
人工智能·rust·开源·atomcode·多模型ai
特立独行的猫A1 天前
Rust+Tauri 2实现个人聚藏APP:一个 “注定上不了“ 的个人收藏夹
rust
jeffwang1 天前
我把同一个压测做错了三次,第四次才发现真正的瓶颈
分布式·性能优化·rust
程序员爱钓鱼1 天前
Rust 生命周期详解:生命周期标注、约束与真实项目应用
后端·面试·rust
MoonBit月兔1 天前
MoonBit 受邀亮相 SPLASH/ISSTA 2026,与 Java、Rust、Eiffel、D 语言核心贡献者同场交流
开发语言·后端·rust
今天AI了吗1 天前
Python vs Rust AI 服务基准测试全景:FastAPI、Axum、Actix-Web 的延迟与吞吐
人工智能·python·rust
Source.Liu1 天前
【A11】项目架构设计笔记
rust