this repo has no description
13
fork

Configure Feed

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

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