an experimental irc client

ui: message scrolling

+88 -8
+2 -2
build.zig.zon
··· 7 7 .hash = "1220affeb3fe37ef09411b5a213b5fdf9bb6568e9913bade204694648983a8b2776d", 8 8 }, 9 9 .vaxis = .{ 10 - .url = "git+https://github.com/rockorager/libvaxis#dbf7e0bf09118a80f7e6184cde1d8f096f82a7da", 11 - .hash = "12207fb15ef5b259a95b581e663713883b172a98855e536a4487ae224b730290b564", 10 + .url = "git+https://github.com/rockorager/libvaxis#0fb96df48ede1823107f9ea7dc9b4dc8f8399d9d", 11 + .hash = "1220f03c86f6352a1e9ee24529e7c031b0bbe802145e5fe1b83beb7c524bfcaa02ba", 12 12 }, 13 13 .zeit = .{ 14 14 .url = "git+https://github.com/rockorager/zeit?ref=main#d943bc4bfe9e18490460dfdd64f48e997065eba8",
+86 -6
src/irc.zig
··· 120 120 has_mouse: bool = false, 121 121 122 122 view: vxfw.SplitView, 123 - message_view: vxfw.ListView, 124 123 member_view: vxfw.ListView, 125 124 text_field: vxfw.TextField, 125 + 126 + scroll: struct { 127 + /// Line offset from the bottom message 128 + offset: u16 = 0, 129 + /// Message offset into the list of messages. We use this to lock the viewport if we have a 130 + /// scroll. Otherwise, when offset == 0 this is effectively ignored (and should be 0) 131 + msg_offset: u32 = 0, 132 + 133 + /// Pending scroll we have to handle while drawing. This could be up or down. By convention 134 + /// we say positive is a scroll up. 135 + pending: i17 = 0, 136 + } = .{}, 126 137 127 138 pub const Member = struct { 128 139 user: *User, ··· 183 194 .width = 16, 184 195 .constrain = .rhs, 185 196 }, 186 - .message_view = .{ .children = .{ .slice = &.{} } }, 187 197 .member_view = .{ 188 198 .children = .{ 189 199 .builder = .{ ··· 433 443 }; 434 444 } 435 445 446 + fn handleMessageViewEvent(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 447 + const self: *Channel = @ptrCast(@alignCast(ptr)); 448 + switch (event) { 449 + .mouse => |mouse| { 450 + if (mouse.button == .wheel_down) { 451 + self.scroll.pending -|= 3; 452 + ctx.consume_event = true; 453 + } 454 + if (mouse.button == .wheel_up) { 455 + self.scroll.pending +|= 3; 456 + ctx.consume_event = true; 457 + } 458 + if (self.scroll.pending != 0) { 459 + return self.doScroll(ctx); 460 + } 461 + }, 462 + .tick => try self.doScroll(ctx), 463 + else => {}, 464 + } 465 + } 466 + 467 + /// Consumes any pending scrolls and schedules another tick if needed 468 + fn doScroll(self: *Channel, ctx: *vxfw.EventContext) anyerror!void { 469 + const animation_tick: u32 = 30; 470 + // No pending scroll. Return early 471 + if (self.scroll.pending == 0) return; 472 + 473 + // Scroll up 474 + if (self.scroll.pending > 0) { 475 + // TODO: check if we need to get more history 476 + // TODO: cehck if we are at oldest, and shouldn't scroll up anymore 477 + 478 + // Consume 1 line, and schedule a tick 479 + self.scroll.offset += 1; 480 + self.scroll.pending -= 1; 481 + ctx.redraw = true; 482 + return ctx.tick(animation_tick, self.messageViewWidget()); 483 + } 484 + 485 + // From here, we only scroll down. First, we check if we are at the bottom already. If we 486 + // are, we have nothing to do 487 + if (self.scroll.offset == 0) { 488 + // Already at bottom. Nothing to do 489 + self.scroll.pending = 0; 490 + return; 491 + } 492 + 493 + // Scroll down 494 + if (self.scroll.pending < 0) { 495 + // Consume 1 line, and schedule a tick 496 + self.scroll.offset -= 1; 497 + self.scroll.pending += 1; 498 + ctx.redraw = true; 499 + return ctx.tick(animation_tick, self.messageViewWidget()); 500 + } 501 + } 502 + 503 + fn messageViewWidget(self: *Channel) vxfw.Widget { 504 + return .{ 505 + .userdata = self, 506 + .eventHandler = Channel.handleMessageViewEvent, 507 + .drawFn = Channel.typeErasedDrawMessageView, 508 + }; 509 + } 510 + 511 + fn typeErasedDrawMessageView(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 512 + const self: *Channel = @ptrCast(@alignCast(ptr)); 513 + return self.drawMessageView(ctx); 514 + } 515 + 436 516 fn drawMessageView(self: *Channel, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 437 517 const max = ctx.max.size(); 438 518 if (max.width == 0 or max.height == 0) { 439 519 return .{ 440 520 .size = max, 441 - .widget = self.contentWidget(), 521 + .widget = self.messageViewWidget(), 442 522 .buffer = &.{}, 443 523 .children = &.{}, 444 524 }; ··· 447 527 var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena); 448 528 449 529 // Row is the row we are printing on. 450 - var row: i17 = max.height; 530 + var row: i17 = max.height + self.scroll.offset; 451 531 var iter = std.mem.reverseIterator(self.messages.items); 452 532 const gutter_width = 6; 453 533 while (iter.next()) |msg| { ··· 458 538 const text: vxfw.Text = .{ .text = msg.bytes }; 459 539 const child_ctx = ctx.withConstraints( 460 540 .{ .height = 0, .width = 0 }, 461 - .{ .width = max.width - gutter_width, .height = null }, 541 + .{ .width = max.width -| gutter_width, .height = null }, 462 542 ); 463 543 const surface = try text.draw(child_ctx); 464 544 ··· 491 571 492 572 return .{ 493 573 .size = max, 494 - .widget = self.contentWidget(), 574 + .widget = self.messageViewWidget(), 495 575 .buffer = &.{}, 496 576 .children = children.items, 497 577 };