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