╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────╮ │1 const std = @import("std"); │ │2 const xit = @import("xit"); │ │3 +const rp = xit.repo; │ │4 const xitui = xit.xitui; │ │5 const term = xitui.terminal;│ │6 const wgt = xitui.widget; │ ╰───────────────────────────────╯ ╭───────────────────────────────────────────────────────────╮ │ 9 const Grid = xitui.grid.Grid; │ │10 const Focus = xitui.focus.Focus; │ │11 const evt = @import("./event.zig"); │ │12 + │ │13 +pub const canonical_repo_opts: rp.RepoOpts(.xit) = .{};│ │14 +pub const DB = rp.Repo(.xit, canonical_repo_opts).DB; │ │15 │ │16 pub const Home = @import("./ui/Home.zig"); │ │17 │ ╰───────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │18 pub const Page = union(enum) { │ │19 home: Home, │ │20 +}; │ │21 + │ │22 +// per-connection mutable state. each SSH session / web session / local TUI │ │23 +// run gets its own │ │24 +pub const Session = struct { │ │25 + user_id: ?[]const u8 = null, │ │26 + arena: ?*std.heap.ArenaAllocator = null, │ │27 + haxy_moment: ?DB.HashMap(.read_only) = null, │ │28 }; │ │29 │ │18 -pub fn run(io: std.Io, allocator: std.mem.Allocator, page: *const Page) !void { │ │19 - var root = try initRoot(allocator, page); │ │30 +pub fn run(io: std.Io, allocator: std.mem.Allocator, page: *const Page, session: *Session) !void {│ │31 + var root = try initRoot(allocator, page, session); │ │32 defer root.deinit(); │ │33 │ │34 var terminal = try term.Terminal.init(io, allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │64 } │ │65 } │ │66 │ │55 -// route a single decoded key into the widget tree, handling 'q'-to-quit and │ │56 -// mouse-click focus traversal along the way. shared between the tty event │ │57 -// loop (ui.run) and the SSH session event loop in serve_ssh.zig. │ │58 -// `terminal` is whatever terminal type the caller is driving — it must │ │59 -// expose requestQuit(). │ │67 pub fn inputKey(root: *Widget, key: inp.Key, terminal: anytype) !void { │ │68 switch (key) { │ │69 .codepoint => |cp| if (cp == 'q') terminal.requestQuit() else try root.input(key, root.getFocus()),│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │82 break; │ │83 } │ │84 } │ │85 + // forward the press into the widget tree so buttons (and any│ │86 + // future click-aware widgets) can react. widgets that don't │ │87 + // care about presses ignore it. │ │88 + try root.input(key, root.getFocus()); │ │89 } else { │ │90 try root.input(key, root.getFocus()); │ │91 } │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 94 } │ │ 95 } │ │ 96 │ │ 86 -pub fn initRoot(allocator: std.mem.Allocator, page: *const Page) !Widget { │ │ 97 +pub fn initRoot(allocator: std.mem.Allocator, page: *const Page, session: *Session) !Widget {│ │ 98 var root: Widget = switch (page.*) { │ │ 88 - .home => |*p| .{ .home = try Home.View.init(allocator, p) }, │ │ 99 + .home => |*p| .{ .home = try Home.View.init(allocator, p, session) }, │ │100 }; │ │101 errdefer root.deinit(); │ │102 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────╮ │115 text: wgt.Text(Widget), │ │116 box: wgt.Box(Widget), │ │117 text_box: wgt.TextBox(Widget), │ │118 + text_input: wgt.TextInput(Widget),│ │119 scroll: wgt.Scroll(Widget), │ │120 stack: wgt.Stack(Widget), │ │121 selectable_list: SelectableList, │ ╰───────────────────────────────────────────╯ ╭───────────────────────────────────────╮ │122 + spacer: Spacer, │ │123 + center: Center, │ │124 home: Home.View, │ │125 home_header: Home.Header.View,│ │126 home_users: Home.Users.View, │ ╰───────────────────────────────────────╯ ╭─────────────────────────────────────────────────╮ │127 home_repos: Home.Repos.View, │ │128 + home_auth_tab: Home.Header.AuthTab.View,│ │129 + home_auth: Home.Auth.View, │ │130 │ │131 pub fn deinit(self: *Widget) void { │ │132 switch (self.*) { │ ╰─────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │329 const inner_box = &self.scroll.child.box; │ │330 if (inner_box.children.values()[index].rect) |rect| { │ │331 self.scroll.scrollToRect(rect); │ │332 + } │ │333 + } │ │334 +}; │ │335 + │ │336 +// an invisible widget that fills the horizontal space granted by its parent. │ │337 +// used inside Box(horiz) with a min_size so the box reserves space for the │ │338 +// children that follow, pushing them to the right. │ │339 +pub const Spacer = struct { │ │340 + allocator: std.mem.Allocator, │ │341 + focus: Focus, │ │342 + grid: ?Grid, │ │343 + │ │344 + pub fn init(allocator: std.mem.Allocator) Spacer { │ │345 + return .{ │ │346 + .allocator = allocator, │ │347 + .focus = Focus.init(allocator, .container), │ │348 + .grid = null, │ │349 + }; │ │350 + } │ │351 + │ │352 + pub fn deinit(self: *Spacer) void { │ │353 + self.focus.deinit(); │ │354 + if (self.grid) |*grid| { │ │355 + grid.deinit(); │ │356 + self.grid = null; │ │357 + } │ │358 + } │ │359 + │ │360 + pub fn build(self: *Spacer, constraint: layout.Constraint, root_focus: *Focus) !void { │ │361 + _ = root_focus; │ │362 + self.clearGrid(); │ │363 + const width = constraint.max_size.width orelse return; │ │364 + if (width == 0) return; │ │365 + self.grid = try Grid.init(self.allocator, .{ .width = width, .height = 1 }); │ │366 + } │ │367 + │ │368 + pub fn input(self: *Spacer, key: inp.Key, root_focus: *Focus) !void { │ │369 + _ = self; │ │370 + _ = key; │ │371 + _ = root_focus; │ │372 + } │ │373 + │ │374 + pub fn clearGrid(self: *Spacer) void { │ │375 + if (self.grid) |*grid| { │ │376 + grid.deinit(); │ │377 + self.grid = null; │ │378 + } │ │379 + } │ │380 + │ │381 + pub fn getGrid(self: Spacer) ?Grid { │ │382 + return self.grid; │ │383 + } │ │384 + │ │385 + pub fn getFocus(self: *Spacer) *Focus { │ │386 + return &self.focus; │ │387 + } │ │388 +}; │ │389 + │ │390 +// a single-child wrapper that builds the child at its natural size and │ │391 +// positions its grid in the middle of the area granted by the parent │ │392 +pub const Center = struct { │ │393 + allocator: std.mem.Allocator, │ │394 + focus: Focus, │ │395 + grid: ?Grid, │ │396 + child: *Widget, │ │397 + direction: Direction, │ │398 + │ │399 + pub const Direction = enum { both, horiz, vert }; │ │400 + │ │401 + pub fn init(allocator: std.mem.Allocator, child_widget: Widget, direction: Direction) !Center {│ │402 + const child = try allocator.create(Widget); │ │403 + errdefer allocator.destroy(child); │ │404 + child.* = child_widget; │ │405 + return .{ │ │406 + .allocator = allocator, │ │407 + .focus = Focus.init(allocator, .container), │ │408 + .grid = null, │ │409 + .child = child, │ │410 + .direction = direction, │ │411 + }; │ │412 + } │ │413 + │ │414 + pub fn deinit(self: *Center) void { │ │415 + self.focus.deinit(); │ │416 + if (self.grid) |*grid| { │ │417 + grid.deinit(); │ │418 + self.grid = null; │ │419 } │ │420 + self.child.deinit(); │ │421 + self.allocator.destroy(self.child); │ │422 } │ │423 + │ │424 + pub fn build(self: *Center, constraint: layout.Constraint, root_focus: *Focus) !void { │ │425 + self.clearGrid(); │ │426 + self.getFocus().clear(); │ │427 + │ │428 + // build the child without forcing it to fill min_size; it sizes │ │429 + // itself to its content within the available max. │ │430 + try self.child.build(.{ │ │431 + .min_size = .{ .width = null, .height = null }, │ │432 + .max_size = constraint.max_size, │ │433 + }, root_focus); │ │434 + │ │435 + const child_grid = self.child.getGrid() orelse return; │ │436 + │ │437 + const width = constraint.max_size.width orelse child_grid.size.width; │ │438 + const height = constraint.max_size.height orelse child_grid.size.height; │ │439 + if (width == 0 or height == 0) return; │ │440 + │ │441 + const offset_x: usize = if (self.direction == .horiz or self.direction == .both) │ │442 + (width -| child_grid.size.width) / 2 │ │443 + else │ │444 + 0; │ │445 + const offset_y: usize = if (self.direction == .vert or self.direction == .both) │ │446 + (height -| child_grid.size.height) / 2 │ │447 + else │ │448 + 0; │ │449 + │ │450 + var grid = try Grid.init(self.allocator, .{ .width = width, .height = height }); │ │451 + errdefer grid.deinit(); │ │452 + try grid.drawGrid(child_grid, offset_x, offset_y); │ │453 + try self.getFocus().addChild(self.child.getFocus(), child_grid.size, offset_x, offset_y); │ │454 + self.getFocus().child_id = self.child.getFocus().id; │ │455 + │ │456 + self.grid = grid; │ │457 + } │ │458 + │ │459 + pub fn input(self: *Center, key: inp.Key, root_focus: *Focus) !void { │ │460 + try self.child.input(key, root_focus); │ │461 + } │ │462 + │ │463 + pub fn clearGrid(self: *Center) void { │ │464 + if (self.grid) |*grid| { │ │465 + grid.deinit(); │ │466 + self.grid = null; │ │467 + } │ │468 + self.child.clearGrid(); │ │469 + } │ │470 + │ │471 + pub fn getGrid(self: Center) ?Grid { │ │472 + return self.grid; │ │473 + } │ │474 + │ │475 + pub fn getFocus(self: *Center) *Focus { │ │476 + return &self.focus; │ │477 + } │ │478 }; │ │479 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯