a modern tui library written in zig
at v0.2.0 25 kB view raw
1const std = @import("std"); 2 3const Screen = @import("Screen.zig"); 4const Cell = @import("Cell.zig"); 5const Mouse = @import("Mouse.zig"); 6const Segment = @import("Cell.zig").Segment; 7const Unicode = @import("Unicode.zig"); 8const gw = @import("gwidth.zig"); 9 10const log = std.log.scoped(.window); 11 12const Window = @This(); 13 14pub const Size = union(enum) { 15 expand, 16 limit: usize, 17}; 18 19/// horizontal offset from the screen 20x_off: usize, 21/// vertical offset from the screen 22y_off: usize, 23/// width of the window. This can't be larger than the terminal screen 24width: usize, 25/// height of the window. This can't be larger than the terminal screen 26height: usize, 27 28screen: *Screen, 29 30/// Deprecated. Use `child` instead 31/// 32/// Creates a new window with offset relative to parent and size clamped to the 33/// parent's size. Windows do not retain a reference to their parent and are 34/// unaware of resizes. 35pub fn initChild( 36 self: Window, 37 x_off: usize, 38 y_off: usize, 39 width: Size, 40 height: Size, 41) Window { 42 const resolved_width = switch (width) { 43 .expand => self.width -| x_off, 44 .limit => |w| blk: { 45 if (w + x_off > self.width) { 46 break :blk self.width -| x_off; 47 } 48 break :blk w; 49 }, 50 }; 51 const resolved_height = switch (height) { 52 .expand => self.height -| y_off, 53 .limit => |h| blk: { 54 if (h + y_off > self.height) { 55 break :blk self.height -| y_off; 56 } 57 break :blk h; 58 }, 59 }; 60 return Window{ 61 .x_off = x_off + self.x_off, 62 .y_off = y_off + self.y_off, 63 .width = resolved_width, 64 .height = resolved_height, 65 .screen = self.screen, 66 }; 67} 68 69pub const ChildOptions = struct { 70 x_off: usize = 0, 71 y_off: usize = 0, 72 /// the width of the resulting child, including any borders 73 width: Size = .expand, 74 /// the height of the resulting child, including any borders 75 height: Size = .expand, 76 border: BorderOptions = .{}, 77}; 78 79pub const BorderOptions = struct { 80 style: Cell.Style = .{}, 81 where: union(enum) { 82 none, 83 all, 84 top, 85 right, 86 bottom, 87 left, 88 other: Locations, 89 } = .none, 90 glyphs: Glyphs = .single_rounded, 91 92 pub const Locations = packed struct { 93 top: bool = false, 94 right: bool = false, 95 bottom: bool = false, 96 left: bool = false, 97 }; 98 99 pub const Glyphs = union(enum) { 100 single_rounded, 101 single_square, 102 /// custom border glyphs. each glyph should be one cell wide and the 103 /// following indices apply: 104 /// [0] = top left 105 /// [1] = horizontal 106 /// [2] = top right 107 /// [3] = vertical 108 /// [4] = bottom right 109 /// [5] = bottom left 110 custom: [6][]const u8, 111 }; 112 113 const single_rounded: [6][]const u8 = .{ "", "", "", "", "", "" }; 114 const single_square: [6][]const u8 = .{ "", "", "", "", "", "" }; 115}; 116 117/// create a child window 118pub fn child(self: Window, opts: ChildOptions) Window { 119 var result = self.initChild(opts.x_off, opts.y_off, opts.width, opts.height); 120 121 const glyphs = switch (opts.border.glyphs) { 122 .single_rounded => BorderOptions.single_rounded, 123 .single_square => BorderOptions.single_square, 124 .custom => |custom| custom, 125 }; 126 127 const top_left: Cell.Character = .{ .grapheme = glyphs[0], .width = 1 }; 128 const horizontal: Cell.Character = .{ .grapheme = glyphs[1], .width = 1 }; 129 const top_right: Cell.Character = .{ .grapheme = glyphs[2], .width = 1 }; 130 const vertical: Cell.Character = .{ .grapheme = glyphs[3], .width = 1 }; 131 const bottom_right: Cell.Character = .{ .grapheme = glyphs[4], .width = 1 }; 132 const bottom_left: Cell.Character = .{ .grapheme = glyphs[5], .width = 1 }; 133 const style = opts.border.style; 134 135 const h = result.height; 136 const w = result.width; 137 138 const loc: BorderOptions.Locations = switch (opts.border.where) { 139 .none => return result, 140 .all => .{ .top = true, .bottom = true, .right = true, .left = true }, 141 .bottom => .{ .bottom = true }, 142 .right => .{ .right = true }, 143 .left => .{ .left = true }, 144 .top => .{ .top = true }, 145 .other => |loc| loc, 146 }; 147 if (loc.top) { 148 var i: usize = 0; 149 while (i < w) : (i += 1) { 150 result.writeCell(i, 0, .{ .char = horizontal, .style = style }); 151 } 152 } 153 if (loc.bottom) { 154 var i: usize = 0; 155 while (i < w) : (i += 1) { 156 result.writeCell(i, h -| 1, .{ .char = horizontal, .style = style }); 157 } 158 } 159 if (loc.left) { 160 var i: usize = 0; 161 while (i < h) : (i += 1) { 162 result.writeCell(0, i, .{ .char = vertical, .style = style }); 163 } 164 } 165 if (loc.right) { 166 var i: usize = 0; 167 while (i < h) : (i += 1) { 168 result.writeCell(w -| 1, i, .{ .char = vertical, .style = style }); 169 } 170 } 171 // draw corners 172 if (loc.top and loc.left) 173 result.writeCell(0, 0, .{ .char = top_left, .style = style }); 174 if (loc.top and loc.right) 175 result.writeCell(w - 1, 0, .{ .char = top_right, .style = style }); 176 if (loc.bottom and loc.left) 177 result.writeCell(0, h -| 1, .{ .char = bottom_left, .style = style }); 178 if (loc.bottom and loc.right) 179 result.writeCell(w - 1, h -| 1, .{ .char = bottom_right, .style = style }); 180 181 const x_off: usize = if (loc.left) 1 else 0; 182 const y_off: usize = if (loc.top) 1 else 0; 183 const h_delt: usize = if (loc.bottom) 1 else 0; 184 const w_delt: usize = if (loc.right) 1 else 0; 185 const h_ch: usize = h - y_off - h_delt; 186 const w_ch: usize = w - x_off - w_delt; 187 return result.initChild(x_off, y_off, .{ .limit = w_ch }, .{ .limit = h_ch }); 188} 189 190/// writes a cell to the location in the window 191pub fn writeCell(self: Window, col: usize, row: usize, cell: Cell) void { 192 if (self.height == 0 or self.width == 0) return; 193 if (self.height <= row or self.width <= col) return; 194 self.screen.writeCell(col + self.x_off, row + self.y_off, cell); 195} 196 197/// reads a cell at the location in the window 198pub fn readCell(self: Window, col: usize, row: usize) ?Cell { 199 if (self.height == 0 or self.width == 0) return null; 200 if (self.height <= row or self.width <= col) return null; 201 return self.screen.readCell(col + self.x_off, row + self.y_off); 202} 203 204/// fills the window with the default cell 205pub fn clear(self: Window) void { 206 self.fill(.{ .default = true }); 207} 208 209/// returns the width of the grapheme. This depends on the terminal capabilities 210pub fn gwidth(self: Window, str: []const u8) usize { 211 return gw.gwidth(str, self.screen.width_method, &self.screen.unicode.width_data) catch 1; 212} 213 214/// fills the window with the provided cell 215pub fn fill(self: Window, cell: Cell) void { 216 if (self.screen.width < self.x_off) 217 return; 218 if (self.screen.height < self.y_off) 219 return; 220 if (self.x_off == 0 and self.width == self.screen.width) { 221 // we have a full width window, therefore contiguous memory. 222 const start = self.y_off * self.width; 223 const end = start + (self.height * self.width); 224 @memset(self.screen.buf[start..end], cell); 225 } else { 226 // Non-contiguous. Iterate over rows an memset 227 var row: usize = self.y_off; 228 const last_row = @min(self.height + self.y_off, self.screen.height); 229 while (row < last_row) : (row += 1) { 230 const start = self.x_off + (row * self.screen.width); 231 const end = @min(start + self.width, start + (self.screen.width - self.x_off)); 232 @memset(self.screen.buf[start..end], cell); 233 } 234 } 235} 236 237/// hide the cursor 238pub fn hideCursor(self: Window) void { 239 self.screen.cursor_vis = false; 240} 241 242/// show the cursor at the given coordinates, 0 indexed 243pub fn showCursor(self: Window, col: usize, row: usize) void { 244 if (self.height == 0 or self.width == 0) return; 245 if (self.height <= row or self.width <= col) return; 246 self.screen.cursor_vis = true; 247 self.screen.cursor_row = row + self.y_off; 248 self.screen.cursor_col = col + self.x_off; 249} 250 251pub fn setCursorShape(self: Window, shape: Cell.CursorShape) void { 252 self.screen.cursor_shape = shape; 253} 254 255/// Options to use when printing Segments to a window 256pub const PrintOptions = struct { 257 /// vertical offset to start printing at 258 row_offset: usize = 0, 259 260 /// wrap behavior for printing 261 wrap: enum { 262 /// wrap at grapheme boundaries 263 grapheme, 264 /// wrap at word boundaries 265 word, 266 /// stop printing after one line 267 none, 268 } = .grapheme, 269 270 /// when true, print will write to the screen for rendering. When false, 271 /// nothing is written. The return value describes the size of the wrapped 272 /// text 273 commit: bool = true, 274}; 275 276pub const PrintResult = struct { 277 col: usize, 278 row: usize, 279 overflow: bool, 280}; 281 282/// prints segments to the window. returns true if the text overflowed with the 283/// given wrap strategy and size. 284pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) !PrintResult { 285 var row = opts.row_offset; 286 switch (opts.wrap) { 287 .grapheme => { 288 var col: usize = 0; 289 const overflow: bool = blk: for (segments) |segment| { 290 var iter = self.screen.unicode.graphemeIterator(segment.text); 291 while (iter.next()) |grapheme| { 292 if (col >= self.width) { 293 row += 1; 294 col = 0; 295 } 296 if (row >= self.height) break :blk true; 297 const s = grapheme.bytes(segment.text); 298 if (std.mem.eql(u8, s, "\n")) { 299 row +|= 1; 300 col = 0; 301 continue; 302 } 303 const w = self.gwidth(s); 304 if (w == 0) continue; 305 if (opts.commit) self.writeCell(col, row, .{ 306 .char = .{ 307 .grapheme = s, 308 .width = w, 309 }, 310 .style = segment.style, 311 .link = segment.link, 312 }); 313 col += w; 314 } 315 } else false; 316 if (col >= self.width) { 317 row += 1; 318 col = 0; 319 } 320 return .{ 321 .row = row, 322 .col = col, 323 .overflow = overflow, 324 }; 325 }, 326 .word => { 327 var col: usize = 0; 328 var overflow: bool = false; 329 var soft_wrapped: bool = false; 330 for (segments) |segment| { 331 var start: usize = 0; 332 var i: usize = 0; 333 while (i < segment.text.len) : (i += 1) { 334 // for (segment.text, 0..) |b, i| { 335 const b = segment.text[i]; 336 const end = switch (b) { 337 ' ', 338 '\r', 339 '\n', 340 => i, 341 else => if (i != segment.text.len - 1) continue else i + 1, 342 }; 343 const word = segment.text[start..end]; 344 // find the start of the next word 345 start = while (i + 1 < segment.text.len) : (i += 1) { 346 if (segment.text[i + 1] == ' ') continue; 347 break i + 1; 348 } else i; 349 const width = self.gwidth(word); 350 const non_wsp_width: usize = for (word, 0..) |wb, wi| { 351 if (wb == '\r' or wb == '\n') { 352 row += 1; 353 col = 0; 354 break width -| wi -| 1; 355 } 356 if (wb != ' ') break width - wi; 357 } else 0; 358 359 if (width + col > self.width and non_wsp_width < self.width) { 360 // wrap 361 row += 1; 362 col = 0; 363 soft_wrapped = true; 364 } 365 if (row >= self.height) { 366 overflow = true; 367 break; 368 } 369 // if we are soft wrapped, (col == 0 and row > 0), then trim 370 // leading spaces 371 const printed_word = if (soft_wrapped) 372 std.mem.trimLeft(u8, word, " ") 373 else 374 word; 375 defer soft_wrapped = false; 376 var iter = self.screen.unicode.graphemeIterator(printed_word); 377 while (iter.next()) |grapheme| { 378 const s = grapheme.bytes(printed_word); 379 const w = self.gwidth(s); 380 if (opts.commit) self.writeCell(col, row, .{ 381 .char = .{ 382 .grapheme = s, 383 .width = w, 384 }, 385 .style = segment.style, 386 .link = segment.link, 387 }); 388 col += w; 389 if (col >= self.width) { 390 row += 1; 391 col = 0; 392 } 393 } 394 switch (b) { 395 ' ' => { 396 if (col > 0) { 397 if (opts.commit) self.writeCell(col, row, .{ 398 .char = .{ 399 .grapheme = " ", 400 .width = 1, 401 }, 402 .style = segment.style, 403 .link = segment.link, 404 }); 405 col += 1; 406 } 407 }, 408 '\r', 409 '\n', 410 => { 411 col = 0; 412 row += 1; 413 }, 414 else => {}, 415 } 416 } 417 } 418 return .{ 419 // remove last row counter 420 .row = row, 421 .col = col, 422 .overflow = overflow, 423 }; 424 }, 425 .none => { 426 var col: usize = 0; 427 const overflow: bool = blk: for (segments) |segment| { 428 var iter = self.screen.unicode.graphemeIterator(segment.text); 429 while (iter.next()) |grapheme| { 430 if (col >= self.width) break :blk true; 431 const s = grapheme.bytes(segment.text); 432 if (std.mem.eql(u8, s, "\n")) break :blk true; 433 const w = self.gwidth(s); 434 if (w == 0) continue; 435 if (opts.commit) self.writeCell(col, row, .{ 436 .char = .{ 437 .grapheme = s, 438 .width = w, 439 }, 440 .style = segment.style, 441 .link = segment.link, 442 }); 443 col +|= w; 444 } 445 } else false; 446 return .{ 447 .row = row, 448 .col = col, 449 .overflow = overflow, 450 }; 451 }, 452 } 453 return false; 454} 455 456/// print a single segment. This is just a shortcut for print(&.{segment}, opts) 457pub fn printSegment(self: Window, segment: Segment, opts: PrintOptions) !PrintResult { 458 return self.print(&.{segment}, opts); 459} 460 461/// scrolls the window down one row (IE inserts a blank row at the bottom of the 462/// screen and shifts all rows up one) 463pub fn scroll(self: Window, n: usize) void { 464 if (n > self.height) return; 465 var row = self.y_off; 466 while (row < self.height - n) : (row += 1) { 467 const dst_start = (row * self.width) + self.x_off; 468 const dst_end = dst_start + self.width; 469 470 const src_start = ((row + n) * self.width) + self.x_off; 471 const src_end = src_start + self.width; 472 @memcpy(self.screen.buf[dst_start..dst_end], self.screen.buf[src_start..src_end]); 473 } 474 const last_row = self.child(.{ 475 .y_off = self.height - n, 476 }); 477 last_row.clear(); 478} 479 480/// returns the mouse event if the mouse event occurred within the window. If 481/// the mouse event occurred outside the window, null is returned 482pub fn hasMouse(win: Window, mouse: ?Mouse) ?Mouse { 483 const event = mouse orelse return null; 484 if (event.col >= win.x_off and 485 event.col < (win.x_off + win.width) and 486 event.row >= win.y_off and 487 event.row < (win.y_off + win.height)) return event else return null; 488} 489 490test "Window size set" { 491 var parent = Window{ 492 .x_off = 0, 493 .y_off = 0, 494 .width = 20, 495 .height = 20, 496 .screen = undefined, 497 }; 498 499 const ch = parent.initChild(1, 1, .expand, .expand); 500 try std.testing.expectEqual(19, ch.width); 501 try std.testing.expectEqual(19, ch.height); 502} 503 504test "Window size set too big" { 505 var parent = Window{ 506 .x_off = 0, 507 .y_off = 0, 508 .width = 20, 509 .height = 20, 510 .screen = undefined, 511 }; 512 513 const ch = parent.initChild(0, 0, .{ .limit = 21 }, .{ .limit = 21 }); 514 try std.testing.expectEqual(20, ch.width); 515 try std.testing.expectEqual(20, ch.height); 516} 517 518test "Window size set too big with offset" { 519 var parent = Window{ 520 .x_off = 0, 521 .y_off = 0, 522 .width = 20, 523 .height = 20, 524 .screen = undefined, 525 }; 526 527 const ch = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 }); 528 try std.testing.expectEqual(10, ch.width); 529 try std.testing.expectEqual(10, ch.height); 530} 531 532test "Window size nested offsets" { 533 var parent = Window{ 534 .x_off = 1, 535 .y_off = 1, 536 .width = 20, 537 .height = 20, 538 .screen = undefined, 539 }; 540 541 const ch = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 }); 542 try std.testing.expectEqual(11, ch.x_off); 543 try std.testing.expectEqual(11, ch.y_off); 544} 545 546test "print: grapheme" { 547 const alloc = std.testing.allocator_instance.allocator(); 548 const unicode = try Unicode.init(alloc); 549 defer unicode.deinit(); 550 var screen: Screen = .{ .width_method = .unicode, .unicode = &unicode }; 551 const win: Window = .{ 552 .x_off = 0, 553 .y_off = 0, 554 .width = 4, 555 .height = 2, 556 .screen = &screen, 557 }; 558 const opts: PrintOptions = .{ 559 .commit = false, 560 .wrap = .grapheme, 561 }; 562 563 { 564 var segments = [_]Segment{ 565 .{ .text = "a" }, 566 }; 567 const result = try win.print(&segments, opts); 568 try std.testing.expectEqual(1, result.col); 569 try std.testing.expectEqual(0, result.row); 570 try std.testing.expectEqual(false, result.overflow); 571 } 572 { 573 var segments = [_]Segment{ 574 .{ .text = "abcd" }, 575 }; 576 const result = try win.print(&segments, opts); 577 try std.testing.expectEqual(0, result.col); 578 try std.testing.expectEqual(1, result.row); 579 try std.testing.expectEqual(false, result.overflow); 580 } 581 { 582 var segments = [_]Segment{ 583 .{ .text = "abcde" }, 584 }; 585 const result = try win.print(&segments, opts); 586 try std.testing.expectEqual(1, result.col); 587 try std.testing.expectEqual(1, result.row); 588 try std.testing.expectEqual(false, result.overflow); 589 } 590 { 591 var segments = [_]Segment{ 592 .{ .text = "abcdefgh" }, 593 }; 594 const result = try win.print(&segments, opts); 595 try std.testing.expectEqual(0, result.col); 596 try std.testing.expectEqual(2, result.row); 597 try std.testing.expectEqual(false, result.overflow); 598 } 599 { 600 var segments = [_]Segment{ 601 .{ .text = "abcdefghi" }, 602 }; 603 const result = try win.print(&segments, opts); 604 try std.testing.expectEqual(0, result.col); 605 try std.testing.expectEqual(2, result.row); 606 try std.testing.expectEqual(true, result.overflow); 607 } 608} 609 610test "print: word" { 611 const alloc = std.testing.allocator_instance.allocator(); 612 const unicode = try Unicode.init(alloc); 613 defer unicode.deinit(); 614 var screen: Screen = .{ 615 .width_method = .unicode, 616 .unicode = &unicode, 617 }; 618 const win: Window = .{ 619 .x_off = 0, 620 .y_off = 0, 621 .width = 4, 622 .height = 2, 623 .screen = &screen, 624 }; 625 const opts: PrintOptions = .{ 626 .commit = false, 627 .wrap = .word, 628 }; 629 630 { 631 var segments = [_]Segment{ 632 .{ .text = "a" }, 633 }; 634 const result = try win.print(&segments, opts); 635 try std.testing.expectEqual(1, result.col); 636 try std.testing.expectEqual(0, result.row); 637 try std.testing.expectEqual(false, result.overflow); 638 } 639 { 640 var segments = [_]Segment{ 641 .{ .text = "a b" }, 642 }; 643 const result = try win.print(&segments, opts); 644 try std.testing.expectEqual(3, result.col); 645 try std.testing.expectEqual(0, result.row); 646 try std.testing.expectEqual(false, result.overflow); 647 } 648 { 649 var segments = [_]Segment{ 650 .{ .text = "a b c" }, 651 }; 652 const result = try win.print(&segments, opts); 653 try std.testing.expectEqual(1, result.col); 654 try std.testing.expectEqual(1, result.row); 655 try std.testing.expectEqual(false, result.overflow); 656 } 657 { 658 var segments = [_]Segment{ 659 .{ .text = "hello" }, 660 }; 661 const result = try win.print(&segments, opts); 662 try std.testing.expectEqual(1, result.col); 663 try std.testing.expectEqual(1, result.row); 664 try std.testing.expectEqual(false, result.overflow); 665 } 666 { 667 var segments = [_]Segment{ 668 .{ .text = "hi tim" }, 669 }; 670 const result = try win.print(&segments, opts); 671 try std.testing.expectEqual(3, result.col); 672 try std.testing.expectEqual(1, result.row); 673 try std.testing.expectEqual(false, result.overflow); 674 } 675 { 676 var segments = [_]Segment{ 677 .{ .text = "hello tim" }, 678 }; 679 const result = try win.print(&segments, opts); 680 try std.testing.expectEqual(0, result.col); 681 try std.testing.expectEqual(2, result.row); 682 try std.testing.expectEqual(true, result.overflow); 683 } 684 { 685 var segments = [_]Segment{ 686 .{ .text = "hello ti" }, 687 }; 688 const result = try win.print(&segments, opts); 689 try std.testing.expectEqual(0, result.col); 690 try std.testing.expectEqual(2, result.row); 691 try std.testing.expectEqual(false, result.overflow); 692 } 693 { 694 var segments = [_]Segment{ 695 .{ .text = "h" }, 696 .{ .text = "e" }, 697 }; 698 const result = try win.print(&segments, opts); 699 try std.testing.expectEqual(2, result.col); 700 try std.testing.expectEqual(0, result.row); 701 try std.testing.expectEqual(false, result.overflow); 702 } 703 { 704 var segments = [_]Segment{ 705 .{ .text = "h" }, 706 .{ .text = "e" }, 707 .{ .text = "l" }, 708 .{ .text = "l" }, 709 .{ .text = "o" }, 710 }; 711 const result = try win.print(&segments, opts); 712 try std.testing.expectEqual(1, result.col); 713 try std.testing.expectEqual(1, result.row); 714 try std.testing.expectEqual(false, result.overflow); 715 } 716 { 717 var segments = [_]Segment{ 718 .{ .text = "he\n" }, 719 }; 720 const result = try win.print(&segments, opts); 721 try std.testing.expectEqual(0, result.col); 722 try std.testing.expectEqual(1, result.row); 723 try std.testing.expectEqual(false, result.overflow); 724 } 725 { 726 var segments = [_]Segment{ 727 .{ .text = "he\n\n" }, 728 }; 729 const result = try win.print(&segments, opts); 730 try std.testing.expectEqual(0, result.col); 731 try std.testing.expectEqual(2, result.row); 732 try std.testing.expectEqual(false, result.overflow); 733 } 734 { 735 var segments = [_]Segment{ 736 .{ .text = "not now" }, 737 }; 738 const result = try win.print(&segments, opts); 739 try std.testing.expectEqual(3, result.col); 740 try std.testing.expectEqual(1, result.row); 741 try std.testing.expectEqual(false, result.overflow); 742 } 743 { 744 var segments = [_]Segment{ 745 .{ .text = "note now" }, 746 }; 747 const result = try win.print(&segments, opts); 748 try std.testing.expectEqual(3, result.col); 749 try std.testing.expectEqual(1, result.row); 750 try std.testing.expectEqual(false, result.overflow); 751 } 752}