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);
                }),
            )
    }
}

运行结果如下

相关推荐
不爱学英文的码字机器4 小时前
[鸿蒙PC命令行移植适配]移植rust三方库tokei到鸿蒙PC的完整实践
华为·rust·harmonyos
EterNity_TiMe_4 小时前
[鸿蒙PC命令行移植适配]移植rust三方库ouch到鸿蒙PC的完整实践
华为·rust·harmonyos
EterNity_TiMe_5 小时前
[鸿蒙PC命令行移植适配]移植rust三方库broot到鸿蒙PC的完整实践
华为·rust·harmonyos
Pocker_Spades_A5 小时前
[鸿蒙PC命令行移植适配]移植rust三方库ox到鸿蒙PC的完整实践
华为·rust·harmonyos
EterNity_TiMe_6 小时前
[鸿蒙PC命令行移植适配]移植rust三方库choose到鸿蒙PC的完整实践
华为·rust·harmonyos
Pocker_Spades_A6 小时前
[鸿蒙PC命令行移植适配]移植rust三方库tojson到鸿蒙PC的完整实践
华为·rust·harmonyos
Pocker_Spades_A6 小时前
[鸿蒙PC命令行移植适配]移植rust三方库hexyl到鸿蒙PC的完整实践
华为·rust·harmonyos
Pocker_Spades_A9 小时前
[鸿蒙PC命令行移植适配]移植rust三方库peep到鸿蒙PC的完整实践
华为·rust·harmonyos