an experimental irc client

scroll: improve animation

rockorager.dev 123e08d3 538115cd

verified
+76 -7
+76 -7
src/irc.zig
··· 164 pending: i17 = 0, 165 } = .{}, 166 167 message_view: struct { 168 mouse: ?vaxis.Mouse = null, 169 hovered_message: ?Message = null, ··· 589 } 590 if (key.matches(vaxis.Key.page_up, .{})) { 591 self.scroll.pending += self.client.app.last_height / 2; 592 try self.doScroll(ctx); 593 return ctx.consumeAndRedraw(); 594 } 595 if (key.matches(vaxis.Key.page_down, .{})) { 596 self.scroll.pending -|= self.client.app.last_height / 2; 597 try self.doScroll(ctx); 598 return ctx.consumeAndRedraw(); 599 } 600 if (key.matches(vaxis.Key.home, .{})) { 601 self.scroll.pending -= self.scroll.offset; 602 self.scroll.msg_offset = null; 603 try self.doScroll(ctx); ··· 868 self.scroll.msg_offset = null; 869 } 870 } 871 - const animation_tick: u32 = 30; 872 // No pending scroll. Return early 873 if (self.scroll.pending == 0) return; 874 875 // Scroll up 876 if (self.scroll.pending > 0) { ··· 879 self.scroll.pending = 0; 880 return; 881 } 882 // Consume 1 line, and schedule a tick 883 - self.scroll.offset += 1; 884 - self.scroll.pending -= 1; 885 ctx.redraw = true; 886 return ctx.tick(animation_tick, self.messageViewWidget()); 887 } ··· 896 897 // Scroll down 898 if (self.scroll.pending < 0) { 899 - // Consume 1 line, and schedule a tick 900 - self.scroll.offset -= 1; 901 - self.scroll.pending += 1; 902 ctx.redraw = true; 903 return ctx.tick(animation_tick, self.messageViewWidget()); 904 } ··· 2727 pub fn connect(self: *Client) !void { 2728 if (self.config.tls) { 2729 const port: u16 = self.config.port orelse 6697; 2730 - self.stream = try std.net.tcpConnectToHost(self.alloc, self.config.server, port); 2731 self.client = try tls.client(self.stream, .{ 2732 .host = self.config.server, 2733 .root_ca = .{ .bundle = self.app.bundle }, ··· 3830 }; 3831 } 3832 }; 3833 3834 test "caseFold" { 3835 try testing.expect(caseFold("a", "A"));
··· 164 pending: i17 = 0, 165 } = .{}, 166 167 + animation_end_ms: u64 = 0, 168 + 169 message_view: struct { 170 mouse: ?vaxis.Mouse = null, 171 hovered_message: ?Message = null, ··· 591 } 592 if (key.matches(vaxis.Key.page_up, .{})) { 593 self.scroll.pending += self.client.app.last_height / 2; 594 + self.animation_end_ms = @intCast(std.time.milliTimestamp() + 200); 595 try self.doScroll(ctx); 596 return ctx.consumeAndRedraw(); 597 } 598 if (key.matches(vaxis.Key.page_down, .{})) { 599 + self.animation_end_ms = @intCast(std.time.milliTimestamp() + 200); 600 self.scroll.pending -|= self.client.app.last_height / 2; 601 try self.doScroll(ctx); 602 return ctx.consumeAndRedraw(); 603 } 604 if (key.matches(vaxis.Key.home, .{})) { 605 + self.animation_end_ms = @intCast(std.time.milliTimestamp() + 200); 606 self.scroll.pending -= self.scroll.offset; 607 self.scroll.msg_offset = null; 608 try self.doScroll(ctx); ··· 873 self.scroll.msg_offset = null; 874 } 875 } 876 // No pending scroll. Return early 877 if (self.scroll.pending == 0) return; 878 + 879 + const animation_tick: u32 = 8; 880 + const now_ms: u64 = @intCast(std.time.milliTimestamp()); 881 882 // Scroll up 883 if (self.scroll.pending > 0) { ··· 886 self.scroll.pending = 0; 887 return; 888 } 889 + 890 + // At this point, we always redraw 891 + ctx.redraw = true; 892 + 893 + // If we are past the end of the animation, or on the last tick, consume the rest of the 894 + // pending scroll 895 + if (self.animation_end_ms <= now_ms) { 896 + self.scroll.offset += @intCast(self.scroll.pending); 897 + self.scroll.pending = 0; 898 + return; 899 + } 900 + 901 + // Calculate the amount to scroll this tick. We use 8ms ticks. 902 + // Total time = end_ms - now_ms 903 + // Lines / ms = self.scroll.pending / total time 904 + // Lines this tick = 8 ms * lines / ms 905 + // All together: (8 ms * self.scroll.pending ) / (end_ms - now_ms) 906 + const delta_scroll = (@as(u64, animation_tick) * @as(u64, @intCast(self.scroll.pending))) / 907 + (self.animation_end_ms - now_ms); 908 + 909 + // Ensure we always scroll at least one line 910 + const resolved_scroll = @max(1, delta_scroll); 911 + 912 // Consume 1 line, and schedule a tick 913 + self.scroll.offset += @intCast(resolved_scroll); 914 + self.scroll.pending -|= @intCast(resolved_scroll); 915 ctx.redraw = true; 916 return ctx.tick(animation_tick, self.messageViewWidget()); 917 } ··· 926 927 // Scroll down 928 if (self.scroll.pending < 0) { 929 + const pending: u16 = @intCast(@abs(self.scroll.pending)); 930 + 931 + // At this point, we always redraw 932 + ctx.redraw = true; 933 + 934 + // If we are past the end of the animation, or on the last tick, consume the rest of the 935 + // pending scroll 936 + if (self.animation_end_ms <= now_ms) { 937 + self.scroll.offset -|= pending; 938 + self.scroll.pending = 0; 939 + return; 940 + } 941 + 942 + // Calculate the amount to scroll this tick. We use 8ms ticks. 943 + // Total time = end_ms - now_ms 944 + // Lines / ms = self.scroll.pending / total time 945 + // Lines this tick = 8 ms * lines / ms 946 + // All together: (8 ms * self.scroll.pending ) / (end_ms - now_ms) 947 + const delta_scroll = (@as(u64, animation_tick) * @as(u64, @intCast(pending))) / 948 + (self.animation_end_ms - now_ms); 949 + 950 + // Ensure we always scroll at least one line 951 + const resolved_scroll = @max(1, delta_scroll); 952 + self.scroll.offset -|= @intCast(resolved_scroll); 953 + self.scroll.pending += @intCast(resolved_scroll); 954 ctx.redraw = true; 955 return ctx.tick(animation_tick, self.messageViewWidget()); 956 } ··· 2779 pub fn connect(self: *Client) !void { 2780 if (self.config.tls) { 2781 const port: u16 = self.config.port orelse 6697; 2782 + self.stream = try tcpConnectToHost(self.alloc, self.config.server, port); 2783 self.client = try tls.client(self.stream, .{ 2784 .host = self.config.server, 2785 .root_ca = .{ .bundle = self.app.bundle }, ··· 3882 }; 3883 } 3884 }; 3885 + 3886 + /// All memory allocated with `allocator` will be freed before this function returns. 3887 + pub fn tcpConnectToHost(allocator: mem.Allocator, name: []const u8, port: u16) std.net.TcpConnectToHostError!std.net.Stream { 3888 + const list = try std.net.getAddressList(allocator, name, port); 3889 + defer list.deinit(); 3890 + 3891 + if (list.addrs.len == 0) return error.UnknownHostName; 3892 + 3893 + for (list.addrs) |addr| { 3894 + const stream = std.net.tcpConnectToAddress(addr) catch |err| { 3895 + log.warn("error connecting to host: {}", .{err}); 3896 + continue; 3897 + }; 3898 + return stream; 3899 + } 3900 + return std.posix.ConnectError.ConnectionRefused; 3901 + } 3902 3903 test "caseFold" { 3904 try testing.expect(caseFold("a", "A"));