╭ CopyableText.zig ─────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const ui = @import("../ui.zig"); 3 const xit = @import("xit"); 4 const xitui = xit.xitui; 5 const wgt = xitui.widget; 6 const layout = xitui.layout; 7 const Key = xitui.input.Key; 8 const Grid = xitui.grid.Grid; 9 const Focus = xitui.focus.Focus; 10 const inp = @import("input.zig"); 11 12 pub const Choice = struct { 13 selector: []const u8 = "", 14 text: []const u8, 15 copyable_text: ?[]const u8 = null, 16 label: []const u8 = "", 17 bottom_label: []const u8 = "", 18 }; 19 20 pub const View = struct { 21 box: wgt.Box(ui.Widget), 22 session: *ui.Session, 23 choices: []Choice, 24 selected: usize = 0, 25 26 pub fn init(allocator: std.mem.Allocator, session: *ui.Session, choices: []const Choice) !View { 27 if (choices.len == 0) return error.MissingCopyableText; 28 29 const owned_choices = try allocator.dupe(Choice, choices); 30 errdefer allocator.free(owned_choices); 31 32 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); 33 errdefer box.deinit(allocator); 34 35 if (choices.len > 1) for (choices) |choice| { 36 var selector_box = try wgt.TextBox(ui.Widget).init(allocator, choice.selector, .{ .border_style = .hidden, .wrap_kind = .none }); 37 errdefer selector_box.deinit(allocator); 38 selector_box.getFocus().focusable = true; 39 try box.children.put(allocator, selector_box.getFocus().id, .{ .widget = .{ .text_box = selector_box }, .rect = null, .min_size = .{ .width = choice.selector.len + 2, .height = 3 } }); 40 }; 41 42 var text_input = try wgt.TextInput(ui.Widget).init(allocator, .{ 43 .border_style = .single, 44 .label = choices[0].label, 45 .bottom_label = choices[0].bottom_label, 46 .read_only = true, 47 .render_content = session.is_terminal, 48 .visible_width = null, 49 }); 50 errdefer text_input.deinit(allocator); 51 text_input.getFocus().focusable = true; 52 try text_input.setContent(allocator, choices[0].text); 53 box.getFocus().child_id = text_input.getFocus().id; 54 try box.children.put(allocator, text_input.getFocus().id, .{ .widget = .{ .text_input = text_input }, .rect = null, .min_size = null, .flex = .shrink }); 55 56 return .{ .box = box, .session = session, .choices = owned_choices }; 57 } 58 59 pub fn initClone(allocator: std.mem.Allocator, session: *ui.Session, identity: []const u8) !View { 60 const aa = session.page_arena.allocator(); 61 var choices: [2]Choice = undefined; 62 var count: usize = 0; 63 if (session.data.git_http_port) |port| { 64 const url = try std.fmt.allocPrint(aa, "http://localhost:{d}/repo/{s}", .{ port, identity }); 65 choices[count] = .{ 66 .selector = "http", 67 .text = url, 68 .copyable_text = try std.fmt.allocPrint(aa, "git clone {s}", .{url}), 69 .label = " clone ", 70 }; 71 count += 1; 72 } 73 if (session.data.git_ssh_port) |port| { 74 const url = try std.fmt.allocPrint(aa, "ssh://localhost:{d}/repo/{s}", .{ port, identity }); 75 choices[count] = .{ 76 .selector = "ssh", 77 .text = url, 78 .copyable_text = try std.fmt.allocPrint(aa, "{s}git clone {s}", .{ session.data.git_ssh_prefix, url }), 79 .label = " clone ", 80 }; 81 count += 1; 82 } 83 if (count == 0) return error.MissingGitPort; 84 return init(allocator, session, choices[0..count]); 85 } 86 87 pub fn minWidth(self: *const View) usize { 88 var width = maxWidth(self.choices) + 2; 89 if (self.choices.len > 1) { 90 for (self.choices) |choice| width += choice.selector.len + 2; 91 } 92 return width; 93 } 94 95 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 96 self.box.deinit(allocator); 97 allocator.free(self.choices); 98 } 99 100 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 101 self.clearGrid(); 102 103 if (self.choices.len > 1) { 104 if (root_focus.grandchild_id) |id| { 105 for (0..self.choices.len) |i| { 106 if (id != self.selector(i).getFocus().id) continue; 107 if (i != self.selected) try self.select(allocator, i); 108 root_focus.setFocus(self.textInput().getFocus().id); 109 break; 110 } 111 } 112 113 const focused = root_focus.grandchild_id == self.textInput().getFocus().id; 114 for (0..self.choices.len) |i| { 115 self.selector(i).options.border_style = if (i == self.selected) 116 (if (focused) .double else .single) 117 else 118 .hidden; 119 } 120 } 121 self.textInput().options.border_style = .single; 122 self.textInput().cursor = 0; 123 self.textInput().scroll_offset = 0; 124 try self.box.build(allocator, constraint, root_focus); 125 126 if (!self.session.is_terminal) { 127 const aa = self.session.arena.allocator(); 128 const id = self.textInput().getFocus().id; 129 try self.session.text_inputs.put(aa, id, self.textInput()); 130 try self.session.input_values.put(aa, id, self.choices[self.selected].text); 131 } 132 } 133 134 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 135 switch (key) { 136 .arrow_left => if (self.selected > 0) try self.select(allocator, self.selected - 1), 137 .arrow_right => if (self.selected + 1 < self.choices.len) try self.select(allocator, self.selected + 1), 138 .enter => if (self.session.is_terminal) self.show(), 139 .mouse => |mouse| { 140 if (self.choices.len > 1) for (0..self.choices.len) |i| { 141 if (!inp.leftClickOn(root_focus, self.selector(i).getFocus().id, mouse)) continue; 142 try self.select(allocator, i); 143 root_focus.setFocus(self.textInput().getFocus().id); 144 return; 145 }; 146 if (self.session.is_terminal and !mouse.ctrl and inp.leftClickOn(root_focus, self.textInput().getFocus().id, mouse)) self.show(); 147 }, 148 else => {}, 149 } 150 } 151 152 fn select(self: *View, allocator: std.mem.Allocator, selected: usize) !void { 153 self.selected = selected; 154 const choice = self.choices[selected]; 155 const text_input = self.textInput(); 156 text_input.options.label = choice.label; 157 text_input.options.bottom_label = choice.bottom_label; 158 try text_input.setContent(allocator, choice.text); 159 } 160 161 fn show(self: *View) void { 162 const choice = self.choices[self.selected]; 163 self.session.host_request = .{ .show_copyable_text = choice.copyable_text orelse choice.text }; 164 } 165 166 fn textInput(self: *View) *wgt.TextInput(ui.Widget) { 167 return &self.box.children.values()[if (self.choices.len > 1) self.choices.len else 0].widget.text_input; 168 } 169 170 fn selector(self: *View, index: usize) *wgt.TextBox(ui.Widget) { 171 return &self.box.children.values()[index].widget.text_box; 172 } 173 174 fn maxWidth(choices: []const Choice) usize { 175 var width: usize = 0; 176 for (choices) |choice| width = @max(width, choice.text.len, choice.label.len, choice.bottom_label.len); 177 return width; 178 } 179 180 pub fn clearGrid(self: *View) void { 181 self.box.clearGrid(); 182 } 183 184 pub fn getGrid(self: View) ?Grid { 185 return self.box.getGrid(); 186 } 187 188 pub fn getFocus(self: *View) *Focus { 189 return self.box.getFocus(); 190 } 191 }; 192