1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-22 21:01:36 +03:00

allow setting selection colors with alpha values

This commit allows the following configuration:

```
wezterm -n --config 'colors = { selection_fg = "clear", selection_bg = "rgba:50% 50% 50% 50%" }'
```

which sets the selection_bg to fully transparent, and selection_bg to
50% transparent gray.

When selection_fg is fully transparent we'll use the normal fg color.

When selection_bg is partially (or fully!) transparent, it will be
alpha blended over the current cell background color.

To support this, the config file will now accept rgba colors specified
as 4 whitespace delimited numeric values. If a value ends with `%` it
is interpreted as a number in the range 0-100.  Otherwise, it is
interpreted as a number in the range 0-255.  The 4 values are
red, green, blue, alpha.

At this time, only the selection_fg and selection_bg settings accept
alpha values.

refs: #1615
This commit is contained in:
Wez Furlong 2022-02-05 15:13:19 -07:00
parent 240026de48
commit 8cb74c62d2
11 changed files with 137 additions and 31 deletions

1
Cargo.lock generated
View File

@ -4701,6 +4701,7 @@ name = "wezterm-color-types"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"serde",
] ]
[[package]] [[package]]

View File

@ -405,7 +405,7 @@ macro_rules! pdu {
/// The overall version of the codec. /// The overall version of the codec.
/// This must be bumped when backwards incompatible changes /// This must be bumped when backwards incompatible changes
/// are made to the types and protocol. /// are made to the types and protocol.
pub const CODEC_VERSION: usize = 17; pub const CODEC_VERSION: usize = 18;
// Defines the Pdu enum. // Defines the Pdu enum.
// Each struct has an explicit identifying number. // Each struct has an explicit identifying number.

View File

@ -7,5 +7,9 @@ repository = "https://github.com/wez/wezterm"
description = "Types for working with colors" description = "Types for working with colors"
license = "MIT" license = "MIT"
[features]
use_serde = ["serde"]
[dependencies] [dependencies]
lazy_static = "1.4" lazy_static = "1.4"
serde = {version="1.0", features = ["derive"], optional=true}

View File

@ -1,3 +1,5 @@
#[cfg(feature = "use_serde")]
use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::str::FromStr; use std::str::FromStr;
@ -209,6 +211,7 @@ impl SrgbaPixel {
/// A pixel value encoded as SRGBA RGBA values in f32 format (range: 0.0-1.0) /// A pixel value encoded as SRGBA RGBA values in f32 format (range: 0.0-1.0)
#[derive(Copy, Clone, Debug, Default, PartialEq)] #[derive(Copy, Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
pub struct SrgbaTuple(pub f32, pub f32, pub f32, pub f32); pub struct SrgbaTuple(pub f32, pub f32, pub f32, pub f32);
impl From<(f32, f32, f32, f32)> for SrgbaTuple { impl From<(f32, f32, f32, f32)> for SrgbaTuple {
@ -230,6 +233,11 @@ lazy_static::lazy_static! {
fn build_colors() -> HashMap<String, SrgbaTuple> { fn build_colors() -> HashMap<String, SrgbaTuple> {
let mut map = HashMap::new(); let mut map = HashMap::new();
let rgb_txt = include_str!("rgb.txt"); let rgb_txt = include_str!("rgb.txt");
map.insert("transparent".to_string(), SrgbaTuple(0., 0., 0., 0.));
map.insert("none".to_string(), SrgbaTuple(0., 0., 0., 0.));
map.insert("clear".to_string(), SrgbaTuple(0., 0., 0., 0.));
for line in rgb_txt.lines() { for line in rgb_txt.lines() {
let mut fields = line.split_ascii_whitespace(); let mut fields = line.split_ascii_whitespace();
let red = fields.next().unwrap(); let red = fields.next().unwrap();
@ -289,6 +297,16 @@ impl SrgbaTuple {
) )
} }
pub fn to_rgba_string(self) -> String {
format!(
"rgba:{} {} {} {}%",
(self.0 * 255.) as u8,
(self.1 * 255.) as u8,
(self.2 * 255.) as u8,
(self.3 * 100.) as u8
)
}
/// Returns a string of the form `rgb:RRRR/GGGG/BBBB` /// Returns a string of the form `rgb:RRRR/GGGG/BBBB`
pub fn to_x11_16bit_rgb_string(self) -> String { pub fn to_x11_16bit_rgb_string(self) -> String {
format!( format!(
@ -402,6 +420,31 @@ impl FromStr for SrgbaTuple {
let blue = digit!(); let blue = digit!();
Ok(Self(red, green, blue, 1.0)) Ok(Self(red, green, blue, 1.0))
} else if s.starts_with("rgba:") {
let fields: Vec<_> = s[5..].split_ascii_whitespace().collect();
if fields.len() == 4 {
fn field(s: &str) -> Result<f32, ()> {
if s.ends_with('%') {
let v: f32 = s[0..s.len() - 1].parse().map_err(|_| ())?;
Ok(v / 100.)
} else {
let v: f32 = s.parse().map_err(|_| ())?;
if v > 255.0 || v < 0. {
Err(())
} else {
Ok(v / 255.)
}
}
}
let r: f32 = field(fields[0])?;
let g: f32 = field(fields[1])?;
let b: f32 = field(fields[2])?;
let a: f32 = field(fields[3])?;
Ok(Self(r, g, b, a))
} else {
Err(())
}
} else if s.starts_with("hsl:") { } else if s.starts_with("hsl:") {
let fields: Vec<_> = s[4..].split_ascii_whitespace().collect(); let fields: Vec<_> = s[4..].split_ascii_whitespace().collect();
if fields.len() == 3 { if fields.len() == 3 {
@ -431,7 +474,7 @@ impl FromStr for SrgbaTuple {
Err(()) Err(())
} }
} else { } else {
Err(()) Self::from_named(s).ok_or(())
} }
} }
} }
@ -470,6 +513,20 @@ impl LinearRgba {
pub const TRANSPARENT: Self = Self::with_components(0., 0., 0., 0.); pub const TRANSPARENT: Self = Self::with_components(0., 0., 0., 0.);
/// Returns true if this color is fully transparent
pub fn is_fully_transparent(self) -> bool {
self.3 == 0.0
}
/// Returns self, except when self is transparent, in which case returns other
pub fn when_fully_transparent(self, other: Self) -> Self {
if self.is_fully_transparent() {
other
} else {
self
}
}
/// Convert to an SRGB u32 pixel /// Convert to an SRGB u32 pixel
pub fn srgba_pixel(self) -> SrgbaPixel { pub fn srgba_pixel(self) -> SrgbaPixel {
SrgbaPixel::rgba( SrgbaPixel::rgba(
@ -501,6 +558,20 @@ mod tests {
assert_eq!(foo.to_rgb_string(), "#0015ff"); assert_eq!(foo.to_rgb_string(), "#0015ff");
} }
#[test]
fn from_rgba() {
assert_eq!(
SrgbaTuple::from_str("clear").unwrap().to_rgba_string(),
"rgba:0 0 0 0%"
);
assert_eq!(
SrgbaTuple::from_str("rgba:100% 0 0 50%")
.unwrap()
.to_rgba_string(),
"rgba:255 0 0 50%"
);
}
#[test] #[test]
fn from_rgb() { fn from_rgb() {
assert!(SrgbaTuple::from_str("").is_err()); assert!(SrgbaTuple::from_str("").is_err());
@ -524,12 +595,4 @@ mod tests {
let grey = SrgbaTuple::from_str("rgb:f0f0/f0f0/f0f0").unwrap(); let grey = SrgbaTuple::from_str("rgb:f0f0/f0f0/f0f0").unwrap();
assert_eq!(grey.to_rgb_string(), "#f0f0f0"); assert_eq!(grey.to_rgb_string(), "#f0f0f0");
} }
#[cfg(feature = "use_serde")]
#[test]
fn roundtrip_rgbcolor() {
let data = varbincode::serialize(&SrgbaTuple::from_named("DarkGreen").unwrap()).unwrap();
eprintln!("serialized as {:?}", data);
let _decoded: SrgbaTuple = varbincode::deserialize(data.as_slice()).unwrap();
}
} }

View File

@ -36,7 +36,7 @@ serde = {version="1.0", features = ["rc", "derive"]}
serde_json = "1.0" serde_json = "1.0"
smol = "1.2" smol = "1.2"
terminfo = "0.7" terminfo = "0.7"
termwiz = { path = "../termwiz" } termwiz = { path = "../termwiz", features=["use_serde"] }
toml = "0.5" toml = "0.5"
umask = { path = "../umask" } umask = { path = "../umask" }
unicode-segmentation = "1.8" unicode-segmentation = "1.8"

View File

@ -1,8 +1,9 @@
use crate::lua::{format_as_escapes, FormatItem}; use crate::lua::{format_as_escapes, FormatItem};
use crate::*; use crate::*;
use luahelper::impl_lua_conversion; use luahelper::impl_lua_conversion;
use std::str::FromStr;
use termwiz::cell::CellAttributes; use termwiz::cell::CellAttributes;
pub use termwiz::color::{ColorSpec, RgbColor}; pub use termwiz::color::{ColorSpec, RgbColor, SrgbaTuple};
#[derive(Debug, Copy, Deserialize, Serialize, Clone)] #[derive(Debug, Copy, Deserialize, Serialize, Clone)]
pub struct HsbTransform { pub struct HsbTransform {
@ -44,6 +45,35 @@ where
.collect()) .collect())
} }
#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy)]
#[serde(try_from = "String", into = "String")]
pub struct RgbaColor {
#[serde(flatten)]
color: SrgbaTuple,
}
impl Into<String> for RgbaColor {
fn into(self) -> String {
self.color.to_rgb_string()
}
}
impl Into<SrgbaTuple> for RgbaColor {
fn into(self) -> SrgbaTuple {
self.color
}
}
impl std::convert::TryFrom<String> for RgbaColor {
type Error = anyhow::Error;
fn try_from(s: String) -> anyhow::Result<RgbaColor> {
Ok(RgbaColor {
color: SrgbaTuple::from_str(&s)
.map_err(|_| anyhow::anyhow!("failed to parse {} as RgbaColor", &s))?,
})
}
}
#[derive(Default, Debug, Deserialize, Serialize, Clone)] #[derive(Default, Debug, Deserialize, Serialize, Clone)]
pub struct Palette { pub struct Palette {
/// The text color to use when the attributes are reset to default /// The text color to use when the attributes are reset to default
@ -55,8 +85,8 @@ pub struct Palette {
pub cursor_bg: Option<RgbColor>, pub cursor_bg: Option<RgbColor>,
pub cursor_border: Option<RgbColor>, pub cursor_border: Option<RgbColor>,
/// The color of selected text /// The color of selected text
pub selection_fg: Option<RgbColor>, pub selection_fg: Option<RgbaColor>,
pub selection_bg: Option<RgbColor>, pub selection_bg: Option<RgbaColor>,
/// A list of 8 colors corresponding to the basic ANSI palette /// A list of 8 colors corresponding to the basic ANSI palette
pub ansi: Option<[RgbColor; 8]>, pub ansi: Option<[RgbColor; 8]>,
/// A list of 8 colors corresponding to bright versions of the /// A list of 8 colors corresponding to bright versions of the
@ -87,7 +117,7 @@ impl From<Palette> for wezterm_term::color::ColorPalette {
macro_rules! apply_color { macro_rules! apply_color {
($name:ident) => { ($name:ident) => {
if let Some($name) = cfg.$name { if let Some($name) = cfg.$name {
p.$name = $name; p.$name = $name.into();
} }
}; };
} }

View File

@ -4,7 +4,7 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt; use std::fmt;
use std::result::Result; use std::result::Result;
pub use termwiz::color::{AnsiColor, ColorAttribute, RgbColor}; pub use termwiz::color::{AnsiColor, ColorAttribute, RgbColor, SrgbaTuple};
#[derive(Clone, PartialEq, Eq)] #[derive(Clone, PartialEq, Eq)]
pub struct Palette256(pub [RgbColor; 256]); pub struct Palette256(pub [RgbColor; 256]);
@ -43,7 +43,7 @@ impl std::iter::FromIterator<RgbColor> for Palette256 {
} }
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
pub struct ColorPalette { pub struct ColorPalette {
pub colors: Palette256, pub colors: Palette256,
@ -52,8 +52,8 @@ pub struct ColorPalette {
pub cursor_fg: RgbColor, pub cursor_fg: RgbColor,
pub cursor_bg: RgbColor, pub cursor_bg: RgbColor,
pub cursor_border: RgbColor, pub cursor_border: RgbColor,
pub selection_fg: RgbColor, pub selection_fg: SrgbaTuple,
pub selection_bg: RgbColor, pub selection_bg: SrgbaTuple,
pub scrollbar_thumb: RgbColor, pub scrollbar_thumb: RgbColor,
pub split: RgbColor, pub split: RgbColor,
} }
@ -114,8 +114,8 @@ impl ColorPalette {
cursor_fg: grey_out(self.cursor_fg), cursor_fg: grey_out(self.cursor_fg),
cursor_bg: grey_out(self.cursor_bg), cursor_bg: grey_out(self.cursor_bg),
cursor_border: grey_out(self.cursor_border), cursor_border: grey_out(self.cursor_border),
selection_fg: grey_out(self.selection_fg), selection_fg: grey_out(self.selection_fg.into()).into(),
selection_bg: grey_out(self.selection_bg), selection_bg: grey_out(self.selection_bg.into()).into(),
scrollbar_thumb: grey_out(self.scrollbar_thumb), scrollbar_thumb: grey_out(self.scrollbar_thumb),
split: grey_out(self.split), split: grey_out(self.split),
} }
@ -206,8 +206,8 @@ impl ColorPalette {
let cursor_border = RgbColor::new_8bpc(0x52, 0xad, 0x70); let cursor_border = RgbColor::new_8bpc(0x52, 0xad, 0x70);
let cursor_fg = colors[AnsiColor::Black as usize]; let cursor_fg = colors[AnsiColor::Black as usize];
let selection_fg = colors[AnsiColor::Black as usize]; let selection_fg = colors[AnsiColor::Black as usize].into();
let selection_bg = RgbColor::new_8bpc(0xff, 0xfa, 0xcd); let selection_bg = RgbColor::new_8bpc(0xff, 0xfa, 0xcd).into();
let scrollbar_thumb = RgbColor::new_8bpc(0x22, 0x22, 0x22); let scrollbar_thumb = RgbColor::new_8bpc(0x22, 0x22, 0x22);
let split = RgbColor::new_8bpc(0x44, 0x44, 0x44); let split = RgbColor::new_8bpc(0x44, 0x44, 0x44);

View File

@ -800,13 +800,13 @@ impl<'a> Performer<'a> {
ColorOrQuery::Query => { ColorOrQuery::Query => {
let response = OperatingSystemCommand::ChangeDynamicColors( let response = OperatingSystemCommand::ChangeDynamicColors(
which_color, which_color,
vec![ColorOrQuery::Color(self.palette().$name)], vec![ColorOrQuery::Color(self.palette().$name.into())],
); );
log::trace!("Color Query response {:?}", response); log::trace!("Color Query response {:?}", response);
write!(self.writer, "{}", response).ok(); write!(self.writer, "{}", response).ok();
self.writer.flush().ok(); self.writer.flush().ok();
} }
ColorOrQuery::Color(c) => self.palette_mut().$name = c, ColorOrQuery::Color(c) => self.palette_mut().$name = c.into(),
} }
}; };
} }

View File

@ -42,7 +42,7 @@ wezterm-color-types = { path = "../color-types" }
[features] [features]
widgets = ["cassowary", "fnv"] widgets = ["cassowary", "fnv"]
use_serde = ["serde"] use_serde = ["serde", "wezterm-color-types/use_serde"]
use_image = ["image"] use_image = ["image"]
docs = ["widgets", "use_serde"] docs = ["widgets", "use_serde"]

View File

@ -60,6 +60,12 @@ pub struct RgbColor {
bits: u32, bits: u32,
} }
impl Into<SrgbaTuple> for RgbColor {
fn into(self) -> SrgbaTuple {
self.to_tuple_rgba()
}
}
impl RgbColor { impl RgbColor {
/// Construct a color from discrete red, green, blue values /// Construct a color from discrete red, green, blue values
/// in the range 0-255. /// in the range 0-255.

View File

@ -1276,8 +1276,8 @@ impl super::TermWindow {
let selrange = self.selection(pos.pane.pane_id()).range.clone(); let selrange = self.selection(pos.pane.pane_id()).range.clone();
let start = Instant::now(); let start = Instant::now();
let selection_fg = rgbcolor_to_window_color(palette.selection_fg); let selection_fg = palette.selection_fg.to_linear();
let selection_bg = rgbcolor_to_window_color(palette.selection_bg); let selection_bg = palette.selection_bg.to_linear();
let cursor_fg = rgbcolor_to_window_color(palette.cursor_fg); let cursor_fg = rgbcolor_to_window_color(palette.cursor_fg);
let cursor_bg = rgbcolor_to_window_color(palette.cursor_bg); let cursor_bg = rgbcolor_to_window_color(palette.cursor_bg);
for (line_idx, line) in lines.iter().enumerate() { for (line_idx, line) in lines.iter().enumerate() {
@ -2495,9 +2495,11 @@ impl super::TermWindow {
visibility, visibility,
) { ) {
// Selected text overrides colors // Selected text overrides colors
(true, _, _, CursorVisibility::Hidden) => { (true, _, _, CursorVisibility::Hidden) => (
(params.selection_fg, params.selection_bg, params.cursor_bg) params.selection_fg.when_fully_transparent(params.fg_color),
} params.selection_bg,
params.cursor_bg,
),
// block Cursor cell overrides colors // block Cursor cell overrides colors
( (
_, _,