2024-01-19 13:45:44 +01:00
|
|
|
const std = @import("std");
|
2024-02-01 02:13:56 +01:00
|
|
|
const ziglyph = @import("ziglyph");
|
|
|
|
const WordIterator = ziglyph.WordIterator;
|
|
|
|
const GraphemeIterator = ziglyph.GraphemeIterator;
|
2024-01-19 13:45:44 +01:00
|
|
|
|
|
|
|
const Screen = @import("Screen.zig");
|
2024-02-11 19:59:33 +01:00
|
|
|
const Cell = @import("Cell.zig");
|
|
|
|
const Segment = @import("Cell.zig").Segment;
|
2024-01-24 13:12:39 +01:00
|
|
|
const gw = @import("gwidth.zig");
|
2024-01-19 13:45:44 +01:00
|
|
|
|
2024-01-19 19:21:14 +01:00
|
|
|
const log = std.log.scoped(.window);
|
|
|
|
|
2024-01-19 13:45:44 +01:00
|
|
|
const Window = @This();
|
|
|
|
|
|
|
|
pub const Size = union(enum) {
|
|
|
|
expand,
|
|
|
|
limit: usize,
|
|
|
|
};
|
|
|
|
|
2024-01-19 17:21:49 +01:00
|
|
|
/// horizontal offset from the screen
|
2024-01-19 13:45:44 +01:00
|
|
|
x_off: usize,
|
2024-01-19 17:21:49 +01:00
|
|
|
/// vertical offset from the screen
|
2024-01-19 13:45:44 +01:00
|
|
|
y_off: usize,
|
2024-01-19 17:21:49 +01:00
|
|
|
/// width of the window. This can't be larger than the terminal screen
|
2024-01-19 13:45:44 +01:00
|
|
|
width: usize,
|
2024-01-19 17:21:49 +01:00
|
|
|
/// height of the window. This can't be larger than the terminal screen
|
2024-01-19 13:45:44 +01:00
|
|
|
height: usize,
|
|
|
|
|
|
|
|
screen: *Screen,
|
|
|
|
|
|
|
|
/// Creates a new window with offset relative to parent and size clamped to the
|
2024-01-19 17:21:49 +01:00
|
|
|
/// parent's size. Windows do not retain a reference to their parent and are
|
|
|
|
/// unaware of resizes.
|
2024-01-19 13:45:44 +01:00
|
|
|
pub fn initChild(
|
2024-01-19 18:30:00 +01:00
|
|
|
self: Window,
|
2024-01-19 13:45:44 +01:00
|
|
|
x_off: usize,
|
|
|
|
y_off: usize,
|
|
|
|
width: Size,
|
|
|
|
height: Size,
|
|
|
|
) Window {
|
|
|
|
const resolved_width = switch (width) {
|
|
|
|
.expand => self.width - x_off,
|
|
|
|
.limit => |w| blk: {
|
|
|
|
if (w + x_off > self.width) {
|
|
|
|
break :blk self.width - x_off;
|
|
|
|
}
|
|
|
|
break :blk w;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
const resolved_height = switch (height) {
|
|
|
|
.expand => self.height - y_off,
|
|
|
|
.limit => |h| blk: {
|
|
|
|
if (h + y_off > self.height) {
|
|
|
|
break :blk self.height - y_off;
|
|
|
|
}
|
|
|
|
break :blk h;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
return Window{
|
2024-01-19 17:17:11 +01:00
|
|
|
.x_off = x_off + self.x_off,
|
|
|
|
.y_off = y_off + self.y_off,
|
2024-01-19 13:45:44 +01:00
|
|
|
.width = resolved_width,
|
|
|
|
.height = resolved_height,
|
|
|
|
.screen = self.screen,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/// writes a cell to the location in the window
|
2024-01-19 19:21:14 +01:00
|
|
|
pub fn writeCell(self: Window, col: usize, row: usize, cell: Cell) void {
|
|
|
|
if (self.height == 0 or self.width == 0) return;
|
2024-01-22 18:18:59 +01:00
|
|
|
if (self.height <= row or self.width <= col) return;
|
2024-01-19 19:21:14 +01:00
|
|
|
self.screen.writeCell(col + self.x_off, row + self.y_off, cell);
|
2024-01-19 13:45:44 +01:00
|
|
|
}
|
|
|
|
|
2024-01-19 20:16:53 +01:00
|
|
|
/// fills the window with the default cell
|
2024-01-19 20:13:20 +01:00
|
|
|
pub fn clear(self: Window) void {
|
2024-01-19 20:16:53 +01:00
|
|
|
self.fill(.{});
|
|
|
|
}
|
|
|
|
|
2024-01-24 13:12:39 +01:00
|
|
|
/// returns the width of the grapheme. This depends on the terminal capabilities
|
|
|
|
pub fn gwidth(self: Window, str: []const u8) usize {
|
|
|
|
const m: gw.Method = if (self.screen.unicode) .unicode else .wcwidth;
|
|
|
|
return gw.gwidth(str, m) catch 1;
|
|
|
|
}
|
|
|
|
|
2024-01-19 20:16:53 +01:00
|
|
|
/// fills the window with the provided cell
|
|
|
|
pub fn fill(self: Window, cell: Cell) void {
|
2024-01-19 20:13:20 +01:00
|
|
|
var row: usize = self.y_off;
|
|
|
|
while (row < (self.height + self.y_off)) : (row += 1) {
|
|
|
|
var col: usize = self.x_off;
|
|
|
|
while (col < (self.width + self.x_off)) : (col += 1) {
|
2024-01-19 20:16:53 +01:00
|
|
|
self.screen.writeCell(col, row, cell);
|
2024-01-19 20:13:20 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-22 18:18:59 +01:00
|
|
|
/// hide the cursor
|
|
|
|
pub fn hideCursor(self: Window) void {
|
|
|
|
self.screen.cursor_vis = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// show the cursor at the given coordinates, 0 indexed
|
|
|
|
pub fn showCursor(self: Window, col: usize, row: usize) void {
|
|
|
|
if (self.height == 0 or self.width == 0) return;
|
|
|
|
if (self.height <= row or self.width <= col) return;
|
|
|
|
self.screen.cursor_vis = true;
|
|
|
|
self.screen.cursor_row = row + self.y_off;
|
|
|
|
self.screen.cursor_col = col + self.x_off;
|
|
|
|
}
|
|
|
|
|
2024-03-01 19:28:29 +01:00
|
|
|
/// Options to use when printing Segments to a window
|
|
|
|
pub const PrintOptions = struct {
|
|
|
|
/// vertical offset to start printing at
|
|
|
|
row_offset: usize = 0,
|
|
|
|
|
|
|
|
/// wrap behavior for printing
|
|
|
|
wrap: enum {
|
|
|
|
/// wrap at grapheme boundaries
|
|
|
|
grapheme,
|
|
|
|
/// wrap at word boundaries
|
|
|
|
word,
|
|
|
|
/// stop printing after one line
|
|
|
|
none,
|
|
|
|
} = .grapheme,
|
|
|
|
};
|
|
|
|
|
|
|
|
/// prints segments to the window. returns true if the text overflowed with the
|
|
|
|
/// given wrap strategy and size.
|
|
|
|
pub fn print(self: Window, segments: []Segment, opts: PrintOptions) !bool {
|
|
|
|
var row = opts.row_offset;
|
|
|
|
switch (opts.wrap) {
|
|
|
|
.grapheme => {
|
|
|
|
var col: usize = 0;
|
|
|
|
for (segments) |segment| {
|
|
|
|
var iter = GraphemeIterator.init(segment.text);
|
|
|
|
while (iter.next()) |grapheme| {
|
|
|
|
if (col >= self.width) {
|
|
|
|
row += 1;
|
|
|
|
col = 0;
|
|
|
|
}
|
|
|
|
if (row >= self.height) return true;
|
|
|
|
const s = grapheme.slice(segment.text);
|
|
|
|
if (std.mem.eql(u8, s, "\n")) {
|
|
|
|
row += 1;
|
|
|
|
col = 0;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const w = self.gwidth(s);
|
|
|
|
self.writeCell(col, row, .{
|
|
|
|
.char = .{
|
|
|
|
.grapheme = s,
|
|
|
|
.width = w,
|
|
|
|
},
|
|
|
|
.style = segment.style,
|
|
|
|
.link = segment.link,
|
|
|
|
});
|
|
|
|
col += w;
|
|
|
|
}
|
2024-02-01 02:13:56 +01:00
|
|
|
}
|
2024-03-01 19:28:29 +01:00
|
|
|
},
|
|
|
|
.word => {
|
|
|
|
var col: usize = 0;
|
|
|
|
var wrapped: bool = false;
|
|
|
|
for (segments) |segment| {
|
|
|
|
var word_iter = try WordIterator.init(segment.text);
|
|
|
|
while (word_iter.next()) |word| {
|
|
|
|
// break lines when we need
|
|
|
|
if (word.bytes[0] == '\r' or word.bytes[0] == '\n') {
|
|
|
|
row += 1;
|
|
|
|
col = 0;
|
|
|
|
wrapped = false;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// break lines when we can't fit this word, and the word isn't longer
|
|
|
|
// than our width
|
|
|
|
const word_width = self.gwidth(word.bytes);
|
|
|
|
if (word_width + col >= self.width and word_width < self.width) {
|
|
|
|
row += 1;
|
|
|
|
col = 0;
|
|
|
|
wrapped = true;
|
|
|
|
}
|
|
|
|
if (row >= self.height) return true;
|
|
|
|
// don't print whitespace in the first column, unless we had a hard
|
|
|
|
// break
|
|
|
|
if (col == 0 and std.mem.eql(u8, word.bytes, " ") and wrapped) continue;
|
|
|
|
var iter = GraphemeIterator.init(word.bytes);
|
|
|
|
while (iter.next()) |grapheme| {
|
|
|
|
if (col >= self.width) {
|
|
|
|
row += 1;
|
|
|
|
col = 0;
|
|
|
|
wrapped = true;
|
|
|
|
}
|
|
|
|
const s = grapheme.slice(word.bytes);
|
|
|
|
const w = self.gwidth(s);
|
|
|
|
self.writeCell(col, row, .{
|
|
|
|
.char = .{
|
|
|
|
.grapheme = s,
|
|
|
|
.width = w,
|
|
|
|
},
|
|
|
|
.style = segment.style,
|
|
|
|
.link = segment.link,
|
|
|
|
});
|
|
|
|
col += w;
|
|
|
|
}
|
|
|
|
}
|
2024-02-01 02:13:56 +01:00
|
|
|
}
|
2024-03-01 19:28:29 +01:00
|
|
|
},
|
|
|
|
.none => {
|
|
|
|
var col: usize = 0;
|
|
|
|
for (segments) |segment| {
|
|
|
|
var iter = GraphemeIterator.init(segment.text);
|
|
|
|
while (iter.next()) |grapheme| {
|
|
|
|
if (col >= self.width) return true;
|
|
|
|
const s = grapheme.slice(segment.text);
|
|
|
|
if (std.mem.eql(u8, s, "\n")) return true;
|
|
|
|
const w = self.gwidth(s);
|
|
|
|
self.writeCell(col, row, .{
|
|
|
|
.char = .{
|
|
|
|
.grapheme = s,
|
|
|
|
.width = w,
|
|
|
|
},
|
|
|
|
.style = segment.style,
|
|
|
|
.link = segment.link,
|
|
|
|
});
|
|
|
|
col += w;
|
2024-02-01 02:13:56 +01:00
|
|
|
}
|
|
|
|
}
|
2024-03-01 19:28:29 +01:00
|
|
|
},
|
2024-02-01 02:13:56 +01:00
|
|
|
}
|
2024-03-01 19:28:29 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// prints text in the window with simple word wrapping.
|
|
|
|
pub fn wrap(self: Window, segments: []Segment) !void {
|
|
|
|
return self.print(segments, .{ .wrap = .word });
|
2024-02-01 02:13:56 +01:00
|
|
|
}
|
|
|
|
|
2024-01-19 13:45:44 +01:00
|
|
|
test "Window size set" {
|
|
|
|
var parent = Window{
|
|
|
|
.x_off = 0,
|
|
|
|
.y_off = 0,
|
|
|
|
.width = 20,
|
|
|
|
.height = 20,
|
|
|
|
.screen = undefined,
|
|
|
|
};
|
|
|
|
|
|
|
|
const child = parent.initChild(1, 1, .expand, .expand);
|
|
|
|
try std.testing.expectEqual(19, child.width);
|
|
|
|
try std.testing.expectEqual(19, child.height);
|
|
|
|
}
|
|
|
|
|
|
|
|
test "Window size set too big" {
|
|
|
|
var parent = Window{
|
|
|
|
.x_off = 0,
|
|
|
|
.y_off = 0,
|
|
|
|
.width = 20,
|
|
|
|
.height = 20,
|
|
|
|
.screen = undefined,
|
|
|
|
};
|
|
|
|
|
|
|
|
const child = parent.initChild(0, 0, .{ .limit = 21 }, .{ .limit = 21 });
|
|
|
|
try std.testing.expectEqual(20, child.width);
|
|
|
|
try std.testing.expectEqual(20, child.height);
|
|
|
|
}
|
|
|
|
|
|
|
|
test "Window size set too big with offset" {
|
|
|
|
var parent = Window{
|
|
|
|
.x_off = 0,
|
|
|
|
.y_off = 0,
|
|
|
|
.width = 20,
|
|
|
|
.height = 20,
|
|
|
|
.screen = undefined,
|
|
|
|
};
|
|
|
|
|
|
|
|
const child = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 });
|
|
|
|
try std.testing.expectEqual(10, child.width);
|
|
|
|
try std.testing.expectEqual(10, child.height);
|
|
|
|
}
|
2024-01-19 17:17:11 +01:00
|
|
|
|
|
|
|
test "Window size nested offsets" {
|
|
|
|
var parent = Window{
|
|
|
|
.x_off = 1,
|
|
|
|
.y_off = 1,
|
|
|
|
.width = 20,
|
|
|
|
.height = 20,
|
|
|
|
.screen = undefined,
|
|
|
|
};
|
|
|
|
|
|
|
|
const child = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 });
|
|
|
|
try std.testing.expectEqual(11, child.x_off);
|
|
|
|
try std.testing.expectEqual(11, child.y_off);
|
|
|
|
}
|