交互事件
ChartInteraction trait 定義了 ChartView 共用的指針、鍵盤與 IME 事件 API,並提供 poll_event 用於讀取圖表的出站事件隊列。將 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 表請參閱設置與事件。