╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ ui.zig ───────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1948 return; 1949 } 1950 // nothing to go back to; switch to the quit confirmation 1951 const root_focus = self.root.getFocus(); 1952 var iter = root_focus.children.iterator(); 1953 while (iter.next()) |entry| { 1954 switch (entry.value_ptr.focus.kind) { 1955 .custom => |custom| if (std.mem.eql(u8, custom, Quit.tab_kind)) { 1956 root_focus.setFocus(entry.key_ptr.*); 1957 // a Stack only builds its selected child, so the quit 1958 // button isn't in the focus tree until a build runs with 1959 // the quit tab selected. build once, then send a 1960 // synthetic arrow_down to drop focus from the tab onto 1961 // the button so it's ready to confirm. 1962 try self.root.build(allocator, .{ 1963 .min_size = .{ .width = null, .height = 40 }, 1964 .max_size = .{ .width = 80, .height = null }, 1965 }, root_focus); 1966 try self.root.input(allocator, .arrow_down, root_focus); 1967 break; 1968 }, 1969 else => {}, 1970 } 1971 } 1972 return; 1973 } 1974 1975 // forward navigation: navigate() set next_page to the page to move to (a 1976 // cross-page link or tab change crossing pages) 1977 if (session.next_page) |route| { 1978 session.next_page = null; 1979 if (session.haxy_moment == null and session.local == null) return; 1980 // the page we navigated to becomes the current page 1981 session.data.current_page = route; 1982 1983 const arena = try allocator.create(std.heap.ArenaAllocator); 1984 arena.* = std.heap.ArenaAllocator.init(allocator); 1985 errdefer freeArena(allocator, arena); 1986 1987 session.page_arena = arena; 1988 1989 const page = try arena.allocator().create(Page); 1990 page.* = try Page.init(arena, session, route); 1991 const new_root = try initRoot(allocator, page, session); 1992 1993 try self.history.append(allocator, .{ .root = self.root, .route = self.route, .arena = self.arena }); 1994 // drop the oldest entry (freeing its widget tree and arena) once over cap 1995 if (self.history.items.len > max_history) { 1996 var oldest = self.history.orderedRemove(0); 1997 oldest.root.deinit(allocator); 1998 freeArena(allocator, oldest.arena); 1999 } 2000 self.root = new_root; 2001 self.route = route; 2002 self.arena = arena; 2003 } 2004 } 2005 }; 2006 2007 pub fn initRoot(allocator: std.mem.Allocator, page: *const Page, session: *Session) !Widget { 2008 const page_widget: Widget = switch (page.*) { 2009 .home => |*p| .{ .home = try .init(allocator, p, session) }, 2010 .user => |*p| .{ .user = try .init(allocator, p, session) }, 2011 .repo => |*p| .{ .repo = try .init(allocator, p, session) }, 2012 }; 2013 2014 chooseAnsiArtForNavigation(session); 2015 2016 // on the TUI/SSH, the page sits above a one-row footer showing the url 2017 var root = if (session.is_terminal) blk: { 2018 var box = try wgt.Box(Widget).init(allocator, .{ .border_style = null, .direction = .vert }); 2019 errdefer box.deinit(allocator); 2020 const bg_id = bg_blk: { 2021 var background = try widget.AnsiBackground.init(allocator, page_widget, session); 2022 errdefer background.deinit(allocator); 2023 const id = background.getFocus().id; 2024 try box.children.put(allocator, id, .{ .widget = .{ .background = background }, .rect = null, .min_size = null }); 2025 break :bg_blk id; 2026 }; 2027 { 2028 var footer = try widget.Footer.init(allocator, session); 2029 errdefer footer.deinit(allocator); 2030 try box.children.put(allocator, footer.getFocus().id, .{ .widget = .{ .footer = footer }, .rect = null, .min_size = .{ .width = null, .height = 1 } }); 2031 } 2032 box.getFocus().child_id = bg_id; 2033 break :blk Widget{ .box = box }; 2034 } else Widget{ .background = try widget.AnsiBackground.init(allocator, page_widget, session) }; 2035 errdefer root.deinit(allocator); 2036 2037 // input-owning views build their TextInputs in init, so reset their 2038 // registrations here 2039 session.text_inputs.clearRetainingCapacity(); 2040 2041 try root.build(allocator, .{ 2042 .min_size = .{ .width = null, .height = 40 }, 2043 .max_size = .{ .width = 100, .height = null }, 2044 }, root.getFocus()); 2045 2046 return root; 2047 } 2048 2049 // native and server-side sessions choose from the embedded collection. WASM 2050 // keeps the art serialized by the server instead. 2051 fn chooseAnsiArtForNavigation(session: *Session) void { 2052 if (comptime ansi_arts.len == 0) return; 2053 const io = session.io orelse return; 2054 var random: usize = undefined; 2055 io.random(std.mem.asBytes(&random)); 2056 session.data.ansi_art = ansi_arts[random % ansi_arts.len]; 2057 } 2058