Skip to main content

TUI Emoji Alignment Pitfalls: The Danger of Manual Offsets

· 4 min read
TeaQL Team
Core Team

Terminal UIs (TUIs) are notoriously tricky when it comes to character widths and alignment. We recently encountered a classic pitfall while building a World Cup knockout bracket tree in Rust using ratatui (or tui-rs).

A seemingly harmless hack introduced to fix an "emoji width illusion" ended up destroying the alignment of the entire layout structure.

The Emoji Width Illusion​

In our bracket tree, the first column (Round of 32) listed teams alongside their national flags (e.g., 🇫🇷 France). Emojis, especially flags composed of multiple Unicode scalars (Regional Indicators), often render as 2 cells wide in many modern terminals.

However, standard string formatting tools in Rust (like format!("{:<w$}", text)) pad strings based on character count, not visual terminal width.

Because the flags took up 2 visual cells but were padded as standard characters, the rows containing matches (even rows) visually stretched 1 cell further to the right than the blank separator rows (odd rows).

To "fix" this, the original author introduced a manual hack:

// On blank rows, add an extra space to compensate for the flag width on match rows
const FLAG_OFFSET: usize = 1;
spans.push(Span::raw(" ".repeat(col0_w + FLAG_OFFSET)));

Visually, this made the first column look perfectly aligned. But internally, it planted a ticking time bomb.

The Layout Engine Desync​

By manually inserting a space character (FLAG_OFFSET), we lied to the underlying TUI layout engine. The engine now genuinely believed that the blank rows started 1 coordinate position further to the right than the match rows (X+1 vs X).

To counter this internal misalignment for the subsequent connecting lines (the bracket trees like ─┐ and ├─), the original code added another hack:

Conn::Vert if is_match_row => " │", // 3 chars wide to catch up to X+1
Conn::Vert => " │", // 2 chars wide

This "negative times a negative" approach perfectly aligned the first stage of the bracket (Round of 16 to Quarter-Finals). But as we extended the tree to Semi-Finals and Finals, this fragile house of cards collapsed.

The Cascading Misalignment​

As the bracket tree advanced, the connectors changed dynamically. Conn::Mid ( ├─) was inherently 3 characters wide, while Conn::Top (─┐) was 2 characters wide.

When these varying-width connectors interacted with the is_match_row hack, the engine's X-coordinates fell completely out of sync. A connector drawn on an odd row would push the subsequent column 2 spaces to the right, while one drawn on an even row might push it 3 spaces to the right.

The result? The vertical lines of the bracket tree looked like jagged teeth, breaking the layout entirely.

The Fix: Standardization over Hacks​

The root cause was trying to outsmart the terminal renderer by injecting fake characters. Modern TUI libraries (and crates like unicode-width) are generally robust enough to handle these calculations if we let them.

The solution was to revert to a strict, standardized grid:

  1. Remove the Fake Offset: We deleted FLAG_OFFSET entirely. The internal TUI coordinates must remain mathematically pure. If a specific terminal renders an emoji too wide, it's better to let that single column slightly overlap than to destroy the global grid coordinates.
  2. Standardize Connector Widths: We replaced the variable-length connectors and the is_match_row logic with a universal, 3-character-wide connector block:
Conn::Top => "─┐ ",
Conn::Bot => "─┘ ",
Conn::Mid => " ├─",
Conn::Vert => " │ ",
Conn::Empty => " ",

By ensuring that every single connector is exactly 3 characters wide, and every vertical segment (│, ┐, ├) aligns precisely at the second character index, the layout engine's X-coordinates remain perfectly synchronized across all rows.

Takeaway​

When building grid-based TUIs, never try to fix a visual terminal rendering quirk by polluting your logical character coordinate system. Standardize your component widths and trust your layout engine. A rigid, predictable grid will always outlast a clever string-padding hack.

Here is the final result of our perfectly aligned World Cup Knockout Bracket Tree, completely devoid of broken connectors or jagged lines:

Perfectly Aligned Knockout Bracket Tree