TUI Emoji Alignment Pitfalls: The Danger of Manual Offsets
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:
- Remove the Fake Offset: We deleted
FLAG_OFFSETentirely. 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. - Standardize Connector Widths: We replaced the variable-length connectors and the
is_match_rowlogic 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:

