an experimental irc client

ui: flesh out channel view

+163 -38
+17 -30
src/app.zig
··· 83 write_queue: comlink.WriteQueue, 84 write_thread: std.Thread, 85 86 - lhs: vxfw.SplitView, 87 - rhs: vxfw.SplitView, 88 buffer_list: vxfw.ListView, 89 90 /// initialize vaxis, lua state 91 - pub fn init(self: *App, gpa: std.mem.Allocator) !void { 92 self.* = .{ 93 .alloc = gpa, 94 .state = .{}, ··· 100 .lua = undefined, 101 .write_queue = .{}, 102 .write_thread = undefined, 103 - .lhs = .{ 104 .width = self.state.buffers.width, 105 .lhs = self.buffer_list.widget(), 106 - .rhs = self.rhs.widget(), 107 - }, 108 - .rhs = .{ 109 - .width = self.state.members.width, 110 - .constrain = .rhs, 111 - .lhs = self.contentWidget(), 112 - .rhs = self.memberWidget(), 113 }, 114 .explicit_join = false, 115 .bundle = .{}, ··· 125 }, 126 .draw_cursor = false, 127 }, 128 }; 129 130 self.lua = try Lua.init(&self.alloc); ··· 233 234 fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 235 const self: *App = @ptrCast(@alignCast(ptr)); 236 237 var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena); 238 _ = &children; ··· 248 249 const sub: vxfw.SubSurface = .{ 250 .origin = .{ .col = 0, .row = 0 }, 251 - .surface = try self.lhs.widget().draw(ctx), 252 }; 253 try children.append(sub); 254 ··· 260 }; 261 } 262 263 - fn bufferWidget(self: *App) vxfw.Widget { 264 - return .{ 265 - .userdata = self, 266 - .captureHandler = null, 267 - .eventHandler = null, 268 - .drawFn = App.typeErasedBufferDrawFn, 269 - }; 270 - } 271 - 272 fn bufferBuilderFn(ptr: *const anyopaque, idx: usize, cursor: usize) ?vxfw.Widget { 273 const self: *const App = @ptrCast(@alignCast(ptr)); 274 var i: usize = 0; ··· 281 } 282 } 283 return null; 284 - } 285 - 286 - fn typeErasedBufferDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 287 - const self: *App = @ptrCast(@alignCast(ptr)); 288 - _ = self; 289 - const text: vxfw.Text = .{ .text = "buffers" }; 290 - return text.draw(ctx); 291 } 292 293 fn contentWidget(self: *App) vxfw.Widget { ··· 1154 pub fn selectedBuffer(self: *App) ?irc.Buffer { 1155 var i: usize = 0; 1156 for (self.clients.items) |client| { 1157 - if (i == self.state.buffers.selected_idx) return .{ .client = client }; 1158 i += 1; 1159 for (client.channels.items) |channel| { 1160 - if (i == self.state.buffers.selected_idx) return .{ .channel = channel }; 1161 i += 1; 1162 } 1163 }
··· 83 write_queue: comlink.WriteQueue, 84 write_thread: std.Thread, 85 86 + view: vxfw.SplitView, 87 buffer_list: vxfw.ListView, 88 + unicode: *const vaxis.Unicode, 89 + 90 + const default_rhs: vxfw.Text = .{ .text = "TODO: update this text" }; 91 92 /// initialize vaxis, lua state 93 + pub fn init(self: *App, gpa: std.mem.Allocator, unicode: *const vaxis.Unicode) !void { 94 self.* = .{ 95 .alloc = gpa, 96 .state = .{}, ··· 102 .lua = undefined, 103 .write_queue = .{}, 104 .write_thread = undefined, 105 + .view = .{ 106 .width = self.state.buffers.width, 107 .lhs = self.buffer_list.widget(), 108 + .rhs = default_rhs.widget(), 109 }, 110 .explicit_join = false, 111 .bundle = .{}, ··· 121 }, 122 .draw_cursor = false, 123 }, 124 + .unicode = unicode, 125 }; 126 127 self.lua = try Lua.init(&self.alloc); ··· 230 231 fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 232 const self: *App = @ptrCast(@alignCast(ptr)); 233 + if (self.selectedBuffer()) |buffer| { 234 + switch (buffer) { 235 + .client => |client| self.view.rhs = client.view(), 236 + .channel => |channel| self.view.rhs = channel.view.widget(), 237 + } 238 + } else self.view.rhs = default_rhs.widget(); 239 240 var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena); 241 _ = &children; ··· 251 252 const sub: vxfw.SubSurface = .{ 253 .origin = .{ .col = 0, .row = 0 }, 254 + .surface = try self.view.widget().draw(ctx), 255 }; 256 try children.append(sub); 257 ··· 263 }; 264 } 265 266 fn bufferBuilderFn(ptr: *const anyopaque, idx: usize, cursor: usize) ?vxfw.Widget { 267 const self: *const App = @ptrCast(@alignCast(ptr)); 268 var i: usize = 0; ··· 275 } 276 } 277 return null; 278 } 279 280 fn contentWidget(self: *App) vxfw.Widget { ··· 1141 pub fn selectedBuffer(self: *App) ?irc.Buffer { 1142 var i: usize = 0; 1143 for (self.clients.items) |client| { 1144 + if (i == self.buffer_list.cursor) return .{ .client = client }; 1145 i += 1; 1146 for (client.channels.items) |channel| { 1147 + if (i == self.buffer_list.cursor) return .{ .channel = channel }; 1148 i += 1; 1149 } 1150 }
+145 -7
src/irc.zig
··· 119 120 has_mouse: bool = false, 121 122 pub const Member = struct { 123 user: *User, 124 ··· 133 else 134 std.ascii.orderIgnoreCase(lhs.user.nick, rhs.user.nick).compare(.lt); 135 } 136 }; 137 138 - pub fn deinit(self: *const Channel, alloc: std.mem.Allocator) void { 139 alloc.free(self.name); 140 self.members.deinit(); 141 if (self.topic) |topic| { ··· 145 alloc.free(msg.bytes); 146 } 147 self.messages.deinit(); 148 } 149 150 pub fn compare(_: void, lhs: *Channel, rhs: *Channel) bool { ··· 188 try ctx.setMouseShape(.pointer); 189 if (mouse.type == .press and mouse.button == .left) { 190 self.client.app.selectBuffer(.{ .channel = self }); 191 return ctx.consumeAndRedraw(); 192 } 193 }, ··· 283 time_tag, 284 }, 285 ); 286 } 287 }; 288 ··· 630 } 631 batches.deinit(); 632 self.fifo.deinit(); 633 } 634 635 pub fn nameWidget(self: *Client, selected: bool) vxfw.Widget { ··· 1350 if (caseFold(name, channel.name)) return channel; 1351 } 1352 const channel = try self.alloc.create(Channel); 1353 - channel.* = .{ 1354 - .name = try self.alloc.dupe(u8, name), 1355 - .members = std.ArrayList(Channel.Member).init(self.alloc), 1356 - .messages = std.ArrayList(Message).init(self.alloc), 1357 - .client = self, 1358 - }; 1359 try self.channels.append(channel); 1360 1361 std.sort.insertion(*Channel, self.channels.items, {}, Channel.compare);
··· 119 120 has_mouse: bool = false, 121 122 + view: vxfw.SplitView, 123 + message_view: vxfw.ListView, 124 + member_view: vxfw.ListView, 125 + text_field: vxfw.TextField, 126 + 127 pub const Member = struct { 128 user: *User, 129 ··· 138 else 139 std.ascii.orderIgnoreCase(lhs.user.nick, rhs.user.nick).compare(.lt); 140 } 141 + 142 + pub fn widget(self: *Member) vxfw.Widget { 143 + return .{ 144 + .userdata = self, 145 + .drawFn = Member.draw, 146 + }; 147 + } 148 + 149 + pub fn draw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 150 + const self: *Member = @ptrCast(@alignCast(ptr)); 151 + const style: vaxis.Style = if (self.user.away) 152 + .{ .dim = true } 153 + else 154 + .{ .fg = self.user.color }; 155 + var prefix = try ctx.arena.alloc(u8, 1); 156 + prefix[0] = self.prefix; 157 + const text: vxfw.RichText = .{ 158 + .text = &.{ 159 + .{ .text = prefix, .style = style }, 160 + .{ .text = self.user.nick, .style = style }, 161 + }, 162 + .softwrap = false, 163 + }; 164 + return text.draw(ctx); 165 + } 166 }; 167 168 + pub fn init( 169 + self: *Channel, 170 + gpa: Allocator, 171 + client: *Client, 172 + name: []const u8, 173 + unicode: *const vaxis.Unicode, 174 + ) Allocator.Error!void { 175 + self.* = .{ 176 + .name = try gpa.dupe(u8, name), 177 + .members = std.ArrayList(Channel.Member).init(gpa), 178 + .messages = std.ArrayList(Message).init(gpa), 179 + .client = client, 180 + .view = .{ 181 + .lhs = self.contentWidget(), 182 + .rhs = self.member_view.widget(), 183 + .width = 16, 184 + .constrain = .rhs, 185 + }, 186 + .message_view = .{ .children = .{ .slice = &.{} } }, 187 + .member_view = .{ 188 + .children = .{ 189 + .builder = .{ 190 + .userdata = self, 191 + .buildFn = Channel.buildMemberList, 192 + }, 193 + }, 194 + .draw_cursor = false, 195 + }, 196 + .text_field = vxfw.TextField.init(gpa, unicode), 197 + }; 198 + } 199 + 200 + pub fn deinit(self: *Channel, alloc: std.mem.Allocator) void { 201 alloc.free(self.name); 202 self.members.deinit(); 203 if (self.topic) |topic| { ··· 207 alloc.free(msg.bytes); 208 } 209 self.messages.deinit(); 210 + self.text_field.deinit(); 211 } 212 213 pub fn compare(_: void, lhs: *Channel, rhs: *Channel) bool { ··· 251 try ctx.setMouseShape(.pointer); 252 if (mouse.type == .press and mouse.button == .left) { 253 self.client.app.selectBuffer(.{ .channel = self }); 254 + try ctx.requestFocus(self.text_field.widget()); 255 return ctx.consumeAndRedraw(); 256 } 257 }, ··· 347 time_tag, 348 }, 349 ); 350 + } 351 + 352 + pub fn contentWidget(self: *Channel) vxfw.Widget { 353 + return .{ 354 + .userdata = self, 355 + .drawFn = Channel.typeErasedViewDraw, 356 + }; 357 + } 358 + 359 + fn typeErasedViewDraw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 360 + const self: *Channel = @ptrCast(@alignCast(ptr)); 361 + 362 + const max = ctx.max.size(); 363 + var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena); 364 + 365 + { 366 + // Draw the topic 367 + const topic: vxfw.Text = .{ 368 + .text = self.topic orelse "", 369 + .softwrap = false, 370 + }; 371 + 372 + const topic_sub: vxfw.SubSurface = .{ 373 + .origin = .{ .col = 0, .row = 0 }, 374 + .surface = try topic.draw(ctx), 375 + }; 376 + 377 + try children.append(topic_sub); 378 + 379 + // Draw a border below the topic 380 + const bot = "─"; 381 + var writer = try std.ArrayList(u8).initCapacity(ctx.arena, bot.len * max.width); 382 + try writer.writer().writeBytesNTimes(bot, max.width); 383 + 384 + const border: vxfw.Text = .{ 385 + .text = writer.items, 386 + .softwrap = false, 387 + }; 388 + 389 + const topic_border: vxfw.SubSurface = .{ 390 + .origin = .{ .col = 0, .row = 1 }, 391 + .surface = try border.draw(ctx), 392 + }; 393 + try children.append(topic_border); 394 + } 395 + 396 + // Draw the text field 397 + try children.append(.{ 398 + .origin = .{ .col = 0, .row = max.height - 1 }, 399 + .surface = try self.text_field.draw(ctx), 400 + }); 401 + 402 + return .{ 403 + .size = max, 404 + .widget = self.contentWidget(), 405 + .buffer = &.{}, 406 + .children = children.items, 407 + }; 408 + } 409 + 410 + fn buildMemberList(ptr: *const anyopaque, idx: usize, _: usize) ?vxfw.Widget { 411 + const self: *const Channel = @ptrCast(@alignCast(ptr)); 412 + if (idx < self.members.items.len) { 413 + return self.members.items[idx].widget(); 414 + } 415 + return null; 416 } 417 }; 418 ··· 760 } 761 batches.deinit(); 762 self.fifo.deinit(); 763 + } 764 + 765 + pub fn view(self: *Client) vxfw.Widget { 766 + return .{ 767 + .userdata = self, 768 + .drawFn = Client.typeErasedViewDraw, 769 + }; 770 + } 771 + 772 + fn typeErasedViewDraw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 773 + _ = ptr; 774 + const text: vxfw.Text = .{ .text = "content" }; 775 + return text.draw(ctx); 776 } 777 778 pub fn nameWidget(self: *Client, selected: bool) vxfw.Widget { ··· 1493 if (caseFold(name, channel.name)) return channel; 1494 } 1495 const channel = try self.alloc.create(Channel); 1496 + try channel.init(self.alloc, self, name, self.app.unicode); 1497 try self.channels.append(channel); 1498 1499 std.sort.insertion(*Channel, self.channels.items, {}, Channel.compare);
+1 -1
src/main.zig
··· 89 // defer app.deinit(); 90 91 var comlink_app: comlink.App = undefined; 92 - try comlink_app.init(gpa.allocator()); 93 defer comlink_app.deinit(); 94 95 try app.run(comlink_app.widget(), .{});
··· 89 // defer app.deinit(); 90 91 var comlink_app: comlink.App = undefined; 92 + try comlink_app.init(gpa.allocator(), &app.vx.unicode); 93 defer comlink_app.deinit(); 94 95 try app.run(comlink_app.widget(), .{});