Skip to content

Unicode & CJK Support ​

RTXUI measures text in grapheme clusters and computes display widths per cluster, so layout works with any script the terminal can render.

Wide Characters ​

Wide characters (such as Chinese, Japanese, and Korean ideographs) occupy two terminal cell columns. RTXUI accounts for this when measuring text, so wide characters align correctly in flexbox and grid layouts.

Control Characters ​

Text is displayed, never executed. An application shows text it did not write -- a filename, a log line, a response from the network -- and a byte like ESC in there is a command to the terminal rather than a character: it would repaint, move the cursor, or recolour the interface, and everything after it on the line would end up somewhere other than where the layout put it.

Every control character except newline and tab is therefore replaced with U+FFFD (the replacement character) before measurement, so it occupies exactly the one cell it is counted as. Newline and tab survive because layout gives them a meaning of its own -- a hard break and a tab stop. A carriage return becomes a line break, as CSS treats it.

Malformed Text ​

Byte sequences that are not valid UTF-8 are replaced the same way, one U+FFFD per bad byte. This is not pedantry about encodings: the engine and the terminal have to agree on how many cells a sequence occupies, and they can only agree about sequences that can actually be decoded.

The sharpest case is a truncated one. A lead byte announces how many bytes follow it, so E4 BD at the end of a string claims a third byte that is not there and takes whatever comes next instead -- the character after it is absorbed and simply disappears from the display. Replacing per byte rather than per sequence is what keeps the following character intact.

Valid text is never touched, including the parts of Unicode that are easy to mistake for damage: wide characters, combining marks, zero-width characters and four-byte code points all pass through as written.

Zero-Width Characters ​

Format characters occupy no cell, and are measured that way: the zero-width space and non-joiner, the bidi marks, embeddings, overrides and isolates, the word joiner and invisible operators, and the byte-order mark. These arrive in ordinary text -- a zero-width space pasted from a web page, a bidi mark inside a filename, a byte-order mark at the head of a file -- and counting one as a cell would put everything after it on that line a column to the left of where the layout expects it.

Nothing is required of you to get any of this: it applies to every string a template interpolates, whatever its source.

Layout Alignment ​

Because widths are computed from display columns rather than byte or code point counts, centering and alignment stay correct when ASCII and wide characters are mixed on the same line.

Fullscreen Demo
cpp
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Double-width text.
//
// CJK ideographs and emoji occupy two terminal cells. Layout measures text in
// cells, not code points, so alignment holds for mixed-width content.
#include <rtxui/rtxui.hpp>

using namespace rtxui;

class CJKDemo : public Component<CJKDemo> {
 public:
  std::string_view view = R"html(
      <div class="container">
        <h1>RTXUI CJK Character Demo</h1>
        <p>This demo showcases full-width CJK (Chinese, Japanese, Korean) characters taking exactly 2 terminal cell slots, aligning perfectly inside layout boxes.</p>

        <div class="card">
          <div class="title">Chinese (中文)</div>
          <div class="row">
            <div class="label">Text:</div>
            <div class="val">RTXUI 支持中文渲染</div>
          </div>
          <div class="row">
            <div class="label">Width check:</div>
            <div class="val">123456789012345678</div>
          </div>
        </div>

        <div class="card">
          <div class="title">Japanese (日本語)</div>
          <div class="row">
            <div class="label">Text:</div>
            <div class="val">ターミナルでの日本語表示</div>
          </div>
          <div class="row">
            <div class="label">Width check:</div>
            <div class="val">123456789012345678901234</div>
          </div>
        </div>

        <div class="card">
          <div class="title">Korean (한국어)</div>
          <div class="row">
            <div class="label">Text:</div>
            <div class="val">한국어 글꼴이 올바르게 렌더링됩니다.</div>
          </div>
          <div class="row">
            <div class="label">Width check:</div>
            <div class="val">123456789012345678901234567890123456</div>
          </div>
        </div>
      </div>

      <style>
        self {

          display: block;
          padding: 2;
          background-color: rgb(13, 17, 23);
          color: #eee;
          overflow-y: scroll;
        }
        .container {
          display: block;
          max-width: 80;
          margin: 0 auto;
        }
        h1 {
          color: #38bdf8;
        }
        p {
          margin-bottom: 1;
        }
        .card {
          display: block;
          margin-top: 1;
          padding: 1;
          border: tall;
          border-color: #3b82f6;
        }
        .title {
          display: block;
          color: #60a5fa;
          font-weight: bold;
          margin-bottom: 1;
        }
        .row {
          display: flex;
          width: 100%;
        }
        .label {
          width: 15;
          color: #94a3b8;
        }
        .val {
          flex-grow: 1;
          color: #e2e8f0;
        }
      </style>
    )html";
};

int main() {
  auto app = Ref<CJKDemo>::New();
  Screen screen(app);
  screen.Loop();
  return 0;
}