a73x

d086c0e1

Add visible selection helpers

a73x   2026-04-09 17:40


diff --git a/src/main.zig b/src/main.zig
index 7baa943..00c054e 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -541,6 +541,101 @@ fn shouldRenderFrame(terminal_dirty: bool, window_dirty: bool, forced: bool) boo
    return terminal_dirty or window_dirty or forced;
}

const GridPoint = struct {
    col: u32,
    row: u32,
};

const SelectionSpan = struct {
    start: GridPoint,
    end: GridPoint,

    fn normalized(self: SelectionSpan) SelectionSpan {
        if (self.start.row < self.end.row) return self;
        if (self.start.row == self.end.row and self.start.col <= self.end.col) return self;
        return .{ .start = self.end, .end = self.start };
    }

    fn isEmpty(self: SelectionSpan) bool {
        const span = self.normalized();
        return span.start.row == span.end.row and span.start.col == span.end.col;
    }

    fn containsCell(self: SelectionSpan, col: u32, row: u32) bool {
        const span = self.normalized();
        if (row < span.start.row or row > span.end.row) return false;
        if (span.start.row == span.end.row) {
            return row == span.start.row and col >= span.start.col and col <= span.end.col;
        }
        if (row == span.start.row) return col >= span.start.col;
        if (row == span.end.row) return col <= span.end.col;
        return true;
    }
};

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

    const max_col = @as(u32, cols) - 1;
    const max_row = @as(u32, rows) - 1;
    const clamped = SelectionSpan{
        .start = .{
            .col = @min(span.start.col, max_col),
            .row = @min(span.start.row, max_row),
        },
        .end = .{
            .col = @min(span.end.col, max_col),
            .row = @min(span.end.row, max_row),
        },
    };

    if (clamped.isEmpty()) return null;
    return clamped.normalized();
}

test "SelectionSpan.normalized orders endpoints in reading order" {
    const span = (SelectionSpan{
        .start = .{ .col = 7, .row = 4 },
        .end = .{ .col = 2, .row = 1 },
    }).normalized();

    try std.testing.expectEqual(@as(u32, 2), span.start.col);
    try std.testing.expectEqual(@as(u32, 1), span.start.row);
    try std.testing.expectEqual(@as(u32, 7), span.end.col);
    try std.testing.expectEqual(@as(u32, 4), span.end.row);
}

test "SelectionSpan.containsCell includes the normalized endpoints" {
    const span = SelectionSpan{
        .start = .{ .col = 3, .row = 2 },
        .end = .{ .col = 1, .row = 1 },
    };

    try std.testing.expect(span.containsCell(1, 1));
    try std.testing.expect(span.containsCell(2, 1));
    try std.testing.expect(span.containsCell(0, 2));
    try std.testing.expect(span.containsCell(3, 2));
    try std.testing.expect(!span.containsCell(0, 0));
    try std.testing.expect(!span.containsCell(4, 2));
}

test "clampSelectionSpan clears single-cell clicks and trims resized spans" {
    try std.testing.expect(clampSelectionSpan(.{
        .start = .{ .col = 5, .row = 5 },
        .end = .{ .col = 5, .row = 5 },
    }, 80, 24) == null);

    const clamped = clampSelectionSpan(.{
        .start = .{ .col = 78, .row = 30 },
        .end = .{ .col = 99, .row = 40 },
    }, 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, 79), clamped.end.col);
    try std.testing.expectEqual(@as(u32, 23), clamped.end.row);
}

const ComparisonVariant = struct {
    label: []const u8,
    coverage: [2]f32,