a73x

be9bf5ac

Fix selection clamping semantics

a73x   2026-04-09 17:44


diff --git a/src/main.zig b/src/main.zig
index 00c054e..9514ac6 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -562,6 +562,8 @@ const SelectionSpan = struct {
    }

    fn containsCell(self: SelectionSpan, col: u32, row: u32) bool {
        if (self.isEmpty()) return false;

        const span = self.normalized();
        if (row < span.start.row or row > span.end.row) return false;
        if (span.start.row == span.end.row) {
@@ -573,8 +575,20 @@ const SelectionSpan = struct {
    }
};

fn selectionIntersectsVisibleGrid(span: SelectionSpan, cols: u16, rows: u16) bool {
    if (cols == 0 or rows == 0) return false;

    const normalized = span.normalized();
    const max_col = @as(u32, cols) - 1;
    const max_row = @as(u32, rows) - 1;

    if (normalized.start.row > max_row) return false;
    if (normalized.start.row == normalized.end.row and normalized.start.col > max_col) return false;
    return true;
}

fn clampSelectionSpan(span: SelectionSpan, cols: u16, rows: u16) ?SelectionSpan {
    if (cols == 0 or rows == 0) return null;
    if (!selectionIntersectsVisibleGrid(span, cols, rows)) return null;

    const max_col = @as(u32, cols) - 1;
    const max_row = @as(u32, rows) - 1;
@@ -619,21 +633,36 @@ test "SelectionSpan.containsCell includes the normalized endpoints" {
    try std.testing.expect(!span.containsCell(4, 2));
}

test "clampSelectionSpan clears single-cell clicks and trims resized spans" {
    try std.testing.expect(clampSelectionSpan(.{
test "SelectionSpan.containsCell treats a degenerate span as empty" {
    const span = SelectionSpan{
        .start = .{ .col = 5, .row = 5 },
        .end = .{ .col = 5, .row = 5 },
    };

    try std.testing.expect(span.isEmpty());
    try std.testing.expect(!span.containsCell(5, 5));
}

test "clampSelectionSpan clears offscreen spans and trims resized spans" {
    try std.testing.expect(clampSelectionSpan(.{
        .start = .{ .col = 5, .row = 30 },
        .end = .{ .col = 10, .row = 40 },
    }, 80, 24) == null);

    const clamped = clampSelectionSpan(.{
        .start = .{ .col = 78, .row = 30 },
        .end = .{ .col = 99, .row = 40 },
        .start = .{ .col = 78, .row = 22 },
        .end = .{ .col = 99, .row = 30 },
    }, 80, 24).?;

    try std.testing.expectEqual(@as(u32, 78), clamped.start.col);
    try std.testing.expectEqual(@as(u32, 23), clamped.start.row);
    try std.testing.expectEqual(@as(u32, 22), clamped.start.row);
    try std.testing.expectEqual(@as(u32, 79), clamped.end.col);
    try std.testing.expectEqual(@as(u32, 23), clamped.end.row);

    try std.testing.expect(clampSelectionSpan(.{
        .start = .{ .col = 81, .row = 5 },
        .end = .{ .col = 90, .row = 5 },
    }, 80, 24) == null);
}

const ComparisonVariant = struct {