Układanie konsoli za pomocą Yoga
Text.js uruchamia Yoga w WebAssembly ze współczynnikiem skali punktów równym 1, więc każdy prostokąt trafia w całe komórki. Tekst jest dzielony za pomocą Intl.Segmenter i mierzony w komórkach.
Layout każdej klatki Text.js oblicza Yoga, silnik flexbox używany przez React Native, skompilowany do WebAssembly. Prostokąty i tekst współdzielą jedno drzewo Yoga. Konsola jest siatką komórek znakowych, więc każda wynikowa pozycja i każdy rozmiar muszą być całkowitą liczbą komórek.
Jeden punkt to jedna komórka
Yoga zaokrągla wyniki do siatki pikseli określonej przez współczynnik skali punktów. Przy wartości 2, jak na ekranie Retina, krawędzie mogą wypadać w połowie punktu. Text.js ładuje Yoga raz i ustawia współczynnik na 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;}Przy współczynniku 1 Yoga zaokrągla każdą krawędź do pełnej jednostki, a Text.js traktuje jedną jednostkę jako jedną komórkę. Yoga zaokrągla bezwzględną pozycję każdej krawędzi i wylicza rozmiary z zaokrąglonych krawędzi, więc sąsiadujące prostokąty mają wspólną krawędź. Podziel wiersz o 81 kolumnach na dwa prostokąty z width="50%", a jeden otrzyma 40 kolumn, drugi 41, bez odstępu i nakładania się.
Text.js sam stosuje domyślne wartości CSS: flexDirection ma wartość row, a flexShrink ma wartość 1, jak w przeglądarce.
Pomiar tekstu
Węzły tekstowe są liśćmi drzewa Yoga z funkcją pomiaru. Yoga wywołuje ją z dostępną szerokością, a Text.js zwraca rozmiar tekstu w komórkach:
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 };}Tekst, który się mieści, jest zwracany w swoim naturalnym rozmiarze. Tekst, który się nie mieści, jest zawijany do dostępnej szerokości, a jego wysokość jest liczbą wierszy. Węzeł zachowuje ostatni wynik zawijania, więc ponowny pomiar tej samej szerokości nie powoduje ponownego zawijania tekstu.
Pomiar klastrów grafemowych
Właściwość length ciągu znaków zlicza jednostki kodowe UTF-16, co niewiele mówi o tym, ile komórek zajmuje tekst. Najpierw tekst jest dzielony na klastry grafemowe za pomocą Intl.Segmenter, więc e z następującym po nim akcentem łączącym stanowi jeden klaster, podobnie jak sekwencja emoji połączona łącznikami o zerowej szerokości. Następnie każdemu klastrowi przypisywana jest szerokość:
| Tekst | Komórki | Dlaczego |
|---|---|---|
abc | 3 | Jedna komórka na znak |
你好 | 4 | Szerokie znaki wschodnioazjatyckie zajmują dwie |
🚀 | 2 | Prezentacja emoji zajmuje dwie |
é (e + U+0301) | 1 | Znaki łączące nie zajmują żadnej |
─│┌▼←⠋ | 6 | Znaki ramek, kształty geometryczne, strzałki i brajl mają stałą szerokość jednej komórki |
Obramowania, znak ▼, strzałki i klatki wskaźnika ładowania są zawsze mierzone jako jedna komórka, więc obramowanie prostokąta nigdy nie przesuwa jego zawartości poza linię.
Drukowalne znaki ASCII pomijają segmentację, a pozostałe szerokości są buforowane dla każdego punktu kodowego i klastra.
Zawijanie
<Text> przyjmuje tryb wrap:
wrap(domyślny) łamie tekst na spacjach, a także wewnątrz słowa, gdy jest ono szersze od wiersza.nowrapzachowuje jeden wiersz i pozwala prostokątowi go przyciąć.truncate-end,truncate-middleitruncate-startzachowują jeden wiersz i zastępują to, co się nie mieści, znakiem….
Skracanie również zlicza komórki, więc wiersz tekstu chińskiego jest ucinany na granicy znaku, a wielokropek zajmuje dokładnie jedną komórkę.
W praktyce
Tutaj panel boczny o szerokości 30 kolumn znajduje się obok panelu, który zajmuje pozostałą szerokość:
<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>W konsoli o szerokości 100 kolumn panel boczny zajmuje 30 kolumn wraz z obramowaniem, a panel rozmowy zajmuje pozostałe 70. Obramowanie ma szerokość jednej komórki z każdej strony, więc tytuł sesji jest skracany do 28 komórek.