From 0b658c8217aaebd93315a2f68ac5ad93c3061a4d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 16 Dec 2023 20:07:25 -0800 Subject: [PATCH 1/3] renderer/metal: constrain PUA glyphs if they aren't next to space --- src/renderer/Metal.zig | 18 +++++++--- src/renderer/cell.zig | 61 +++++++++++++++++++++++++++++++++ src/renderer/metal/shaders.zig | 1 + src/renderer/shaders/cell.metal | 13 +++++++ 4 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 src/renderer/cell.zig diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index ea151819a..a3005751f 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -11,6 +11,7 @@ const objc = @import("objc"); const macos = @import("macos"); const imgui = @import("imgui"); const glslang = @import("glslang"); +const ziglyph = @import("ziglyph"); const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); const font = @import("../font/main.zig"); @@ -19,6 +20,7 @@ const renderer = @import("../renderer.zig"); const math = @import("../math.zig"); const Surface = @import("../Surface.zig"); const link = @import("link.zig"); +const fgMode = @import("cell.zig").fgMode; const shadertoy = @import("shadertoy.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; @@ -1780,11 +1782,17 @@ pub fn updateCell( }, ); - // If we're rendering a color font, we use the color atlas - const presentation = try self.font_group.group.presentationFromIndex(shaper_run.font_index); - const mode: mtl_shaders.Cell.Mode = switch (presentation) { - .text => .fg, - .emoji => .fg_color, + const mode: mtl_shaders.Cell.Mode = switch (try fgMode( + &self.font_group.group, + screen, + cell, + shaper_run, + x, + y, + )) { + .normal => .fg, + .color => .fg_color, + .constrained => .fg_constrained, }; self.cells.appendAssumeCapacity(.{ diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig new file mode 100644 index 000000000..b9407a4b1 --- /dev/null +++ b/src/renderer/cell.zig @@ -0,0 +1,61 @@ +const ziglyph = @import("ziglyph"); +const font = @import("../font/main.zig"); +const terminal = @import("../terminal/main.zig"); + +pub const FgMode = enum { + /// Normal non-colored text rendering. The text can leave the cell + /// size if it is larger than the cell to allow for ligatures. + normal, + + /// Colored text rendering, specifically Emoji. + color, + + /// Similar to normal but the text must be constrained to the cell + /// size. If a glyph is larger than the cell then it must be resized + /// to fit. + constrained, +}; + +/// Returns the appropriate foreground mode for the given cell. This is +/// meant to be called from the typical updateCell function within a +/// renderer. +pub fn fgMode( + group: *font.Group, + screen: *terminal.Screen, + cell: terminal.Screen.Cell, + shaper_run: font.shape.TextRun, + x: usize, + y: usize, +) !FgMode { + const presentation = try group.presentationFromIndex(shaper_run.font_index); + return switch (presentation) { + // Emoji is always full size and color. + .emoji => .color, + + // If it is text it is slightly more complex. If we are a codepoint + // in the private use area and we are at the end or the next cell + // is not empty, we need to constrain rendering. + // + // We do this specifically so that Nerd Fonts can render their + // icons without overlapping with subsequent characters. But if + // the subsequent character is empty, then we allow it to use + // the full glyph size. See #1071. + .text => text: { + if (!ziglyph.general_category.isPrivateUse(@intCast(cell.char))) { + break :text .normal; + } + + // If the next cell is empty, then we allow it to use the + // full glyph size. + if (x < screen.cols - 1) { + const next_cell = screen.getCell(.active, y, x + 1); + if (next_cell.char == 0 or next_cell.char == ' ') { + break :text .normal; + } + } + + // Must be constrained + break :text .constrained; + }, + }; +} diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index bd938802d..5a3cd196d 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -101,6 +101,7 @@ pub const Cell = extern struct { pub const Mode = enum(u8) { bg = 1, fg = 2, + fg_constrained = 3, fg_color = 7, strikethrough = 8, }; diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index 9d1731095..ccb340231 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -4,6 +4,7 @@ using namespace metal; enum Mode : uint8_t { MODE_BG = 1u, MODE_FG = 2u, + MODE_FG_CONSTRAINED = 3u, MODE_FG_COLOR = 7u, MODE_STRIKETHROUGH = 8u, }; @@ -150,6 +151,7 @@ vertex VertexOut uber_vertex( break; case MODE_FG: + case MODE_FG_CONSTRAINED: case MODE_FG_COLOR: { float2 glyph_size = float2(input.glyph_size); float2 glyph_offset = float2(input.glyph_offset); @@ -159,6 +161,16 @@ vertex VertexOut uber_vertex( // So we flip it with `cell_size.y - glyph_offset.y`. glyph_offset.y = cell_size_scaled.y - glyph_offset.y; + // If we're constrained then we need to scale the glyph. + if (input.mode == MODE_FG_CONSTRAINED) { + if (glyph_size.x > cell_size_scaled.x) { + float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x); + glyph_offset.y += glyph_size.y - new_y; + glyph_size.y = new_y; + glyph_size.x = cell_size_scaled.x; + } + } + // Calculate the final position of the cell which uses our glyph size // and glyph offset to create the correct bounding box for the glyph. cell_pos = cell_pos + glyph_size * position + glyph_offset; @@ -211,6 +223,7 @@ fragment float4 uber_fragment( case MODE_BG: return in.color; + case MODE_FG_CONSTRAINED: case MODE_FG: { // Normalize the texture coordinates to [0,1] float2 size = float2(textureGreyscale.get_width(), textureGreyscale.get_height()); From 231a2b63699a0e295b97e4126c772b950ec533ba Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 16 Dec 2023 20:11:37 -0800 Subject: [PATCH 2/3] renderer/opengl: implement fg_constrained --- src/renderer/Metal.zig | 1 - src/renderer/OpenGL.zig | 16 ++++++++++++---- src/renderer/opengl/CellProgram.zig | 1 + src/renderer/shaders/cell.f.glsl | 2 ++ src/renderer/shaders/cell.v.glsl | 15 ++++++++++++++- 5 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index a3005751f..a606619ab 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -11,7 +11,6 @@ const objc = @import("objc"); const macos = @import("macos"); const imgui = @import("imgui"); const glslang = @import("glslang"); -const ziglyph = @import("ziglyph"); const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); const font = @import("../font/main.zig"); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 61d3bd7fe..1cc16b060 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -9,6 +9,7 @@ const testing = std.testing; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const link = @import("link.zig"); +const fgMode = @import("cell.zig").fgMode; const shadertoy = @import("shadertoy.zig"); const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); @@ -1489,10 +1490,17 @@ pub fn updateCell( ); // If we're rendering a color font, we use the color atlas - const presentation = try self.font_group.group.presentationFromIndex(shaper_run.font_index); - const mode: CellProgram.CellMode = switch (presentation) { - .text => .fg, - .emoji => .fg_color, + const mode: CellProgram.CellMode = switch (try fgMode( + &self.font_group.group, + screen, + cell, + shaper_run, + x, + y, + )) { + .normal => .fg, + .color => .fg_color, + .constrained => .fg_constrained, }; self.cells.appendAssumeCapacity(.{ diff --git a/src/renderer/opengl/CellProgram.zig b/src/renderer/opengl/CellProgram.zig index 83bbbab72..1b51aa795 100644 --- a/src/renderer/opengl/CellProgram.zig +++ b/src/renderer/opengl/CellProgram.zig @@ -51,6 +51,7 @@ pub const Cell = extern struct { pub const CellMode = enum(u8) { bg = 1, fg = 2, + fg_constrained = 3, fg_color = 7, strikethrough = 8, diff --git a/src/renderer/shaders/cell.f.glsl b/src/renderer/shaders/cell.f.glsl index c261d03e5..e408fffc8 100644 --- a/src/renderer/shaders/cell.f.glsl +++ b/src/renderer/shaders/cell.f.glsl @@ -26,6 +26,7 @@ uniform vec2 cell_size; // See vertex shader const uint MODE_BG = 1u; const uint MODE_FG = 2u; +const uint MODE_FG_CONSTRAINED = 3u; const uint MODE_FG_COLOR = 7u; const uint MODE_STRIKETHROUGH = 8u; @@ -38,6 +39,7 @@ void main() { break; case MODE_FG: + case MODE_FG_CONSTRAINED: a = texture(text, glyph_tex_coords).r; vec3 premult = color.rgb * color.a; out_FragColor = vec4(premult.rgb*a, a); diff --git a/src/renderer/shaders/cell.v.glsl b/src/renderer/shaders/cell.v.glsl index bf35469db..18b586508 100644 --- a/src/renderer/shaders/cell.v.glsl +++ b/src/renderer/shaders/cell.v.glsl @@ -6,6 +6,7 @@ // NOTE: this must be kept in sync with the fragment shader const uint MODE_BG = 1u; const uint MODE_FG = 2u; +const uint MODE_FG_CONSTRAINED = 3u; const uint MODE_FG_COLOR = 7u; const uint MODE_STRIKETHROUGH = 8u; @@ -179,6 +180,7 @@ void main() { break; case MODE_FG: + case MODE_FG_CONSTRAINED: case MODE_FG_COLOR: vec2 glyph_offset_calc = glyph_offset; @@ -187,8 +189,19 @@ void main() { // So we flip it with `cell_size.y - glyph_offset.y`. glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y; + // If this is a constrained mode, we need to constrain it! + vec2 glyph_size_calc = glyph_size; + if (mode == MODE_FG_CONSTRAINED) { + if (glyph_size.x > cell_size_scaled.x) { + float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x); + glyph_offset_calc.y = glyph_offset_calc.y + (glyph_size.y - new_y); + glyph_size_calc.y = new_y; + glyph_size_calc.x = cell_size_scaled.x; + } + } + // Calculate the final position of the cell. - cell_pos = cell_pos + (glyph_size * position) + glyph_offset_calc; + cell_pos = cell_pos + (glyph_size_calc * position) + glyph_offset_calc; gl_Position = projection * vec4(cell_pos, cell_z, 1.0); // We need to convert our texture position and size to normalized From 9178fabc5d7c095d0f3781f56f23ca6cda2a7f63 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 16 Dec 2023 20:18:14 -0800 Subject: [PATCH 3/3] renderer: also constrain PUA chars if preceded by PUA --- src/renderer/cell.zig | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index b9407a4b1..6a323d4fe 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -45,13 +45,23 @@ pub fn fgMode( break :text .normal; } + // If we are at the end of the screen its definitely constrained + if (x == screen.cols - 1) break :text .constrained; + + // If we have a previous cell and it was PUA then we need to + // also constrain. This is so that multiple PUA glyphs align. + if (x > 0) { + const prev_cell = screen.getCell(.active, y, x - 1); + if (ziglyph.general_category.isPrivateUse(@intCast(prev_cell.char))) { + break :text .constrained; + } + } + // If the next cell is empty, then we allow it to use the // full glyph size. - if (x < screen.cols - 1) { - const next_cell = screen.getCell(.active, y, x + 1); - if (next_cell.char == 0 or next_cell.char == ' ') { - break :text .normal; - } + const next_cell = screen.getCell(.active, y, x + 1); + if (next_cell.char == 0 or next_cell.char == ' ') { + break :text .normal; } // Must be constrained