使用 Yoga 配置主控台版面
Text.js 在 WebAssembly 中執行 Yoga,點縮放係數設為 1,讓每個區塊都對齊完整儲存格。文字以 Intl.Segmenter 分段,並以儲存格為單位測量。
Text.js 的每個影格都由編譯為 WebAssembly 的 Yoga 負責版面配置,Yoga 是 React Native 使用的 flexbox 引擎。方框和文字共用一棵 Yoga 樹。主控台是由字元儲存格組成的網格,因此算出的每個位置與尺寸都必須是整數個儲存格。
一點就是一個儲存格
Yoga 會依照點縮放係數定義的像素網格,將結果四捨五入。係數為 2 時,如同 Retina 螢幕,邊緣可以落在半點的位置。Text.js 只載入 Yoga 一次,並將係數設為 1:
export function initYoga(): Promise<Yoga> { if (yoga !== null) return Promise.resolve(yoga); if (yogaPromise === null) { yogaPromise = loadYoga().then((instance) => { yoga = instance; const config = instance.Config.create(); config.setPointScaleFactor(1); config.setUseWebDefaults(false); yogaConfig = config; return instance; }); yogaPromise.catch(() => { yogaPromise = null; }); } return yogaPromise;}係數為 1 時,Yoga 會將每個邊緣四捨五入至整數單位,而 Text.js 將一個單位視為一個儲存格。Yoga 會將每個邊緣的絕對位置四捨五入,再根據取整後的邊緣算出尺寸,因此相鄰的方框會共用邊緣。將一個 81 欄的橫列分成兩個 width="50%" 方框,其中一個會取得 40 欄,另一個取得 41 欄,沒有間隙,也不會重疊。
Text.js 自行套用 CSS 預設值:flexDirection 為 row,flexShrink 為 1,與瀏覽器相同。
測量文字
文字節點是帶有測量函式的 Yoga 葉節點。Yoga 呼叫此函式時會傳入可用寬度,Text.js 則回傳以儲存格為單位的文字尺寸:
private measure(width: number, widthMode: MeasureMode, _height: number, _heightMode: MeasureMode): { width: number; height: number } { const natural = this.naturalSize(); if (widthMode === MeasureMode.Undefined || Number.isNaN(width)) return natural; const avail = Math.max(0, Math.floor(width)); if (natural.width <= avail) { return { width: widthMode === MeasureMode.Exactly ? avail : natural.width, height: natural.height }; } if ((this.props.wrap ?? "wrap") !== "wrap") return { width: avail, height: natural.height }; const lines = this.getLines(avail); let maxLine = 0; for (const l of lines) if (l.width > maxLine) maxLine = l.width; return { width: widthMode === MeasureMode.Exactly ? avail : Math.min(avail, maxLine), height: lines.length };}放得下的文字會以原始尺寸回傳。放不下的文字會依可用寬度換行,高度則是行數。節點會保留上次的換行結果,因此再次測量相同寬度時,不會重新將文字換行。
測量字素叢集
字串的 length 計算的是 UTF-16 編碼單元,無法充分反映字串占用多少儲存格。文字會先透過 Intl.Segmenter 分割成字素叢集,因此 e 後接一個組合重音符號會形成一個叢集,以零寬連接符連接的 emoji 序列也一樣。接著為每個叢集指定寬度:
| 文字 | 儲存格數 | 原因 |
|---|---|---|
abc | 3 | 每個字元占一個儲存格 |
你好 | 4 | 東亞寬字元占兩個儲存格 |
🚀 | 2 | emoji 顯示形式占兩個儲存格 |
é (e + U+0301) | 1 | 組合標記不占儲存格 |
─│┌▼←⠋ | 6 | 製表符號、幾何圖形、箭頭與點字固定占一個儲存格 |
框線、▼ 標記、箭頭與旋轉指示器的影格一律以一個儲存格測量,因此方框的框線不會擠歪內容。
可列印的 ASCII 字元會略過分割,其他寬度則依碼位與叢集分別快取。
換行
<Text> 接受 wrap 模式:
wrap(預設值)會在空格處換行,單字寬於一行時則在單字內換行。nowrap保持單行,由方框裁切內容。truncate-end、truncate-middle和truncate-start保持單行,並以…取代放不下的內容。
截斷也會計算儲存格數,因此一行中文會在字元邊界截斷,而刪節號恰好占一個儲存格。
實際應用
這裡有一個 30 欄寬的側邊欄,旁邊的面板占用剩餘寬度:
<Box flexDirection="row" width="100%" height="100%"> <Box width={30} flexDirection="column" border="rounded" title="Sessions" overflow="hidden"> <Text wrap="truncate-end">session-03 · refactor the renderer pipeline</Text> </Box> <Box flexGrow={1} flexDirection="column" border="single" title="Conversation" paddingX={1}> <Text>Message 0: 你好 🚀</Text> </Box></Box>在 100 欄寬的主控台中,側邊欄連同框線占 30 欄,對話面板則占其餘 70 欄。框線在兩側各占一個儲存格,因此工作階段標題會在 28 個儲存格處截斷。