mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 16:26:08 +03:00
Add rectangle select
This adds rectangle select mode; when dragging with ctrl+alt (or super+alt on MacOS), this allows you to select a rectangular region of the terminal instead of the full start-end points of the buffer.
This commit is contained in:
@ -2209,6 +2209,17 @@ pub fn cursorPosCallback(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks to see if super is on in mods (MacOS) or ctrl. We use this for
|
||||||
|
// rectangle select along with alt.
|
||||||
|
//
|
||||||
|
// Not to be confused with ctrlOrSuper in Config.
|
||||||
|
fn ctrlOrSuper(mods: input.Mods) bool {
|
||||||
|
if (comptime builtin.target.isDarwin()) {
|
||||||
|
return mods.super;
|
||||||
|
}
|
||||||
|
return mods.ctrl;
|
||||||
|
}
|
||||||
|
|
||||||
/// Double-click dragging moves the selection one "word" at a time.
|
/// Double-click dragging moves the selection one "word" at a time.
|
||||||
fn dragLeftClickDouble(
|
fn dragLeftClickDouble(
|
||||||
self: *Surface,
|
self: *Surface,
|
||||||
@ -2238,11 +2249,13 @@ fn dragLeftClickDouble(
|
|||||||
self.setSelection(.{
|
self.setSelection(.{
|
||||||
.start = word_current.start,
|
.start = word_current.start,
|
||||||
.end = word_start.end,
|
.end = word_start.end,
|
||||||
|
.rectangle = ctrlOrSuper(self.mouse.mods) and self.mouse.mods.alt,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
self.setSelection(.{
|
self.setSelection(.{
|
||||||
.start = word_start.start,
|
.start = word_start.start,
|
||||||
.end = word_current.end,
|
.end = word_current.end,
|
||||||
|
.rectangle = ctrlOrSuper(self.mouse.mods) and self.mouse.mods.alt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2330,6 +2343,7 @@ fn dragLeftClickSingle(
|
|||||||
self.setSelection(if (selected) .{
|
self.setSelection(if (selected) .{
|
||||||
.start = screen_point,
|
.start = screen_point,
|
||||||
.end = screen_point,
|
.end = screen_point,
|
||||||
|
.rectangle = ctrlOrSuper(self.mouse.mods) and self.mouse.mods.alt,
|
||||||
} else null);
|
} else null);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -2369,7 +2383,11 @@ fn dragLeftClickSingle(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.setSelection(.{ .start = start, .end = screen_point });
|
self.setSelection(.{
|
||||||
|
.start = start,
|
||||||
|
.end = screen_point,
|
||||||
|
.rectangle = ctrlOrSuper(self.mouse.mods) and self.mouse.mods.alt,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2278,13 +2278,17 @@ fn selectionSliceString(
|
|||||||
const start_idx = row_i * (self.cols + 1);
|
const start_idx = row_i * (self.cols + 1);
|
||||||
if (start_idx >= slice.len) break;
|
if (start_idx >= slice.len) break;
|
||||||
|
|
||||||
// Our end index is usually a full row, but if we're the final
|
const end_idx = if (slices.sel.rectangle)
|
||||||
// row then we just use the length.
|
// Rectangle select: calculate end with bottom offset.
|
||||||
const end_idx = @min(slice.len, start_idx + self.cols + 1);
|
start_idx + slices.bot_offset + 2 // think "column count" + 1
|
||||||
|
else
|
||||||
|
// Normal select: our end index is usually a full row, but if
|
||||||
|
// we're the final row then we just use the length.
|
||||||
|
@min(slice.len, start_idx + self.cols + 1);
|
||||||
|
|
||||||
// We may have to skip some cells from the beginning if we're
|
// We may have to skip some cells from the beginning if we're the
|
||||||
// the first row.
|
// first row, of if we're using rectangle select.
|
||||||
var skip: usize = if (row_count == 0) slices.top_offset else 0;
|
var skip: usize = if (row_count == 0 or slices.sel.rectangle) slices.top_offset else 0;
|
||||||
|
|
||||||
// If we have runtime safety we need to initialize the row
|
// If we have runtime safety we need to initialize the row
|
||||||
// so that the proper union tag is set. In release modes we
|
// so that the proper union tag is set. In release modes we
|
||||||
@ -2334,8 +2338,9 @@ fn selectionSliceString(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this row is not soft-wrapped, add a newline
|
// If this row is not soft-wrapped or if we're using rectangle
|
||||||
if (!row.header().flags.wrap) {
|
// select, add a newline
|
||||||
|
if (!row.header().flags.wrap or slices.sel.rectangle) {
|
||||||
try strbuilder.append('\n');
|
try strbuilder.append('\n');
|
||||||
if (mapbuilder) |b| {
|
if (mapbuilder) |b| {
|
||||||
try b.append(.{
|
try b.append(.{
|
||||||
@ -2373,6 +2378,12 @@ const SelectionSlices = struct {
|
|||||||
// Top offset can be used to determine if a newline is required by
|
// Top offset can be used to determine if a newline is required by
|
||||||
// seeing if the cell index plus the offset cleanly divides by screen cols.
|
// seeing if the cell index plus the offset cleanly divides by screen cols.
|
||||||
top_offset: usize,
|
top_offset: usize,
|
||||||
|
|
||||||
|
// Our bottom offset is used in rectangle select to always determine the
|
||||||
|
// maximum cell in a given row.
|
||||||
|
bot_offset: usize,
|
||||||
|
|
||||||
|
// Our selection storage cell chunks.
|
||||||
top: []StorageCell,
|
top: []StorageCell,
|
||||||
bot: []StorageCell,
|
bot: []StorageCell,
|
||||||
};
|
};
|
||||||
@ -2388,6 +2399,7 @@ fn selectionSlices(self: *Screen, sel_raw: Selection) SelectionSlices {
|
|||||||
.rows = 0,
|
.rows = 0,
|
||||||
.sel = sel_raw,
|
.sel = sel_raw,
|
||||||
.top_offset = 0,
|
.top_offset = 0,
|
||||||
|
.bot_offset = 0,
|
||||||
.top = self.storage.storage[0..0],
|
.top = self.storage.storage[0..0],
|
||||||
.bot = self.storage.storage[0..0],
|
.bot = self.storage.storage[0..0],
|
||||||
};
|
};
|
||||||
@ -2428,6 +2440,7 @@ fn selectionSlices(self: *Screen, sel_raw: Selection) SelectionSlices {
|
|||||||
// Get the true "top" and "bottom"
|
// Get the true "top" and "bottom"
|
||||||
const sel_top = sel.topLeft();
|
const sel_top = sel.topLeft();
|
||||||
const sel_bot = sel.bottomRight();
|
const sel_bot = sel.bottomRight();
|
||||||
|
const sel_isRect = sel.rectangle;
|
||||||
|
|
||||||
// We get the slices for the full top and bottom (inclusive).
|
// We get the slices for the full top and bottom (inclusive).
|
||||||
const sel_top_offset = self.rowOffset(.{ .screen = sel_top.y });
|
const sel_top_offset = self.rowOffset(.{ .screen = sel_top.y });
|
||||||
@ -2441,8 +2454,9 @@ fn selectionSlices(self: *Screen, sel_raw: Selection) SelectionSlices {
|
|||||||
// bottom of the storage, then from the top.
|
// bottom of the storage, then from the top.
|
||||||
return .{
|
return .{
|
||||||
.rows = sel_bot.y - sel_top.y + 1,
|
.rows = sel_bot.y - sel_top.y + 1,
|
||||||
.sel = .{ .start = sel_top, .end = sel_bot },
|
.sel = .{ .start = sel_top, .end = sel_bot, .rectangle = sel_isRect },
|
||||||
.top_offset = sel_top.x,
|
.top_offset = sel_top.x,
|
||||||
|
.bot_offset = sel_bot.x,
|
||||||
.top = slices[0],
|
.top = slices[0],
|
||||||
.bot = slices[1],
|
.bot = slices[1],
|
||||||
};
|
};
|
||||||
@ -5349,6 +5363,105 @@ test "Screen: selectionString with zero width joiner" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Screen: selectionString, rectangle, basic" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 5, 30, 0);
|
||||||
|
defer s.deinit();
|
||||||
|
const str =
|
||||||
|
\\Lorem ipsum dolor
|
||||||
|
\\sit amet, consectetur
|
||||||
|
\\adipiscing elit, sed do
|
||||||
|
\\eiusmod tempor incididunt
|
||||||
|
\\ut labore et dolore
|
||||||
|
;
|
||||||
|
const sel = Selection{
|
||||||
|
.start = .{ .x = 2, .y = 1 },
|
||||||
|
.end = .{ .x = 6, .y = 3 },
|
||||||
|
.rectangle = true,
|
||||||
|
};
|
||||||
|
const expected =
|
||||||
|
\\t ame
|
||||||
|
\\ipisc
|
||||||
|
\\usmod
|
||||||
|
;
|
||||||
|
try s.testWriteString(str);
|
||||||
|
|
||||||
|
const contents = try s.selectionString(alloc, sel, true);
|
||||||
|
defer alloc.free(contents);
|
||||||
|
try testing.expectEqualStrings(expected, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Screen: selectionString, rectangle, w/EOL" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 5, 30, 0);
|
||||||
|
defer s.deinit();
|
||||||
|
const str =
|
||||||
|
\\Lorem ipsum dolor
|
||||||
|
\\sit amet, consectetur
|
||||||
|
\\adipiscing elit, sed do
|
||||||
|
\\eiusmod tempor incididunt
|
||||||
|
\\ut labore et dolore
|
||||||
|
;
|
||||||
|
const sel = Selection{
|
||||||
|
.start = .{ .x = 12, .y = 0 },
|
||||||
|
.end = .{ .x = 26, .y = 4 },
|
||||||
|
.rectangle = true,
|
||||||
|
};
|
||||||
|
const expected =
|
||||||
|
\\dolor
|
||||||
|
\\nsectetur
|
||||||
|
\\lit, sed do
|
||||||
|
\\or incididunt
|
||||||
|
\\ dolore
|
||||||
|
;
|
||||||
|
try s.testWriteString(str);
|
||||||
|
|
||||||
|
const contents = try s.selectionString(alloc, sel, true);
|
||||||
|
defer alloc.free(contents);
|
||||||
|
try testing.expectEqualStrings(expected, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Screen: selectionString, rectangle, more complex w/breaks" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 8, 30, 0);
|
||||||
|
defer s.deinit();
|
||||||
|
const str =
|
||||||
|
\\Lorem ipsum dolor
|
||||||
|
\\sit amet, consectetur
|
||||||
|
\\adipiscing elit, sed do
|
||||||
|
\\eiusmod tempor incididunt
|
||||||
|
\\ut labore et dolore
|
||||||
|
\\
|
||||||
|
\\magna aliqua. Ut enim
|
||||||
|
\\ad minim veniam, quis
|
||||||
|
;
|
||||||
|
const sel = Selection{
|
||||||
|
.start = .{ .x = 11, .y = 2 },
|
||||||
|
.end = .{ .x = 26, .y = 7 },
|
||||||
|
.rectangle = true,
|
||||||
|
};
|
||||||
|
const expected =
|
||||||
|
\\elit, sed do
|
||||||
|
\\por incididunt
|
||||||
|
\\t dolore
|
||||||
|
\\
|
||||||
|
\\a. Ut enim
|
||||||
|
\\niam, quis
|
||||||
|
;
|
||||||
|
try s.testWriteString(str);
|
||||||
|
|
||||||
|
const contents = try s.selectionString(alloc, sel, true);
|
||||||
|
defer alloc.free(contents);
|
||||||
|
try testing.expectEqualStrings(expected, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
test "Screen: dirty with getCellPtr" {
|
test "Screen: dirty with getCellPtr" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
@ -15,6 +15,11 @@ const ScreenPoint = point.ScreenPoint;
|
|||||||
start: ScreenPoint,
|
start: ScreenPoint,
|
||||||
end: ScreenPoint,
|
end: ScreenPoint,
|
||||||
|
|
||||||
|
/// Whether or not this selection refers to a rectangle, rather than whole
|
||||||
|
/// lines of a buffer. In this mode, start and end refer to the top left and
|
||||||
|
/// bottom right of the rectangle, or vice versa if the selection is backwards.
|
||||||
|
rectangle: bool = false,
|
||||||
|
|
||||||
/// Converts a selection screen points to viewport points (still typed
|
/// Converts a selection screen points to viewport points (still typed
|
||||||
/// as ScreenPoints) if the selection is present within the viewport
|
/// as ScreenPoints) if the selection is present within the viewport
|
||||||
/// of the screen.
|
/// of the screen.
|
||||||
@ -31,6 +36,7 @@ pub fn toViewport(self: Selection, screen: *const Screen) ?Selection {
|
|||||||
return Selection{
|
return Selection{
|
||||||
.start = .{ .x = start.x, .y = start.y },
|
.start = .{ .x = start.x, .y = start.y },
|
||||||
.end = .{ .x = end.x, .y = end.y },
|
.end = .{ .x = end.x, .y = end.y },
|
||||||
|
.rectangle = self.rectangle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +57,11 @@ pub fn contains(self: Selection, p: ScreenPoint) bool {
|
|||||||
// Honestly there is probably way more efficient boolean logic here.
|
// Honestly there is probably way more efficient boolean logic here.
|
||||||
// Look back at this in the future...
|
// Look back at this in the future...
|
||||||
|
|
||||||
|
// If we're in rectangle select, we can short-circuit with an easy check
|
||||||
|
// here
|
||||||
|
if (self.rectangle)
|
||||||
|
return p.y >= tl.y and p.y <= br.y and p.x >= tl.x and p.x <= br.x;
|
||||||
|
|
||||||
// If tl/br are same line
|
// If tl/br are same line
|
||||||
if (tl.y == br.y) return p.y == tl.y and
|
if (tl.y == br.y) return p.y == tl.y and
|
||||||
p.x >= tl.x and
|
p.x >= tl.x and
|
||||||
@ -101,6 +112,14 @@ pub fn containedRow(self: Selection, screen: *const Screen, p: ScreenPoint) ?Sel
|
|||||||
const br = self.bottomRight();
|
const br = self.bottomRight();
|
||||||
if (p.y < tl.y or p.y > br.y) return null;
|
if (p.y < tl.y or p.y > br.y) return null;
|
||||||
|
|
||||||
|
// Rectangle case: we can return early as the x range will always be the
|
||||||
|
// same. We've already validated that the row is in the selection.
|
||||||
|
if (self.rectangle) return .{
|
||||||
|
.start = .{ .y = p.y, .x = tl.x },
|
||||||
|
.end = .{ .y = p.y, .x = br.x },
|
||||||
|
.rectangle = true,
|
||||||
|
};
|
||||||
|
|
||||||
if (p.y == tl.y) {
|
if (p.y == tl.y) {
|
||||||
// If the selection is JUST this line, return it as-is.
|
// If the selection is JUST this line, return it as-is.
|
||||||
if (p.y == br.y) {
|
if (p.y == br.y) {
|
||||||
@ -154,8 +173,8 @@ pub fn ordered(self: Selection, desired: Order) Selection {
|
|||||||
const tl = self.topLeft();
|
const tl = self.topLeft();
|
||||||
const br = self.bottomRight();
|
const br = self.bottomRight();
|
||||||
return switch (desired) {
|
return switch (desired) {
|
||||||
.forward => .{ .start = tl, .end = br },
|
.forward => .{ .start = tl, .end = br, .rectangle = self.rectangle },
|
||||||
.reverse => .{ .start = br, .end = tl },
|
.reverse => .{ .start = br, .end = tl, .rectangle = self.rectangle },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,6 +231,78 @@ test "Selection: contains" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Selection: contains, rectangle" {
|
||||||
|
const testing = std.testing;
|
||||||
|
{
|
||||||
|
const sel: Selection = .{
|
||||||
|
.start = .{ .x = 3, .y = 3 },
|
||||||
|
.end = .{ .x = 7, .y = 9 },
|
||||||
|
.rectangle = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
try testing.expect(sel.contains(.{ .x = 5, .y = 6 })); // Center
|
||||||
|
try testing.expect(sel.contains(.{ .x = 3, .y = 6 })); // Left border
|
||||||
|
try testing.expect(sel.contains(.{ .x = 7, .y = 6 })); // Right border
|
||||||
|
try testing.expect(sel.contains(.{ .x = 5, .y = 3 })); // Top border
|
||||||
|
try testing.expect(sel.contains(.{ .x = 5, .y = 9 })); // Bottom border
|
||||||
|
|
||||||
|
try testing.expect(!sel.contains(.{ .x = 5, .y = 2 })); // Above center
|
||||||
|
try testing.expect(!sel.contains(.{ .x = 5, .y = 10 })); // Below center
|
||||||
|
try testing.expect(!sel.contains(.{ .x = 2, .y = 6 })); // Left center
|
||||||
|
try testing.expect(!sel.contains(.{ .x = 8, .y = 6 })); // Right center
|
||||||
|
try testing.expect(!sel.contains(.{ .x = 8, .y = 3 })); // Just right of top right
|
||||||
|
try testing.expect(!sel.contains(.{ .x = 2, .y = 9 })); // Just left of bottom left
|
||||||
|
|
||||||
|
try testing.expect(!sel.containsRow(.{ .x = 1, .y = 1 }));
|
||||||
|
try testing.expect(sel.containsRow(.{ .x = 1, .y = 3 })); // x does not matter
|
||||||
|
try testing.expect(sel.containsRow(.{ .x = 1, .y = 6 }));
|
||||||
|
try testing.expect(sel.containsRow(.{ .x = 5, .y = 9 }));
|
||||||
|
try testing.expect(!sel.containsRow(.{ .x = 5, .y = 10 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse
|
||||||
|
{
|
||||||
|
const sel: Selection = .{
|
||||||
|
.start = .{ .x = 7, .y = 9 },
|
||||||
|
.end = .{ .x = 3, .y = 3 },
|
||||||
|
.rectangle = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
try testing.expect(sel.contains(.{ .x = 5, .y = 6 })); // Center
|
||||||
|
try testing.expect(sel.contains(.{ .x = 3, .y = 6 })); // Left border
|
||||||
|
try testing.expect(sel.contains(.{ .x = 7, .y = 6 })); // Right border
|
||||||
|
try testing.expect(sel.contains(.{ .x = 5, .y = 3 })); // Top border
|
||||||
|
try testing.expect(sel.contains(.{ .x = 5, .y = 9 })); // Bottom border
|
||||||
|
|
||||||
|
try testing.expect(!sel.contains(.{ .x = 5, .y = 2 })); // Above center
|
||||||
|
try testing.expect(!sel.contains(.{ .x = 5, .y = 10 })); // Below center
|
||||||
|
try testing.expect(!sel.contains(.{ .x = 2, .y = 6 })); // Left center
|
||||||
|
try testing.expect(!sel.contains(.{ .x = 8, .y = 6 })); // Right center
|
||||||
|
try testing.expect(!sel.contains(.{ .x = 8, .y = 3 })); // Just right of top right
|
||||||
|
try testing.expect(!sel.contains(.{ .x = 2, .y = 9 })); // Just left of bottom left
|
||||||
|
|
||||||
|
try testing.expect(!sel.containsRow(.{ .x = 1, .y = 1 }));
|
||||||
|
try testing.expect(sel.containsRow(.{ .x = 1, .y = 3 })); // x does not matter
|
||||||
|
try testing.expect(sel.containsRow(.{ .x = 1, .y = 6 }));
|
||||||
|
try testing.expect(sel.containsRow(.{ .x = 5, .y = 9 }));
|
||||||
|
try testing.expect(!sel.containsRow(.{ .x = 5, .y = 10 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single line
|
||||||
|
// NOTE: This is the same as normal selection but we just do it for brevity
|
||||||
|
{
|
||||||
|
const sel: Selection = .{
|
||||||
|
.start = .{ .x = 5, .y = 1 },
|
||||||
|
.end = .{ .x = 10, .y = 1 },
|
||||||
|
.rectangle = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
try testing.expect(sel.contains(.{ .x = 6, .y = 1 }));
|
||||||
|
try testing.expect(!sel.contains(.{ .x = 2, .y = 1 }));
|
||||||
|
try testing.expect(!sel.contains(.{ .x = 12, .y = 1 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "Selection: containedRow" {
|
test "Selection: containedRow" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
var screen = try Screen.init(testing.allocator, 5, 10, 0);
|
var screen = try Screen.init(testing.allocator, 5, 10, 0);
|
||||||
@ -245,6 +336,39 @@ test "Selection: containedRow" {
|
|||||||
}, sel.containedRow(&screen, .{ .x = 2, .y = 2 }).?);
|
}, sel.containedRow(&screen, .{ .x = 2, .y = 2 }).?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rectangle
|
||||||
|
{
|
||||||
|
const sel: Selection = .{
|
||||||
|
.start = .{ .x = 3, .y = 1 },
|
||||||
|
.end = .{ .x = 6, .y = 3 },
|
||||||
|
.rectangle = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Not contained
|
||||||
|
try testing.expect(sel.containedRow(&screen, .{ .x = 1, .y = 4 }) == null);
|
||||||
|
|
||||||
|
// Start line
|
||||||
|
try testing.expectEqual(Selection{
|
||||||
|
.start = .{ .x = 3, .y = 1 },
|
||||||
|
.end = .{ .x = 6, .y = 1 },
|
||||||
|
.rectangle = true,
|
||||||
|
}, sel.containedRow(&screen, .{ .x = 1, .y = 1 }).?);
|
||||||
|
|
||||||
|
// End line
|
||||||
|
try testing.expectEqual(Selection{
|
||||||
|
.start = .{ .x = 3, .y = 3 },
|
||||||
|
.end = .{ .x = 6, .y = 3 },
|
||||||
|
.rectangle = true,
|
||||||
|
}, sel.containedRow(&screen, .{ .x = 2, .y = 3 }).?);
|
||||||
|
|
||||||
|
// Middle line
|
||||||
|
try testing.expectEqual(Selection{
|
||||||
|
.start = .{ .x = 3, .y = 2 },
|
||||||
|
.end = .{ .x = 6, .y = 2 },
|
||||||
|
.rectangle = true,
|
||||||
|
}, sel.containedRow(&screen, .{ .x = 2, .y = 2 }).?);
|
||||||
|
}
|
||||||
|
|
||||||
// Single-line selection
|
// Single-line selection
|
||||||
{
|
{
|
||||||
const sel: Selection = .{
|
const sel: Selection = .{
|
||||||
|
Reference in New Issue
Block a user