e752cef0
Fix visible selection resize semantics
a73x 2026-04-09 17:55
diff --git a/src/main.zig b/src/main.zig index efe47ad..5a4a7dd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -575,20 +575,35 @@ fn clampSelectionSpan(span: SelectionSpan, cols: u16, rows: u16) ?SelectionSpan const max_col = @as(u32, cols) - 1; const max_row = @as(u32, rows) - 1; if (normalized.start.row > max_row) return null; if (normalized.start.row == normalized.end.row and normalized.start.col > max_col) return null; const clamped = SelectionSpan{ .start = .{ .col = @min(normalized.start.col, max_col), .row = @min(normalized.start.row, max_row), }, .end = .{ .col = @min(normalized.end.col, max_col), .row = @min(normalized.end.row, max_row), }, }; const visible_end_row = @min(normalized.end.row, max_row); if (normalized.start.row > visible_end_row) return null; var visible_start: ?GridPoint = null; var visible_end: ?GridPoint = null; return clamped.normalized(); var row = normalized.start.row; while (row <= visible_end_row) : (row += 1) { const row_start_col = if (row == normalized.start.row) normalized.start.col else 0; const row_end_col = if (row == normalized.end.row) @min(normalized.end.col, max_col) else max_col; if (row_start_col > max_col or row_start_col > row_end_col) continue; if (visible_start == null) { visible_start = .{ .col = row_start_col, .row = row, }; } visible_end = .{ .col = row_end_col, .row = row, }; } return if (visible_start) |start_point| .{ .start = start_point, .end = visible_end.?, } else null; } test "SelectionSpan.normalized orders endpoints in reading order" { @@ -658,6 +673,30 @@ test "clampSelectionSpan clears offscreen spans and trims resized spans" { try std.testing.expectEqual(@as(u32, 23), clamped.end.row); } test "clampSelectionSpan starts the visible span on the next row when the first row is clipped on the right" { const clamped = clampSelectionSpan(.{ .start = .{ .col = 5, .row = 0 }, .end = .{ .col = 2, .row = 2 }, }, 4, 3).?; try std.testing.expectEqual(@as(u32, 0), clamped.start.col); try std.testing.expectEqual(@as(u32, 1), clamped.start.row); try std.testing.expectEqual(@as(u32, 2), clamped.end.col); try std.testing.expectEqual(@as(u32, 2), clamped.end.row); } test "clampSelectionSpan extends the clipped bottom row to the last visible column" { const clamped = clampSelectionSpan(.{ .start = .{ .col = 0, .row = 0 }, .end = .{ .col = 2, .row = 5 }, }, 4, 2).?; try std.testing.expectEqual(@as(u32, 0), clamped.start.col); try std.testing.expectEqual(@as(u32, 0), clamped.start.row); try std.testing.expectEqual(@as(u32, 3), clamped.end.col); try std.testing.expectEqual(@as(u32, 1), clamped.end.row); } test "clampSelectionSpan preserves a larger span that collapses to one visible cell" { const clamped = clampSelectionSpan(.{ .start = .{ .col = 0, .row = 0 }, @@ -1152,6 +1191,19 @@ fn mapKeysymToInputKey(keysym: u32) ?vt.InputKey { }; } test "event loop waits indefinitely when idle and wakes for imminent repeat" { try std.testing.expectEqual(@as(i32, -1), computePollTimeoutMs(null, false)); try std.testing.expectEqual(@as(i32, 0), computePollTimeoutMs(5, true)); try std.testing.expectEqual(@as(i32, 17), computePollTimeoutMs(17, false)); } test "event loop redraws only when terminal or window state changed" { try std.testing.expect(shouldRenderFrame(true, false, false)); try std.testing.expect(shouldRenderFrame(false, true, false)); try std.testing.expect(shouldRenderFrame(false, false, true)); try std.testing.expect(!shouldRenderFrame(false, false, false)); } test "planRowRefresh requests full rebuild for full dirty state" { const plan = planRowRefresh(.full, &.{ false, true, false }, .{ .cursor = .{