╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────╮ │src/test/ui.zig│ ╰───────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 4 const Commits = @import("../ui/Repo/Commits.zig"); │ │ 5 const Events = @import("../ui/Repo/Events.zig"); │ │ 6 const xit = @import("xit"); │ │ 7 + │ │ 8 +test "web controls omit hidden inputs but retain off-screen inputs" { │ │ 9 + const wgt = xit.xitui.widget; │ │10 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); │ │11 + defer arena.deinit(); │ │12 + const allocator = arena.allocator(); │ │13 + var session = ui.Session{ .arena = &arena, .page_arena = &arena, .is_terminal = false }; │ │14 + var root = ui.Widget{ .box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }) }; │ │15 + defer root.deinit(allocator); │ │16 + var scroll = try wgt.Scroll(ui.Widget).init(allocator, .{ .box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }) }, .{ .web_native = true });│ │17 + for ([_][]const u8{ "visible", "hidden", "offscreen" }, 0..) |name, i| { │ │18 + var input = try wgt.TextInput.init(allocator, .{ .name = name, .render_content = false }); │ │19 + input.getFocus().mode = .all; │ │20 + try scroll.child.box.children.put(allocator, input.getFocus().id, .{ │ │21 + .widget = .{ .text_input = input }, │ │22 + .rect = null, │ │23 + .min_size = .{ .width = 20, .height = 3 }, │ │24 + .hidden = i == 1, │ │25 + }); │ │26 + } │ │27 + for (scroll.child.box.children.values()) |*child| { │ │28 + const input = &child.widget.text_input; │ │29 + try session.text_inputs.put(allocator, input.getFocus().id, input); │ │30 + } │ │31 + try root.box.children.put(allocator, scroll.getFocus().id, .{ .widget = .{ .scroll = scroll }, .rect = null, .min_size = null }); │ │32 + const focus = root.getFocus(); │ │33 + try root.build(allocator, .{ .min_size = .{ .width = null, .height = null }, .max_size = .{ .width = 30, .height = 3 } }, focus); │ │34 │ │35 + const children = &root.box.children.values()[0].widget.scroll.child.box.children; │ │36 + const offscreen = children.values()[2].rect orelse return error.MissingRect; │ │37 + try std.testing.expect(offscreen.y >= 3); │ │38 + const html = try @import("../web.zig").generateHtml(allocator, &root, &session); │ │39 + try std.testing.expect(std.mem.indexOf(u8, html, "name=\"visible\"") != null); │ │40 + try std.testing.expect(std.mem.indexOf(u8, html, "name=\"hidden\"") == null); │ │41 + try std.testing.expect(std.mem.indexOf(u8, html, "name=\"offscreen\"") != null); │ │42 +} │ │43 + │ │44 test "diff windows span the net changes across commits" { │ │45 const io = std.testing.io; │ │46 const allocator = std.testing.allocator; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1339 } │ │1340 try target_repo.removeBranch(io, .{ .name = other_ref.name }); │ │1341 try evt.consume(.server, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &.{}); │ │1342 + try pch.writeBranchPatch(.server, .xit, .{}, io, allocator, target_repo, std.fmt.bytesToHex(evt.EventWithId.randomId(random), .lower), .{│ │1343 + .title = "Merge the existing feature branch", │ │1344 + .description = "This patch tracks the feature branch in this repo without creating a fork.", │ │1345 + .tags = "feature", │ │1346 + .source_branch = "feature", │ │1347 + .target_branch = "master", │ │1348 + }, null, patch_author); │ │1349 } │ │1350 │ │1351 // recursively copy the contents of src_dir into dest_dir │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │1709 pub const FormFeedback = union(enum) { │ │1710 pub const LoginFailure = enum { unknown_user, wrong_password }; │ │1711 pub const ThreadFailure = enum { required_title }; │ │1712 - pub const PatchFailure = enum { required_title, invalid_target_branch };│ │1712 + pub const PatchFailure = enum { │ │1713 + required_title, │ │1714 + invalid_target_branch, │ │1715 + invalid_source_branch, │ │1716 + unrelated_branches, │ │1717 + │ │1718 + pub fn fromError(err: anyerror) ?PatchFailure { │ │1719 + return switch (err) { │ │1720 + error.InvalidTargetBranch => .invalid_target_branch, │ │1721 + error.InvalidSourceBranch => .invalid_source_branch, │ │1722 + error.UnrelatedBranches => .unrelated_branches, │ │1723 + else => null, │ │1724 + }; │ │1725 + } │ │1726 + }; │ │1727 │ │1728 login: struct { │ │1729 failure: LoginFailure, │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────╮ │1744 tags: []const u8, │ │1745 description: []const u8, │ │1746 target_branch: []const u8, │ │1747 + source_branch: []const u8 = "",│ │1748 + branch_source: bool = false, │ │1749 } = null, │ │1750 }, │ │1751 discussion: struct { │ ╰─────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Fork.zig│ ╰───────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │79 .record = retained_patch, │ │80 .author = try ui.Author.initFromEmail(haxy_moment, arena, retained_patch.author_email),│ │81 .draft = fork_record.event.stage == .draft, │ │82 - .fork_oid = try aa.dupe(u8, &fork_oid), │ │82 + .revision_oid = try aa.dupe(u8, &fork_oid), │ │83 .fork_exists = true, │ │84 }; │ │85 var patch_data = try Patches.detailResult(aa, identity.identity, retained_entry); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │229 switch (any_repo) { │ │230 inline else => |*opened| { │ │231 // local mode: bring the event db up to date with the events branch │ │232 - if (session.local != null) try evt.consume(.local, .repo, repo_kind, opened.self_repo_opts, io, gpa, opened, evt.events_ref, &.{}); │ │232 + if (session.local != null) { │ │233 + try evt.consume(.local, .repo, repo_kind, opened.self_repo_opts, io, gpa, opened, evt.events_ref, &.{}); │ │234 + try @import("../patch.zig").refreshBranches(.local, repo_kind, opened.self_repo_opts, io, gpa, opened, null); │ │235 + } │ │236 const files_data = try Files.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, location, requested_ref_or_oid, requested_ref_value, files_dir, files_line);│ │237 const target_branch = if (files_data.ref_or_oid == .branch) files_data.ref_or_oid_value else ""; │ │238 break :blk .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Patches.zig│ ╰───────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 15 const pch = @import("../../patch.zig"); │ │ 16 │ │ 17 const wasm = builtin.target.cpu.arch == .wasm32; │ │ 18 + │ │ 19 +pub const Source = struct { │ │ 20 + box: wgt.Box(ui.Widget), │ │ 21 + session: *ui.Session, │ │ 22 + existing: bool, │ │ 23 + input_index: usize, │ │ 24 + │ │ 25 + pub fn init(allocator: std.mem.Allocator, session: *ui.Session, initial: []const u8, existing: bool) !Source { │ │ 26 + var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │ 27 + errdefer box.deinit(allocator); │ │ 28 + const labels = [_][]const u8{ "from a new fork", "from an existing branch" }; │ │ 29 + for (labels, 0..) |label, i| { │ │ 30 + if (session.data.host_kind == .local) { │ │ 31 + if (i == 0) continue; │ │ 32 + } │ │ 33 + var selector = try wgt.TextBox.init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │ 34 + errdefer selector.deinit(allocator); │ │ 35 + selector.getFocus().mode = .all; │ │ 36 + try box.children.put(allocator, selector.getFocus().id, .{ .widget = .{ .text_box = selector }, .rect = null, .min_size = .{ .width = label.len + 2, .height = 3 } }); │ │ 37 + } │ │ 38 + const input_index = box.children.count(); │ │ 39 + var input_box = try wgt.TextInput.init(allocator, .{ .label = " source branch ", .name = "source_branch", .rounded_corners = true, .visible_width = null, .render_content = session.is_terminal });│ │ 40 + errdefer input_box.deinit(allocator); │ │ 41 + input_box.getFocus().mode = .all; │ │ 42 + try input_box.setContent(allocator, initial); │ │ 43 + try box.children.put(allocator, input_box.getFocus().id, .{ .widget = .{ .text_input = input_box }, .rect = null, .min_size = .{ .width = 15, .height = 3 } }); │ │ 44 + box.getFocus().child_id = box.children.keys()[if (existing) input_index - 1 else 0]; │ │ 45 + return .{ .box = box, .session = session, .existing = existing or input_index == 1, .input_index = input_index }; │ │ 46 + } │ │ 47 + │ │ 48 + pub fn field(self: *Source) *wgt.TextInput { │ │ 49 + return &self.box.children.values()[self.input_index].widget.text_input; │ │ 50 + } │ │ 51 + pub fn deinit(self: *Source, allocator: std.mem.Allocator) void { │ │ 52 + self.box.deinit(allocator); │ │ 53 + } │ │ 54 + pub fn clearGrid(self: *Source) void { │ │ 55 + self.box.clearGrid(); │ │ 56 + } │ │ 57 + pub fn getGrid(self: Source) ?xitui.grid.Grid { │ │ 58 + return self.box.getGrid(); │ │ 59 + } │ │ 60 + pub fn getFocus(self: *Source) *xitui.focus.Focus { │ │ 61 + return self.box.getFocus(); │ │ 62 + } │ │ 63 + │ │ 64 + pub fn build(self: *Source, allocator: std.mem.Allocator, constraint: xitui.layout.Constraint, root_focus: *xitui.focus.Focus) !void { │ │ 65 + self.clearGrid(); │ │ 66 + self.box.children.values()[self.input_index].hidden = !self.existing; │ │ 67 + for (self.box.children.values()[0..self.input_index], 0..) |*child, i| { │ │ 68 + child.widget.text_box.options.border_style = if ((i == self.input_index - 1) == self.existing) .single else .hidden; │ │ 69 + } │ │ 70 + try self.box.build(allocator, constraint, root_focus); │ │ 71 + if (!self.session.is_terminal) { │ │ 72 + try self.session.text_inputs.put(self.session.arena.allocator(), self.field().getFocus().id, self.field()); │ │ 73 + } │ │ 74 + } │ │ 75 │ │ 76 + pub fn input(self: *Source, allocator: std.mem.Allocator, key: xitui.input.Key, root_focus: *xitui.focus.Focus) !void { │ │ 77 + const current = self.box.getFocus().child_id orelse return; │ │ 78 + const index = self.box.children.getIndex(current) orelse return; │ │ 79 + if (index == self.input_index and key != .arrow_left) return self.field().input(allocator, key, root_focus); │ │ 80 + switch (key) { │ │ 81 + .arrow_left => if (index > 0) { │ │ 82 + self.existing = index == self.input_index; │ │ 83 + root_focus.setFocus(self.box.children.keys()[index - 1]); │ │ 84 + }, │ │ 85 + .arrow_right => { │ │ 86 + self.existing = true; │ │ 87 + root_focus.setFocus(self.box.children.keys()[index + 1]); │ │ 88 + }, │ │ 89 + .enter => { │ │ 90 + self.existing = index == self.input_index - 1; │ │ 91 + if (self.existing) root_focus.setFocus(self.field().getFocus().id); │ │ 92 + }, │ │ 93 + .mouse => |mouse| { │ │ 94 + for (self.box.children.keys()[0..self.input_index], 0..) |id, i| { │ │ 95 + if (@import("../input.zig").leftClickOn(root_focus, id, mouse)) self.existing = i == self.input_index - 1; │ │ 96 + } │ │ 97 + }, │ │ 98 + else => {}, │ │ 99 + } │ │100 + } │ │101 +}; │ │102 + │ │103 // how many patches one window shows before a "next" link appears. │ │104 pub const page_size = 20; │ │105 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────╮ │118 comments: Comment.Window = .empty, │ │119 attachments: []const Attachment.WithId = &.{},│ │120 draft: bool = false, │ │ 37 - fork_oid: []const u8 = "", │ │121 + revision_oid: []const u8 = "", │ │122 no_changes: bool = false, │ │123 fork_exists: bool = false, │ │124 mergeability: pch.Mergeability = .{}, │ ╰───────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │246 │ │247 pub fn canMerge(entry: Entry, session: *const ui.Session) bool { │ │248 return !entry.draft and │ │165 - entry.fork_exists and │ │249 + (entry.fork_exists or entry.record.event.source_branch != null) and│ │250 !entry.conflicted and │ │251 entry.record.event.revision != null and │ │252 entry.record.event.status.kind() == .open and │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────╮ │254 session.data.user_id != null;│ │255 } │ │256 │ │173 -pub fn createDraft( │ │257 +pub fn create( │ │258 data: *const Self, │ │259 session: *ui.Session, │ │260 allocator: std.mem.Allocator, │ ╰──────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯