← وبلاگ

چیدمان کنسول با Yoga

⁦Text.js⁩ موتور Yoga را در WebAssembly با ضریب مقیاس نقطه‌ای 1 اجرا می‌کند تا هر کادر روی یک سلول کامل قرار بگیرد. متن با Intl.Segmenter بخش‌بندی و بر حسب سلول اندازه‌گیری می‌شود.

Adam

چیدمان هر فریم 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 همراه با یک علامت تکیه ترکیبی پس از آن، یک خوشه است و دنباله‌ای از ایموجی‌ها که با اتصال‌دهنده‌های بدون عرض به هم وصل شده‌اند نیز یک خوشه است. سپس به هر خوشه یک عرض اختصاص داده می‌شود:

متنسلول‌هادلیل
abc3یک سلول برای هر نویسه
你好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 سلول کوتاه می‌شود.