diff --git a/src/Surface.zig b/src/Surface.zig index 10185bf41..fe15e7659 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -229,6 +229,11 @@ pub fn init( group.codepoint_map = config.@"font-codepoint-map".map; } + // Set our styles + group.styles.set(.bold, config.@"font-style-bold" != .false); + group.styles.set(.italic, config.@"font-style-italic" != .false); + group.styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); + // Search for fonts if (font.Discover != void) discover: { const disco = try app.fontDiscover() orelse { @@ -243,7 +248,7 @@ pub fn init( if (config.@"font-family") |family| { var disco_it = try disco.discover(.{ .family = family, - .style = config.@"font-style", + .style = config.@"font-style".nameValue(), .size = font_size.points, .variations = config.@"font-variation".list.items, }); @@ -256,7 +261,7 @@ pub fn init( if (config.@"font-family-bold") |family| { var disco_it = try disco.discover(.{ .family = family, - .style = config.@"font-style-bold", + .style = config.@"font-style-bold".nameValue(), .size = font_size.points, .bold = true, .variations = config.@"font-variation-bold".list.items, @@ -270,7 +275,7 @@ pub fn init( if (config.@"font-family-italic") |family| { var disco_it = try disco.discover(.{ .family = family, - .style = config.@"font-style-italic", + .style = config.@"font-style-italic".nameValue(), .size = font_size.points, .italic = true, .variations = config.@"font-variation-italic".list.items, @@ -284,7 +289,7 @@ pub fn init( if (config.@"font-family-bold-italic") |family| { var disco_it = try disco.discover(.{ .family = family, - .style = config.@"font-style-bold-italic", + .style = config.@"font-style-bold-italic".nameValue(), .size = font_size.points, .bold = true, .italic = true, diff --git a/src/cli/args.zig b/src/cli/args.zig index 833914c88..9eb09b3cc 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -154,10 +154,11 @@ fn parseIntoField( else => field.type, }; - // If we are a struct and have parseCLI, we call that and use - // that to set the value. - switch (@typeInfo(Field)) { - .Struct => if (@hasDecl(Field, "parseCLI")) { + // If we are a type that can have decls and have a parseCLI decl, + // we call that and use that to set the value. + const fieldInfo = @typeInfo(Field); + if (fieldInfo == .Struct or fieldInfo == .Union or fieldInfo == .Enum) { + if (@hasDecl(Field, "parseCLI")) { const fnInfo = @typeInfo(@TypeOf(Field.parseCLI)).Fn; switch (fnInfo.params.len) { // 1 arg = (input) => output @@ -182,8 +183,10 @@ fn parseIntoField( } return; - }, + } + } + switch (fieldInfo) { .Enum => { @field(dst, field.name) = std.meta.stringToEnum( Field, diff --git a/src/config/Config.zig b/src/config/Config.zig index 736a82747..16b465168 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -33,12 +33,19 @@ const c = @cImport({ /// styles. This looks up the style based on the font style string advertised /// by the font itself. For example, "Iosevka Heavy" has a style of "Heavy". /// +/// You can also use these fields to completely disable a font style. If +/// you set the value of the configuration below to literal "false" then +/// that font style will be disabled. If the running program in the terminal +/// requests a disabled font style, the regular font style will be used +/// instead. +/// /// These are only valid if there is an exact font-family also specified. -/// If no font-family is specified, then the font-style is ignored. -@"font-style": ?[:0]const u8 = null, -@"font-style-bold": ?[:0]const u8 = null, -@"font-style-italic": ?[:0]const u8 = null, -@"font-style-bold-italic": ?[:0]const u8 = null, +/// If no font-family is specified, then the font-style is ignored unless +/// you're disabling the font style. +@"font-style": FontStyle = .{ .default = {} }, +@"font-style-bold": FontStyle = .{ .default = {} }, +@"font-style-italic": FontStyle = .{ .default = {} }, +@"font-style-bold-italic": FontStyle = .{ .default = {} }, /// Apply a font feature. This can be repeated multiple times to enable /// multiple font features. You can NOT set multiple font features with @@ -1006,12 +1013,20 @@ fn cloneValue(alloc: Allocator, comptime T: type, src: T) !T { else => {}, } + // If we're a type that can have decls and we have clone, then + // call clone and be done. + const t = @typeInfo(T); + if (t == .Struct or t == .Enum or t == .Union) { + if (@hasDecl(T, "clone")) return try src.clone(alloc); + } + // Back into types of types - switch (@typeInfo(T)) { + switch (t) { inline .Bool, .Int, .Float, .Enum, + .Union, => return src, .Optional => |info| return try cloneValue( @@ -1705,6 +1720,63 @@ pub const RepeatableCodepointMap = struct { } }; +pub const FontStyle = union(enum) { + const Self = @This(); + + /// Use the default font style that font discovery finds. + default: void, + + /// Disable this font style completely. This will fall back to using + /// the regular font when this style is encountered. + false: void, + + /// A specific named font style to use for this style. + name: [:0]const u8, + + pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { + const value = input orelse return error.ValueRequired; + + if (std.mem.eql(u8, value, "default")) { + self.* = .{ .default = {} }; + return; + } + + if (std.mem.eql(u8, value, "false")) { + self.* = .{ .false = {} }; + return; + } + + const nameZ = try alloc.dupeZ(u8, value); + self.* = .{ .name = nameZ }; + } + + /// Returns the string name value that can be used with a font + /// descriptor. + pub fn nameValue(self: Self) ?[:0]const u8 { + return switch (self) { + .default, .false => null, + .name => self.name, + }; + } + + test "parseCLI" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var p: Self = .{ .default = {} }; + try p.parseCLI(alloc, "default"); + try testing.expectEqual(Self{ .default = {} }, p); + + try p.parseCLI(alloc, "false"); + try testing.expectEqual(Self{ .false = {} }, p); + + try p.parseCLI(alloc, "bold"); + try testing.expectEqualStrings("bold", p.name); + } +}; + /// Options for copy on select behavior. pub const CopyOnSelect = enum { /// Disables copy on select entirely. diff --git a/src/font/Group.zig b/src/font/Group.zig index 62b62fc72..88eb6b3aa 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -33,6 +33,9 @@ const log = std.log.scoped(.font_group); // to the user so we can change this later. const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(GroupFace)); +/// Packed array of booleans to indicate if a style is enabled or not. +pub const StyleStatus = std.EnumArray(Style, bool); + /// Map of descriptors to faces. This is used with manual codepoint maps /// to ensure that we don't load the same font multiple times. /// @@ -72,6 +75,12 @@ size: font.face.DesiredSize, /// Instead, use the functions available on Group. faces: StyleArray, +/// The set of statuses and whether they're enabled or not. This defaults +/// to true. This can be changed at runtime with no ill effect. If you +/// change this at runtime and are using a GroupCache, the GroupCache +/// must be reset. +styles: StyleStatus = StyleStatus.initFill(true), + /// If discovery is available, we'll look up fonts where we can't find /// the codepoint. This can be set after initialization. discover: ?*font.Discover = null, @@ -150,13 +159,6 @@ pub fn addFace(self: *Group, style: Style, face: GroupFace) !FontIndex { return .{ .style = style, .idx = @intCast(idx) }; } -/// Returns true if we have a face for the given style, though the face may -/// not be loaded yet. -pub fn hasFaceForStyle(self: Group, style: Style) bool { - const list = self.faces.get(style); - return list.items.len > 0; -} - /// This will automatically create an italicized font from the regular /// font face if we don't have any italicized fonts. pub fn italicize(self: *Group) !void { @@ -278,6 +280,11 @@ pub fn indexForCodepoint( style: Style, p: ?Presentation, ) ?FontIndex { + // If we've disabled a font style, then fall back to regular. + if (style != .regular and !self.styles.get(style)) { + return self.indexForCodepoint(cp, .regular, p); + } + // Codepoint overrides. if (self.indexForCodepointOverride(cp)) |idx_| { if (idx_) |idx| return idx; @@ -669,6 +676,50 @@ test { } } +test "disabled font style" { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = @import("test.zig").fontRegular; + + var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); + defer atlas_greyscale.deinit(alloc); + + var lib = try Library.init(); + defer lib.deinit(); + + var group = try init(alloc, lib, .{ .points = 12 }); + defer group.deinit(); + + // Disable bold + group.styles.set(.bold, false); + + // Same font but we can test the style in the index + _ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) }); + _ = try group.addFace(.bold, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) }); + _ = try group.addFace(.italic, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) }); + + // Regular should work fine + { + const idx = group.indexForCodepoint('A', .regular, null).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); + } + + // Bold should go to regular + { + const idx = group.indexForCodepoint('A', .bold, null).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); + } + + // Italic should still work + { + const idx = group.indexForCodepoint('A', .italic, null).?; + try testing.expectEqual(Style.italic, idx.style); + try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); + } +} + test "face count limit" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 8a621c3eb..7e9051d37 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -102,6 +102,7 @@ texture_color: objc.Object, // MTLTexture pub const DerivedConfig = struct { font_thicken: bool, font_features: std.ArrayList([]const u8), + font_styles: font.Group.StyleStatus, cursor_color: ?terminal.color.RGB, cursor_text: ?terminal.color.RGB, background: terminal.color.RGB, @@ -121,10 +122,17 @@ pub const DerivedConfig = struct { }; errdefer font_features.deinit(); + // Get our font styles + var font_styles = font.Group.StyleStatus.initFill(true); + font_styles.set(.bold, config.@"font-style-bold" != .false); + font_styles.set(.italic, config.@"font-style-italic" != .false); + font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); + return .{ .background_opacity = @max(0, @min(1, config.@"background-opacity")), .font_thicken = config.@"font-thicken", .font_features = font_features, + .font_styles = font_styles, .cursor_color = if (config.@"cursor-color") |col| col.toTerminalRGB() @@ -975,13 +983,15 @@ fn prepKittyGraphics( /// Update the configuration. pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void { - // If font thickening settings change, we need to reset our - // font texture completely because we need to re-render the glyphs. - if (self.config.font_thicken != config.font_thicken) { - self.font_group.reset(); - self.font_group.atlas_greyscale.clear(); - self.font_group.atlas_color.clear(); - } + // On configuration change we always reset our font group. There + // are a variety of configurations that can change font settings + // so to be safe we just always reset it. This has a performance hit + // when its not necessary but config reloading shouldn't be so + // common to cause a problem. + self.font_group.reset(); + self.font_group.group.styles = config.font_styles; + self.font_group.atlas_greyscale.clear(); + self.font_group.atlas_color.clear(); // We always redo the font shaper in case font features changed. We // could check to see if there was an actual config change but this is diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 67e682609..44e52b4a6 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -216,6 +216,7 @@ const GPUCellMode = enum(u8) { pub const DerivedConfig = struct { font_thicken: bool, font_features: std.ArrayList([]const u8), + font_styles: font.Group.StyleStatus, cursor_color: ?terminal.color.RGB, cursor_text: ?terminal.color.RGB, background: terminal.color.RGB, @@ -235,10 +236,17 @@ pub const DerivedConfig = struct { }; errdefer font_features.deinit(); + // Get our font styles + var font_styles = font.Group.StyleStatus.initFill(true); + font_styles.set(.bold, config.@"font-style-bold" != .false); + font_styles.set(.italic, config.@"font-style-italic" != .false); + font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); + return .{ .background_opacity = @max(0, @min(1, config.@"background-opacity")), .font_thicken = config.@"font-thicken", .font_features = font_features, + .font_styles = font_styles, .cursor_color = if (config.@"cursor-color") |col| col.toTerminalRGB() @@ -1205,13 +1213,15 @@ fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.Grid /// Update the configuration. pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void { - // If font thickening settings change, we need to reset our - // font texture completely because we need to re-render the glyphs. - if (self.config.font_thicken != config.font_thicken) { - self.font_group.reset(); - self.font_group.atlas_greyscale.clear(); - self.font_group.atlas_color.clear(); - } + // On configuration change we always reset our font group. There + // are a variety of configurations that can change font settings + // so to be safe we just always reset it. This has a performance hit + // when its not necessary but config reloading shouldn't be so + // common to cause a problem. + self.font_group.reset(); + self.font_group.group.styles = config.font_styles; + self.font_group.atlas_greyscale.clear(); + self.font_group.atlas_color.clear(); // We always redo the font shaper in case font features changed. We // could check to see if there was an actual config change but this is