╭ changed ──────────╮ │src/embed/script.js│ ╰───────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │127 if (current.tagName === "INPUT" && !preserveValue && current.type !== "file" && current.value !== desired.value) {│ │128 current.value = desired.value; │ │129 } │ │130 + // the live checked state is what posts, so it follows the grid's selection │ │131 + if (current.tagName === "INPUT" && current.type === "radio") current.checked = desired.checked; │ │132 } │ │133 │ │134 function patchNode(current, desired) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │2554 // a submit button that overrides its enclosing form's action │ │2555 pub const submit_action_prefix = "submit:"; │ │2556 │ │2557 +// one option of a radio field, as `radio:<name>=<value>` │ │2558 +pub const radio_prefix = "radio:"; │ │2559 + │ │2560 // the url an `ax:` link points at, or null for anything else. a host that│ │2561 // suppresses the anchor's own navigation needs this to follow the link. │ │2562 pub fn rawLink(root_focus: *Focus, focus_id: usize) ?[]const u8 { │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭────────────────────────────────────────────────────╮ │41 repo_header: ui.Repo.Header.View, │ │42 fork_header: ui.Fork.Header.View, │ │43 search_box: SearchBox, │ │44 + radio: Radio, │ │45 search_header: SearchHeader, │ │46 repo_issues_header: ui.Repo.Issues.Header, │ │47 repo_patches_header: ui.Repo.Patches.Header,│ ╰────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 940 │ │ 941 pub fn getFocus(self: *SearchBox) *Focus { │ │ 942 return self.text_input.getFocus(); │ │ 943 + } │ │ 944 +}; │ │ 945 + │ │ 946 +// a row of bordered options posted as one form field. the selection is the │ │ 947 +// focused option, so a click selects on the web too. │ │ 948 +pub const Radio = struct { │ │ 949 + box: wgt.Box(Widget), │ │ 950 + values: []const []const u8, │ │ 951 + │ │ 952 + pub fn init(allocator: std.mem.Allocator, session: *ui.Session, name: []const u8, values: []const []const u8, initial: []const u8) !Radio { │ │ 953 + var box = try wgt.Box(Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │ 954 + errdefer box.deinit(allocator); │ │ 955 + for (values) |value| { │ │ 956 + var option = try wgt.TextBox.init(allocator, value, .{ .border_style = .single, .round_corners = true, .wrap_kind = .none }); │ │ 957 + errdefer option.deinit(allocator); │ │ 958 + option.getFocus().mode = .all; │ │ 959 + option.getFocus().kind = .{ .custom = try std.fmt.allocPrint(session.page_arena.allocator(), ui.radio_prefix ++ "{s}={s}", .{ name, value }) }; │ │ 960 + if (std.mem.eql(u8, value, initial)) box.getFocus().child_id = option.getFocus().id; │ │ 961 + try box.children.put(allocator, option.getFocus().id, .{ .widget = .{ .text_box = option }, .rect = null, .min_size = .{ .width = try xitui.width.displayWidth(value) + 2, .height = 3 } });│ │ 962 + } │ │ 963 + return .{ .box = box, .values = values }; │ │ 964 + } │ │ 965 + │ │ 966 + pub fn deinit(self: *Radio, allocator: std.mem.Allocator) void { │ │ 967 + self.box.deinit(allocator); │ │ 968 + } │ │ 969 + │ │ 970 + // the focused option's value │ │ 971 + pub fn selected(self: *Radio) []const u8 { │ │ 972 + const index = self.box.children.getIndex(self.box.getFocus().child_id orelse unreachable) orelse unreachable; │ │ 973 + return self.values[index]; │ │ 974 + } │ │ 975 + │ │ 976 + pub fn build(self: *Radio, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │ 977 + self.clearGrid(); │ │ 978 + const selected_id = self.box.getFocus().child_id; │ │ 979 + for (self.box.children.keys(), self.box.children.values()) |id, *child| { │ │ 980 + child.widget.text_box.options.border_style = if (id == selected_id) .single else .hidden; │ │ 981 + } │ │ 982 + try self.box.build(allocator, constraint, root_focus); │ │ 983 + } │ │ 984 + │ │ 985 + pub fn input(self: *Radio, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │ 986 + _ = allocator; │ │ 987 + const ids = self.box.children.keys(); │ │ 988 + const index = self.box.children.getIndex(self.box.getFocus().child_id orelse return) orelse return; │ │ 989 + switch (key) { │ │ 990 + .arrow_left => if (index > 0) root_focus.setFocus(ids[index - 1]), │ │ 991 + .arrow_right => if (index + 1 < ids.len) root_focus.setFocus(ids[index + 1]), │ │ 992 + else => {}, │ │ 993 + } │ │ 994 + } │ │ 995 + │ │ 996 + pub fn clearGrid(self: *Radio) void { │ │ 997 + self.box.clearGrid(); │ │ 998 + } │ │ 999 + │ │1000 + pub fn getGrid(self: Radio) ?Grid { │ │1001 + return self.box.getGrid(); │ │1002 + } │ │1003 + │ │1004 + pub fn getFocus(self: *Radio) *Focus { │ │1005 + return self.box.getFocus(); │ │1006 } │ │1007 }; │ │1008 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2261 } │ │2262 var pos_buf: [128]u8 = undefined; │ │2263 try output.appendSlice(allocator, try std.fmt.bufPrint(&pos_buf, " style=\"left:{d}ch;top:{d}em;width:{d}ch;height:{d}em\"></button>", .{ r.x, r.y, r.size.width, r.size.height })); │ │2264 + } else if (std.mem.startsWith(u8, custom, ui.radio_prefix)) { │ │2265 + // an invisible native radio over the option, checked when the option is its row's selection │ │2266 + const field = custom[ui.radio_prefix.len..]; │ │2267 + const separator = std.mem.indexOfScalar(u8, field, '=') orelse continue; │ │2268 + const row = root_focus.children.get(child.parent_id) orelse continue; │ │2269 + var id_buf: [128]u8 = undefined; │ │2270 + try output.appendSlice(allocator, try std.fmt.bufPrint(&id_buf, "<input type=\"radio\" data-key=\"control:{d}\" data-focus-id=\"{d}\"", .{ id, id })); │ │2271 + try appendFormAttribute(allocator, output, root_focus, id); │ │2272 + try output.appendSlice(allocator, " name=\""); │ │2273 + try appendEscapedHtml(allocator, output, field[0..separator]); │ │2274 + try output.appendSlice(allocator, "\" value=\""); │ │2275 + try appendEscapedHtml(allocator, output, field[separator + 1 ..]); │ │2276 + try output.append(allocator, '"'); │ │2277 + if (row.focus.child_id == id) try output.appendSlice(allocator, " checked"); │ │2278 + var pos_buf: [128]u8 = undefined; │ │2279 + try output.appendSlice(allocator, try std.fmt.bufPrint(&pos_buf, " style=\"opacity:0;left:{d}ch;top:{d}em;width:{d}ch;height:{d}em\">", .{ r.x, r.y, r.size.width, r.size.height }));│ │2280 } else if (std.mem.startsWith(u8, custom, ui.file_input_prefix)) { │ │2281 try output.appendSlice(allocator, "<input type=\"file\" data-preserve-value data-action=\""); │ │2282 try appendEscapedHtml(allocator, output, custom[ui.file_input_prefix.len..]); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯