diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 3cf306f91..d54e98811 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -236,8 +236,8 @@ pub fn isCovering(cp: u21) bool { } /// Returns true of the codepoint is a "symbol-like" character, which -/// for now we define as anything in a private use area and anything -/// in several unicode blocks: +/// for now we define as anything in a private use area, except +/// the Powerline range, and anything in several unicode blocks: /// - Dingbats /// - Emoticons /// - Miscellaneous Symbols @@ -249,11 +249,13 @@ pub fn isCovering(cp: u21) bool { /// In the future it may be prudent to expand this to encompass more /// symbol-like characters, and/or exclude some PUA sections. pub fn isSymbol(cp: u21) bool { - return symbols.get(cp); + return symbols.get(cp) and !isPowerline(cp); } /// Returns the appropriate `constraint_width` for /// the provided cell when rendering its glyph(s). +/// +/// Tested as part of the Screen tests. pub fn constraintWidth(cell_pin: terminal.Pin) u2 { const cell = cell_pin.rowAndCell().cell; const cp = cell.codepoint(); @@ -274,9 +276,7 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { // If we have a previous cell and it was a symbol then we need // to also constrain. This is so that multiple PUA glyphs align. - // As an exception, we ignore powerline glyphs since they are - // used for box drawing and we consider them whitespace. - if (cell_pin.x > 0) prev: { + if (cell_pin.x > 0) { const prev_cp = prev_cp: { var copy = cell_pin; copy.x -= 1; @@ -284,9 +284,6 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { break :prev_cp prev_cell.codepoint(); }; - // We consider powerline glyphs whitespace. - if (isPowerline(prev_cp)) break :prev; - if (isSymbol(prev_cp)) { return 1; } @@ -300,10 +297,7 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { const next_cell = copy.rowAndCell().cell; break :next_cp next_cell.codepoint(); }; - if (next_cp == 0 or - isSpace(next_cp) or - isPowerline(next_cp)) - { + if (next_cp == 0 or isSpace(next_cp)) { return 2; } @@ -312,9 +306,10 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { } /// Whether min contrast should be disabled for a given glyph. +/// True for glyphs used for terminal graphics, such as box +/// drawing characters, block elements, and Powerline glyphs. pub fn noMinContrast(cp: u21) bool { - // TODO: We should disable for all box drawing type characters. - return isPowerline(cp); + return isBoxDrawing(cp) or isBlockElement(cp) or isLegacyComputing(cp) or isPowerline(cp); } // Some general spaces, others intentionally kept @@ -328,10 +323,36 @@ fn isSpace(char: u21) bool { }; } +// Returns true if the codepoint is a box drawing character. +fn isBoxDrawing(char: u21) bool { + return switch (char) { + 0x2500...0x257F => true, + else => false, + }; +} + +// Returns true if the codepoint is a block element. +fn isBlockElement(char: u21) bool { + return switch (char) { + 0x2580...0x259F => true, + else => false, + }; +} + +// Returns true if the codepoint is in a Symbols for Legacy +// Computing block, including supplements. +fn isLegacyComputing(char: u21) bool { + return switch (char) { + 0x1FB00...0x1FBFF => true, + 0x1CC00...0x1CEBF => true, // Supplement introduced in Unicode 16.0 + else => false, + }; +} + // Returns true if the codepoint is a part of the Powerline range. fn isPowerline(char: u21) bool { return switch (char) { - 0xE0B0...0xE0C8, 0xE0CA, 0xE0CC...0xE0D2, 0xE0D4 => true, + 0xE0B0...0xE0D7 => true, else => false, }; } diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 7be4d7c12..0a5c8e7b0 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -14,6 +14,7 @@ const Selection = @import("Selection.zig"); const PageList = @import("PageList.zig"); const StringMap = @import("StringMap.zig"); const pagepkg = @import("page.zig"); +const cellpkg = @import("../renderer/cell.zig"); const point = @import("point.zig"); const size = @import("size.zig"); const style = @import("style.zig"); @@ -9097,3 +9098,97 @@ test "Screen UTF8 cell map with blank prefix" { .y = 1, }, cell_map.items[3]); } + +test "Screen cell constraint widths" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 4, 1, 0); + defer s.deinit(); + + // for each case, the numbers in the comment denote expected + // constraint widths for the symbol-containing cells + + // symbol->nothing: 2 + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(2, cellpkg.constraintWidth(p0)); + s.reset(); + } + + // symbol->character: 1 + { + try s.testWriteString("z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, cellpkg.constraintWidth(p0)); + s.reset(); + } + + // symbol->space: 2 + { + try s.testWriteString(" z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(2, cellpkg.constraintWidth(p0)); + s.reset(); + } + // symbol->no-break space: 1 + { + try s.testWriteString("\u{00a0}z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, cellpkg.constraintWidth(p0)); + s.reset(); + } + + // symbol->end of row: 1 + { + try s.testWriteString(" "); + const p3 = s.pages.pin(.{ .screen = .{ .x = 3, .y = 0 } }).?; + try testing.expectEqual(1, cellpkg.constraintWidth(p3)); + s.reset(); + } + + // character->symbol: 2 + { + try s.testWriteString("z"); + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(2, cellpkg.constraintWidth(p1)); + s.reset(); + } + + // symbol->symbol: 1,1 + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(1, cellpkg.constraintWidth(p0)); + try testing.expectEqual(1, cellpkg.constraintWidth(p1)); + s.reset(); + } + + // symbol->space->symbol: 2,2 + { + try s.testWriteString(" "); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const p2 = s.pages.pin(.{ .screen = .{ .x = 2, .y = 0 } }).?; + try testing.expectEqual(2, cellpkg.constraintWidth(p0)); + try testing.expectEqual(2, cellpkg.constraintWidth(p2)); + s.reset(); + } + + // symbol->powerline: 1 (dedicated test because powerline is special-cased in cellpkg) + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, cellpkg.constraintWidth(p0)); + s.reset(); + } + + // powerline->symbol: 2 (dedicated test because powerline is special-cased in cellpkg) + { + try s.testWriteString(""); + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(2, cellpkg.constraintWidth(p1)); + s.reset(); + } +}