render: frame-paced presentation with per-row damage and blink

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I4e925b4d1d904d9592060e968d84ec906a6a6964
This commit is contained in:
raf 2026-06-25 08:17:55 +03:00
commit f1c8271d31
No known key found for this signature in database
GPG key ID: 29D95B64378DB4BF
4 changed files with 274 additions and 102 deletions

View file

@ -140,6 +140,8 @@ pub struct Grid {
/// How many lines the viewport is scrolled back from the live bottom.
view_offset: usize,
cursor_shape: CursorShape,
/// Whether the cursor shape is a blinking variant (DECSCUSR odd codes).
cursor_blink: bool,
cursor_visible: bool,
/// Application cursor-keys mode (DECCKM): arrows send SS3 instead of CSI.
app_cursor: bool,
@ -177,6 +179,7 @@ impl Grid {
scrollback: VecDeque::new(),
view_offset: 0,
cursor_shape: CursorShape::default(),
cursor_blink: false,
cursor_visible: true,
cursor_color: None,
app_cursor: false,
@ -265,6 +268,14 @@ impl Grid {
self.cursor_shape
}
pub fn set_cursor_blink(&mut self, blink: bool) {
self.cursor_blink = blink;
}
pub fn cursor_blink(&self) -> bool {
self.cursor_blink
}
pub fn set_cursor_visible(&mut self, visible: bool) {
self.cursor_visible = visible;
}
@ -819,6 +830,22 @@ impl Grid {
col >= lo && col <= hi
}
/// The inclusive `(lo, hi)` column span selected on absolute row `row`, if
/// any part of that row is selected.
pub fn selection_span_on(&self, row: usize) -> Option<(usize, usize)> {
let (start, end) = self.ordered_selection()?;
if row < start.row || row > end.row {
return None;
}
let lo = if row == start.row { start.col } else { 0 };
let hi = if row == end.row {
end.col
} else {
self.abs_row(row).len().saturating_sub(1)
};
Some((lo, hi))
}
/// The selected text, with trailing blanks trimmed per line and rows joined
/// by newlines. `None` if there is no selection.
pub fn selection_text(&self) -> Option<String> {