renderer/metal: minimum contrast ratio is configurable

This commit is contained in:
Mitchell Hashimoto
2023-12-01 21:24:38 -08:00
parent 6c859cca82
commit 7af4009f27
4 changed files with 42 additions and 7 deletions

View File

@ -195,6 +195,12 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
/// the selection color will vary across the selection. /// the selection color will vary across the selection.
@"selection-invert-fg-bg": bool = false, @"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 /// Color palette for the 256 color form that many terminal applications
/// use. The syntax of this configuration is "N=HEXCODE" where "n" /// 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 /// 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 // Clamp our split opacity
self.@"unfocused-split-opacity" = @min(1.0, @max(0.15, self.@"unfocused-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 // Minimmum window size
if (self.@"window-width" > 0) self.@"window-width" = @max(10, self.@"window-width"); 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"); if (self.@"window-height" > 0) self.@"window-height" = @max(4, self.@"window-height");

View File

@ -152,6 +152,7 @@ pub const DerivedConfig = struct {
selection_background: ?terminal.color.RGB, selection_background: ?terminal.color.RGB,
selection_foreground: ?terminal.color.RGB, selection_foreground: ?terminal.color.RGB,
invert_selection_fg_bg: bool, invert_selection_fg_bg: bool,
min_contrast: f32,
custom_shaders: std.ArrayListUnmanaged([]const u8), custom_shaders: std.ArrayListUnmanaged([]const u8),
custom_shader_animation: bool, custom_shader_animation: bool,
links: link.Set, links: link.Set,
@ -203,6 +204,7 @@ pub const DerivedConfig = struct {
.background = config.background.toTerminalRGB(), .background = config.background.toTerminalRGB(),
.foreground = config.foreground.toTerminalRGB(), .foreground = config.foreground.toTerminalRGB(),
.invert_selection_fg_bg = config.@"selection-invert-fg-bg", .invert_selection_fg_bg = config.@"selection-invert-fg-bg",
.min_contrast = @floatCast(config.@"minimum-contrast"),
.selection_background = if (config.@"selection-background") |bg| .selection_background = if (config.@"selection-background") |bg|
bg.toTerminalRGB() bg.toTerminalRGB()
@ -374,6 +376,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.cell_size = undefined, .cell_size = undefined,
.strikethrough_position = @floatFromInt(metrics.strikethrough_position), .strikethrough_position = @floatFromInt(metrics.strikethrough_position),
.strikethrough_thickness = @floatFromInt(metrics.strikethrough_thickness), .strikethrough_thickness = @floatFromInt(metrics.strikethrough_thickness),
.min_contrast = options.config.min_contrast,
}, },
// Fonts // Fonts
@ -531,6 +534,7 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void {
}, },
.strikethrough_position = @floatFromInt(metrics.strikethrough_position), .strikethrough_position = @floatFromInt(metrics.strikethrough_position),
.strikethrough_thickness = @floatFromInt(metrics.strikethrough_thickness), .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 // 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; self.font_shaper = font_shaper;
} }
// Set our new minimum contrast
self.uniforms.min_contrast = config.min_contrast;
self.config.deinit(); self.config.deinit();
self.config = config.*; self.config = config.*;
} }
@ -1305,6 +1312,7 @@ pub fn setScreenSize(
}, },
.strikethrough_position = old.strikethrough_position, .strikethrough_position = old.strikethrough_position,
.strikethrough_thickness = old.strikethrough_thickness, .strikethrough_thickness = old.strikethrough_thickness,
.min_contrast = old.min_contrast,
}; };
// Reset our buffer sizes so that we free memory when the screen shrinks. // 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 }; 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 the cell has a character, draw it
if (cell.char > 0) { if (cell.char > 0) {

View File

@ -126,6 +126,10 @@ pub const Uniforms = extern struct {
/// Metrics for underline/strikethrough /// Metrics for underline/strikethrough
strikethrough_position: f32, strikethrough_position: f32,
strikethrough_thickness: 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. /// The uniforms used for custom postprocess shaders.

View File

@ -13,6 +13,7 @@ struct Uniforms {
float2 cell_size; float2 cell_size;
float strikethrough_position; float strikethrough_position;
float strikethrough_thickness; float strikethrough_thickness;
float min_contrast;
}; };
struct VertexIn { struct VertexIn {
@ -29,7 +30,11 @@ struct VertexIn {
// the text color. For styles, this is the color of the style. // the text color. For styles, this is the color of the style.
uchar4 color [[ attribute(5) ]]; 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) ]]; uchar4 bg_color [[ attribute(7) ]];
// The position of the glyph in the texture (x,y) // 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); 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 fg_premult = fg.rgb * fg.a;
float3 bg_premult = bg.rgb * bg.a; float3 bg_premult = bg.rgb * bg.a;
float ratio = contrast_ratio(fg_premult, bg_premult); float ratio = contrast_ratio(fg_premult, bg_premult);
if (ratio <= 3.0f) { if (ratio < min) {
float ratio = contrast_ratio(float3(1.0f), bg_premult); float white_ratio = contrast_ratio(float3(1.0f), bg_premult);
if (ratio > 3.0f) { float black_ratio = contrast_ratio(float3(0.0f), bg_premult);
if (white_ratio > black_ratio) {
return float4(1.0f); return float4(1.0f);
} else { } else {
return float4(0.0f, 0.0f, 0.0f, 1.0f); 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. // (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.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; break;
} }