╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │355 \\any significant changes before submitting a pull request. │ │356 \\See the docs under `docs/dev` for more details. │ │357 \\ │ │358 - \\## A Heading Far Too Long To Fit In The Subtitle Font Within A Pane│ │358 + \\## A Heading Too Long To Fit On One Line In The Subtitle Font │ │359 \\ │ │360 - \\Long headings fall back to bold text. │ │360 + \\Long headings wrap onto more lines in their font. │ │361 \\ │ │362 \\## License │ │363 \\ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/Repo/Markdown.zig│ ╰────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │278 try box.children.put(allocator, owned.getFocus().id, .{ .widget = owned, .rect = null, .min_size = null });│ │279 } │ │280 │ │281 -// a heading in the title font when it fits, else the subtitle font, else │ │282 -// bold text. │ │281 +// a heading in its level's title font, word-wrapped to the width, or bold │ │282 +// text when there's no font for it. │ │283 pub const Heading = struct { │ │284 focus: *Focus, │ │285 text: RichText, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │286 - title: ?ui.Title.View = null, │ │287 - title_width: usize = 0, │ │288 - sub_title: ?ui.SubTitle.View = null, │ │289 - sub_title_width: usize = 0, │ │290 - shown: enum { title, sub_title, text } = .text, │ │286 + // the font this heading draws in, if any, and the text it draws │ │287 + font: ?enum { title, sub_title } = null, │ │288 + plain: []const u8 = "", │ │289 + font_box: wgt.TextBox, │ │290 │ │291 fn init(allocator: std.mem.Allocator, page_arena: *std.heap.ArenaAllocator, level: u3, spans: []const RichText.Span, fonts: bool) !Heading {│ │293 - var self: Heading = .{ │ │294 - .focus = try Focus.create(allocator, .container), │ │295 - .text = undefined, │ │296 - }; │ │297 - errdefer self.focus.destroy(allocator); │ │292 + const focus = try Focus.create(allocator, .container); │ │293 + errdefer focus.destroy(allocator); │ │294 + │ │295 // the bold fallback leads with the level's #s, so levels stay apart │ │296 const marked = try page_arena.allocator().alloc(RichText.Span, spans.len + 1); │ │297 marked[0] = .{ .text = try std.fmt.allocPrint(page_arena.allocator(), "{s} ", .{"######"[0..level]}), .style = .{ .bold = true } }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │298 @memcpy(marked[1..], spans); │ │302 - self.text = try RichText.init(allocator, marked); │ │303 - errdefer self.text.deinit(allocator); │ │299 + var text = try RichText.init(allocator, marked); │ │300 + errdefer text.deinit(allocator); │ │301 + │ │302 + var font_box = try wgt.TextBox.init(allocator, "", .{ .border_style = null, .wrap_kind = .none });│ │303 + errdefer font_box.deinit(allocator); │ │304 + var self: Heading = .{ .focus = focus, .text = text, .font_box = font_box }; │ │305 │ │306 var plain: std.ArrayList(u8) = .empty; │ │307 var has_links = false; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │309 try plain.appendSlice(page_arena.allocator(), span.text); │ │310 has_links = has_links or span.link.len > 0; │ │311 } │ │311 - const text = std.mem.trim(u8, plain.items, " "); │ │312 - if (!fonts or level > 2 or has_links or text.len == 0) return self; │ │313 - │ │314 - if (level == 1) { │ │315 - const title = try ui.Title.init(page_arena, text, .solid); │ │316 - self.title_width = try title.width(); │ │317 - self.title = try ui.Title.View.init(allocator, &title); │ │318 - } │ │319 - errdefer if (self.title) |*title| title.deinit(allocator); │ │320 - const sub_title = try ui.SubTitle.init(page_arena, text); │ │321 - self.sub_title_width = try sub_title.width(); │ │322 - self.sub_title = try ui.SubTitle.View.init(allocator, &sub_title); │ │312 + self.plain = std.mem.trim(u8, plain.items, " "); │ │313 + if (fonts and level <= 2 and !has_links and self.plain.len > 0) self.font = if (level == 1) .title else .sub_title;│ │314 return self; │ │315 } │ │316 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │317 pub fn deinit(self: *Heading, allocator: std.mem.Allocator) void { │ │318 self.focus.destroy(allocator); │ │319 self.text.deinit(allocator); │ │329 - if (self.title) |*title| title.deinit(allocator); │ │330 - if (self.sub_title) |*sub_title| sub_title.deinit(allocator); │ │320 + self.font_box.deinit(allocator); │ │321 } │ │322 │ │323 pub fn build(self: *Heading, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void {│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │324 self.clearGrid(); │ │325 self.focus.clear(); │ │336 - const max_width = constraint.max_size.width orelse std.math.maxInt(usize); │ │337 - self.shown = if (self.title != null and self.title_width <= max_width) │ │338 - .title │ │339 - else if (self.sub_title != null and self.sub_title_width <= max_width) │ │340 - .sub_title │ │341 - else │ │342 - .text; │ │343 - switch (self.shown) { │ │344 - .title => try self.buildShown(allocator, constraint, root_focus, if (self.title) |*title| title else unreachable), │ │345 - .sub_title => try self.buildShown(allocator, constraint, root_focus, if (self.sub_title) |*sub_title| sub_title else unreachable),│ │346 - .text => try self.buildShown(allocator, constraint, root_focus, &self.text), │ │326 + if (try self.wrapFont(allocator, constraint.max_size.width)) { │ │327 + try self.buildShown(allocator, constraint, root_focus, &self.font_box); │ │328 + } else { │ │329 + try self.buildShown(allocator, constraint, root_focus, &self.text); │ │330 + } │ │331 + } │ │332 + │ │333 + // fill the font box with the heading wrapped at `max_width`, or return │ │334 + // false when there's no font │ │335 + fn wrapFont(self: *Heading, allocator: std.mem.Allocator, max_width: ?usize) !bool { │ │336 + const font = self.font orelse return false; │ │337 + // a glyph and the gap after it, so n glyphs take n * char_width - 1 columns │ │338 + const char_width: usize = if (font == .title) 4 else 3; │ │339 + const max_chars = if (max_width) |w| (w + 1) / char_width else null; │ │340 + │ │341 + var arena = std.heap.ArenaAllocator.init(allocator); │ │342 + defer arena.deinit(); │ │343 + const aa = arena.allocator(); │ │344 + // the fonts only cover ascii, so each byte is one char │ │345 + const chars = try aa.alloc(u21, self.plain.len); │ │346 + for (self.plain, chars) |c, *char| char.* = c; │ │347 + var lines: std.ArrayList(wgt.Line) = .empty; │ │348 + try wgt.wrapLines(aa, &lines, chars, .word, max_chars); │ │349 + │ │350 + // a blank row between lines keeps the glyphs from touching │ │351 + var out: std.ArrayList(u8) = .empty; │ │352 + for (lines.items, 0..) |line, i| { │ │353 + if (i > 0) try out.appendSlice(aa, "\n\n"); │ │354 + const text = std.mem.trimEnd(u8, self.plain[line.start..line.end], " "); │ │355 + try out.appendSlice(aa, switch (font) { │ │356 + .title => (try ui.Title.init(&arena, text, .solid)).content, │ │357 + .sub_title => (try ui.SubTitle.init(&arena, text)).content, │ │358 + }); │ │359 } │ │360 + try self.font_box.setContent(allocator, out.items); │ │361 + return true; │ │362 } │ │363 │ │364 fn buildShown(self: *Heading, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus, widget: anytype) !void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────╮ │376 │ │377 pub fn clearGrid(self: *Heading) void { │ │378 self.text.clearGrid(); │ │365 - if (self.title) |*title| title.clearGrid(); │ │366 - if (self.sub_title) |*sub_title| sub_title.clearGrid();│ │379 + self.font_box.clearGrid(); │ │380 } │ │381 │ │382 pub fn getGrid(self: Heading) ?Grid { │ ╰────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │370 - return switch (self.shown) { │ │371 - .title => if (self.title) |title| title.getGrid() else null, │ │372 - .sub_title => if (self.sub_title) |sub_title| sub_title.getGrid() else null,│ │373 - .text => self.text.getGrid(), │ │374 - }; │ │383 + // only the one last built has a grid │ │384 + return self.font_box.getGrid() orelse self.text.getGrid(); │ │385 } │ │386 │ │387 pub fn getFocus(self: *Heading) *Focus { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────╮ │src/ui/SubTitle.zig│ ╰───────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │18 }; │ │19 } │ │20 │ │21 -pub fn width(self: Self) !usize { │ │22 - const line_end = std.mem.indexOfScalar(u8, self.content, '\n') orelse self.content.len;│ │23 - return xitui.width.displayWidth(self.content[0..line_end]); │ │24 -} │ │25 - │ │21 pub const View = struct { │ │22 text_box: wgt.TextBox, │ │23 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯