╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Comment.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────────╮ │ 9 const Key = xitui.input.Key; │ │10 const Grid = xitui.grid.Grid; │ │11 const Focus = xitui.focus.Focus; │ │12 +const Markdown = @import("Markdown.zig");│ │13 │ │14 pub const page_size = 10; │ │15 │ ╰─────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │213 } │ │214 │ │215 const body_text = if (entry.comment.removed) "(removed)" else entry.comment.event.body; │ │215 - var body_box = try wgt.TextBox.init(allocator, body_text, .{ .border_style = .single, .round_corners = true, .wrap_kind = .word }); │ │216 - errdefer body_box.deinit(allocator); │ │217 - body_box.getFocus().mode = .all; │ │218 - try box.children.put(allocator, body_box.getFocus().id, .{ .widget = .{ .text_box = body_box }, .rect = null, .min_size = null }); │ │216 + var body_frame = try Markdown.Frame.init(allocator, body_text, "", "", session.page_arena); │ │217 + errdefer body_frame.deinit(allocator); │ │218 + try box.children.put(allocator, body_frame.getFocus().id, .{ .widget = .{ .markdown_frame = body_frame }, .rect = null, .min_size = null });│ │219 │ │220 var gap = try wgt.Text.init(allocator, ""); │ │221 errdefer gap.deinit(allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │230 return &self.box.children.values()[metadata_index].widget.box; │ │231 } │ │232 │ │233 - fn body(self: *Item) *wgt.TextBox { │ │234 - return &self.box.children.values()[body_index].widget.text_box; │ │233 + pub fn body(self: *Item) *Markdown.Frame { │ │234 + return &self.box.children.values()[body_index].widget.markdown_frame;│ │235 } │ │236 │ │237 pub fn bodyFocused(self: *Item) bool { │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │869 }, │ │870 // step between the rendered markdown's links; the web leaves tab to the browser │ │871 .tab, .back_tab => if (on_content and self.session.is_terminal) if (self.markdownView()) |view| {│ │872 - try view.stepLink(allocator, root_focus, sc, key == .tab); │ │872 + try view.stepLink(allocator, root_focus, sc, 0, 0, key == .tab); │ │873 }, │ │874 else => {}, │ │875 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/Repo/Markdown.zig│ ╰────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────╮ │51 markdown: ui.Repo.Markdown.View, │ │52 markdown_heading: ui.Repo.Markdown.Heading,│ │53 markdown_gutter: ui.Repo.Markdown.Gutter, │ │54 + markdown_frame: ui.Repo.Markdown.Frame, │ │55 repo_commits: ui.Repo.Commits.View, │ │56 diff_view: ui.Repo.Diff.View, │ │57 repo_refs: ui.Repo.Refs.View, │ ╰───────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭─────────────────────────────────────────────────────────╮ │18 const diff3 = @import("../../diff3.zig"); │ │19 const Comment = @import("../Repo/Comment.zig"); │ │20 const Attachment = @import("../Repo/Attachment.zig");│ │21 +const Markdown = @import("../Repo/Markdown.zig"); │ │22 │ │23 const Widget = widget.Widget; │ │24 const WordFlow = widget.WordFlow; │ ╰─────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯