an experimental irc client

ui: flesh out channel view

+163 -38
+17 -30
src/app.zig
··· 83 83 write_queue: comlink.WriteQueue, 84 84 write_thread: std.Thread, 85 85 86 - lhs: vxfw.SplitView, 87 - rhs: vxfw.SplitView, 86 + view: vxfw.SplitView, 88 87 buffer_list: vxfw.ListView, 88 + unicode: *const vaxis.Unicode, 89 + 90 + const default_rhs: vxfw.Text = .{ .text = "TODO: update this text" }; 89 91 90 92 /// initialize vaxis, lua state 91 - pub fn init(self: *App, gpa: std.mem.Allocator) !void { 93 + pub fn init(self: *App, gpa: std.mem.Allocator, unicode: *const vaxis.Unicode) !void { 92 94 self.* = .{ 93 95 .alloc = gpa, 94 96 .state = .{}, ··· 100 102 .lua = undefined, 101 103 .write_queue = .{}, 102 104 .write_thread = undefined, 103 - .lhs = .{ 105 + .view = .{ 104 106 .width = self.state.buffers.width, 105 107 .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(), 108 + .rhs = default_rhs.widget(), 113 109 }, 114 110 .explicit_join = false, 115 111 .bundle = .{}, ··· 125 121 }, 126 122 .draw_cursor = false, 127 123 }, 124 + .unicode = unicode, 128 125 }; 129 126 130 127 self.lua = try Lua.init(&self.alloc); ··· 233 230 234 231 fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 235 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(); 236 239 237 240 var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena); 238 241 _ = &children; ··· 248 251 249 252 const sub: vxfw.SubSurface = .{ 250 253 .origin = .{ .col = 0, .row = 0 }, 251 - .surface = try self.lhs.widget().draw(ctx), 254 + .surface = try self.view.widget().draw(ctx), 252 255 }; 253 256 try children.append(sub); 254 257 ··· 260 263 }; 261 264 } 262 265 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 266 fn bufferBuilderFn(ptr: *const anyopaque, idx: usize, cursor: usize) ?vxfw.Widget { 273 267 const self: *const App = @ptrCast(@alignCast(ptr)); 274 268 var i: usize = 0; ··· 281 275 } 282 276 } 283 277 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 278 } 292 279 293 280 fn contentWidget(self: *App) vxfw.Widget { ··· 1154 1141 pub fn selectedBuffer(self: *App) ?irc.Buffer { 1155 1142 var i: usize = 0; 1156 1143 for (self.clients.items) |client| { 1157 - if (i == self.state.buffers.selected_idx) return .{ .client = client }; 1144 + if (i == self.buffer_list.cursor) return .{ .client = client }; 1158 1145 i += 1; 1159 1146 for (client.channels.items) |channel| { 1160 - if (i == self.state.buffers.selected_idx) return .{ .channel = channel }; 1147 + if (i == self.buffer_list.cursor) return .{ .channel = channel }; 1161 1148 i += 1; 1162 1149 } 1163 1150 }
+145 -7
src/irc.zig
··· 119 119 120 120 has_mouse: bool = false, 121 121 122 + view: vxfw.SplitView, 123 + message_view: vxfw.ListView, 124 + member_view: vxfw.ListView, 125 + text_field: vxfw.TextField, 126 + 122 127 pub const Member = struct { 123 128 user: *User, 124 129 ··· 133 138 else 134 139 std.ascii.orderIgnoreCase(lhs.user.nick, rhs.user.nick).compare(.lt); 135 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 + } 136 166 }; 137 167 138 - pub fn deinit(self: *const Channel, alloc: std.mem.Allocator) void { 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 { 139 201 alloc.free(self.name); 140 202 self.members.deinit(); 141 203 if (self.topic) |topic| { ··· 145 207 alloc.free(msg.bytes); 146 208 } 147 209 self.messages.deinit(); 210 + self.text_field.deinit(); 148 211 } 149 212 150 213 pub fn compare(_: void, lhs: *Channel, rhs: *Channel) bool { ··· 188 251 try ctx.setMouseShape(.pointer); 189 252 if (mouse.type == .press and mouse.button == .left) { 190 253 self.client.app.selectBuffer(.{ .channel = self }); 254 + try ctx.requestFocus(self.text_field.widget()); 191 255 return ctx.consumeAndRedraw(); 192 256 } 193 257 }, ··· 283 347 time_tag, 284 348 }, 285 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; 286 416 } 287 417 }; 288 418 ··· 630 760 } 631 761 batches.deinit(); 632 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); 633 776 } 634 777 635 778 pub fn nameWidget(self: *Client, selected: bool) vxfw.Widget { ··· 1350 1493 if (caseFold(name, channel.name)) return channel; 1351 1494 } 1352 1495 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 - }; 1496 + try channel.init(self.alloc, self, name, self.app.unicode); 1359 1497 try self.channels.append(channel); 1360 1498 1361 1499 std.sort.insertion(*Channel, self.channels.items, {}, Channel.compare);
+1 -1
src/main.zig
··· 89 89 // defer app.deinit(); 90 90 91 91 var comlink_app: comlink.App = undefined; 92 - try comlink_app.init(gpa.allocator()); 92 + try comlink_app.init(gpa.allocator(), &app.vx.unicode); 93 93 defer comlink_app.deinit(); 94 94 95 95 try app.run(comlink_app.widget(), .{});