diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/builtins.zig | 26 | ||||
| -rw-r--r-- | src/config.zig | 24 | ||||
| -rw-r--r-- | src/gui/gui.zig | 60 |
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.*; |
