╭ Markdown.zig ─────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const ui = @import("../../ui.zig"); 3 const md = @import("../../markdown.zig"); 4 const Files = @import("Files.zig"); 5 const xit = @import("xit"); 6 const xitui = xit.xitui; 7 const wgt = xitui.widget; 8 const layout = xitui.layout; 9 const Key = xitui.input.Key; 10 const Grid = xitui.grid.Grid; 11 const Focus = xitui.focus.Focus; 12 const RichText = ui.widget.RichText; 13 14 // a rendered markdown file: one child per block, word-wrapped to the width. 15 pub const View = struct { 16 box: wgt.Box(ui.Widget), 17 // where focus rests off the links, covering the whole document; it's 18 // registered after the links so they win hit-testing 19 body: *Focus, 20 21 pub fn init(allocator: std.mem.Allocator, doc: md.Document, data: *const Files, file_path: []const u8, page_arena: *std.heap.ArenaAllocator) !View { 22 const builder: Builder = .{ 23 .allocator = allocator, 24 .data = data, 25 .dir = if (std.mem.lastIndexOfScalar(u8, file_path, '/')) |slash| file_path[0..slash] else "", 26 .page_arena = page_arena, 27 .fonts = !hasNonAsciiHeading(doc.blocks), 28 }; 29 var box = try builder.blocksBox(doc.blocks, true); 30 errdefer box.deinit(allocator); 31 const body = try Focus.create(allocator, .container); 32 body.mode = .all; 33 box.getFocus().child_id = body.id; 34 return .{ .box = box, .body = body }; 35 } 36 37 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 38 self.box.deinit(allocator); 39 self.body.destroy(allocator); 40 } 41 42 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 43 self.clearGrid(); 44 try self.box.build(allocator, constraint, root_focus); 45 const grid = self.box.getGrid() orelse return; 46 try self.box.getFocus().addChild(allocator, self.body, grid.size, 0, 0); 47 } 48 49 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 50 _ = self; 51 _ = allocator; 52 _ = key; 53 _ = root_focus; 54 } 55 56 pub fn clearGrid(self: *View) void { 57 self.box.clearGrid(); 58 } 59 60 pub fn getGrid(self: View) ?Grid { 61 return self.box.getGrid(); 62 } 63 64 pub fn getFocus(self: *View) *Focus { 65 return self.box.getFocus(); 66 } 67 68 // whether `id` is the body or one of the links 69 pub fn owns(self: *View, id: usize) bool { 70 return self.box.getFocus().children.contains(id); 71 } 72 73 // focus the next or previous link in document order, scrolling it into 74 // view; from the body, start at the first link in view. stepping past 75 // either end returns to the body. 76 pub fn stepLink(self: *View, allocator: std.mem.Allocator, root_focus: *Focus, scroll: *wgt.Scroll(ui.Widget), forward: bool) !void { 77 var stops: std.ArrayList(usize) = .empty; 78 defer stops.deinit(allocator); 79 try collectStops(allocator, &self.box, self.box.getFocus(), &stops); 80 const children = self.box.getFocus().children; 81 82 const current = if (root_focus.grandchild_id) |id| std.mem.indexOfScalar(usize, stops.items, id) else null; 83 const target: ?usize = if (current) |c| 84 (if (forward) (if (c + 1 < stops.items.len) c + 1 else null) else (if (c > 0) c - 1 else null)) 85 else entry: { 86 const top: usize = @intCast(@max(scroll.y, 0)); 87 if (forward) { 88 for (stops.items, 0..) |id, i| { 89 if ((children.get(id) orelse unreachable).rect.y >= top) break :entry i; 90 } 91 break :entry null; 92 } 93 const bottom: usize = if (scroll.grid) |g| top + g.size.height - scroll.bar_h else std.math.maxInt(usize); 94 var i = stops.items.len; 95 while (i > 0) { 96 i -= 1; 97 if ((children.get(stops.items[i]) orelse unreachable).rect.y < bottom) break :entry i; 98 } 99 break :entry null; 100 }; 101 102 const id = stops.items[target orelse return root_focus.setFocus(self.body.id)]; 103 root_focus.setFocus(id); 104 const rect = (children.get(id) orelse unreachable).rect; 105 scroll.scrollToRect(.{ .x = @intCast(rect.x), .y = @intCast(rect.y), .size = rect.size }); 106 } 107 }; 108 109 // the focus ids of the laid-out links under `box`, in document order 110 fn collectStops(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), view_focus: *Focus, stops: *std.ArrayList(usize)) !void { 111 for (box.children.values()) |*child| switch (child.widget) { 112 .rich_text => |*rich_text| try addStops(allocator, rich_text, view_focus, stops), 113 .markdown_heading => |*heading| try addStops(allocator, &heading.text, view_focus, stops), 114 .markdown_gutter => |*gutter| try collectStops(allocator, &gutter.child, view_focus, stops), 115 .box => |*inner| try collectStops(allocator, inner, view_focus, stops), 116 else => {}, 117 }; 118 } 119 120 // each link's first piece is its stop 121 fn addStops(allocator: std.mem.Allocator, rich_text: *RichText, view_focus: *Focus, stops: *std.ArrayList(usize)) !void { 122 for (rich_text.link_focuses) |focuses| { 123 if (focuses.items.len > 0 and view_focus.children.contains(focuses.items[0].id)) try stops.append(allocator, focuses.items[0].id); 124 } 125 } 126 127 // turns blocks into widgets, resolving links against the viewed file. 128 const Builder = struct { 129 allocator: std.mem.Allocator, 130 data: *const Files, 131 // the viewed file's directory, which relative links resolve against 132 dir: []const u8, 133 page_arena: *std.heap.ArenaAllocator, 134 // whether headings may use the title fonts 135 fonts: bool, 136 137 // a vertical box of `blocks`, with a blank row between them when `gap` 138 fn blocksBox(b: Builder, blocks: []const md.Block, gap: bool) anyerror!wgt.Box(ui.Widget) { 139 var box = try wgt.Box(ui.Widget).init(b.allocator, .{ .border_style = null, .direction = .vert, .gap = @intFromBool(gap) }); 140 errdefer box.deinit(b.allocator); 141 for (blocks) |block| try b.addBlock(&box, block); 142 return box; 143 } 144 145 fn addBlock(b: Builder, box: *wgt.Box(ui.Widget), block: md.Block) !void { 146 const allocator = b.allocator; 147 switch (block) { 148 .heading => |heading| { 149 const spans = try b.runSpans(heading.inlines, .{ .bold = true }); 150 try put(allocator, box, .{ .markdown_heading = try Heading.init(allocator, b.page_arena, heading.level, spans, b.fonts) }); 151 }, 152 .paragraph => |inlines| try put(allocator, box, .{ .rich_text = try RichText.init(allocator, try b.runSpans(inlines, .{})) }), 153 .code => |lines| { 154 const text = try std.mem.join(b.page_arena.allocator(), "\n", lines); 155 try put(allocator, box, .{ .text_box = try wgt.TextBox.init(allocator, text, .{ .border_style = .single, .round_corners = true, .wrap_kind = .char }) }); 156 }, 157 .quote => |blocks| { 158 const gutter = gutter: { 159 var inner = try b.blocksBox(blocks, true); 160 errdefer inner.deinit(allocator); 161 break :gutter try Gutter.init(allocator, "│ ", true, .{ .dim = true }, inner); 162 }; 163 try put(allocator, box, .{ .markdown_gutter = gutter }); 164 }, 165 .list => |list| { 166 var list_box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); 167 errdefer list_box.deinit(allocator); 168 // ordered markers are right-aligned to the widest number 169 const number_width = std.fmt.count("{d}", .{list.start + list.items.len -| 1}); 170 for (list.items, 0..) |item, i| { 171 const marker = if (item.task) |checked| 172 (if (checked) "☑ " else "☐ ") 173 else if (list.ordered) 174 try std.fmt.allocPrint(b.page_arena.allocator(), "{[n]d:>[w]}. ", .{ .n = list.start + i, .w = number_width }) 175 else 176 "• "; 177 const gutter = gutter: { 178 var inner = try b.blocksBox(item.blocks, false); 179 errdefer inner.deinit(allocator); 180 break :gutter try Gutter.init(allocator, marker, false, .{}, inner); 181 }; 182 try put(allocator, &list_box, .{ .markdown_gutter = gutter }); 183 } 184 try put(allocator, box, .{ .box = list_box }); 185 }, 186 // wider than any pane, and clipped to the width 187 .rule => try put(allocator, box, .{ .text_box = try wgt.TextBox.init(allocator, "─" ** 512, .{ .border_style = null, .wrap_kind = .none, .style = .{ .dim = true } }) }), 188 .raw => |lines| { 189 const text = try std.mem.join(b.page_arena.allocator(), "\n", lines); 190 try put(allocator, box, .{ .text_box = try wgt.TextBox.init(allocator, text, .{ .border_style = null, .wrap_kind = .none }) }); 191 }, 192 } 193 } 194 195 // styled spans for `inlines` over `base`, with each link resolved 196 fn runSpans(b: Builder, inlines: []const md.Inline, base: Grid.Style) ![]const RichText.Span { 197 const out = try b.page_arena.allocator().alloc(RichText.Span, inlines.len); 198 for (inlines, out) |run, *span| { 199 const link = if (run.link) |dest| try b.resolveLink(dest) else ""; 200 var style = base; 201 style.bold = style.bold or run.style.bold; 202 style.italic = run.style.italic; 203 style.strikethrough = run.style.strike; 204 if (run.style.code) style.fg = .{ .ansi = .yellow }; 205 if (link.len > 0) { 206 style.fg = .{ .ansi = .cyan }; 207 style.underline = true; 208 } 209 span.* = .{ .text = run.text, .style = style, .link = link }; 210 } 211 return out; 212 } 213 214 // the focus kind a link follows, or "" when it isn't clickable: web links 215 // are raw links, and a repo path is a files route relative to this file 216 fn resolveLink(b: Builder, dest: []const u8) ![]const u8 { 217 const aa = b.page_arena.allocator(); 218 for ([_][]const u8{ "http:", "https:", "mailto:" }) |scheme| { 219 if (std.ascii.startsWithIgnoreCase(dest, scheme)) return std.fmt.allocPrint(aa, "{s}{s}", .{ ui.raw_link_prefix, dest }); 220 } 221 if (hasScheme(dest) or std.mem.startsWith(u8, dest, "//")) return ""; 222 const end = std.mem.indexOfAny(u8, dest, "?#") orelse dest.len; 223 if (end == 0) return ""; 224 const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, dest[0..end])); 225 226 var segments: std.ArrayList([]const u8) = .empty; 227 if (decoded[0] != '/') { 228 var base = std.mem.tokenizeScalar(u8, b.dir, '/'); 229 while (base.next()) |segment| try segments.append(aa, segment); 230 } 231 var parts = std.mem.tokenizeScalar(u8, decoded, '/'); 232 while (parts.next()) |part| { 233 if (std.mem.eql(u8, part, ".")) continue; 234 if (std.mem.eql(u8, part, "..")) { 235 // climbing above the root isn't a repo path 236 if (segments.pop() == null) return ""; 237 continue; 238 } 239 try segments.append(aa, part); 240 } 241 const path = try std.mem.join(aa, "/", segments.items); 242 const route = b.data.filesRoute(path, 0) orelse return ""; 243 return std.fmt.allocPrint(aa, "a:{s}", .{try route.toUrl(b.page_arena)}); 244 } 245 }; 246 247 // whether any heading, however nested, has non-ascii text, in which case 248 // none use the title fonts so they all look alike 249 fn hasNonAsciiHeading(blocks: []const md.Block) bool { 250 for (blocks) |block| switch (block) { 251 .heading => |heading| for (heading.inlines) |run| { 252 for (run.text) |c| if (!std.ascii.isAscii(c)) return true; 253 }, 254 .quote => |inner| if (hasNonAsciiHeading(inner)) return true, 255 .list => |list| for (list.items) |item| { 256 if (hasNonAsciiHeading(item.blocks)) return true; 257 }, 258 else => {}, 259 }; 260 return false; 261 } 262 263 // whether `dest` starts with a url scheme 264 fn hasScheme(dest: []const u8) bool { 265 const colon = std.mem.indexOfScalar(u8, dest, ':') orelse return false; 266 if (colon == 0 or !std.ascii.isAlphabetic(dest[0])) return false; 267 for (dest[0..colon]) |c| { 268 if (!std.ascii.isAlphanumeric(c) and c != '+' and c != '.' and c != '-') return false; 269 } 270 return true; 271 } 272 273 fn put(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), widget: ui.Widget) !void { 274 var owned = widget; 275 errdefer owned.deinit(allocator); 276 try box.children.put(allocator, owned.getFocus().id, .{ .widget = owned, .rect = null, .min_size = null }); 277 } 278 279 // a heading in the title font when it fits, else the subtitle font, else 280 // bold text. the fonts only cover plain text of their own glyphs. 281 pub const Heading = struct { 282 focus: *Focus, 283 text: RichText, 284 title: ?ui.Title.View = null, 285 title_width: usize = 0, 286 sub_title: ?ui.SubTitle.View = null, 287 sub_title_width: usize = 0, 288 shown: enum { title, sub_title, text } = .text, 289 290 fn init(allocator: std.mem.Allocator, page_arena: *std.heap.ArenaAllocator, level: u3, spans: []const RichText.Span, fonts: bool) !Heading { 291 var self: Heading = .{ 292 .focus = try Focus.create(allocator, .container), 293 .text = undefined, 294 }; 295 errdefer self.focus.destroy(allocator); 296 self.text = try RichText.init(allocator, spans); 297 errdefer self.text.deinit(allocator); 298 299 var plain: std.ArrayList(u8) = .empty; 300 var has_links = false; 301 for (spans) |span| { 302 try plain.appendSlice(page_arena.allocator(), span.text); 303 has_links = has_links or span.link.len > 0; 304 } 305 const text = std.mem.trim(u8, plain.items, " "); 306 if (!fonts or level > 2 or has_links or text.len == 0 or !ui.Title.covers(text) or !ui.SubTitle.covers(text)) return self; 307 308 if (level == 1) { 309 const title = try ui.Title.init(page_arena, text, .solid); 310 self.title_width = try title.width(); 311 self.title = try ui.Title.View.init(allocator, &title); 312 } 313 errdefer if (self.title) |*title| title.deinit(allocator); 314 const sub_title = try ui.SubTitle.init(page_arena, text); 315 self.sub_title_width = try sub_title.width(); 316 self.sub_title = try ui.SubTitle.View.init(allocator, &sub_title); 317 return self; 318 } 319 320 pub fn deinit(self: *Heading, allocator: std.mem.Allocator) void { 321 self.focus.destroy(allocator); 322 self.text.deinit(allocator); 323 if (self.title) |*title| title.deinit(allocator); 324 if (self.sub_title) |*sub_title| sub_title.deinit(allocator); 325 } 326 327 pub fn build(self: *Heading, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 328 self.clearGrid(); 329 self.focus.clear(); 330 const max_width = constraint.max_size.width orelse std.math.maxInt(usize); 331 self.shown = if (self.title != null and self.title_width <= max_width) 332 .title 333 else if (self.sub_title != null and self.sub_title_width <= max_width) 334 .sub_title 335 else 336 .text; 337 switch (self.shown) { 338 .title => try self.buildShown(allocator, constraint, root_focus, if (self.title) |*title| title else unreachable), 339 .sub_title => try self.buildShown(allocator, constraint, root_focus, if (self.sub_title) |*sub_title| sub_title else unreachable), 340 .text => try self.buildShown(allocator, constraint, root_focus, &self.text), 341 } 342 } 343 344 fn buildShown(self: *Heading, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus, widget: anytype) !void { 345 try widget.build(allocator, constraint, root_focus); 346 const grid = widget.getGrid() orelse return; 347 try self.focus.addChild(allocator, widget.getFocus(), grid.size, 0, 0); 348 } 349 350 pub fn input(self: *Heading, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 351 _ = self; 352 _ = allocator; 353 _ = key; 354 _ = root_focus; 355 } 356 357 pub fn clearGrid(self: *Heading) void { 358 self.text.clearGrid(); 359 if (self.title) |*title| title.clearGrid(); 360 if (self.sub_title) |*sub_title| sub_title.clearGrid(); 361 } 362 363 pub fn getGrid(self: Heading) ?Grid { 364 return switch (self.shown) { 365 .title => if (self.title) |title| title.getGrid() else null, 366 .sub_title => if (self.sub_title) |sub_title| sub_title.getGrid() else null, 367 .text => self.text.getGrid(), 368 }; 369 } 370 371 pub fn getFocus(self: *Heading) *Focus { 372 return self.focus; 373 } 374 }; 375 376 // a marker column beside a child, so wrapped lines hang under the child's 377 // first line: a list item's bullet, or a quote's bar on every row. 378 pub const Gutter = struct { 379 focus: *Focus, 380 grid: ?Grid, 381 child: wgt.Box(ui.Widget), 382 marker: []const u8, 383 // draw the marker on every row rather than only the first 384 repeat: bool, 385 style: Grid.Style, 386 387 // takes ownership of `child` on success 388 fn init(allocator: std.mem.Allocator, marker: []const u8, repeat: bool, style: Grid.Style, child: wgt.Box(ui.Widget)) !Gutter { 389 return .{ .focus = try Focus.create(allocator, .container), .grid = null, .child = child, .marker = marker, .repeat = repeat, .style = style }; 390 } 391 392 pub fn deinit(self: *Gutter, allocator: std.mem.Allocator) void { 393 self.focus.destroy(allocator); 394 self.clearGrid(); 395 self.child.deinit(allocator); 396 } 397 398 pub fn build(self: *Gutter, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 399 self.clearGrid(); 400 self.focus.clear(); 401 const marker_width = try xitui.width.displayWidth(self.marker); 402 if (constraint.max_size.width) |w| if (w <= marker_width) return; 403 if (constraint.max_size.height == 0) return; 404 405 try self.child.build(allocator, .{ 406 .min_size = .{ .width = null, .height = null }, 407 .max_size = .{ .width = if (constraint.max_size.width) |w| w - marker_width else null, .height = constraint.max_size.height }, 408 }, root_focus); 409 const child_grid = self.child.getGrid(); 410 const child_size: layout.Size = if (child_grid) |g| g.size else .{ .width = 0, .height = 0 }; 411 412 var grid = try Grid.init(allocator, .{ .width = marker_width + child_size.width, .height = @max(1, child_size.height) }); 413 errdefer grid.deinit(); 414 for (0..if (self.repeat) grid.size.height else 1) |y| { 415 var x: usize = 0; 416 var utf8 = (try std.unicode.Utf8View.init(self.marker)).iterator(); 417 while (utf8.nextCodepoint()) |rune| { 418 (try grid.cell(x, y)).style = self.style; 419 try grid.setRune(x, y, rune); 420 x += xitui.width.cellWidth(rune); 421 } 422 } 423 if (child_grid) |g| { 424 try grid.drawGrid(g, marker_width, 0); 425 try self.focus.addChild(allocator, self.child.getFocus(), g.size, marker_width, 0); 426 } 427 self.grid = grid; 428 } 429 430 pub fn input(self: *Gutter, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 431 _ = self; 432 _ = allocator; 433 _ = key; 434 _ = root_focus; 435 } 436 437 pub fn clearGrid(self: *Gutter) void { 438 if (self.grid) |*grid| { 439 grid.deinit(); 440 self.grid = null; 441 } 442 self.child.clearGrid(); 443 } 444 445 pub fn getGrid(self: Gutter) ?Grid { 446 return self.grid; 447 } 448 449 pub fn getFocus(self: *Gutter) *Focus { 450 return self.focus; 451 } 452 }; 453