╭ ui.zig ───────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const xit = @import("xit"); 3 const rp = xit.repo; 4 const hash = xit.hash; 5 const xitui = xit.xitui; 6 const term = xitui.terminal; 7 const wgt = xitui.widget; 8 const layout = xitui.layout; 9 const inp = xitui.input; 10 const Grid = xitui.grid.Grid; 11 const Focus = xitui.focus.Focus; 12 const evt = @import("./event.zig"); 13 14 pub const canonical_repo_opts: rp.RepoOpts(.xit) = .{}; 15 pub const DB = rp.Repo(.xit, canonical_repo_opts).DB; 16 17 pub const Home = @import("./ui/Home.zig"); 18 19 pub const Page = union(enum) { 20 home: Home, 21 }; 22 23 // a top-level "page" the user can navigate to 24 pub const RoutablePage = enum { 25 home_users, 26 home_repos, 27 home_auth, 28 29 pub const default: RoutablePage = .home_users; 30 31 pub fn url(self: RoutablePage) []const u8 { 32 return switch (self) { 33 .home_users => "/users", 34 .home_repos => "/repos", 35 .home_auth => "/auth", 36 }; 37 } 38 39 pub fn fromUrl(path: []const u8) ?RoutablePage { 40 if (std.mem.eql(u8, path, "/")) return default; 41 if (std.mem.eql(u8, path, "/users")) return .home_users; 42 if (std.mem.eql(u8, path, "/repos")) return .home_repos; 43 if (std.mem.eql(u8, path, "/auth")) return .home_auth; 44 return null; 45 } 46 }; 47 48 // per-connection mutable state. each SSH session / web session / local TUI 49 // run gets its own 50 pub const Session = struct { 51 data: Data = .{}, // serializable data sent down to web client 52 arena: *std.heap.ArenaAllocator, 53 haxy_moment: ?DB.HashMap(.read_only) = null, // db cursor (null on the wasm side) 54 55 pub const Data = struct { 56 user_id: ?[]const u8 = null, 57 // a transient outcome to surface from the last /login POST attempt 58 login_failure: ?Home.Auth.Login.Failure = null, 59 current_page: RoutablePage = .default, 60 }; 61 62 pub fn init( 63 comptime repo_opts: rp.RepoOpts(.xit), 64 arena: *std.heap.ArenaAllocator, 65 repo: *rp.Repo(.xit, repo_opts), 66 data: Data, 67 ) !Session { 68 const RepoDB = rp.Repo(.xit, repo_opts).DB; 69 70 const history = try RepoDB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 71 72 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 73 const moment = try RepoDB.HashMap(.read_only).init(moment_cursor); 74 75 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 76 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 77 _ = try last_object_id_cursor.readBytes(&last_object_id); 78 79 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 80 const haxy = try RepoDB.ArrayList(.read_only).init(haxy_cursor); 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 selectable_list: SelectableList, 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 // a scrollable list of selectable single-line items. content is replaced 242 // via setItems, which dupes the strings into the widget's own allocation. 243 pub const SelectableList = struct { 244 scroll: wgt.Scroll(Widget), 245 lines: std.ArrayList([]const u8), 246 247 pub fn init(allocator: std.mem.Allocator) !SelectableList { 248 var self = blk: { 249 var inner_box = try wgt.Box(Widget).init(allocator, .{ .border_style = null, .rounded_corners = true, .direction = .vert }); 250 errdefer inner_box.deinit(allocator); 251 252 var scroll = try wgt.Scroll(Widget).init(allocator, .{ .box = inner_box }, .vert); 253 errdefer scroll.deinit(allocator); 254 255 break :blk SelectableList{ 256 .scroll = scroll, 257 .lines = .empty, 258 }; 259 }; 260 errdefer self.deinit(allocator); 261 return self; 262 } 263 264 pub fn deinit(self: *SelectableList, allocator: std.mem.Allocator) void { 265 self.scroll.deinit(allocator); 266 for (self.lines.items) |line| allocator.free(line); 267 self.lines.deinit(allocator); 268 } 269 270 pub fn setItems(self: *SelectableList, allocator: std.mem.Allocator, items: []const []const u8) !void { 271 const inner_box = &self.scroll.child.box; 272 273 for (inner_box.children.values()) |*child| { 274 child.widget.deinit(allocator); 275 } 276 inner_box.children.clearAndFree(allocator); 277 278 for (self.lines.items) |line| allocator.free(line); 279 self.lines.clearAndFree(allocator); 280 281 self.scroll.x = 0; 282 self.scroll.y = 0; 283 // keep the focus tree in sync with the widget tree so setFocus can 284 // descend even when the pane isn't laid out this build (e.g. narrow 285 // terminal where only the focused pane fits). build's clear+addChild 286 // would do this otherwise, but it gets skipped for unbuilt panes. 287 inner_box.getFocus().clear(); 288 inner_box.getFocus().child_id = null; 289 290 for (items) |item| { 291 const line = try allocator.dupe(u8, item); 292 { 293 errdefer allocator.free(line); 294 try self.lines.append(allocator, line); 295 } 296 297 var text_box = try wgt.TextBox(Widget).init(allocator, line, .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .none }); 298 errdefer text_box.deinit(allocator); 299 text_box.getFocus().focusable = true; 300 try inner_box.children.put(allocator, text_box.getFocus().id, .{ .widget = .{ .text_box = text_box }, .rect = null, .min_size = null }); 301 try inner_box.getFocus().addChild(text_box.getFocus(), .{ .width = 0, .height = 0 }, 0, 0); 302 } 303 304 if (inner_box.children.count() > 0) { 305 inner_box.getFocus().child_id = inner_box.children.keys()[0]; 306 } 307 } 308 309 pub fn build(self: *SelectableList, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 310 self.clearGrid(); 311 const children = &self.scroll.child.box.children; 312 for (children.keys(), children.values()) |id, *item| { 313 item.widget.text_box.options.border_style = if (self.getFocus().child_id == id) 314 (if (root_focus.grandchild_id == id) .double else .single) 315 else 316 .hidden; 317 } 318 try self.scroll.build(allocator, constraint, root_focus); 319 } 320 321 pub fn input(self: *SelectableList, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 322 _ = allocator; 323 if (self.getFocus().child_id) |child_id| { 324 const children = &self.scroll.child.box.children; 325 if (children.getIndex(child_id)) |current_index| { 326 var index = current_index; 327 328 switch (key) { 329 .arrow_up => { 330 index -|= 1; 331 }, 332 .arrow_down => { 333 if (index + 1 < children.count()) { 334 index += 1; 335 } 336 }, 337 .home => { 338 index = 0; 339 }, 340 .end => { 341 if (children.count() > 0) { 342 index = children.count() - 1; 343 } 344 }, 345 .page_up => { 346 if (self.getGrid()) |grid| { 347 const half_count = (grid.size.height / 3) / 2; 348 index -|= half_count; 349 } 350 }, 351 .page_down => { 352 if (self.getGrid()) |grid| { 353 if (children.count() > 0) { 354 const half_count = (grid.size.height / 3) / 2; 355 index = @min(index + half_count, children.count() - 1); 356 } 357 } 358 }, 359 .mouse => |mouse| switch (mouse.action) { 360 .scroll => |dir| switch (dir) { 361 .up => index -|= 1, 362 .down => if (index + 1 < children.count()) { 363 index += 1; 364 }, 365 }, 366 else => {}, 367 }, 368 else => {}, 369 } 370 371 if (index != current_index) { 372 try root_focus.setFocus(children.keys()[index]); 373 self.updateScroll(index); 374 } 375 } 376 } 377 } 378 379 pub fn clearGrid(self: *SelectableList) void { 380 self.scroll.clearGrid(); 381 } 382 383 pub fn getGrid(self: SelectableList) ?Grid { 384 return self.scroll.getGrid(); 385 } 386 387 pub fn getFocus(self: *SelectableList) *Focus { 388 return self.scroll.getFocus(); 389 } 390 391 pub fn getSelectedIndex(self: SelectableList) ?usize { 392 if (self.scroll.child.box.focus.child_id) |child_id| { 393 const children = &self.scroll.child.box.children; 394 return children.getIndex(child_id); 395 } else { 396 return null; 397 } 398 } 399 400 fn updateScroll(self: *SelectableList, index: usize) void { 401 const inner_box = &self.scroll.child.box; 402 if (inner_box.children.values()[index].rect) |rect| { 403 self.scroll.scrollToRect(rect); 404 } 405 } 406 }; 407 408 // an invisible widget that fills the horizontal space granted by its parent. 409 // used inside Box(horiz) with a min_size so the box reserves space for the 410 // children that follow, pushing them to the right. 411 pub const Spacer = struct { 412 focus: Focus, 413 grid: ?Grid, 414 415 pub fn init(allocator: std.mem.Allocator) Spacer { 416 return .{ 417 .focus = Focus.init(allocator, .container), 418 .grid = null, 419 }; 420 } 421 422 pub fn deinit(self: *Spacer, allocator: std.mem.Allocator) void { 423 _ = allocator; 424 self.focus.deinit(); 425 if (self.grid) |*grid| { 426 grid.deinit(); 427 self.grid = null; 428 } 429 } 430 431 pub fn build(self: *Spacer, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 432 _ = root_focus; 433 self.clearGrid(); 434 const width = constraint.max_size.width orelse return; 435 if (width == 0) return; 436 self.grid = try Grid.init(allocator, .{ .width = width, .height = 1 }); 437 } 438 439 pub fn input(self: *Spacer, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 440 _ = self; 441 _ = allocator; 442 _ = key; 443 _ = root_focus; 444 } 445 446 pub fn clearGrid(self: *Spacer) void { 447 if (self.grid) |*grid| { 448 grid.deinit(); 449 self.grid = null; 450 } 451 } 452 453 pub fn getGrid(self: Spacer) ?Grid { 454 return self.grid; 455 } 456 457 pub fn getFocus(self: *Spacer) *Focus { 458 return &self.focus; 459 } 460 }; 461 462 // a single-child wrapper that builds the child at its natural size and 463 // positions its grid in the middle of the area granted by the parent 464 pub const Center = struct { 465 focus: Focus, 466 grid: ?Grid, 467 child: *Widget, 468 direction: Direction, 469 470 pub const Direction = enum { both, horiz, vert }; 471 472 pub fn init(allocator: std.mem.Allocator, child_widget: Widget, direction: Direction) !Center { 473 const child = try allocator.create(Widget); 474 errdefer allocator.destroy(child); 475 child.* = child_widget; 476 return .{ 477 .focus = Focus.init(allocator, .container), 478 .grid = null, 479 .child = child, 480 .direction = direction, 481 }; 482 } 483 484 pub fn deinit(self: *Center, allocator: std.mem.Allocator) void { 485 self.focus.deinit(); 486 if (self.grid) |*grid| { 487 grid.deinit(); 488 self.grid = null; 489 } 490 self.child.deinit(allocator); 491 allocator.destroy(self.child); 492 } 493 494 pub fn build(self: *Center, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 495 self.clearGrid(); 496 self.getFocus().clear(); 497 498 // build the child without forcing it to fill min_size; it sizes 499 // itself to its content within the available max. 500 try self.child.build(allocator, .{ 501 .min_size = .{ .width = null, .height = null }, 502 .max_size = constraint.max_size, 503 }, root_focus); 504 505 const child_grid = self.child.getGrid() orelse return; 506 507 // prefer max when given; otherwise grow to max(min, child) so that 508 // when only a min is set (e.g. the wasm path passes viewport rows 509 // as min_height with no max), we can still vertically center while 510 // letting taller content extend past the min. 511 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; 512 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; 513 if (width == 0 or height == 0) return; 514 515 const offset_x: usize = if (self.direction == .horiz or self.direction == .both) 516 (width -| child_grid.size.width) / 2 517 else 518 0; 519 const offset_y: usize = if (self.direction == .vert or self.direction == .both) 520 (height -| child_grid.size.height) / 2 521 else 522 0; 523 524 var grid = try Grid.init(allocator, .{ .width = width, .height = height }); 525 errdefer grid.deinit(); 526 try grid.drawGrid(child_grid, offset_x, offset_y); 527 try self.getFocus().addChild(self.child.getFocus(), child_grid.size, offset_x, offset_y); 528 self.getFocus().child_id = self.child.getFocus().id; 529 530 self.grid = grid; 531 } 532 533 pub fn input(self: *Center, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 534 try self.child.input(allocator, key, root_focus); 535 } 536 537 pub fn clearGrid(self: *Center) void { 538 if (self.grid) |*grid| { 539 grid.deinit(); 540 self.grid = null; 541 } 542 self.child.clearGrid(); 543 } 544 545 pub fn getGrid(self: Center) ?Grid { 546 return self.grid; 547 } 548 549 pub fn getFocus(self: *Center) *Focus { 550 return &self.focus; 551 } 552 }; 553