refactor: shared Unicode display-width utility with grapheme cluster support
Consolidate two inconsistent codepoint_width implementations from Screen.cpp and EventEditor.cpp into nocal/tui/unicode.hpp. The unified table covers all CJK, Hangul, emoji, symbol, and combining mark ranges. display_width and fit_text handle grapheme clusters (ZWJ sequences, skin-tone/hair modifiers, regional indicator pairs, variation selectors) so calendar text with emoji or East Asian characters renders correctly.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
#include "nocal/tui/EventEditor.hpp"
|
||||
#include "nocal/tui/unicode.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -147,81 +148,6 @@ std::string single_line(std::string_view value)
|
||||
return result;
|
||||
}
|
||||
|
||||
struct DecodedCodepoint {
|
||||
std::uint32_t value;
|
||||
std::size_t length;
|
||||
};
|
||||
|
||||
DecodedCodepoint decode_codepoint(const std::string_view text, const std::size_t at) noexcept
|
||||
{
|
||||
const auto first = static_cast<unsigned char>(text[at]);
|
||||
if (first < 0x80U) return {first, 1};
|
||||
std::uint32_t value = 0;
|
||||
std::size_t length = 1;
|
||||
if ((first & 0xe0U) == 0xc0U) {
|
||||
value = first & 0x1fU;
|
||||
length = 2;
|
||||
} else if ((first & 0xf0U) == 0xe0U) {
|
||||
value = first & 0x0fU;
|
||||
length = 3;
|
||||
} else if ((first & 0xf8U) == 0xf0U) {
|
||||
value = first & 0x07U;
|
||||
length = 4;
|
||||
} else {
|
||||
return {0xfffdU, 1};
|
||||
}
|
||||
if (at + length > text.size()) return {0xfffdU, 1};
|
||||
for (std::size_t offset = 1; offset < length; ++offset) {
|
||||
const auto byte = static_cast<unsigned char>(text[at + offset]);
|
||||
if ((byte & 0xc0U) != 0x80U) return {0xfffdU, 1};
|
||||
value = (value << 6U) | (byte & 0x3fU);
|
||||
}
|
||||
return {value, length};
|
||||
}
|
||||
|
||||
int codepoint_width(const std::uint32_t value) noexcept
|
||||
{
|
||||
if (value == 0 || (value >= 0x0300U && value <= 0x036fU) ||
|
||||
(value >= 0x1ab0U && value <= 0x1affU) ||
|
||||
(value >= 0x1dc0U && value <= 0x1dffU) ||
|
||||
(value >= 0x20d0U && value <= 0x20ffU) ||
|
||||
(value >= 0xfe00U && value <= 0xfe0fU) ||
|
||||
(value >= 0xfe20U && value <= 0xfe2fU)) {
|
||||
return 0;
|
||||
}
|
||||
if ((value >= 0x1100U && value <= 0x115fU) || value == 0x2329U || value == 0x232aU ||
|
||||
(value >= 0x2e80U && value <= 0xa4cfU) ||
|
||||
(value >= 0xac00U && value <= 0xd7a3U) ||
|
||||
(value >= 0xf900U && value <= 0xfaffU) ||
|
||||
(value >= 0xfe10U && value <= 0xfe19U) ||
|
||||
(value >= 0xfe30U && value <= 0xfe6fU) ||
|
||||
(value >= 0xff00U && value <= 0xff60U) ||
|
||||
(value >= 0xffe0U && value <= 0xffe6U) ||
|
||||
(value >= 0x2600U && value <= 0x27bfU) ||
|
||||
(value >= 0x1f300U && value <= 0x1faffU) ||
|
||||
(value >= 0x20000U && value <= 0x3fffdU)) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string fit(std::string_view text, const int width)
|
||||
{
|
||||
if (width <= 0) return {};
|
||||
std::string result;
|
||||
int used = 0;
|
||||
for (std::size_t at = 0; at < text.size();) {
|
||||
const auto decoded = decode_codepoint(text, at);
|
||||
const auto cell_width = codepoint_width(decoded.value);
|
||||
if (used + cell_width > width) break;
|
||||
result.append(text.substr(at, decoded.length));
|
||||
at += decoded.length;
|
||||
used += cell_width;
|
||||
}
|
||||
result.append(static_cast<std::size_t>(width - used), ' ');
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string style(std::string text, const std::string_view code, const bool ansi)
|
||||
{
|
||||
if (!ansi || text.empty()) return text;
|
||||
@@ -232,7 +158,7 @@ std::string interior_line(std::string content, const int width)
|
||||
{
|
||||
if (width <= 0) return {};
|
||||
if (width == 1) return "│";
|
||||
return "│" + fit(content, width - 2) + "│";
|
||||
return "│" + fit_text(content, width - 2, false) + "│";
|
||||
}
|
||||
|
||||
std::string styled_interior_line(std::string content, const int width,
|
||||
@@ -240,7 +166,7 @@ std::string styled_interior_line(std::string content, const int width,
|
||||
{
|
||||
if (width <= 0) return {};
|
||||
if (width == 1) return "│";
|
||||
return "│" + style(fit(content, width - 2), code, ansi) + "│";
|
||||
return "│" + style(fit_text(content, width - 2, false), code, ansi) + "│";
|
||||
}
|
||||
|
||||
std::string horizontal_line(const int width, const std::string_view left,
|
||||
@@ -508,7 +434,7 @@ std::string EventEditor::render(const int requested_width, const int requested_h
|
||||
if (selected) value += "_";
|
||||
}
|
||||
std::string row = selected ? " › " : " ";
|
||||
row += fit(labels[index], 11);
|
||||
row += fit_text(labels[index], 11, false);
|
||||
row += " ";
|
||||
row += value;
|
||||
lines.push_back(styled_interior_line(std::move(row), width, selected ? "7" : "", ansi));
|
||||
|
||||
Reference in New Issue
Block a user