Skip to content

交互事件

ChartInteraction trait 定义了 ChartView 共用的指针、键盘与 IME 事件 API。将 UI 框架产生的原始输入事件转发给图表,图表内部负责处理命中测试、手势识别、准星更新、标注拖拽、分栏调整、光标反馈和文字编辑。

手势检测完全由图表内部完成。 单击、双击和拖拽均在图表内部通过时序与位置追踪来识别。你只需转发原始指针事件,图表会自动触发相应的 ChartEvent。右键上下文菜单通过 context_menu_items 方法同步获取。

导入

rust
use navi_chart::{ChartView, ChartInteraction};

用法

用一个泛型函数将任意图表视图接入事件循环:

rust
use navi_chart::{ChartInteraction, Modifiers, PointerKind, PointerButton};

fn wire_events<C>(cv: &C, event: MyUiEvent)
where
    C: ChartInteraction,
{
    match event {
        MyUiEvent::Wheel(delta, x)              => cv.on_wheel(delta, x),
        MyUiEvent::Scroll(dx, dy)               => cv.on_scroll(dx, dy),
        MyUiEvent::Pinch(scale, cx, cy)         => cv.on_pinch(scale, Point { x: cx, y: cy }),
        MyUiEvent::PointerMove(id, x, y)        => cv.on_pointer_move(id, Point { x, y }, Modifiers::empty()),
        MyUiEvent::PointerDown(id, kind, x, y)  => { cv.on_pointer_down(id, kind, PointerButton::Primary, Point { x, y }, Modifiers::empty()); }
        MyUiEvent::PointerUp(id, x, y)          => cv.on_pointer_up(id, Point { x, y }),
        MyUiEvent::PointerCancel(id)            => cv.on_pointer_cancel(id),
        MyUiEvent::PointerEnter                 => cv.on_pointer_enter(),
        MyUiEvent::PointerLeave                 => cv.on_pointer_leave(),
        // 单击和双击由图表内部检测,无需单独的事件
        _ => {}
    }

    // 每次事件处理后更新 CSS 光标
    let cursor = cv.cursor_at(last_x, last_y);
    set_cursor(cursor);
}

指针方法

方法说明
on_wheel(delta, anchor_x)累积缩放冲量。delta 正值=放大,负值=缩小,anchor_x 为指针 X 坐标(逻辑像素),用作缩放锚点使锚点下的 K 线保持不动。通过 on_tick 以惯性衰减方式平滑应用。
on_scroll(dx, dy)直接平移,不加 Rust 端惯性(适用于触控板双指滑动,OS 自带动量)。dx 正值=向更新的 K 线方向平移。
on_pinch(scale, center)scale 倍率缩放,锚点为 centerscale > 1 放大)。由 OS 识别的捏合手势驱动(触控板或触摸屏)。
on_pointer_down(id, kind, button, pos, modifiers)处理指针按下事件。id 用于区分多个并发指针(第二个指针触发捏合)。若事件被消费(如分栏拖拽或标注手柄被抓取),返回 true——此时应阻止宿主的默认行为。单击和双击手势由图表从 Down/Up 内部识别,无需单独调用。 右键上下文菜单通过 context_menu_items 单独处理。
on_pointer_move(id, pos, modifiers)处理指针移动(悬停、拖拽或捏合)。每次移动事件都需调用。
on_pointer_up(id, pos)处理指针松开事件。完成分栏调整和标注拖拽,播种平移惯性,触发待处理的单击或双击手势。
on_pointer_cancel(id)处理指针交互被取消(OS 捕获手势或触摸被中断)。不会触发单击。
on_pointer_enter()处理指针进入 canvas 事件。准星和悬停状态会在随后第一次 on_pointer_move 时自然恢复。在每次 pointerenter 时调用。
on_pointer_leave()处理指针离开 canvas 事件,隐藏准星。进行中的拖拽不受影响(平台保持指针捕获直到松开)。在每次 pointerleave 时调用。
context_menu_items(pos)返回指定位置右键点击的 ContextMenuItem 列表。右键点击或 contextmenu 事件触发时同步调用,用返回的列表填充原生或自定义上下文菜单。此方法可能有副作用——若点击落在可选中元素上,会推送 SelectionChanged 事件。
cursor_at(pos)返回指针位置对应的 Cursor 变体。在每次指针移动时调用,并将结果映射到平台的光标 API。
hit_test(pos)返回该位置的 HitTarget。不改变选择状态,也不推送事件。
on_tick()推进平移和缩放惯性物理计算一帧,若有位移则触发重绘。在每个 requestAnimationFrame 帧无条件调用——无活跃惯性时为空操作,持久循环的开销可忽略不计。

上下文菜单

右键点击或 contextmenu 事件触发时,同步调用 context_menu_items(pos) 获取该位置的菜单项。若点击落在可选中元素上,该方法可能推送 SelectionChanged 事件作为副作用。用返回的 Vec<ContextMenuItem> 填充原生或自定义上下文菜单:

js
canvas.addEventListener('contextmenu', e => {
    e.preventDefault(); // 阻止浏览器默认菜单
    const items = cv.context_menu_items({ x: e.offsetX, y: e.offsetY });
    if (items.length > 0) {
        show_context_menu(e.clientX, e.clientY, items);
    }
    // 清空事件队列——context_menu_items 可能已推送 SelectionChanged
    drain_events(cv);
});

Cursor 枚举

cursor_at 返回 Cursor 枚举,可映射为以下 CSS 光标值:

变体CSS 光标使用场景
Default"default"普通指针模式
Crosshair"crosshair"悬停在图表绘图区
Pointer"pointer"悬停在可点击元素(按钮、标签)上
Move"move"悬停在已选中标注上(拖拽移动)
NSResize"ns-resize"悬停在分栏拖拽手柄上(垂直调整)
EWResize"ew-resize"水平调整手柄
NESWResize"nesw-resize"角调整手柄
NWSEResize"nwse-resize"角调整手柄
Text"text"悬停在可编辑文本标注上
Grabbing"grabbing"正在拖拽时

ChartElement

selection()hit_test() 返回 Option<ChartElement>

rust
match cv.selection() {
    Some(ChartElement::Bar) => {
        // K 线系列被选中
    }
    Some(ChartElement::Series { script_id, graph_id }) => {
        // 某条系列折线或标记
    }
    Some(ChartElement::Annotation(id)) => {
        // 某个标注——使用 id 查询其属性
    }
    None => {}
}

选中状态

方法说明
selection()返回当前选中的 ChartElement,无选中则返回 None。选中变化也会通过 SelectionChanged 事件通知。
select(element)以编程方式设置选中元素。传入 None 清除选中状态。选中发生变化时推送 SelectionChanged 事件并触发重绘。
rust
// 以编程方式选中某个标注
cv.select(Some(ChartElement::Annotation(id)));

// 选中 K 线系列
cv.select(Some(ChartElement::Bar));

// 清除选中状态
cv.select(None);

键盘与 IME 方法

方法说明
captures_keyboard()当前激活工具正在消费键盘/IME 输入时返回 true(即文字标注编辑中)。此时应将所有键盘与 IME 事件转发给图表,并将焦点移至宿主的 IME 输入桥接控件。
key_down(key, modifiers)将按键事件转发给激活工具。文字编辑会话因此开始或结束时(激活工具已切换)返回 true——应重新检查 captures_keyboard() 并相应更新 IME 焦点。
text_input(text)将可打印字符或粘贴文本转发给激活工具。
composition_start()通知激活工具 IME 输入法组合输入已开始。
composition_update(text, cursor)更新进行中的 IME 组合字符串。
composition_end(text)结束 IME 组合。text 为空字符串表示用户取消。
focus_lost()通知激活工具输入焦点已失去——文字编辑工具会将其视为隐式提交。
request_text_edit(id)以编程方式进入标注 id 的文字编辑模式。切换成功返回 true;若 id 不存在或该标注无可编辑文字则返回 false
text_editor_selection()返回编辑器中当前选中的文字,无活跃编辑会话或选区为空时返回 None
text_editor_cut()剪切:返回选中文字并从编辑器缓冲区删除。
caret_pixel()编辑会话激活时返回文字光标的近似屏幕坐标 (x, y),否则返回 None。每帧调用此方法并将 IME 桥接输入框移动到对应位置,使输入法选字框跟随光标。
poll_event()从队列取出并返回下一个待处理 ChartEvent,队列为空则返回 None。每次交互方法调用后循环调用,直到返回 None

典型事件循环接入示例

rust
use navi_chart::{ChartInteraction, ChartSettings, Modifiers, PointerKind, PointerButton};

fn handle_pointer_event<C>(cv: &C, event: PointerEvent)
where
    C: ChartInteraction + ChartSettings,
{
    let mods = Modifiers::from_bits_truncate(event.modifier_bits);

    match event.kind {
        PointerEventKind::Down => {
            let consumed = cv.on_pointer_down(
                event.id, event.pointer_kind, event.button, event.pos, mods,
            );
            if consumed {
                event.prevent_default();
            }
        }
        // 始终转发指针移动——图表内部在按下状态时驱动平移,否则更新准星
        PointerEventKind::Move   => cv.on_pointer_move(event.id, event.pos, mods),
        PointerEventKind::Up     => cv.on_pointer_up(event.id, event.pos),
        PointerEventKind::Cancel => cv.on_pointer_cancel(event.id),
        PointerEventKind::Enter  => cv.on_pointer_enter(),
        PointerEventKind::Leave  => cv.on_pointer_leave(),
        // 无需 Click 或 DblClick 事件——图表从 Down/Up 内部检测这些手势。
        // 上下文菜单通过 contextmenu DOM 事件 + context_menu_items() 处理。
    }

    // 每次交互调用后清空事件队列
    while let Some(ev) = cv.poll_event() {
        handle_chart_event(ev);
    }

    set_cursor(cv.cursor_at(event.pos));
}

// 图表创建后启动一次持久动画帧循环。
// on_tick 每帧推进惯性物理,仅在有活跃惯性时重绘——静止时调用开销极小。
fn start_tick_loop(cv: Rc<impl ChartInteraction + 'static>) {
    request_animation_frame(move || {
        cv.on_tick();
        start_tick_loop(cv.clone());
    });
}

键盘与 IME 输入

captures_keyboard() 返回 true 时,文字标注正在被编辑。将键盘事件从 UI 转发给图表:

rust
use navi_chart::{ChartInteraction, Key, Modifiers};

fn handle_keyboard<C>(cv: &C, event: KeyEvent)
where
    C: ChartInteraction,
{
    if !cv.captures_keyboard() {
        return;
    }
    match event {
        KeyEvent::KeyDown(key, mods)              => { cv.key_down(key, mods); }
        KeyEvent::TextInput(text)                 => cv.text_input(&text),
        KeyEvent::CompositionStart                => cv.composition_start(),
        KeyEvent::CompositionUpdate(text, cursor) => cv.composition_update(&text, cursor),
        KeyEvent::CompositionEnd(text)            => cv.composition_end(&text),
        KeyEvent::FocusLost                       => cv.focus_lost(),
    }
}

剪贴板

captures_keyboard()true 时,拦截平台的复制、剪切和粘贴事件,将其桥接给图表。text_input 与普通字符输入走同一条路径——粘贴只是文字的另一个来源:

rust
// 复制——在平台复制事件触发时(Ctrl+C 或等效快捷键)
if let Some(text) = cv.text_editor_selection() {
    platform_clipboard_write(&text);
}

// 剪切——在平台剪切事件触发时(Ctrl+X 或等效快捷键)
if let Some(text) = cv.text_editor_cut() {
    platform_clipboard_write(&text);
}

// 粘贴——在平台粘贴事件触发时(Ctrl+V 或等效快捷键)
if let Some(text) = platform_clipboard_read() {
    cv.text_input(&text);
}

以编程方式进入文字编辑

从宿主代码(如工具栏按钮)进入指定标注的文字编辑模式:

rust
cv.request_text_edit(annotation_id);

完整的 ChartEvent 表请参阅设置与事件

基于 MIT 许可证发布。