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