diff --git a/.gitignore b/.gitignore index 694a9c496..29ad49c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ /docs/config/lua/mux-events/index.md /docs/config/lua/mux-window/index.md /docs/config/lua/pane/index.md +/docs/config/lua/wezterm.color/index.md /docs/config/lua/wezterm.gui/index.md /docs/config/lua/wezterm.mux/index.md /docs/config/lua/wezterm/index.md diff --git a/Cargo.lock b/Cargo.lock index f88264f25..926e4751f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -607,6 +607,16 @@ dependencies = [ "zstd", ] +[[package]] +name = "color-funcs" +version = "0.1.0" +dependencies = [ + "anyhow", + "config", + "csscolorparser", + "luahelper", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -851,6 +861,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841475b11553e394b89eebb5cdfe8ca0b960005ad464bca369ade02a62da053e" dependencies = [ + "lab", "phf 0.10.1", ] @@ -1092,6 +1103,7 @@ dependencies = [ "battery", "chrono", "cocoa", + "color-funcs", "config", "dirs-next", "env_logger", @@ -1898,6 +1910,12 @@ dependencies = [ "log", ] +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + [[package]] name = "lazy_static" version = "1.4.0" diff --git a/ci/generate-docs.py b/ci/generate-docs.py index 0c6750111..aa38b1f33 100644 --- a/ci/generate-docs.py +++ b/ci/generate-docs.py @@ -316,6 +316,10 @@ TOC = [ "module: wezterm", "config/lua/wezterm", ), + Gen( + "module: wezterm.color", + "config/lua/wezterm.color", + ), Gen( "module: wezterm.gui", "config/lua/wezterm.gui", diff --git a/color-types/src/lib.rs b/color-types/src/lib.rs index cfafe63f8..42f62f062 100644 --- a/color-types/src/lib.rs +++ b/color-types/src/lib.rs @@ -333,6 +333,129 @@ impl SrgbaTuple { (self.2 * 65535.) as u16 ) } + + pub fn to_hsla(self) -> (f64, f64, f64, f64) { + csscolorparser::Color::from_rgba(self.0.into(), self.1.into(), self.2.into(), self.3.into()) + .to_hsla() + } + + pub fn from_hsla(h: f64, s: f64, l: f64, a: f64) -> Self { + let (r, g, b, a) = csscolorparser::Color::from_hsla(h, s, l, a).rgba(); + Self(r as f32, g as f32, b as f32, a as f32) + } + + /// Scale the color towards the maximum saturation by factor, a value ranging from 0.0 to 1.0. + pub fn saturate(&self, factor: f64) -> Self { + let (h, s, l, a) = self.to_hsla(); + let s = apply_scale(s, factor); + Self::from_hsla(h, s, l, a) + } + + /// Increase the saturation by amount, a value ranging from 0.0 to 1.0. + pub fn saturate_fixed(&self, amount: f64) -> Self { + let (h, s, l, a) = self.to_hsla(); + let s = apply_fixed(s, amount); + Self::from_hsla(h, s, l, a) + } + + /// Scale the color towards the maximum lightness by factor, a value ranging from 0.0 to 1.0 + pub fn lighten(&self, factor: f64) -> Self { + let (h, s, l, a) = self.to_hsla(); + let l = apply_scale(l, factor); + Self::from_hsla(h, s, l, a) + } + + /// Lighten the color by amount, a value ranging from 0.0 to 1.0 + pub fn lighten_fixed(&self, amount: f64) -> Self { + let (h, s, l, a) = self.to_hsla(); + let l = apply_fixed(l, amount); + Self::from_hsla(h, s, l, a) + } + + /// Find the complementary color: the one opposite it on the color wheel + pub fn adjust_hue_fixed(&self, amount: f64) -> Self { + let (h, s, l, a) = self.to_hsla(); + let h = normalize_angle(h + amount); + Self::from_hsla(h, s, l, a) + } + + pub fn complement(&self) -> Self { + self.adjust_hue_fixed(180.) + } + + pub fn triad(&self) -> (Self, Self) { + (self.adjust_hue_fixed(120.), self.adjust_hue_fixed(-120.)) + } + + pub fn square(&self) -> (Self, Self, Self) { + ( + self.adjust_hue_fixed(90.), + self.adjust_hue_fixed(270.), + self.adjust_hue_fixed(180.), + ) + } +} + +/// From "Paint Inspired Color Compositing" by Gosset and Chen +/// has a python +/// implementation +/// has a copy of the paper +/// itself at +pub fn ryb_to_rgb(r: f32, y: f32, b: f32) -> (f32, f32, f32) { + fn cubic(t: f32, a: f32, b: f32) -> f32 { + let weight = t * t * (3. - 2. * t); + a + weight * (b - a) + } + + let red = { + let x0 = cubic(b, 1.0, 0.163); + let x1 = cubic(b, 1.0, 0.0); + let x2 = cubic(b, 1.0, 0.5); + let x3 = cubic(b, 1.0, 0.2); + let y0 = cubic(y, x0, x1); + let y1 = cubic(y, x2, x3); + cubic(r, y0, y1) + }; + + let green = { + let x0 = cubic(b, 1.0, 0.373); + let x1 = cubic(b, 1.0, 0.66); + let x2 = cubic(b, 0.0, 0.0); + let x3 = cubic(b, 0.5, 0.094); + let y0 = cubic(y, x0, x1); + let y1 = cubic(y, x2, x3); + cubic(r, y0, y1) + }; + + let blue = { + let x0 = cubic(b, 1.0, 0.6); + let x1 = cubic(b, 0.0, 0.2); + let x2 = cubic(b, 0.0, 0.5); + let x3 = cubic(b, 0.0, 0.0); + let y0 = cubic(y, x0, x1); + let y1 = cubic(y, x2, x3); + cubic(r, y0, y1) + }; + + (red, green, blue) +} + +fn normalize_angle(t: f64) -> f64 { + let mut t = t % 360.0; + if t < 0.0 { + t += 360.0; + } + t +} + +fn apply_scale(current: f64, factor: f64) -> f64 { + let difference = if factor >= 0. { 1.0 - current } else { current }; + let delta = difference.max(0.) * factor; + (current + delta).max(0.) +} + +fn apply_fixed(current: f64, amount: f64) -> f64 { + (current + amount).max(0.) } impl Hash for SrgbaTuple { diff --git a/docs/changelog.md b/docs/changelog.md index 210c19c25..f62e2197f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -18,6 +18,7 @@ As features stabilize some brief notes about them will accumulate here. * [window:set_position](config/lua/window/set_position.md) method for controlling window position. * [window:maximize](config/lua/window/maximize.md) and [window:restore](config/lua/window/restore.md) methods for controlling window maximization state. * [window:get_selection_escapes_for_pane](config/lua/window/get_selection_escapes_for_pane.md) method for getting the current selection including escape sequences. [#2223](https://github.com/wez/wezterm/issues/2223) +* New [wezterm.color](config/lua/wezterm.color/index.md) module for working with colors. * New [wezterm.gui](config/lua/wezterm.gui/index.md) module and [mux_window:gui_window](config/lua/mux-window/gui_window.md) method. * New [wezterm.gui.screens()](config/lua/wezterm.gui/screens.md) function for getting information about the available screens/monitors/displays * You may now use [wezterm.format](config/lua/wezterm/format.md) (or otherwise use strings with escape sequences) in the labels of the [Launcher Menu](config/launch.md#the-launcher-menu). diff --git a/docs/config/lua/wezterm.color/index.markdown b/docs/config/lua/wezterm.color/index.markdown new file mode 100644 index 000000000..089555642 --- /dev/null +++ b/docs/config/lua/wezterm.color/index.markdown @@ -0,0 +1,6 @@ +*Since: nightly builds only* + +The `wezterm.color` module exposes functions that work with colors. + +## Available functions, constants + diff --git a/docs/config/lua/wezterm.color/parse.md b/docs/config/lua/wezterm.color/parse.md new file mode 100644 index 000000000..45d4b424d --- /dev/null +++ b/docs/config/lua/wezterm.color/parse.md @@ -0,0 +1,117 @@ +# wezterm.color.parse(string) + +*Since: nightly builds only* + +Parses the passed color and returns an `RgbaColor` object. +`RgbaColor` objects evaluate as strings but have a number of methods +that allow transforming colors. + +``` +> wezterm.color.parse("black") +#000000 +``` + +This example picks a foreground color, computes its complement +and darkens it to use it as a background color: + +```lua +local wezterm = require 'wezterm' + +local fg = wezterm.color.parse("yellow") +local bg = fg:complement():darken(0.2) + +return { + colors = { + foreground = fg, + background = bg, + } +} +``` + +## `color:complement()` + +*Since: nightly builds only* + +Returns the complement of the color. The complement is computed +by converting to HSL, rotating by 180 degrees and converting back +to RGBA. + +## `color:triad()` + +*Since: nightly builds only* + +Returns the other two colors that form a triad. The other colors +are at +/- 120 degrees in the HSL color wheel. + +```lua +local a, b = wezterm:color.parse("yellow"):triad() +``` + +## `color:square()` + +*Since: nightly builds only* + +Returns the other three colors that form a square. The other colors +are 90 degrees apart on the HSL color wheel. + +```lua +local a, b, = wezterm:color.parse("yellow"):square() +``` + +## `color:saturate(factor)` + +*Since: nightly builds only* + +Scales the color towards the maximum saturation by the provided factor, which +should be in the range `0.0` through `1.0`. + +## `color:saturate_fixed(amount)` + +*Since: nightly builds only* + +Increase the saturation by amount, a value ranging from `0.0` to `1.0`. + +## `color:desaturate(factor)` + +*Since: nightly builds only* + +Scales the color towards the minimum saturation by the provided factor, which +should be in the range `0.0` through `1.0`. + +## `color:desaturate_fixed(amount)` + +*Since: nightly builds only* + +Decrease the saturation by amount, a value ranging from `0.0` to `1.0`. + +## `color:lighten(factor)` + +*Since: nightly builds only* + +Scales the color towards the maximum lightness by the provided factor, which +should be in the range `0.0` through `1.0`. + +## `color:lighten_fixed(amount)` + +*Since: nightly builds only* + +Increase the lightness by amount, a value ranging from `0.0` to `1.0`. + +## `color:darken(factor)` + +*Since: nightly builds only* + +Scales the color towards the minimum lightness by the provided factor, which +should be in the range `0.0` through `1.0`. + +## `color:darken_fixed(amount)` + +*Since: nightly builds only* + +Decrease the lightness by amount, a value ranging from `0.0` to `1.0`. + +## `color:adjust_hue_fixed(degrees)` + +*Since: nightly builds only* + +Adjust the hue angle by the specified number of degrees. diff --git a/env-bootstrap/Cargo.toml b/env-bootstrap/Cargo.toml index ae5c17a62..3a125de1c 100644 --- a/env-bootstrap/Cargo.toml +++ b/env-bootstrap/Cargo.toml @@ -17,6 +17,7 @@ log = "0.4" env_logger = "0.9" termwiz = { path = "../termwiz" } battery = { path = "../lua-api-crates/battery" } +color-funcs = { path = "../lua-api-crates/color-funcs" } termwiz-funcs = { path = "../lua-api-crates/termwiz-funcs" } logging = { path = "../lua-api-crates/logging" } mux-lua = { path = "../lua-api-crates/mux" } diff --git a/env-bootstrap/src/lib.rs b/env-bootstrap/src/lib.rs index 0805ec53b..d32bcb2f0 100644 --- a/env-bootstrap/src/lib.rs +++ b/env-bootstrap/src/lib.rs @@ -161,6 +161,7 @@ fn register_panic_hook() { fn register_lua_modules() { for func in [ battery::register, + color_funcs::register, termwiz_funcs::register, logging::register, mux_lua::register, diff --git a/lua-api-crates/color-funcs/Cargo.toml b/lua-api-crates/color-funcs/Cargo.toml new file mode 100644 index 000000000..06a02307e --- /dev/null +++ b/lua-api-crates/color-funcs/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "color-funcs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +config = { path = "../../config" } +csscolorparser = {version="0.6", features=["lab"]} +luahelper = { path = "../../luahelper" } diff --git a/lua-api-crates/color-funcs/src/lib.rs b/lua-api-crates/color-funcs/src/lib.rs new file mode 100644 index 000000000..8bf2a1b97 --- /dev/null +++ b/lua-api-crates/color-funcs/src/lib.rs @@ -0,0 +1,89 @@ +use config::lua::get_or_create_sub_module; +use config::lua::mlua::{self, Lua, MetaMethod, UserData, UserDataMethods}; +use config::RgbaColor; + +#[derive(Clone)] +struct ColorWrap(RgbaColor); + +impl ColorWrap { + pub fn complement(&self) -> Self { + Self(self.0.complement().into()) + } + pub fn triad(&self) -> (Self, Self) { + let (a, b) = self.0.triad(); + (Self(a.into()), Self(b.into())) + } + pub fn square(&self) -> (Self, Self, Self) { + let (a, b, c) = self.0.square(); + (Self(a.into()), Self(b.into()), Self(c.into())) + } + pub fn saturate(&self, factor: f64) -> Self { + Self(self.0.saturate(factor).into()) + } + pub fn saturate_fixed(&self, amount: f64) -> Self { + Self(self.0.saturate_fixed(amount).into()) + } + pub fn lighten(&self, factor: f64) -> Self { + Self(self.0.lighten(factor).into()) + } + pub fn lighten_fixed(&self, amount: f64) -> Self { + Self(self.0.lighten_fixed(amount).into()) + } + pub fn adjust_hue_fixed(&self, amount: f64) -> Self { + Self(self.0.adjust_hue_fixed(amount).into()) + } +} + +impl UserData for ColorWrap { + fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(MetaMethod::ToString, |_, this, _: ()| { + let s: String = this.0.into(); + Ok(s) + }); + methods.add_meta_method(MetaMethod::Eq, |_, this, other: ColorWrap| { + Ok(this.0 == other.0) + }); + methods.add_method("complement", |_, this, _: ()| Ok(this.complement())); + methods.add_method("triad", |_, this, _: ()| Ok(this.triad())); + methods.add_method("square", |_, this, _: ()| Ok(this.square())); + methods.add_method("saturate", |_, this, factor: f64| Ok(this.saturate(factor))); + + methods.add_method("desaturate", |_, this, factor: f64| { + Ok(this.saturate(-factor)) + }); + + methods.add_method("saturate_fixed", |_, this, amount: f64| { + Ok(this.saturate_fixed(amount)) + }); + methods.add_method("desaturate_fixed", |_, this, amount: f64| { + Ok(this.saturate_fixed(-amount)) + }); + + methods.add_method("lighten", |_, this, factor: f64| Ok(this.lighten(factor))); + + methods.add_method("darken", |_, this, factor: f64| Ok(this.lighten(-factor))); + + methods.add_method("lighten_fixed", |_, this, amount: f64| { + Ok(this.lighten_fixed(amount)) + }); + methods.add_method("darken_fixed", |_, this, amount: f64| { + Ok(this.lighten_fixed(-amount)) + }); + + methods.add_method("adjust_hue_fixed", |_, this, amount: f64| { + Ok(this.adjust_hue_fixed(amount)) + }); + } +} + +pub fn register(lua: &Lua) -> anyhow::Result<()> { + let color = get_or_create_sub_module(lua, "color")?; + color.set("parse", lua.create_function(parse_color)?)?; + Ok(()) +} + +fn parse_color<'lua>(_: &'lua Lua, spec: String) -> mlua::Result { + let color = + RgbaColor::try_from(spec).map_err(|err| mlua::Error::external(format!("{err:#}")))?; + Ok(ColorWrap(color)) +}