Skip to main content

emux_render/
text.rs

1//! Text rendering: cell-to-terminal-output conversion.
2
3use crossterm::style::{Attribute, Color as CtColor, ContentStyle};
4use emux_term::grid::{Cell, UnderlineStyle};
5use emux_term::Color;
6
7/// Convert an `emux_term::Color` to a crossterm `Color`.
8pub fn color_to_crossterm(color: &Color) -> CtColor {
9    match color {
10        Color::Default => CtColor::Reset,
11        Color::Indexed(idx) => CtColor::AnsiValue(*idx),
12        Color::Rgb(r, g, b) => CtColor::Rgb { r: *r, g: *g, b: *b },
13    }
14}
15
16/// Build a `ContentStyle` from a cell's attributes and colors.
17pub fn cell_style(cell: &Cell) -> ContentStyle {
18    let mut style = ContentStyle::new();
19    style.foreground_color = Some(color_to_crossterm(&cell.fg));
20    style.background_color = Some(color_to_crossterm(&cell.bg));
21
22    if cell.attrs.bold {
23        style.attributes.set(Attribute::Bold);
24    }
25    if cell.attrs.italic {
26        style.attributes.set(Attribute::Italic);
27    }
28    match cell.attrs.underline {
29        UnderlineStyle::None => {}
30        UnderlineStyle::Single => {
31            style.attributes.set(Attribute::Underlined);
32        }
33        UnderlineStyle::Double => {
34            style.attributes.set(Attribute::DoubleUnderlined);
35        }
36        UnderlineStyle::Curly => {
37            style.attributes.set(Attribute::Undercurled);
38        }
39    }
40    if cell.attrs.blink {
41        style.attributes.set(Attribute::SlowBlink);
42    }
43    if cell.attrs.reverse {
44        style.attributes.set(Attribute::Reverse);
45    }
46    if cell.attrs.invisible {
47        style.attributes.set(Attribute::Hidden);
48    }
49    if cell.attrs.strikethrough {
50        style.attributes.set(Attribute::CrossedOut);
51    }
52
53    style
54}
55
56/// Convert a row of cells into a sequence of styled text spans.
57///
58/// Adjacent cells with the same style are coalesced into a single span.
59/// Wide-char continuation cells (width == 0) are skipped.  The output
60/// is padded with spaces to exactly `width` columns.
61pub fn render_row(cells: &[Cell], width: usize) -> Vec<(ContentStyle, String)> {
62    let mut spans: Vec<(ContentStyle, String)> = Vec::new();
63    let mut col = 0;
64
65    for cell in cells.iter().take(width) {
66        // Skip continuation cells for wide characters
67        if cell.width == 0 {
68            col += 1;
69            continue;
70        }
71
72        let style = cell_style(cell);
73        let ch = if cell.c < ' ' { ' ' } else { cell.c };
74
75        if let Some(last) = spans.last_mut() {
76            if last.0 == style {
77                last.1.push(ch);
78            } else {
79                spans.push((style, ch.to_string()));
80            }
81        } else {
82            spans.push((style, ch.to_string()));
83        }
84
85        col += cell.width as usize;
86    }
87
88    // Pad to the full width if needed
89    while col < width {
90        let style = ContentStyle::new();
91        if let Some(last) = spans.last_mut() {
92            if last.0 == style {
93                last.1.push(' ');
94            } else {
95                spans.push((style, " ".to_string()));
96            }
97        } else {
98            spans.push((style, " ".to_string()));
99        }
100        col += 1;
101    }
102
103    spans
104}