╭ changed ───────────────╮ │src/ui/Repo/Markdown.zig│ ╰────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │73 │ │74 // focus the next or previous link in document order, scrolling it into │ │75 // view; from the body, start at the first link in view. stepping past │ │76 - // either end returns to the body. │ │77 - pub fn stepLink(self: *View, allocator: std.mem.Allocator, root_focus: *Focus, scroll: *wgt.Scroll(ui.Widget), forward: bool) !void { │ │76 + // either end returns to the body. `x` and `y` place the view in `scroll`'s │ │77 + // content. │ │78 + pub fn stepLink(self: *View, allocator: std.mem.Allocator, root_focus: *Focus, scroll: *wgt.Scroll(ui.Widget), x: isize, y: isize, forward: bool) !void {│ │79 var stops: std.ArrayList(usize) = .empty; │ │80 defer stops.deinit(allocator); │ │81 try collectStops(allocator, &self.box, self.box.getFocus(), &stops); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │85 const target: ?usize = if (current) |c| │ │86 (if (forward) (if (c + 1 < stops.items.len) c + 1 else null) else (if (c > 0) c - 1 else null)) │ │87 else entry: { │ │87 - const top: usize = @intCast(@max(scroll.y, 0)); │ │88 + // the visible rows, in the view's coordinates │ │89 + const top = @max(scroll.y, 0) - y; │ │90 + const bottom = if (scroll.grid) |g| top + @as(isize, @intCast(g.size.height - scroll.bar_h)) else std.math.maxInt(isize);│ │91 if (forward) { │ │92 for (stops.items, 0..) |id, i| { │ │93 if ((children.get(id) orelse unreachable).rect.y >= top) break :entry i; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │94 } │ │95 break :entry null; │ │96 } │ │94 - const bottom: usize = if (scroll.grid) |g| top + g.size.height - scroll.bar_h else std.math.maxInt(usize);│ │97 var i = stops.items.len; │ │98 while (i > 0) { │ │99 i -= 1; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │105 const id = stops.items[target orelse return root_focus.setFocus(self.body.id)]; │ │106 root_focus.setFocus(id); │ │107 const rect = (children.get(id) orelse unreachable).rect; │ │106 - scroll.scrollToRect(.{ .x = @intCast(rect.x), .y = @intCast(rect.y), .size = rect.size }); │ │108 + scroll.scrollToRect(.{ .x = x + @as(isize, @intCast(rect.x)), .y = y + @as(isize, @intCast(rect.y)), .size = rect.size }); │ │109 + } │ │110 +}; │ │111 + │ │112 +// word-wrapped markdown in a rounded border that turns double while focus is inside │ │113 +pub const Frame = struct { │ │114 + box: wgt.Box(ui.Widget), │ │115 + │ │116 + pub fn init(allocator: std.mem.Allocator, text: []const u8, label: []const u8, bottom_label: []const u8, page_arena: *std.heap.ArenaAllocator) !Frame { │ │117 + const doc = try md.parseText(page_arena.allocator(), text); │ │118 + │ │119 + var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = .single, .round_corners = true, .direction = .vert, .label = label, .bottom_label = bottom_label });│ │120 + errdefer box.deinit(allocator); │ │121 + var markdown = try View.init(allocator, doc, null, "", page_arena); │ │122 + errdefer markdown.deinit(allocator); │ │123 + try box.children.put(allocator, markdown.getFocus().id, .{ .widget = .{ .markdown = markdown }, .rect = null, .min_size = null }); │ │124 + box.getFocus().child_id = markdown.getFocus().id; │ │125 + return .{ .box = box }; │ │126 + } │ │127 + │ │128 + pub fn deinit(self: *Frame, allocator: std.mem.Allocator) void { │ │129 + self.box.deinit(allocator); │ │130 + } │ │131 + │ │132 + pub fn build(self: *Frame, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │133 + const focused = if (root_focus.grandchild_id) |id| self.markdownView().owns(id) else false; │ │134 + self.box.options.border_style = if (focused) .double else .single; │ │135 + try self.box.build(allocator, constraint, root_focus); │ │136 + } │ │137 + │ │138 + pub fn input(self: *Frame, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │139 + _ = self; │ │140 + _ = allocator; │ │141 + _ = key; │ │142 + _ = root_focus; │ │143 + } │ │144 + │ │145 + pub fn clearGrid(self: *Frame) void { │ │146 + self.box.clearGrid(); │ │147 + } │ │148 + │ │149 + pub fn getGrid(self: Frame) ?Grid { │ │150 + return self.box.getGrid(); │ │151 + } │ │152 + │ │153 + pub fn getFocus(self: *Frame) *Focus { │ │154 + return self.box.getFocus(); │ │155 + } │ │156 + │ │157 + // step between the links, `x` and `y` placing the frame in `scroll`'s content │ │158 + pub fn stepLink(self: *Frame, allocator: std.mem.Allocator, root_focus: *Focus, scroll: *wgt.Scroll(ui.Widget), x: isize, y: isize, forward: bool) !void { │ │159 + // the view sits inside the border │ │160 + try self.markdownView().stepLink(allocator, root_focus, scroll, x + 1, y + 1, forward); │ │161 } │ │162 + │ │163 + // where focus rests off the links │ │164 + pub fn body(self: *Frame) *Focus { │ │165 + return self.markdownView().body; │ │166 + } │ │167 + │ │168 + fn markdownView(self: *Frame) *View { │ │169 + return &self.box.children.values()[0].widget.markdown; │ │170 + } │ │171 }; │ │172 │ │173 // the focus ids of the laid-out links under `box`, in document order │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯