← ブログ

Yogaによるコンソールのレイアウト

Text.jsはポイントスケール係数1でWebAssembly上のYogaを実行するため、すべてのボックスがセル単位で配置されます。テキストはIntl.Segmenterで分割し、セル単位で測定します。

Adam

Text.jsの各フレームは、React Nativeで使われているflexboxエンジンをWebAssemblyにコンパイルしたYogaでレイアウトされます。ボックスとテキストは1つのYogaツリーを共有します。コンソールは文字セルのグリッドなので、算出される位置とサイズはすべて整数個のセルで表す必要があります。

1ポイントは1セル

Yogaは、ポイントスケール係数で定義されたピクセルグリッドに結果を丸めます。Retina画面のように係数が2の場合、端の位置が半ポイント単位になることがあります。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は1単位を1セルとして扱います。Yogaは各端の絶対位置を丸め、その端からサイズを算出するため、隣接するボックスは端を共有します。81列の行を2つの 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 に結合アクセントが続く場合は1つのクラスターになり、ゼロ幅接合子でつながれた絵文字列も同様です。その後、各クラスターに幅を割り当てます。

テキストセル数理由
abc31文字につき1セル
你好4東アジアの幅広文字は2セルを使用
🚀2絵文字表示は2セルを使用
é (e + U+0301)1結合文字はセルを使用しない
─│┌▼←⠋6罫線文字、幾何学図形、矢印、点字は1セルに固定

罫線、▼ マーク、矢印、スピナーのフレームは常に1セルとして計測されるため、ボックスの罫線で内容の位置がずれることはありません。

印字可能なASCII文字では分割を省略し、それ以外の幅はコードポイントごと、クラスターごとにキャッシュします。

折り返し

<Text> は wrap モードを受け取ります。

  • wrap(デフォルト)は空白で折り返し、単語が行幅より長い場合は単語の途中で折り返します。
  • nowrap は1行を維持し、ボックスで切り取ります。
  • truncate-end、truncate-middle、truncate-start は1行を維持し、収まらない部分を … に置き換えます。

省略処理でもセルを数えるため、中国語の行は文字の境界で切り取られ、省略記号はちょうど1セルを占めます。

実際の使用例

この例では、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列を占めます。罫線は左右それぞれ1セルなので、セッションのタイトルは28セルで省略されます。