Skip to content

交互事件

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 倍率縮放,錨點為 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 許可證發佈。