Terminal: fix ECH & DCH wide char boundary cond. behavior

This commit is contained in:
Qwerasd
2024-09-05 21:14:53 -04:00
parent 04271c6a07
commit 8d12044f1d
3 changed files with 186 additions and 50 deletions

View File

@ -3172,6 +3172,34 @@ pub const Pin = struct {
set.set(self.y); set.set(self.y);
} }
/// Resets the soft-wrap state of the row this pin is on, appropriately
/// handling the wrap and wrap_continuation flags for the previous and
/// next row as well.
///
/// DOES NOT handle clearing spacer heads.
/// Use `Screen.splitCellBoundary` for that.
///
/// TODO: test
pub fn resetWrap(self: Pin) void {
const rac = self.rowAndCell();
// This row does not wrap
rac.row.wrap = false;
// This row is not wrapped to
rac.row.wrap_continuation = false;
// The previous row does not wrap to this row
if (self.up(1)) |prev_row| {
prev_row.rowAndCell().row.wrap = false;
}
// The next row is not wrapped to
if (self.down(1)) |next_row| {
next_row.rowAndCell().row.wrap_continuation = false;
}
}
/// Returns true if the row of this pin should never have its background /// Returns true if the row of this pin should never have its background
/// color extended for filling padding space in the renderer. This is /// color extended for filling padding space in the renderer. This is
/// a set of heuristics that help making our padding look better. /// a set of heuristics that help making our padding look better.

View File

@ -1041,6 +1041,17 @@ pub fn cursorMarkDirty(self: *Screen) void {
self.cursor.page_pin.markDirty(); self.cursor.page_pin.markDirty();
} }
/// Reset the cursor row's soft-wrap state and the cursor's pending wrap.
/// Also clears spacer heads from the cursor row and prior row as necessary.
pub fn cursorResetWrap(self: *Screen) void {
// Handle boundary conditions on left and right of the row.
self.splitCellBoundary(self.cursor.page_pin.*, 0);
self.splitCellBoundary(self.cursor.page_pin.*, self.cursor.page_pin.page.data.size.cols);
self.cursor.page_pin.resetWrap();
self.cursor.pending_wrap = false;
}
/// Options for scrolling the viewport of the terminal grid. The reason /// Options for scrolling the viewport of the terminal grid. The reason
/// we have this in addition to PageList.Scroll is because we have additional /// we have this in addition to PageList.Scroll is because we have additional
/// scroll behaviors that are not part of the PageList.Scroll enum. /// scroll behaviors that are not part of the PageList.Scroll enum.
@ -1282,6 +1293,118 @@ pub fn clearPrompt(self: *Screen) void {
} }
} }
/// Clean up boundary conditions where a cell will become discontiguous with
/// a neighboring cell because either one of them will be moved and/or cleared.
///
/// Handles the boundary between the cell at `x` and the cell at `x - 1`.
///
/// So, for example, when moving a region of cells [a, b] (inclusive), call this
/// function with `x = a` and `x = b + 1`. It is okay if `x` is out of bounds by
/// 1, this will be interpreted correctly.
///
/// DOES NOT MODIFY ROW WRAP STATE! See `resetWrap` for that.
///
/// The following boundary conditions are handled:
///
/// - `x - 1` is a wide character and `x` is a spacer tail:
/// o Both cells will be cleared.
/// o If `x - 1` is the start of the row and was wrapped from a previous row
/// then the previous row is checked for a spacer head, which is cleared if
/// present.
///
/// - `x == 0` and is a wide character:
/// o If the row is a wrap continuation then the previous row will be checked
/// for a spacer head, which is cleared if present.
///
/// - `x == cols` and `x - 1` is a spacer head:
/// o `x - 1` will be cleared.
pub fn splitCellBoundary(
self: *Screen,
row: Pin,
x: size.CellCountInt,
) void {
row.page.data.pauseIntegrityChecks(true);
defer row.page.data.pauseIntegrityChecks(false);
const rac = row.rowAndCell();
const cells = row.cells(.all);
const cols = row.page.data.size.cols;
// `x` may be up to an INCLUDING `cols`, since that signifies splitting
// the boundary to the right of the final cell in the row.
assert(x <= cols);
// [ A B C D E F|]
// ^ Boundary between final cell and row end.
if (x == cols) {
// Spacer head at end of wrapped row.
if (cells[cols - 1].wide == .spacer_head) {
self.clearCells(
&row.page.data,
rac.row,
cells[cols - 1 ..][0..1],
);
}
return;
}
// [|A B C D E F ]
// ^ Boundary between first cell and row start.
//
// OR
//
// [ A|B C D E F ]
// ^ Boundary between first cell and second cell.
//
// First cell may be a wrapped wide cell with a spacer
// head on the previous row that needs to be cleared.
if (x == 0 or x == 1) {
// If the first cell in a row is wide the previous row
// may have a spacer head which needs to be cleared.
if (cells[0].wide == .wide and rac.row.wrap_continuation) {
if (row.up(1)) |p_row| {
const p_rac = p_row.rowAndCell();
const p_cells = p_row.cells(.all);
const p_cell = p_cells[p_row.page.data.size.cols - 1];
if (p_cell.wide == .spacer_head) {
self.clearCells(
&p_row.page.data,
p_rac.row,
p_cells[p_row.page.data.size.cols - 1 ..][0..1],
);
}
}
}
// If x is 0 then we're done.
if (x == 0) return;
}
// [ ... X|Y ... ]
// ^ Boundary between two cells in the middle of the row.
assert(x > 0);
assert(x < cols);
const left = cells[x - 1];
switch (left.wide) {
// There should not be spacer heads in the middle of the row.
.spacer_head => unreachable,
// We don't need to do anything for narrow cells or spacer tails.
.narrow, .spacer_tail => {},
// A wide char would be split, so must be cleared.
.wide => {
self.clearCells(
&row.page.data,
rac.row,
cells[x - 1 ..][0..2],
);
},
}
}
/// Returns the blank cell to use when doing terminal operations that /// Returns the blank cell to use when doing terminal operations that
/// require preserving the bg color. /// require preserving the bg color.
pub fn blankCell(self: *const Screen) Cell { pub fn blankCell(self: *const Screen) Cell {

View File

@ -1907,29 +1907,29 @@ pub fn deleteChars(self: *Terminal, count_req: usize) void {
if (self.screen.cursor.x < self.scrolling_region.left or if (self.screen.cursor.x < self.scrolling_region.left or
self.screen.cursor.x > self.scrolling_region.right) return; self.screen.cursor.x > self.scrolling_region.right) return;
// This resets the soft-wrap of this line
self.screen.cursor.page_row.wrap = false;
// This resets the pending wrap state
self.screen.cursor.pending_wrap = false;
// left is just the cursor position but as a multi-pointer // left is just the cursor position but as a multi-pointer
const left: [*]Cell = @ptrCast(self.screen.cursor.page_cell); const left: [*]Cell = @ptrCast(self.screen.cursor.page_cell);
var page = &self.screen.cursor.page_pin.page.data; var page = &self.screen.cursor.page_pin.page.data;
// If our X is a wide spacer tail then we need to erase the
// previous cell too so we don't split a multi-cell character.
if (self.screen.cursor.page_cell.wide == .spacer_tail) {
assert(self.screen.cursor.x > 0);
self.screen.clearCells(page, self.screen.cursor.page_row, (left - 1)[0..2]);
}
// Remaining cols from our cursor to the right margin. // Remaining cols from our cursor to the right margin.
const rem = self.scrolling_region.right - self.screen.cursor.x + 1; const rem = self.scrolling_region.right - self.screen.cursor.x + 1;
// We can only insert blanks up to our remaining cols // We can only insert blanks up to our remaining cols
const count = @min(count_req, rem); const count = @min(count_req, rem);
self.screen.splitCellBoundary(
self.screen.cursor.page_pin.*,
self.screen.cursor.x,
);
self.screen.splitCellBoundary(
self.screen.cursor.page_pin.*,
self.screen.cursor.x + count,
);
self.screen.splitCellBoundary(
self.screen.cursor.page_pin.*,
self.scrolling_region.right + 1,
);
// This is the amount of space at the right of the scroll region // This is the amount of space at the right of the scroll region
// that will NOT be blank, so we need to shift the correct cols right. // that will NOT be blank, so we need to shift the correct cols right.
// "scroll_amount" is the number of such cols. // "scroll_amount" is the number of such cols.
@ -1941,35 +1941,6 @@ pub fn deleteChars(self: *Terminal, count_req: usize) void {
const right: [*]Cell = left + (scroll_amount - 1); const right: [*]Cell = left + (scroll_amount - 1);
const end: *Cell = @ptrCast(right + count);
switch (end.wide) {
.narrow, .wide => {},
// If our end is a spacer head then we need to clear it since
// spacer heads must be at the end.
.spacer_head => {
self.screen.clearCells(page, self.screen.cursor.page_row, end[0..1]);
},
// If our last cell we're shifting is wide, then we need to clear
// it to be empty so we don't split the multi-cell char.
.spacer_tail => {
const wide: [*]Cell = right + count - 1;
assert(wide[0].wide == .wide);
self.screen.clearCells(page, self.screen.cursor.page_row, wide[0..2]);
},
}
// If our first cell is a wide char then we need to also clear
// the spacer tail following it.
if (x[0].wide == .wide) {
self.screen.clearCells(
page,
self.screen.cursor.page_row,
x[0..2],
);
}
while (@intFromPtr(x) <= @intFromPtr(right)) : (x += 1) { while (@intFromPtr(x) <= @intFromPtr(right)) : (x += 1) {
const src: *Cell = @ptrCast(x + count); const src: *Cell = @ptrCast(x + count);
const dst: *Cell = @ptrCast(x); const dst: *Cell = @ptrCast(x);
@ -1980,6 +1951,9 @@ pub fn deleteChars(self: *Terminal, count_req: usize) void {
// Insert blanks. The blanks preserve the background color. // Insert blanks. The blanks preserve the background color.
self.screen.clearCells(page, self.screen.cursor.page_row, x[0 .. rem - scroll_amount]); self.screen.clearCells(page, self.screen.cursor.page_row, x[0 .. rem - scroll_amount]);
// Our row's soft-wrap is always reset.
self.screen.cursorResetWrap();
// Our row is always dirty // Our row is always dirty
self.screen.cursorMarkDirty(); self.screen.cursorMarkDirty();
} }
@ -1987,12 +1961,6 @@ pub fn deleteChars(self: *Terminal, count_req: usize) void {
pub fn eraseChars(self: *Terminal, count_req: usize) void { pub fn eraseChars(self: *Terminal, count_req: usize) void {
const count = @max(count_req, 1); const count = @max(count_req, 1);
// This resets the soft-wrap of this line
self.screen.cursor.page_row.wrap = false;
// This resets the pending wrap state
self.screen.cursor.pending_wrap = false;
// Our last index is at most the end of the number of chars we have // Our last index is at most the end of the number of chars we have
// in the current line. // in the current line.
const end = end: { const end = end: {
@ -2009,6 +1977,23 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void {
break :end end; break :end end;
}; };
// Handle any boundary conditions on the edges of the erased area.
//
// TODO(qwerasd): This isn't actually correct if you take in to account
// protected modes. We need to figure out how to make `clearCells` or at
// least `clearUnprotectedCells` handle boundary conditions...
self.screen.splitCellBoundary(
self.screen.cursor.page_pin.*,
self.screen.cursor.x,
);
self.screen.splitCellBoundary(
self.screen.cursor.page_pin.*,
end,
);
// Reset our row's soft-wrap.
self.screen.cursorResetWrap();
// Mark our cursor row as dirty // Mark our cursor row as dirty
self.screen.cursorMarkDirty(); self.screen.cursorMarkDirty();
@ -2051,8 +2036,8 @@ pub fn eraseLine(
x -= 1; x -= 1;
} }
// This resets the soft-wrap of this line // Reset our row's soft-wrap.
self.screen.cursor.page_row.wrap = false; self.screen.cursorResetWrap();
break :right .{ x, self.cols }; break :right .{ x, self.cols };
}, },