╭ changed ──────────────╮ │src/ui/UserSettings.zig│ ╰───────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │13 │ │14 const wasm = builtin.target.cpu.arch == .wasm32; │ │15 │ │16 -// the logged-in user's settings, or the login form when logged out │ │17 -pub fn initView(allocator: std.mem.Allocator, session: *ui.Session) !ui.Widget { │ │18 - if (session.data.user_id != null) return .{ .user_settings = try View.init(allocator, session) }; │ │19 - return .{ .user_login = try ui.UserLogin.View.init(allocator, session) }; │ │16 +// the logged-in user's notifications and settings, or the login form when │ │17 +// logged out. `user_route` is the page's user tab route. │ │18 +pub fn initView(allocator: std.mem.Allocator, session: *ui.Session, notifications: *const ui.UserNotifications, user_route: ui.RoutablePage) !ui.Widget { │ │19 + if (session.data.user_id != null) return .{ .user_notifications = try ui.UserNotifications.View.init(allocator, notifications, session, user_route) };│ │20 + return .{ .user_login = try ui.UserLogin.View.init(allocator, session, user_route) }; │ │21 } │ │22 │ │23 // the user tab's label: the logged-in user's name, or "login" │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │26 return session.data.user_name orelse unreachable; │ │27 } │ │28 │ │28 -const logout_label = "logout"; │ │29 +// the user tab's bottom label: the unread notification count, if any │ │30 +pub fn tabBottomLabel(session: *const ui.Session) ![]const u8 { │ │31 + if (session.data.unread_notifs == 0) return ""; │ │32 + return session.page_arena.allocator().print("({d})", .{session.data.unread_notifs});│ │33 +} │ │34 │ │35 pub const View = struct { │ │31 - // a vertical box: the logout sub-header above a centered form, scrolled │ │32 - // when it outgrows the window │ │36 + // a vertical box holding the centered form, scrolled when it outgrows the window │ │37 box: wgt.Box(ui.Widget), │ │38 session: *ui.Session, │ │35 - logout_id: usize, │ │39 // the form's controls in order: email, the passwords, then submit │ │40 control_ids: [5]usize, │ │41 │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │39 - const header_index = 0; │ │40 - const scroll_index = 1; │ │41 - // a row taller than the button, leaving a blank line above the form │ │42 - const header_height = 4; │ │42 + const scroll_index = 0; │ │43 // the form's children: the email box, the bordered password box, submit│ │44 const passwords_index = 1; │ │45 const submit_index = 2; │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │46 │ │47 - pub fn init(allocator: std.mem.Allocator, session: *ui.Session) !View { │ │47 + // `route` is the user settings route, which the forms post under │ │48 + pub fn init(allocator: std.mem.Allocator, session: *ui.Session, route: ui.RoutablePage) !View { │ │49 + const url = try route.toUrl(session.page_arena); │ │50 var outer = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .vert }); │ │51 errdefer outer.deinit(allocator); │ │50 - │ │51 - var logout_id: usize = undefined; │ │52 - { │ │53 - // the form kind makes the web renderer post the button to /logout │ │54 - var header = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .horiz }); │ │55 - errdefer header.deinit(allocator); │ │56 - header.getFocus().kind = .{ .custom = "form:logout" }; │ │57 - // the spacer pushes the logout button to the right edge │ │58 - { │ │59 - var spacer = try ui.widget.Spacer.init(allocator); │ │60 - errdefer spacer.deinit(allocator); │ │61 - try header.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │62 - } │ │63 - { │ │64 - var button = try wgt.TextBox.init(allocator, logout_label, .{ .border = .single, .round_corners = true, .wrap_kind = .none }); │ │65 - errdefer button.deinit(allocator); │ │66 - button.getFocus().mode = .all; │ │67 - // the renderer distinguishes plain clickables from buttons that │ │68 - // should POST to a server route by this kind. │ │69 - button.getFocus().kind = .{ .custom = "submit" }; │ │70 - logout_id = button.getFocus().id; │ │71 - header.getFocus().child_id = logout_id; │ │72 - try header.children.put(allocator, logout_id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = logout_label.len + 2, .height = 3 } });│ │73 - } │ │74 - try outer.children.put(allocator, header.getFocus().id, .{ .widget = .{ .box = header }, .rect = null, .min_size = .{ .width = null, .height = header_height } }); │ │75 - } │ │52 │ │53 var control_ids: [5]usize = undefined; │ │54 { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │55 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .vert }); │ │56 errdefer box.deinit(allocator); │ │81 - // the form posts to the page's own user tab │ │82 - box.getFocus().kind = .{ .custom = "form:user" }; │ │57 + box.getFocus().kind = .{ .custom = try session.page_arena.allocator().print("form:{s}/user", .{url}) };│ │58 │ │59 const saved_fields = if (session.formFeedback(.user_settings)) |saved| saved.fields else null; │ │60 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │105 try outer.children.put(allocator, scroll.getFocus().id, .{ .widget = .{ .scroll = scroll }, .rect = null, .min_size = null });│ │106 } │ │107 │ │133 - outer.getFocus().child_id = outer.children.keys()[header_index]; │ │134 - return .{ .box = outer, .session = session, .logout_id = logout_id, .control_ids = control_ids }; │ │108 + outer.getFocus().child_id = outer.children.keys()[scroll_index]; │ │109 + return .{ .box = outer, .session = session, .control_ids = control_ids }; │ │110 } │ │111 │ │112 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │130 .text_input => |*ti| try self.session.text_inputs.put(inputs_arena, ti.getFocus().id, ti), │ │131 else => {}, │ │132 }; │ │158 - // the window's height below the sub-header as a minimum centers a form that fits │ │159 - const viewport_height = constraint.max_size.height orelse constraint.min_size.height; │ │160 - self.box.children.values()[scroll_index].min_size = .{ .width = null, .height = if (viewport_height) |height| height -| header_height else null };│ │133 + // the window's height as a minimum centers a form that fits │ │134 + self.box.children.values()[scroll_index].min_size = .{ .width = null, .height = constraint.max_size.height orelse constraint.min_size.height }; │ │135 try self.box.build(allocator, .{ │ │136 .min_size = .{ .width = constraint.min_size.width, .height = null }, │ │137 .max_size = constraint.max_size, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │139 } │ │140 │ │141 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │168 - if (key == .mouse and inp.leftClickOn(root_focus, self.logout_id, key.mouse)) return self.logOut(); │ │169 - if (self.headerActive()) { │ │170 - if (key == .tab or inp.vertDirection(key) == .down) return root_focus.setFocus(self.control_ids[0]);│ │171 - if (key == .enter) try self.logOut(); │ │172 - return; │ │173 - } │ │174 - │ │142 const focused = root_focus.grandchild_id orelse return; │ │143 const cur = std.mem.indexOfScalar(usize, &self.control_ids, focused) orelse return; │ │144 const submit_id = self.control_ids[self.control_ids.len - 1]; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │146 const direction = inp.vertDirection(key); │ │147 const up = direction == .up or key == .back_tab; │ │148 if (up or direction == .down or key == .tab) { │ │182 - // up leaves the form from its first control for the logout button│ │183 - if (up and cur == 0) return root_focus.setFocus(self.logout_id); │ │149 + // up from the first control is the sub header's, through `atTop` │ │150 + if (up and cur == 0) return; │ │151 const next = if (up) cur - 1 else cur + 1; │ │152 if (next == self.control_ids.len) return; │ │153 root_focus.setFocus(self.control_ids[next]); │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │160 else => {}, │ │161 } │ │162 if (!on_submit) try (try self.formInput(focused)).input(allocator, key, root_focus);│ │196 - } │ │197 - │ │198 - fn logOut(self: *View) !void { │ │199 - self.session.logOut(); │ │200 - // leave the user tab for the page it belongs to, where the web's │ │201 - // /logout redirect also lands │ │202 - try self.session.navigate(self.session.data.current_page.pageRoot()); │ │163 } │ │164 │ │165 // the boxes holding the form's inputs: the form itself and the password box │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯