Skip to main content

par_term_terminal/terminal/
rendering.rs

1use super::TerminalManager;
2use par_term_config::{Cell, Theme};
3
4impl TerminalManager {
5    /// Get terminal grid with scrollback offset as Cell array for CellRenderer
6    pub fn get_cells_with_scrollback(
7        &self,
8        scroll_offset: usize,
9        selection: Option<((usize, usize), (usize, usize))>,
10        rectangular: bool,
11        _cursor: Option<((usize, usize), f32)>,
12    ) -> Vec<Cell> {
13        let pty = self.pty_session.lock();
14        let terminal = pty.terminal();
15        let mut term = terminal.lock();
16        let grid = term.active_grid();
17
18        let cursor_with_style = None;
19
20        let rows = grid.rows();
21        let cols = grid.cols();
22        let scrollback_len = grid.scrollback_len();
23        let clamped_offset = scroll_offset.min(scrollback_len);
24        let total_lines = scrollback_len + rows;
25        let end_line = total_lines.saturating_sub(clamped_offset);
26        let start_line = end_line.saturating_sub(rows);
27
28        let mut cells = Vec::with_capacity(rows * cols);
29
30        for line_idx in start_line..end_line {
31            let screen_row = line_idx - start_line;
32
33            if line_idx < scrollback_len {
34                if let Some(line) = grid.scrollback_line(line_idx) {
35                    Self::push_line_from_slice(
36                        line,
37                        cols,
38                        &mut cells,
39                        screen_row,
40                        selection,
41                        rectangular,
42                        cursor_with_style,
43                        &self.theme,
44                    );
45                } else {
46                    Self::push_empty_cells(cols, &mut cells);
47                }
48            } else {
49                let grid_row = line_idx - scrollback_len;
50                Self::push_grid_row(
51                    grid,
52                    grid_row,
53                    cols,
54                    &mut cells,
55                    screen_row,
56                    selection,
57                    rectangular,
58                    cursor_with_style,
59                    &self.theme,
60                );
61            }
62        }
63
64        // Apply trigger highlights on top of cell colors
65        let highlights = term.get_trigger_highlights();
66        for highlight in &highlights {
67            let abs_row = scrollback_len + highlight.row;
68            if abs_row < start_line || abs_row >= end_line {
69                continue;
70            }
71            let screen_row = abs_row - start_line;
72
73            for col in highlight.col_start..highlight.col_end.min(cols) {
74                let cell_idx = screen_row * cols + col;
75                if cell_idx < cells.len() {
76                    if let Some((r, g, b)) = highlight.fg {
77                        cells[cell_idx].fg_color = [r, g, b, 255];
78                    }
79                    if let Some((r, g, b)) = highlight.bg {
80                        cells[cell_idx].bg_color = [r, g, b, 255];
81                    }
82                }
83            }
84        }
85        term.clear_expired_highlights();
86
87        cells
88    }
89
90    #[allow(clippy::too_many_arguments)]
91    pub(crate) fn push_line_from_slice(
92        line: &[par_term_emu_core_rust::cell::Cell],
93        cols: usize,
94        dest: &mut Vec<Cell>,
95        screen_row: usize,
96        selection: Option<((usize, usize), (usize, usize))>,
97        rectangular: bool,
98        cursor: Option<(
99            (usize, usize),
100            f32,
101            par_term_emu_core_rust::cursor::CursorStyle,
102        )>,
103        theme: &Theme,
104    ) {
105        let copy_len = cols.min(line.len());
106        for (col, cell) in line[..copy_len].iter().enumerate() {
107            let is_selected = Self::is_cell_selected(col, screen_row, selection, rectangular);
108            let cursor_info = cursor.and_then(|((cx, cy), opacity, style)| {
109                if cx == col && cy == screen_row {
110                    Some((opacity, style))
111                } else {
112                    None
113                }
114            });
115            dest.push(Self::convert_term_cell_with_theme(
116                cell,
117                is_selected,
118                cursor_info,
119                theme,
120            ));
121        }
122
123        if copy_len < cols {
124            Self::push_empty_cells(cols - copy_len, dest);
125        }
126    }
127
128    #[allow(clippy::too_many_arguments)]
129    pub(crate) fn push_grid_row(
130        grid: &par_term_emu_core_rust::grid::Grid,
131        row: usize,
132        cols: usize,
133        dest: &mut Vec<Cell>,
134        screen_row: usize,
135        selection: Option<((usize, usize), (usize, usize))>,
136        rectangular: bool,
137        cursor: Option<(
138            (usize, usize),
139            f32,
140            par_term_emu_core_rust::cursor::CursorStyle,
141        )>,
142        theme: &Theme,
143    ) {
144        for col in 0..cols {
145            let is_selected = Self::is_cell_selected(col, screen_row, selection, rectangular);
146            let cursor_info = cursor.and_then(|((cx, cy), opacity, style)| {
147                if cx == col && cy == screen_row {
148                    Some((opacity, style))
149                } else {
150                    None
151                }
152            });
153            if let Some(cell) = grid.get(col, row) {
154                dest.push(Self::convert_term_cell_with_theme(
155                    cell,
156                    is_selected,
157                    cursor_info,
158                    theme,
159                ));
160            } else {
161                dest.push(Cell::default());
162            }
163        }
164    }
165
166    pub(crate) fn push_empty_cells(count: usize, dest: &mut Vec<Cell>) {
167        for _ in 0..count {
168            dest.push(Cell::default());
169        }
170    }
171
172    /// Check if a cell at (col, row) is within the selection range
173    pub(crate) fn is_cell_selected(
174        col: usize,
175        row: usize,
176        selection: Option<((usize, usize), (usize, usize))>,
177        rectangular: bool,
178    ) -> bool {
179        if let Some(((start_col, start_row), (end_col, end_row))) = selection {
180            if rectangular {
181                let min_col = start_col.min(end_col);
182                let max_col = start_col.max(end_col);
183                let min_row = start_row.min(end_row);
184                let max_row = start_row.max(end_row);
185
186                return col >= min_col && col <= max_col && row >= min_row && row <= max_row;
187            }
188
189            if start_row == end_row {
190                return row == start_row && col >= start_col && col <= end_col;
191            }
192
193            if row == start_row {
194                return col >= start_col;
195            } else if row == end_row {
196                return col <= end_col;
197            } else if row > start_row && row < end_row {
198                return true;
199            }
200        }
201        false
202    }
203
204    pub(crate) fn convert_term_cell_with_theme(
205        term_cell: &par_term_emu_core_rust::cell::Cell,
206        is_selected: bool,
207        cursor_info: Option<(f32, par_term_emu_core_rust::cursor::CursorStyle)>,
208        theme: &Theme,
209    ) -> Cell {
210        use par_term_emu_core_rust::color::{Color as TermColor, NamedColor};
211        use par_term_emu_core_rust::cursor::CursorStyle as TermCursorStyle;
212
213        let bg_rgb = term_cell.bg.to_rgb();
214        let fg_rgb = term_cell.fg.to_rgb();
215        let has_colored_bg = bg_rgb != (0, 0, 0);
216        let has_reverse = term_cell.flags.reverse();
217
218        if has_colored_bg || has_reverse {
219            log::debug!(
220                "Cell with colored BG or REVERSE: '{}' (U+{:04X}): fg={:?} (RGB:{},{},{}), bg={:?} (RGB:{},{},{}), reverse={}, flags={:?}",
221                if term_cell.c.is_control() {
222                    '?'
223                } else {
224                    term_cell.c
225                },
226                term_cell.c as u32,
227                term_cell.fg,
228                fg_rgb.0,
229                fg_rgb.1,
230                fg_rgb.2,
231                term_cell.bg,
232                bg_rgb.0,
233                bg_rgb.1,
234                bg_rgb.2,
235                has_reverse,
236                term_cell.flags
237            );
238        }
239
240        // Apply theme colors for ANSI colors (Named colors)
241        let fg = match &term_cell.fg {
242            TermColor::Named(named) => {
243                #[allow(unreachable_patterns)]
244                let theme_color = match named {
245                    NamedColor::Black => theme.black,
246                    NamedColor::Red => theme.red,
247                    NamedColor::Green => theme.green,
248                    NamedColor::Yellow => theme.yellow,
249                    NamedColor::Blue => theme.blue,
250                    NamedColor::Magenta => theme.magenta,
251                    NamedColor::Cyan => theme.cyan,
252                    NamedColor::White => theme.white,
253                    NamedColor::BrightBlack => theme.bright_black,
254                    NamedColor::BrightRed => theme.bright_red,
255                    NamedColor::BrightGreen => theme.bright_green,
256                    NamedColor::BrightYellow => theme.bright_yellow,
257                    NamedColor::BrightBlue => theme.bright_blue,
258                    NamedColor::BrightMagenta => theme.bright_magenta,
259                    NamedColor::BrightCyan => theme.bright_cyan,
260                    NamedColor::BrightWhite => theme.bright_white,
261                    _ => theme.foreground,
262                };
263                (theme_color.r, theme_color.g, theme_color.b)
264            }
265            _ => term_cell.fg.to_rgb(),
266        };
267
268        let bg = match &term_cell.bg {
269            TermColor::Named(named) => {
270                #[allow(unreachable_patterns)]
271                let theme_color = match named {
272                    NamedColor::Black => theme.black,
273                    NamedColor::Red => theme.red,
274                    NamedColor::Green => theme.green,
275                    NamedColor::Yellow => theme.yellow,
276                    NamedColor::Blue => theme.blue,
277                    NamedColor::Magenta => theme.magenta,
278                    NamedColor::Cyan => theme.cyan,
279                    NamedColor::White => theme.white,
280                    NamedColor::BrightBlack => theme.bright_black,
281                    NamedColor::BrightRed => theme.bright_red,
282                    NamedColor::BrightGreen => theme.bright_green,
283                    NamedColor::BrightYellow => theme.bright_yellow,
284                    NamedColor::BrightBlue => theme.bright_blue,
285                    NamedColor::BrightMagenta => theme.bright_magenta,
286                    NamedColor::BrightCyan => theme.bright_cyan,
287                    NamedColor::BrightWhite => theme.bright_white,
288                    _ => theme.background,
289                };
290                (theme_color.r, theme_color.g, theme_color.b)
291            }
292            _ => term_cell.bg.to_rgb(),
293        };
294
295        let is_reverse = term_cell.flags.reverse();
296
297        let (fg_color, bg_color) = if let Some((opacity, style)) = cursor_info {
298            let blend = |normal: u8, inverted: u8, opacity: f32| -> u8 {
299                (normal as f32 * (1.0 - opacity) + inverted as f32 * opacity) as u8
300            };
301
302            match style {
303                TermCursorStyle::SteadyBlock | TermCursorStyle::BlinkingBlock => (
304                    [
305                        blend(fg.0, bg.0, opacity),
306                        blend(fg.1, bg.1, opacity),
307                        blend(fg.2, bg.2, opacity),
308                        255,
309                    ],
310                    [
311                        blend(bg.0, fg.0, opacity),
312                        blend(bg.1, fg.1, opacity),
313                        blend(bg.2, fg.2, opacity),
314                        255,
315                    ],
316                ),
317                TermCursorStyle::SteadyBar
318                | TermCursorStyle::BlinkingBar
319                | TermCursorStyle::SteadyUnderline
320                | TermCursorStyle::BlinkingUnderline => (
321                    [
322                        blend(fg.0, bg.0, opacity),
323                        blend(fg.1, bg.1, opacity),
324                        blend(fg.2, bg.2, opacity),
325                        255,
326                    ],
327                    [
328                        blend(bg.0, fg.0, opacity),
329                        blend(bg.1, fg.1, opacity),
330                        blend(bg.2, fg.2, opacity),
331                        255,
332                    ],
333                ),
334            }
335        } else if is_selected || is_reverse {
336            ([bg.0, bg.1, bg.2, 255], [fg.0, fg.1, fg.2, 255])
337        } else {
338            ([fg.0, fg.1, fg.2, 255], [bg.0, bg.1, bg.2, 255])
339        };
340
341        let grapheme = if term_cell.has_combining_chars() {
342            term_cell.get_grapheme()
343        } else {
344            term_cell.base_char().to_string()
345        };
346
347        Cell {
348            grapheme,
349            fg_color,
350            bg_color,
351            bold: term_cell.flags.bold(),
352            italic: term_cell.flags.italic(),
353            underline: term_cell.flags.underline(),
354            strikethrough: term_cell.flags.strikethrough(),
355            hyperlink_id: term_cell.flags.hyperlink_id,
356            wide_char: term_cell.flags.wide_char(),
357            wide_char_spacer: term_cell.flags.wide_char_spacer(),
358        }
359    }
360}