╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────────────╮ │src/ui/UserNotifications.zig│ ╰────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │133 { │ │134 var stack = try wgt.Stack(ui.Widget).init(allocator); │ │135 errdefer stack.deinit(allocator); │ │130 - { │ │136 + // an empty first window has no rows, not even paging ones │ │137 + if (data.isEmpty()) { │ │138 + var center = try ui.widget.initEmpty(allocator); │ │139 + errdefer center.deinit(allocator); │ │140 + try stack.children.put(allocator, center.getFocus().id, .{ .center = center }); │ │141 + } else { │ │142 var list = try ui.widget.FlowBox.Scroll.init(allocator, .{}, !session.is_terminal);│ │143 errdefer list.deinit(allocator); │ │144 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │175 try box.children.put(allocator, stack.getFocus().id, .{ .widget = .{ .stack = stack }, .rect = null, .min_size = null });│ │176 } │ │177 box.getFocus().child_id = box.children.keys()[content_index]; │ │167 - return .{ .box = box, .session = session, .control_ids = control_ids, .view_index = @backingInt(data.view) }; │ │178 + return .{ .box = box, .data = data, .session = session, .control_ids = control_ids, .view_index = @backingInt(data.view) }; │ │179 } │ │180 │ │181 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │221 const focused = root_focus.grandchild_id orelse return; │ │222 if (key == .enter and focused == logout_id) return logOut(self.session); │ │223 if (direction == .down) { │ │213 - // an empty list has nothing to focus │ │214 - if (self.contentStack().getSelected()) |selected| if (selected.* == .flow_box_scroll and selected.flow_box_scroll.isEmpty()) return;│ │224 + // the empty message has nothing to focus │ │225 + if (self.view_index == @backingInt(ui.RoutablePage.UserView.notifications) and self.data.isEmpty()) return; │ │226 root_focus.setFocus(self.contentStack().getFocus().id); │ │227 } else if (std.mem.indexOfScalar(usize, &self.control_ids, focused)) |current| { │ │228 if (inp.moveTab(key, current, self.control_ids.len)) |next| root_focus.setFocus(self.control_ids[next]); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │339 │ │340 return Center.init(allocator, .{ .box = box }); │ │341 } │ │342 + │ │343 +const empty_label = "nothing to see here"; │ │344 │ │345 +// the message a list or tab shows in place of its empty first window │ │346 +pub fn initEmpty(allocator: std.mem.Allocator) !Center { │ │347 + var text_box = try wgt.TextBox.init(allocator, empty_label, .{ .border = null, .wrap_kind = .none });│ │348 + errdefer text_box.deinit(allocator); │ │349 + return Center.init(allocator, .{ .text_box = text_box }); │ │350 +} │ │351 + │ │352 pub const FlowBox = struct { │ │353 focus: *Focus, │ │354 grid: ?Grid, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │600 │ │601 pub fn getFocus(self: *Scroll) *Focus { │ │602 return self.scroll.getFocus(); │ │594 - } │ │595 - │ │596 - pub fn isEmpty(self: Scroll) bool { │ │597 - return self.scroll.child.flow_box.text_boxes.items.len == 0;│ │603 } │ │604 │ │605 pub fn atTop(self: Scroll) bool { │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1913 inline for (@typeInfo(Status).@"enum".field_values) |value| { │ │1914 var split = try initSplit(allocator, session, data, @fromBackingInt(@intCast(value)), false);│ │1915 errdefer split.deinit(allocator); │ │1916 - try stack.children.put(allocator, split.getFocus().id, .{ .box = split }); │ │1916 + try stack.children.put(allocator, split.getFocus().id, split); │ │1917 } │ │1918 │ │1919 - // the labels view │ │1920 - { │ │1919 + // the labels view, or the empty message without labels │ │1920 + if (labelsEmpty(data)) { │ │1921 + var center = try widget.initEmpty(allocator); │ │1922 + errdefer center.deinit(allocator); │ │1923 + try stack.children.put(allocator, center.getFocus().id, .{ .center = center }); │ │1924 + } else { │ │1925 var tf = try WordFlow.init(allocator); │ │1926 errdefer tf.deinit(allocator); │ │1927 var items: std.ArrayList(WordFlow.Item) = .empty; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │2052 if (supports_drafts) { │ │2053 var split = try initSplit(allocator, session, data, null, true); │ │2054 errdefer split.deinit(allocator); │ │2051 - try stack.children.put(allocator, split.getFocus().id, .{ .box = split });│ │2055 + try stack.children.put(allocator, split.getFocus().id, split); │ │2056 } │ │2057 │ │2058 // the conflicts split; its tab exists only when the repo has │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │2061 if (supports_conflicts and data.conflicts.count > 0) { │ │2062 var split = try initSplit(allocator, session, data, null, false); │ │2063 errdefer split.deinit(allocator); │ │2060 - try stack.children.put(allocator, split.getFocus().id, .{ .box = split });│ │2064 + try stack.children.put(allocator, split.getFocus().id, split); │ │2065 } │ │2066 │ │2067 // the stack starts on the page's view. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2078 } │ │2079 │ │2080 // the master-detail split showing `status`'s window, or the conflicts │ │2077 - // window when null. │ │2078 - fn initSplit(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self, status_maybe: ?Status, drafts: bool) !wgt.Box(Widget) { │ │2081 + // window when null. an empty first window shows the empty message instead. │ │2082 + fn initSplit(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self, status_maybe: ?Status, drafts: bool) !Widget { │ │2083 + const win = if (status_maybe) |status| data.window(status) else if (drafts and supports_drafts) &data.drafts else if (supports_conflicts) &data.conflicts else unreachable;│ │2084 + if (windowEmpty(win)) return .{ .center = try widget.initEmpty(allocator) }; │ │2085 + │ │2086 var box = try wgt.Box(Widget).init(allocator, .{ .border = null, .direction = .horiz }); │ │2087 errdefer box.deinit(allocator); │ │2081 - │ │2082 - const win = if (status_maybe) |status| data.window(status) else if (drafts and supports_drafts) &data.drafts else if (supports_conflicts) &data.conflicts else unreachable;│ │2088 │ │2089 // the thread list (one focusable row per title), plus a "next" link that │ │2090 // reloads the page rooted at the following thread. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2131 │ │2132 // the page opens on the list, unless the url names a thread │ │2133 box.getFocus().child_id = box.children.keys()[if (data.selected_id.len > 0) detail_index else list_index];│ │2129 - return box; │ │2134 + return .{ .box = box }; │ │2135 } │ │2136 │ │2137 // a thread form, plus the target branch for patches, prefilled from │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯