╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ ui.zig ───────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
2704 while (iter.next()) |entry| { 2705 switch (entry.value_ptr.focus.kind) { 2706 .custom => |custom| if (std.mem.eql(u8, custom, Quit.tab_kind)) { 2707 root_focus.setFocus(entry.key_ptr.*); 2708 // a Stack only builds its selected child, so the quit 2709 // button isn't in the focus tree until a build runs with 2710 // the quit tab selected. build once, then send a 2711 // synthetic arrow_down to drop focus from the tab onto 2712 // the button so it's ready to confirm. 2713 try self.root.build(allocator, .{ 2714 .min_size = .{ .width = null, .height = 40 }, 2715 .max_size = .{ .width = 80, .height = null }, 2716 }, root_focus); 2717 try self.root.input(allocator, .arrow_down, root_focus); 2718 break; 2719 }, 2720 else => {}, 2721 } 2722 } 2723 return; 2724 } 2725 2726 // forward navigation: navigate() set next_page to the page to move to (a 2727 // cross-page link or tab change crossing pages) 2728 if (session.next_page) |route| { 2729 session.next_page = null; 2730 if (session.haxy_moment == null and session.local == null) return; 2731 const previous_route = session.data.current_page; 2732 // the page we navigated to becomes the current page 2733 session.data.current_page = route; 2734 session.back = .available; 2735 2736 const arena = try allocator.create(std.heap.ArenaAllocator); 2737 arena.* = std.heap.ArenaAllocator.init(allocator); 2738 errdefer freeArena(allocator, arena); 2739 2740 session.page_arena = arena; 2741 2742 const page = try arena.allocator().create(Page); 2743 page.* = try Page.init(arena, session, route); 2744 const new_root = try initRoot(allocator, page, session); 2745 2746 try self.history.append(allocator, .{ .root = self.root, .route = previous_route, .arena = self.arena }); 2747 // drop the oldest entry (freeing its widget tree and arena) once over cap 2748 if (self.history.items.len > max_history) { 2749 var oldest = self.history.orderedRemove(0); 2750 oldest.root.deinit(allocator); 2751 freeArena(allocator, oldest.arena); 2752 } 2753 self.root = new_root; 2754 self.arena = arena; 2755 } 2756 } 2757 }; 2758 2759 pub fn initRoot(allocator: std.mem.Allocator, page: *const Page, session: *Session) !Widget { 2760 const page_widget: Widget = switch (page.*) { 2761 .home => |*p| .{ .home = try .init(allocator, p, session) }, 2762 .user => |*p| .{ .user = try .init(allocator, p, session) }, 2763 .repo => |*p| .{ .repo = try .init(allocator, p, session) }, 2764 .fork => |*p| .{ .fork = try .init(allocator, p, session) }, 2765 }; 2766 2767 chooseAnsiArtForNavigation(session); 2768 2769 // on the TUI/SSH, the page sits above a one-row footer showing the url 2770 var root = if (session.is_terminal) blk: { 2771 var box = try wgt.Box(Widget).init(allocator, .{ .border_style = null, .direction = .vert }); 2772 errdefer box.deinit(allocator); 2773 const bg_id = bg_blk: { 2774 var background = try widget.AnsiBackground.init(allocator, page_widget, session); 2775 errdefer background.deinit(allocator); 2776 const id = background.getFocus().id; 2777 try box.children.put(allocator, id, .{ .widget = .{ .background = background }, .rect = null, .min_size = null }); 2778 break :bg_blk id; 2779 }; 2780 { 2781 var footer = try widget.Footer.init(allocator, session); 2782 errdefer footer.deinit(allocator); 2783 try box.children.put(allocator, footer.getFocus().id, .{ .widget = .{ .footer = footer }, .rect = null, .min_size = .{ .width = null, .height = 1 } }); 2784 } 2785 box.getFocus().child_id = bg_id; 2786 break :blk Widget{ .box = box }; 2787 } else Widget{ .background = try widget.AnsiBackground.init(allocator, page_widget, session) }; 2788 errdefer root.deinit(allocator); 2789 2790 // input-owning views build their TextInputs in init, so reset their 2791 // registrations here 2792 session.text_inputs.clearRetainingCapacity(); 2793 2794 try root.build(allocator, .{ 2795 .min_size = .{ .width = null, .height = 40 }, 2796 .max_size = .{ .width = 100, .height = null }, 2797 }, root.getFocus()); 2798 2799 return root; 2800 } 2801 2802 // native and server-side sessions choose from the embedded collection. WASM 2803 // keeps the art serialized by the server instead. 2804 fn chooseAnsiArtForNavigation(session: *Session) void { 2805 if (comptime ansi_arts.len == 0) return; 2806 const io = session.io orelse return; 2807 var random: usize = undefined; 2808 io.random(std.mem.asBytes(&random)); 2809 session.data.ansi_art = ansi_arts[random % ansi_arts.len]; 2810 } 2811