Skip to main content

rmux_core/screen/
view.rs

1use crate::grid::{GridLine, GridLineFlags};
2use crate::input::{Colour, COLOUR_DEFAULT};
3
4use super::Screen;
5
6/// Borrowed read-only view of one rendered screen cell.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct ScreenCellRef<'a> {
9    text: &'a str,
10    width: u8,
11    padding: bool,
12    attr: u16,
13    fg: Colour,
14    bg: Colour,
15    us: Colour,
16    link: u32,
17}
18
19impl<'a> ScreenCellRef<'a> {
20    /// Returns the stored cell text.
21    #[must_use]
22    pub const fn text(&self) -> &'a str {
23        self.text
24    }
25
26    /// Returns the display width of the cell.
27    #[must_use]
28    pub const fn width(&self) -> u8 {
29        self.width
30    }
31
32    /// Returns whether this cell is padding for a wide glyph.
33    #[must_use]
34    pub const fn is_padding(&self) -> bool {
35        self.padding
36    }
37
38    /// Returns the cell attributes.
39    #[must_use]
40    pub const fn attr(&self) -> u16 {
41        self.attr
42    }
43
44    /// Returns the foreground colour.
45    #[must_use]
46    pub const fn fg(&self) -> Colour {
47        self.fg
48    }
49
50    /// Returns the background colour.
51    #[must_use]
52    pub const fn bg(&self) -> Colour {
53        self.bg
54    }
55
56    /// Returns the underline colour.
57    #[must_use]
58    pub const fn us(&self) -> Colour {
59        self.us
60    }
61
62    /// Returns the hyperlink inner ID for the cell.
63    #[must_use]
64    pub const fn link(&self) -> u32 {
65        self.link
66    }
67}
68
69fn blank_cell_ref() -> ScreenCellRef<'static> {
70    ScreenCellRef {
71        text: " ",
72        width: 1,
73        padding: false,
74        attr: 0,
75        fg: COLOUR_DEFAULT,
76        bg: COLOUR_DEFAULT,
77        us: COLOUR_DEFAULT,
78        link: 0,
79    }
80}
81
82/// Read-only copy of one rendered screen cell for copy-mode consumers.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ScreenCellView {
85    pub(super) text: String,
86    pub(super) width: u8,
87    pub(super) padding: bool,
88    pub(super) attr: u16,
89    pub(super) fg: crate::input::Colour,
90    pub(super) bg: crate::input::Colour,
91    pub(super) us: crate::input::Colour,
92    pub(super) link: u32,
93}
94
95impl ScreenCellView {
96    /// Returns the stored cell text.
97    #[must_use]
98    pub fn text(&self) -> &str {
99        &self.text
100    }
101
102    /// Returns the display width of the cell.
103    #[must_use]
104    pub const fn width(&self) -> u8 {
105        self.width
106    }
107
108    /// Returns whether the cell is padding for a wide glyph.
109    #[must_use]
110    pub const fn is_padding(&self) -> bool {
111        self.padding
112    }
113
114    /// Returns the cell attributes.
115    #[must_use]
116    pub const fn attr(&self) -> u16 {
117        self.attr
118    }
119
120    /// Returns the cell foreground colour.
121    #[must_use]
122    pub const fn fg(&self) -> crate::input::Colour {
123        self.fg
124    }
125
126    /// Returns the cell background colour.
127    #[must_use]
128    pub const fn bg(&self) -> crate::input::Colour {
129        self.bg
130    }
131
132    /// Returns the cell underline colour.
133    #[must_use]
134    pub const fn us(&self) -> crate::input::Colour {
135        self.us
136    }
137
138    /// Returns the hyperlink inner ID for the cell.
139    #[must_use]
140    pub const fn link(&self) -> u32 {
141        self.link
142    }
143}
144
145/// Read-only copy of one absolute screen line for copy-mode consumers.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct ScreenLineView {
148    pub(super) cells: Vec<ScreenCellView>,
149    width: u32,
150    pub(super) wrapped: bool,
151    pub(super) start_prompt: bool,
152    pub(super) start_output: bool,
153    pub(super) time: i64,
154}
155
156impl ScreenLineView {
157    /// Returns the stored cells for the line.
158    #[must_use]
159    pub fn cells(&self) -> &[ScreenCellView] {
160        &self.cells
161    }
162
163    /// Returns the terminal-width column span represented by this line.
164    #[must_use]
165    pub const fn width(&self) -> u32 {
166        self.width
167    }
168
169    /// Returns one cell by column.
170    #[must_use]
171    pub fn cell(&self, x: u32) -> Option<&ScreenCellView> {
172        self.cells.get(x as usize)
173    }
174
175    /// Returns whether the line wraps onto the following row.
176    #[must_use]
177    pub const fn wrapped(&self) -> bool {
178        self.wrapped
179    }
180
181    /// Returns whether the line starts a shell prompt block.
182    #[must_use]
183    pub const fn start_prompt(&self) -> bool {
184        self.start_prompt
185    }
186
187    /// Returns whether the line starts a shell output block.
188    #[must_use]
189    pub const fn start_output(&self) -> bool {
190        self.start_output
191    }
192
193    /// Returns the line timestamp.
194    #[must_use]
195    pub const fn time(&self) -> i64 {
196        self.time
197    }
198
199    /// Resolves the owning non-padding cell for a column.
200    #[must_use]
201    pub fn owning_cell_x(&self, x: u32) -> Option<u32> {
202        if x >= self.width {
203            return None;
204        }
205        let Some(cell) = self.cell(x) else {
206            return Some(x);
207        };
208        if !cell.is_padding() {
209            return Some(x);
210        }
211
212        let mut owner = x;
213        while owner > 0 {
214            owner -= 1;
215            let candidate = self.cell(owner)?;
216            if !candidate.is_padding() {
217                let width = u32::from(candidate.width().max(1));
218                if owner.saturating_add(width) > x {
219                    return Some(owner);
220                }
221                return None;
222            }
223        }
224        None
225    }
226}
227
228impl Screen {
229    /// Visits borrowed cells for one visible row, padding to `cols` cells.
230    ///
231    /// Returns `false` when `row` is outside the visible viewport. Plain ASCII
232    /// compact rows are visited directly from their compact text storage, so
233    /// callers that only need the visible viewport can avoid the owned
234    /// [`ScreenLineView`] allocation path.
235    pub fn visit_visible_line_cells(
236        &self,
237        row: usize,
238        cols: usize,
239        mut visit: impl FnMut(ScreenCellRef<'_>),
240    ) -> bool {
241        let Some(line) = self
242            .grid
243            .visible_line(u32::try_from(row).unwrap_or(u32::MAX))
244        else {
245            return false;
246        };
247        if let Some(text) = line.plain_text() {
248            let text_cols = text.len().min(cols);
249            for col in 0..text_cols {
250                visit(ScreenCellRef {
251                    text: &text[col..col + 1],
252                    width: 1,
253                    padding: false,
254                    attr: 0,
255                    fg: COLOUR_DEFAULT,
256                    bg: COLOUR_DEFAULT,
257                    us: COLOUR_DEFAULT,
258                    link: 0,
259                });
260            }
261            for _ in text_cols..cols {
262                visit(blank_cell_ref());
263            }
264            return true;
265        }
266
267        let mut emitted = 0_usize;
268        for cell in line.cells().iter().take(cols) {
269            visit(ScreenCellRef {
270                text: cell.text(),
271                width: cell.width(),
272                padding: cell.is_padding(),
273                attr: cell.attr(),
274                fg: cell.fg(),
275                bg: cell.bg(),
276                us: cell.us(),
277                link: cell.link(),
278            });
279            emitted += 1;
280        }
281        for _ in emitted..cols {
282            visit(blank_cell_ref());
283        }
284        true
285    }
286
287    /// Returns a read-only copy of one absolute line.
288    #[must_use]
289    pub fn absolute_line_view(&self, absolute_y: usize) -> Option<ScreenLineView> {
290        let line = self.grid.absolute_line(absolute_y)?;
291        let width = u32::from(self.grid.size().cols.max(1));
292        let cells = if let Some(text) = line.plain_text() {
293            let mut cells = text
294                .bytes()
295                .map(|byte| ScreenCellView {
296                    text: char::from(byte).to_string(),
297                    width: 1,
298                    padding: false,
299                    attr: 0,
300                    fg: crate::input::COLOUR_DEFAULT,
301                    bg: crate::input::COLOUR_DEFAULT,
302                    us: crate::input::COLOUR_DEFAULT,
303                    link: 0,
304                })
305                .collect::<Vec<_>>();
306            cells.resize_with(width as usize, || ScreenCellView {
307                text: " ".to_owned(),
308                width: 1,
309                padding: false,
310                attr: 0,
311                fg: crate::input::COLOUR_DEFAULT,
312                bg: crate::input::COLOUR_DEFAULT,
313                us: crate::input::COLOUR_DEFAULT,
314                link: 0,
315            });
316            cells
317        } else {
318            line.cells()
319                .iter()
320                .map(|cell| ScreenCellView {
321                    text: cell.text().to_owned(),
322                    width: cell.width(),
323                    padding: cell.is_padding(),
324                    attr: cell.attr(),
325                    fg: cell.fg(),
326                    bg: cell.bg(),
327                    us: cell.us(),
328                    link: cell.link(),
329                })
330                .collect()
331        };
332        Some(ScreenLineView {
333            cells,
334            width,
335            wrapped: line.flags().contains(GridLineFlags::WRAPPED),
336            start_prompt: line.flags().contains(GridLineFlags::START_PROMPT),
337            start_output: line.flags().contains(GridLineFlags::START_OUTPUT),
338            time: line.time(),
339        })
340    }
341
342    /// Clones the screen as a standalone viewport over its absolute lines.
343    #[must_use]
344    pub fn clone_viewport(&self, top_line: usize, cursor_x: u32, cursor_absolute_y: usize) -> Self {
345        let size = self.grid.size();
346        let rows = usize::from(size.rows.max(1));
347        let cols = u32::from(size.cols.max(1));
348        let total_lines = self.absolute_line_count();
349        let top_line = top_line.min(total_lines.saturating_sub(rows));
350        let mut viewport = Self::new(size, 0);
351
352        viewport.mode = self.mode;
353        viewport.cursor_style = self.cursor_style;
354        viewport.title = self.title.clone();
355        viewport.window_name = self.window_name.clone();
356        viewport.path = self.path.clone();
357        viewport.title_stack = self.title_stack.clone();
358        viewport.hyperlinks = self.hyperlinks.clone();
359        viewport.active_hyperlink = self.active_hyperlink;
360        viewport.bell_count = 0;
361        viewport.utf8_config = self.utf8_config.clone();
362
363        let lines = (0..rows)
364            .map(|offset| {
365                self.grid
366                    .absolute_line(top_line + offset)
367                    .cloned()
368                    .unwrap_or_else(|| GridLine::new(cols))
369            })
370            .collect();
371        viewport.grid.replace_visible(lines);
372
373        viewport.cursor_x = cursor_x.min(viewport.max_cursor_x());
374        viewport.cursor_y = if (top_line..top_line + rows).contains(&cursor_absolute_y) {
375            (cursor_absolute_y - top_line) as u32
376        } else {
377            0
378        };
379        viewport.pending_wrap = false;
380        viewport.saved_cursor_x = None;
381        viewport.saved_cursor_y = None;
382        viewport.saved_cursor_pending_wrap = false;
383        viewport.saved_grid = None;
384        viewport.rupper = 0;
385        viewport.rlower = u32::from(size.rows.max(1)).saturating_sub(1);
386        viewport.reset_tabs();
387        viewport
388    }
389}