diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b042b95..7a9beb7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Secondary device attributes escape (`CSI > 0 c`) +### Changed + +- Cursors are now inverted when their fixed color is similar to the cell's background + ## 0.5.0-dev ### Packaging diff --git a/alacritty_terminal/src/term/color.rs b/alacritty_terminal/src/term/color.rs index 9aeb7061..fc35952b 100644 --- a/alacritty_terminal/src/term/color.rs +++ b/alacritty_terminal/src/term/color.rs @@ -25,6 +25,42 @@ pub struct Rgb { pub b: u8, } +impl Rgb { + /// Implementation of W3C's luminance algorithm: + /// https://www.w3.org/TR/WCAG20/#relativeluminancedef + fn luminance(self) -> f64 { + let channel_luminance = |channel| { + let channel = channel as f64 / 255.; + if channel <= 0.03928 { + channel / 12.92 + } else { + f64::powf((channel + 0.055) / 1.055, 2.4) + } + }; + + let r_luminance = channel_luminance(self.r); + let g_luminance = channel_luminance(self.g); + let b_luminance = channel_luminance(self.b); + + 0.2126 * r_luminance + 0.7152 * g_luminance + 0.0722 * b_luminance + } + + /// Implementation of W3C's contrast algorithm: + /// https://www.w3.org/TR/WCAG20/#contrast-ratiodef + pub fn contrast(self, other: Rgb) -> f64 { + let self_luminance = self.luminance(); + let other_luminance = other.luminance(); + + let (darker, lighter) = if self_luminance > other_luminance { + (other_luminance, self_luminance) + } else { + (self_luminance, other_luminance) + }; + + (lighter + 0.05) / (darker + 0.05) + } +} + // A multiply function for Rgb, as the default dim is just *2/3. impl Mul for Rgb { type Output = Rgb; @@ -370,3 +406,28 @@ impl IndexMut for List { &mut self.0[idx as usize] } } + +#[cfg(test)] +mod tests { + use super::*; + + use std::f64::EPSILON; + + #[test] + fn contrast() { + let rgb1 = Rgb { r: 0xff, g: 0xff, b: 0xff }; + let rgb2 = Rgb { r: 0x00, g: 0x00, b: 0x00 }; + assert!((rgb1.contrast(rgb2) - 21.).abs() < EPSILON); + + let rgb1 = Rgb { r: 0xff, g: 0xff, b: 0xff }; + assert!((rgb1.contrast(rgb1) - 1.).abs() < EPSILON); + + let rgb1 = Rgb { r: 0xff, g: 0x00, b: 0xff }; + let rgb2 = Rgb { r: 0x00, g: 0xff, b: 0x00 }; + assert!((rgb1.contrast(rgb2) - 2.285_543_608_124_253_3).abs() < EPSILON); + + let rgb1 = Rgb { r: 0x12, g: 0x34, b: 0x56 }; + let rgb2 = Rgb { r: 0xfe, g: 0xdc, b: 0xba }; + assert!((rgb1.contrast(rgb2) - 9.786_558_997_257_74).abs() < EPSILON); + } +} diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index f27852de..7d00961e 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -31,6 +31,9 @@ mod search; /// Max size of the window title stack. const TITLE_STACK_MAX_DEPTH: usize = 4096; +/// Minimum contrast between a fixed cursor color and the cell's background. +const MIN_CURSOR_CONTRAST: f64 = 1.5; + /// Maximum number of linewraps followed outside of the viewport during search highlighting. const MAX_SEARCH_LINES: usize = 100; @@ -389,13 +392,19 @@ impl<'a, C> Iterator for RenderableCellsIter<'a, C> { if self.cursor.point.line == self.inner.line() && self.cursor.point.col == self.inner.column() { - // Handle cell below cursor. if self.cursor.rendered { + // Handle cell below cursor. let cell = self.inner.next()?; let mut cell = RenderableCell::new(self, cell); if self.cursor.key.style == CursorStyle::Block { - cell.fg = self.cursor.text_color.color(cell.fg, cell.bg); + // Invert cursor if static background is close to the cell's background. + match self.cursor.cursor_color { + CellRgb::Rgb(col) if col.contrast(cell.bg) >= MIN_CURSOR_CONTRAST => { + cell.fg = self.cursor.text_color.color(cell.fg, cell.bg); + }, + _ => cell.fg = cell.bg, + } } return Some(cell); @@ -412,7 +421,13 @@ impl<'a, C> Iterator for RenderableCellsIter<'a, C> { let mut cell = RenderableCell::new(self, cell); cell.inner = RenderableCellContent::Cursor(self.cursor.key); - cell.fg = self.cursor.cursor_color.color(cell.fg, cell.bg); + + // Only apply static color if it isn't close to the cell's current background. + if let CellRgb::Rgb(color) = self.cursor.cursor_color { + if color.contrast(cell.bg) >= MIN_CURSOR_CONTRAST { + cell.fg = self.cursor.cursor_color.color(cell.fg, cell.bg); + } + } return Some(cell); }