diff --git a/build.zig b/build.zig index 88f8363..fecddf9 100644 --- a/build.zig +++ b/build.zig @@ -10,6 +10,10 @@ pub fn build(b: *std.Build) void { .optimize = optimize, .target = target, }); + const zg_dep = b.dependency("zg", .{ + .optimize = optimize, + .target = target, + }); const zigimg_dep = b.dependency("zigimg", .{ .optimize = optimize, .target = target, @@ -30,6 +34,7 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); vaxis_mod.addImport("ziglyph", ziglyph_dep.module("ziglyph")); + vaxis_mod.addImport("grapheme", zg_dep.module("grapheme")); vaxis_mod.addImport("zigimg", zigimg_dep.module("zigimg")); vaxis_mod.addImport("gap_buffer", gap_buffer_dep.module("gap_buffer")); vaxis_mod.addImport("znvim", znvim_dep.module("znvim")); @@ -67,6 +72,7 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); tests.root_module.addImport("ziglyph", ziglyph_dep.module("ziglyph")); + tests.root_module.addImport("grapheme", zg_dep.module("grapheme")); tests.root_module.addImport("zigimg", zigimg_dep.module("zigimg")); tests.root_module.addImport("gap_buffer", gap_buffer_dep.module("gap_buffer")); tests.root_module.addImport("znvim", znvim_dep.module("znvim")); diff --git a/build.zig.zon b/build.zig.zon index 54942f4..4e8b7b1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -20,5 +20,9 @@ .url = "git+https://github.com/jinzhongjia/znvim#7927b8042872d5fa5f30862302bea1290d372d4f", .hash = "12202372c2043a9ac557144d327c09638ccd8d615bba459ba17d1a7a4197a213d939", }, + .zg = .{ + .url = "git+https://codeberg.org/dude_the_builder/zg#16735685fcc3410de361ba3411788ad1fb4fe188", + .hash = "1220fe9ac5cdb41833d327a78745614e67d472469f8666567bd8cf9f5847a52b1c51", + }, }, } diff --git a/examples/text_input.zig b/examples/text_input.zig index 54722cc..6b33dba 100644 --- a/examples/text_input.zig +++ b/examples/text_input.zig @@ -30,7 +30,7 @@ pub fn main() !void { const alloc = gpa.allocator(); // Initialize Vaxis - var vx = try vaxis.init(.{}); + var vx = try vaxis.init(alloc, .{}); // deinit takes an optional allocator. If your program is exiting, you can // choose to pass a null allocator to save some exit time. defer vx.deinit(alloc); @@ -53,7 +53,7 @@ pub fn main() !void { // init our text input widget. The text input widget needs an allocator to // store the contents of the input - var text_input = TextInput.init(alloc); + var text_input = TextInput.init(alloc, &vx.unicode); defer text_input.deinit(); // Sends queries to terminal to detect certain features. This should diff --git a/src/Screen.zig b/src/Screen.zig index ceea4bb..a4e6526 100644 --- a/src/Screen.zig +++ b/src/Screen.zig @@ -5,6 +5,8 @@ const Cell = @import("Cell.zig"); const Shape = @import("Mouse.zig").Shape; const Image = @import("Image.zig"); const Winsize = @import("Tty.zig").Winsize; +const Unicode = @import("Unicode.zig"); +const Method = @import("gwidth.zig").Method; const log = std.log.scoped(.screen); @@ -22,13 +24,14 @@ cursor_row: usize = 0, cursor_col: usize = 0, cursor_vis: bool = false, -/// true when we measure cells with unicode -unicode: bool = false, +unicode: *const Unicode = undefined, + +width_method: Method = .wcwidth, mouse_shape: Shape = .default, cursor_shape: Cell.CursorShape = .default, -pub fn init(alloc: std.mem.Allocator, winsize: Winsize) !Screen { +pub fn init(alloc: std.mem.Allocator, winsize: Winsize, unicode: *const Unicode) !Screen { const w = winsize.cols; const h = winsize.rows; var self = Screen{ @@ -37,6 +40,7 @@ pub fn init(alloc: std.mem.Allocator, winsize: Winsize) !Screen { .height = h, .width_pix = winsize.x_pixel, .height_pix = winsize.y_pixel, + .unicode = unicode, }; for (self.buf, 0..) |_, i| { self.buf[i] = .{}; diff --git a/src/Tty.zig b/src/Tty.zig index d8684c4..360c7f5 100644 --- a/src/Tty.zig +++ b/src/Tty.zig @@ -175,8 +175,8 @@ pub fn run( }, .cap_unicode => { log.info("unicode capability detected", .{}); - loop.vaxis.caps.unicode = true; - loop.vaxis.screen.unicode = true; + loop.vaxis.caps.unicode = .unicode; + loop.vaxis.screen.width_method = .unicode; }, .cap_da1 => { std.Thread.Futex.wake(&loop.vaxis.query_futex, 10); diff --git a/src/Unicode.zig b/src/Unicode.zig new file mode 100644 index 0000000..8b3ea19 --- /dev/null +++ b/src/Unicode.zig @@ -0,0 +1,26 @@ +const std = @import("std"); +const grapheme = @import("grapheme"); + +/// A thin wrapper around zg data +const Unicode = @This(); + +grapheme_data: grapheme.GraphemeData, + +/// initialize all unicode data vaxis may possibly need +pub fn init(alloc: std.mem.Allocator) !Unicode { + const grapheme_data = try grapheme.GraphemeData.init(alloc); + + return .{ + .grapheme_data = grapheme_data, + }; +} + +/// free all data +pub fn deinit(self: *Unicode) void { + self.grapheme_data.deinit(); +} + +/// creates a grapheme iterator based on str +pub fn graphemeIterator(self: *const Unicode, str: []const u8) grapheme.Iterator { + return grapheme.Iterator.init(str, &self.grapheme_data); +} diff --git a/src/Vaxis.zig b/src/Vaxis.zig index c88dca2..bd7216e 100644 --- a/src/Vaxis.zig +++ b/src/Vaxis.zig @@ -29,7 +29,7 @@ pub const Capabilities = struct { kitty_keyboard: bool = false, kitty_graphics: bool = false, rgb: bool = false, - unicode: bool = false, + unicode: gwidth.Method = .wcwidth, }; pub const Options = struct {}; @@ -63,18 +63,21 @@ query_futex: atomic.Value(u32) = atomic.Value(u32).init(0), // images next_img_id: u32 = 1, +unicode: Unicode, + // statistics renders: usize = 0, render_dur: i128 = 0, render_timer: std.time.Timer, /// Initialize Vaxis with runtime options -pub fn init(_: Options) !Vaxis { +pub fn init(alloc: std.mem.Allocator, _: Options) !Vaxis { return .{ .tty = null, .screen = .{}, .screen_last = .{}, .render_timer = try std.time.Timer.start(), + .unicode = try Unicode.init(alloc), }; } @@ -111,6 +114,7 @@ pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator) void { log.debug("total renders = {d}", .{self.renders}); log.debug("microseconds per render = {d}", .{tpr}); } + self.unicode.deinit(); } /// resize allocates a slice of cells equal to the number of cells @@ -119,8 +123,8 @@ pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator) void { pub fn resize(self: *Vaxis, alloc: std.mem.Allocator, winsize: Winsize) !void { log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows }); self.screen.deinit(alloc); - self.screen = try Screen.init(alloc, winsize); - self.screen.unicode = self.caps.unicode; + self.screen = try Screen.init(alloc, winsize, &self.unicode); + self.screen.width_method = self.caps.unicode; // try self.screen.int(alloc, winsize.cols, winsize.rows); // we only init our current screen. This has the effect of redrawing // every cell @@ -201,7 +205,7 @@ pub fn queryTerminal(self: *Vaxis) !void { if (self.caps.kitty_keyboard) { try self.enableKittyKeyboard(.{}); } - if (self.caps.unicode) { + if (self.caps.unicode == .unicode) { _ = try tty.write(ctlseqs.unicode_set); } } @@ -256,7 +260,7 @@ pub fn render(self: *Vaxis) !void { const w = blk: { if (cell.char.width != 0) break :blk cell.char.width; - const method: gwidth.Method = if (self.caps.unicode) .unicode else .wcwidth; + const method: gwidth.Method = self.caps.unicode; const width = gwidth.gwidth(cell.char.grapheme, method) catch 1; break :blk @max(1, width); }; diff --git a/src/Window.zig b/src/Window.zig index 07103a3..c4d377e 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -1,6 +1,4 @@ const std = @import("std"); -const ziglyph = @import("ziglyph"); -const GraphemeIterator = ziglyph.GraphemeIterator; const Screen = @import("Screen.zig"); const Cell = @import("Cell.zig"); @@ -202,8 +200,7 @@ pub fn clear(self: Window) void { /// 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; + return gw.gwidth(str, self.screen.width_method) catch 1; } /// fills the window with the provided cell @@ -270,7 +267,7 @@ pub fn print(self: Window, segments: []Segment, opts: PrintOptions) !PrintResult .grapheme => { var col: usize = 0; const overflow: bool = blk: for (segments) |segment| { - var iter = GraphemeIterator.init(segment.text); + var iter = self.screen.unicode.graphemeIterator(segment.text); while (iter.next()) |grapheme| { if (row >= self.height) break :blk true; const s = grapheme.slice(segment.text); @@ -353,7 +350,7 @@ pub fn print(self: Window, segments: []Segment, opts: PrintOptions) !PrintResult else word; defer soft_wrapped = false; - var iter = GraphemeIterator.init(printed_word); + var iter = self.screen.unicode.graphemeIterator(segment.text); while (iter.next()) |grapheme| { const s = grapheme.slice(printed_word); const w = self.gwidth(s); @@ -405,7 +402,7 @@ pub fn print(self: Window, segments: []Segment, opts: PrintOptions) !PrintResult .none => { var col: usize = 0; const overflow: bool = blk: for (segments) |segment| { - var iter = GraphemeIterator.init(segment.text); + var iter = self.screen.unicode.graphemeIterator(segment.text); while (iter.next()) |grapheme| { if (col >= self.width) break :blk true; const s = grapheme.slice(segment.text); diff --git a/src/main.zig b/src/main.zig index 5887f27..72d7fc7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -22,8 +22,8 @@ pub const widgets = @import("widgets.zig"); pub const gwidth = @import("gwidth.zig"); /// Initialize a Vaxis application. -pub fn init(opts: Vaxis.Options) !Vaxis { - return Vaxis.init(opts); +pub fn init(alloc: std.mem.Allocator, opts: Vaxis.Options) !Vaxis { + return Vaxis.init(alloc, opts); } test { diff --git a/src/widgets/TextInput.zig b/src/widgets/TextInput.zig index 55cdeee..04fec4f 100644 --- a/src/widgets/TextInput.zig +++ b/src/widgets/TextInput.zig @@ -3,8 +3,8 @@ const assert = std.debug.assert; const Key = @import("../Key.zig"); const Cell = @import("../Cell.zig"); const Window = @import("../Window.zig"); -const GraphemeIterator = @import("ziglyph").GraphemeIterator; const GapBuffer = @import("gap_buffer").GapBuffer; +const Unicode = @import("../Unicode.zig"); const log = std.log.scoped(.text_input); @@ -31,9 +31,12 @@ prev_cursor_idx: usize = 0, /// approximate distance from an edge before we scroll scroll_offset: usize = 4, -pub fn init(alloc: std.mem.Allocator) TextInput { +unicode: *const Unicode, + +pub fn init(alloc: std.mem.Allocator, unicode: *const Unicode) TextInput { return TextInput{ .buf = GapBuffer(u8).init(alloc), + .unicode = unicode, }; } @@ -73,7 +76,7 @@ pub fn update(self: *TextInput, event: Event) !void { /// insert text at the cursor position pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) !void { - var iter = GraphemeIterator.init(data); + var iter = self.unicode.graphemeIterator(data); var byte_offset_to_cursor = self.byteOffsetToCursor(); while (iter.next()) |text| { try self.buf.insertSliceBefore(byte_offset_to_cursor, text.slice(data)); @@ -101,7 +104,7 @@ pub fn sliceToCursor(self: *TextInput, buf: []u8) []const u8 { /// calculates the display width from the draw_offset to the cursor fn widthToCursor(self: *TextInput, win: Window) usize { var width: usize = 0; - var first_iter = GraphemeIterator.init(self.buf.items); + var first_iter = self.unicode.graphemeIterator(self.buf.items); var i: usize = 0; while (first_iter.next()) |grapheme| { defer i += 1; @@ -109,18 +112,18 @@ fn widthToCursor(self: *TextInput, win: Window) usize { continue; } if (i == self.cursor_idx) return width; - const g = grapheme.slice(self.buf.items); + const g = grapheme.bytes(self.buf.items); width += win.gwidth(g); } const second_half = self.buf.secondHalf(); - var second_iter = GraphemeIterator.init(second_half); + var second_iter = self.unicode.graphemeIterator(second_half); while (second_iter.next()) |grapheme| { defer i += 1; if (i < self.draw_offset) { continue; } if (i == self.cursor_idx) return width; - const g = grapheme.slice(second_half); + const g = grapheme.bytes(second_half); width += win.gwidth(g); } return width; @@ -141,7 +144,7 @@ pub fn draw(self: *TextInput, win: Window) void { // assumption!! the gap is never within a grapheme // one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay. - var first_iter = GraphemeIterator.init(self.buf.items); + var first_iter = self.unicode.graphemeIterator(self.buf.items); var col: usize = 0; var i: usize = 0; while (first_iter.next()) |grapheme| { @@ -149,7 +152,7 @@ pub fn draw(self: *TextInput, win: Window) void { i += 1; continue; } - const g = grapheme.slice(self.buf.items); + const g = grapheme.bytes(self.buf.items); const w = win.gwidth(g); if (col + w >= win.width) { win.writeCell(win.width - 1, 0, .{ .char = ellipsis }); @@ -166,13 +169,13 @@ pub fn draw(self: *TextInput, win: Window) void { if (i == self.cursor_idx) self.prev_cursor_col = col; } const second_half = self.buf.secondHalf(); - var second_iter = GraphemeIterator.init(second_half); + var second_iter = self.unicode.graphemeIterator(second_half); while (second_iter.next()) |grapheme| { if (i < self.draw_offset) { i += 1; continue; } - const g = grapheme.slice(second_half); + const g = grapheme.bytes(second_half); const w = win.gwidth(g); if (col + w > win.width) { win.writeCell(win.width - 1, 0, .{ .char = ellipsis }); @@ -223,7 +226,7 @@ fn reset(self: *TextInput) void { pub fn byteOffsetToCursor(self: TextInput) usize { // assumption! the gap is never in the middle of a grapheme // one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay. - var iter = GraphemeIterator.init(self.buf.items); + var iter = self.unicode.graphemeIterator(self.buf.items); var offset: usize = 0; var i: usize = 0; while (iter.next()) |grapheme| { @@ -231,7 +234,7 @@ pub fn byteOffsetToCursor(self: TextInput) usize { offset += grapheme.len; i += 1; } else { - var second_iter = GraphemeIterator.init(self.buf.secondHalf()); + var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf()); while (second_iter.next()) |grapheme| { if (i == self.cursor_idx) break; offset += grapheme.len; @@ -257,7 +260,7 @@ fn deleteToStart(self: *TextInput) !void { fn deleteBeforeCursor(self: *TextInput) !void { // assumption! the gap is never in the middle of a grapheme // one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay. - var iter = GraphemeIterator.init(self.buf.items); + var iter = self.unicode.graphemeIterator(self.buf.items); var offset: usize = 0; var i: usize = 1; while (iter.next()) |grapheme| { @@ -270,7 +273,7 @@ fn deleteBeforeCursor(self: *TextInput) !void { offset += grapheme.len; i += 1; } else { - var second_iter = GraphemeIterator.init(self.buf.secondHalf()); + var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf()); while (second_iter.next()) |grapheme| { if (i == self.cursor_idx) { try self.buf.replaceRangeBefore(offset, grapheme.len, &.{}); @@ -287,7 +290,7 @@ fn deleteBeforeCursor(self: *TextInput) !void { fn deleteAtCursor(self: *TextInput) !void { // assumption! the gap is never in the middle of a grapheme // one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay. - var iter = GraphemeIterator.init(self.buf.items); + var iter = self.unicode.graphemeIterator(self.buf.items); var offset: usize = 0; var i: usize = 1; while (iter.next()) |grapheme| { @@ -299,7 +302,7 @@ fn deleteAtCursor(self: *TextInput) !void { offset += grapheme.len; i += 1; } else { - var second_iter = GraphemeIterator.init(self.buf.secondHalf()); + var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf()); while (second_iter.next()) |grapheme| { if (i == self.cursor_idx + 1) { try self.buf.replaceRangeAfter(offset, grapheme.len, &.{});