a73x

be9bf5ac

Fix selection clamping semantics

a73x   2026-04-09 17:44

Commit message
Fix selection clamping semantics

src/main.zig
Old New
@@ -562,6 +562,8 @@ const SelectionSpan = struct {
562 } 562 }
563 563
564 fn containsCell(self: SelectionSpan, col: u32, row: u32) bool { 564 fn containsCell(self: SelectionSpan, col: u32, row: u32) bool {
565 if (self.isEmpty()) return false;
566
565 const span = self.normalized(); 567 const span = self.normalized();
566 if (row < span.start.row or row > span.end.row) return false; 568 if (row < span.start.row or row > span.end.row) return false;
567 if (span.start.row == span.end.row) { 569 if (span.start.row == span.end.row) {
@@ -573,8 +575,20 @@ const SelectionSpan = struct {
573 } 575 }
574 }; 576 };
575 577
578 fn selectionIntersectsVisibleGrid(span: SelectionSpan, cols: u16, rows: u16) bool {
579 if (cols == 0 or rows == 0) return false;
580
581 const normalized = span.normalized();
582 const max_col = @as(u32, cols) - 1;
583 const max_row = @as(u32, rows) - 1;
584
585 if (normalized.start.row > max_row) return false;
586 if (normalized.start.row == normalized.end.row and normalized.start.col > max_col) return false;
587 return true;
588 }
589
576 fn clampSelectionSpan(span: SelectionSpan, cols: u16, rows: u16) ?SelectionSpan { 590 fn clampSelectionSpan(span: SelectionSpan, cols: u16, rows: u16) ?SelectionSpan {
577 if (cols == 0 or rows == 0) return null; 591 if (!selectionIntersectsVisibleGrid(span, cols, rows)) return null;
578 592
579 const max_col = @as(u32, cols) - 1; 593 const max_col = @as(u32, cols) - 1;
580 const max_row = @as(u32, rows) - 1; 594 const max_row = @as(u32, rows) - 1;
@@ -619,21 +633,36 @@ test "SelectionSpan.containsCell includes the normalized endpoints" {
619 try std.testing.expect(!span.containsCell(4, 2)); 633 try std.testing.expect(!span.containsCell(4, 2));
620 } 634 }
621 635
622 test "clampSelectionSpan clears single-cell clicks and trims resized spans" { 636 test "SelectionSpan.containsCell treats a degenerate span as empty" {
623 try std.testing.expect(clampSelectionSpan(.{ 637 const span = SelectionSpan{
624 .start = .{ .col = 5, .row = 5 }, 638 .start = .{ .col = 5, .row = 5 },
625 .end = .{ .col = 5, .row = 5 }, 639 .end = .{ .col = 5, .row = 5 },
640 };
641
642 try std.testing.expect(span.isEmpty());
643 try std.testing.expect(!span.containsCell(5, 5));
644 }
645
646 test "clampSelectionSpan clears offscreen spans and trims resized spans" {
647 try std.testing.expect(clampSelectionSpan(.{
648 .start = .{ .col = 5, .row = 30 },
649 .end = .{ .col = 10, .row = 40 },
626 }, 80, 24) == null); 650 }, 80, 24) == null);
627 651
628 const clamped = clampSelectionSpan(.{ 652 const clamped = clampSelectionSpan(.{
629 .start = .{ .col = 78, .row = 30 }, 653 .start = .{ .col = 78, .row = 22 },
630 .end = .{ .col = 99, .row = 40 }, 654 .end = .{ .col = 99, .row = 30 },
631 }, 80, 24).?; 655 }, 80, 24).?;
632 656
633 try std.testing.expectEqual(@as(u32, 78), clamped.start.col); 657 try std.testing.expectEqual(@as(u32, 78), clamped.start.col);
634 try std.testing.expectEqual(@as(u32, 23), clamped.start.row); 658 try std.testing.expectEqual(@as(u32, 22), clamped.start.row);
635 try std.testing.expectEqual(@as(u32, 79), clamped.end.col); 659 try std.testing.expectEqual(@as(u32, 79), clamped.end.col);
636 try std.testing.expectEqual(@as(u32, 23), clamped.end.row); 660 try std.testing.expectEqual(@as(u32, 23), clamped.end.row);
661
662 try std.testing.expect(clampSelectionSpan(.{
663 .start = .{ .col = 81, .row = 5 },
664 .end = .{ .col = 90, .row = 5 },
665 }, 80, 24) == null);
637 } 666 }
638 667
639 const ComparisonVariant = struct { 668 const ComparisonVariant = struct {