交互事件
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 倍率缩放,锚点为 center(scale > 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 表请参阅设置与事件。