aboutsummaryrefslogtreecommitdiff
path: root/src/protocol/render_ansi.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/protocol/render_ansi.rs')
-rw-r--r--src/protocol/render_ansi.rs375
1 files changed, 371 insertions, 4 deletions
diff --git a/src/protocol/render_ansi.rs b/src/protocol/render_ansi.rs
index 97685d4d..fe3ab68f 100644
--- a/src/protocol/render_ansi.rs
+++ b/src/protocol/render_ansi.rs
@@ -31,7 +31,9 @@ use std::io::Write;
use unicode_width::UnicodeWidthStr;
-use crate::protocol::{underline_style_from_modifier, CellData, FrameData};
+use crate::protocol::{
+ underline_style_from_modifier, CellData, CursorState, FrameData, PaneSurfacePatchRow,
+};
const REVERSED_MODIFIER: u16 = 1 << 6;
const SYNC_OUTPUT_END: &[u8] = b"\x1b[?2026l";
@@ -135,8 +137,101 @@ impl BlitEncoder {
self.last_frame.as_ref() == Some(frame)
}
- pub(crate) fn last_frame(&self) -> Option<&FrameData> {
- self.last_frame.as_ref()
+ pub(crate) fn encode_patch(
+ &self,
+ rows: &[PaneSurfacePatchRow],
+ cursor: Option<CursorState>,
+ suppress_visible_cursor: bool,
+ ) -> Option<EncodedBlit> {
+ let frame = self.last_frame.as_ref()?;
+ if rows.iter().any(|row| !patch_row_fits(frame, row)) || patch_rows_overlap(rows) {
+ return None;
+ }
+ let mut bytes = Vec::new();
+ let mut next_last_visible_cursor = self.last_visible_cursor;
+ let mut next_last_cursor_shape = self.last_cursor_shape;
+ blit_patch_to(
+ &mut bytes,
+ frame,
+ rows,
+ cursor,
+ &mut next_last_visible_cursor,
+ &mut next_last_cursor_shape,
+ repeat_ime_anchor_after_sync(),
+ suppress_visible_cursor,
+ );
+ Some(EncodedBlit {
+ bytes,
+ full: false,
+ next_last_visible_cursor,
+ next_last_cursor_shape,
+ })
+ }
+
+ pub(crate) fn patch_rows_with_drawn_cursor(
+ &self,
+ rows: &[PaneSurfacePatchRow],
+ cursor: Option<&CursorState>,
+ ) -> Option<Vec<PaneSurfacePatchRow>> {
+ let frame = self.last_frame.as_ref()?;
+ let mut rows = rows.to_vec();
+ let previous = frame
+ .cursor
+ .as_ref()
+ .filter(|cursor| cursor.visible)
+ .map(|cursor| clamp_cursor_position(frame, cursor.x, cursor.y));
+ let next = cursor
+ .filter(|cursor| cursor.visible)
+ .map(|cursor| clamp_cursor_position(frame, cursor.x, cursor.y));
+
+ if let Some((x, y)) = previous.filter(|position| Some(*position) != next) {
+ if patch_cell_mut(&mut rows, x, y).is_none() {
+ let mut cell = frame.cells.get(frame_cell_index(frame, x, y)?)?.clone();
+ cell.modifier ^= REVERSED_MODIFIER;
+ rows.push(PaneSurfacePatchRow {
+ x,
+ y,
+ cells: vec![cell],
+ });
+ }
+ }
+ if let Some((x, y)) = next {
+ if let Some(cell) = patch_cell_mut(&mut rows, x, y) {
+ cell.modifier ^= REVERSED_MODIFIER;
+ } else if previous != next {
+ let mut cell = frame.cells.get(frame_cell_index(frame, x, y)?)?.clone();
+ cell.modifier ^= REVERSED_MODIFIER;
+ rows.push(PaneSurfacePatchRow {
+ x,
+ y,
+ cells: vec![cell],
+ });
+ }
+ }
+ Some(rows)
+ }
+
+ pub(crate) fn commit_patch(
+ &mut self,
+ rows: &[PaneSurfacePatchRow],
+ cursor: Option<CursorState>,
+ encoded: EncodedBlit,
+ ) -> bool {
+ let Some(frame) = self.last_frame.as_mut() else {
+ return false;
+ };
+ for row in rows {
+ let start = usize::from(row.y) * usize::from(frame.width) + usize::from(row.x);
+ let end = start + row.cells.len();
+ let Some(target) = frame.cells.get_mut(start..end) else {
+ return false;
+ };
+ target.clone_from_slice(&row.cells);
+ }
+ frame.cursor = cursor;
+ self.last_visible_cursor = encoded.next_last_visible_cursor;
+ self.last_cursor_shape = encoded.next_last_cursor_shape;
+ true
}
}
@@ -373,7 +468,6 @@ fn build_sgr(fg: u32, bg: u32, modifier: u16) -> String {
// ---------------------------------------------------------------------------
/// Checks if two cells are visually identical.
-#[cfg(test)]
fn cells_equal(a: &CellData, b: &CellData) -> bool {
a.symbol == b.symbol
&& a.fg == b.fg
@@ -444,6 +538,116 @@ fn blit_frame_to_with_cursor_memory_and_policy(
);
}
+fn frame_cell_index(frame: &FrameData, x: u16, y: u16) -> Option<usize> {
+ (x < frame.width && y < frame.height)
+ .then(|| usize::from(y) * usize::from(frame.width) + usize::from(x))
+}
+
+fn patch_cell_mut(rows: &mut [PaneSurfacePatchRow], x: u16, y: u16) -> Option<&mut CellData> {
+ rows.iter_mut().rev().find_map(|row| {
+ if row.y != y || x < row.x {
+ return None;
+ }
+ row.cells.get_mut(usize::from(x - row.x))
+ })
+}
+
+fn patch_rows_overlap(rows: &[PaneSurfacePatchRow]) -> bool {
+ rows.iter().enumerate().any(|(index, left)| {
+ let left_end = left.x.saturating_add(left.cells.len() as u16);
+ rows[index + 1..].iter().any(|right| {
+ if left.y != right.y {
+ return false;
+ }
+ let right_end = right.x.saturating_add(right.cells.len() as u16);
+ left.x < right_end && right.x < left_end
+ })
+ })
+}
+
+fn patch_row_fits(frame: &FrameData, row: &PaneSurfacePatchRow) -> bool {
+ let Ok(len) = u16::try_from(row.cells.len()) else {
+ return false;
+ };
+ if row.y >= frame.height
+ || row.x.saturating_add(len) > frame.width
+ || row.cells.iter().any(|cell| cell.hyperlink.is_some())
+ {
+ return false;
+ }
+ let start = usize::from(row.y) * usize::from(frame.width) + usize::from(row.x);
+ let end = start + row.cells.len();
+ frame
+ .cells
+ .get(start..end)
+ .is_some_and(|cells| cells.iter().all(|cell| cell.hyperlink.is_none()))
+}
+
+fn blit_patch_to(
+ mut writer: impl Write,
+ frame: &FrameData,
+ rows: &[PaneSurfacePatchRow],
+ cursor: Option<CursorState>,
+ last_visible_cursor: &mut Option<(u16, u16)>,
+ last_cursor_shape: &mut u8,
+ repeat_ime_anchor: bool,
+ suppress_visible_cursor: bool,
+) {
+ let _ = writer.write_all(b"\x1b[?2026h\x1b[?25l\x1b]8;;\x1b\\");
+ let mut last_sgr = String::new();
+ let mut active_hyperlink = None;
+ for row in rows {
+ let mut invalidated = 0usize;
+ let mut to_skip = 0usize;
+ let mut next_inline_col = None;
+ for (offset, cell) in row.cells.iter().enumerate() {
+ let col = row.x + offset as u16;
+ let idx = usize::from(row.y) * usize::from(frame.width) + usize::from(col);
+ let prev_cell = &frame.cells[idx];
+ if !cell.skip && (!cells_equal(cell, prev_cell) || invalidated > 0) && to_skip == 0 {
+ let cursor_position =
+ (next_inline_col != Some(col) || invalidated > 0).then_some((col, row.y));
+ write_cell(
+ &mut writer,
+ cursor_position,
+ cell,
+ &mut last_sgr,
+ &mut active_hyperlink,
+ frame,
+ );
+ next_inline_col = (cell.symbol.is_ascii() && cell_width(cell) == 1)
+ .then_some(col.saturating_add(1));
+ }
+ to_skip = cell_width(cell).saturating_sub(1);
+ let affected_width = cmp::max(cell_width(cell), cell_width(prev_cell));
+ invalidated = cmp::max(affected_width, invalidated).saturating_sub(1);
+ }
+ }
+ close_hyperlink(&mut writer, &mut active_hyperlink);
+ if !last_sgr.is_empty() {
+ let _ = writer.write_all(b"\x1b[0m");
+ }
+
+ let cursor_frame = FrameData {
+ cells: Vec::new(),
+ width: frame.width,
+ height: frame.height,
+ cursor,
+ hyperlinks: Vec::new(),
+ graphics: Vec::new(),
+ };
+ let mut host_cursor = resolve_host_cursor_state(&cursor_frame, last_visible_cursor);
+ if suppress_visible_cursor && host_cursor.visible {
+ host_cursor.visible = false;
+ }
+ write_host_cursor_state(&mut writer, host_cursor, last_cursor_shape);
+ let _ = writer.write_all(b"\x1b[?2026l");
+ if repeat_ime_anchor {
+ write_ime_anchor_cursor_state(&mut writer, host_cursor);
+ }
+ let _ = writer.flush();
+}
+
fn blit_frame_to_with_cursor_memory_and_clear_policy(
mut writer: impl Write,
frame: &FrameData,
@@ -1545,6 +1749,169 @@ mod tests {
}
#[test]
+ fn retained_patch_matches_full_diff_and_updates_the_encoder_baseline() {
+ let previous = make_frame(
+ 4,
+ 2,
+ vec![
+ make_cell("a", 0, 0, 0),
+ make_cell("b", 0, 0, 0),
+ make_cell("c", 0, 0, 0),
+ make_cell("d", 0, 0, 0),
+ make_cell("e", 0, 0, 0),
+ make_cell("f", 0, 0, 0),
+ make_cell("g", 0, 0, 0),
+ make_cell("h", 0, 0, 0),
+ ],
+ );
+ let mut encoder = BlitEncoder::new();
+ let initial = encoder.encode(&previous, false);
+ encoder.commit(previous.clone(), initial);
+
+ let rows = vec![PaneSurfacePatchRow {
+ x: 0,
+ y: 1,
+ cells: vec![
+ make_cell("E", 0, 0, 0),
+ make_cell("f", 0, 0, 0),
+ make_cell("G", 0, 0, 0),
+ make_cell("h", 0, 0, 0),
+ ],
+ }];
+ let cursor = Some(CursorState {
+ x: 3,
+ y: 1,
+ visible: true,
+ shape: 2,
+ });
+ let mut expected = previous;
+ expected.cells[4..8].clone_from_slice(&rows[0].cells);
+ expected.cursor = cursor.clone();
+
+ let full_diff = encoder.encode(&expected, false);
+ let patch = encoder
+ .encode_patch(&rows, cursor.clone(), false)
+ .expect("valid retained patch");
+ assert_eq!(patch.bytes, full_diff.bytes);
+ assert!(encoder.commit_patch(&rows, cursor, patch));
+ assert!(encoder.is_current(&expected));
+ }
+
+ #[test]
+ fn retained_patch_width_transition_matches_full_diff_with_following_cell() {
+ let previous = make_frame(
+ 3,
+ 1,
+ vec![
+ make_cell("界", 0, 0, 0),
+ make_cell("z", 0, 0, 0),
+ make_cell("q", 0, 0, 0),
+ ],
+ );
+ let mut encoder = BlitEncoder::new();
+ let initial = encoder.encode(&previous, false);
+ encoder.commit(previous.clone(), initial);
+
+ let rows = vec![PaneSurfacePatchRow {
+ x: 0,
+ y: 0,
+ cells: vec![make_cell("x", 0, 0, 0), make_cell("z", 0, 0, 0)],
+ }];
+ let mut expected = previous;
+ expected.cells[0..2].clone_from_slice(&rows[0].cells);
+
+ let full_diff = encoder.encode(&expected, false);
+ let patch = encoder
+ .encode_patch(&rows, None, false)
+ .expect("valid retained patch");
+ assert_eq!(patch.bytes, full_diff.bytes);
+ }
+
+ #[test]
+ fn retained_patch_rejects_overlapping_rows() {
+ let frame = make_frame(
+ 3,
+ 1,
+ vec![
+ make_cell("a", 0, 0, 0),
+ make_cell("b", 0, 0, 0),
+ make_cell("c", 0, 0, 0),
+ ],
+ );
+ let mut encoder = BlitEncoder::new();
+ let initial = encoder.encode(&frame, false);
+ encoder.commit(frame, initial);
+ let rows = vec![
+ PaneSurfacePatchRow {
+ x: 0,
+ y: 0,
+ cells: vec![make_cell("A", 0, 0, 0), make_cell("B", 0, 0, 0)],
+ },
+ PaneSurfacePatchRow {
+ x: 1,
+ y: 0,
+ cells: vec![make_cell("C", 0, 0, 0)],
+ },
+ ];
+
+ assert!(encoder.encode_patch(&rows, None, false).is_none());
+ }
+
+ #[test]
+ fn retained_patch_preserves_the_client_drawn_cursor_overlay() {
+ let mut previous = make_frame(
+ 3,
+ 1,
+ vec![
+ make_cell("a", 0, 0, 0),
+ make_cell("b", 0, 0, 0),
+ make_cell("c", 0, 0, 0),
+ ],
+ );
+ previous.cursor = Some(CursorState {
+ x: 0,
+ y: 0,
+ visible: true,
+ shape: 0,
+ });
+ let previous_drawn = frame_with_drawn_cursor(previous.clone());
+ let mut encoder = BlitEncoder::new();
+ let initial = encoder.encode_with_suppressed_visible_cursor(&previous_drawn, false);
+ encoder.commit(previous_drawn, initial);
+
+ let rows = vec![PaneSurfacePatchRow {
+ x: 0,
+ y: 0,
+ cells: vec![
+ make_cell("A", 0, 0, 0),
+ make_cell("b", 0, 0, 0),
+ make_cell("c", 0, 0, 0),
+ ],
+ }];
+ let cursor = Some(CursorState {
+ x: 1,
+ y: 0,
+ visible: true,
+ shape: 0,
+ });
+ let drawn_rows = encoder
+ .patch_rows_with_drawn_cursor(&rows, cursor.as_ref())
+ .expect("drawn cursor patch rows");
+ let mut expected = previous;
+ expected.cells[0..3].clone_from_slice(&rows[0].cells);
+ expected.cursor = cursor.clone();
+ let expected = frame_with_drawn_cursor(expected);
+
+ let full_diff = encoder.encode_with_suppressed_visible_cursor(&expected, false);
+ let patch = encoder
+ .encode_patch(&drawn_rows, cursor.clone(), true)
+ .expect("valid drawn cursor patch");
+ assert_eq!(patch.bytes, full_diff.bytes);
+ assert!(encoder.commit_patch(&drawn_rows, cursor, patch));
+ assert!(encoder.is_current(&expected));
+ }
+
+ #[test]
fn blit_frame_positions_cursor() {
let frame = FrameData {
cells: vec![make_cell("A", 0, 0, 0)],