← Blog

Menata konsol dengan Yoga

Text.js menjalankan Yoga di WebAssembly dengan faktor skala titik 1, sehingga setiap kotak menempati sel utuh. Teks disegmentasi dengan Intl.Segmenter dan diukur dalam sel.

Adam

Yoga, mesin flexbox yang digunakan React Native dan dikompilasi ke WebAssembly, mengatur layout setiap frame Text.js. Kotak dan teks berbagi satu pohon Yoga. Konsol berupa kisi sel karakter, sehingga setiap posisi dan ukuran yang dihasilkannya harus berupa jumlah sel yang bulat.

Satu poin adalah satu sel

Yoga membulatkan hasilnya ke kisi piksel yang ditentukan oleh faktor skala poin. Pada faktor 2, seperti pada layar Retina, tepi dapat berada di setengah poin. Text.js memuat Yoga sekali dan menetapkan faktornya ke 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;}

Dengan faktor 1, Yoga membulatkan setiap tepi ke satuan bulat, dan Text.js memperlakukan satu satuan sebagai satu sel. Yoga membulatkan posisi absolut setiap tepi dan menghitung ukuran dari tepi yang telah dibulatkan, sehingga kotak yang bersebelahan berbagi satu tepi. Bagi baris selebar 81 kolom menjadi dua kotak width="50%" dan satu kotak mendapat 40 kolom, yang lain 41, tanpa celah dan tanpa tumpang tindih.

Text.js menerapkan nilai default CSS sendiri: flexDirection adalah row dan flexShrink adalah 1, seperti pada browser.

Mengukur teks

Node teks adalah daun pada pohon Yoga yang memiliki fungsi pengukuran. Yoga memanggilnya dengan lebar yang tersedia, dan Text.js mengembalikan ukuran teks dalam sel:

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 };}

Teks yang muat dikembalikan dengan ukuran alaminya. Teks yang tidak muat dibungkus sesuai lebar yang tersedia, dan tingginya adalah jumlah baris. Node menyimpan hasil pembungkusan terakhir, sehingga pengukuran ulang dengan lebar yang sama tidak membungkus ulang teks.

Mengukur klaster grafem

length pada string menghitung unit kode UTF-16, yang tidak banyak menunjukkan jumlah sel yang diisinya. Teks terlebih dahulu dipecah menjadi klaster grafem dengan Intl.Segmenter, sehingga e yang diikuti aksen penggabung menjadi satu klaster, begitu pula rangkaian emoji yang disambungkan dengan zero-width joiner. Setiap klaster kemudian diberi lebar:

TeksSelAlasan
abc3Satu sel per karakter
你好4Karakter lebar Asia Timur memakai dua sel
🚀2Tampilan emoji memakai dua sel
é (e + U+0301)1Tanda penggabung tidak memakai sel
─│┌▼←⠋6Karakter pembentuk kotak, bentuk geometris, panah, dan braille ditetapkan memakai satu sel

Batas, tanda ▼, panah, dan frame spinner selalu diukur sebagai satu sel, sehingga batas kotak tidak pernah menggeser isinya keluar dari barisan.

ASCII yang dapat dicetak melewati segmentasi, dan lebar lainnya disimpan dalam cache per titik kode dan per klaster.

Pembungkusan teks

<Text> menerima mode wrap:

  • wrap (default) memutus baris pada spasi, dan di dalam kata jika kata tersebut lebih lebar daripada baris.
  • nowrap mempertahankan satu baris dan membiarkan kotak memotongnya.
  • truncate-end, truncate-middle, dan truncate-start mempertahankan satu baris dan mengganti bagian yang tidak muat dengan ….

Pemotongan juga menghitung sel, sehingga satu baris teks bahasa Mandarin dipotong pada batas karakter, dan elipsis memakai tepat satu sel.

Dalam praktik

Di sini, bilah samping selebar 30 kolom berada di sebelah panel yang memakai lebar sisanya:

<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>

Pada konsol selebar 100 kolom, bilah samping memakai 30 kolom termasuk batasnya, dan panel percakapan memakai 70 kolom sisanya. Batas memakai satu sel di setiap sisi, sehingga judul sesi dipotong pada 28 sel.