an experimental irc client

scroll: improve animation

rockorager.dev 123e08d3 538115cd

verified
+76 -7
+76 -7
src/irc.zig
··· 164 164 pending: i17 = 0, 165 165 } = .{}, 166 166 167 + animation_end_ms: u64 = 0, 168 + 167 169 message_view: struct { 168 170 mouse: ?vaxis.Mouse = null, 169 171 hovered_message: ?Message = null, ··· 589 591 } 590 592 if (key.matches(vaxis.Key.page_up, .{})) { 591 593 self.scroll.pending += self.client.app.last_height / 2; 594 + self.animation_end_ms = @intCast(std.time.milliTimestamp() + 200); 592 595 try self.doScroll(ctx); 593 596 return ctx.consumeAndRedraw(); 594 597 } 595 598 if (key.matches(vaxis.Key.page_down, .{})) { 599 + self.animation_end_ms = @intCast(std.time.milliTimestamp() + 200); 596 600 self.scroll.pending -|= self.client.app.last_height / 2; 597 601 try self.doScroll(ctx); 598 602 return ctx.consumeAndRedraw(); 599 603 } 600 604 if (key.matches(vaxis.Key.home, .{})) { 605 + self.animation_end_ms = @intCast(std.time.milliTimestamp() + 200); 601 606 self.scroll.pending -= self.scroll.offset; 602 607 self.scroll.msg_offset = null; 603 608 try self.doScroll(ctx); ··· 868 873 self.scroll.msg_offset = null; 869 874 } 870 875 } 871 - const animation_tick: u32 = 30; 872 876 // No pending scroll. Return early 873 877 if (self.scroll.pending == 0) return; 878 + 879 + const animation_tick: u32 = 8; 880 + const now_ms: u64 = @intCast(std.time.milliTimestamp()); 874 881 875 882 // Scroll up 876 883 if (self.scroll.pending > 0) { ··· 879 886 self.scroll.pending = 0; 880 887 return; 881 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 + 882 912 // Consume 1 line, and schedule a tick 883 - self.scroll.offset += 1; 884 - self.scroll.pending -= 1; 913 + self.scroll.offset += @intCast(resolved_scroll); 914 + self.scroll.pending -|= @intCast(resolved_scroll); 885 915 ctx.redraw = true; 886 916 return ctx.tick(animation_tick, self.messageViewWidget()); 887 917 } ··· 896 926 897 927 // Scroll down 898 928 if (self.scroll.pending < 0) { 899 - // Consume 1 line, and schedule a tick 900 - self.scroll.offset -= 1; 901 - self.scroll.pending += 1; 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); 902 954 ctx.redraw = true; 903 955 return ctx.tick(animation_tick, self.messageViewWidget()); 904 956 } ··· 2727 2779 pub fn connect(self: *Client) !void { 2728 2780 if (self.config.tls) { 2729 2781 const port: u16 = self.config.port orelse 6697; 2730 - self.stream = try std.net.tcpConnectToHost(self.alloc, self.config.server, port); 2782 + self.stream = try tcpConnectToHost(self.alloc, self.config.server, port); 2731 2783 self.client = try tls.client(self.stream, .{ 2732 2784 .host = self.config.server, 2733 2785 .root_ca = .{ .bundle = self.app.bundle }, ··· 3830 3882 }; 3831 3883 } 3832 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 + } 3833 3902 3834 3903 test "caseFold" { 3835 3904 try testing.expect(caseFold("a", "A"));