aboutsummaryrefslogtreecommitdiff
path: root/niri-config/src
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-07-16 10:22:03 +0300
committerGitHub <noreply@github.com>2024-07-16 07:22:03 +0000
commit3ace97660fde7fe1f0cc07a3925d1114af9a9c2f (patch)
tree9d736a1d403875737566ad8817bb347a9e8056fe /niri-config/src
parent0824737757d10cbeb844871c3f67756ca969cf7c (diff)
downloadniri-3ace97660fde7fe1f0cc07a3925d1114af9a9c2f.tar.gz
niri-3ace97660fde7fe1f0cc07a3925d1114af9a9c2f.tar.bz2
niri-3ace97660fde7fe1f0cc07a3925d1114af9a9c2f.zip
Implement gradient color interpolation option (#548)
* Added the better color averaging code (tested & functional) * rustfmt * Make Color f32 0..1, clarify premul/unpremul * Fix imports and test name * Premultiply gradient colors matching CSS * Fix indentation * fixup * Add gradient image --------- Co-authored-by: K's Thinkpad <K.T.Kraft@protonmail.com>
Diffstat (limited to 'niri-config/src')
-rw-r--r--niri-config/src/lib.rs258
1 files changed, 215 insertions, 43 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index e5998bfb..6396f6dc 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -410,8 +410,8 @@ impl Default for FocusRing {
Self {
off: false,
width: FloatOrInt(4.),
- active_color: Color::new(127, 200, 255, 255),
- inactive_color: Color::new(80, 80, 80, 255),
+ active_color: Color::from_rgba8_unpremul(127, 200, 255, 255),
+ inactive_color: Color::from_rgba8_unpremul(80, 80, 80, 255),
active_gradient: None,
inactive_gradient: None,
}
@@ -428,6 +428,8 @@ pub struct Gradient {
pub angle: i16,
#[knuffel(property, default)]
pub relative_to: GradientRelativeTo,
+ #[knuffel(property(name = "in"), str, default)]
+ pub in_: GradientInterpolation,
}
#[derive(knuffel::DecodeScalar, Debug, Default, Clone, Copy, PartialEq, Eq)]
@@ -437,6 +439,30 @@ pub enum GradientRelativeTo {
WorkspaceView,
}
+#[derive(Default, Debug, Clone, Copy, PartialEq)]
+pub struct GradientInterpolation {
+ pub color_space: GradientColorSpace,
+ pub hue_interpolation: HueInterpolation,
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub enum GradientColorSpace {
+ #[default]
+ Srgb,
+ SrgbLinear,
+ Oklab,
+ Oklch,
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub enum HueInterpolation {
+ #[default]
+ Shorter,
+ Longer,
+ Increasing,
+ Decreasing,
+}
+
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct Border {
#[knuffel(child)]
@@ -458,8 +484,8 @@ impl Default for Border {
Self {
off: true,
width: FloatOrInt(4.),
- active_color: Color::new(255, 200, 127, 255),
- inactive_color: Color::new(80, 80, 80, 255),
+ active_color: Color::from_rgba8_unpremul(255, 200, 127, 255),
+ inactive_color: Color::from_rgba8_unpremul(80, 80, 80, 255),
active_gradient: None,
inactive_gradient: None,
}
@@ -492,23 +518,49 @@ impl From<FocusRing> for Border {
}
}
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+/// RGB color in [0, 1] with unpremultiplied alpha.
+#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Color {
- pub r: u8,
- pub g: u8,
- pub b: u8,
- pub a: u8,
+ pub r: f32,
+ pub g: f32,
+ pub b: f32,
+ pub a: f32,
}
impl Color {
- pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
+ pub const fn new_unpremul(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
-}
-impl From<Color> for [f32; 4] {
- fn from(c: Color) -> Self {
- let [r, g, b, a] = [c.r, c.g, c.b, c.a].map(|x| x as f32 / 255.);
+ pub fn from_rgba8_unpremul(r: u8, g: u8, b: u8, a: u8) -> Self {
+ Self::from_array_unpremul([r, g, b, a].map(|x| x as f32 / 255.))
+ }
+
+ pub fn from_array_premul([r, g, b, a]: [f32; 4]) -> Self {
+ let a = a.clamp(0., 1.);
+
+ if a == 0. {
+ Self::new_unpremul(0., 0., 0., 0.)
+ } else {
+ Self {
+ r: (r / a).clamp(0., 1.),
+ g: (g / a).clamp(0., 1.),
+ b: (b / a).clamp(0., 1.),
+ a,
+ }
+ }
+ }
+
+ pub fn from_array_unpremul([r, g, b, a]: [f32; 4]) -> Self {
+ Self { r, g, b, a }
+ }
+
+ pub fn to_array_unpremul(self) -> [f32; 4] {
+ [self.r, self.g, self.b, self.a]
+ }
+
+ pub fn to_array_premul(self) -> [f32; 4] {
+ let [r, g, b, a] = [self.r, self.g, self.b, self.a];
[r * a, g * a, b * a, a]
}
}
@@ -1429,12 +1481,73 @@ impl CornerRadius {
}
}
+impl FromStr for GradientInterpolation {
+ type Err = miette::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut iter = s.split_whitespace();
+ let in_part1 = iter.next();
+ let in_part2 = iter.next();
+ let in_part3 = iter.next();
+
+ let Some(in_part1) = in_part1 else {
+ return Err(miette!("missing color space"));
+ };
+
+ let color = match in_part1 {
+ "srgb" => GradientColorSpace::Srgb,
+ "srgb-linear" => GradientColorSpace::SrgbLinear,
+ "oklab" => GradientColorSpace::Oklab,
+ "oklch" => GradientColorSpace::Oklch,
+ x => {
+ return Err(miette!(
+ "invalid color space {x}; can be srgb, srgb-linear, oklab or oklch"
+ ))
+ }
+ };
+
+ let interpolation = if let Some(in_part2) = in_part2 {
+ if color != GradientColorSpace::Oklch {
+ return Err(miette!("only oklch color space can have hue interpolation"));
+ }
+
+ if in_part3 != Some("hue") {
+ return Err(miette!(
+ "interpolation must end with \"hue\", like \"oklch shorter hue\""
+ ));
+ } else if iter.next().is_some() {
+ return Err(miette!("unexpected text after hue interpolation"));
+ } else {
+ match in_part2 {
+ "shorter" => HueInterpolation::Shorter,
+ "longer" => HueInterpolation::Longer,
+ "increasing" => HueInterpolation::Increasing,
+ "decreasing" => HueInterpolation::Decreasing,
+ x => {
+ return Err(miette!(
+ "invalid hue interpolation {x}; \
+ can be shorter, longer, increasing, decreasing"
+ ))
+ }
+ }
+ }
+ } else {
+ HueInterpolation::default()
+ };
+
+ Ok(Self {
+ color_space: color,
+ hue_interpolation: interpolation,
+ })
+ }
+}
+
impl FromStr for Color {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- let [r, g, b, a] = csscolorparser::parse(s).into_diagnostic()?.to_rgba8();
- Ok(Self { r, g, b, a })
+ let color = csscolorparser::parse(s).into_diagnostic()?.to_array();
+ Ok(Self::from_array_unpremul(color.map(|x| x as f32)))
}
}
@@ -1453,7 +1566,7 @@ struct ColorRgba {
impl From<ColorRgba> for Color {
fn from(value: ColorRgba) -> Self {
let ColorRgba { r, g, b, a } = value;
- Self { r, g, b, a }
+ Self::from_array_unpremul([r, g, b, a].map(|x| x as f32 / 255.))
}
}
@@ -2771,41 +2884,25 @@ mod tests {
focus_ring: FocusRing {
off: false,
width: FloatOrInt(5.),
- active_color: Color {
- r: 0,
- g: 100,
- b: 200,
- a: 255,
- },
- inactive_color: Color {
- r: 255,
- g: 200,
- b: 100,
- a: 0,
- },
+ active_color: Color::from_rgba8_unpremul(0, 100, 200, 255),
+ inactive_color: Color::from_rgba8_unpremul(255, 200, 100, 0),
active_gradient: Some(Gradient {
- from: Color::new(10, 20, 30, 255),
- to: Color::new(0, 128, 255, 255),
+ from: Color::from_rgba8_unpremul(10, 20, 30, 255),
+ to: Color::from_rgba8_unpremul(0, 128, 255, 255),
angle: 180,
relative_to: GradientRelativeTo::WorkspaceView,
+ in_: GradientInterpolation {
+ color_space: GradientColorSpace::Srgb,
+ hue_interpolation: HueInterpolation::Shorter,
+ },
}),
inactive_gradient: None,
},
border: Border {
off: false,
width: FloatOrInt(3.),
- active_color: Color {
- r: 255,
- g: 200,
- b: 127,
- a: 255,
- },
- inactive_color: Color {
- r: 255,
- g: 200,
- b: 100,
- a: 0,
- },
+ active_color: Color::from_rgba8_unpremul(255, 200, 127, 255),
+ inactive_color: Color::from_rgba8_unpremul(255, 200, 100, 0),
active_gradient: None,
inactive_gradient: None,
},
@@ -3096,6 +3193,81 @@ mod tests {
}
#[test]
+ fn parse_gradient_interpolation() {
+ assert_eq!(
+ "srgb".parse::<GradientInterpolation>().unwrap(),
+ GradientInterpolation {
+ color_space: GradientColorSpace::Srgb,
+ ..Default::default()
+ }
+ );
+ assert_eq!(
+ "srgb-linear".parse::<GradientInterpolation>().unwrap(),
+ GradientInterpolation {
+ color_space: GradientColorSpace::SrgbLinear,
+ ..Default::default()
+ }
+ );
+ assert_eq!(
+ "oklab".parse::<GradientInterpolation>().unwrap(),
+ GradientInterpolation {
+ color_space: GradientColorSpace::Oklab,
+ ..Default::default()
+ }
+ );
+ assert_eq!(
+ "oklch".parse::<GradientInterpolation>().unwrap(),
+ GradientInterpolation {
+ color_space: GradientColorSpace::Oklch,
+ ..Default::default()
+ }
+ );
+ assert_eq!(
+ "oklch shorter hue"
+ .parse::<GradientInterpolation>()
+ .unwrap(),
+ GradientInterpolation {
+ color_space: GradientColorSpace::Oklch,
+ hue_interpolation: HueInterpolation::Shorter,
+ }
+ );
+ assert_eq!(
+ "oklch longer hue".parse::<GradientInterpolation>().unwrap(),
+ GradientInterpolation {
+ color_space: GradientColorSpace::Oklch,
+ hue_interpolation: HueInterpolation::Longer,
+ }
+ );
+ assert_eq!(
+ "oklch decreasing hue"
+ .parse::<GradientInterpolation>()
+ .unwrap(),
+ GradientInterpolation {
+ color_space: GradientColorSpace::Oklch,
+ hue_interpolation: HueInterpolation::Decreasing,
+ }
+ );
+ assert_eq!(
+ "oklch increasing hue"
+ .parse::<GradientInterpolation>()
+ .unwrap(),
+ GradientInterpolation {
+ color_space: GradientColorSpace::Oklch,
+ hue_interpolation: HueInterpolation::Increasing,
+ }
+ );
+
+ assert!("".parse::<GradientInterpolation>().is_err());
+ assert!("srgb shorter hue".parse::<GradientInterpolation>().is_err());
+ assert!("oklch shorter".parse::<GradientInterpolation>().is_err());
+ assert!("oklch shorter h".parse::<GradientInterpolation>().is_err());
+ assert!("oklch a hue".parse::<GradientInterpolation>().is_err());
+ assert!("oklch shorter hue a"
+ .parse::<GradientInterpolation>()
+ .is_err());
+ }
+
+ #[test]
fn parse_iso_level3_shift() {
assert_eq!(
"ISO_Level3_Shift+A".parse::<Key>().unwrap(),