this repo has no description
13
fork

Configure Feed

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

at main 589 lines 20 kB view raw
1const std = @import("std"); 2const vaxis = @import("../main.zig"); 3const uucode = @import("uucode"); 4 5const testing = std.testing; 6 7const assert = std.debug.assert; 8 9const Allocator = std.mem.Allocator; 10 11pub const App = @import("App.zig"); 12 13// Widgets 14pub const Border = @import("Border.zig"); 15pub const Button = @import("Button.zig"); 16pub const Center = @import("Center.zig"); 17pub const FlexColumn = @import("FlexColumn.zig"); 18pub const FlexRow = @import("FlexRow.zig"); 19pub const ListView = @import("ListView.zig"); 20pub const Padding = @import("Padding.zig"); 21pub const RichText = @import("RichText.zig"); 22pub const ScrollView = @import("ScrollView.zig"); 23pub const ScrollBars = @import("ScrollBars.zig"); 24pub const SizedBox = @import("SizedBox.zig"); 25pub const SplitView = @import("SplitView.zig"); 26pub const Spinner = @import("Spinner.zig"); 27pub const Text = @import("Text.zig"); 28pub const TextField = @import("TextField.zig"); 29 30pub const CommandList = std.ArrayList(Command); 31 32pub const UserEvent = struct { 33 name: []const u8, 34 data: ?*const anyopaque = null, 35}; 36 37pub const Event = union(enum) { 38 key_press: vaxis.Key, 39 key_release: vaxis.Key, 40 mouse: vaxis.Mouse, 41 focus_in, // window has gained focus 42 focus_out, // window has lost focus 43 paste_start, // bracketed paste start 44 paste_end, // bracketed paste end 45 paste: []const u8, // osc 52 paste, caller must free 46 color_report: vaxis.Color.Report, // osc 4, 10, 11, 12 response 47 color_scheme: vaxis.Color.Scheme, // light / dark OS theme changes 48 winsize: vaxis.Winsize, // the window size has changed. This event is always sent when the loop is started 49 app: UserEvent, // A custom event from the app 50 tick, // An event from a Tick command 51 init, // sent when the application starts 52 mouse_leave, // The mouse has left the widget 53 mouse_enter, // The mouse has entered the widget 54}; 55 56pub const Tick = struct { 57 deadline: std.Io.Timestamp, 58 widget: Widget, 59 60 pub fn lessThan(_: void, lhs: Tick, rhs: Tick) bool { 61 return lhs.deadline.nanoseconds > rhs.deadline.nanoseconds; 62 } 63 64 pub fn in(io: std.Io, ms: u32, widget: Widget) Command { 65 const now: std.Io.Timestamp = .now(io, .awake); 66 const deadline = now.addDuration(.fromMilliseconds(ms)); 67 return .{ 68 .tick = .{ 69 .deadline = deadline, 70 .widget = widget, 71 }, 72 }; 73 } 74}; 75 76pub const Command = union(enum) { 77 /// Callback the event with a tick event at the specified deadlline 78 tick: Tick, 79 /// Change the mouse shape. This also has an implicit redraw 80 set_mouse_shape: vaxis.Mouse.Shape, 81 /// Request that this widget receives focus 82 request_focus: Widget, 83 84 /// Try to copy the provided text to the host clipboard. Uses OSC 52. Silently fails if terminal 85 /// doesn't support OSC 52 86 copy_to_clipboard: []const u8, 87 88 /// Set the title of the terminal 89 set_title: []const u8, 90 91 /// Queue a refresh of the entire screen. Implicitly sets redraw 92 queue_refresh, 93 94 /// Send a system notification 95 notify: struct { 96 title: ?[]const u8, 97 body: []const u8, 98 }, 99 100 query_color: vaxis.Cell.Color.Kind, 101}; 102 103pub const EventContext = struct { 104 phase: Phase = .at_target, 105 io: std.Io, 106 alloc: Allocator, 107 cmds: CommandList, 108 109 /// The event was handled, do not pass it on 110 consume_event: bool = false, 111 /// Tells the event loop to redraw the UI 112 redraw: bool = true, 113 /// Quit the application 114 quit: bool = false, 115 116 pub const Phase = enum { 117 capturing, 118 at_target, 119 bubbling, 120 }; 121 122 pub fn addCmd(self: *EventContext, cmd: Command) Allocator.Error!void { 123 try self.cmds.append(self.alloc, cmd); 124 } 125 126 pub fn tick(self: *EventContext, ms: u32, widget: Widget) Allocator.Error!void { 127 try self.addCmd(Tick.in(self.io, ms, widget)); 128 } 129 130 pub fn consumeAndRedraw(self: *EventContext) void { 131 self.consume_event = true; 132 self.redraw = true; 133 } 134 135 pub fn consumeEvent(self: *EventContext) void { 136 self.consume_event = true; 137 } 138 139 pub fn setMouseShape(self: *EventContext, shape: vaxis.Mouse.Shape) Allocator.Error!void { 140 try self.addCmd(.{ .set_mouse_shape = shape }); 141 self.redraw = true; 142 } 143 144 pub fn requestFocus(self: *EventContext, widget: Widget) Allocator.Error!void { 145 try self.addCmd(.{ .request_focus = widget }); 146 } 147 148 /// Copy content to clipboard. 149 /// content is duplicated using self.alloc. 150 /// Caller retains ownership of their copy of content. 151 pub fn copyToClipboard(self: *EventContext, content: []const u8) Allocator.Error!void { 152 try self.addCmd(.{ .copy_to_clipboard = try self.alloc.dupe(u8, content) }); 153 } 154 155 /// Set window title. 156 /// title is duplicated using self.alloc. 157 /// Caller retains ownership of their copy of title. 158 pub fn setTitle(self: *EventContext, title: []const u8) Allocator.Error!void { 159 try self.addCmd(.{ .set_title = try self.alloc.dupe(u8, title) }); 160 } 161 162 pub fn queueRefresh(self: *EventContext) Allocator.Error!void { 163 try self.addCmd(.queue_refresh); 164 self.redraw = true; 165 } 166 167 /// Send a system notification. This function dupes title and body using it's own allocator. 168 /// They will be freed once the notification has been sent 169 pub fn sendNotification( 170 self: *EventContext, 171 maybe_title: ?[]const u8, 172 body: []const u8, 173 ) Allocator.Error!void { 174 const alloc = self.alloc; 175 if (maybe_title) |title| { 176 return self.addCmd(.{ .notify = .{ 177 .title = try alloc.dupe(u8, title), 178 .body = try alloc.dupe(u8, body), 179 } }); 180 } 181 return self.addCmd(.{ .notify = .{ 182 .title = null, 183 .body = try alloc.dupe(u8, body), 184 } }); 185 } 186 187 pub fn queryColor(self: *EventContext, kind: vaxis.Cell.Color.Kind) Allocator.Error!void { 188 try self.addCmd(.{ .query_color = kind }); 189 } 190}; 191 192pub const DrawContext = struct { 193 // Allocator backed by an arena. Widgets do not need to free their own resources, they will be 194 // freed after rendering 195 arena: std.mem.Allocator, 196 // Constraints 197 min: Size, 198 max: MaxSize, 199 200 // Size of a single cell, in pixels 201 cell_size: Size, 202 203 // Unicode stuff 204 var width_method: vaxis.gwidth.Method = .unicode; 205 206 pub fn init(method: vaxis.gwidth.Method) void { 207 DrawContext.width_method = method; 208 } 209 210 pub fn stringWidth(_: DrawContext, str: []const u8) usize { 211 return vaxis.gwidth.gwidth( 212 str, 213 DrawContext.width_method, 214 ); 215 } 216 217 pub fn graphemeIterator(_: DrawContext, str: []const u8) vaxis.unicode.GraphemeIterator { 218 return vaxis.unicode.graphemeIterator(str); 219 } 220 221 pub fn withConstraints(self: DrawContext, min: Size, max: MaxSize) DrawContext { 222 return .{ 223 .arena = self.arena, 224 .min = min, 225 .max = max, 226 .cell_size = self.cell_size, 227 }; 228 } 229}; 230 231pub const Size = struct { 232 width: u16 = 0, 233 height: u16 = 0, 234}; 235 236pub const MaxSize = struct { 237 width: ?u16 = null, 238 height: ?u16 = null, 239 240 /// Returns true if the row would fall outside of this height. A null height value is infinite 241 /// and always returns false 242 pub fn outsideHeight(self: MaxSize, row: u16) bool { 243 const max = self.height orelse return false; 244 return row >= max; 245 } 246 247 /// Returns true if the col would fall outside of this width. A null width value is infinite 248 /// and always returns false 249 pub fn outsideWidth(self: MaxSize, col: u16) bool { 250 const max = self.width orelse return false; 251 return col >= max; 252 } 253 254 /// Asserts that neither height nor width are null 255 pub fn size(self: MaxSize) Size { 256 assert(self.width != null); 257 assert(self.height != null); 258 return .{ 259 .width = self.width.?, 260 .height = self.height.?, 261 }; 262 } 263 264 pub fn fromSize(other: Size) MaxSize { 265 return .{ 266 .width = other.width, 267 .height = other.height, 268 }; 269 } 270}; 271 272/// The Widget interface 273pub const Widget = struct { 274 userdata: *anyopaque, 275 captureHandler: ?*const fn (userdata: *anyopaque, ctx: *EventContext, event: Event) anyerror!void = null, 276 eventHandler: ?*const fn (userdata: *anyopaque, ctx: *EventContext, event: Event) anyerror!void = null, 277 drawFn: *const fn (userdata: *anyopaque, ctx: DrawContext) Allocator.Error!Surface, 278 279 pub fn captureEvent(self: Widget, ctx: *EventContext, event: Event) anyerror!void { 280 if (self.captureHandler) |handle| { 281 return handle(self.userdata, ctx, event); 282 } 283 } 284 285 pub fn handleEvent(self: Widget, ctx: *EventContext, event: Event) anyerror!void { 286 if (self.eventHandler) |handle| { 287 return handle(self.userdata, ctx, event); 288 } 289 } 290 291 pub fn draw(self: Widget, ctx: DrawContext) Allocator.Error!Surface { 292 return self.drawFn(self.userdata, ctx); 293 } 294 295 /// Returns true if the Widgets point to the same widget instance. To be considered the same, 296 /// the userdata and drawFn fields must point to the same values in both widgets 297 pub fn eql(self: Widget, other: Widget) bool { 298 return @intFromPtr(self.userdata) == @intFromPtr(other.userdata) and 299 @intFromPtr(self.drawFn) == @intFromPtr(other.drawFn); 300 } 301}; 302 303pub const FlexItem = struct { 304 widget: Widget, 305 /// A value of zero means the child will have it's inherent size. Any value greater than zero 306 /// and the remaining space will be proportioned to each item 307 flex: u8 = 1, 308 309 pub fn init(child: Widget, flex: u8) FlexItem { 310 return .{ .widget = child, .flex = flex }; 311 } 312}; 313 314pub const Point = struct { 315 row: u16, 316 col: u16, 317}; 318 319pub const RelativePoint = struct { 320 row: i17, 321 col: i17, 322}; 323 324/// Result of a hit test 325pub const HitResult = struct { 326 local: Point, 327 widget: Widget, 328}; 329 330pub const CursorState = struct { 331 /// Local coordinates 332 row: u16, 333 /// Local coordinates 334 col: u16, 335 shape: vaxis.Cell.CursorShape = .default, 336}; 337 338pub const Surface = struct { 339 /// Size of this surface 340 size: Size, 341 /// The widget this surface belongs to 342 widget: Widget, 343 344 /// Cursor state 345 cursor: ?CursorState = null, 346 347 /// Contents of this surface. Must be len == 0 or len == size.width * size.height 348 buffer: []vaxis.Cell, 349 350 children: []SubSurface, 351 352 pub fn empty(widget: Widget) Surface { 353 return .{ 354 .size = .{}, 355 .widget = widget, 356 .buffer = &.{}, 357 .children = &.{}, 358 }; 359 } 360 361 /// Creates a slice of vaxis.Cell's equal to size.width * size.height 362 pub fn createBuffer(allocator: Allocator, size: Size) Allocator.Error![]vaxis.Cell { 363 const buffer = try allocator.alloc(vaxis.Cell, size.width * size.height); 364 @memset(buffer, .{ .default = true }); 365 return buffer; 366 } 367 368 pub fn init(allocator: Allocator, widget: Widget, size: Size) Allocator.Error!Surface { 369 return .{ 370 .size = size, 371 .widget = widget, 372 .buffer = try Surface.createBuffer(allocator, size), 373 .children = &.{}, 374 }; 375 } 376 377 pub fn initWithChildren( 378 allocator: Allocator, 379 widget: Widget, 380 size: Size, 381 children: []SubSurface, 382 ) Allocator.Error!Surface { 383 return .{ 384 .size = size, 385 .widget = widget, 386 .buffer = try Surface.createBuffer(allocator, size), 387 .children = children, 388 }; 389 } 390 391 pub fn writeCell(self: Surface, col: u16, row: u16, cell: vaxis.Cell) void { 392 if (self.size.width <= col) return; 393 if (self.size.height <= row) return; 394 const i = (row * self.size.width) + col; 395 assert(i < self.buffer.len); 396 self.buffer[i] = cell; 397 } 398 399 pub fn readCell(self: Surface, col: usize, row: usize) vaxis.Cell { 400 assert(col < self.size.width and row < self.size.height); 401 const i = (row * self.size.width) + col; 402 assert(i < self.buffer.len); 403 return self.buffer[i]; 404 } 405 406 /// Creates a new surface of the same width, with the buffer trimmed to a given height 407 pub fn trimHeight(self: Surface, height: u16) Surface { 408 assert(height <= self.size.height); 409 return .{ 410 .size = .{ .width = self.size.width, .height = height }, 411 .widget = self.widget, 412 .buffer = self.buffer[0 .. self.size.width * height], 413 .children = self.children, 414 }; 415 } 416 417 /// Walks the Surface tree to produce a list of all widgets that intersect Point. Point will 418 /// always be translated to local Surface coordinates. Asserts that this Surface does contain Point 419 pub fn hitTest(self: Surface, allocator: Allocator, list: *std.ArrayList(HitResult), point: Point) Allocator.Error!void { 420 assert(point.col < self.size.width and point.row < self.size.height); 421 // Add this widget to the hit list if it has an event or capture handler 422 if (self.widget.eventHandler != null or self.widget.captureHandler != null) 423 try list.append(allocator, .{ .local = point, .widget = self.widget }); 424 for (self.children) |child| { 425 if (!child.containsPoint(point)) continue; 426 const child_point: Point = .{ 427 .row = @intCast(point.row - child.origin.row), 428 .col = @intCast(point.col - child.origin.col), 429 }; 430 try child.surface.hitTest(allocator, list, child_point); 431 } 432 } 433 434 /// Copies all cells from Surface to Window 435 pub fn render(self: Surface, win: vaxis.Window, focused: Widget) void { 436 // render self first 437 if (self.buffer.len > 0) { 438 assert(self.buffer.len == self.size.width * self.size.height); 439 for (self.buffer, 0..) |cell, i| { 440 const row = i / self.size.width; 441 const col = i % self.size.width; 442 win.writeCell(@intCast(col), @intCast(row), cell); 443 } 444 } 445 446 if (self.cursor) |cursor| { 447 if (self.widget.eql(focused)) { 448 win.showCursor(cursor.col, cursor.row); 449 win.setCursorShape(cursor.shape); 450 } 451 } 452 453 // Sort children by z-index 454 std.mem.sort(SubSurface, self.children, {}, SubSurface.lessThan); 455 456 // for each child, we make a window and render to it 457 for (self.children) |child| { 458 const child_win = win.child(.{ 459 .x_off = @intCast(child.origin.col), 460 .y_off = @intCast(child.origin.row), 461 .width = @intCast(child.surface.size.width), 462 .height = @intCast(child.surface.size.height), 463 }); 464 child.surface.render(child_win, focused); 465 } 466 } 467 468 /// Returns true if the surface satisfies a set of constraints 469 pub fn satisfiesConstraints(self: Surface, min: Size, max: Size) bool { 470 return self.size.width < max.width and 471 self.size.width > min.width and 472 self.size.height < max.height and 473 self.size.height > min.height; 474 } 475}; 476 477pub const SubSurface = struct { 478 /// Origin relative to parent 479 origin: RelativePoint, 480 /// This surface 481 surface: Surface, 482 /// z-index relative to siblings 483 z_index: u8 = 0, 484 485 pub fn lessThan(_: void, lhs: SubSurface, rhs: SubSurface) bool { 486 return lhs.z_index < rhs.z_index; 487 } 488 489 /// Returns true if this SubSurface contains Point. Point must be in parent local units 490 pub fn containsPoint(self: SubSurface, point: Point) bool { 491 return point.col >= self.origin.col and 492 point.row >= self.origin.row and 493 point.col < (self.origin.col + self.surface.size.width) and 494 point.row < (self.origin.row + self.surface.size.height); 495 } 496}; 497 498test { 499 std.testing.refAllDecls(@This()); 500} 501 502test "SubSurface: containsPoint" { 503 const surf: SubSurface = .{ 504 .origin = .{ .row = 2, .col = 2 }, 505 .surface = .{ 506 .size = .{ .width = 10, .height = 10 }, 507 .widget = undefined, 508 .children = &.{}, 509 .buffer = &.{}, 510 }, 511 .z_index = 0, 512 }; 513 514 try testing.expect(surf.containsPoint(.{ .row = 2, .col = 2 })); 515 try testing.expect(surf.containsPoint(.{ .row = 3, .col = 3 })); 516 try testing.expect(surf.containsPoint(.{ .row = 11, .col = 11 })); 517 518 try testing.expect(!surf.containsPoint(.{ .row = 1, .col = 1 })); 519 try testing.expect(!surf.containsPoint(.{ .row = 12, .col = 12 })); 520 try testing.expect(!surf.containsPoint(.{ .row = 2, .col = 12 })); 521 try testing.expect(!surf.containsPoint(.{ .row = 12, .col = 2 })); 522} 523 524test "refAllDecls" { 525 std.testing.refAllDecls(@This()); 526} 527 528test "Surface: satisfiesConstraints" { 529 const surf: Surface = .{ 530 .size = .{ .width = 10, .height = 10 }, 531 .widget = undefined, 532 .children = &.{}, 533 .buffer = &.{}, 534 }; 535 536 try testing.expect(surf.satisfiesConstraints(.{ .width = 1, .height = 1 }, .{ .width = 20, .height = 20 })); 537 try testing.expect(!surf.satisfiesConstraints(.{ .width = 10, .height = 10 }, .{ .width = 20, .height = 20 })); 538 try testing.expect(!surf.satisfiesConstraints(.{ .width = 1, .height = 1 }, .{ .width = 10, .height = 10 })); 539} 540 541test "All widgets have a doctest and refAllDecls test" { 542 const io = std.testing.io; 543 // This test goes through every file in src/ and checks that it has a doctest (the filename 544 // stripped of ".zig" matches a test name) and a test called "refAllDecls". It makes no 545 // guarantees about the quality of the test, but it does ensure it exists which at least makes 546 // it easy to fail CI early, or spot bad tests vs non-existant tests 547 const excludes = &[_][]const u8{ "vxfw.zig", "App.zig" }; 548 549 var cwd = try std.Io.Dir.cwd().openDir(io, "./src/vxfw", .{ .iterate = true }); 550 var iter = cwd.iterate(); 551 defer cwd.close(io); 552 outer: while (try iter.next(io)) |file| { 553 if (file.kind != .file) continue; 554 for (excludes) |ex| if (std.mem.eql(u8, ex, file.name)) continue :outer; 555 556 const container_name = if (std.mem.lastIndexOf(u8, file.name, ".zig")) |idx| 557 file.name[0..idx] 558 else 559 continue; 560 const data = try cwd.readFileAllocOptions(io, file.name, std.testing.allocator, .limited(10_000_000), .of(u8), 0x00); 561 defer std.testing.allocator.free(data); 562 var ast = try std.zig.Ast.parse(std.testing.allocator, data, .zig); 563 defer ast.deinit(std.testing.allocator); 564 565 var has_doctest: bool = false; 566 var has_refAllDecls: bool = false; 567 for (ast.rootDecls()) |root_decl| { 568 const decl = ast.nodes.get(@intFromEnum(root_decl)); 569 switch (decl.tag) { 570 .test_decl => { 571 const test_name = ast.tokenSlice(decl.main_token + 1); 572 if (std.mem.eql(u8, "\"refAllDecls\"", test_name)) 573 has_refAllDecls = true 574 else if (std.mem.eql(u8, container_name, test_name)) 575 has_doctest = true; 576 }, 577 else => continue, 578 } 579 } 580 if (!has_doctest) { 581 std.log.err("file {s} has no doctest", .{file.name}); 582 return error.TestExpectedDoctest; 583 } 584 if (!has_refAllDecls) { 585 std.log.err("file {s} has no 'refAllDecls' test", .{file.name}); 586 return error.TestExpectedRefAllDecls; 587 } 588 } 589}