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