summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig49
1 files changed, 39 insertions, 10 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 8a947065..4d821e60 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -399,13 +399,28 @@ pub const Surface = struct {
/// Print UTF-8 text into a row, no wrap, clipped to [x, x+w). Returns the
/// column after the last written cell. Wide glyphs take two cells.
+ ///
+ /// The text is NOT trusted to be valid UTF-8 — a file pane holds whatever
+ /// bytes are on disk (latin-1 source, an ELF opened by mistake), a path can
+ /// be any bytes at all, and a search row splices both. std's unchecked
+ /// iterator panics on a bad start byte, so decode by hand and paint one
+ /// U+FFFD per undecodable byte (what a terminal does).
fn print(s: *Surface, x: u16, y: u16, w: u16, text: []const u8, style: CellStyle) u16 {
var col = x;
const end = x + w;
- var it = std.unicode.Utf8View.initUnchecked(text).iterator();
- while (it.nextCodepointSlice()) |cp_slice| {
+ var i: usize = 0;
+ while (i < text.len) {
if (col >= end) break;
- const cp = std.unicode.utf8Decode(cp_slice) catch continue;
+ // n == 0: not a start byte at all. A short tail or a bad
+ // continuation decodes to null the same way — one U+FFFD, one byte.
+ const n = std.unicode.utf8ByteSequenceLength(text[i]) catch 0;
+ const decoded: ?u21 = if (n > 0 and i + n <= text.len)
+ (std.unicode.utf8Decode(text[i .. i + n]) catch null)
+ else
+ null;
+ const cp_slice = if (decoded == null) "\u{FFFD}" else text[i .. i + n];
+ i += if (decoded == null) 1 else n;
+ const cp = decoded orelse 0xFFFD;
if (cp == '\r') continue;
const width: u16 = if (cp < 0x80) 1 else uucode.get(.width, cp);
if (width == 0) continue;
@@ -3292,7 +3307,10 @@ pub const Pardes = struct {
const row0 = eb.row0;
if (y[y.len - 1] == '\n') {
const block_text = y[0 .. y.len - 1];
- const n = modal.lineCount(block_text);
+ // a yanked BLANK line is "\n": the block is empty and lineCount
+ // says 0 lines, but it still pastes as one (empty) line — without
+ // the floor every `n - 1` below underflows and panics.
+ const n = @max(1, modal.lineCount(block_text));
var out: []u8 = undefined;
if (before) {
const row: usize = @intCast(@max(0, b.lo_row - row0));
@@ -5461,7 +5479,8 @@ pub const Pardes = struct {
for (0..p.ncol) |c| {
const last = c + 1 == p.ncol;
const fw = @as(f32, @floatFromInt(p.screen_w)) * p.col_weight[c] / wsum;
- const cw: u16 = if (last) (p.screen_w -| x) else @max(1, @as(u16, @intFromFloat(@round(fw))));
+ const wroom = p.screen_w -| x; // same clamp as the rows below
+ const cw: u16 = if (last) wroom else @min(wroom, @max(1, @as(u16, @intFromFloat(@round(fw)))));
p.col_x[c] = x;
p.col_w[c] = cw;
@@ -5478,7 +5497,14 @@ pub const Pardes = struct {
const pane = p.panes[id] orelse continue;
const lastk = k + 1 == p.col_n[c];
const fh = @as(f32, @floatFromInt(avail_h)) * pane.vweight / vsum;
- const ch: u16 = if (lastk) (p.screen_h -| y) else @max(1, @as(u16, @intFromFloat(@round(fh))));
+ // every pane wants at least one row, so a column with more
+ // panes than the window has rows would walk `y` off the bottom
+ // and hand renderPane a rect outside the surface (assert, then
+ // panic — shrink a window with a few stacked panes). Clamp to
+ // what is left: the panes past the edge get h = 0 and render
+ // nothing until the window grows back.
+ const room = p.screen_h -| y;
+ const ch: u16 = if (lastk) room else @min(room, @max(1, @as(u16, @intFromFloat(@round(fh)))));
p.rects[id] = .{ .x = x, .y = y, .w = cw, .h = ch };
y +|= ch;
}
@@ -5558,17 +5584,20 @@ pub const Pardes = struct {
};
// resize-handle hint / drag previews: a dash overlay that keeps the
- // underlying colors (border drags + hover), or the move indicator
+ // underlying colors (border drags + hover), or the move indicator. A
+ // drag holds the coordinates of the last mouse event, so a resize
+ // mid-drag (tiling WM, font-size change) can leave them off the new
+ // surface — every arm below checks before it draws.
switch (p.drag) {
- .border_v => |d| {
+ .border_v => |d| if (d.cur_x < s.cols) {
var row: u16 = TOPBAR_H;
while (row < s.rows) : (row += 1) s.overlayDash(d.cur_x, row, "╎");
},
- .border_h => |d| {
+ .border_h => |d| if (d.cur_y < s.rows) {
var col = p.col_x[d.col];
while (col < p.col_x[d.col] + p.col_w[d.col]) : (col += 1) s.overlayDash(col, d.cur_y, "╌");
},
- .move => |d| {
+ .move => |d| if (d.cur_x < s.cols) {
if (p.movePlacement(d.id, d.cur_x, d.cur_y)) |placement| {
var col: u16 = p.col_x[placement.preview_col];
while (col < p.col_x[placement.preview_col] + p.col_w[placement.preview_col]) : (col += 1) {