flow/src/renderer/vaxis/Plane.zig

518 lines
17 KiB
Zig

const std = @import("std");
const Style = @import("theme").Style;
const ThemeColor = @import("theme").Color;
const FontStyle = @import("theme").FontStyle;
const StyleBits = @import("style.zig").StyleBits;
const Cell = @import("Cell.zig");
const vaxis = @import("vaxis");
const Buffer = @import("Buffer");
const color = @import("color");
const RGB = @import("color").RGB;
const Plane = @This();
const name_buf_len = 128;
window: vaxis.Window,
row: i32 = 0,
col: i32 = 0,
name_buf: [name_buf_len]u8,
name_len: usize,
cache: GraphemeCache = .{},
style: vaxis.Cell.Style = .{},
style_base: vaxis.Cell.Style = .{},
scrolling: bool = false,
transparent: bool = false,
pub const Options = struct {
y: usize = 0,
x: usize = 0,
rows: usize = 0,
cols: usize = 0,
name: []const u8,
flags: option = .none,
};
pub const option = enum {
none,
VSCROLL,
};
pub fn init(nopts: *const Options, parent_: Plane) !Plane {
const opts: vaxis.Window.ChildOptions = .{
.x_off = @as(i17, @intCast(nopts.x)),
.y_off = @as(i17, @intCast(nopts.y)),
.width = @as(u16, @intCast(nopts.cols)),
.height = @as(u16, @intCast(nopts.rows)),
.border = .{},
};
const len = @min(nopts.name.len, name_buf_len);
var plane: Plane = .{
.window = parent_.window.child(opts),
.name_buf = undefined,
.name_len = len,
.scrolling = nopts.flags == .VSCROLL,
};
@memcpy(plane.name_buf[0..len], nopts.name[0..len]);
return plane;
}
pub fn deinit(_: *Plane) void {}
pub fn name(self: Plane, buf: []u8) []const u8 {
@memcpy(buf[0..self.name_len], self.name_buf[0..self.name_len]);
return buf[0..self.name_len];
}
pub fn above(_: Plane) ?Plane {
return null;
}
pub fn below(_: Plane) ?Plane {
return null;
}
pub fn erase(self: Plane) void {
self.window.fill(.{ .style = self.style_base });
}
pub inline fn abs_y(self: Plane) c_int {
return @intCast(self.window.y_off);
}
pub inline fn abs_x(self: Plane) c_int {
return @intCast(self.window.x_off);
}
pub inline fn dim_y(self: Plane) c_uint {
return @intCast(self.window.height);
}
pub inline fn dim_x(self: Plane) c_uint {
return @intCast(self.window.width);
}
pub fn abs_yx_to_rel_nearest_x(self: Plane, y: c_int, x: c_int, xoffset: c_int) struct { c_int, c_int } {
if (self.window.screen.width == 0 or self.window.screen.height == 0) return self.abs_yx_to_rel(y, x);
const xextra = self.window.screen.width_pix % self.window.screen.width;
const xcell = (self.window.screen.width_pix - xextra) / self.window.screen.width;
if (xcell == 0)
return self.abs_yx_to_rel(y, x);
if (xoffset > xcell / 2)
return self.abs_yx_to_rel(y, x + 1);
return self.abs_yx_to_rel(y, x);
}
pub fn abs_yx_to_rel(self: Plane, y: c_int, x: c_int) struct { c_int, c_int } {
return .{ y - self.abs_y(), x - self.abs_x() };
}
pub fn abs_y_to_rel(self: Plane, y: c_int) c_int {
return y - self.abs_y();
}
pub fn rel_yx_to_abs(self: Plane, y: c_int, x: c_int) struct { c_int, c_int } {
return .{ self.abs_y() + y, self.abs_x() + x };
}
pub fn hide(_: Plane) void {}
pub fn move_yx(self: *Plane, y: c_int, x: c_int) !void {
self.window.y_off = @intCast(y);
self.window.x_off = @intCast(x);
}
pub fn resize_simple(self: *Plane, ylen: c_uint, xlen: c_uint) !void {
self.window.height = @intCast(ylen);
self.window.width = @intCast(xlen);
}
pub fn home(self: *Plane) void {
self.row = 0;
self.col = 0;
}
pub fn fill(self: *Plane, egc: []const u8) void {
for (0..self.dim_y()) |y|
for (0..self.dim_x()) |x|
self.write_cell(x, y, egc);
}
pub fn fill_width(self: *Plane, comptime fmt: anytype, args: anytype) !usize {
var buf: [fmt.len + 4096]u8 = undefined;
var pos: usize = 0;
const width = self.window.width;
var text_width: usize = 0;
while (text_width < width) {
const text = try std.fmt.bufPrint(buf[pos..], fmt, args);
pos += text.len;
text_width += self.egc_chunk_width(text, 0, 8);
}
return self.putstr(buf[0..pos]);
}
pub fn print(self: *Plane, comptime fmt: anytype, args: anytype) !usize {
var buf: [fmt.len + 4096]u8 = undefined;
const text = try std.fmt.bufPrint(&buf, fmt, args);
return self.putstr(text);
}
pub fn print_aligned_right(self: *Plane, y: c_int, comptime fmt: anytype, args: anytype) !usize {
var buf: [fmt.len + 4096]u8 = undefined;
const width = self.window.width;
const text = try std.fmt.bufPrint(&buf, fmt, args);
const text_width = self.egc_chunk_width(text, 0, 8);
self.row = @intCast(y);
self.col = @intCast(if (text_width >= width) 0 else width - text_width);
return self.putstr(text);
}
pub fn print_aligned_center(self: *Plane, y: c_int, comptime fmt: anytype, args: anytype) !usize {
var buf: [fmt.len + 4096]u8 = undefined;
const width = self.window.width;
const text = try std.fmt.bufPrint(&buf, fmt, args);
const text_width = self.egc_chunk_width(text, 0, 8);
self.row = @intCast(y);
self.col = @intCast(if (text_width >= width) 0 else (width - text_width) / 2);
return self.putstr(text);
}
pub fn putstr(self: *Plane, text: []const u8) !usize {
var result: usize = 0;
const height = self.window.height;
const width = self.window.width;
var iter = self.window.screen.unicode.graphemeIterator(text);
while (iter.next()) |grapheme| {
const s = grapheme.bytes(text);
if (std.mem.eql(u8, s, "\n")) {
if (self.scrolling and self.row == height - 1)
self.window.scroll(1)
else
self.row += 1;
self.col = 0;
result += 1;
continue;
}
if (self.col >= width) {
if (self.scrolling) {
self.row += 1;
self.col = 0;
} else return result;
}
self.write_cell(@intCast(self.col), @intCast(self.row), s);
result += 1;
}
return result;
}
pub fn putstr_unicode(self: *Plane, text: []const u8) !usize {
var result: usize = 0;
var iter = self.window.screen.unicode.graphemeIterator(text);
while (iter.next()) |grapheme| {
const s_ = grapheme.bytes(text);
const s = switch (s_[0]) {
0...31 => |code| Buffer.unicode.control_code_to_unicode(code),
else => s_,
};
self.write_cell(@intCast(self.col), @intCast(self.row), s);
result += 1;
}
return result;
}
pub fn putchar(self: *Plane, ecg: []const u8) void {
self.write_cell(@intCast(self.col), @intCast(self.row), ecg);
}
pub fn putc(self: *Plane, cell: *const Cell) !usize {
return self.putc_yx(@intCast(self.row), @intCast(self.col), cell);
}
pub fn putc_yx(self: *Plane, y: c_int, x: c_int, cell: *const Cell) !usize {
try self.cursor_move_yx(y, x);
const w = if (cell.cell.char.width == 0) self.window.gwidth(cell.cell.char.grapheme) else cell.cell.char.width;
if (w == 0) return w;
self.window.writeCell(@intCast(self.col), @intCast(self.row), cell.cell);
self.col += @intCast(w);
return w;
}
fn write_cell(self: *Plane, col: usize, row: usize, egc: []const u8) void {
var cell: vaxis.Cell = self.window.readCell(@intCast(col), @intCast(row)) orelse .{ .style = self.style };
const w = self.window.gwidth(egc);
cell.char.grapheme = self.cache.put(egc);
cell.char.width = @intCast(w);
if (self.transparent) {
cell.style.fg = self.style.fg;
} else {
cell.style = self.style;
}
self.window.writeCell(@intCast(col), @intCast(row), cell);
self.col += @intCast(w);
}
pub fn cursor_yx(self: Plane, y: *c_uint, x: *c_uint) void {
y.* = @intCast(self.row);
x.* = @intCast(self.col);
}
pub fn cursor_y(self: Plane) c_uint {
return @intCast(self.row);
}
pub fn cursor_x(self: Plane) c_uint {
return @intCast(self.col);
}
pub fn cursor_move_yx(self: *Plane, y: c_int, x: c_int) !void {
if (self.window.height == 0 or self.window.width == 0) return;
if (self.window.height <= y or self.window.width <= x) return;
if (y >= 0)
self.row = @intCast(y);
if (x >= 0)
self.col = @intCast(x);
}
pub fn cursor_move_rel(self: *Plane, y: c_int, x: c_int) !void {
if (self.window.height == 0 or self.window.width == 0) return error.OutOfBounds;
const new_y: isize = @as(c_int, @intCast(self.row)) + y;
const new_x: isize = @as(c_int, @intCast(self.col)) + x;
if (new_y < 0 or new_x < 0) return error.OutOfBounds;
if (self.window.height <= new_y or self.window.width <= new_x) return error.OutOfBounds;
self.row = @intCast(new_y);
self.col = @intCast(new_x);
}
pub fn cell_init(self: Plane) Cell {
return .{ .cell = .{ .style = self.style } };
}
pub fn cell_load(self: *Plane, cell: *Cell, gcluster: []const u8) !usize {
var cols: c_int = 0;
const bytes = self.egc_length(gcluster, &cols, 0, 1);
cell.cell.char.grapheme = self.cache.put(gcluster[0..bytes]);
cell.cell.char.width = @intCast(cols);
return bytes;
}
pub fn at_cursor_cell(self: Plane, cell: *Cell) !usize {
cell.* = .{};
if (self.window.readCell(@intCast(self.col), @intCast(self.row))) |cell_| cell.cell = cell_;
return if (std.mem.eql(u8, cell.cell.char.grapheme, " ")) 0 else cell.cell.char.grapheme.len;
}
pub fn set_styles(self: *Plane, stylebits: StyleBits) void {
self.style.strikethrough = false;
self.style.bold = false;
self.style.ul_style = .off;
self.style.italic = false;
self.on_styles(stylebits);
}
pub fn on_styles(self: *Plane, stylebits: StyleBits) void {
if (stylebits.struck) self.style.strikethrough = true;
if (stylebits.bold) self.style.bold = true;
if (stylebits.undercurl) self.style.ul_style = .curly;
if (stylebits.underline) self.style.ul_style = .single;
if (stylebits.italic) self.style.italic = true;
}
pub fn off_styles(self: *Plane, stylebits: StyleBits) void {
if (stylebits.struck) self.style.strikethrough = false;
if (stylebits.bold) self.style.bold = false;
if (stylebits.undercurl) self.style.ul_style = .off;
if (stylebits.underline) self.style.ul_style = .off;
if (stylebits.italic) self.style.italic = false;
}
pub fn set_fg_rgb(self: *Plane, col: ThemeColor) !void {
self.style.fg = to_cell_color(col);
}
pub fn set_fg_rgb_alpha(self: *Plane, alpha_fg: ThemeColor, col: ThemeColor) !void {
self.style.fg = apply_alpha_theme(alpha_fg, col);
}
pub fn set_bg_rgb(self: *Plane, col: ThemeColor) !void {
self.style.bg = to_cell_color(col);
}
pub fn set_bg_rgb_alpha(self: *Plane, alpha_bg: ThemeColor, col: ThemeColor) !void {
self.style.bg = apply_alpha_theme(alpha_bg, col);
}
pub fn set_fg_palindex(self: *Plane, idx: c_uint) !void {
self.style.fg = .{ .index = @intCast(idx) };
}
pub fn set_bg_palindex(self: *Plane, idx: c_uint) !void {
self.style.bg = .{ .index = @intCast(idx) };
}
pub inline fn set_base_style(self: *Plane, style_: Style) void {
self.style_base.fg = if (style_.fg) |col| to_cell_color(col) else .default;
self.style_base.bg = if (style_.bg) |col| to_cell_color(col) else .default;
if (style_.fs) |fs| set_font_style(&self.style, fs);
self.set_style(style_);
}
pub fn set_base_style_transparent(self: *Plane, _: [*:0]const u8, style_: Style) void {
self.style_base.fg = if (style_.fg) |col| to_cell_color(col) else .default;
self.style_base.bg = if (style_.bg) |col| to_cell_color(col) else .default;
if (style_.fs) |fs| set_font_style(&self.style, fs);
self.set_style(style_);
self.transparent = true;
}
pub fn set_base_style_bg_transparent(self: *Plane, _: [*:0]const u8, style_: Style) void {
self.style_base.fg = if (style_.fg) |col| to_cell_color(col) else .default;
self.style_base.bg = if (style_.bg) |col| to_cell_color(col) else .default;
if (style_.fs) |fs| set_font_style(&self.style, fs);
self.set_style(style_);
self.transparent = true;
}
fn apply_alpha(base: vaxis.Cell.Color, col: ThemeColor) vaxis.Cell.Color {
const alpha = col.alpha;
return if (alpha == 0xFF or base != .rgb)
.{ .rgb = RGB.to_u8s(RGB.from_u24(col.color)) }
else
.{ .rgb = color.apply_alpha(RGB.from_u8s(base.rgb), RGB.from_u24(col.color), alpha).to_u8s() };
}
fn apply_alpha_theme(base: ThemeColor, col: ThemeColor) vaxis.Cell.Color {
const alpha = col.alpha;
return if (alpha == 0xFF)
.{ .rgb = RGB.to_u8s(RGB.from_u24(col.color)) }
else
.{ .rgb = color.apply_alpha(RGB.from_u24(base.color), RGB.from_u24(col.color), alpha).to_u8s() };
}
pub inline fn reverse_style(self: *Plane) void {
const swap = self.style.fg;
self.style.fg = self.style.bg;
self.style.bg = swap;
}
pub inline fn set_style(self: *Plane, style_: Style) void {
if (style_.fg) |col| self.style.fg = apply_alpha(self.style_base.bg, col);
if (style_.bg) |col| self.style.bg = apply_alpha(self.style_base.bg, col);
if (style_.fs) |fs| set_font_style(&self.style, fs);
self.transparent = false;
}
pub inline fn set_style_bg_transparent(self: *Plane, style_: Style) void {
if (style_.fg) |col| self.style.fg = apply_alpha(self.style_base.fg, col);
if (style_.bg) |col| self.style.bg = apply_alpha(self.style_base.bg, col);
if (style_.fs) |fs| set_font_style(&self.style, fs);
self.transparent = true;
}
inline fn set_font_style(style: *vaxis.Cell.Style, fs: FontStyle) void {
switch (fs) {
.normal => {
style.bold = false;
style.italic = false;
style.dim = false;
},
.bold => style.bold = true,
.italic => style.italic = true,
.underline => style.ul_style = .single,
.undercurl => style.ul_style = .curly,
.strikethrough => style.strikethrough = true,
}
}
inline fn is_control_code(c: u8) bool {
return switch (c) {
0...8, 10...31 => true,
else => false,
};
}
pub fn egc_length(self: *const Plane, egcs: []const u8, colcount: *c_int, abs_col: usize, tab_width: usize) usize {
if (egcs.len == 0) {
colcount.* = 0;
return 0;
}
if (is_control_code(egcs[0])) {
colcount.* = 1;
return 1;
}
if (egcs[0] == '\t') {
colcount.* = @intCast(tab_width - (abs_col % tab_width));
return 1;
}
var iter = self.window.screen.unicode.graphemeIterator(egcs);
const grapheme = iter.next() orelse {
colcount.* = 1;
return 1;
};
const s = grapheme.bytes(egcs);
const w = self.window.gwidth(s);
colcount.* = @intCast(w);
return s.len;
}
pub fn egc_chunk_width(self: *const Plane, chunk_: []const u8, abs_col_: usize, tab_width: usize) usize {
var abs_col = abs_col_;
var chunk = chunk_;
var colcount: usize = 0;
var cols: c_int = 0;
while (chunk.len > 0) {
const bytes = self.egc_length(chunk, &cols, abs_col, tab_width);
colcount += @intCast(cols);
abs_col += @intCast(cols);
if (chunk.len < bytes) break;
chunk = chunk[bytes..];
}
return colcount;
}
pub fn egc_last(self: *const Plane, egcs: []const u8) []const u8 {
var iter = self.window.screen.unicode.graphemeIterator(egcs);
var last: []const u8 = egcs[0..0];
while (iter.next()) |grapheme| last = grapheme.bytes(egcs);
return last;
}
pub fn metrics(self: *const Plane, tab_width: usize) Buffer.Metrics {
return .{
.ctx = self,
.egc_length = struct {
fn f(self_: Buffer.Metrics, egcs: []const u8, colcount: *c_int, abs_col: usize) usize {
const plane: *const Plane = @ptrCast(@alignCast(self_.ctx));
return plane.egc_length(egcs, colcount, abs_col, self_.tab_width);
}
}.f,
.egc_chunk_width = struct {
fn f(self_: Buffer.Metrics, chunk_: []const u8, abs_col_: usize) usize {
const plane: *const Plane = @ptrCast(@alignCast(self_.ctx));
return plane.egc_chunk_width(chunk_, abs_col_, self_.tab_width);
}
}.f,
.tab_width = tab_width,
.egc_last = struct {
fn f(self_: Buffer.Metrics, egcs: []const u8) []const u8 {
const plane: *const Plane = @ptrCast(@alignCast(self_.ctx));
return plane.egc_last(egcs);
}
}.f,
};
}
const GraphemeCache = struct {
buf: [1024 * 16]u8 = undefined,
idx: usize = 0,
pub fn put(self: *GraphemeCache, bytes: []const u8) []u8 {
if (self.idx + bytes.len > self.buf.len) self.idx = 0;
defer self.idx += bytes.len;
@memcpy(self.buf[self.idx .. self.idx + bytes.len], bytes);
return self.buf[self.idx .. self.idx + bytes.len];
}
};
fn to_cell_color(col: ThemeColor) vaxis.Cell.Color {
return .{ .rgb = RGB.to_u8s(RGB.from_u24(col.color)) };
}