summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/builtins.zig26
-rw-r--r--src/config.zig24
-rw-r--r--src/gui/gui.zig60
3 files changed, 105 insertions, 5 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index 146789b0..a5dbf6cd 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -19,6 +19,9 @@ pub const capabilities: config.Runtime.Capabilities = .{
.tagline_font_size = pardes.platform != .tty and pardes.platform != .esp32p4,
.window_opacity = pardes.platform == .gui or pardes.platform == .macos,
.window_blur = pardes.platform == .macos,
+ // Only the SDL shell shapes with HarfBuzz and honours the setting. macOS
+ // draws CoreText ligatures of its own, but nothing there reads it yet.
+ .ligatures = pardes.platform == .gui,
};
pub const Ctx = struct {
@@ -164,6 +167,29 @@ test "capabilities exactly gate setting and effect-source builtins" {
try std.testing.expectEqual(EffectCode.enabled, effect_code_registered);
}
+test "Ligatures is a setting only in the shell that shapes text" {
+ const exists = @hasField(registry.Builtin(), "Ligatures");
+ try std.testing.expectEqual(pardes.platform == .gui, exists);
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ try std.testing.expectEqual(exists, p.executeBuiltinLine(p.active, "Ligatures off"));
+ try std.testing.expectEqual(!exists, p.settings.ligatures);
+ var storage: [8192]u8 = undefined;
+ var out: std.Io.Writer = .fixed(&storage);
+ try config.Runtime.writeReport(&out, .{
+ .startup_config_path = null,
+ .platform = @tagName(pardes.platform),
+ .theme_name = p.theme().name,
+ .compiled_default_shell = config.default_shell,
+ .hover_delay_frames = null,
+ .native_images = false,
+ .capabilities = capabilities,
+ .state = &p.settings,
+ });
+ try std.testing.expectEqual(exists, std.mem.indexOf(u8, storage[0..out.end], "Ligatures off\n") != null);
+ try std.testing.expectEqual(exists, std.mem.indexOf(u8, storage[0..out.end], "Ligatures") != null);
+}
+
comptime {
for ([_][]const u8{ "Peek", "Poke", "Hexdump" }) |name|
if (@hasField(registry.Builtin(), name) != Board.enabled) @compileError(
diff --git a/src/config.zig b/src/config.zig
index 4e46b1d8..4b47b188 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -111,6 +111,7 @@ pub const leader_path = paths: {
table.set(.DumpDir, null);
if (builtins.capabilities.window_opacity) table.set(.WindowOpacity, null);
if (builtins.capabilities.window_blur) table.set(.WindowBlur, null);
+ if (builtins.capabilities.ligatures) table.set(.Ligatures, null);
if (builtins.capabilities.panel_transitions) {
table.set(.PanelSlide, "as");
table.set(.PanelZoom, "az");
@@ -573,6 +574,9 @@ pub const Runtime = struct {
workspace_tag: bool = true,
column_tags: bool = true,
syntax_bold: bool = false,
+ /// A font's programming ligatures (`->`, `!=`) draw across their cells.
+ /// Only a shell that shapes text has the setting (`Capabilities.ligatures`).
+ ligatures: bool = true,
/// Builtins announce themselves on the message row.
verbose: bool = true,
/// A message falls into its row and dissolves out of it.
@@ -679,6 +683,8 @@ pub const Runtime = struct {
tagline_font_size: bool,
window_opacity: bool = false,
window_blur: bool = false,
+ /// The shell shapes text, so it can draw ligatures and stop drawing them.
+ ligatures: bool = false,
};
pub const Capability = std.meta.FieldEnum(Capabilities);
@@ -686,7 +692,7 @@ pub const Runtime = struct {
/// The message timings a setting word sets, each in milliseconds.
pub const MessageTime = enum { linger, fall, dissolve };
- pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, column_tags, syntax_bold, verbose, message_animation };
+ pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, column_tags, syntax_bold, verbose, message_animation, ligatures };
pub const Scene = std.meta.FieldEnum(layout.SceneEffect);
pub const Action = union(enum) {
@@ -737,6 +743,7 @@ pub const Runtime = struct {
.{ .word = "SyntaxBold", .action = .{ .toggle = .syntax_bold } },
.{ .word = "Verbose", .action = .{ .toggle = .verbose } },
.{ .word = "MessageAnimation", .action = .{ .toggle = .message_animation } },
+ .{ .word = "Ligatures", .action = .{ .toggle = .ligatures }, .availability = .ligatures },
.{ .word = "MessageLinger", .action = .{ .message_ms = .linger } },
.{ .word = "MessageFall", .action = .{ .message_ms = .fall } },
.{ .word = "MessageDissolve", .action = .{ .message_ms = .dissolve } },
@@ -796,6 +803,13 @@ pub const Runtime = struct {
@compileError("panel effects must use the panel-transition capability"),
.scene => if (setting.availability != .scene_shaders)
@compileError("scene effects must use the scene-shader capability"),
+ // A toggle is plain state any shell can hold, except one for
+ // what only some shells draw: that one exists where they do.
+ .toggle => |field| if (field == .ligatures) {
+ if (setting.availability != .ligatures)
+ @compileError("the ligatures toggle must use the ligatures capability");
+ } else if (setting.availability != null)
+ @compileError("unconditional toggles cannot carry a backend capability"),
else => if (setting.availability != null)
@compileError("unconditional settings cannot carry a backend capability"),
}
@@ -925,6 +939,10 @@ pub const Runtime = struct {
var wrote_transition = false;
for (settings) |setting| switch (setting.action) {
.toggle => |field| {
+ // A toggle for what this shell does not draw (Ligatures in a
+ // terminal, whose text its emulator draws) is no setting here:
+ // it is not a builtin, so the report does not list it either.
+ if (!setting.enabled(context.capabilities)) continue;
const value = switch (field) {
inline else => |f| @field(state, @tagName(f)),
};
@@ -1197,6 +1215,7 @@ pub const Runtime = struct {
.tagline_font_size = true,
.window_opacity = true,
.window_blur = true,
+ .ligatures = true,
},
.state = &state,
};
@@ -1232,6 +1251,7 @@ pub const Runtime = struct {
.syntax_bold => "SyntaxBold on\n",
.verbose => "Verbose off\n",
.message_animation => "MessageAnimation off\n",
+ .ligatures => "Ligatures off\n",
},
.shell => "Shell requested (new panes) fish\n",
.tagline_size => "TaglineSize 73\n",
@@ -1334,6 +1354,8 @@ pub const Runtime = struct {
try std.testing.expect(std.mem.indexOf(u8, defaults_report, "TaglineSize unsupported\n") != null);
try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Look hover delay: off\n") != null);
try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Native images: off\n") != null);
+ // A shell that does not shape text has no Ligatures setting to report.
+ try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Ligatures") == null);
defaults_context.platform = "web";
defaults_context.capabilities.panel_transitions = false;
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 98a203a6..e3229b38 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -2453,6 +2453,7 @@ fn localSession(
core.native_image_patches = true;
observeGuiFont(g, core);
syncTaglineFont(g, core);
+ syncLigatures(g, core);
var prompt_rcs = host_io.Shell.prepare();
defer prompt_rcs.deinit();
@@ -2580,6 +2581,7 @@ fn localSession(
shell.surface = null;
observeGuiFont(g, core);
syncTaglineFont(g, core);
+ syncLigatures(g, core);
}
}
@@ -3966,6 +3968,7 @@ fn pollFrame(ctx: ?*anyopaque) void {
};
syncWindowOpacity(g, core);
syncTaglineFont(g, core);
+ syncLigatures(g, core);
if (core.takeFontRequest()) |path| blk: {
const bytes = filesystem.readFile(s.gpa, path) catch {
core.rejectFont();
@@ -6056,6 +6059,17 @@ fn resetGlyphAtlas(g: *Gui) void {
g.atlas_dirty = true;
}
+/// A changed Ligatures setting drops what was resolved under the old one: the
+/// per-codepoint cells (which say whether a cell is shaped) and the shaped
+/// words. The atlas keeps its glyphs, plain and strip alike; the plain ones
+/// draw either way, and the strips come back into use when ligatures do.
+/// The frame this runs before redraws every cell with the new setting.
+fn syncLigatures(g: *Gui, core: *pardes.Pardes) void {
+ if (core.settings.ligatures == g.words.ligatures) return;
+ g.words.clear();
+ g.words.ligatures = core.settings.ligatures;
+}
+
fn syncTaglineFont(g: *Gui, core: *pardes.Pardes) void {
const percent = core.settings.font.tagline_percent;
if (percent == g.tagline_percent) return;
@@ -6083,6 +6097,10 @@ const Words = struct {
text: std.ArrayListUnmanaged(u32) = .empty,
slots: std.ArrayListUnmanaged(Slot) = .empty,
shaped: std.ArrayListUnmanaged(c.UIShapedCell) = .empty,
+ /// The Ligatures setting. Off, no cell is shaped, so each draws its own
+ /// glyph, as in a font without ligatures. It lives here because every
+ /// cell and word above was resolved under it (`syncLigatures`).
+ ligatures: bool = true,
/// The word the last cell was in, so the rest of it costs a compare. It
/// is positional, so every frame forgets it: the cells change under it.
memo: Memo = .{},
@@ -6144,9 +6162,9 @@ fn wordBreak(a: WordCell, b: WordCell, i: usize, cursor: ?usize) bool {
/// The atlas slot `line[at]` draws: blank for a plain space (or concealed
/// text), its own glyph when no ligature could replace that glyph (every
-/// cell, in a font without ligatures), else its slice of the word around it,
-/// shaped once and cached by the word's text. The cells of a word being walked
-/// cost a compare. Taglines are not shaped: they are drawn at two pitches
+/// cell, in a font without ligatures or with Ligatures off), else its slice
+/// of the word around it, shaped once and cached by the word's text. The
+/// cells of a word being walked cost a compare. Taglines are not shaped: they are drawn at two pitches
/// (tagline_width in a tag layer, a whole cell in the grid), and a strip would
/// not line up in one of them.
fn cellSlot(g: *Gui, line: []const pardes.Cell, at: usize, cursor: ?usize, role: pardes.FontRole) Slot {
@@ -6163,7 +6181,7 @@ fn cellSlot(g: *Gui, line: []const pardes.Cell, at: usize, cursor: ?usize, role:
if (!known.found_existing) {
const drawn = glyphForCodepoint(g, here.cp);
const font = if (drawn.face == 0) g.font else g.fallbacks[drawn.face - 1].?.face;
- known.value_ptr.* = if (role == .body and c.ui_font_substitutes(font, drawn.glyph) != 0)
+ known.value_ptr.* = if (role == .body and words.ligatures and c.ui_font_substitutes(font, drawn.glyph) != 0)
Words.shape_slot
else
cachedGlyph(g, .{ .face = drawn.face, .glyph = drawn.glyph, .role = role, .decoration = here.decoration }) orelse g.space_slot;
@@ -6328,6 +6346,40 @@ test "a ligature draws across its cells, one slice each, and comes apart under t
try std.testing.expectEqual(c.ui_font_glyph(font, '-'), (try glyphKeyAt(&g, cellSlot(&g, &line, 1, null, .tagline))).glyph);
}
+test "Ligatures off draws a ligature as its cells' own glyphs, and on shapes it again" {
+ const bytes = try filesystem.readFile(std.testing.allocator, "assets/MapleMono-NF-Regular.ttf");
+ defer std.testing.allocator.free(bytes);
+ const font = c.ui_font_new(bytes.ptr, @intCast(bytes.len)) orelse return error.FontInit;
+ defer c.ui_font_free(font);
+ var g = try textTestGui(font, 27);
+ defer font_allocator = std.heap.smp_allocator;
+ defer std.testing.allocator.free(g.atlas_stage);
+ defer g.glyphs.deinit();
+ defer g.words.deinit(std.testing.allocator);
+ const core = try pardes.Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 });
+ defer core.deinit();
+
+ const text = "a->b";
+ const line = textTestLine(text);
+ var shaped: [text.len]Slot = undefined;
+ for (&shaped, 0..) |*slot, i| slot.* = cellSlot(&g, &line, i, null, .body);
+ try std.testing.expectEqual(shaped[1].u + g.cell_w, shaped[2].u);
+
+ // Off, every cell is the slot the per-glyph path gives its codepoint.
+ try std.testing.expect(core.executeBuiltinLine(core.active, "Ligatures off"));
+ syncLigatures(&g, core);
+ for (text, 0..) |byte, i| {
+ const plain = cachedGlyph(&g, .{ .face = 0, .glyph = c.ui_font_glyph(font, byte), .role = .body }).?;
+ try std.testing.expectEqual(plain, cellSlot(&g, &line, i, null, .body));
+ }
+ try std.testing.expect(!std.meta.eql(shaped[1], cellSlot(&g, &line, 1, null, .body)));
+
+ // On again, the arrow is its strip once more.
+ try std.testing.expect(core.executeBuiltinLine(core.active, "Ligatures on"));
+ syncLigatures(&g, core);
+ for (shaped, 0..) |slot, i| try std.testing.expectEqual(slot, cellSlot(&g, &line, i, null, .body));
+}
+
fn glyphKeyAt(g: *Gui, slot: Slot) !GlyphKey {
var it = g.glyphs.iterator();
while (it.next()) |entry| if (std.meta.eql(entry.value_ptr.*, slot)) return entry.key_ptr.*;