╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │731 inner.children.clearAndFree(allocator); │ │732 inner.getFocus().child_id = null; │ │733 │ │734 + // the open/close button │ │735 + { │ │736 + const action: []const u8 = switch (entry.issue.status) { │ │737 + .open => "close", │ │738 + .closed => "open", │ │739 + }; │ │740 + var row = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │741 + errdefer row.deinit(allocator); │ │742 + const pa = self.session.page_arena.allocator(); │ │743 + row.getFocus().kind = .{ .custom = if (self.data.identity.len == 0) │ │744 + try std.fmt.allocPrint(pa, "form:/issues/{s}/{s}", .{ entry.id, action }) │ │745 + else │ │746 + try std.fmt.allocPrint(pa, "form:/repo/{s}/issues/{s}/{s}", .{ self.data.identity, entry.id, action }) }; │ │747 + │ │748 + { │ │749 + var spacer = try ui.Spacer.init(allocator); │ │750 + errdefer spacer.deinit(allocator); │ │751 + try row.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │752 + } │ │753 + │ │754 + { │ │755 + var button = try wgt.TextBox(ui.Widget).init(allocator, action, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │756 + errdefer button.deinit(allocator); │ │757 + button.getFocus().focusable = true; │ │758 + // the renderer distinguishes plain clickables from buttons that │ │759 + // should POST to a server route by this kind. │ │760 + button.getFocus().kind = .{ .custom = "submit" }; │ │761 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = action.len + 2, .height = null } });│ │762 + } │ │763 + │ │764 + row.getFocus().child_id = row.children.keys()[button_in_row_index]; │ │765 + try inner.children.put(allocator, row.getFocus().id, .{ .widget = .{ .box = row }, .rect = null, .min_size = null }); │ │766 + } │ │767 + │ │768 // the issue's tags, each linking to this status's list filtered to │ │769 // that tag. │ │770 { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │832 } │ │833 const i = self.selectedSplitIndex() orelse return; │ │834 if (self.detailActive(i)) { │ │801 - try self.detailInput(i, key, root_focus); │ │835 + try self.detailInput(allocator, i, key, root_focus);│ │836 } else { │ │837 try self.listInput(i, key, root_focus); │ │838 } │ ╰─────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │873 } │ │874 } │ │875 │ │842 - fn detailInput(self: *View, index: usize, key: Key, root_focus: *Focus) !void { │ │843 - if (self.tagsFocused(index)) { │ │876 + fn detailInput(self: *View, allocator: std.mem.Allocator, index: usize, key: Key, root_focus: *Focus) !void {│ │877 + if (self.statusButtonFocused(index)) { │ │878 + try self.statusButtonInput(allocator, index, key, root_focus); │ │879 + } else if (self.tagsFocused(index)) { │ │880 try self.tagsInput(index, key, root_focus); │ │881 } else { │ │882 try self.descriptionInput(index, key, root_focus); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │883 + } │ │884 + } │ │885 + │ │886 + // the open/close button: enter or a click flips the issue's status; │ │887 + // arrows cross to the neighboring widgets. │ │888 + fn statusButtonInput(self: *View, allocator: std.mem.Allocator, index: usize, key: Key, root_focus: *Focus) !void { │ │889 + switch (key) { │ │890 + .arrow_left => try self.focusList(index, root_focus), │ │891 + .arrow_up => self.focusHeader(root_focus), │ │892 + .arrow_down => if (self.tagFlow(index) != null) try self.focusTags(index, root_focus) else try self.focusDescription(index, root_focus),│ │893 + .enter => try self.toggleIssueStatus(allocator, index), │ │894 + .mouse => |mouse| switch (mouse.action) { │ │895 + .scroll => |dir| { │ │896 + const sc = self.detailScroll(index); │ │897 + sc.y += if (dir == .up) @as(isize, -1) else 1; │ │898 + sc.clampToContent(); │ │899 + }, │ │900 + else => if (self.statusButton(index)) |button| { │ │901 + if (inp.leftClickOn(root_focus, button.getFocus().id, mouse)) try self.toggleIssueStatus(allocator, index); │ │902 + }, │ │903 + }, │ │904 + else => {}, │ │905 } │ │906 } │ │907 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │910 switch (key) { │ │911 .arrow_left => return self.focusList(index, root_focus), │ │912 // once the scroll can't move further, cross into the tags (or the│ │855 - // header tabs when the issue has none). │ │913 + // open/close button when the issue has none). │ │914 .arrow_up => { │ │915 const before = sc.y; │ │916 sc.y -= 1; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │917 sc.clampToContent(); │ │918 if (sc.y == before) { │ │861 - if (self.tagFlow(index) != null) try self.focusTags(index, root_focus) else self.focusHeader(root_focus); │ │919 + if (self.tagFlow(index) != null) try self.focusTags(index, root_focus) else self.focusStatusButton(index, root_focus);│ │920 } │ │921 return; │ │922 }, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1032 try self.session.navigate(route); │ │1033 } │ │1034 │ │1035 + // flip the shown issue's status by re-emitting its event, then reload the │ │1036 + // page rooted at the issue so the view reflects the change. this is the │ │1037 + // terminal path; the web posts the button's form to the status route. │ │1038 + fn toggleIssueStatus(self: *View, allocator: std.mem.Allocator, index: usize) !void { │ │1039 + if (comptime wasm) return; │ │1040 + const io = self.session.io orelse return; │ │1041 + const src = self.data.repo_source orelse return; │ │1042 + const sel = self.detailed_index[index] orelse return; │ │1043 + const entry = self.window(index).issues[sel]; │ │1044 + │ │1045 + var updated = entry.issue; │ │1046 + updated.status = switch (entry.issue.status) { │ │1047 + .open => .closed, │ │1048 + .closed => .open, │ │1049 + }; │ │1050 + │ │1051 + const event = evt.EventWithId{ │ │1052 + .id = entry.id[0 .. evt.event_id_size * 2].*, │ │1053 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │1054 + .event = .{ .issue = updated }, │ │1055 + }; │ │1056 + │ │1057 + switch (src.repo_kind) { │ │1058 + inline else => |repo_kind| { │ │1059 + var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, .{ .path = src.path }); │ │1060 + defer any_repo.deinit(io, allocator); │ │1061 + switch (any_repo) { │ │1062 + inline else => |*repo| try evt.commitAndConsume(repo_kind, repo.self_repo_opts, io, allocator, repo, evt.events_ref, &.{event}),│ │1063 + } │ │1064 + }, │ │1065 + } │ │1066 + │ │1067 + const route = ui.RoutablePage.repoIssuesRoute(self.data.identity, updated.status, "", entry.id) orelse return; │ │1068 + try self.session.navigate(route); │ │1069 + } │ │1070 + │ │1071 // arrow keys move the tag selection; at the flow's edges focus crosses to │ │1072 // the neighboring widgets. │ │1073 fn tagsInput(self: *View, index: usize, key: Key, root_focus: *Focus) !void { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1079 switch (key) { │ │1080 .arrow_left => if (cur > 0) self.focusTag(index, tf, root_focus, cur - 1) else try self.focusList(index, root_focus), │ │1081 .arrow_right => if (cur + 1 < count) self.focusTag(index, tf, root_focus, cur + 1), │ │ 988 - .arrow_up => if (tf.rowStep(cur, false)) |i| self.focusTag(index, tf, root_focus, i) else self.focusHeader(root_focus), │ │1082 + .arrow_up => if (tf.rowStep(cur, false)) |i| self.focusTag(index, tf, root_focus, i) else self.focusStatusButton(index, root_focus), │ │1083 .arrow_down => if (tf.rowStep(cur, true)) |i| self.focusTag(index, tf, root_focus, i) else try self.focusDescription(index, root_focus),│ │1084 .home => self.focusTag(index, tf, root_focus, 0), │ │1085 .end => self.focusTag(index, tf, root_focus, count - 1), │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────╮ │1094 } │ │1095 } │ │1096 │ │1003 - const tags_child_index: usize = 0; │ │1097 + const button_row_index: usize = 0; │ │1098 + const button_in_row_index: usize = 1; │ │1099 + const tags_child_index: usize = 1; │ │1100 │ │1101 fn tagFlow(self: *View, index: usize) ?*ui.TagFlow {│ │1102 const inner = self.detailInner(index); │ ╰──────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │1007 - if (inner.children.count() == 0) return null; │ │1103 + if (inner.children.count() <= tags_child_index) return null; │ │1104 return switch (inner.children.values()[tags_child_index].widget) {│ │1105 .tag_flow => |*tf| tf, │ │1106 else => null, │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯