Files
codex/codex-rs/tui/src/line_truncation.rs
T
Joe Savona 1a2f5babe8 Add inherited styling to TUI display trees (#59988)
## What changed

Support `bold`, `italic`, and `underline` nodes, plus text properties for color, weight, italics, and underlining. Nested text can override inherited styles, including disabling modifiers.

Add row backgrounds that apply to children and gaps. Text colors and row backgrounds accept the eight standard ANSI color names or `#RRGGBB` values, with RGB colors resolved through the terminal palette. Preserve the last visible span's style on overflow ellipses while keeping them unlinked.

## Testing

Add coverage for style serialization and invalid values, nested modifier depth limits, inherited styles and overrides, row backgrounds, styled truncation, and foreground/background palette resolution, including a rendered buffer snapshot.

GitOrigin-RevId: 6b19b6b740cd754890c8adb489fba414a6ee62d3
2026-11-06 21:36:56 +00:00

115 lines
3.1 KiB
Rust

use ratatui::buffer::CellWidth;
use ratatui::style::Style;
use ratatui::text::Line;
use ratatui::text::Span;
/// Paragraph paints each visible grapheme separately, even when the whole string
/// has a narrower Unicode width (for example, Arabic lam followed by alef).
pub(crate) fn span_width(span: &Span<'_>) -> usize {
span.styled_graphemes(Style::default())
.map(|g| usize::from(g.symbol.cell_width()))
.sum()
}
pub(crate) fn line_width(line: &Line<'_>) -> usize {
line.iter().map(span_width).sum()
}
pub(crate) fn truncate_line_to_width(line: Line<'static>, max_width: usize) -> Line<'static> {
if max_width == 0 {
return Line::from(Vec::<Span<'static>>::new());
}
let Line {
style,
alignment,
spans,
} = line;
let mut used = 0usize;
let mut spans_out: Vec<Span<'static>> = Vec::with_capacity(spans.len());
for span in spans {
let span_width = span_width(&span);
if span_width == 0 {
spans_out.push(span);
continue;
}
if used >= max_width {
break;
}
if used + span_width <= max_width {
used += span_width;
spans_out.push(span);
continue;
}
let mut prefix = String::new();
for grapheme in span.styled_graphemes(Style::default()) {
let grapheme_width = usize::from(grapheme.symbol.cell_width());
if used + grapheme_width > max_width {
break;
}
prefix.push_str(grapheme.symbol);
used += grapheme_width;
}
if !prefix.is_empty() {
spans_out.push(Span::styled(prefix, span.style));
}
break;
}
Line {
style,
alignment,
spans: spans_out,
}
}
/// Truncate a styled line to `max_width` and append an ellipsis on overflow.
///
/// Intended for short UI rows. This preserves a fast no-overflow path (width
/// pre-scan + return original line unchanged) and uses `truncate_line_to_width`
/// for the overflow case.
/// Performance should be reevaluated if using this method in loops/over larger content in the future.
pub(crate) fn truncate_line_with_ellipsis_if_overflow(
line: Line<'static>,
max_width: usize,
) -> Line<'static> {
if max_width == 0 {
return Line::from(Vec::<Span<'static>>::new());
}
if line_width(&line) <= max_width {
return line;
}
truncate_line_with_ellipsis(line, max_width)
}
/// Fit an already overflowing line into a nonempty area, including the ellipsis.
pub(crate) fn truncate_line_with_ellipsis(line: Line<'static>, max_width: usize) -> Line<'static> {
debug_assert!(max_width > 0);
let truncated = truncate_line_to_width(line, max_width.saturating_sub(1));
let Line {
style,
alignment,
mut spans,
} = truncated;
let ellipsis_style = spans.last().map(|span| span.style).unwrap_or_default();
spans.push(Span::styled("…", ellipsis_style));
Line {
style,
alignment,
spans,
}
}
#[cfg(test)]
#[path = "line_truncation_tests.rs"]
mod tests;