Skip to Content
Plugin 開發Canvas 模式

Canvas 模式

前面教的是 Widget 模式:你宣告控制項,畫面由主程式負責。能用 Widget 就用 Widget。 它是唯一在終端機版與視窗版都完整可攜的路徑,而且你的程式碼完全不出現在繪圖路徑上,所以繪圖出問題也不會是你的錯。

只有需要自由繪製時才改用 Canvas:暫存器熱圖、位元時序圖這類沒有現成控制項可用的畫面。 Canvas 模式讓你在面板裡逐格畫字元,代價是你要自己管繪圖的一切,還要遵守一組嚴格的規則。

範例是 PDK 裡的 plugins\canvas_demo\:一個 8x8 的暫存器熱圖,可以用方向鍵與滑鼠滾輪捲動、點一格選取、按住左鍵拖曳塗格子, 並把面板尺寸顯示在標題列。想知道 Canvas Plugin 的完整寫法,把它讀完就夠了。

怎麼掛成 Canvas

你在 IcPlugin 裡宣告 panel_mode = NX_PANEL_MODE_CANVAS,並實作 ui_render_fn。 使用者掛載時指定面板:

p canvas_demo @p3

該象限會自動改型成 Canvas 面板。沒有指定 @p<N> 時的行為見架構總覽。

繪圖函式與 Context Guard

ui_render_fn(ctx, panel_id, width, height) 每個畫面更新週期被呼叫一次,width/height 是內容區的大小(字元格)。 它裡面用三個函式畫圖,座標都是面板內的相對字元格,左上角是 0,0:

函式做什麼
DrawText(ctx, x, y, fg, bg, text)在指定位置畫一段文字,超出面板寬度的部分由主程式剪掉
DrawBox(ctx, x, y, w, h, fg, bg)畫一個矩形框。框線用什麼字元由主題決定,你不指定
DrawRegion(ctx, x, y, w, h, cells)一次提交整塊字元格(w * h 個 NxCanvasCell,逐列排列)

顏色不是 RGB,是語意色(NX_COLOR_PRIMARY、NX_COLOR_WARN、NX_COLOR_BG_PANEL……),實際顏色跟著使用者的主題走。

這三個函式只在 ui_render_fn 執行的期間有效:在別的地方(例如 on_complete、poll_fn)呼叫,一律回 ERR_INVALID_CONTEXT(-84)。 這道檢查叫 Context Guard。Widget 模式沒有這個問題,這是它被優先推薦的原因之一。

10 ms 預算:事後量測,超過一次就下線

ui_render_fn 有 10 ms 的時間預算。注意它是事後量測,不是中途打斷:

  1. Core 讓你的函式照樣畫完。
  2. 畫完之後量花了多久。
  3. 超過 10 ms:本次畫出來的結果照常採用,但 Core 記一筆錯誤(Log 裡會看到 plugin render took N ms (limit 10 ms) ... marked faulted and will not be rendered again), 這顆 Plugin 立即下線,下一幀起不再被呼叫(錯誤碼 ERR_RENDER_TIMEOUT,-83)。

沒有第二次機會:Core 不統計「連續幾次超時」,一次就夠。所以:

  • 不要逐字元呼叫 DrawText,每次呼叫都要跨一次 DLL 邊界,字元多的時候光呼叫就吃掉預算。 改成先把整塊畫面填進一個固定大小的陣列,再呼叫一次 DrawRegion。canvas_demo 就是這樣做的(它的 s_cells[])。
  • ui_render_fn 裡禁止:讀寫 I2C、Sleep、鎖任何共享資料、malloc。 canvas_demo 的 s_cells[] 是靜態配置的固定大小陣列,理由就是最後一條。

資料從哪裡來:render 之前先準備好

ui_render_fn 既然不能碰硬體,資料要事先放進你自己的緩衝區。標準做法:

  1. poll_fn 用 SubmitJob(低優先級、附 on_complete)送出讀取,見讀寫硬體的三種方式。
  2. on_complete(在 I2C Worker 上)把讀到的資料 memcpy 進自己的快照緩衝區,然後呼叫 NotifyUiRefresh(ctx)。
  3. ui_render_fn(在 UI Thread 上)只讀那份快照,畫出來。

NotifyUiRefresh 是 Worker 端通知畫面更新的唯一合法手段;同一個更新週期內呼叫多次會被合併成一次重繪。 不要在 on_complete 裡想直接畫圖。

也可以在 render 之前用 GetRegSnapshot 拿 Core 記著的暫存器內容(它不碰硬體,附有效遮罩,見同一章)。

按鍵、滑鼠與面板尺寸

  • on_key_press:兩個版本都會送。Canvas 面板沒有原生子控件,所以視窗版也不會有「按鍵被控件吃掉」的問題 (這只影響 Widget 面板)。ESC 與六個保留的 Ctrl 組合鍵不會送給你,見生命週期與執行緒。
  • on_mouse_event:只送給 Canvas 面板,座標是內容區內的相對抽象格。按下即捕獲(拖出面板後 DRAG/UP 仍會送達,座標可為負或超出寬高, 用之前先檢查範圍);沒有 hover、雙擊與修飾鍵。滾輪只在「游標下的 Canvas 面板同時是有焦點的面板」時送出。 回 NX_EVT_CONSUMED 表示整段手勢你都吃掉;回 NX_EVT_PASS 則主程式照常處理(例如右鍵貼上),但後續的 DRAG/UP 仍然送給你。
  • on_layout_changed:只在內容區尺寸有變才送。把 width/height 快取起來,繪圖與判斷「面板太小畫不下」時使用; 它與 ui_render_fn 收到的尺寸是同一份幾何。canvas_demo 在面板小於 24 x 9 格時只畫一行 panel too small, 而不是畫出一半的熱圖。
  • 不要假設面板有多大、字有多寬,也不要寫死絕對座標;兩個版本的格子大小不同。

一份自我檢查

寫完 Canvas Plugin,逐條確認:

  • ui_render_fn 裡沒有任何硬體存取、Sleep、鎖、malloc。
  • 整塊畫面用 DrawRegion 一次提交,不是逐字元 DrawText。
  • 資料在 on_complete 或 poll_fn 裡先複製進自己的緩衝區,並呼叫了 NotifyUiRefresh。
  • 面板太小的時候有退路(顯示一行提示,不要越界寫)。
  • 滑鼠座標用之前檢查過範圍。
  • 在兩個版本(終端機與視窗)都試過。

逐支繪圖函式的精確契約見 API 參考的 Canvas 繪圖一節,Canvas 的規則全集在開發指南第 7 章。這兩份文件也隨 PDK 附在 docs\ 裡(NEXUS_TUI_PLUGIN_API_REFERENCE.md、NEXUS_TUI_PLUGIN_GUIDE.md)。