2024-01-19 13:45:44 +01:00
|
|
|
const std = @import("std");
|
|
|
|
|
2024-02-24 16:08:20 +01:00
|
|
|
pub const Screen = @import("Screen.zig");
|
2024-02-11 19:59:33 +01:00
|
|
|
const Cell = @import("Cell.zig");
|
2024-04-10 21:37:28 +02:00
|
|
|
const Mouse = @import("Mouse.zig");
|
2024-02-11 19:59:33 +01:00
|
|
|
const Segment = @import("Cell.zig").Segment;
|
2024-04-29 21:00:08 +02:00
|
|
|
const Unicode = @import("Unicode.zig");
|
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-02-24 16:08:20 +01:00
|
|
|
pub const Window = @This();
|
2024-01-19 13:45:44 +01:00
|
|
|
|
|
|
|
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,
|
|
|
|
|
2024-04-19 18:10:36 +02:00
|
|
|
/// Deprecated. Use `child` instead
|
|
|
|
///
|
2024-01-19 13:45:44 +01:00
|
|
|
/// 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) {
|
2024-03-21 19:54:49 +01:00
|
|
|
.expand => self.width -| x_off,
|
2024-01-19 13:45:44 +01:00
|
|
|
.limit => |w| blk: {
|
|
|
|
if (w + x_off > self.width) {
|
2024-03-21 19:54:49 +01:00
|
|
|
break :blk self.width -| x_off;
|
2024-01-19 13:45:44 +01:00
|
|
|
}
|
|
|
|
break :blk w;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
const resolved_height = switch (height) {
|
2024-03-02 04:29:16 +01:00
|
|
|
.expand => self.height -| y_off,
|
2024-01-19 13:45:44 +01:00
|
|
|
.limit => |h| blk: {
|
|
|
|
if (h + y_off > self.height) {
|
2024-03-02 04:29:16 +01:00
|
|
|
break :blk self.height -| y_off;
|
2024-01-19 13:45:44 +01:00
|
|
|
}
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-03-21 19:54:49 +01:00
|
|
|
pub const ChildOptions = struct {
|
|
|
|
x_off: usize = 0,
|
|
|
|
y_off: usize = 0,
|
|
|
|
/// the width of the resulting child, including any borders
|
|
|
|
width: Size = .expand,
|
|
|
|
/// the height of the resulting child, including any borders
|
|
|
|
height: Size = .expand,
|
|
|
|
border: BorderOptions = .{},
|
|
|
|
};
|
|
|
|
|
|
|
|
pub const BorderOptions = struct {
|
|
|
|
style: Cell.Style = .{},
|
2024-03-26 17:41:54 +01:00
|
|
|
where: union(enum) {
|
2024-03-21 19:54:49 +01:00
|
|
|
none,
|
|
|
|
all,
|
|
|
|
top,
|
|
|
|
right,
|
|
|
|
bottom,
|
|
|
|
left,
|
2024-03-26 17:41:54 +01:00
|
|
|
other: Locations,
|
2024-03-21 19:54:49 +01:00
|
|
|
} = .none,
|
|
|
|
glyphs: Glyphs = .single_rounded,
|
|
|
|
|
2024-03-26 17:41:54 +01:00
|
|
|
pub const Locations = packed struct {
|
|
|
|
top: bool = false,
|
|
|
|
right: bool = false,
|
|
|
|
bottom: bool = false,
|
|
|
|
left: bool = false,
|
|
|
|
};
|
|
|
|
|
2024-03-21 19:54:49 +01:00
|
|
|
pub const Glyphs = union(enum) {
|
|
|
|
single_rounded,
|
|
|
|
single_square,
|
|
|
|
/// custom border glyphs. each glyph should be one cell wide and the
|
|
|
|
/// following indices apply:
|
|
|
|
/// [0] = top left
|
|
|
|
/// [1] = horizontal
|
|
|
|
/// [2] = top right
|
|
|
|
/// [3] = vertical
|
|
|
|
/// [4] = bottom right
|
|
|
|
/// [5] = bottom left
|
|
|
|
custom: [6][]const u8,
|
|
|
|
};
|
|
|
|
|
|
|
|
const single_rounded: [6][]const u8 = .{ "╭", "─", "╮", "│", "╯", "╰" };
|
|
|
|
const single_square: [6][]const u8 = .{ "┌", "─", "┐", "│", "┘", "└" };
|
|
|
|
};
|
|
|
|
|
|
|
|
/// create a child window
|
|
|
|
pub fn child(self: Window, opts: ChildOptions) Window {
|
|
|
|
var result = self.initChild(opts.x_off, opts.y_off, opts.width, opts.height);
|
|
|
|
|
|
|
|
const glyphs = switch (opts.border.glyphs) {
|
|
|
|
.single_rounded => BorderOptions.single_rounded,
|
|
|
|
.single_square => BorderOptions.single_square,
|
|
|
|
.custom => |custom| custom,
|
|
|
|
};
|
|
|
|
|
|
|
|
const top_left: Cell.Character = .{ .grapheme = glyphs[0], .width = 1 };
|
|
|
|
const horizontal: Cell.Character = .{ .grapheme = glyphs[1], .width = 1 };
|
|
|
|
const top_right: Cell.Character = .{ .grapheme = glyphs[2], .width = 1 };
|
|
|
|
const vertical: Cell.Character = .{ .grapheme = glyphs[3], .width = 1 };
|
|
|
|
const bottom_right: Cell.Character = .{ .grapheme = glyphs[4], .width = 1 };
|
|
|
|
const bottom_left: Cell.Character = .{ .grapheme = glyphs[5], .width = 1 };
|
|
|
|
const style = opts.border.style;
|
|
|
|
|
|
|
|
const h = result.height;
|
|
|
|
const w = result.width;
|
|
|
|
|
2024-03-26 17:41:54 +01:00
|
|
|
const loc: BorderOptions.Locations = switch (opts.border.where) {
|
2024-03-21 19:54:49 +01:00
|
|
|
.none => return result,
|
2024-03-26 17:41:54 +01:00
|
|
|
.all => .{ .top = true, .bottom = true, .right = true, .left = true },
|
|
|
|
.bottom => .{ .bottom = true },
|
|
|
|
.right => .{ .right = true },
|
|
|
|
.left => .{ .left = true },
|
|
|
|
.top => .{ .top = true },
|
|
|
|
.other => |loc| loc,
|
|
|
|
};
|
|
|
|
if (loc.top) {
|
|
|
|
var i: usize = 0;
|
|
|
|
while (i < w) : (i += 1) {
|
|
|
|
result.writeCell(i, 0, .{ .char = horizontal, .style = style });
|
|
|
|
}
|
2024-03-21 19:54:49 +01:00
|
|
|
}
|
2024-03-26 17:41:54 +01:00
|
|
|
if (loc.bottom) {
|
|
|
|
var i: usize = 0;
|
|
|
|
while (i < w) : (i += 1) {
|
|
|
|
result.writeCell(i, h -| 1, .{ .char = horizontal, .style = style });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (loc.left) {
|
|
|
|
var i: usize = 0;
|
|
|
|
while (i < h) : (i += 1) {
|
|
|
|
result.writeCell(0, i, .{ .char = vertical, .style = style });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (loc.right) {
|
|
|
|
var i: usize = 0;
|
|
|
|
while (i < h) : (i += 1) {
|
|
|
|
result.writeCell(w -| 1, i, .{ .char = vertical, .style = style });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// draw corners
|
|
|
|
if (loc.top and loc.left)
|
|
|
|
result.writeCell(0, 0, .{ .char = top_left, .style = style });
|
|
|
|
if (loc.top and loc.right)
|
|
|
|
result.writeCell(w - 1, 0, .{ .char = top_right, .style = style });
|
|
|
|
if (loc.bottom and loc.left)
|
|
|
|
result.writeCell(0, h -| 1, .{ .char = bottom_left, .style = style });
|
|
|
|
if (loc.bottom and loc.right)
|
|
|
|
result.writeCell(w - 1, h -| 1, .{ .char = bottom_right, .style = style });
|
|
|
|
|
|
|
|
const x_off: usize = if (loc.left) 1 else 0;
|
|
|
|
const y_off: usize = if (loc.top) 1 else 0;
|
|
|
|
const h_delt: usize = if (loc.bottom) 1 else 0;
|
|
|
|
const w_delt: usize = if (loc.right) 1 else 0;
|
|
|
|
const h_ch: usize = h - y_off - h_delt;
|
|
|
|
const w_ch: usize = w - x_off - w_delt;
|
|
|
|
return result.initChild(x_off, y_off, .{ .limit = w_ch }, .{ .limit = h_ch });
|
2024-03-21 19:54:49 +01:00
|
|
|
}
|
|
|
|
|
2024-01-19 13:45:44 +01:00
|
|
|
/// 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-05-08 12:58:36 +02:00
|
|
|
/// reads a cell at the location in the window
|
|
|
|
pub fn readCell(self: Window, col: usize, row: usize) ?Cell {
|
|
|
|
if (self.height == 0 or self.width == 0) return null;
|
|
|
|
if (self.height <= row or self.width <= col) return null;
|
|
|
|
return self.screen.readCell(col + self.x_off, row + self.y_off);
|
|
|
|
}
|
|
|
|
|
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-04-30 13:49:29 +02:00
|
|
|
self.fill(.{ .default = true });
|
2024-01-19 20:16:53 +01:00
|
|
|
}
|
|
|
|
|
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 {
|
2024-04-29 21:00:08 +02:00
|
|
|
return gw.gwidth(str, self.screen.width_method, &self.screen.unicode.width_data) catch 1;
|
2024-01-24 13:12:39 +01:00
|
|
|
}
|
|
|
|
|
2024-01-19 20:16:53 +01:00
|
|
|
/// fills the window with the provided cell
|
|
|
|
pub fn fill(self: Window, cell: Cell) void {
|
2024-04-30 19:33:46 +02:00
|
|
|
if (self.x_off == 0 and self.width == self.screen.width) {
|
|
|
|
// we have a full width window, therefore contiguous memory.
|
|
|
|
const start = self.y_off * self.width;
|
|
|
|
const end = start + (self.height * self.width);
|
|
|
|
@memset(self.screen.buf[start..end], cell);
|
|
|
|
} else {
|
|
|
|
// Non-contiguous. Iterate over rows an memset
|
|
|
|
var row: usize = self.y_off;
|
2024-05-13 03:24:31 +02:00
|
|
|
const last_row = @min(self.height + self.y_off, self.screen.height);
|
2024-05-11 13:38:08 +02:00
|
|
|
while (row < last_row) : (row += 1) {
|
2024-04-30 19:33:46 +02:00
|
|
|
const start = self.x_off + (row * self.screen.width);
|
2024-05-11 13:38:08 +02:00
|
|
|
const end = @min(start + self.width, start + (self.screen.width - self.x_off));
|
2024-04-30 19:33:46 +02:00
|
|
|
@memset(self.screen.buf[start..end], 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-04-15 14:14:31 +02:00
|
|
|
pub fn setCursorShape(self: Window, shape: Cell.CursorShape) void {
|
|
|
|
self.screen.cursor_shape = shape;
|
|
|
|
}
|
|
|
|
|
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,
|
2024-04-20 02:12:52 +02:00
|
|
|
|
|
|
|
/// when true, print will write to the screen for rendering. When false,
|
|
|
|
/// nothing is written. The return value describes the size of the wrapped
|
|
|
|
/// text
|
|
|
|
commit: bool = true,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub const PrintResult = struct {
|
|
|
|
col: usize,
|
|
|
|
row: usize,
|
|
|
|
overflow: bool,
|
2024-03-01 19:28:29 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
/// prints segments to the window. returns true if the text overflowed with the
|
|
|
|
/// given wrap strategy and size.
|
2024-05-03 20:58:23 +02:00
|
|
|
pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) !PrintResult {
|
2024-03-01 19:28:29 +01:00
|
|
|
var row = opts.row_offset;
|
|
|
|
switch (opts.wrap) {
|
|
|
|
.grapheme => {
|
|
|
|
var col: usize = 0;
|
2024-04-20 02:12:52 +02:00
|
|
|
const overflow: bool = blk: for (segments) |segment| {
|
2024-04-29 20:01:04 +02:00
|
|
|
var iter = self.screen.unicode.graphemeIterator(segment.text);
|
2024-03-01 19:28:29 +01:00
|
|
|
while (iter.next()) |grapheme| {
|
2024-04-20 02:12:52 +02:00
|
|
|
if (row >= self.height) break :blk true;
|
2024-04-29 21:00:08 +02:00
|
|
|
const s = grapheme.bytes(segment.text);
|
2024-03-01 19:28:29 +01:00
|
|
|
if (std.mem.eql(u8, s, "\n")) {
|
|
|
|
row += 1;
|
|
|
|
col = 0;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const w = self.gwidth(s);
|
2024-03-12 01:01:43 +01:00
|
|
|
if (w == 0) continue;
|
2024-04-20 02:12:52 +02:00
|
|
|
if (opts.commit) self.writeCell(col, row, .{
|
2024-03-01 19:28:29 +01:00
|
|
|
.char = .{
|
|
|
|
.grapheme = s,
|
|
|
|
.width = w,
|
|
|
|
},
|
|
|
|
.style = segment.style,
|
|
|
|
.link = segment.link,
|
|
|
|
});
|
|
|
|
col += w;
|
2024-04-20 02:12:52 +02:00
|
|
|
if (col >= self.width) {
|
|
|
|
row += 1;
|
|
|
|
col = 0;
|
|
|
|
}
|
2024-03-01 19:28:29 +01:00
|
|
|
}
|
2024-04-20 02:12:52 +02:00
|
|
|
} else false;
|
|
|
|
|
|
|
|
return .{
|
|
|
|
.row = row,
|
|
|
|
.col = col,
|
|
|
|
.overflow = overflow,
|
|
|
|
};
|
2024-03-01 19:28:29 +01:00
|
|
|
},
|
|
|
|
.word => {
|
|
|
|
var col: usize = 0;
|
2024-04-20 05:31:37 +02:00
|
|
|
var overflow: bool = false;
|
|
|
|
var soft_wrapped: bool = false;
|
|
|
|
for (segments) |segment| {
|
|
|
|
var start: usize = 0;
|
|
|
|
var i: usize = 0;
|
|
|
|
while (i < segment.text.len) : (i += 1) {
|
|
|
|
// for (segment.text, 0..) |b, i| {
|
|
|
|
const b = segment.text[i];
|
|
|
|
const end = switch (b) {
|
|
|
|
' ',
|
|
|
|
'\r',
|
|
|
|
'\n',
|
|
|
|
=> i,
|
|
|
|
else => if (i != segment.text.len - 1) continue else i + 1,
|
|
|
|
};
|
|
|
|
const word = segment.text[start..end];
|
|
|
|
// find the start of the next word
|
|
|
|
start = while (i + 1 < segment.text.len) : (i += 1) {
|
|
|
|
if (segment.text[i + 1] == ' ') continue;
|
|
|
|
break i + 1;
|
|
|
|
} else i;
|
|
|
|
const width = self.gwidth(word);
|
|
|
|
const non_wsp_width: usize = for (word, 0..) |wb, wi| {
|
|
|
|
if (wb == '\r' or wb == '\n') {
|
2024-03-01 19:28:29 +01:00
|
|
|
row += 1;
|
|
|
|
col = 0;
|
2024-04-20 05:31:37 +02:00
|
|
|
break width -| wi -| 1;
|
2024-03-01 19:28:29 +01:00
|
|
|
}
|
2024-04-20 05:31:37 +02:00
|
|
|
if (wb != ' ') break width - wi;
|
|
|
|
} else 0;
|
|
|
|
|
|
|
|
if (width + col > self.width and non_wsp_width < self.width) {
|
|
|
|
// wrap
|
|
|
|
row += 1;
|
|
|
|
col = 0;
|
|
|
|
soft_wrapped = true;
|
|
|
|
}
|
|
|
|
if (row >= self.height) {
|
|
|
|
overflow = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// if we are soft wrapped, (col == 0 and row > 0), then trim
|
|
|
|
// leading spaces
|
|
|
|
const printed_word = if (soft_wrapped)
|
|
|
|
std.mem.trimLeft(u8, word, " ")
|
|
|
|
else
|
|
|
|
word;
|
|
|
|
defer soft_wrapped = false;
|
2024-04-29 21:00:08 +02:00
|
|
|
var iter = self.screen.unicode.graphemeIterator(printed_word);
|
2024-04-20 05:31:37 +02:00
|
|
|
while (iter.next()) |grapheme| {
|
2024-04-29 21:00:08 +02:00
|
|
|
const s = grapheme.bytes(printed_word);
|
2024-04-20 05:31:37 +02:00
|
|
|
const w = self.gwidth(s);
|
|
|
|
if (opts.commit) self.writeCell(col, row, .{
|
|
|
|
.char = .{
|
|
|
|
.grapheme = s,
|
|
|
|
.width = w,
|
|
|
|
},
|
|
|
|
.style = segment.style,
|
|
|
|
.link = segment.link,
|
|
|
|
});
|
|
|
|
col += w;
|
|
|
|
if (col >= self.width) {
|
|
|
|
row += 1;
|
|
|
|
col = 0;
|
2024-04-20 02:12:52 +02:00
|
|
|
}
|
2024-04-20 05:31:37 +02:00
|
|
|
}
|
|
|
|
switch (b) {
|
|
|
|
' ' => {
|
|
|
|
if (col > 0) {
|
|
|
|
if (opts.commit) self.writeCell(col, row, .{
|
|
|
|
.char = .{
|
|
|
|
.grapheme = " ",
|
|
|
|
.width = 1,
|
|
|
|
},
|
|
|
|
.style = segment.style,
|
|
|
|
.link = segment.link,
|
|
|
|
});
|
|
|
|
col += 1;
|
2024-04-20 02:12:52 +02:00
|
|
|
}
|
2024-04-20 05:31:37 +02:00
|
|
|
},
|
|
|
|
'\r',
|
|
|
|
'\n',
|
|
|
|
=> {
|
|
|
|
col = 0;
|
|
|
|
row += 1;
|
|
|
|
},
|
|
|
|
else => {},
|
2024-03-01 19:28:29 +01:00
|
|
|
}
|
|
|
|
}
|
2024-04-20 05:31:37 +02:00
|
|
|
}
|
2024-04-20 02:12:52 +02:00
|
|
|
return .{
|
|
|
|
// remove last row counter
|
2024-04-20 05:31:37 +02:00
|
|
|
.row = row,
|
2024-04-20 02:12:52 +02:00
|
|
|
.col = col,
|
|
|
|
.overflow = overflow,
|
|
|
|
};
|
2024-03-01 19:28:29 +01:00
|
|
|
},
|
|
|
|
.none => {
|
|
|
|
var col: usize = 0;
|
2024-04-20 02:12:52 +02:00
|
|
|
const overflow: bool = blk: for (segments) |segment| {
|
2024-04-29 20:01:04 +02:00
|
|
|
var iter = self.screen.unicode.graphemeIterator(segment.text);
|
2024-03-01 19:28:29 +01:00
|
|
|
while (iter.next()) |grapheme| {
|
2024-04-20 02:12:52 +02:00
|
|
|
if (col >= self.width) break :blk true;
|
2024-04-29 21:00:08 +02:00
|
|
|
const s = grapheme.bytes(segment.text);
|
2024-04-20 02:12:52 +02:00
|
|
|
if (std.mem.eql(u8, s, "\n")) break :blk true;
|
2024-03-01 19:28:29 +01:00
|
|
|
const w = self.gwidth(s);
|
2024-03-12 01:01:43 +01:00
|
|
|
if (w == 0) continue;
|
2024-04-20 02:12:52 +02:00
|
|
|
if (opts.commit) self.writeCell(col, row, .{
|
2024-03-01 19:28:29 +01:00
|
|
|
.char = .{
|
|
|
|
.grapheme = s,
|
|
|
|
.width = w,
|
|
|
|
},
|
|
|
|
.style = segment.style,
|
|
|
|
.link = segment.link,
|
|
|
|
});
|
|
|
|
col += w;
|
2024-02-01 02:13:56 +01:00
|
|
|
}
|
2024-04-20 02:12:52 +02:00
|
|
|
} else false;
|
|
|
|
return .{
|
|
|
|
.row = row,
|
|
|
|
.col = col,
|
|
|
|
.overflow = overflow,
|
|
|
|
};
|
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;
|
|
|
|
}
|
|
|
|
|
2024-04-19 18:10:36 +02:00
|
|
|
/// Deprecated. use print instead
|
2024-05-03 20:58:23 +02:00
|
|
|
pub fn wrap(self: Window, segments: []const Segment) !void {
|
2024-03-10 13:47:28 +01:00
|
|
|
_ = try self.print(segments, .{ .wrap = .word });
|
2024-02-01 02:13:56 +01:00
|
|
|
}
|
|
|
|
|
2024-04-10 21:37:28 +02:00
|
|
|
/// scrolls the window down one row (IE inserts a blank row at the bottom of the
|
|
|
|
/// screen and shifts all rows up one)
|
|
|
|
pub fn scroll(self: Window, n: usize) void {
|
|
|
|
if (n > self.height) return;
|
|
|
|
var row = self.y_off;
|
|
|
|
while (row < self.height - n) : (row += 1) {
|
|
|
|
const dst_start = (row * self.width) + self.x_off;
|
|
|
|
const dst_end = dst_start + self.width;
|
|
|
|
|
|
|
|
const src_start = ((row + n) * self.width) + self.x_off;
|
|
|
|
const src_end = src_start + self.width;
|
|
|
|
@memcpy(self.screen.buf[dst_start..dst_end], self.screen.buf[src_start..src_end]);
|
|
|
|
}
|
|
|
|
const last_row = self.child(.{
|
|
|
|
.y_off = self.height - n,
|
|
|
|
});
|
|
|
|
last_row.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// returns the mouse event if the mouse event occurred within the window. If
|
|
|
|
/// the mouse event occurred outside the window, null is returned
|
2024-04-16 20:04:30 +02:00
|
|
|
pub fn hasMouse(win: Window, mouse: ?Mouse) ?Mouse {
|
2024-04-10 21:37:28 +02:00
|
|
|
const event = mouse orelse return null;
|
|
|
|
if (event.col >= win.x_off and
|
|
|
|
event.col < (win.x_off + win.width) and
|
|
|
|
event.row >= win.y_off and
|
|
|
|
event.row < (win.y_off + win.height)) return event else return null;
|
|
|
|
}
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
|
2024-03-21 19:54:49 +01:00
|
|
|
const ch = parent.initChild(1, 1, .expand, .expand);
|
|
|
|
try std.testing.expectEqual(19, ch.width);
|
|
|
|
try std.testing.expectEqual(19, ch.height);
|
2024-01-19 13:45:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
test "Window size set too big" {
|
|
|
|
var parent = Window{
|
|
|
|
.x_off = 0,
|
|
|
|
.y_off = 0,
|
|
|
|
.width = 20,
|
|
|
|
.height = 20,
|
|
|
|
.screen = undefined,
|
|
|
|
};
|
|
|
|
|
2024-03-21 19:54:49 +01:00
|
|
|
const ch = parent.initChild(0, 0, .{ .limit = 21 }, .{ .limit = 21 });
|
|
|
|
try std.testing.expectEqual(20, ch.width);
|
|
|
|
try std.testing.expectEqual(20, ch.height);
|
2024-01-19 13:45:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
test "Window size set too big with offset" {
|
|
|
|
var parent = Window{
|
|
|
|
.x_off = 0,
|
|
|
|
.y_off = 0,
|
|
|
|
.width = 20,
|
|
|
|
.height = 20,
|
|
|
|
.screen = undefined,
|
|
|
|
};
|
|
|
|
|
2024-03-21 19:54:49 +01:00
|
|
|
const ch = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 });
|
|
|
|
try std.testing.expectEqual(10, ch.width);
|
|
|
|
try std.testing.expectEqual(10, ch.height);
|
2024-01-19 13:45:44 +01:00
|
|
|
}
|
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,
|
|
|
|
};
|
|
|
|
|
2024-03-21 19:54:49 +01:00
|
|
|
const ch = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 });
|
|
|
|
try std.testing.expectEqual(11, ch.x_off);
|
|
|
|
try std.testing.expectEqual(11, ch.y_off);
|
2024-01-19 17:17:11 +01:00
|
|
|
}
|
2024-04-20 02:12:52 +02:00
|
|
|
|
|
|
|
test "print: grapheme" {
|
2024-04-29 21:00:08 +02:00
|
|
|
const alloc = std.testing.allocator_instance.allocator();
|
|
|
|
const unicode = try Unicode.init(alloc);
|
|
|
|
defer unicode.deinit();
|
|
|
|
var screen: Screen = .{ .width_method = .unicode, .unicode = &unicode };
|
2024-04-20 02:12:52 +02:00
|
|
|
const win: Window = .{
|
|
|
|
.x_off = 0,
|
|
|
|
.y_off = 0,
|
|
|
|
.width = 4,
|
|
|
|
.height = 2,
|
|
|
|
.screen = &screen,
|
|
|
|
};
|
|
|
|
const opts: PrintOptions = .{
|
|
|
|
.commit = false,
|
|
|
|
.wrap = .grapheme,
|
|
|
|
};
|
|
|
|
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "a" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(1, result.col);
|
|
|
|
try std.testing.expectEqual(0, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "abcd" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(0, result.col);
|
|
|
|
try std.testing.expectEqual(1, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "abcde" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(1, result.col);
|
|
|
|
try std.testing.expectEqual(1, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "abcdefgh" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(0, result.col);
|
|
|
|
try std.testing.expectEqual(2, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "abcdefghi" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(0, result.col);
|
|
|
|
try std.testing.expectEqual(2, result.row);
|
|
|
|
try std.testing.expectEqual(true, result.overflow);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
test "print: word" {
|
2024-04-29 21:00:08 +02:00
|
|
|
const alloc = std.testing.allocator_instance.allocator();
|
|
|
|
const unicode = try Unicode.init(alloc);
|
|
|
|
defer unicode.deinit();
|
2024-04-20 02:12:52 +02:00
|
|
|
var screen: Screen = .{
|
2024-04-29 21:00:08 +02:00
|
|
|
.width_method = .unicode,
|
|
|
|
.unicode = &unicode,
|
2024-04-20 02:12:52 +02:00
|
|
|
};
|
|
|
|
const win: Window = .{
|
|
|
|
.x_off = 0,
|
|
|
|
.y_off = 0,
|
|
|
|
.width = 4,
|
|
|
|
.height = 2,
|
|
|
|
.screen = &screen,
|
|
|
|
};
|
|
|
|
const opts: PrintOptions = .{
|
|
|
|
.commit = false,
|
|
|
|
.wrap = .word,
|
|
|
|
};
|
|
|
|
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "a" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(1, result.col);
|
|
|
|
try std.testing.expectEqual(0, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "a b" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(3, result.col);
|
|
|
|
try std.testing.expectEqual(0, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "a b c" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(1, result.col);
|
|
|
|
try std.testing.expectEqual(1, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "hello" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(1, result.col);
|
|
|
|
try std.testing.expectEqual(1, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "hi tim" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(3, result.col);
|
|
|
|
try std.testing.expectEqual(1, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "hello tim" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(0, result.col);
|
|
|
|
try std.testing.expectEqual(2, result.row);
|
|
|
|
try std.testing.expectEqual(true, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "hello ti" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(0, result.col);
|
|
|
|
try std.testing.expectEqual(2, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
2024-04-20 05:31:37 +02:00
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "h" },
|
|
|
|
.{ .text = "e" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(2, result.col);
|
|
|
|
try std.testing.expectEqual(0, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "h" },
|
|
|
|
.{ .text = "e" },
|
|
|
|
.{ .text = "l" },
|
|
|
|
.{ .text = "l" },
|
|
|
|
.{ .text = "o" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(1, result.col);
|
|
|
|
try std.testing.expectEqual(1, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "he\n" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(0, result.col);
|
|
|
|
try std.testing.expectEqual(1, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "he\n\n" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(0, result.col);
|
|
|
|
try std.testing.expectEqual(2, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "not now" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(3, result.col);
|
|
|
|
try std.testing.expectEqual(1, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
var segments = [_]Segment{
|
|
|
|
.{ .text = "note now" },
|
|
|
|
};
|
|
|
|
const result = try win.print(&segments, opts);
|
|
|
|
try std.testing.expectEqual(3, result.col);
|
|
|
|
try std.testing.expectEqual(1, result.row);
|
|
|
|
try std.testing.expectEqual(false, result.overflow);
|
|
|
|
}
|
2024-04-20 02:12:52 +02:00
|
|
|
}
|