╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │ 7 const inp = xitui.input; │ │ 8 const Grid = xitui.grid.Grid; │ │ 9 const Focus = xitui.focus.Focus; │ │10 +const evt = @import("./event.zig"); │ │11 +const pg = @import("./page.zig"); │ │12 │ │11 -pub fn run(io: std.Io, allocator: std.mem.Allocator) !void { │ │12 - var root = try initRoot(allocator); │ │13 +pub fn run(io: std.Io, allocator: std.mem.Allocator, page: *const pg.Page) !void {│ │14 + var root = try initRoot(allocator, page); │ │15 defer root.deinit(); │ │16 │ │17 var terminal = try term.Terminal.init(io, allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │50 } │ │51 } │ │52 │ │51 -pub fn initRoot(allocator: std.mem.Allocator) !Widget { │ │52 - var root = Widget{ .widget_list = try WidgetList.init(allocator) }; │ │53 +pub fn initRoot(allocator: std.mem.Allocator, page: *const pg.Page) !Widget { │ │54 + var root: Widget = switch (page.*) { │ │55 + .user_repo => |*p| .{ .user_repo_view = try UserRepoView.init(allocator, p) },│ │56 + }; │ │57 errdefer root.deinit(); │ │58 │ │59 try root.build(.{ │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 96 } │ │ 97 } │ │ 98 } │ │ 99 + │ │100 +pub const Widget = union(enum) { │ │101 + text: wgt.Text(Widget), │ │102 + box: wgt.Box(Widget), │ │103 + text_box: wgt.TextBox(Widget), │ │104 + scroll: wgt.Scroll(Widget), │ │105 + selectable_list: SelectableList, │ │106 + user_repo_view: UserRepoView, │ │107 + │ │108 + pub fn deinit(self: *Widget) void { │ │109 + switch (self.*) { │ │110 + inline else => |*case| case.deinit(), │ │111 + } │ │112 + } │ │113 │ │ 96 -const WidgetList = struct { │ │114 + pub fn build(self: *Widget, constraint: layout.Constraint, root_focus: *Focus) anyerror!void { │ │115 + switch (self.*) { │ │116 + inline else => |*case| try case.build(constraint, root_focus), │ │117 + } │ │118 + } │ │119 + │ │120 + pub fn input(self: *Widget, key: inp.Key, root_focus: *Focus) anyerror!void { │ │121 + switch (self.*) { │ │122 + inline else => |*case| try case.input(key, root_focus), │ │123 + } │ │124 + } │ │125 + │ │126 + pub fn clearGrid(self: *Widget) void { │ │127 + switch (self.*) { │ │128 + inline else => |*case| case.clearGrid(), │ │129 + } │ │130 + } │ │131 + │ │132 + pub fn getGrid(self: Widget) ?Grid { │ │133 + switch (self) { │ │134 + inline else => |*case| return case.getGrid(), │ │135 + } │ │136 + } │ │137 + │ │138 + pub fn getFocus(self: *Widget) *Focus { │ │139 + switch (self.*) { │ │140 + inline else => |*case| return case.getFocus(), │ │141 + } │ │142 + } │ │143 +}; │ │144 + │ │145 +// a scrollable list of selectable single-line items. content is replaced │ │146 +// via setItems, which dupes the strings into the widget's own allocation. │ │147 +const SelectableList = struct { │ │148 allocator: std.mem.Allocator, │ │149 scroll: wgt.Scroll(Widget), │ │150 + lines: std.ArrayList([]const u8), │ │151 │ │100 - pub fn init(allocator: std.mem.Allocator) !WidgetList { │ │152 + pub fn init(allocator: std.mem.Allocator) !SelectableList { │ │153 var self = blk: { │ │102 - var inner_box = try wgt.Box(Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │154 + var inner_box = try wgt.Box(Widget).init(allocator, .{ .border_style = null, .rounded_corners = true, .direction = .vert });│ │155 errdefer inner_box.deinit(); │ │156 │ │157 var scroll = try wgt.Scroll(Widget).init(allocator, .{ .box = inner_box }, .vert); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────╮ │158 errdefer scroll.deinit(); │ │159 │ │108 - break :blk WidgetList{ │ │160 + break :blk SelectableList{ │ │161 .allocator = allocator,│ │162 .scroll = scroll, │ │163 + .lines = .empty, │ │164 }; │ │165 }; │ │166 errdefer self.deinit(); │ ╰────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │167 + return self; │ │168 + } │ │169 │ │170 + pub fn deinit(self: *SelectableList) void { │ │171 + self.scroll.deinit(); │ │172 + for (self.lines.items) |line| self.allocator.free(line); │ │173 + self.lines.deinit(self.allocator); │ │174 + } │ │175 + │ │176 + pub fn setItems(self: *SelectableList, items: []const []const u8) !void { │ │177 const inner_box = &self.scroll.child.box; │ │178 │ │117 - { │ │118 - var text_box = try wgt.TextBox(Widget).init(allocator, "this is a TextBox", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │119 - errdefer text_box.deinit(); │ │120 - text_box.getFocus().focusable = true; │ │121 - try inner_box.children.put(allocator, text_box.getFocus().id, .{ .widget = .{ .text_box = text_box }, .rect = null, .min_size = null }); │ │179 + for (inner_box.children.values()) |*child| { │ │180 + child.widget.deinit(); │ │181 } │ │182 + inner_box.children.clearAndFree(self.allocator); │ │183 │ │124 - { │ │125 - var text_box = try wgt.TextBox(Widget).init(allocator, "this is a\nmulti-line TextBox", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ │184 + for (self.lines.items) |line| self.allocator.free(line); │ │185 + self.lines.clearAndFree(self.allocator); │ │186 + │ │187 + self.scroll.x = 0; │ │188 + self.scroll.y = 0; │ │189 + self.scroll.getFocus().child_id = null; │ │190 + │ │191 + for (items) |item| { │ │192 + const line = try self.allocator.dupe(u8, item); │ │193 + { │ │194 + errdefer self.allocator.free(line); │ │195 + try self.lines.append(self.allocator, line); │ │196 + } │ │197 + │ │198 + var text_box = try wgt.TextBox(Widget).init(self.allocator, line, .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .none }); │ │199 errdefer text_box.deinit(); │ │200 text_box.getFocus().focusable = true; │ │128 - try inner_box.children.put(allocator, text_box.getFocus().id, .{ .widget = .{ .text_box = text_box }, .rect = null, .min_size = null }); │ │201 + try inner_box.children.put(self.allocator, text_box.getFocus().id, .{ .widget = .{ .text_box = text_box }, .rect = null, .min_size = null }); │ │202 } │ │203 │ │204 if (inner_box.children.count() > 0) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │205 self.scroll.getFocus().child_id = inner_box.children.keys()[0]; │ │206 } │ │134 - │ │135 - return self; │ │207 } │ │208 │ │138 - pub fn deinit(self: *WidgetList) void { │ │139 - self.scroll.deinit(); │ │140 - } │ │141 - │ │142 - pub fn build(self: *WidgetList, constraint: layout.Constraint, root_focus: *Focus) !void { │ │209 + pub fn build(self: *SelectableList, constraint: layout.Constraint, root_focus: *Focus) !void {│ │210 self.clearGrid(); │ │211 const children = &self.scroll.child.box.children; │ │145 - for (children.keys(), children.values()) |id, *commit| { │ │146 - commit.widget.text_box.options.border_style = if (self.getFocus().child_id == id) │ │212 + for (children.keys(), children.values()) |id, *item| { │ │213 + item.widget.text_box.options.border_style = if (self.getFocus().child_id == id) │ │214 (if (root_focus.grandchild_id == id) .double else .single) │ │215 else │ │149 - .single; │ │216 + .hidden; │ │217 } │ │151 - try self.scroll.build(constraint, root_focus); │ │218 + // force a minimum row of content so an empty list still produces a │ │219 + // non-zero grid. without this, xitui's Scroll.build copies from a │ │220 + // zero-cell child grid and traps with OOB in release builds because │ │221 + // NDSlice.at's bounds check is gated on runtime_safety. │ │222 + var adjusted = constraint; │ │223 + adjusted.min_size.height = @max(adjusted.min_size.height orelse 0, 1); │ │224 + try self.scroll.build(adjusted, root_focus); │ │225 } │ │226 │ │154 - pub fn input(self: *WidgetList, key: inp.Key, root_focus: *Focus) !void { │ │227 + pub fn input(self: *SelectableList, key: inp.Key, root_focus: *Focus) !void { │ │228 if (self.getFocus().child_id) |child_id| { │ │229 const children = &self.scroll.child.box.children; │ │230 if (children.getIndex(child_id)) |current_index| { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────╮ │272 } │ │273 } │ │274 │ │202 - pub fn clearGrid(self: *WidgetList) void { │ │275 + pub fn clearGrid(self: *SelectableList) void {│ │276 self.scroll.clearGrid(); │ │277 } │ │278 │ ╰───────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────╮ │206 - pub fn getGrid(self: WidgetList) ?Grid { │ │279 + pub fn getGrid(self: SelectableList) ?Grid {│ │280 return self.scroll.getGrid(); │ │281 } │ │282 │ ╰─────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────╮ │210 - pub fn getFocus(self: *WidgetList) *Focus { │ │283 + pub fn getFocus(self: *SelectableList) *Focus {│ │284 return self.scroll.getFocus(); │ │285 } │ │286 │ ╰────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────╮ │214 - pub fn getSelectedIndex(self: WidgetList) ?usize { │ │287 + pub fn getSelectedIndex(self: SelectableList) ?usize { │ │288 if (self.scroll.child.box.focus.child_id) |child_id| {│ │289 const children = &self.scroll.child.box.children; │ │290 return children.getIndex(child_id); │ ╰───────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯