چیدمان کنسول با Yoga
Text.js موتور Yoga را در WebAssembly با ضریب مقیاس نقطهای 1 اجرا میکند تا هر کادر روی یک سلول کامل قرار بگیرد. متن با Intl.Segmenter بخشبندی و بر حسب سلول اندازهگیری میشود.
چیدمان هر فریم Text.js را Yoga انجام میدهد، موتور flexbox مورد استفاده در React Native که به WebAssembly کامپایل شده است. کادرها و متن در یک درخت 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 همراه با یک علامت تکیه ترکیبی پس از آن، یک خوشه است و دنبالهای از ایموجیها که با اتصالدهندههای بدون عرض به هم وصل شدهاند نیز یک خوشه است. سپس به هر خوشه یک عرض اختصاص داده میشود:
| متن | سلولها | دلیل |
|---|---|---|
abc | 3 | یک سلول برای هر نویسه |
你好 | 4 | اشغال دو سلول برای نویسههای عریض آسیای شرقی |
🚀 | 2 | اشغال دو سلول برای نمایش ایموجی |
é (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 سلول کوتاه میشود.