this repo has no description
13
fork

Configure Feed

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

at 3943a6f42f7f4c73ab9493dfb0a4e123be15d302 604 lines 21 kB view raw
1const std = @import("std"); 2const vaxis = @import("../main.zig"); 3const vxfw = @import("vxfw.zig"); 4 5const assert = std.debug.assert; 6 7const Allocator = std.mem.Allocator; 8 9const EventLoop = vaxis.Loop(vxfw.Event); 10const Widget = vxfw.Widget; 11 12const App = @This(); 13 14allocator: Allocator, 15tty: vaxis.Tty, 16vx: vaxis.Vaxis, 17timers: std.ArrayList(vxfw.Tick), 18wants_focus: ?vxfw.Widget, 19buffer: [1024]u8, 20 21/// Runtime options 22pub const Options = struct { 23 /// Frames per second 24 framerate: u8 = 60, 25}; 26 27/// Create an application. We require stable pointers to do the set up, so this will create an App 28/// object on the heap. Call destroy when the app is complete to reset terminal state and release 29/// resources 30pub fn init(allocator: Allocator) !App { 31 var app: App = .{ 32 .allocator = allocator, 33 .tty = undefined, 34 .vx = try vaxis.init(allocator, .{ 35 .system_clipboard_allocator = allocator, 36 .kitty_keyboard_flags = .{ 37 .report_events = true, 38 }, 39 }), 40 .timers = std.ArrayList(vxfw.Tick){}, 41 .wants_focus = null, 42 .buffer = undefined, 43 }; 44 app.tty = try vaxis.Tty.init(&app.buffer); 45 return app; 46} 47 48pub fn deinit(self: *App) void { 49 self.timers.deinit(self.allocator); 50 self.vx.deinit(self.allocator, self.tty.writer()); 51 self.tty.deinit(); 52} 53 54pub fn run(self: *App, widget: vxfw.Widget, opts: Options) anyerror!void { 55 const tty = &self.tty; 56 const vx = &self.vx; 57 58 var loop: EventLoop = .{ .tty = tty, .vaxis = vx }; 59 try loop.start(); 60 defer loop.stop(); 61 62 // Send the init event 63 loop.postEvent(.init); 64 // Also always initialize the app with a focus event 65 loop.postEvent(.focus_in); 66 67 try vx.enterAltScreen(tty.writer()); 68 try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s); 69 try vx.setBracketedPaste(tty.writer(), true); 70 try vx.subscribeToColorSchemeUpdates(tty.writer()); 71 72 { 73 // This part deserves a comment. loop.init installs a signal handler for the tty. We wait to 74 // init the loop until we know if we need this handler. We don't need it if the terminal 75 // supports in-band-resize 76 if (!vx.state.in_band_resize) try loop.init(); 77 } 78 79 // NOTE: We don't use pixel mouse anywhere 80 vx.caps.sgr_pixels = false; 81 try vx.setMouseMode(tty.writer(), true); 82 83 vxfw.DrawContext.init(vx.screen.width_method); 84 85 const framerate: u64 = if (opts.framerate > 0) opts.framerate else 60; 86 // Calculate tick rate 87 const tick_ms: u64 = @divFloor(std.time.ms_per_s, framerate); 88 89 // Set up arena and context 90 var arena = std.heap.ArenaAllocator.init(self.allocator); 91 defer arena.deinit(); 92 93 var mouse_handler = MouseHandler.init(widget); 94 defer mouse_handler.deinit(self.allocator); 95 var focus_handler = FocusHandler.init(self.allocator, widget); 96 try focus_handler.path_to_focused.append(self.allocator, widget); 97 defer focus_handler.deinit(self.allocator); 98 99 // Timestamp of our next frame 100 var next_frame_ms: u64 = @intCast(std.time.milliTimestamp()); 101 102 // Create our event context 103 var ctx: vxfw.EventContext = .{ 104 .alloc = self.allocator, 105 .phase = .capturing, 106 .cmds = vxfw.CommandList{}, 107 .consume_event = false, 108 .redraw = false, 109 .quit = false, 110 }; 111 defer ctx.cmds.deinit(self.allocator); 112 113 while (true) { 114 const now_ms: u64 = @intCast(std.time.milliTimestamp()); 115 if (now_ms >= next_frame_ms) { 116 // Deadline exceeded. Schedule the next frame 117 next_frame_ms = now_ms + tick_ms; 118 } else { 119 // Sleep until the deadline 120 std.Thread.sleep((next_frame_ms - now_ms) * std.time.ns_per_ms); 121 next_frame_ms += tick_ms; 122 } 123 124 try self.checkTimers(&ctx); 125 126 { 127 loop.queue.lock(); 128 defer loop.queue.unlock(); 129 while (loop.queue.drain()) |event| { 130 defer { 131 // Reset our context 132 ctx.consume_event = false; 133 ctx.phase = .capturing; 134 } 135 switch (event) { 136 .key_press => { 137 try focus_handler.handleEvent(&ctx, event); 138 try self.handleCommand(&ctx.cmds); 139 }, 140 .focus_out => { 141 try mouse_handler.mouseExit(self, &ctx); 142 try focus_handler.handleEvent(&ctx, .focus_out); 143 try self.handleCommand(&ctx.cmds); 144 }, 145 .focus_in => { 146 try focus_handler.handleEvent(&ctx, .focus_in); 147 try self.handleCommand(&ctx.cmds); 148 }, 149 .mouse => |mouse| try mouse_handler.handleMouse(self, &ctx, mouse), 150 .winsize => |ws| { 151 try vx.resize(self.allocator, tty.writer(), ws); 152 ctx.redraw = true; 153 }, 154 else => { 155 try focus_handler.handleEvent(&ctx, event); 156 try self.handleCommand(&ctx.cmds); 157 }, 158 } 159 } 160 } 161 162 // If we have a focus change, handle that event before we layout 163 if (self.wants_focus) |wants_focus| { 164 try focus_handler.focusWidget(&ctx, wants_focus); 165 try self.handleCommand(&ctx.cmds); 166 self.wants_focus = null; 167 } 168 169 // Check if we should quit 170 if (ctx.quit) return; 171 172 // Check if we need a redraw 173 if (!ctx.redraw) continue; 174 ctx.redraw = false; 175 // Clear the arena. 176 _ = arena.reset(.free_all); 177 // Assert that we have handled all commands 178 assert(ctx.cmds.items.len == 0); 179 180 const surface: vxfw.Surface = blk: { 181 // Draw the root widget 182 const surface = try self.doLayout(widget, &arena); 183 184 // Check if any hover or mouse effects changed 185 try mouse_handler.updateMouse(self, surface, &ctx); 186 // Our focus may have changed. Handle that here 187 if (self.wants_focus) |wants_focus| { 188 try focus_handler.focusWidget(&ctx, wants_focus); 189 try self.handleCommand(&ctx.cmds); 190 self.wants_focus = null; 191 } 192 193 assert(ctx.cmds.items.len == 0); 194 if (!ctx.redraw) break :blk surface; 195 // If updating the mouse required a redraw, we do the layout again 196 break :blk try self.doLayout(widget, &arena); 197 }; 198 199 // Store the last frame 200 mouse_handler.last_frame = surface; 201 // Update the focus handler list 202 try focus_handler.update(self.allocator, surface); 203 try self.render(surface, focus_handler.focused_widget); 204 } 205} 206 207fn doLayout( 208 self: *App, 209 widget: vxfw.Widget, 210 arena: *std.heap.ArenaAllocator, 211) !vxfw.Surface { 212 const vx = &self.vx; 213 214 const draw_context: vxfw.DrawContext = .{ 215 .arena = arena.allocator(), 216 .min = .{ .width = 0, .height = 0 }, 217 .max = .{ 218 .width = @intCast(vx.screen.width), 219 .height = @intCast(vx.screen.height), 220 }, 221 .cell_size = .{ 222 .width = vx.screen.width_pix / vx.screen.width, 223 .height = vx.screen.height_pix / vx.screen.height, 224 }, 225 }; 226 return widget.draw(draw_context); 227} 228 229fn render( 230 self: *App, 231 surface: vxfw.Surface, 232 focused_widget: vxfw.Widget, 233) !void { 234 const vx = &self.vx; 235 const tty = &self.tty; 236 237 const win = vx.window(); 238 win.clear(); 239 win.hideCursor(); 240 win.setCursorShape(.default); 241 242 const root_win = win.child(.{ 243 .width = surface.size.width, 244 .height = surface.size.height, 245 }); 246 surface.render(root_win, focused_widget); 247 248 try vx.render(tty.writer()); 249} 250 251fn addTick(self: *App, tick: vxfw.Tick) Allocator.Error!void { 252 try self.timers.append(self.allocator, tick); 253 std.sort.insertion(vxfw.Tick, self.timers.items, {}, vxfw.Tick.lessThan); 254} 255 256fn handleCommand(self: *App, cmds: *vxfw.CommandList) Allocator.Error!void { 257 defer cmds.clearRetainingCapacity(); 258 for (cmds.items) |cmd| { 259 switch (cmd) { 260 .tick => |tick| try self.addTick(tick), 261 .set_mouse_shape => |shape| self.vx.setMouseShape(shape), 262 .request_focus => |widget| self.wants_focus = widget, 263 .copy_to_clipboard => |content| { 264 defer self.allocator.free(content); 265 self.vx.copyToSystemClipboard(self.tty.writer(), content, self.allocator) catch |err| { 266 switch (err) { 267 error.OutOfMemory => return Allocator.Error.OutOfMemory, 268 else => std.log.err("copy error: {}", .{err}), 269 } 270 }; 271 }, 272 .set_title => |title| { 273 defer self.allocator.free(title); 274 self.vx.setTitle(self.tty.writer(), title) catch |err| { 275 std.log.err("set_title error: {}", .{err}); 276 }; 277 }, 278 .queue_refresh => self.vx.queueRefresh(), 279 .notify => |notification| { 280 self.vx.notify(self.tty.writer(), notification.title, notification.body) catch |err| { 281 std.log.err("notify error: {}", .{err}); 282 }; 283 const alloc = self.allocator; 284 if (notification.title) |title| { 285 alloc.free(title); 286 } 287 alloc.free(notification.body); 288 }, 289 .query_color => |kind| { 290 self.vx.queryColor(self.tty.writer(), kind) catch |err| { 291 std.log.err("queryColor error: {}", .{err}); 292 }; 293 }, 294 } 295 } 296} 297 298fn checkTimers(self: *App, ctx: *vxfw.EventContext) anyerror!void { 299 const now_ms = std.time.milliTimestamp(); 300 301 // timers are always sorted descending 302 while (self.timers.pop()) |tick| { 303 if (now_ms < tick.deadline_ms) { 304 // re-add the timer 305 try self.timers.append(self.allocator, tick); 306 break; 307 } 308 try tick.widget.handleEvent(ctx, .tick); 309 } 310 try self.handleCommand(&ctx.cmds); 311} 312 313const MouseHandler = struct { 314 last_frame: vxfw.Surface, 315 last_hit_list: []vxfw.HitResult, 316 mouse: ?vaxis.Mouse, 317 318 fn init(root: Widget) MouseHandler { 319 return .{ 320 .last_frame = .{ 321 .size = .{ .width = 0, .height = 0 }, 322 .widget = root, 323 .buffer = &.{}, 324 .children = &.{}, 325 }, 326 .last_hit_list = &.{}, 327 .mouse = null, 328 }; 329 } 330 331 fn deinit(self: MouseHandler, gpa: Allocator) void { 332 gpa.free(self.last_hit_list); 333 } 334 335 fn updateMouse( 336 self: *MouseHandler, 337 app: *App, 338 surface: vxfw.Surface, 339 ctx: *vxfw.EventContext, 340 ) anyerror!void { 341 const mouse = self.mouse orelse return; 342 // For mouse events we store the last frame and use that for hit testing 343 const last_frame = surface; 344 345 var hits = std.ArrayList(vxfw.HitResult){}; 346 defer hits.deinit(app.allocator); 347 const sub: vxfw.SubSurface = .{ 348 .origin = .{ .row = 0, .col = 0 }, 349 .surface = last_frame, 350 .z_index = 0, 351 }; 352 const mouse_point: vxfw.Point = .{ 353 .row = @intCast(mouse.row), 354 .col = @intCast(mouse.col), 355 }; 356 if (sub.containsPoint(mouse_point)) { 357 try last_frame.hitTest(app.allocator, &hits, mouse_point); 358 } 359 360 // We store the hit list from the last mouse event to determine mouse_enter and mouse_leave 361 // events. If list a is the previous hit list, and list b is the current hit list: 362 // - Widgets in a but not in b get a mouse_leave event 363 // - Widgets in b but not in a get a mouse_enter event 364 // - Widgets in both receive nothing 365 const a = self.last_hit_list; 366 const b = hits.items; 367 368 // Find widgets in a but not b 369 for (a) |a_item| { 370 const a_widget = a_item.widget; 371 for (b) |b_item| { 372 const b_widget = b_item.widget; 373 if (a_widget.eql(b_widget)) break; 374 } else { 375 // a_item is not in b 376 try a_widget.handleEvent(ctx, .mouse_leave); 377 try app.handleCommand(&ctx.cmds); 378 } 379 } 380 381 // Widgets in b but not in a 382 for (b) |b_item| { 383 const b_widget = b_item.widget; 384 for (a) |a_item| { 385 const a_widget = a_item.widget; 386 if (b_widget.eql(a_widget)) break; 387 } else { 388 // b_item is not in a. 389 try b_widget.handleEvent(ctx, .mouse_enter); 390 try app.handleCommand(&ctx.cmds); 391 } 392 } 393 394 // Store a copy of this hit list for next frame 395 app.allocator.free(self.last_hit_list); 396 self.last_hit_list = try app.allocator.dupe(vxfw.HitResult, hits.items); 397 } 398 399 fn handleMouse(self: *MouseHandler, app: *App, ctx: *vxfw.EventContext, mouse: vaxis.Mouse) anyerror!void { 400 // For mouse events we store the last frame and use that for hit testing 401 const last_frame = self.last_frame; 402 self.mouse = mouse; 403 404 var hits = std.ArrayList(vxfw.HitResult){}; 405 defer hits.deinit(app.allocator); 406 const sub: vxfw.SubSurface = .{ 407 .origin = .{ .row = 0, .col = 0 }, 408 .surface = last_frame, 409 .z_index = 0, 410 }; 411 const mouse_point: vxfw.Point = .{ 412 .row = @intCast(mouse.row), 413 .col = @intCast(mouse.col), 414 }; 415 if (sub.containsPoint(mouse_point)) { 416 try last_frame.hitTest(app.allocator, &hits, mouse_point); 417 } 418 419 // Handle mouse_enter and mouse_leave events 420 { 421 // We store the hit list from the last mouse event to determine mouse_enter and mouse_leave 422 // events. If list a is the previous hit list, and list b is the current hit list: 423 // - Widgets in a but not in b get a mouse_leave event 424 // - Widgets in b but not in a get a mouse_enter event 425 // - Widgets in both receive nothing 426 const a = self.last_hit_list; 427 const b = hits.items; 428 429 // Find widgets in a but not b 430 for (a) |a_item| { 431 const a_widget = a_item.widget; 432 for (b) |b_item| { 433 const b_widget = b_item.widget; 434 if (a_widget.eql(b_widget)) break; 435 } else { 436 // a_item is not in b 437 try a_widget.handleEvent(ctx, .mouse_leave); 438 try app.handleCommand(&ctx.cmds); 439 } 440 } 441 442 // Widgets in b but not in a 443 for (b) |b_item| { 444 const b_widget = b_item.widget; 445 for (a) |a_item| { 446 const a_widget = a_item.widget; 447 if (b_widget.eql(a_widget)) break; 448 } else { 449 // b_item is not in a. 450 try b_widget.handleEvent(ctx, .mouse_enter); 451 try app.handleCommand(&ctx.cmds); 452 } 453 } 454 455 // Store a copy of this hit list for next frame 456 app.allocator.free(self.last_hit_list); 457 self.last_hit_list = try app.allocator.dupe(vxfw.HitResult, hits.items); 458 } 459 460 const target = hits.pop() orelse return; 461 462 // capturing phase 463 ctx.phase = .capturing; 464 for (hits.items) |item| { 465 var m_local = mouse; 466 m_local.col = item.local.col; 467 m_local.row = item.local.row; 468 try item.widget.captureEvent(ctx, .{ .mouse = m_local }); 469 try app.handleCommand(&ctx.cmds); 470 471 if (ctx.consume_event) return; 472 } 473 474 // target phase 475 ctx.phase = .at_target; 476 { 477 var m_local = mouse; 478 m_local.col = target.local.col; 479 m_local.row = target.local.row; 480 try target.widget.handleEvent(ctx, .{ .mouse = m_local }); 481 try app.handleCommand(&ctx.cmds); 482 483 if (ctx.consume_event) return; 484 } 485 486 // Bubbling phase 487 ctx.phase = .bubbling; 488 while (hits.pop()) |item| { 489 var m_local = mouse; 490 m_local.col = item.local.col; 491 m_local.row = item.local.row; 492 try item.widget.handleEvent(ctx, .{ .mouse = m_local }); 493 try app.handleCommand(&ctx.cmds); 494 495 if (ctx.consume_event) return; 496 } 497 } 498 499 /// sends .mouse_leave to all of the widgets from the last_hit_list 500 fn mouseExit(self: *MouseHandler, app: *App, ctx: *vxfw.EventContext) anyerror!void { 501 for (self.last_hit_list) |item| { 502 try item.widget.handleEvent(ctx, .mouse_leave); 503 try app.handleCommand(&ctx.cmds); 504 } 505 } 506}; 507 508/// Maintains a tree of focusable nodes. Delivers events to the currently focused node, walking up 509/// the tree until the event is handled 510const FocusHandler = struct { 511 root: Widget, 512 focused_widget: vxfw.Widget, 513 path_to_focused: std.ArrayList(Widget), 514 515 fn init(_: Allocator, root: Widget) FocusHandler { 516 return .{ 517 .root = root, 518 .focused_widget = root, 519 .path_to_focused = std.ArrayList(Widget){}, 520 }; 521 } 522 523 fn deinit(self: *FocusHandler, allocator: Allocator) void { 524 self.path_to_focused.deinit(allocator); 525 } 526 527 /// Update the focus list 528 fn update(self: *FocusHandler, allocator: Allocator, surface: vxfw.Surface) Allocator.Error!void { 529 // clear path 530 self.path_to_focused.clearAndFree(allocator); 531 532 // Find the path to the focused widget. This builds a list that has the first element as the 533 // focused widget, and walks backward to the root. It's possible our focused widget is *not* 534 // in this tree. If this is the case, we refocus to the root widget 535 _ = try self.childHasFocus(allocator, surface); 536 537 if (!self.root.eql(surface.widget)) { 538 // If the root of surface is not the initial widget, we append the initial widget 539 try self.path_to_focused.append(allocator, self.root); 540 } 541 542 // reverse path_to_focused so that it is root first 543 std.mem.reverse(Widget, self.path_to_focused.items); 544 } 545 546 /// Returns true if a child of surface is the focused widget 547 fn childHasFocus( 548 self: *FocusHandler, 549 allocator: Allocator, 550 surface: vxfw.Surface, 551 ) Allocator.Error!bool { 552 // Check if we are the focused widget 553 if (self.focused_widget.eql(surface.widget)) { 554 try self.path_to_focused.append(allocator, surface.widget); 555 return true; 556 } 557 for (surface.children) |child| { 558 // Add child to list if it is the focused widget or one of it's own children is 559 if (try self.childHasFocus(allocator, child.surface)) { 560 try self.path_to_focused.append(allocator, surface.widget); 561 return true; 562 } 563 } 564 return false; 565 } 566 567 fn focusWidget(self: *FocusHandler, ctx: *vxfw.EventContext, widget: vxfw.Widget) anyerror!void { 568 // Focusing a widget requires it to have an event handler 569 assert(widget.eventHandler != null); 570 if (self.focused_widget.eql(widget)) return; 571 572 ctx.phase = .at_target; 573 try self.focused_widget.handleEvent(ctx, .focus_out); 574 self.focused_widget = widget; 575 try self.focused_widget.handleEvent(ctx, .focus_in); 576 } 577 578 fn handleEvent(self: *FocusHandler, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 579 const path = self.path_to_focused.items; 580 assert(path.len > 0); 581 582 // Capturing phase. We send capture events from the root to the target (inclusive of target) 583 ctx.phase = .capturing; 584 for (path) |widget| { 585 try widget.captureEvent(ctx, event); 586 if (ctx.consume_event) return; 587 } 588 589 // Target phase. This is only sent to the target 590 ctx.phase = .at_target; 591 const target = self.path_to_focused.getLast(); 592 try target.handleEvent(ctx, event); 593 if (ctx.consume_event) return; 594 595 // Bubbling phase. Bubbling phase moves from target (exclusive) to the root 596 ctx.phase = .bubbling; 597 const target_idx = path.len - 1; 598 var iter = std.mem.reverseIterator(path[0..target_idx]); 599 while (iter.next()) |widget| { 600 try widget.handleEvent(ctx, event); 601 if (ctx.consume_event) return; 602 } 603 } 604};