par_term/terminal/
rendering.rs

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