this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

vxfw: introduce the vaxis framework

This was previously being developed at github.com/rockorager/vtk. I
really liked how it came together, and am moving it into the vaxis repo
piece by piece.

+473
+2
src/main.zig
··· 30 30 pub const Event = @import("event.zig").Event; 31 31 pub const Unicode = @import("Unicode.zig"); 32 32 33 + pub const vxfw = @import("vxfw/vxfw.zig"); 34 + 33 35 pub const Tty = tty.Tty; 34 36 35 37 /// The size of the terminal screen
+471
src/vxfw/vxfw.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("../main.zig"); 3 + 4 + const grapheme = vaxis.grapheme; 5 + const testing = std.testing; 6 + 7 + const assert = std.debug.assert; 8 + 9 + const Allocator = std.mem.Allocator; 10 + 11 + pub const CommandList = std.ArrayList(Command); 12 + 13 + pub const UserEvent = struct { 14 + name: []const u8, 15 + data: ?*const anyopaque = null, 16 + }; 17 + 18 + pub const Event = union(enum) { 19 + key_press: vaxis.Key, 20 + key_release: vaxis.Key, 21 + mouse: vaxis.Mouse, 22 + focus_in, // window has gained focus 23 + focus_out, // window has lost focus 24 + paste_start, // bracketed paste start 25 + paste_end, // bracketed paste end 26 + paste: []const u8, // osc 52 paste, caller must free 27 + color_report: vaxis.Color.Report, // osc 4, 10, 11, 12 response 28 + color_scheme: vaxis.Color.Scheme, // light / dark OS theme changes 29 + winsize: vaxis.Winsize, // the window size has changed. This event is always sent when the loop is started 30 + app: UserEvent, // A custom event from the app 31 + tick, // An event from a Tick command 32 + init, // sent when the application starts 33 + mouse_leave, // The mouse has left the widget 34 + }; 35 + 36 + pub const Tick = struct { 37 + deadline_ms: i64, 38 + widget: Widget, 39 + 40 + pub fn lessThan(_: void, lhs: Tick, rhs: Tick) bool { 41 + return lhs.deadline_ms > rhs.deadline_ms; 42 + } 43 + 44 + pub fn in(ms: u32, widget: Widget) Command { 45 + const now = std.time.milliTimestamp(); 46 + return .{ .tick = .{ 47 + .deadline_ms = now + ms, 48 + .widget = widget, 49 + } }; 50 + } 51 + }; 52 + 53 + pub const Command = union(enum) { 54 + /// Callback the event with a tick event at the specified deadlline 55 + tick: Tick, 56 + /// Change the mouse shape. This also has an implicit redraw 57 + set_mouse_shape: vaxis.Mouse.Shape, 58 + /// Request that this widget receives focus 59 + request_focus: Widget, 60 + }; 61 + 62 + pub const EventContext = struct { 63 + phase: Phase = .at_target, 64 + cmds: CommandList, 65 + 66 + /// The event was handled, do not pass it on 67 + consume_event: bool = false, 68 + /// Tells the event loop to redraw the UI 69 + redraw: bool = true, 70 + /// Quit the application 71 + quit: bool = false, 72 + 73 + pub const Phase = enum { 74 + // TODO: Capturing phase 75 + // capturing, 76 + at_target, 77 + bubbling, 78 + }; 79 + 80 + pub fn addCmd(self: *EventContext, cmd: Command) Allocator.Error!void { 81 + try self.cmds.append(cmd); 82 + } 83 + 84 + pub fn tick(self: *EventContext, ms: u32, widget: Widget) Allocator.Error!void { 85 + try self.addCmd(Tick.in(ms, widget)); 86 + } 87 + 88 + pub fn consumeAndRedraw(self: *EventContext) void { 89 + self.consume_event = true; 90 + self.redraw = true; 91 + } 92 + 93 + pub fn consumeEvent(self: *EventContext) void { 94 + self.consume_event = true; 95 + } 96 + 97 + pub fn setMouseShape(self: *EventContext, shape: vaxis.Mouse.Shape) Allocator.Error!void { 98 + try self.addCmd(.{ .set_mouse_shape = shape }); 99 + self.redraw = true; 100 + } 101 + 102 + pub fn requestFocus(self: *EventContext, widget: Widget) Allocator.Error!void { 103 + try self.addCmd(.{ .request_focus = widget }); 104 + } 105 + }; 106 + 107 + pub const DrawContext = struct { 108 + // Allocator backed by an arena. Widgets do not need to free their own resources, they will be 109 + // freed after rendering 110 + arena: std.mem.Allocator, 111 + // Constraints 112 + min: Size, 113 + max: MaxSize, 114 + 115 + // Unicode stuff 116 + var unicode: ?*const vaxis.Unicode = null; 117 + var width_method: vaxis.gwidth.Method = .unicode; 118 + 119 + pub fn init(ucd: *const vaxis.Unicode, method: vaxis.gwidth.Method) void { 120 + DrawContext.unicode = ucd; 121 + DrawContext.width_method = method; 122 + } 123 + 124 + pub fn stringWidth(_: DrawContext, str: []const u8) usize { 125 + assert(DrawContext.unicode != null); // DrawContext not initialized 126 + return vaxis.gwidth.gwidth( 127 + str, 128 + DrawContext.width_method, 129 + &DrawContext.unicode.?.width_data, 130 + ); 131 + } 132 + 133 + pub fn graphemeIterator(_: DrawContext, str: []const u8) grapheme.Iterator { 134 + assert(DrawContext.unicode != null); // DrawContext not initialized 135 + return DrawContext.unicode.?.graphemeIterator(str); 136 + } 137 + 138 + pub fn withConstraints(self: DrawContext, min: Size, max: MaxSize) DrawContext { 139 + return .{ 140 + .arena = self.arena, 141 + .min = min, 142 + .max = max, 143 + }; 144 + } 145 + }; 146 + 147 + pub const Size = struct { 148 + width: u16 = 0, 149 + height: u16 = 0, 150 + }; 151 + 152 + pub const MaxSize = struct { 153 + width: ?u16 = null, 154 + height: ?u16 = null, 155 + 156 + /// Returns true if the row would fall outside of this height. A null height value is infinite 157 + /// and always returns false 158 + pub fn outsideHeight(self: MaxSize, row: u16) bool { 159 + const max = self.height orelse return false; 160 + return row >= max; 161 + } 162 + 163 + /// Returns true if the col would fall outside of this width. A null width value is infinite 164 + /// and always returns false 165 + pub fn outsideWidth(self: MaxSize, col: u16) bool { 166 + const max = self.width orelse return false; 167 + return col >= max; 168 + } 169 + 170 + /// Asserts that neither height nor width are null 171 + pub fn size(self: MaxSize) Size { 172 + assert(self.width != null); 173 + assert(self.height != null); 174 + return .{ 175 + .width = self.width.?, 176 + .height = self.height.?, 177 + }; 178 + } 179 + }; 180 + 181 + /// The Widget interface 182 + pub const Widget = struct { 183 + userdata: *anyopaque, 184 + eventHandler: *const fn (userdata: *anyopaque, ctx: *EventContext, event: Event) anyerror!void, 185 + drawFn: *const fn (userdata: *anyopaque, ctx: DrawContext) Allocator.Error!Surface, 186 + 187 + pub fn handleEvent(self: Widget, ctx: *EventContext, event: Event) anyerror!void { 188 + return self.eventHandler(self.userdata, ctx, event); 189 + } 190 + 191 + pub fn draw(self: Widget, ctx: DrawContext) Allocator.Error!Surface { 192 + return self.drawFn(self.userdata, ctx); 193 + } 194 + 195 + /// Returns true if the Widgets point to the same widget instance 196 + pub fn eql(self: Widget, other: Widget) bool { 197 + return @intFromPtr(self.userdata) == @intFromPtr(other.userdata) and 198 + @intFromPtr(self.eventHandler) == @intFromPtr(other.eventHandler) and 199 + @intFromPtr(self.drawFn) == @intFromPtr(other.drawFn); 200 + } 201 + }; 202 + 203 + pub const FlexItem = struct { 204 + widget: Widget, 205 + /// A value of zero means the child will have it's inherent size. Any value greater than zero 206 + /// and the remaining space will be proportioned to each item 207 + flex: u8 = 1, 208 + 209 + pub fn init(child: Widget, flex: u8) FlexItem { 210 + return .{ .widget = child, .flex = flex }; 211 + } 212 + }; 213 + 214 + pub const Point = struct { 215 + row: u16, 216 + col: u16, 217 + }; 218 + 219 + pub const RelativePoint = struct { 220 + row: i17, 221 + col: i17, 222 + }; 223 + 224 + /// Result of a hit test 225 + pub const HitResult = struct { 226 + local: Point, 227 + widget: Widget, 228 + }; 229 + 230 + pub const CursorState = struct { 231 + /// Local coordinates 232 + row: u16, 233 + /// Local coordinates 234 + col: u16, 235 + shape: vaxis.Cell.CursorShape = .default, 236 + }; 237 + 238 + pub const Surface = struct { 239 + /// Size of this surface 240 + size: Size, 241 + /// The widget this surface belongs to 242 + widget: Widget, 243 + 244 + /// If this widget / Surface is focusable 245 + focusable: bool = false, 246 + /// If this widget can handle mouse events 247 + handles_mouse: bool = false, 248 + 249 + /// Cursor state 250 + cursor: ?CursorState = null, 251 + 252 + /// Contents of this surface. Must be len == 0 or len == size.width * size.height 253 + buffer: []vaxis.Cell, 254 + 255 + children: []SubSurface, 256 + 257 + /// Creates a slice of vaxis.Cell's equal to size.width * size.height 258 + pub fn createBuffer(allocator: Allocator, size: Size) Allocator.Error![]vaxis.Cell { 259 + const buffer = try allocator.alloc(vaxis.Cell, size.width * size.height); 260 + @memset(buffer, .{ .default = true }); 261 + return buffer; 262 + } 263 + 264 + pub fn init(allocator: Allocator, widget: Widget, size: Size) Allocator.Error!Surface { 265 + return .{ 266 + .size = size, 267 + .widget = widget, 268 + .buffer = try Surface.createBuffer(allocator, size), 269 + .children = &.{}, 270 + }; 271 + } 272 + 273 + pub fn initWithChildren( 274 + allocator: Allocator, 275 + widget: Widget, 276 + size: Size, 277 + children: []SubSurface, 278 + ) Allocator.Error!Surface { 279 + return .{ 280 + .size = size, 281 + .widget = widget, 282 + .buffer = try Surface.createBuffer(allocator, size), 283 + .children = children, 284 + }; 285 + } 286 + 287 + pub fn writeCell(self: Surface, col: u16, row: u16, cell: vaxis.Cell) void { 288 + if (self.size.width <= col) return; 289 + if (self.size.height <= row) return; 290 + const i = (row * self.size.width) + col; 291 + assert(i < self.buffer.len); 292 + self.buffer[i] = cell; 293 + } 294 + 295 + pub fn readCell(self: Surface, col: usize, row: usize) vaxis.Cell { 296 + assert(col < self.size.width and row < self.size.height); 297 + const i = (row * self.size.width) + col; 298 + assert(i < self.buffer.len); 299 + return self.buffer[i]; 300 + } 301 + 302 + /// Creates a new surface of the same width, with the buffer trimmed to a given height 303 + pub fn trimHeight(self: Surface, height: u16) Surface { 304 + assert(height <= self.size.height); 305 + return .{ 306 + .size = .{ .width = self.size.width, .height = height }, 307 + .widget = self.widget, 308 + .buffer = self.buffer[0 .. self.size.width * height], 309 + .children = self.children, 310 + .focusable = self.focusable, 311 + .handles_mouse = self.handles_mouse, 312 + }; 313 + } 314 + 315 + /// Walks the Surface tree to produce a list of all widgets that intersect Point. Point will 316 + /// always be translated to local Surface coordinates. Asserts that this Surface does contain Point 317 + pub fn hitTest(self: Surface, list: *std.ArrayList(HitResult), point: Point) Allocator.Error!void { 318 + assert(point.col < self.size.width and point.row < self.size.height); 319 + if (self.handles_mouse) 320 + try list.append(.{ .local = point, .widget = self.widget }); 321 + for (self.children) |child| { 322 + if (!child.containsPoint(point)) continue; 323 + const child_point: Point = .{ 324 + .row = @intCast(point.row - child.origin.row), 325 + .col = @intCast(point.col - child.origin.col), 326 + }; 327 + try child.surface.hitTest(list, child_point); 328 + } 329 + } 330 + 331 + /// Copies all cells from Surface to Window 332 + pub fn render(self: Surface, win: vaxis.Window, focused: Widget) void { 333 + // render self first 334 + if (self.buffer.len > 0) { 335 + assert(self.buffer.len == self.size.width * self.size.height); 336 + for (self.buffer, 0..) |cell, i| { 337 + const row = i / self.size.width; 338 + const col = i % self.size.width; 339 + win.writeCell(@intCast(col), @intCast(row), cell); 340 + } 341 + } 342 + 343 + if (self.cursor) |cursor| { 344 + if (self.widget.eql(focused)) { 345 + win.showCursor(cursor.col, cursor.row); 346 + win.setCursorShape(cursor.shape); 347 + } 348 + } 349 + 350 + // Sort children by z-index 351 + std.mem.sort(SubSurface, self.children, {}, SubSurface.lessThan); 352 + 353 + // for each child, we make a window and render to it 354 + for (self.children) |child| { 355 + const child_win = win.child(.{ 356 + .x_off = @intCast(child.origin.col), 357 + .y_off = @intCast(child.origin.row), 358 + .width = @intCast(child.surface.size.width), 359 + .height = @intCast(child.surface.size.height), 360 + }); 361 + child.surface.render(child_win, focused); 362 + } 363 + } 364 + 365 + /// Returns true if the surface satisfies a set of constraints 366 + pub fn satisfiesConstraints(self: Surface, min: Size, max: Size) bool { 367 + return self.size.width < min.width and 368 + self.size.width > max.width and 369 + self.size.height < min.height and 370 + self.size.height > max.height; 371 + } 372 + }; 373 + 374 + pub const SubSurface = struct { 375 + /// Origin relative to parent 376 + origin: RelativePoint, 377 + /// This surface 378 + surface: Surface, 379 + /// z-index relative to siblings 380 + z_index: u8 = 0, 381 + 382 + pub fn lessThan(_: void, lhs: SubSurface, rhs: SubSurface) bool { 383 + return lhs.z_index < rhs.z_index; 384 + } 385 + 386 + /// Returns true if this SubSurface contains Point. Point must be in parent local units 387 + pub fn containsPoint(self: SubSurface, point: Point) bool { 388 + return point.col >= self.origin.col and 389 + point.row >= self.origin.row and 390 + point.col < (self.origin.col + self.surface.size.width) and 391 + point.row < (self.origin.row + self.surface.size.height); 392 + } 393 + }; 394 + 395 + /// A noop event handler for widgets which don't require any event handling 396 + pub fn noopEventHandler(_: *anyopaque, _: *EventContext, _: Event) anyerror!void {} 397 + 398 + test { 399 + std.testing.refAllDecls(@This()); 400 + } 401 + 402 + test "SubSurface: containsPoint" { 403 + const surf: SubSurface = .{ 404 + .origin = .{ .row = 2, .col = 2 }, 405 + .surface = .{ 406 + .size = .{ .width = 10, .height = 10 }, 407 + .widget = undefined, 408 + .children = &.{}, 409 + .buffer = &.{}, 410 + }, 411 + .z_index = 0, 412 + }; 413 + 414 + try testing.expect(surf.containsPoint(.{ .row = 2, .col = 2 })); 415 + try testing.expect(surf.containsPoint(.{ .row = 3, .col = 3 })); 416 + try testing.expect(surf.containsPoint(.{ .row = 11, .col = 11 })); 417 + 418 + try testing.expect(!surf.containsPoint(.{ .row = 1, .col = 1 })); 419 + try testing.expect(!surf.containsPoint(.{ .row = 12, .col = 12 })); 420 + try testing.expect(!surf.containsPoint(.{ .row = 2, .col = 12 })); 421 + try testing.expect(!surf.containsPoint(.{ .row = 12, .col = 2 })); 422 + } 423 + 424 + test "All widgets have a doctest and refAllDecls test" { 425 + // This test goes through every file in src/ and checks that it has a doctest (the filename 426 + // stripped of ".zig" matches a test name) and a test called "refAllDecls". It makes no 427 + // guarantees about the quality of the test, but it does ensure it exists which at least makes 428 + // it easy to fail CI early, or spot bad tests vs non-existant tests 429 + const excludes = &[_][]const u8{"vxfw.zig"}; 430 + 431 + var cwd = try std.fs.cwd().openDir("./src/vxfw", .{ .iterate = true }); 432 + var iter = cwd.iterate(); 433 + defer cwd.close(); 434 + outer: while (try iter.next()) |file| { 435 + if (file.kind != .file) continue; 436 + for (excludes) |ex| if (std.mem.eql(u8, ex, file.name)) continue :outer; 437 + 438 + const container_name = if (std.mem.lastIndexOf(u8, file.name, ".zig")) |idx| 439 + file.name[0..idx] 440 + else 441 + continue; 442 + const data = try cwd.readFileAllocOptions(std.testing.allocator, file.name, 10_000_000, null, @alignOf(u8), 0x00); 443 + defer std.testing.allocator.free(data); 444 + var ast = try std.zig.Ast.parse(std.testing.allocator, data, .zig); 445 + defer ast.deinit(std.testing.allocator); 446 + 447 + var has_doctest: bool = false; 448 + var has_refAllDecls: bool = false; 449 + for (ast.rootDecls()) |root_decl| { 450 + const decl = ast.nodes.get(root_decl); 451 + switch (decl.tag) { 452 + .test_decl => { 453 + const test_name = ast.tokenSlice(decl.data.lhs); 454 + if (std.mem.eql(u8, "\"refAllDecls\"", test_name)) 455 + has_refAllDecls = true 456 + else if (std.mem.eql(u8, container_name, test_name)) 457 + has_doctest = true; 458 + }, 459 + else => continue, 460 + } 461 + } 462 + if (!has_doctest) { 463 + std.log.err("file {s} has no doctest", .{file.name}); 464 + return error.TestExpectedDoctest; 465 + } 466 + if (!has_refAllDecls) { 467 + std.log.err("file {s} has no 'refAllDecls' test", .{file.name}); 468 + return error.TestExpectedRefAllDecls; 469 + } 470 + } 471 + }