事件 Builder

事件 Builder 用来给控件挂外置事件处理函数。它适合应用侧代码:你不需要写一个新的控件类型,也不需要实现 View,直接在创建控件时链式绑定事件。

完整方法列表、handler 类型和派发状态见 Handler API。这一页只讲怎么用。

基本写法

导入 EventBuilder 后,所有实现了 ViewIdentity 的值都可以链式绑定事件。普通控件、ViewBox,以及函数返回的 impl IntoView 都满足这个条件。

use flor::view::builder::EventBuilder;
use flor_lys::label::label;

let item = label("保存")
    .on_click(|view_id, key_state, mouse_position| {
        println!("{view_id} clicked at {mouse_position:?}");
    })
    .on_mouse_enter(|view_id| {
        println!("{view_id} entered");
    })
    .on_mouse_leave(|view_id| {
        println!("{view_id} left");
    });

事件回调的完整参数不是每种都一样。比如鼠标点击的完整参数是 ViewIdKeyStateMousePosition;鼠标进入、离开的完整参数是 ViewId;键盘事件的完整参数是 ViewIdKeyCode 和修饰键状态。现在多数 handler 也支持省略不用的参数,具体签名查 EventBuilder APIhandler 包装类型

看到示例里的参数数量不一样时,按“同一个 handler 的不同转换形态”理解。事件 Builder 会把你传入的闭包、函数或方法项转换成对应 handler;带有额外事件参数的 handler 通常支持完整参数、无参数、只接收 ViewId、省略 ViewId 四种形态。机制细节见 外置事件的机制说明IntoEventHandler API

不只可以传闭包

事件 Builder 的参数不是“只能传闭包”。源码里的方法参数是 impl IntoEventHandler<On...Handler, Args>,handler 类型保留了完整签名的 From<F> 转换,同时额外支持无参数、只接收 ViewId、或省略 ViewId 的常见写法。所以你可以传闭包,也可以传普通函数、关联函数或方法项。

use flor::base::platform::{KeyState, MousePosition};
use flor::view::builder::EventBuilder;
use flor::view::ViewId;
use flor_lys::label::label;

fn save_clicked(view_id: ViewId, _key_state: KeyState, _pos: MousePosition) {
    println!("save from {view_id}");
}

let save = label("保存")
    .on_click(save_clicked);

关联函数也可以直接传:

use flor::base::platform::{KeyState, MousePosition};
use flor::view::builder::EventBuilder;
use flor::view::ViewId;
use flor_lys::label::label;

struct Actions;

impl Actions {
    fn open(view_id: ViewId, _key_state: KeyState, _pos: MousePosition) {
        println!("open from {view_id}");
    }
}

let open = label("打开")
    .on_click(Actions::open);

泛型函数或泛型关联函数也可以传,但要让 Rust 得到一个已经确定类型的函数项:

use flor::view::builder::EventBuilder;
use flor::view::ViewId;
use flor_lys::label::label;

fn trace_click<const SLOT: usize>(view_id: ViewId) {
    println!("slot {SLOT}: {view_id}");
}

let debug = label("调试")
    .on_click(trace_click::<1>);

带实例状态的方法通常用闭包捕获实例,再在闭包里调用方法;这样被传给 builder 的仍然是一个满足目标签名的 Fn

闭包适合就地写少量逻辑;函数、方法项和泛型函数适合复用逻辑,或者把页面代码拆得更清楚。只要最终签名匹配,传哪一种都可以。

Args 泛型怎么理解

事件方法长这样:

fn on_click<Args>(
    self,
    handler: impl IntoEventHandler<OnClickHandler, Args>,
) -> Self;

这里的 Args 不是事件运行时传入的参数,也不需要用户手写。它只是给 Rust 做 trait 推导的标记:完整参数、无参数、只接收 ViewId、省略 ViewId 会分别匹配到不同的 IntoEventHandler 实现。

只有在你把事件 handler 当成自己函数的参数继续转发时,才需要把这个泛型写出来:

use flor::view::builder::EventBuilder;
use flor::view::handler::{IntoEventHandler, OnClickHandler};
use flor::view::View;
use flor_lys::button::button;

fn action_button<Args>(
    text: &'static str,
    on_click: impl IntoEventHandler<OnClickHandler, Args>,
) -> impl View {
    button(text).on_click(on_click)
}

let save = action_button("保存", || {
    println!("save");
});

省略不用的参数

如果只是不使用某些参数,可以继续用 __name 忽略:

use flor::view::builder::EventBuilder;
use flor_lys::label::label;

let item = label("删除")
    .on_click(|view_id, _, _| {
        println!("delete {view_id}");
    });

现在也可以直接减少参数数量。常见支持形态有三类:

use flor::view::builder::EventBuilder;
use flor_lys::label::label;

let save = label("保存").on_click(|| {
    println!("save");
});

let source = label("来源").on_click(|view_id| {
    println!("save from {view_id}");
});

let inspect = label("检查").on_click(|key_state, _pos| {
    println!("ctrl: {}", key_state.control_is_down);
});

完整参数写法仍然可用;省略参数适合业务逻辑不关心控件 ID、鼠标位置或按键状态的场景。

鼠标事件

鼠标事件通常用于点击、按下、松开、移动、进入和离开。

use flor::view::builder::EventBuilder;
use flor_lys::label::label;

let item = label("可点击")
    .on_button_down(|view_id, _, pos| {
        println!("down {view_id}: {pos:?}");
    })
    .on_button_up(|view_id, _, pos| {
        println!("up {view_id}: {pos:?}");
    })
    .on_click(|view_id, _, _| {
        println!("click {view_id}");
    });

常规鼠标命中事件的 MousePosition 是目标控件局部坐标。完整鼠标事件列表看 Handler API 的当前派发状态

键盘事件

键盘事件派发给当前焦点控件。要让控件收到控件级 on_key_down / on_key_up,先让它进入焦点表;焦点机制见 焦点机制

use flor::base::platform::{HandleResult, KeyCode};
use flor::view::builder::{EventBuilder, FocusIndexBuilder};
use flor_lys::label::label;

let editor = label("按 Ctrl+S 保存")
    .focus_index(0)
    .on_key_down(|code, is_alt, is_ctrl, is_shift| {
        if !is_alt && is_ctrl && !is_shift && code == KeyCode::S {
            println!("save");
            HandleResult::Handled
        } else {
            HandleResult::Default
        }
    });

键盘 handler 返回 HandleResult。返回 Handled 表示这个按键已经处理;返回 Default 表示交给默认处理。键盘 handler 可以使用完整参数,也可以省略 ViewId;完整签名见 键盘事件 API

焦点和生命周期事件

on_focus / on_blur 由焦点管理器触发,第二个参数是虚拟焦点序号。

use flor::view::builder::{EventBuilder, FocusIndexBuilder};
use flor_lys::label::label;

let input = label("名称")
    .focus_index(0)
    .on_focus(|virtual_index| {
        println!("focus index: {virtual_index}");
    })
    .on_blur(|| {
        println!("blur");
    });

on_create 适合在控件树创建后做一次运行时初始化,例如为弹出层推入焦点作用域:

use flor::view::builder::EventBuilder;
use flor_lys::label::label;

let dialog = label("弹出层")
    .on_create(|view_id| {
        view_id.push_focus_scope();
    });

生命周期类 handler 的别名和参数见 View 与生命周期事件 API

功能特性事件

拖放和主题变化这类事件受 feature 控制。启用对应 feature 后,相关 builder 方法才可用。完整说明见 Handler API

use flor::view::builder::EventBuilder;
use flor_lys::label::label;

#[cfg(feature = "drag-drop")]
let drop_area = label("拖放到这里")
    .on_drop(|_key_state, _mouse_position, data, _effect| {
        println!("drop: {data:?}");
    });

如果你不确定一个事件当前是否已经接入派发路径,先看 当前派发状态。教程页不会重复维护每个方法的完整状态表。