╭ Title.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 const Style = enum { solid, scanlines }; 16 17 pub fn init(arena: *std.heap.ArenaAllocator, orig_content: []const u8, style: Style) !Self { 18 return .{ 19 .content = try renderTitle(arena.allocator(), orig_content, style), 20 }; 21 } 22 23 pub fn width(self: Self) !usize { 24 const line_end = std.mem.indexOfScalar(u8, self.content, '\n') orelse self.content.len; 25 return xitui.width.displayWidth(self.content[0..line_end]); 26 } 27 28 pub const View = struct { 29 text_box: wgt.TextBox, 30 31 pub fn init(allocator: std.mem.Allocator, data: *const Self) !View { 32 var text_box = try wgt.TextBox.init(allocator, data.content, .{ .border = null, .wrap_kind = .none }); 33 errdefer text_box.deinit(allocator); 34 return .{ .text_box = text_box }; 35 } 36 37 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 38 self.text_box.deinit(allocator); 39 } 40 41 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 42 try self.text_box.build(allocator, constraint, root_focus); 43 } 44 45 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 46 _ = self; 47 _ = allocator; 48 _ = key; 49 _ = root_focus; 50 } 51 52 pub fn clearGrid(self: *View) void { 53 self.text_box.clearGrid(); 54 } 55 56 pub fn getGrid(self: View) ?Grid { 57 return self.text_box.getGrid(); 58 } 59 60 pub fn getFocus(self: *View) *Focus { 61 return self.text_box.getFocus(); 62 } 63 }; 64 65 // 3-row sextant title font, optionally with scanlines. 66 // 67 // each glyph is a 6x9 sub-pixel bitmap that renders as a 3x3 grid of 68 // sextant-block characters (U+1FB00-1FB3B). 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. 71 72 const Glyph = [9][]const u8; 73 74 fn glyphFor(c: u8) ?Glyph { 75 return switch (std.ascii.toUpper(c)) { 76 ' ' => .{ 77 "......", 78 "......", 79 "......", 80 "......", 81 "......", 82 "......", 83 "......", 84 "......", 85 "......", 86 }, 87 'A' => .{ 88 "..##..", 89 ".####.", 90 ".#..#.", 91 "##..##", 92 "######", 93 "#....#", 94 "#....#", 95 "#....#", 96 "#....#", 97 }, 98 'B' => .{ 99 "#####.", 100 "#....#", 101 "#....#", 102 "#....#", 103 "#####.", 104 "#....#", 105 "#....#", 106 "#....#", 107 "#####.", 108 }, 109 'C' => .{ 110 ".####.", 111 "#....#", 112 "#.....", 113 "#.....", 114 "#.....", 115 "#.....", 116 "#.....", 117 "#....#", 118 ".####.", 119 }, 120 'D' => .{ 121 "#####.", 122 "#....#", 123 "#....#", 124 "#....#", 125 "#....#", 126 "#....#", 127 "#....#", 128 "#....#", 129 "#####.", 130 }, 131 'E' => .{ 132 "######", 133 "#.....", 134 "#.....", 135 "#.....", 136 "#####.", 137 "#.....", 138 "#.....", 139 "#.....", 140 "######", 141 }, 142 'F' => .{ 143 "######", 144 "#.....", 145 "#.....", 146 "#.....", 147 "#####.", 148 "#.....", 149 "#.....", 150 "#.....", 151 "#.....", 152 }, 153 'G' => .{ 154 ".####.", 155 "#....#", 156 "#.....", 157 "#.....", 158 "#..###", 159 "#....#", 160 "#....#", 161 "#....#", 162 ".####.", 163 }, 164 'H' => .{ 165 "#....#", 166 "#....#", 167 "#....#", 168 "#....#", 169 "######", 170 "#....#", 171 "#....#", 172 "#....#", 173 "#....#", 174 }, 175 'I' => .{ 176 "######", 177 "..##..", 178 "..##..", 179 "..##..", 180 "..##..", 181 "..##..", 182 "..##..", 183 "..##..", 184 "######", 185 }, 186 'J' => .{ 187 "....##", 188 "....##", 189 "....##", 190 "....##", 191 "....##", 192 "....##", 193 "#...##", 194 "#..##.", 195 ".##...", 196 }, 197 'K' => .{ 198 "#....#", 199 "#...#.", 200 "#..#..", 201 "#.#...", 202 "##....", 203 "#.#...", 204 "#..#..", 205 "#...#.", 206 "#....#", 207 }, 208 'L' => .{ 209 "#.....", 210 "#.....", 211 "#.....", 212 "#.....", 213 "#.....", 214 "#.....", 215 "#.....", 216 "#.....", 217 "######", 218 }, 219 'M' => .{ 220 "#....#", 221 "##..##", 222 "#.##.#", 223 "#.##.#", 224 "#....#", 225 "#....#", 226 "#....#", 227 "#....#", 228 "#....#", 229 }, 230 'N' => .{ 231 "#....#", 232 "##...#", 233 "##...#", 234 "#.#..#", 235 "#..#.#", 236 "#...##", 237 "#...##", 238 "#....#", 239 "#....#", 240 }, 241 'O' => .{ 242 ".####.", 243 "#....#", 244 "#....#", 245 "#....#", 246 "#....#", 247 "#....#", 248 "#....#", 249 "#....#", 250 ".####.", 251 }, 252 'P' => .{ 253 "#####.", 254 "#....#", 255 "#....#", 256 "#....#", 257 "#####.", 258 "#.....", 259 "#.....", 260 "#.....", 261 "#.....", 262 }, 263 'Q' => .{ 264 ".####.", 265 "#....#", 266 "#....#", 267 "#....#", 268 "#....#", 269 "#....#", 270 "#..#.#", 271 ".####.", 272 ".....#", 273 }, 274 'R' => .{ 275 "#####.", 276 "#....#", 277 "#....#", 278 "#....#", 279 "#####.", 280 "#.#...", 281 "#..#..", 282 "#...#.", 283 "#....#", 284 }, 285 'S' => .{ 286 ".####.", 287 "#....#", 288 "#.....", 289 "#.....", 290 ".####.", 291 ".....#", 292 ".....#", 293 "#....#", 294 ".####.", 295 }, 296 'T' => .{ 297 "######", 298 "..##..", 299 "..##..", 300 "..##..", 301 "..##..", 302 "..##..", 303 "..##..", 304 "..##..", 305 "..##..", 306 }, 307 'U' => .{ 308 "#....#", 309 "#....#", 310 "#....#", 311 "#....#", 312 "#....#", 313 "#....#", 314 "#....#", 315 "#....#", 316 ".####.", 317 }, 318 'V' => .{ 319 "#....#", 320 "#....#", 321 "#....#", 322 ".#..#.", 323 ".#..#.", 324 ".#..#.", 325 ".#..#.", 326 ".#..#.", 327 "..##..", 328 }, 329 'W' => .{ 330 "#....#", 331 "#....#", 332 "#....#", 333 "#....#", 334 "#....#", 335 "#.##.#", 336 "#.##.#", 337 "##..##", 338 ".#..#.", 339 }, 340 'X' => .{ 341 "#....#", 342 ".#..#.", 343 ".#..#.", 344 "..##..", 345 "..##..", 346 "..##..", 347 ".#..#.", 348 ".#..#.", 349 "#....#", 350 }, 351 'Y' => .{ 352 "#....#", 353 ".#..#.", 354 ".#..#.", 355 "..##..", 356 "..##..", 357 "..##..", 358 "..##..", 359 "..##..", 360 "..##..", 361 }, 362 'Z' => .{ 363 "######", 364 "....#.", 365 "....#.", 366 "...#..", 367 "..##..", 368 "..#...", 369 ".#....", 370 ".#....", 371 "######", 372 }, 373 '0' => .{ 374 ".####.", 375 "#...##", 376 "#..#.#", 377 "#..#.#", 378 "#.#..#", 379 "#.#..#", 380 "##...#", 381 "#....#", 382 ".####.", 383 }, 384 '1' => .{ 385 "..##..", 386 ".###..", 387 "..##..", 388 "..##..", 389 "..##..", 390 "..##..", 391 "..##..", 392 "..##..", 393 "######", 394 }, 395 '2' => .{ 396 ".####.", 397 "#....#", 398 ".....#", 399 "....#.", 400 "...#..", 401 "..#...", 402 ".#....", 403 "#.....", 404 "######", 405 }, 406 '3' => .{ 407 ".####.", 408 "#....#", 409 ".....#", 410 ".....#", 411 ".####.", 412 ".....#", 413 ".....#", 414 "#....#", 415 ".####.", 416 }, 417 '4' => .{ 418 "....#.", 419 "...##.", 420 "..#.#.", 421 ".#..#.", 422 "#...#.", 423 "######", 424 "....#.", 425 "....#.", 426 "....#.", 427 }, 428 '5' => .{ 429 "######", 430 "#.....", 431 "#.....", 432 "#.....", 433 "#####.", 434 ".....#", 435 ".....#", 436 "#....#", 437 ".####.", 438 }, 439 '6' => .{ 440 ".####.", 441 "#....#", 442 "#.....", 443 "#.....", 444 "#####.", 445 "#....#", 446 "#....#", 447 "#....#", 448 ".####.", 449 }, 450 '7' => .{ 451 "######", 452 ".....#", 453 "....#.", 454 "....#.", 455 "...#..", 456 "...#..", 457 "..#...", 458 "..#...", 459 ".#....", 460 }, 461 '8' => .{ 462 ".####.", 463 "#....#", 464 "#....#", 465 "#....#", 466 ".####.", 467 "#....#", 468 "#....#", 469 "#....#", 470 ".####.", 471 }, 472 '9' => .{ 473 ".####.", 474 "#....#", 475 "#....#", 476 "#....#", 477 ".#####", 478 ".....#", 479 ".....#", 480 "#....#", 481 ".####.", 482 }, 483 '-' => .{ 484 "......", 485 "......", 486 "......", 487 "......", 488 ".####.", 489 "......", 490 "......", 491 "......", 492 "......", 493 }, 494 '.' => .{ 495 "......", 496 "......", 497 "......", 498 "......", 499 "......", 500 "......", 501 "......", 502 "..##..", 503 "..##..", 504 }, 505 '?' => .{ 506 ".####.", 507 "#....#", 508 ".....#", 509 "....#.", 510 "..##..", 511 "..##..", 512 "......", 513 "..##..", 514 "..##..", 515 }, 516 '!' => .{ 517 "..##..", 518 "..##..", 519 "..##..", 520 "..##..", 521 "..##..", 522 "..##..", 523 "......", 524 "..##..", 525 "..##..", 526 }, 527 '\'' => .{ 528 "..##..", 529 "..##..", 530 "...#..", 531 "......", 532 "......", 533 "......", 534 "......", 535 "......", 536 "......", 537 }, 538 ',' => .{ 539 "......", 540 "......", 541 "......", 542 "......", 543 "......", 544 "......", 545 "..##..", 546 "..##..", 547 "..#...", 548 }, 549 ':' => .{ 550 "......", 551 "......", 552 "..##..", 553 "..##..", 554 "......", 555 "......", 556 "......", 557 "..##..", 558 "..##..", 559 }, 560 '/' => .{ 561 ".....#", 562 "....#.", 563 "....#.", 564 "...#..", 565 "...#..", 566 "..#...", 567 "..#...", 568 ".#....", 569 "#.....", 570 }, 571 else => null, 572 }; 573 } 574 575 // whether every char of `text` has a glyph 576 pub fn covers(text: []const u8) bool { 577 for (text) |c| { 578 if (glyphFor(c) == null) return false; 579 } 580 return true; 581 } 582 583 // map a 2x3 sub-pixel pattern to its corresponding sextant codepoint 584 fn sextantCodepoint(pattern: u6) u21 { 585 return switch (pattern) { 586 0b000000 => ' ', 587 0b010101 => 0x258C, // ▌ left half block 588 0b101010 => 0x2590, // ▐ right half block 589 0b111111 => 0x2588, // █ full block 590 else => blk: { 591 var offset: u21 = @as(u21, pattern) - 1; 592 if (pattern > 0b010101) offset -= 1; 593 if (pattern > 0b101010) offset -= 1; 594 break :blk 0x1FB00 + offset; 595 }, 596 }; 597 } 598 599 fn renderTitle(allocator: std.mem.Allocator, input: []const u8, style: Style) ![]const u8 { 600 var rows: [3]std.ArrayList(u8) = .{ .empty, .empty, .empty }; 601 defer for (&rows) |*r| r.deinit(allocator); 602 603 var first = true; 604 for (input) |c| { 605 const glyph = glyphFor(c) orelse continue; 606 607 // scanlines: keep on-pixels only at even bitmap rows. 608 var kept: [9][6]bool = std.mem.zeroes([9][6]bool); 609 for (0..9) |r| { 610 if (style == .scanlines and r % 2 != 0) continue; 611 for (0..6) |col| { 612 kept[r][col] = glyph[r][col] == '#'; 613 } 614 } 615 616 if (!first) { 617 for (&rows) |*r| try r.append(allocator, ' '); 618 } 619 first = false; 620 621 for (0..3) |text_row| { 622 for (0..3) |text_col| { 623 var pattern: u6 = 0; 624 for (0..3) |sub_row| { 625 for (0..2) |sub_col| { 626 const row_idx = text_row * 3 + sub_row; 627 const col_idx = text_col * 2 + sub_col; 628 if (kept[row_idx][col_idx]) { 629 pattern |= @as(u6, 1) << @intCast(sub_row * 2 + sub_col); 630 } 631 } 632 } 633 const cp = sextantCodepoint(pattern); 634 var utf8_buf: [4]u8 = undefined; 635 const len = try std.unicode.utf8Encode(cp, &utf8_buf); 636 try rows[text_row].appendSlice(allocator, utf8_buf[0..len]); 637 } 638 } 639 } 640 641 var out: std.ArrayList(u8) = .empty; 642 errdefer out.deinit(allocator); 643 try out.appendSlice(allocator, rows[0].items); 644 try out.append(allocator, '\n'); 645 try out.appendSlice(allocator, rows[1].items); 646 try out.append(allocator, '\n'); 647 try out.appendSlice(allocator, rows[2].items); 648 return out.toOwnedSlice(allocator); 649 } 650