╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ ui.zig ───────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
81 82 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 83 const haxy_moments = try RepoDB.HashMap(.read_only).init(haxy_moments_cursor); 84 85 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 86 const haxy_moment = try RepoDB.HashMap(.read_only).init(haxy_moment_cursor); 87 88 return .{ 89 .data = data, 90 .arena = arena, 91 .haxy_moment = haxy_moment, 92 }; 93 } 94 }; 95 96 // what the server hands to the client (and what main_wasm parses on _start). 97 // keeps Page free of any per-request session state. 98 pub const Snapshot = struct { 99 page: Page, 100 session: Session.Data = .{}, 101 }; 102 103 pub fn run(io: std.Io, allocator: std.mem.Allocator, page: *const Page, session: *Session) !void { 104 var root = try initRoot(allocator, page, session); 105 defer root.deinit(allocator); 106 107 var terminal = try term.Terminal.init(io, allocator); 108 defer terminal.deinit(io); 109 110 var last_size = layout.Size{ .width = 0, .height = 0 }; 111 var last_grid = try Grid.init(allocator, last_size); 112 defer last_grid.deinit(); 113 114 while (!terminal.shouldQuit()) { 115 const grid_changed = try terminal.render(&root, &last_grid, &last_size); 116 117 // process any inputs. 118 // 119 // if the grid didn't change, then first do a blocking 120 // read, so the thread will sleep until further input. 121 // after that, all remaining reads are non-blocking so 122 // we can process the rest of the queued inputs. 123 // 124 // if the grid *did* change, then only do non-blocking 125 // reads. we do not want to sleep the thread because 126 // there may be an animation that requires more looping. 127 var blocking = !grid_changed; 128 while (try terminal.readKey(io, blocking)) |key| { 129 try inputKey(allocator, &root, key, &terminal); 130 blocking = false; 131 } 132 133 try root.build(allocator, .{ 134 .min_size = .{ .width = null, .height = null }, 135 .max_size = .{ .width = last_size.width, .height = last_size.height }, 136 }, root.getFocus()); 137 } 138 } 139 140 pub fn inputKey(allocator: std.mem.Allocator, root: *Widget, key: inp.Key, terminal: anytype) !void { 141 switch (key) { 142 .escape => terminal.requestQuit(), 143 .mouse => |mouse| { 144 if (mouse.action == .press and mouse.action.press == .left) { 145 const root_focus = root.getFocus(); 146 var iter = root_focus.children.iterator(); 147 while (iter.next()) |entry| { 148 const child = entry.value_ptr.*; 149 if (!child.focus.focusable) continue; 150 const r = child.rect; 151 if (mouse.x >= r.x and mouse.y >= r.y and 152 mouse.x < r.x + r.size.width and mouse.y < r.y + r.size.height) 153 { 154 try root_focus.setFocus(entry.key_ptr.*); 155 break; 156 } 157 } 158 // forward the press into the widget tree so buttons (and any 159 // future click-aware widgets) can react. widgets that don't 160 // care about presses ignore it. 161 try root.input(allocator, key, root.getFocus()); 162 } else { 163 try root.input(allocator, key, root.getFocus()); 164 } 165 }, 166 else => try root.input(allocator, key, root.getFocus()), 167 } 168 } 169 170 pub fn initRoot(allocator: std.mem.Allocator, page: *const Page, session: *Session) !Widget { 171 var root: Widget = switch (page.*) { 172 .home => |*p| .{ .home = try .init(allocator, p, session) }, 173 }; 174 errdefer root.deinit(allocator); 175 176 try root.build(allocator, .{ 177 .min_size = .{ .width = null, .height = null }, 178 .max_size = .{ .width = 80, .height = null }, 179 }, root.getFocus()); 180 if (root.getFocus().child_id) |child_id| { 181 try root.getFocus().setFocus(child_id); 182 } 183 184 return root; 185 } 186 187 pub const Widget = union(enum) { 188 text: wgt.Text(Widget), 189 box: wgt.Box(Widget), 190 text_box: wgt.TextBox(Widget), 191 text_input: wgt.TextInput(Widget), 192 scroll: wgt.Scroll(Widget), 193 stack: wgt.Stack(Widget), 194 flow_box: FlowBox, 195 spacer: Spacer, 196 center: Center, 197 home: Home.View, 198 home_header: Home.Header.View, 199 home_users: Home.Users.View, 200 home_repos: Home.Repos.View, 201 home_auth_tab: Home.Header.AuthTab.View, 202 home_auth: Home.Auth.View, 203 204 pub fn deinit(self: *Widget, allocator: std.mem.Allocator) void { 205 switch (self.*) { 206 inline else => |*case| case.deinit(allocator), 207 } 208 } 209 210 pub fn build(self: *Widget, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) anyerror!void { 211 switch (self.*) { 212 inline else => |*case| try case.build(allocator, constraint, root_focus), 213 } 214 } 215 216 pub fn input(self: *Widget, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) anyerror!void { 217 switch (self.*) { 218 inline else => |*case| try case.input(allocator, key, root_focus), 219 } 220 } 221 222 pub fn clearGrid(self: *Widget) void { 223 switch (self.*) { 224 inline else => |*case| case.clearGrid(), 225 } 226 } 227 228 pub fn getGrid(self: Widget) ?Grid { 229 switch (self) { 230 inline else => |*case| return case.getGrid(), 231 } 232 } 233 234 pub fn getFocus(self: *Widget) *Focus { 235 switch (self.*) { 236 inline else => |*case| return case.getFocus(), 237 } 238 } 239 }; 240 241 pub const FlowBox = struct { 242 focus: Focus, 243 grid: ?Grid, 244 text_boxes: std.ArrayList(wgt.TextBox(Widget)), 245 lines: std.ArrayList([]const u8), 246 // column count from the last build — FlowBox.Scroll.input uses it so arrow 247 // up/down can step by a row's worth of items. 248 last_cols: usize, 249 options: Options, 250 251 const border_rows: usize = 2; 252 253 pub const Options = struct { 254 cell_width: usize = 40, 255 cell_height: usize = 3, 256 }; 257 258 pub fn init(options: Options) FlowBox { 259 return .{ 260 .focus = Focus.init(.container), 261 .grid = null, 262 .text_boxes = .empty, 263 .lines = .empty, 264 .last_cols = 1, 265 .options = options, 266 }; 267 } 268 269 pub fn deinit(self: *FlowBox, allocator: std.mem.Allocator) void { 270 self.focus.deinit(allocator); 271 if (self.grid) |*grid| { 272 grid.deinit(); 273 self.grid = null; 274 } 275 for (self.text_boxes.items) |*tb| tb.deinit(allocator); 276 self.text_boxes.deinit(allocator); 277 for (self.lines.items) |line| allocator.free(line); 278 self.lines.deinit(allocator); 279 } 280 281 pub fn setItems(self: *FlowBox, allocator: std.mem.Allocator, items: []const []const u8) !void { 282 for (self.text_boxes.items) |*tb| tb.deinit(allocator); 283 self.text_boxes.clearAndFree(allocator); 284 285 for (self.lines.items) |line| allocator.free(line); 286 self.lines.clearAndFree(allocator); 287 288 self.focus.clear(); 289 self.focus.child_id = null; 290 291 for (items) |item| { 292 const line = try allocator.dupe(u8, item); 293 { 294 errdefer allocator.free(line); 295 try self.lines.append(allocator, line); 296 } 297 298 var text_box = try wgt.TextBox(Widget).init(allocator, line, .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .word }); 299 errdefer text_box.deinit(allocator); 300 text_box.getFocus().focusable = true; 301 try self.text_boxes.append(allocator, text_box); 302 } 303 304 if (self.text_boxes.items.len > 0) { 305 self.focus.child_id = self.text_boxes.items[0].getFocus().id; 306 } 307 } 308 309 pub fn build(self: *FlowBox, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 310 self.clearGrid(); 311 self.focus.clear(); 312 313 const cell_width = self.options.cell_width; 314 const max_width = constraint.max_size.width orelse cell_width; 315 const cols = if (cell_width == 0) 1 else @max(1, max_width / cell_width); 316 self.last_cols = cols; 317 318 const count = self.text_boxes.items.len; 319 if (count == 0) return; 320 const slot_height = self.options.cell_height + border_rows; 321 if (slot_height == 0) return; 322 323 // build at the slot size so every text box fits a single grid cell 324 for (self.text_boxes.items) |*tb| { 325 tb.options.border_style = if (self.focus.child_id == tb.getFocus().id) .single else .hidden; 326 try tb.build(allocator, .{ 327 .min_size = .{ .width = cell_width, .height = null }, 328 .max_size = .{ .width = cell_width, .height = slot_height }, 329 }, root_focus); 330 } 331 332 const rows = (count + cols - 1) / cols; 333 const content_height = rows * slot_height; 334 const total_width = cols * cell_width; 335 if (total_width == 0 or content_height == 0) return; 336 337 var grid = try Grid.init(allocator, .{ .width = total_width, .height = content_height }); 338 errdefer grid.deinit(); 339 340 for (self.text_boxes.items, 0..) |*tb, i| { 341 const tb_grid = tb.getGrid() orelse continue; 342 const col = i % cols; 343 const row = i / cols; 344 const cell_x = col * cell_width; 345 const cell_y = row * slot_height; 346 try self.focus.addChild(allocator, tb.getFocus(), tb_grid.size, cell_x, cell_y); 347 try grid.drawGrid(tb_grid, cell_x, cell_y); 348 } 349 350 self.grid = grid; 351 } 352 353 pub fn input(self: *FlowBox, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 354 _ = self; 355 _ = allocator; 356 _ = key; 357 _ = root_focus; 358 } 359 360 pub fn clearGrid(self: *FlowBox) void { 361 if (self.grid) |*grid| { 362 grid.deinit(); 363 self.grid = null; 364 } 365 for (self.text_boxes.items) |*tb| tb.clearGrid(); 366 } 367 368 pub fn getGrid(self: FlowBox) ?Grid { 369 return self.grid; 370 } 371 372 pub fn getFocus(self: *FlowBox) *Focus { 373 return &self.focus; 374 } 375 376 pub fn cellRect(self: FlowBox, index: usize) ?layout.IRect { 377 if (self.last_cols == 0 or index >= self.text_boxes.items.len) return null; 378 const slot_height = self.options.cell_height + border_rows; 379 const col = index % self.last_cols; 380 const row = index / self.last_cols; 381 return .{ 382 .x = @intCast(col * self.options.cell_width), 383 .y = @intCast(row * slot_height), 384 .size = .{ .width = self.options.cell_width, .height = slot_height }, 385 }; 386 } 387 388 pub fn indexOfFocusId(self: FlowBox, focus_id: usize) ?usize { 389 for (self.text_boxes.items, 0..) |tb, i| { 390 if (tb.box.focus.id == focus_id) return i; 391 } 392 return null; 393 } 394 395 pub const Scroll = struct { 396 scroll: wgt.Scroll(Widget), 397 398 pub fn init(allocator: std.mem.Allocator, options: FlowBox.Options) !Scroll { 399 var layout_inner = FlowBox.init(options); 400 errdefer layout_inner.deinit(allocator); 401 var scroll = try wgt.Scroll(Widget).init(allocator, .{ .flow_box = layout_inner }, .vert); 402 errdefer scroll.deinit(allocator); 403 return .{ .scroll = scroll }; 404 } 405 406 pub fn deinit(self: *Scroll, allocator: std.mem.Allocator) void { 407 self.scroll.deinit(allocator); 408 } 409 410 pub fn setItems(self: *Scroll, allocator: std.mem.Allocator, items: []const []const u8) !void { 411 self.scroll.x = 0; 412 self.scroll.y = 0; 413 try self.inner().setItems(allocator, items); 414 } 415 416 pub fn build(self: *Scroll, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 417 try self.scroll.build(allocator, constraint, root_focus); 418 } 419 420 pub fn input(self: *Scroll, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 421 _ = allocator; 422 const in = self.inner(); 423 const child_id = in.focus.child_id orelse return; 424 const current_index = in.indexOfFocusId(child_id) orelse return; 425 const count = in.text_boxes.items.len; 426 if (count == 0) return; 427 const cols = in.last_cols; 428 const slot_height = in.options.cell_height + FlowBox.border_rows; 429 430 var index = current_index; 431 switch (key) { 432 .arrow_up => index -|= cols, 433 .arrow_down => if (index + cols < count) { 434 index += cols; 435 }, 436 .arrow_left => index -|= 1, 437 .arrow_right => if (index + 1 < count) { 438 index += 1; 439 }, 440 .home => index = 0, 441 .end => index = count - 1, 442 .page_up => { 443 if (self.scroll.grid) |grid| if (slot_height > 0) { 444 const rows_per_page = grid.size.height / slot_height; 445 index -|= rows_per_page * cols; 446 }; 447 }, 448 .page_down => { 449 if (self.scroll.grid) |grid| if (slot_height > 0) { 450 const rows_per_page = grid.size.height / slot_height; 451 index = @min(index + rows_per_page * cols, count - 1); 452 }; 453 }, 454 // scroll wheel moves the focused cell by a full row so the 455 // viewport (via scrollToRect) follows in row-sized steps, 456 // matching how a scroll wheel feels in a grid view. 457 .mouse => |mouse| switch (mouse.action) { 458 .scroll => |dir| switch (dir) { 459 .up => index -|= cols, 460 .down => if (index + cols < count) { 461 index += cols; 462 }, 463 }, 464 else => {}, 465 }, 466 else => {}, 467 } 468 469 if (index != current_index) { 470 try root_focus.setFocus(in.text_boxes.items[index].getFocus().id); 471 if (in.cellRect(index)) |rect| { 472 self.scroll.scrollToRect(rect); 473 } 474 } 475 } 476 477 pub fn clearGrid(self: *Scroll) void { 478 self.scroll.clearGrid(); 479 } 480 481 pub fn getGrid(self: Scroll) ?Grid { 482 return self.scroll.getGrid(); 483 } 484 485 pub fn getFocus(self: *Scroll) *Focus { 486 return self.scroll.getFocus(); 487 } 488 489 pub fn getSelectedIndex(self: Scroll) ?usize { 490 const in = self.scroll.child.flow_box; 491 const child_id = in.focus.child_id orelse return null; 492 return in.indexOfFocusId(child_id); 493 } 494 495 fn inner(self: *Scroll) *FlowBox { 496 return &self.scroll.child.flow_box; 497 } 498 }; 499 }; 500 501 // an invisible widget that fills the horizontal space granted by its parent. 502 // used inside Box(horiz) with a min_size so the box reserves space for the 503 // children that follow, pushing them to the right. 504 pub const Spacer = struct { 505 focus: Focus, 506 grid: ?Grid, 507 508 pub fn init() Spacer { 509 return .{ 510 .focus = Focus.init(.container), 511 .grid = null, 512 }; 513 } 514 515 pub fn deinit(self: *Spacer, allocator: std.mem.Allocator) void { 516 self.focus.deinit(allocator); 517 if (self.grid) |*grid| { 518 grid.deinit(); 519 self.grid = null; 520 } 521 } 522 523 pub fn build(self: *Spacer, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 524 _ = root_focus; 525 self.clearGrid(); 526 const width = constraint.max_size.width orelse return; 527 if (width == 0) return; 528 self.grid = try Grid.init(allocator, .{ .width = width, .height = 1 }); 529 } 530 531 pub fn input(self: *Spacer, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 532 _ = self; 533 _ = allocator; 534 _ = key; 535 _ = root_focus; 536 } 537 538 pub fn clearGrid(self: *Spacer) void { 539 if (self.grid) |*grid| { 540 grid.deinit(); 541 self.grid = null; 542 } 543 } 544 545 pub fn getGrid(self: Spacer) ?Grid { 546 return self.grid; 547 } 548 549 pub fn getFocus(self: *Spacer) *Focus { 550 return &self.focus; 551 } 552 }; 553 554 // a single-child wrapper that builds the child at its natural size and 555 // positions its grid in the middle of the area granted by the parent 556 pub const Center = struct { 557 focus: Focus, 558 grid: ?Grid, 559 child: *Widget, 560 direction: Direction, 561 562 pub const Direction = enum { both, horiz, vert }; 563 564 pub fn init(allocator: std.mem.Allocator, child_widget: Widget, direction: Direction) !Center { 565 const child = try allocator.create(Widget); 566 errdefer allocator.destroy(child); 567 child.* = child_widget; 568 return .{ 569 .focus = Focus.init(.container), 570 .grid = null, 571 .child = child, 572 .direction = direction, 573 }; 574 } 575 576 pub fn deinit(self: *Center, allocator: std.mem.Allocator) void { 577 self.focus.deinit(allocator); 578 if (self.grid) |*grid| { 579 grid.deinit(); 580 self.grid = null; 581 } 582 self.child.deinit(allocator); 583 allocator.destroy(self.child); 584 } 585 586 pub fn build(self: *Center, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 587 self.clearGrid(); 588 self.getFocus().clear(); 589 590 // build the child without forcing it to fill min_size; it sizes 591 // itself to its content within the available max. 592 try self.child.build(allocator, .{ 593 .min_size = .{ .width = null, .height = null }, 594 .max_size = constraint.max_size, 595 }, root_focus); 596 597 const child_grid = self.child.getGrid() orelse return; 598 599 // prefer max when given; otherwise grow to max(min, child) so that 600 // when only a min is set (e.g. the wasm path passes viewport rows 601 // as min_height with no max), we can still vertically center while 602 // letting taller content extend past the min. 603 const width = if (constraint.max_size.width) |w| w else if (constraint.min_size.width) |min_w| @max(min_w, child_grid.size.width) else child_grid.size.width; 604 const height = if (constraint.max_size.height) |h| h else if (constraint.min_size.height) |min_h| @max(min_h, child_grid.size.height) else child_grid.size.height; 605 if (width == 0 or height == 0) return; 606 607 const offset_x: usize = if (self.direction == .horiz or self.direction == .both) 608 (width -| child_grid.size.width) / 2 609 else 610 0; 611 const offset_y: usize = if (self.direction == .vert or self.direction == .both) 612 (height -| child_grid.size.height) / 2 613 else 614 0; 615 616 var grid = try Grid.init(allocator, .{ .width = width, .height = height }); 617 errdefer grid.deinit(); 618 try grid.drawGrid(child_grid, offset_x, offset_y); 619 try self.getFocus().addChild(allocator, self.child.getFocus(), child_grid.size, offset_x, offset_y); 620 self.getFocus().child_id = self.child.getFocus().id; 621 622 self.grid = grid; 623 } 624 625 pub fn input(self: *Center, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 626 try self.child.input(allocator, key, root_focus); 627 } 628 629 pub fn clearGrid(self: *Center) void { 630 if (self.grid) |*grid| { 631 grid.deinit(); 632 self.grid = null; 633 } 634 self.child.clearGrid(); 635 } 636 637 pub fn getGrid(self: Center) ?Grid { 638 return self.grid; 639 } 640 641 pub fn getFocus(self: *Center) *Focus { 642 return &self.focus; 643 } 644 }; 645