a modern tui library written in zig

Compare changes

Choose any two refs to compare.

+62
bench/bench.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("vaxis"); 3 + 4 + fn parseIterations(allocator: std.mem.Allocator) !usize { 5 + var args = try std.process.argsWithAllocator(allocator); 6 + defer args.deinit(); 7 + _ = args.next(); 8 + if (args.next()) |val| { 9 + return std.fmt.parseUnsigned(usize, val, 10); 10 + } 11 + return 200; 12 + } 13 + 14 + fn printResults(writer: anytype, label: []const u8, iterations: usize, elapsed_ns: u64, total_bytes: usize) !void { 15 + const ns_per_frame = elapsed_ns / @as(u64, @intCast(iterations)); 16 + const bytes_per_frame = total_bytes / iterations; 17 + try writer.print( 18 + "{s}: frames={d} total_ns={d} ns/frame={d} bytes={d} bytes/frame={d}\n", 19 + .{ label, iterations, elapsed_ns, ns_per_frame, total_bytes, bytes_per_frame }, 20 + ); 21 + } 22 + 23 + pub fn main() !void { 24 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 25 + defer _ = gpa.deinit(); 26 + const allocator = gpa.allocator(); 27 + 28 + const iterations = try parseIterations(allocator); 29 + 30 + var vx = try vaxis.init(allocator, .{}); 31 + var init_writer = std.io.Writer.Allocating.init(allocator); 32 + defer init_writer.deinit(); 33 + defer vx.deinit(allocator, &init_writer.writer); 34 + 35 + const winsize = vaxis.Winsize{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 }; 36 + try vx.resize(allocator, &init_writer.writer, winsize); 37 + 38 + const stdout = std.fs.File.stdout().deprecatedWriter(); 39 + 40 + var idle_writer = std.io.Writer.Allocating.init(allocator); 41 + defer idle_writer.deinit(); 42 + var timer = try std.time.Timer.start(); 43 + var i: usize = 0; 44 + while (i < iterations) : (i += 1) { 45 + try vx.render(&idle_writer.writer); 46 + } 47 + const idle_ns = timer.read(); 48 + const idle_bytes: usize = idle_writer.writer.end; 49 + try printResults(stdout, "idle", iterations, idle_ns, idle_bytes); 50 + 51 + var dirty_writer = std.io.Writer.Allocating.init(allocator); 52 + defer dirty_writer.deinit(); 53 + timer.reset(); 54 + i = 0; 55 + while (i < iterations) : (i += 1) { 56 + vx.queueRefresh(); 57 + try vx.render(&dirty_writer.writer); 58 + } 59 + const dirty_ns = timer.read(); 60 + const dirty_bytes: usize = dirty_writer.writer.end; 61 + try printResults(stdout, "dirty", iterations, dirty_ns, dirty_bytes); 62 + }
+21
build.zig
··· 41 41 split_view, 42 42 table, 43 43 text_input, 44 + text_view, 45 + list_view, 44 46 vaxis, 45 47 view, 46 48 vt, ··· 63 65 64 66 const example_run = b.addRunArtifact(example); 65 67 example_step.dependOn(&example_run.step); 68 + 69 + // Benchmarks 70 + const bench_step = b.step("bench", "Run benchmarks"); 71 + const bench = b.addExecutable(.{ 72 + .name = "bench", 73 + .root_module = b.createModule(.{ 74 + .root_source_file = b.path("bench/bench.zig"), 75 + .target = target, 76 + .optimize = optimize, 77 + .imports = &.{ 78 + .{ .name = "vaxis", .module = vaxis_mod }, 79 + }, 80 + }), 81 + }); 82 + const bench_run = b.addRunArtifact(bench); 83 + if (b.args) |args| { 84 + bench_run.addArgs(args); 85 + } 86 + bench_step.dependOn(&bench_run.step); 66 87 67 88 // Tests 68 89 const tests_step = b.step("test", "Run tests");
+2 -3
build.zig.zon
··· 5 5 .minimum_zig_version = "0.15.1", 6 6 .dependencies = .{ 7 7 .zigimg = .{ 8 - // TODO .url = "git+https://github.com/zigimg/zigimg", 9 - .url = "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz", 10 - .hash = "zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms", 8 + .url = "git+https://github.com/zigimg/zigimg#eab2522c023b9259db8b13f2f90d609b7437e5f6", 9 + .hash = "zigimg-0.1.0-8_eo2vUZFgAAtN1c6dAO5DdqL0d4cEWHtn6iR5ucZJti", 11 10 }, 12 11 .uucode = .{ 13 12 .url = "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732",
+1 -1
examples/image.zig
··· 36 36 37 37 var read_buffer: [1024 * 1024]u8 = undefined; // 1MB buffer 38 38 var img1 = try vaxis.zigimg.Image.fromFilePath(alloc, "examples/zig.png", &read_buffer); 39 - defer img1.deinit(); 39 + defer img1.deinit(alloc); 40 40 41 41 const imgs = [_]vaxis.Image{ 42 42 try vx.transmitImage(alloc, tty.writer(), &img1, .rgba),
+99
examples/list_view.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("vaxis"); 3 + const vxfw = vaxis.vxfw; 4 + 5 + const Text = vxfw.Text; 6 + const ListView = vxfw.ListView; 7 + const Widget = vxfw.Widget; 8 + 9 + const Model = struct { 10 + list_view: ListView, 11 + 12 + pub fn widget(self: *Model) Widget { 13 + return .{ 14 + .userdata = self, 15 + .eventHandler = Model.typeErasedEventHandler, 16 + .drawFn = Model.typeErasedDrawFn, 17 + }; 18 + } 19 + 20 + pub fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 21 + const self: *Model = @ptrCast(@alignCast(ptr)); 22 + try ctx.requestFocus(self.list_view.widget()); 23 + switch (event) { 24 + .key_press => |key| { 25 + if (key.matches('q', .{}) or key.matchExact('c', .{ .ctrl = true })) { 26 + ctx.quit = true; 27 + return; 28 + } 29 + }, 30 + else => {}, 31 + } 32 + } 33 + 34 + fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface { 35 + const self: *Model = @ptrCast(@alignCast(ptr)); 36 + const max = ctx.max.size(); 37 + 38 + const list_view: vxfw.SubSurface = .{ 39 + .origin = .{ .row = 1, .col = 1 }, 40 + .surface = try self.list_view.draw(ctx), 41 + }; 42 + 43 + const children = try ctx.arena.alloc(vxfw.SubSurface, 1); 44 + children[0] = list_view; 45 + 46 + return .{ 47 + .size = max, 48 + .widget = self.widget(), 49 + .buffer = &.{}, 50 + .children = children, 51 + }; 52 + } 53 + }; 54 + 55 + pub fn main() !void { 56 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 57 + defer _ = gpa.deinit(); 58 + 59 + const allocator = gpa.allocator(); 60 + 61 + var app = try vxfw.App.init(allocator); 62 + defer app.deinit(); 63 + 64 + const model = try allocator.create(Model); 65 + defer allocator.destroy(model); 66 + 67 + const n = 80; 68 + var texts = try std.ArrayList(Widget).initCapacity(allocator, n); 69 + 70 + var allocs = try std.ArrayList(*Text).initCapacity(allocator, n); 71 + defer { 72 + for (allocs.items) |tw| { 73 + allocator.free(tw.text); 74 + allocator.destroy(tw); 75 + } 76 + allocs.deinit(allocator); 77 + texts.deinit(allocator); 78 + } 79 + 80 + for (0..n) |i| { 81 + const t = std.fmt.allocPrint(allocator, "List Item {d}", .{i}) catch "placeholder"; 82 + const tw = try allocator.create(Text); 83 + tw.* = .{ .text = t }; 84 + _ = try allocs.append(allocator, tw); 85 + _ = try texts.append(allocator, tw.widget()); 86 + } 87 + 88 + model.* = .{ 89 + .list_view = .{ 90 + .wheel_scroll = 3, 91 + .scroll = .{ 92 + .wants_cursor = true, 93 + }, 94 + .children = .{ .slice = texts.items }, 95 + }, 96 + }; 97 + 98 + try app.run(model.widget(), .{}); 99 + }
+66
examples/text_view.zig
··· 1 + const std = @import("std"); 2 + const log = std.log.scoped(.main); 3 + const vaxis = @import("vaxis"); 4 + 5 + const TextView = vaxis.widgets.TextView; 6 + 7 + const Event = union(enum) { 8 + key_press: vaxis.Key, 9 + winsize: vaxis.Winsize, 10 + }; 11 + 12 + pub fn main() !void { 13 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 14 + 15 + defer { 16 + const deinit_status = gpa.deinit(); 17 + if (deinit_status == .leak) { 18 + log.err("memory leak", .{}); 19 + } 20 + } 21 + 22 + const alloc = gpa.allocator(); 23 + var buffer: [1024]u8 = undefined; 24 + var tty = try vaxis.Tty.init(&buffer); 25 + defer tty.deinit(); 26 + var vx = try vaxis.init(alloc, .{}); 27 + defer vx.deinit(alloc, tty.writer()); 28 + var loop: vaxis.Loop(Event) = .{ 29 + .vaxis = &vx, 30 + .tty = &tty, 31 + }; 32 + try loop.init(); 33 + try loop.start(); 34 + defer loop.stop(); 35 + try vx.enterAltScreen(tty.writer()); 36 + try vx.queryTerminal(tty.writer(), 20 * std.time.ns_per_s); 37 + var text_view = TextView{}; 38 + var text_view_buffer = TextView.Buffer{}; 39 + defer text_view_buffer.deinit(alloc); 40 + try text_view_buffer.append(alloc, .{ .bytes = "Press Enter to add a line, Up/Down to scroll, 'c' to close." }); 41 + 42 + var counter: i32 = 0; 43 + var lineBuf: [128]u8 = undefined; 44 + 45 + while (true) { 46 + const event = loop.nextEvent(); 47 + switch (event) { 48 + .key_press => |key| { 49 + // Close demo 50 + if (key.matches('c', .{})) break; 51 + if (key.matches(vaxis.Key.enter, .{})) { 52 + counter += 1; 53 + const new_content = try std.fmt.bufPrint(&lineBuf, "\nLine {d}", .{counter}); 54 + try text_view_buffer.append(alloc, .{ .bytes = new_content }); 55 + } 56 + text_view.input(key); 57 + }, 58 + .winsize => |ws| try vx.resize(alloc, tty.writer(), ws), 59 + } 60 + const win = vx.window(); 61 + win.clear(); 62 + text_view.draw(win, text_view_buffer); 63 + try vx.render(tty.writer()); 64 + try tty.writer.flush(); 65 + } 66 + }
+18 -4
src/InternalScreen.zig
··· 83 83 row: u16, 84 84 cell: Cell, 85 85 ) void { 86 - if (self.width < col) { 86 + if (self.width <= col) { 87 87 // column out of bounds 88 88 return; 89 89 } 90 - if (self.height < row) { 90 + if (self.height <= row) { 91 91 // height out of bounds 92 92 return; 93 93 } ··· 110 110 } 111 111 112 112 pub fn readCell(self: *InternalScreen, col: u16, row: u16) ?Cell { 113 - if (self.width < col) { 113 + if (self.width <= col) { 114 114 // column out of bounds 115 115 return null; 116 116 } 117 - if (self.height < row) { 117 + if (self.height <= row) { 118 118 // height out of bounds 119 119 return null; 120 120 } ··· 131 131 .default = cell.default, 132 132 }; 133 133 } 134 + 135 + test "InternalScreen: out-of-bounds read/write are ignored" { 136 + var screen = try InternalScreen.init(std.testing.allocator, 2, 2); 137 + defer screen.deinit(std.testing.allocator); 138 + 139 + const sentinel: Cell = .{ .char = .{ .grapheme = "A", .width = 1 } }; 140 + screen.writeCell(0, 1, sentinel); 141 + 142 + const oob_cell: Cell = .{ .char = .{ .grapheme = "X", .width = 1 } }; 143 + screen.writeCell(2, 0, oob_cell); 144 + const read_back = screen.readCell(0, 1) orelse return error.TestUnexpectedResult; 145 + try std.testing.expect(std.mem.eql(u8, read_back.char.grapheme, "A")); 146 + try std.testing.expect(screen.readCell(2, 0) == null); 147 + }
+78 -35
src/Vaxis.zig
··· 360 360 assert(self.screen.buf.len == @as(usize, @intCast(self.screen.width)) * self.screen.height); // correct size 361 361 assert(self.screen.buf.len == self.screen_last.buf.len); // same size 362 362 363 - // Set up sync before we write anything 364 - // TODO: optimize sync so we only sync _when we have changes_. This 365 - // requires a smarter buffered writer, we'll probably have to write 366 - // our own 367 - try tty.writeAll(ctlseqs.sync_set); 368 - errdefer tty.writeAll(ctlseqs.sync_reset) catch {}; 363 + var started: bool = false; 364 + var sync_active: bool = false; 365 + errdefer if (sync_active) tty.writeAll(ctlseqs.sync_reset) catch {}; 369 366 370 - // Send the cursor to 0,0 371 - // TODO: this needs to move after we optimize writes. We only do 372 - // this if we have an update to make. We also need to hide cursor 373 - // and then reshow it if needed 374 - try tty.writeAll(ctlseqs.hide_cursor); 375 - if (self.state.alt_screen) 376 - try tty.writeAll(ctlseqs.home) 377 - else { 378 - try tty.writeByte('\r'); 379 - for (0..self.state.cursor.row) |_| { 380 - try tty.writeAll(ctlseqs.ri); 381 - } 382 - } 383 - try tty.writeAll(ctlseqs.sgr_reset); 367 + const cursor_vis_changed = self.screen.cursor_vis != self.screen_last.cursor_vis; 368 + const cursor_shape_changed = self.screen.cursor_shape != self.screen_last.cursor_shape; 369 + const mouse_shape_changed = self.screen.mouse_shape != self.screen_last.mouse_shape; 370 + const cursor_pos_changed = self.screen.cursor_vis and 371 + (self.screen.cursor_row != self.state.cursor.row or 372 + self.screen.cursor_col != self.state.cursor.col); 373 + const needs_render = self.refresh or cursor_vis_changed or cursor_shape_changed or mouse_shape_changed or cursor_pos_changed; 384 374 385 375 // initialize some variables 386 376 var reposition: bool = false; ··· 388 378 var col: u16 = 0; 389 379 var cursor: Style = .{}; 390 380 var link: Hyperlink = .{}; 391 - var cursor_pos: struct { 381 + const CursorPos = struct { 392 382 row: u16 = 0, 393 383 col: u16 = 0, 394 - } = .{}; 384 + }; 385 + var cursor_pos: CursorPos = .{}; 395 386 396 - // Clear all images 397 - if (self.caps.kitty_graphics) 398 - try tty.writeAll(ctlseqs.kitty_graphics_clear); 387 + const startRender = struct { 388 + fn run( 389 + vx: *Vaxis, 390 + io: *IoWriter, 391 + cursor_pos_ptr: *CursorPos, 392 + reposition_ptr: *bool, 393 + started_ptr: *bool, 394 + sync_active_ptr: *bool, 395 + ) !void { 396 + if (started_ptr.*) return; 397 + started_ptr.* = true; 398 + sync_active_ptr.* = true; 399 + // Set up sync before we write anything 400 + try io.writeAll(ctlseqs.sync_set); 401 + // Send the cursor to 0,0 402 + try io.writeAll(ctlseqs.hide_cursor); 403 + if (vx.state.alt_screen) 404 + try io.writeAll(ctlseqs.home) 405 + else { 406 + try io.writeByte('\r'); 407 + for (0..vx.state.cursor.row) |_| { 408 + try io.writeAll(ctlseqs.ri); 409 + } 410 + } 411 + try io.writeAll(ctlseqs.sgr_reset); 412 + cursor_pos_ptr.* = .{}; 413 + reposition_ptr.* = true; 414 + // Clear all images 415 + if (vx.caps.kitty_graphics) 416 + try io.writeAll(ctlseqs.kitty_graphics_clear); 417 + } 418 + }; 399 419 400 420 // Reset skip flag on all last_screen cells 401 421 for (self.screen_last.buf) |*last_cell| { 402 422 last_cell.skip = false; 423 + } 424 + 425 + if (needs_render) { 426 + try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active); 403 427 } 404 428 405 429 var i: usize = 0; ··· 446 470 try tty.writeAll(ctlseqs.osc8_clear); 447 471 } 448 472 continue; 473 + } 474 + if (!started) { 475 + try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active); 449 476 } 450 477 self.screen_last.buf[i].skipped = false; 451 478 defer { ··· 730 757 cursor_pos.col = col + w; 731 758 cursor_pos.row = row; 732 759 } 760 + if (!started) return; 733 761 if (self.screen.cursor_vis) { 734 762 if (self.state.alt_screen) { 735 763 try tty.print( ··· 761 789 self.state.cursor.row = cursor_pos.row; 762 790 self.state.cursor.col = cursor_pos.col; 763 791 } 792 + self.screen_last.cursor_vis = self.screen.cursor_vis; 764 793 if (self.screen.mouse_shape != self.screen_last.mouse_shape) { 765 794 try tty.print( 766 795 ctlseqs.osc22_mouse_shape, ··· 851 880 const ypos = mouse.row; 852 881 const xextra = self.screen.width_pix % self.screen.width; 853 882 const yextra = self.screen.height_pix % self.screen.height; 854 - const xcell = (self.screen.width_pix - xextra) / self.screen.width; 855 - const ycell = (self.screen.height_pix - yextra) / self.screen.height; 883 + const xcell: i16 = @intCast((self.screen.width_pix - xextra) / self.screen.width); 884 + const ycell: i16 = @intCast((self.screen.height_pix - yextra) / self.screen.height); 856 885 if (xcell == 0 or ycell == 0) return mouse; 857 - result.col = xpos / xcell; 858 - result.row = ypos / ycell; 859 - result.xoffset = xpos % xcell; 860 - result.yoffset = ypos % ycell; 886 + result.col = @divFloor(xpos, xcell); 887 + result.row = @divFloor(ypos, ycell); 888 + result.xoffset = @intCast(@mod(xpos, xcell)); 889 + result.yoffset = @intCast(@mod(ypos, ycell)); 861 890 } 862 891 return result; 863 892 } ··· 995 1024 const buf = switch (format) { 996 1025 .png => png: { 997 1026 const png_buf = try arena.allocator().alloc(u8, img.imageByteSize()); 998 - const png = try img.writeToMemory(png_buf, .{ .png = .{} }); 1027 + const png = try img.writeToMemory(arena.allocator(), png_buf, .{ .png = .{} }); 999 1028 break :png png; 1000 1029 }, 1001 1030 .rgb => rgb: { 1002 - try img.convert(.rgb24); 1031 + try img.convert(arena.allocator(), .rgb24); 1003 1032 break :rgb img.rawBytes(); 1004 1033 }, 1005 1034 .rgba => rgba: { 1006 - try img.convert(.rgba32); 1035 + try img.convert(arena.allocator(), .rgba32); 1007 1036 break :rgba img.rawBytes(); 1008 1037 }, 1009 1038 }; ··· 1027 1056 .path => |path| try zigimg.Image.fromFilePath(alloc, path, &read_buffer), 1028 1057 .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes), 1029 1058 }; 1030 - defer img.deinit(); 1059 + defer img.deinit(alloc); 1031 1060 return self.transmitImage(alloc, tty, &img, .png); 1032 1061 } 1033 1062 ··· 1409 1438 try tty.print(ctlseqs.osc7, .{uri.fmt(.{ .scheme = true, .authority = true, .path = true })}); 1410 1439 try tty.flush(); 1411 1440 } 1441 + 1442 + test "render: no output when no changes" { 1443 + var vx = try Vaxis.init(std.testing.allocator, .{}); 1444 + var deinit_writer = std.io.Writer.Allocating.init(std.testing.allocator); 1445 + defer deinit_writer.deinit(); 1446 + defer vx.deinit(std.testing.allocator, &deinit_writer.writer); 1447 + 1448 + var render_writer = std.io.Writer.Allocating.init(std.testing.allocator); 1449 + defer render_writer.deinit(); 1450 + try vx.render(&render_writer.writer); 1451 + const output = try render_writer.toOwnedSlice(); 1452 + defer std.testing.allocator.free(output); 1453 + try std.testing.expectEqual(@as(usize, 0), output.len); 1454 + }
+26 -28
src/queue.zig
··· 30 30 self.not_empty.wait(&self.mutex); 31 31 } 32 32 std.debug.assert(!self.isEmptyLH()); 33 - if (self.isFullLH()) { 34 - // If we are full, wake up a push that might be 35 - // waiting here. 36 - self.not_full.signal(); 37 - } 38 - 39 - return self.popLH(); 33 + return self.popAndSignalLH(); 40 34 } 41 35 42 36 /// Push an item into the queue. Blocks until an item has been ··· 48 42 self.not_full.wait(&self.mutex); 49 43 } 50 44 std.debug.assert(!self.isFullLH()); 51 - const was_empty = self.isEmptyLH(); 52 - 53 - self.buf[self.mask(self.write_index)] = item; 54 - self.write_index = self.mask2(self.write_index + 1); 55 - 56 - // If we were empty, wake up a pop if it was waiting. 57 - if (was_empty) { 58 - self.not_empty.signal(); 59 - } 45 + self.pushAndSignalLH(item); 60 46 } 61 47 62 48 /// Push an item into the queue. Returns true when the item ··· 64 50 /// was full. 65 51 pub fn tryPush(self: *Self, item: T) bool { 66 52 self.mutex.lock(); 67 - if (self.isFullLH()) { 68 - self.mutex.unlock(); 69 - return false; 70 - } 71 - self.mutex.unlock(); 72 - self.push(item); 53 + defer self.mutex.unlock(); 54 + if (self.isFullLH()) return false; 55 + self.pushAndSignalLH(item); 73 56 return true; 74 57 } 75 58 ··· 77 60 /// available. 78 61 pub fn tryPop(self: *Self) ?T { 79 62 self.mutex.lock(); 80 - if (self.isEmptyLH()) { 81 - self.mutex.unlock(); 82 - return null; 83 - } 84 - self.mutex.unlock(); 85 - return self.pop(); 63 + defer self.mutex.unlock(); 64 + if (self.isEmptyLH()) return null; 65 + return self.popAndSignalLH(); 86 66 } 87 67 88 68 /// Poll the queue. This call blocks until events are in the queue ··· 148 128 /// Returns `index` modulo twice the length of the backing slice. 149 129 fn mask2(self: Self, index: usize) usize { 150 130 return index % (2 * self.buf.len); 131 + } 132 + 133 + fn pushAndSignalLH(self: *Self, item: T) void { 134 + const was_empty = self.isEmptyLH(); 135 + self.buf[self.mask(self.write_index)] = item; 136 + self.write_index = self.mask2(self.write_index + 1); 137 + if (was_empty) { 138 + self.not_empty.signal(); 139 + } 140 + } 141 + 142 + fn popAndSignalLH(self: *Self) T { 143 + const was_full = self.isFullLH(); 144 + const result = self.popLH(); 145 + if (was_full) { 146 + self.not_full.signal(); 147 + } 148 + return result; 151 149 } 152 150 153 151 fn popLH(self: *Self) T {
+3 -2
src/tty.zig
··· 453 453 0xc0 => '`', 454 454 0xdb => '[', 455 455 0xdc => '\\', 456 + 0xdf => '\\', 456 457 0xe2 => '\\', 457 458 0xdd => ']', 458 459 0xde => '\'', ··· 575 576 }; 576 577 577 578 const mouse: Mouse = .{ 578 - .col = @as(u16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index 579 - .row = @as(u16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index 579 + .col = @as(i16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index 580 + .row = @as(i16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index 580 581 .mods = mods, 581 582 .type = event_type, 582 583 .button = btn,
+2 -1
src/widgets/TextView.zig
··· 85 85 while (iter.next()) |result| { 86 86 if (prev_break and !result.is_break) { 87 87 // Start of a new grapheme 88 - grapheme_start = iter.i - std.unicode.utf8CodepointSequenceLength(result.cp) catch 1; 88 + const cp_len: usize = std.unicode.utf8CodepointSequenceLength(result.cp) catch 1; 89 + grapheme_start = iter.i - cp_len; 89 90 } 90 91 91 92 if (result.is_break) {