tangled
alpha
login
or
join now
rockorager.dev
/
comlink
2
fork
atom
an experimental irc client
2
fork
atom
overview
issues
pulls
pipelines
ui: show most irc messages in network tab
rockorager.dev
1 year ago
17687522
50c0afc4
verified
This commit was signed with the committer's
known signature
.
rockorager.dev
SSH Key Fingerprint:
SHA256:qn/Fjy7CpbcogGEPB14Y53hLnQleZNFY9lkQnuudFLs=
+397
-9
1 changed file
expand all
collapse all
unified
split
src
irc.zig
+397
-9
src/irc.zig
···
33
/// Number of failed pings before we consider the connection failed
34
const keepalive_retries: i32 = 3;
35
0
0
0
36
pub const Buffer = union(enum) {
37
client: *Client,
38
channel: *Channel,
···
71
MARKREAD,
72
NOTICE,
73
PART,
0
74
PRIVMSG,
75
TAGMSG,
76
···
108
.{ "MARKREAD", .MARKREAD },
109
.{ "NOTICE", .NOTICE },
110
.{ "PART", .PART },
0
111
.{ "PRIVMSG", .PRIVMSG },
112
.{ "TAGMSG", .TAGMSG },
113
});
···
168
completer_shown: bool = false,
169
typing_last_active: u32 = 0,
170
typing_last_sent: u32 = 0,
171
-
172
-
// Gutter (left side where time is printed) width
173
-
const gutter_width = 6;
174
175
pub const Member = struct {
176
user: *User,
···
1386
return msg;
1387
}
1388
0
0
0
0
0
0
0
1389
pub const ParamIterator = struct {
1390
params: ?[]const u8,
1391
index: usize = 0,
···
1673
completer_shown: bool,
1674
1675
list_modal: ListModal,
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1676
1677
pub fn init(
1678
self: *Client,
···
1700
.text_field = .init(alloc, app.unicode),
1701
.completer_shown = false,
1702
.list_modal = undefined,
0
1703
};
1704
self.list_modal.init(alloc, self);
1705
self.text_field.style = .{ .bg = self.app.blendBg(10) };
···
1752
self.channels.deinit();
1753
1754
self.list_modal.deinit(self.alloc);
0
0
0
0
1755
1756
var user_iter = self.users.valueIterator();
1757
while (user_iter.next()) |user| {
···
1828
1829
var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena);
1830
{
0
0
0
0
0
0
0
0
0
0
0
0
1831
// Draw the character limit. 14 is length of message overhead "PRIVMSG :\r\n"
1832
const max_limit = 510;
1833
const limit = try std.fmt.allocPrint(
···
1999
const msg = Message.init(line);
2000
const client = self;
2001
switch (msg.command()) {
2002
-
.unknown => {},
0
0
0
0
2003
.CAP => {
0
0
2004
// syntax: <client> <ACK/NACK> :caps
2005
var iter = msg.paramIterator();
2006
_ = iter.next() orelse return; // client
···
2056
}
2057
},
2058
.RPL_WELCOME => {
0
0
2059
const now = try zeit.instant(.{});
2060
var now_buf: [30]u8 = undefined;
2061
const now_fmt = try now.time().bufPrint(&now_buf, .rfc3339);
···
2074
// on_connect callback
2075
try lua.onConnect(self.app.lua, client);
2076
},
2077
-
.RPL_YOURHOST => {},
2078
-
.RPL_CREATED => {},
2079
-
.RPL_MYINFO => {},
0
0
0
0
0
0
0
0
0
2080
.RPL_ISUPPORT => {
0
0
2081
// syntax: <client> <token>[ <token>] :are supported
2082
var iter = msg.paramIterator();
2083
_ = iter.next() orelse return; // client
···
2095
}
2096
}
2097
},
2098
-
.RPL_LOGGEDIN => {},
0
0
0
2099
.RPL_TOPIC => {
2100
// syntax: <client> <channel> :<topic>
2101
var iter = msg.paramIterator();
···
2110
channel.topic = try self.alloc.dupe(u8, topic);
2111
},
2112
.RPL_TRYAGAIN => {
0
0
2113
if (self.list_modal.expecting_response) {
2114
self.list_modal.expecting_response = false;
2115
try self.list_modal.finish(ctx);
···
2126
try self.list_modal.addMessage(self.alloc, msg);
2127
},
2128
.RPL_LISTEND => try self.list_modal.finish(ctx),
2129
-
.RPL_SASLSUCCESS => {},
0
0
0
2130
.RPL_WHOREPLY => {
2131
// syntax: <client> <channel> <username> <host> <server> <nick> <flags> :<hopcount> <real name>
2132
var iter = msg.paramIterator();
···
2224
ctx.redraw = true;
2225
},
2226
.BOUNCER => {
0
0
2227
var iter = msg.paramIterator();
2228
while (iter.next()) |param| {
2229
if (mem.eql(u8, param, "NETWORK")) {
···
2780
);
2781
channel.history_requested = true;
2782
},
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
2783
}
2784
}
2785
};
···
33
/// Number of failed pings before we consider the connection failed
34
const keepalive_retries: i32 = 3;
35
36
+
// Gutter (left side where time is printed) width
37
+
const gutter_width = 6;
38
+
39
pub const Buffer = union(enum) {
40
client: *Client,
41
channel: *Channel,
···
74
MARKREAD,
75
NOTICE,
76
PART,
77
+
PONG,
78
PRIVMSG,
79
TAGMSG,
80
···
112
.{ "MARKREAD", .MARKREAD },
113
.{ "NOTICE", .NOTICE },
114
.{ "PART", .PART },
115
+
.{ "PONG", .PONG },
116
.{ "PRIVMSG", .PRIVMSG },
117
.{ "TAGMSG", .TAGMSG },
118
});
···
173
completer_shown: bool = false,
174
typing_last_active: u32 = 0,
175
typing_last_sent: u32 = 0,
0
0
0
176
177
pub const Member = struct {
178
user: *User,
···
1388
return msg;
1389
}
1390
1391
+
pub fn dupe(self: Message, alloc: std.mem.Allocator) Allocator.Error!Message {
1392
+
return .{
1393
+
.bytes = try alloc.dupe(u8, self.bytes),
1394
+
.timestamp_s = self.timestamp_s,
1395
+
};
1396
+
}
1397
+
1398
pub const ParamIterator = struct {
1399
params: ?[]const u8,
1400
index: usize = 0,
···
1682
completer_shown: bool,
1683
1684
list_modal: ListModal,
1685
+
messages: std.ArrayListUnmanaged(Message),
1686
+
scroll: struct {
1687
+
/// Line offset from the bottom message
1688
+
offset: u16 = 0,
1689
+
/// Message offset into the list of messages. We use this to lock the viewport if we have a
1690
+
/// scroll. Otherwise, when offset == 0 this is effectively ignored (and should be 0)
1691
+
msg_offset: ?usize = null,
1692
+
1693
+
/// Pending scroll we have to handle while drawing. This could be up or down. By convention
1694
+
/// we say positive is a scroll up.
1695
+
pending: i17 = 0,
1696
+
} = .{},
1697
+
can_scroll_up: bool = false,
1698
+
message_view: struct {
1699
+
mouse: ?vaxis.Mouse = null,
1700
+
hovered_message: ?Message = null,
1701
+
} = .{},
1702
1703
pub fn init(
1704
self: *Client,
···
1726
.text_field = .init(alloc, app.unicode),
1727
.completer_shown = false,
1728
.list_modal = undefined,
1729
+
.messages = .empty,
1730
};
1731
self.list_modal.init(alloc, self);
1732
self.text_field.style = .{ .bg = self.app.blendBg(10) };
···
1779
self.channels.deinit();
1780
1781
self.list_modal.deinit(self.alloc);
1782
+
for (self.messages.items) |msg| {
1783
+
self.alloc.free(msg.bytes);
1784
+
}
1785
+
self.messages.deinit(self.alloc);
1786
1787
var user_iter = self.users.valueIterator();
1788
while (user_iter.next()) |user| {
···
1859
1860
var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena);
1861
{
1862
+
const message_view_ctx = ctx.withConstraints(ctx.min, .{
1863
+
.height = max.height - 2,
1864
+
.width = max.width,
1865
+
});
1866
+
const s = try self.drawMessageView(message_view_ctx);
1867
+
try children.append(.{
1868
+
.origin = .{ .col = 0, .row = 0 },
1869
+
.surface = s,
1870
+
});
1871
+
}
1872
+
1873
+
{
1874
// Draw the character limit. 14 is length of message overhead "PRIVMSG :\r\n"
1875
const max_limit = 510;
1876
const limit = try std.fmt.allocPrint(
···
2042
const msg = Message.init(line);
2043
const client = self;
2044
switch (msg.command()) {
2045
+
.unknown => {
2046
+
const msg2 = try msg.dupe(self.alloc);
2047
+
try self.messages.append(self.alloc, msg2);
2048
+
},
2049
+
.PONG => {},
2050
.CAP => {
2051
+
const msg2 = try msg.dupe(self.alloc);
2052
+
try self.messages.append(self.alloc, msg2);
2053
// syntax: <client> <ACK/NACK> :caps
2054
var iter = msg.paramIterator();
2055
_ = iter.next() orelse return; // client
···
2105
}
2106
},
2107
.RPL_WELCOME => {
2108
+
const msg2 = try msg.dupe(self.alloc);
2109
+
try self.messages.append(self.alloc, msg2);
2110
const now = try zeit.instant(.{});
2111
var now_buf: [30]u8 = undefined;
2112
const now_fmt = try now.time().bufPrint(&now_buf, .rfc3339);
···
2125
// on_connect callback
2126
try lua.onConnect(self.app.lua, client);
2127
},
2128
+
.RPL_YOURHOST => {
2129
+
const msg2 = try msg.dupe(self.alloc);
2130
+
try self.messages.append(self.alloc, msg2);
2131
+
},
2132
+
.RPL_CREATED => {
2133
+
const msg2 = try msg.dupe(self.alloc);
2134
+
try self.messages.append(self.alloc, msg2);
2135
+
},
2136
+
.RPL_MYINFO => {
2137
+
const msg2 = try msg.dupe(self.alloc);
2138
+
try self.messages.append(self.alloc, msg2);
2139
+
},
2140
.RPL_ISUPPORT => {
2141
+
const msg2 = try msg.dupe(self.alloc);
2142
+
try self.messages.append(self.alloc, msg2);
2143
// syntax: <client> <token>[ <token>] :are supported
2144
var iter = msg.paramIterator();
2145
_ = iter.next() orelse return; // client
···
2157
}
2158
}
2159
},
2160
+
.RPL_LOGGEDIN => {
2161
+
const msg2 = try msg.dupe(self.alloc);
2162
+
try self.messages.append(self.alloc, msg2);
2163
+
},
2164
.RPL_TOPIC => {
2165
// syntax: <client> <channel> :<topic>
2166
var iter = msg.paramIterator();
···
2175
channel.topic = try self.alloc.dupe(u8, topic);
2176
},
2177
.RPL_TRYAGAIN => {
2178
+
const msg2 = try msg.dupe(self.alloc);
2179
+
try self.messages.append(self.alloc, msg2);
2180
if (self.list_modal.expecting_response) {
2181
self.list_modal.expecting_response = false;
2182
try self.list_modal.finish(ctx);
···
2193
try self.list_modal.addMessage(self.alloc, msg);
2194
},
2195
.RPL_LISTEND => try self.list_modal.finish(ctx),
2196
+
.RPL_SASLSUCCESS => {
2197
+
const msg2 = try msg.dupe(self.alloc);
2198
+
try self.messages.append(self.alloc, msg2);
2199
+
},
2200
.RPL_WHOREPLY => {
2201
// syntax: <client> <channel> <username> <host> <server> <nick> <flags> :<hopcount> <real name>
2202
var iter = msg.paramIterator();
···
2294
ctx.redraw = true;
2295
},
2296
.BOUNCER => {
2297
+
const msg2 = try msg.dupe(self.alloc);
2298
+
try self.messages.append(self.alloc, msg2);
2299
var iter = msg.paramIterator();
2300
while (iter.next()) |param| {
2301
if (mem.eql(u8, param, "NETWORK")) {
···
2852
);
2853
channel.history_requested = true;
2854
},
2855
+
}
2856
+
}
2857
+
2858
+
fn messageViewWidget(self: *Client) vxfw.Widget {
2859
+
return .{
2860
+
.userdata = self,
2861
+
.eventHandler = Client.handleMessageViewEvent,
2862
+
.drawFn = Client.typeErasedDrawMessageView,
2863
+
};
2864
+
}
2865
+
2866
+
fn handleMessageViewEvent(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
2867
+
const self: *Client = @ptrCast(@alignCast(ptr));
2868
+
switch (event) {
2869
+
.mouse => |mouse| {
2870
+
if (self.message_view.mouse) |last_mouse| {
2871
+
// We need to redraw if the column entered the gutter
2872
+
if (last_mouse.col >= gutter_width and mouse.col < gutter_width)
2873
+
ctx.redraw = true
2874
+
// Or if the column exited the gutter
2875
+
else if (last_mouse.col < gutter_width and mouse.col >= gutter_width)
2876
+
ctx.redraw = true
2877
+
// Or if the row changed
2878
+
else if (last_mouse.row != mouse.row)
2879
+
ctx.redraw = true
2880
+
// Or if we did a middle click, and now released it
2881
+
else if (last_mouse.button == .middle)
2882
+
ctx.redraw = true;
2883
+
} else {
2884
+
// If we didn't have the mouse previously, we redraw
2885
+
ctx.redraw = true;
2886
+
}
2887
+
2888
+
// Save this mouse state for when we draw
2889
+
self.message_view.mouse = mouse;
2890
+
2891
+
// A middle press on a hovered message means we copy the content
2892
+
if (mouse.type == .press and
2893
+
mouse.button == .middle and
2894
+
self.message_view.hovered_message != null)
2895
+
{
2896
+
const msg = self.message_view.hovered_message orelse unreachable;
2897
+
try ctx.copyToClipboard(msg.bytes);
2898
+
return ctx.consumeAndRedraw();
2899
+
}
2900
+
if (mouse.button == .wheel_down) {
2901
+
self.scroll.pending -|= 1;
2902
+
ctx.consume_event = true;
2903
+
ctx.redraw = true;
2904
+
}
2905
+
if (mouse.button == .wheel_up) {
2906
+
self.scroll.pending +|= 1;
2907
+
ctx.consume_event = true;
2908
+
ctx.redraw = true;
2909
+
}
2910
+
if (self.scroll.pending != 0) {
2911
+
try self.doScroll(ctx);
2912
+
}
2913
+
},
2914
+
.mouse_leave => {
2915
+
self.message_view.mouse = null;
2916
+
self.message_view.hovered_message = null;
2917
+
ctx.redraw = true;
2918
+
},
2919
+
.tick => {
2920
+
try self.doScroll(ctx);
2921
+
},
2922
+
else => {},
2923
+
}
2924
+
}
2925
+
2926
+
fn typeErasedDrawMessageView(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
2927
+
const self: *Client = @ptrCast(@alignCast(ptr));
2928
+
return self.drawMessageView(ctx);
2929
+
}
2930
+
2931
+
fn drawMessageView(self: *Client, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
2932
+
self.message_view.hovered_message = null;
2933
+
const max = ctx.max.size();
2934
+
if (max.width == 0 or max.height == 0 or self.messages.items.len == 0) {
2935
+
return .{
2936
+
.size = max,
2937
+
.widget = self.messageViewWidget(),
2938
+
.buffer = &.{},
2939
+
.children = &.{},
2940
+
};
2941
+
}
2942
+
2943
+
var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena);
2944
+
2945
+
// Row is the row we are printing on. We add the offset to achieve our scroll location
2946
+
var row: i17 = max.height + self.scroll.offset;
2947
+
// Message offset
2948
+
const offset = self.scroll.msg_offset orelse self.messages.items.len;
2949
+
2950
+
const messages = self.messages.items[0..offset];
2951
+
var iter = std.mem.reverseIterator(messages);
2952
+
2953
+
assert(messages.len > 0);
2954
+
// Initialize sender and maybe_instant to the last message values
2955
+
const last_msg = iter.next() orelse unreachable;
2956
+
// Reset iter index
2957
+
iter.index += 1;
2958
+
var this_instant = last_msg.localTime(&self.app.tz);
2959
+
2960
+
while (iter.next()) |msg| {
2961
+
// Break if we have gone past the top of the screen
2962
+
if (row < 0) break;
2963
+
2964
+
// Get the server time for the *next* message. We'll use this to decide printing of
2965
+
// username and time
2966
+
const maybe_next_instant: ?zeit.Instant = blk: {
2967
+
const next_msg = iter.next() orelse break :blk null;
2968
+
// Fix the index of the iterator
2969
+
iter.index += 1;
2970
+
break :blk next_msg.localTime(&self.app.tz);
2971
+
};
2972
+
2973
+
defer {
2974
+
// After this loop, we want to save these values for the next iteration
2975
+
if (maybe_next_instant) |next_instant| {
2976
+
this_instant = next_instant;
2977
+
}
2978
+
}
2979
+
2980
+
// Draw the message so we have it's wrapped height
2981
+
const text: vxfw.Text = .{ .text = msg.bytes };
2982
+
const child_ctx = ctx.withConstraints(
2983
+
.{ .width = max.width -| gutter_width, .height = 1 },
2984
+
.{ .width = max.width -| gutter_width, .height = null },
2985
+
);
2986
+
const surface = try text.draw(child_ctx);
2987
+
2988
+
// See if our message contains the mouse. We'll highlight it if it does
2989
+
const message_has_mouse: bool = blk: {
2990
+
const mouse = self.message_view.mouse orelse break :blk false;
2991
+
break :blk mouse.col >= gutter_width and
2992
+
mouse.row < row and
2993
+
mouse.row >= row - surface.size.height;
2994
+
};
2995
+
2996
+
if (message_has_mouse) {
2997
+
const last_mouse = self.message_view.mouse orelse unreachable;
2998
+
// If we had a middle click, we highlight yellow to indicate we copied the text
2999
+
const bg: vaxis.Color = if (last_mouse.button == .middle and last_mouse.type == .press)
3000
+
.{ .index = 3 }
3001
+
else
3002
+
.{ .index = 8 };
3003
+
// Set the style for the entire message
3004
+
for (surface.buffer) |*cell| {
3005
+
cell.style.bg = bg;
3006
+
}
3007
+
// Create a surface to highlight the entire area under the message
3008
+
const hl_surface = try vxfw.Surface.init(
3009
+
ctx.arena,
3010
+
text.widget(),
3011
+
.{ .width = max.width -| gutter_width, .height = surface.size.height },
3012
+
);
3013
+
const base: vaxis.Cell = .{ .style = .{ .bg = bg } };
3014
+
@memset(hl_surface.buffer, base);
3015
+
3016
+
try children.append(.{
3017
+
.origin = .{ .row = row - surface.size.height, .col = gutter_width },
3018
+
.surface = hl_surface,
3019
+
});
3020
+
3021
+
self.message_view.hovered_message = msg;
3022
+
}
3023
+
3024
+
// Adjust the row we print on for the wrapped height of this message
3025
+
row -= surface.size.height;
3026
+
try children.append(.{
3027
+
.origin = .{ .row = row, .col = gutter_width },
3028
+
.surface = surface,
3029
+
});
3030
+
3031
+
var style: vaxis.Style = .{ .dim = true };
3032
+
// The time text we will print
3033
+
const buf: []const u8 = blk: {
3034
+
const time = this_instant.time();
3035
+
// Check our next time. If *this* message occurs on a different day, we want to
3036
+
// print the date
3037
+
if (maybe_next_instant) |next_instant| {
3038
+
const next_time = next_instant.time();
3039
+
if (time.day != next_time.day) {
3040
+
style = .{};
3041
+
break :blk try std.fmt.allocPrint(
3042
+
ctx.arena,
3043
+
"{d:0>2}/{d:0>2}",
3044
+
.{ @intFromEnum(time.month), time.day },
3045
+
);
3046
+
}
3047
+
}
3048
+
3049
+
// if it is the first message, we also want to print the date
3050
+
if (iter.index == 0) {
3051
+
style = .{};
3052
+
break :blk try std.fmt.allocPrint(
3053
+
ctx.arena,
3054
+
"{d:0>2}/{d:0>2}",
3055
+
.{ @intFromEnum(time.month), time.day },
3056
+
);
3057
+
}
3058
+
3059
+
// Otherwise, we print clock time
3060
+
break :blk try std.fmt.allocPrint(
3061
+
ctx.arena,
3062
+
"{d:0>2}:{d:0>2}",
3063
+
.{ time.hour, time.minute },
3064
+
);
3065
+
};
3066
+
3067
+
const time_text: vxfw.Text = .{
3068
+
.text = buf,
3069
+
.style = style,
3070
+
.softwrap = false,
3071
+
};
3072
+
const time_ctx = ctx.withConstraints(
3073
+
.{ .width = 0, .height = 1 },
3074
+
.{ .width = max.width -| gutter_width, .height = null },
3075
+
);
3076
+
try children.append(.{
3077
+
.origin = .{ .row = row, .col = 0 },
3078
+
.surface = try time_text.draw(time_ctx),
3079
+
});
3080
+
}
3081
+
3082
+
// Set the can_scroll_up flag. this is true if we drew past the top of the screen
3083
+
self.can_scroll_up = row <= 0;
3084
+
if (row > 0) {
3085
+
row -= 1;
3086
+
// If we didn't draw past the top of the screen, we must have reached the end of
3087
+
// history. Draw an indicator letting the user know this
3088
+
const bot = "━";
3089
+
var writer = try std.ArrayList(u8).initCapacity(ctx.arena, bot.len * max.width);
3090
+
try writer.writer().writeBytesNTimes(bot, max.width);
3091
+
3092
+
const border: vxfw.Text = .{
3093
+
.text = writer.items,
3094
+
.style = .{ .fg = .{ .index = 8 } },
3095
+
.softwrap = false,
3096
+
};
3097
+
const border_ctx = ctx.withConstraints(.{}, .{ .height = 1, .width = max.width });
3098
+
3099
+
const unread: vxfw.SubSurface = .{
3100
+
.origin = .{ .col = 0, .row = row },
3101
+
.surface = try border.draw(border_ctx),
3102
+
};
3103
+
3104
+
try children.append(unread);
3105
+
const no_more_history: vxfw.Text = .{
3106
+
.text = " Perhaps the archives are incomplete ",
3107
+
.style = .{ .fg = .{ .index = 8 } },
3108
+
.softwrap = false,
3109
+
};
3110
+
const no_history_surf = try no_more_history.draw(border_ctx);
3111
+
const new_sub: vxfw.SubSurface = .{
3112
+
.origin = .{ .col = (max.width -| no_history_surf.size.width) / 2, .row = row },
3113
+
.surface = no_history_surf,
3114
+
};
3115
+
try children.append(new_sub);
3116
+
}
3117
+
return .{
3118
+
.size = max,
3119
+
.widget = self.messageViewWidget(),
3120
+
.buffer = &.{},
3121
+
.children = children.items,
3122
+
};
3123
+
}
3124
+
3125
+
/// Consumes any pending scrolls and schedules another tick if needed
3126
+
fn doScroll(self: *Client, ctx: *vxfw.EventContext) anyerror!void {
3127
+
defer {
3128
+
// At the end of this function, we anchor our msg_offset if we have any amount of
3129
+
// scroll. This prevents new messages from automatically scrolling us
3130
+
if (self.scroll.offset > 0 and self.scroll.msg_offset == null) {
3131
+
self.scroll.msg_offset = @intCast(self.messages.items.len);
3132
+
}
3133
+
// If we have no offset, we reset our anchor
3134
+
if (self.scroll.offset == 0) {
3135
+
self.scroll.msg_offset = null;
3136
+
}
3137
+
}
3138
+
const animation_tick: u32 = 30;
3139
+
// No pending scroll. Return early
3140
+
if (self.scroll.pending == 0) return;
3141
+
3142
+
// Scroll up
3143
+
if (self.scroll.pending > 0) {
3144
+
// Check if we can scroll up. If we can't, we are done
3145
+
if (!self.can_scroll_up) {
3146
+
self.scroll.pending = 0;
3147
+
return;
3148
+
}
3149
+
// Consume 1 line, and schedule a tick
3150
+
self.scroll.offset += 1;
3151
+
self.scroll.pending -= 1;
3152
+
ctx.redraw = true;
3153
+
return ctx.tick(animation_tick, self.messageViewWidget());
3154
+
}
3155
+
3156
+
// From here, we only scroll down. First, we check if we are at the bottom already. If we
3157
+
// are, we have nothing to do
3158
+
if (self.scroll.offset == 0) {
3159
+
// Already at bottom. Nothing to do
3160
+
self.scroll.pending = 0;
3161
+
return;
3162
+
}
3163
+
3164
+
// Scroll down
3165
+
if (self.scroll.pending < 0) {
3166
+
// Consume 1 line, and schedule a tick
3167
+
self.scroll.offset -= 1;
3168
+
self.scroll.pending += 1;
3169
+
ctx.redraw = true;
3170
+
return ctx.tick(animation_tick, self.messageViewWidget());
3171
}
3172
}
3173
};