╭ SubTitle.zig ─────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const ui = @import("../ui.zig"); 3 const xit = @import("xit"); 4 const xitui = xit.xitui; 5 const wgt = xitui.widget; 6 const layout = xitui.layout; 7 const Key = xitui.input.Key; 8 const Grid = xitui.grid.Grid; 9 const Focus = xitui.focus.Focus; 10 11 content: []const u8, 12 13 const Self = @This(); 14 15 pub fn init(arena: *std.heap.ArenaAllocator, orig_content: []const u8) !Self { 16 return .{ 17 .content = try renderSubTitle(arena.allocator(), orig_content), 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 26 pub const View = struct { 27 text_box: wgt.TextBox, 28 29 pub fn init(allocator: std.mem.Allocator, data: *const Self) !View { 30 var text_box = try wgt.TextBox.init(allocator, data.content, .{ .border_style = null, .wrap_kind = .none }); 31 errdefer text_box.deinit(allocator); 32 return .{ .text_box = text_box }; 33 } 34 35 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 36 self.text_box.deinit(allocator); 37 } 38 39 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 40 try self.text_box.build(allocator, constraint, root_focus); 41 } 42 43 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 44 _ = self; 45 _ = allocator; 46 _ = key; 47 _ = root_focus; 48 } 49 50 pub fn clearGrid(self: *View) void { 51 self.text_box.clearGrid(); 52 } 53 54 pub fn getGrid(self: View) ?Grid { 55 return self.text_box.getGrid(); 56 } 57 58 pub fn getFocus(self: *View) *Focus { 59 return self.text_box.getFocus(); 60 } 61 }; 62 63 // 2-row sextant subtitle font, a half-height companion to Title. 64 // 65 // each glyph is a 4x6 sub-pixel bitmap that renders as a 2x2 grid of 66 // sextant-block characters (U+1FB00-1FB3B), the same character set as Title so 67 // the two share a look. (the glyphs are 4 sub-pixels wide rather than 6, which 68 // is one sextant cell narrower per character.) the four 2x3 patterns that 69 // aren't sextants (empty, left column, right column, full) fall back to 70 // U+0020, U+258C, U+2590, U+2588 respectively. unlike Title there are no scanlines: 71 // at this height every sub-pixel renders, which keeps the shorter glyphs 72 // legible. 73 74 const Glyph = [6][]const u8; 75 76 fn glyphFor(c: u8) ?Glyph { 77 return switch (std.ascii.toUpper(c)) { 78 ' ' => .{ 79 "....", 80 "....", 81 "....", 82 "....", 83 "....", 84 "....", 85 }, 86 'A' => .{ 87 ".##.", 88 "#..#", 89 "####", 90 "#..#", 91 "#..#", 92 "#..#", 93 }, 94 'B' => .{ 95 "###.", 96 "#..#", 97 "###.", 98 "#..#", 99 "#..#", 100 "###.", 101 }, 102 'C' => .{ 103 ".###", 104 "#...", 105 "#...", 106 "#...", 107 "#...", 108 ".###", 109 }, 110 'D' => .{ 111 "###.", 112 "#..#", 113 "#..#", 114 "#..#", 115 "#..#", 116 "###.", 117 }, 118 'E' => .{ 119 "####", 120 "#...", 121 "###.", 122 "#...", 123 "#...", 124 "####", 125 }, 126 'F' => .{ 127 "####", 128 "#...", 129 "###.", 130 "#...", 131 "#...", 132 "#...", 133 }, 134 'G' => .{ 135 ".###", 136 "#...", 137 "#.##", 138 "#..#", 139 "#..#", 140 ".###", 141 }, 142 'H' => .{ 143 "#..#", 144 "#..#", 145 "####", 146 "#..#", 147 "#..#", 148 "#..#", 149 }, 150 'I' => .{ 151 "####", 152 ".##.", 153 ".##.", 154 ".##.", 155 ".##.", 156 "####", 157 }, 158 'J' => .{ 159 "..##", 160 "...#", 161 "...#", 162 "...#", 163 "#..#", 164 ".##.", 165 }, 166 'K' => .{ 167 "#..#", 168 "#.#.", 169 "##..", 170 "##..", 171 "#.#.", 172 "#..#", 173 }, 174 'L' => .{ 175 "#...", 176 "#...", 177 "#...", 178 "#...", 179 "#...", 180 "####", 181 }, 182 'M' => .{ 183 "#..#", 184 "####", 185 "####", 186 "#..#", 187 "#..#", 188 "#..#", 189 }, 190 'N' => .{ 191 "#..#", 192 "##.#", 193 "##.#", 194 "#.##", 195 "#.##", 196 "#..#", 197 }, 198 'O' => .{ 199 ".##.", 200 "#..#", 201 "#..#", 202 "#..#", 203 "#..#", 204 ".##.", 205 }, 206 'P' => .{ 207 "###.", 208 "#..#", 209 "###.", 210 "#...", 211 "#...", 212 "#...", 213 }, 214 'Q' => .{ 215 ".##.", 216 "#..#", 217 "#..#", 218 "#.##", 219 ".##.", 220 "...#", 221 }, 222 'R' => .{ 223 "###.", 224 "#..#", 225 "###.", 226 "##..", 227 "#.#.", 228 "#..#", 229 }, 230 'S' => .{ 231 ".###", 232 "#...", 233 ".##.", 234 "...#", 235 "#..#", 236 "###.", 237 }, 238 'T' => .{ 239 "####", 240 ".##.", 241 ".##.", 242 ".##.", 243 ".##.", 244 ".##.", 245 }, 246 'U' => .{ 247 "#..#", 248 "#..#", 249 "#..#", 250 "#..#", 251 "#..#", 252 ".##.", 253 }, 254 'V' => .{ 255 "#..#", 256 "#..#", 257 "#..#", 258 "#..#", 259 ".##.", 260 ".##.", 261 }, 262 'W' => .{ 263 "#..#", 264 "#..#", 265 "#..#", 266 "####", 267 "####", 268 ".##.", 269 }, 270 'X' => .{ 271 "#..#", 272 "#..#", 273 ".##.", 274 ".##.", 275 "#..#", 276 "#..#", 277 }, 278 'Y' => .{ 279 "#..#", 280 "#..#", 281 ".##.", 282 ".##.", 283 ".##.", 284 ".##.", 285 }, 286 'Z' => .{ 287 "####", 288 "...#", 289 "..#.", 290 ".#..", 291 "#...", 292 "####", 293 }, 294 '0' => .{ 295 ".##.", 296 "#.##", 297 "#.##", 298 "##.#", 299 "##.#", 300 ".##.", 301 }, 302 '1' => .{ 303 ".##.", 304 "###.", 305 ".##.", 306 ".##.", 307 ".##.", 308 "####", 309 }, 310 '2' => .{ 311 ".##.", 312 "#..#", 313 "..#.", 314 ".#..", 315 "#...", 316 "####", 317 }, 318 '3' => .{ 319 "###.", 320 "...#", 321 ".##.", 322 "...#", 323 "#..#", 324 "###.", 325 }, 326 '4' => .{ 327 "..#.", 328 ".##.", 329 "#.#.", 330 "####", 331 "..#.", 332 "..#.", 333 }, 334 '5' => .{ 335 "####", 336 "#...", 337 "###.", 338 "...#", 339 "#..#", 340 "###.", 341 }, 342 '6' => .{ 343 ".##.", 344 "#...", 345 "###.", 346 "#..#", 347 "#..#", 348 ".##.", 349 }, 350 '7' => .{ 351 "####", 352 "...#", 353 "..#.", 354 "..#.", 355 ".#..", 356 ".#..", 357 }, 358 '8' => .{ 359 ".##.", 360 "#..#", 361 ".##.", 362 "#..#", 363 "#..#", 364 ".##.", 365 }, 366 '9' => .{ 367 ".##.", 368 "#..#", 369 "#..#", 370 ".###", 371 "...#", 372 ".##.", 373 }, 374 '-' => .{ 375 "....", 376 "....", 377 ".##.", 378 ".##.", 379 "....", 380 "....", 381 }, 382 '.' => .{ 383 "....", 384 "....", 385 "....", 386 "....", 387 ".##.", 388 ".##.", 389 }, 390 else => null, 391 }; 392 } 393 394 // whether every char of `text` has a glyph 395 pub fn covers(text: []const u8) bool { 396 for (text) |c| { 397 if (glyphFor(c) == null) return false; 398 } 399 return true; 400 } 401 402 // map a 2x3 sub-pixel pattern to its corresponding sextant codepoint 403 fn sextantCodepoint(pattern: u6) u21 { 404 return switch (pattern) { 405 0b000000 => ' ', 406 0b010101 => 0x258C, // ▌ left half block 407 0b101010 => 0x2590, // ▐ right half block 408 0b111111 => 0x2588, // █ full block 409 else => blk: { 410 var offset: u21 = @as(u21, pattern) - 1; 411 if (pattern > 0b010101) offset -= 1; 412 if (pattern > 0b101010) offset -= 1; 413 break :blk 0x1FB00 + offset; 414 }, 415 }; 416 } 417 418 fn renderSubTitle(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { 419 var rows: [2]std.ArrayList(u8) = .{ .empty, .empty }; 420 defer for (&rows) |*r| r.deinit(allocator); 421 422 var first = true; 423 for (input) |c| { 424 const glyph = glyphFor(c) orelse continue; 425 426 var on: [6][4]bool = std.mem.zeroes([6][4]bool); 427 for (0..6) |r| { 428 for (0..4) |col| { 429 on[r][col] = glyph[r][col] == '#'; 430 } 431 } 432 433 if (!first) { 434 for (&rows) |*r| try r.append(allocator, ' '); 435 } 436 first = false; 437 438 for (0..2) |text_row| { 439 for (0..2) |text_col| { 440 var pattern: u6 = 0; 441 for (0..3) |sub_row| { 442 for (0..2) |sub_col| { 443 const row_idx = text_row * 3 + sub_row; 444 const col_idx = text_col * 2 + sub_col; 445 if (on[row_idx][col_idx]) { 446 pattern |= @as(u6, 1) << @intCast(sub_row * 2 + sub_col); 447 } 448 } 449 } 450 const cp = sextantCodepoint(pattern); 451 var utf8_buf: [4]u8 = undefined; 452 const len = try std.unicode.utf8Encode(cp, &utf8_buf); 453 try rows[text_row].appendSlice(allocator, utf8_buf[0..len]); 454 } 455 } 456 } 457 458 var out: std.ArrayList(u8) = .empty; 459 errdefer out.deinit(allocator); 460 try out.appendSlice(allocator, rows[0].items); 461 try out.append(allocator, '\n'); 462 try out.appendSlice(allocator, rows[1].items); 463 return out.toOwnedSlice(allocator); 464 } 465