diff --git a/src/config/Config.zig b/src/config/Config.zig index 216e7b685..4b4231455 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -195,6 +195,12 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// the selection color will vary across the selection. @"selection-invert-fg-bg": bool = false, +/// The minimum contrast ratio between the foreground and background +/// colors. The contrast ratio is a value between 1 and 21. A value of +/// 1 allows for no contrast (i.e. black on black). This value is +/// the contrast ratio as defined by the WCAG 2.0 specification. +@"minimum-contrast": f64 = 1, + /// Color palette for the 256 color form that many terminal applications /// use. The syntax of this configuration is "N=HEXCODE" where "n" /// is 0 to 255 (for the 256 colors) and HEXCODE is a typical RGB @@ -1569,6 +1575,9 @@ pub fn finalize(self: *Config) !void { // Clamp our split opacity self.@"unfocused-split-opacity" = @min(1.0, @max(0.15, self.@"unfocused-split-opacity")); + // Clamp our contrast + self.@"minimum-contrast" = @min(21, @max(1, self.@"minimum-contrast")); + // Minimmum window size if (self.@"window-width" > 0) self.@"window-width" = @max(10, self.@"window-width"); if (self.@"window-height" > 0) self.@"window-height" = @max(4, self.@"window-height"); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index df494ef41..d230c6d74 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -152,6 +152,7 @@ pub const DerivedConfig = struct { selection_background: ?terminal.color.RGB, selection_foreground: ?terminal.color.RGB, invert_selection_fg_bg: bool, + min_contrast: f32, custom_shaders: std.ArrayListUnmanaged([]const u8), custom_shader_animation: bool, links: link.Set, @@ -203,6 +204,7 @@ pub const DerivedConfig = struct { .background = config.background.toTerminalRGB(), .foreground = config.foreground.toTerminalRGB(), .invert_selection_fg_bg = config.@"selection-invert-fg-bg", + .min_contrast = @floatCast(config.@"minimum-contrast"), .selection_background = if (config.@"selection-background") |bg| bg.toTerminalRGB() @@ -374,6 +376,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .cell_size = undefined, .strikethrough_position = @floatFromInt(metrics.strikethrough_position), .strikethrough_thickness = @floatFromInt(metrics.strikethrough_thickness), + .min_contrast = options.config.min_contrast, }, // Fonts @@ -531,6 +534,7 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void { }, .strikethrough_position = @floatFromInt(metrics.strikethrough_position), .strikethrough_thickness = @floatFromInt(metrics.strikethrough_thickness), + .min_contrast = self.uniforms.min_contrast, }; // Recalculate our cell size. If it is the same as before, then we do @@ -1257,6 +1261,9 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void { self.font_shaper = font_shaper; } + // Set our new minimum contrast + self.uniforms.min_contrast = config.min_contrast; + self.config.deinit(); self.config = config.*; } @@ -1305,6 +1312,7 @@ pub fn setScreenSize( }, .strikethrough_position = old.strikethrough_position, .strikethrough_thickness = old.strikethrough_thickness, + .min_contrast = old.min_contrast, }; // Reset our buffer sizes so that we free memory when the screen shrinks. @@ -1705,7 +1713,12 @@ pub fn updateCell( }); break :bg .{ rgb.r, rgb.g, rgb.b, bg_alpha }; - } else .{ 0, 0, 0, 0 }; + } else .{ + self.current_background_color.r, + self.current_background_color.g, + self.current_background_color.b, + @intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))), + }; // If the cell has a character, draw it if (cell.char > 0) { diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 6980a309e..bd938802d 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -126,6 +126,10 @@ pub const Uniforms = extern struct { /// Metrics for underline/strikethrough strikethrough_position: f32, strikethrough_thickness: f32, + + /// The minimum contrast ratio for text. The contrast ratio is calculated + /// according to the WCAG 2.0 spec. + min_contrast: f32, }; /// The uniforms used for custom postprocess shaders. diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index b8116e1c0..2abd5acd0 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -13,6 +13,7 @@ struct Uniforms { float2 cell_size; float strikethrough_position; float strikethrough_thickness; + float min_contrast; }; struct VertexIn { @@ -29,7 +30,11 @@ struct VertexIn { // the text color. For styles, this is the color of the style. uchar4 color [[ attribute(5) ]]; - // The fields below are present only when rendering text. + // The fields below are present only when rendering text (fg mode) + + // The background color of the cell. This is used to determine if + // we need to render the text with a different color to ensure + // contrast. uchar4 bg_color [[ attribute(7) ]]; // The position of the glyph in the texture (x,y) @@ -79,13 +84,17 @@ float contrast_ratio(float3 color1, float3 color2) { return (max(l1, l2) + 0.05f) / (min(l1, l2) + 0.05f); } -float4 contrasted_color(float4 fg, float4 bg) { +// Return the fg if the contrast ratio is greater than min, otherwise +// return a color that satisfies the contrast ratio. Currently, the color +// is always white or black, whichever has the highest contrast ratio. +float4 contrasted_color(float min, float4 fg, float4 bg) { float3 fg_premult = fg.rgb * fg.a; float3 bg_premult = bg.rgb * bg.a; float ratio = contrast_ratio(fg_premult, bg_premult); - if (ratio <= 3.0f) { - float ratio = contrast_ratio(float3(1.0f), bg_premult); - if (ratio > 3.0f) { + if (ratio < min) { + float white_ratio = contrast_ratio(float3(1.0f), bg_premult); + float black_ratio = contrast_ratio(float3(0.0f), bg_premult); + if (white_ratio > black_ratio) { return float4(1.0f); } else { return float4(0.0f, 0.0f, 0.0f, 1.0f); @@ -159,7 +168,7 @@ vertex VertexOut uber_vertex( // (between 0.0 and 1.0) and must be done in the fragment shader. out.tex_coord = float2(input.glyph_pos) + float2(input.glyph_size) * position; - out.color = contrasted_color(out.color, float4(input.bg_color) / 255.0f); + out.color = contrasted_color(uniforms.min_contrast, out.color, float4(input.bg_color) / 255.0f); break; }