a73x

d086c0e1

Add visible selection helpers

a73x   2026-04-09 17:40

Commit message
Add visible selection helpers

src/main.zig
Old New
@@ -541,6 +541,101 @@ fn shouldRenderFrame(terminal_dirty: bool, window_dirty: bool, forced: bool) boo
541 return terminal_dirty or window_dirty or forced; 541 return terminal_dirty or window_dirty or forced;
542 } 542 }
543 543
544 const GridPoint = struct {
545 col: u32,
546 row: u32,
547 };
548
549 const SelectionSpan = struct {
550 start: GridPoint,
551 end: GridPoint,
552
553 fn normalized(self: SelectionSpan) SelectionSpan {
554 if (self.start.row < self.end.row) return self;
555 if (self.start.row == self.end.row and self.start.col <= self.end.col) return self;
556 return .{ .start = self.end, .end = self.start };
557 }
558
559 fn isEmpty(self: SelectionSpan) bool {
560 const span = self.normalized();
561 return span.start.row == span.end.row and span.start.col == span.end.col;
562 }
563
564 fn containsCell(self: SelectionSpan, col: u32, row: u32) bool {
565 const span = self.normalized();
566 if (row < span.start.row or row > span.end.row) return false;
567 if (span.start.row == span.end.row) {
568 return row == span.start.row and col >= span.start.col and col <= span.end.col;
569 }
570 if (row == span.start.row) return col >= span.start.col;
571 if (row == span.end.row) return col <= span.end.col;
572 return true;
573 }
574 };
575
576 fn clampSelectionSpan(span: SelectionSpan, cols: u16, rows: u16) ?SelectionSpan {
577 if (cols == 0 or rows == 0) return null;
578
579 const max_col = @as(u32, cols) - 1;
580 const max_row = @as(u32, rows) - 1;
581 const clamped = SelectionSpan{
582 .start = .{
583 .col = @min(span.start.col, max_col),
584 .row = @min(span.start.row, max_row),
585 },
586 .end = .{
587 .col = @min(span.end.col, max_col),
588 .row = @min(span.end.row, max_row),
589 },
590 };
591
592 if (clamped.isEmpty()) return null;
593 return clamped.normalized();
594 }
595
596 test "SelectionSpan.normalized orders endpoints in reading order" {
597 const span = (SelectionSpan{
598 .start = .{ .col = 7, .row = 4 },
599 .end = .{ .col = 2, .row = 1 },
600 }).normalized();
601
602 try std.testing.expectEqual(@as(u32, 2), span.start.col);
603 try std.testing.expectEqual(@as(u32, 1), span.start.row);
604 try std.testing.expectEqual(@as(u32, 7), span.end.col);
605 try std.testing.expectEqual(@as(u32, 4), span.end.row);
606 }
607
608 test "SelectionSpan.containsCell includes the normalized endpoints" {
609 const span = SelectionSpan{
610 .start = .{ .col = 3, .row = 2 },
611 .end = .{ .col = 1, .row = 1 },
612 };
613
614 try std.testing.expect(span.containsCell(1, 1));
615 try std.testing.expect(span.containsCell(2, 1));
616 try std.testing.expect(span.containsCell(0, 2));
617 try std.testing.expect(span.containsCell(3, 2));
618 try std.testing.expect(!span.containsCell(0, 0));
619 try std.testing.expect(!span.containsCell(4, 2));
620 }
621
622 test "clampSelectionSpan clears single-cell clicks and trims resized spans" {
623 try std.testing.expect(clampSelectionSpan(.{
624 .start = .{ .col = 5, .row = 5 },
625 .end = .{ .col = 5, .row = 5 },
626 }, 80, 24) == null);
627
628 const clamped = clampSelectionSpan(.{
629 .start = .{ .col = 78, .row = 30 },
630 .end = .{ .col = 99, .row = 40 },
631 }, 80, 24).?;
632
633 try std.testing.expectEqual(@as(u32, 78), clamped.start.col);
634 try std.testing.expectEqual(@as(u32, 23), clamped.start.row);
635 try std.testing.expectEqual(@as(u32, 79), clamped.end.col);
636 try std.testing.expectEqual(@as(u32, 23), clamped.end.row);
637 }
638
544 const ComparisonVariant = struct { 639 const ComparisonVariant = struct {
545 label: []const u8, 640 label: []const u8,
546 coverage: [2]f32, 641 coverage: [2]f32,