|
| 1 | +# Fix emoji alignment issue in table borders |
| 2 | + |
| 3 | +## Description |
| 4 | + |
| 5 | +Emojis in table content cause misalignment with the table borders. When emojis are used in cells (like weather icons 🌤️, 📍, 🌡️, ☁️), the table border lines don't line up properly with the content, creating a visual misalignment. |
| 6 | + |
| 7 | +## Example |
| 8 | + |
| 9 | +Sample output showing the problem: |
| 10 | +``` |
| 11 | +═════════════════════════════════════════ 🌤️ Weather Report ═════════════════════════════════════════ |
| 12 | +╭─────────────────┬───────────────────────╮ |
| 13 | +│ 📍 Location │ Dallas, United States │ |
| 14 | +│ 🌡️ Temperature │ 25.2°C (77.4°F) │ |
| 15 | +│ ☁️ Condition │ Overcast │ |
| 16 | +╰─────────────────┴───────────────────────╯ |
| 17 | +``` |
| 18 | + |
| 19 | +## Root Cause |
| 20 | + |
| 21 | +`AnsiStringUtils.GetVisibleLength()` returns `StripAnsiCodes(text).Length` which counts .NET string length (UTF-16 code units), not terminal display columns. Emojis like `📍` take 2 terminal columns but `.Length` counts them as 1-2 characters, making borders too short. |
| 22 | + |
| 23 | +## Implementation |
| 24 | + |
| 25 | +Added `UnicodeWidth` utility class that calculates terminal display width accounting for wide characters (emoji, CJK) and zero-width characters (combining marks, ZWJ). Updated `GetVisibleLength()` to use it. No external dependencies — uses .NET 10 built-in `Rune`, `StringInfo`, and `UnicodeCategory` APIs (all AOT-compatible). |
| 26 | + |
| 27 | +### Wide character ranges covered (width 2) |
| 28 | + |
| 29 | +**Emoji_Presentation=Yes (BMP, from Unicode 16.0 emoji-data.txt):** |
| 30 | +- U+231A-231B ⌚⌛, U+23E9-23EC ⏩⏪⏫⏬, U+23F0 ⏰, U+23F3 ⏳ |
| 31 | +- U+25FD-25FE ◽◾ |
| 32 | +- U+2614-2615 ☔☕, U+2648-2653 ♈-♓, U+267F ♿, U+2693 ⚓, U+26A1 ⚡ |
| 33 | +- U+26AA-26AB ⚪⚫, U+26BD-26BE ⚽⚾, U+26C4-26C5 ⛄⛅, U+26CE ⛎ |
| 34 | +- U+26D4 ⛔, U+26EA ⛪, U+26F2-26F3 ⛲⛳, U+26F5 ⛵, U+26FA ⛺, U+26FD ⛽ |
| 35 | +- U+2705 ✅, U+270A-270B ✊✋, U+2728 ✨, U+274C ❌, U+274E ❎ |
| 36 | +- U+2753-2755 ❓❔❕, U+2757 ❗, U+2795-2797 ➕➖➗, U+27B0 ➰, U+27BF ➿ |
| 37 | +- U+2B1B-2B1C ⬛⬜, U+2B50 ⭐, U+2B55 ⭕ |
| 38 | + |
| 39 | +**Text-presentation characters (☀♻✈▶◀▪▫ etc.)** are width 1 as single |
| 40 | +runes. They become width 2 when combined with VS16 (U+FE0F) as |
| 41 | +multi-codepoint grapheme clusters, handled automatically by GetTextWidth. |
| 42 | + |
| 43 | +**Emoji blocks (SMP):** |
| 44 | +- U+1F000-U+1FAFF, U+1FC00-U+1FFFF — Emoji blocks |
| 45 | +- U+1F1E0-U+1F1FF — Regional indicator symbols (flags) |
| 46 | + |
| 47 | +**CJK:** |
| 48 | +- U+2E80-U+303E — CJK Radicals, Kangxi, Ideographic Description |
| 49 | +- U+3041-U+33BF — Hiragana, Katakana, Bopomofo, CJK Compatibility |
| 50 | +- U+3400-U+4DBF — CJK Extension A |
| 51 | +- U+4E00-U+9FFF — CJK Unified Ideographs |
| 52 | +- U+A000-U+A4CF — Yi Syllables and Radicals |
| 53 | +- U+AC00-U+D7A3 — Hangul Syllables |
| 54 | +- U+F900-U+FAFF — CJK Compatibility Ideographs |
| 55 | +- U+FE10-U+FE19 — Vertical Forms |
| 56 | +- U+FE30-U+FE6F — CJK Compatibility Forms |
| 57 | +- U+FF01-U+FF60, U+FFE0-U+FFE6 — Fullwidth forms |
| 58 | +- U+1100-U+115F — Hangul Jamo |
| 59 | +- U+2329-U+232A — Wide angle brackets |
| 60 | +- U+20000-U+2A6DF — CJK Extension B |
| 61 | +- U+2A700-U+2B73F — CJK Extension C |
| 62 | +- U+2B740-U+2B81F — CJK Extension D |
| 63 | +- U+2B820-U+2CEAF — CJK Extension E |
| 64 | +- U+2CEB0-U+2EBEF — CJK Extension F |
| 65 | +- U+2F800-U+2FA1F — CJK Compatibility Ideographs Supplement |
| 66 | +- U+30000-U+3134F — CJK Extension G |
| 67 | +- U+31350-U+323AF — CJK Extension H |
| 68 | + |
| 69 | +**Zero-width (width 0):** |
| 70 | +- Control characters |
| 71 | +- UnicodeCategory.NonSpacingMark, EnclosingMark, Format |
| 72 | +- U+00AD (soft hyphen), U+200B-U+200D (ZWSP, ZWNJ, ZWJ), U+2060 (WJ) |
| 73 | +- U+FE00-U+FE0F (variation selectors), U+E0100-U+E01EF (VS supplement) |
| 74 | + |
| 75 | +**Multi-codepoint grapheme clusters** (ZWJ sequences, flags, skin-tone) → width 2 |
| 76 | + |
| 77 | +### Files changed |
| 78 | + |
| 79 | +- `source/timewarp-terminal/widgets/unicode-width.cs` (new) |
| 80 | +- `source/timewarp-terminal/widgets/ansi-string-utils.cs` (modified) |
| 81 | +- `source/timewarp-terminal/widgets/table-widget.cs` (modified) |
| 82 | +- `source/timewarp-terminal/global-usings.cs` (modified) |
| 83 | +- `samples/emoji-table-widget.cs` (new) |
| 84 | +- `tests/unicode-width-01-basic.cs` (new) |
| 85 | +- `tests/ansi-string-utils-03-emoji-width.cs` (new) |
| 86 | +- `tests/table-widget-06-emoji.cs` (new) |
| 87 | + |
| 88 | +## Checklist |
| 89 | + |
| 90 | +- [x] Investigate the table rendering code to understand how cell width calculations work |
| 91 | +- [x] Identify why emojis cause border misalignment (emoji display width vs character count) |
| 92 | +- [x] Research proper emoji width handling (full-width vs half-width emojis) |
| 93 | +- [x] Implement UnicodeWidth utility class with comprehensive ranges |
| 94 | +- [x] Update GetVisibleLength to use display width |
| 95 | +- [x] Update WrapText for grapheme-cluster-aware iteration |
| 96 | +- [x] Update TruncateWithEllipsis for display-width-aware slicing |
| 97 | +- [x] Add tests for UnicodeWidth, AnsiStringUtils emoji, and table emoji rendering |
| 98 | +- [x] Verify tables without emojis still render correctly (92 tests pass) |
| 99 | +- [x] Visually verify weather report table alignment |
0 commit comments