Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce font-shaping-break config option #5374

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Introduce font-shaping-break config option
dpatterbee committed Jan 26, 2025
commit 0d8bc2b4330e04932a938f69e881be9158151d25
1 change: 1 addition & 0 deletions src/config.zig
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;
pub const CopyOnSelect = Config.CopyOnSelect;
pub const CustomShaderAnimation = Config.CustomShaderAnimation;
pub const FontSyntheticStyle = Config.FontSyntheticStyle;
pub const FontShapingBreak = Config.FontShapingBreak;
pub const FontStyle = Config.FontStyle;
pub const FreetypeLoadFlags = Config.FreetypeLoadFlags;
pub const Keybinds = Config.Keybinds;
11 changes: 11 additions & 0 deletions src/config/Config.zig
Original file line number Diff line number Diff line change
@@ -257,6 +257,12 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// This is currently only supported on macOS.
@"font-thicken-strength": u8 = 255,

/// Where to break font shaping runs.
///
/// Currently only one option, "cursor", which is enabled by default and breaks
/// text runs under the cursor.
@"font-shaping-break": FontShapingBreak = .{},

/// What color space to use when performing alpha blending.
///
/// This affects how text looks for different background/foreground color pairs.
@@ -5588,6 +5594,11 @@ pub const FontSyntheticStyle = packed struct {
@"bold-italic": bool = true,
};

/// See "font-shaping-break" for documentation
pub const FontShapingBreak = packed struct {
cursor: bool = true,
};

/// See "link" for documentation.
pub const RepeatableLink = struct {
const Self = @This();
219 changes: 179 additions & 40 deletions src/font/shaper/coretext.zig

Large diffs are not rendered by default.

214 changes: 174 additions & 40 deletions src/font/shaper/harfbuzz.zig

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/font/shaper/noop.zig
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const trace = @import("tracy").trace;
const font = @import("../main.zig");
const config = @import("../../config.zig");
const Face = font.Face;
const Collection = font.Collection;
const DeferredFace = font.DeferredFace;
@@ -75,6 +76,7 @@ pub const Shaper = struct {
row: terminal.Pin,
selection: ?terminal.Selection,
cursor_x: ?usize,
break_config: config.FontShapingBreak,
) font.shape.RunIterator {
return .{
.hooks = .{ .shaper = self },
@@ -83,6 +85,7 @@ pub const Shaper = struct {
.row = row,
.selection = selection,
.cursor_x = cursor_x,
.break_config = break_config,
};
}

63 changes: 34 additions & 29 deletions src/font/shaper/run.zig
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@ const shape = @import("../shape.zig");
const terminal = @import("../../terminal/main.zig");
const autoHash = std.hash.autoHash;
const Hasher = std.hash.Wyhash;
const configpkg = @import("../../config.zig");
const Config = configpkg.Config;

/// A single text run. A text run is only valid for one Shaper instance and
/// until the next run is created. A text run never goes across multiple
@@ -40,6 +42,7 @@ pub const RunIterator = struct {
row: terminal.Pin,
selection: ?terminal.Selection = null,
cursor_x: ?usize = null,
break_config: configpkg.FontShapingBreak,
i: usize = 0,

pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun {
@@ -175,36 +178,38 @@ pub const RunIterator = struct {
break :emoji null;
};

// If our cursor is on this line then we break the run around the
// cursor. This means that any row with a cursor has at least
// three breaks: before, exactly the cursor, and after.
//
// We do not break a cell that is exactly the grapheme. If there
// are cells following that contain joiners, we allow those to
// break. This creates an effect where hovering over an emoji
// such as a skin-tone emoji is fine, but hovering over the
// joiners will show the joiners allowing you to modify the
// emoji.
if (!cell.hasGrapheme()) {
if (self.cursor_x) |cursor_x| {
// Exactly: self.i is the cursor and we iterated once. This
// means that we started exactly at the cursor and did at
// exactly one iteration. Why exactly one? Because we may
// start at our cursor but do many if our cursor is exactly
// on an emoji.
if (self.i == cursor_x and j == self.i + 1) break;

// Before: up to and not including the cursor. This means
// that we started before the cursor (self.i < cursor_x)
// and j is now at the cursor meaning we haven't yet processed
// the cursor.
if (self.i < cursor_x and j == cursor_x) {
assert(j > 0);
break;
if (self.break_config.cursor) {
// If our cursor is on this line then we break the run around the
// cursor. This means that any row with a cursor has at least
// three breaks: before, exactly the cursor, and after.
//
// We do not break a cell that is exactly the grapheme. If there
// are cells following that contain joiners, we allow those to
// break. This creates an effect where hovering over an emoji
// such as a skin-tone emoji is fine, but hovering over the
// joiners will show the joiners allowing you to modify the
// emoji.
if (!cell.hasGrapheme()) {
if (self.cursor_x) |cursor_x| {
// Exactly: self.i is the cursor and we iterated once. This
// means that we started exactly at the cursor and did at
// exactly one iteration. Why exactly one? Because we may
// start at our cursor but do many if our cursor is exactly
// on an emoji.
if (self.i == cursor_x and j == self.i + 1) break;

// Before: up to and not including the cursor. This means
// that we started before the cursor (self.i < cursor_x)
// and j is now at the cursor meaning we haven't yet processed
// the cursor.
if (self.i < cursor_x and j == cursor_x) {
assert(j > 0);
break;
}

// After: after the cursor. We don't need to do anything
// special, we just let the run complete.
}

// After: after the cursor. We don't need to do anything
// special, we just let the run complete.
}
}

3 changes: 3 additions & 0 deletions src/font/shaper/web_canvas.zig
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator;
const ziglyph = @import("ziglyph");
const font = @import("../main.zig");
const terminal = @import("../../terminal/main.zig");
const config = @import("../../config.zig");

const log = std.log.scoped(.font_shaper);

@@ -65,13 +66,15 @@ pub const Shaper = struct {
row: terminal.Screen.Row,
selection: ?terminal.Selection,
cursor_x: ?usize,
break_config: config.FontShapingBreak,
) font.shape.RunIterator {
return .{
.hooks = .{ .shaper = self },
.group = group,
.row = row,
.selection = selection,
.cursor_x = cursor_x,
.break_config = break_config,
};
}

3 changes: 3 additions & 0 deletions src/renderer/Metal.zig
Original file line number Diff line number Diff line change
@@ -374,6 +374,7 @@ pub const DerivedConfig = struct {
font_thicken_strength: u8,
font_features: std.ArrayListUnmanaged([:0]const u8),
font_styles: font.CodepointResolver.StyleStatus,
font_shaping_break: configpkg.FontShapingBreak,
cursor_color: ?terminal.color.RGB,
cursor_invert: bool,
cursor_opacity: f64,
@@ -427,6 +428,7 @@ pub const DerivedConfig = struct {
.font_thicken_strength = config.@"font-thicken-strength",
.font_features = font_features.list,
.font_styles = font_styles,
.font_shaping_break = config.@"font-shaping-break",

.cursor_color = if (!cursor_invert and config.@"cursor-color" != null)
config.@"cursor-color".?.toTerminalRGB()
@@ -2493,6 +2495,7 @@ fn rebuildCells(
row,
row_selection,
if (shape_cursor) screen.cursor.x else null,
self.config.font_shaping_break,
);
var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc);
var shaper_cells: ?[]const font.shape.Cell = null;
3 changes: 3 additions & 0 deletions src/renderer/OpenGL.zig
Original file line number Diff line number Diff line change
@@ -275,6 +275,7 @@ pub const DerivedConfig = struct {
font_thicken_strength: u8,
font_features: std.ArrayListUnmanaged([:0]const u8),
font_styles: font.CodepointResolver.StyleStatus,
font_shaping_break: configpkg.FontShapingBreak,
cursor_color: ?terminal.color.RGB,
cursor_invert: bool,
cursor_text: ?terminal.color.RGB,
@@ -325,6 +326,7 @@ pub const DerivedConfig = struct {
.font_thicken_strength = config.@"font-thicken-strength",
.font_features = font_features.list,
.font_styles = font_styles,
.font_shaping_break = config.@"font-shaping-break",

.cursor_color = if (!cursor_invert and config.@"cursor-color" != null)
config.@"cursor-color".?.toTerminalRGB()
@@ -1349,6 +1351,7 @@ pub fn rebuildCells(
row,
row_selection,
if (shape_cursor) screen.cursor.x else null,
self.config.font_shaping_break,
);
var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc);
var shaper_cells: ?[]const font.shape.Cell = null;