aboutsummaryrefslogtreecommitdiff
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
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>
-rw-r--r--niri-config/src/lib.rs258
-rw-r--r--niri-visual-tests/src/cases/gradient_angle.rs7
-rw-r--r--niri-visual-tests/src/cases/gradient_area.rs9
-rw-r--r--niri-visual-tests/src/cases/gradient_oklab.rs53
-rw-r--r--niri-visual-tests/src/cases/gradient_oklab_alpha.rs51
-rw-r--r--niri-visual-tests/src/cases/gradient_oklch_alpha.rs53
-rw-r--r--niri-visual-tests/src/cases/gradient_oklch_decreasing.rs53
-rw-r--r--niri-visual-tests/src/cases/gradient_oklch_increasing.rs53
-rw-r--r--niri-visual-tests/src/cases/gradient_oklch_longer.rs53
-rw-r--r--niri-visual-tests/src/cases/gradient_oklch_shorter.rs53
-rw-r--r--niri-visual-tests/src/cases/gradient_srgb.rs53
-rw-r--r--niri-visual-tests/src/cases/gradient_srgb_alpha.rs51
-rw-r--r--niri-visual-tests/src/cases/gradient_srgblinear.rs53
-rw-r--r--niri-visual-tests/src/cases/gradient_srgblinear_alpha.rs51
-rw-r--r--niri-visual-tests/src/cases/layout.rs4
-rw-r--r--niri-visual-tests/src/cases/mod.rs11
-rw-r--r--niri-visual-tests/src/cases/tile.rs2
-rw-r--r--niri-visual-tests/src/main.rs26
-rw-r--r--resources/default-config.kdl1
-rw-r--r--src/layout/focus_ring.rs15
-rw-r--r--src/layout/tile.rs7
-rw-r--r--src/render_helpers/border.rs43
-rw-r--r--src/render_helpers/shaders/border.frag174
-rw-r--r--src/render_helpers/shaders/mod.rs2
-rw-r--r--src/window/mapped.rs7
-rw-r--r--wiki/Configuration:-Layout.md25
-rw-r--r--wiki/img/gradients-oklch.png3
27 files changed, 1092 insertions, 79 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(),
diff --git a/niri-visual-tests/src/cases/gradient_angle.rs b/niri-visual-tests/src/cases/gradient_angle.rs
index 7d0b7542..fa70dc73 100644
--- a/niri-visual-tests/src/cases/gradient_angle.rs
+++ b/niri-visual-tests/src/cases/gradient_angle.rs
@@ -4,7 +4,7 @@ use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::render_helpers::border::BorderRenderElement;
-use niri_config::CornerRadius;
+use niri_config::{Color, CornerRadius, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
@@ -64,8 +64,9 @@ impl TestCase for GradientAngle {
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
- [1., 0., 0., 1.],
- [0., 1., 0., 1.],
+ GradientInterpolation::default(),
+ Color::new_unpremul(1., 0., 0., 1.),
+ Color::new_unpremul(0., 1., 0., 1.),
self.angle - FRAC_PI_2,
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
diff --git a/niri-visual-tests/src/cases/gradient_area.rs b/niri-visual-tests/src/cases/gradient_area.rs
index 13f50d4c..5463cfc4 100644
--- a/niri-visual-tests/src/cases/gradient_area.rs
+++ b/niri-visual-tests/src/cases/gradient_area.rs
@@ -5,7 +5,7 @@ use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::layout::focus_ring::FocusRing;
use niri::render_helpers::border::BorderRenderElement;
-use niri_config::{Color, CornerRadius, FloatOrInt};
+use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Rectangle, Size};
@@ -23,7 +23,7 @@ impl GradientArea {
let border = FocusRing::new(niri_config::FocusRing {
off: false,
width: FloatOrInt(1.),
- active_color: Color::new(255, 255, 255, 128),
+ active_color: Color::from_rgba8_unpremul(255, 255, 255, 128),
inactive_color: Color::default(),
active_gradient: None,
inactive_gradient: None,
@@ -104,8 +104,9 @@ impl TestCase for GradientArea {
[BorderRenderElement::new(
area.size,
g_area,
- [1., 0., 0., 1.],
- [0., 1., 0., 1.],
+ GradientInterpolation::default(),
+ Color::new_unpremul(1., 0., 0., 1.),
+ Color::new_unpremul(0., 1., 0., 1.),
FRAC_PI_4,
Rectangle::from_loc_and_size((0, 0), rect_size).to_f64(),
0.,
diff --git a/niri-visual-tests/src/cases/gradient_oklab.rs b/niri-visual-tests/src/cases/gradient_oklab.rs
new file mode 100644
index 00000000..abebb213
--- /dev/null
+++ b/niri-visual-tests/src/cases/gradient_oklab.rs
@@ -0,0 +1,53 @@
+use niri::render_helpers::border::BorderRenderElement;
+use niri_config::{
+ Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
+};
+use smithay::backend::renderer::element::RenderElement;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::utils::{Logical, Physical, Rectangle, Size};
+
+use super::TestCase;
+
+pub struct GradientOklab {
+ gradient_format: GradientInterpolation,
+}
+
+impl GradientOklab {
+ pub fn new(_size: Size<i32, Logical>) -> Self {
+ Self {
+ gradient_format: GradientInterpolation {
+ color_space: GradientColorSpace::Oklab,
+ hue_interpolation: HueInterpolation::Shorter,
+ },
+ }
+ }
+}
+
+impl TestCase for GradientOklab {
+ fn render(
+ &mut self,
+ _renderer: &mut GlesRenderer,
+ size: Size<i32, Physical>,
+ ) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
+ let (a, b) = (size.w / 6, size.h / 3);
+ let size = (size.w - a * 2, size.h - b * 2);
+ let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
+
+ [BorderRenderElement::new(
+ area.size,
+ Rectangle::from_loc_and_size((0., 0.), area.size),
+ self.gradient_format,
+ Color::new_unpremul(1., 0., 0., 1.),
+ Color::new_unpremul(0., 1., 0., 1.),
+ 0.,
+ Rectangle::from_loc_and_size((0., 0.), area.size),
+ 0.,
+ CornerRadius::default(),
+ 1.,
+ )
+ .with_location(area.loc)]
+ .into_iter()
+ .map(|elem| Box::new(elem) as _)
+ .collect()
+ }
+}
diff --git a/niri-visual-tests/src/cases/gradient_oklab_alpha.rs b/niri-visual-tests/src/cases/gradient_oklab_alpha.rs
new file mode 100644
index 00000000..31ae59ef
--- /dev/null
+++ b/niri-visual-tests/src/cases/gradient_oklab_alpha.rs
@@ -0,0 +1,51 @@
+use niri::render_helpers::border::BorderRenderElement;
+use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
+use smithay::backend::renderer::element::RenderElement;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::utils::{Logical, Physical, Rectangle, Size};
+
+use super::TestCase;
+
+pub struct GradientOklabAlpha {
+ gradient_format: GradientInterpolation,
+}
+
+impl GradientOklabAlpha {
+ pub fn new(_size: Size<i32, Logical>) -> Self {
+ Self {
+ gradient_format: GradientInterpolation {
+ color_space: GradientColorSpace::Oklab,
+ hue_interpolation: Default::default(),
+ },
+ }
+ }
+}
+
+impl TestCase for GradientOklabAlpha {
+ fn render(
+ &mut self,
+ _renderer: &mut GlesRenderer,
+ size: Size<i32, Physical>,
+ ) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
+ let (a, b) = (size.w / 6, size.h / 3);
+ let size = (size.w - a * 2, size.h - b * 2);
+ let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
+
+ [BorderRenderElement::new(
+ area.size,
+ Rectangle::from_loc_and_size((0., 0.), area.size),
+ self.gradient_format,
+ Color::new_unpremul(1., 0., 0., 1.),
+ Color::new_unpremul(0., 1., 0., 0.),
+ 0.,
+ Rectangle::from_loc_and_size((0., 0.), area.size),
+ 0.,
+ CornerRadius::default(),
+ 1.,
+ )
+ .with_location(area.loc)]
+ .into_iter()
+ .map(|elem| Box::new(elem) as _)
+ .collect()
+ }
+}
diff --git a/niri-visual-tests/src/cases/gradient_oklch_alpha.rs b/niri-visual-tests/src/cases/gradient_oklch_alpha.rs
new file mode 100644
index 00000000..022f59ec
--- /dev/null
+++ b/niri-visual-tests/src/cases/gradient_oklch_alpha.rs
@@ -0,0 +1,53 @@
+use niri::render_helpers::border::BorderRenderElement;
+use niri_config::{
+ Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
+};
+use smithay::backend::renderer::element::RenderElement;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::utils::{Logical, Physical, Rectangle, Size};
+
+use super::TestCase;
+
+pub struct GradientOklchAlpha {
+ gradient_format: GradientInterpolation,
+}
+
+impl GradientOklchAlpha {
+ pub fn new(_size: Size<i32, Logical>) -> Self {
+ Self {
+ gradient_format: GradientInterpolation {
+ color_space: GradientColorSpace::Oklch,
+ hue_interpolation: HueInterpolation::Longer,
+ },
+ }
+ }
+}
+
+impl TestCase for GradientOklchAlpha {
+ fn render(
+ &mut self,
+ _renderer: &mut GlesRenderer,
+ size: Size<i32, Physical>,
+ ) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
+ let (a, b) = (size.w / 6, size.h / 3);
+ let size = (size.w - a * 2, size.h - b * 2);
+ let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
+
+ [BorderRenderElement::new(
+ area.size,
+ Rectangle::from_loc_and_size((0., 0.), area.size),
+ self.gradient_format,
+ Color::new_unpremul(1., 0., 0., 1.),
+ Color::new_unpremul(0., 1., 0., 0.),
+ 0.,
+ Rectangle::from_loc_and_size((0., 0.), area.size),
+ 0.,
+ CornerRadius::default(),
+ 1.,
+ )
+ .with_location(area.loc)]
+ .into_iter()
+ .map(|elem| Box::new(elem) as _)
+ .collect()
+ }
+}
diff --git a/niri-visual-tests/src/cases/gradient_oklch_decreasing.rs b/niri-visual-tests/src/cases/gradient_oklch_decreasing.rs
new file mode 100644
index 00000000..7039f2c8
--- /dev/null
+++ b/niri-visual-tests/src/cases/gradient_oklch_decreasing.rs
@@ -0,0 +1,53 @@
+use niri::render_helpers::border::BorderRenderElement;
+use niri_config::{
+ Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
+};
+use smithay::backend::renderer::element::RenderElement;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::utils::{Logical, Physical, Rectangle, Size};
+
+use super::TestCase;
+
+pub struct GradientOklchDecreasing {
+ gradient_format: GradientInterpolation,
+}
+
+impl GradientOklchDecreasing {
+ pub fn new(_size: Size<i32, Logical>) -> Self {
+ Self {
+ gradient_format: GradientInterpolation {
+ color_space: GradientColorSpace::Oklch,
+ hue_interpolation: HueInterpolation::Decreasing,
+ },
+ }
+ }
+}
+
+impl TestCase for GradientOklchDecreasing {
+ fn render(
+ &mut self,
+ _renderer: &mut GlesRenderer,
+ size: Size<i32, Physical>,
+ ) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
+ let (a, b) = (size.w / 6, size.h / 3);
+ let size = (size.w - a * 2, size.h - b * 2);
+ let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
+
+ [BorderRenderElement::new(
+ area.size,
+ Rectangle::from_loc_and_size((0., 0.), area.size),
+ self.gradient_format,
+ Color::new_unpremul(1., 0., 0., 1.),
+ Color::new_unpremul(0., 1., 0., 1.),
+ 0.,
+ Rectangle::from_loc_and_size((0., 0.), area.size),
+ 0.,
+ CornerRadius::default(),
+ 1.,
+ )
+ .with_location(area.loc)]
+ .into_iter()
+ .map(|elem| Box::new(elem) as _)
+ .collect()
+ }
+}
diff --git a/niri-visual-tests/src/cases/gradient_oklch_increasing.rs b/niri-visual-tests/src/cases/gradient_oklch_increasing.rs
new file mode 100644
index 00000000..2a020923
--- /dev/null
+++ b/niri-visual-tests/src/cases/gradient_oklch_increasing.rs
@@ -0,0 +1,53 @@
+use niri::render_helpers::border::BorderRenderElement;
+use niri_config::{
+ Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
+};
+use smithay::backend::renderer::element::RenderElement;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::utils::{Logical, Physical, Rectangle, Size};
+
+use super::TestCase;
+
+pub struct GradientOklchIncreasing {
+ gradient_format: GradientInterpolation,
+}
+
+impl GradientOklchIncreasing {
+ pub fn new(_size: Size<i32, Logical>) -> Self {
+ Self {
+ gradient_format: GradientInterpolation {
+ color_space: GradientColorSpace::Oklch,
+ hue_interpolation: HueInterpolation::Increasing,
+ },
+ }
+ }
+}
+
+impl TestCase for GradientOklchIncreasing {
+ fn render(
+ &mut self,
+ _renderer: &mut GlesRenderer,
+ size: Size<i32, Physical>,
+ ) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
+ let (a, b) = (size.w / 6, size.h / 3);
+ let size = (size.w - a * 2, size.h - b * 2);
+ let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
+
+ [BorderRenderElement::new(
+ area.size,
+ Rectangle::from_loc_and_size((0., 0.), area.size),
+ self.gradient_format,
+ Color::new_unpremul(1., 0., 0., 1.),
+ Color::new_unpremul(0., 1., 0., 1.),
+ 0.,
+ Rectangle::from_loc_and_size((0., 0.), area.size),
+ 0.,
+ CornerRadius::default(),
+ 1.,
+ )
+ .with_location(area.loc)]
+ .into_iter()
+ .map(|elem| Box::new(elem) as _)
+ .collect()
+ }
+}
diff --git a/niri-visual-tests/src/cases/gradient_oklch_longer.rs b/niri-visual-tests/src/cases/gradient_oklch_longer.rs
new file mode 100644
index 00000000..d63259fd
--- /dev/null
+++ b/niri-visual-tests/src/cases/gradient_oklch_longer.rs
@@ -0,0 +1,53 @@
+use niri::render_helpers::border::BorderRenderElement;
+use niri_config::{
+ Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
+};
+use smithay::backend::renderer::element::RenderElement;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::utils::{Logical, Physical, Rectangle, Size};
+
+use super::TestCase;
+
+pub struct GradientOklchLonger {
+ gradient_format: GradientInterpolation,
+}
+
+impl GradientOklchLonger {
+ pub fn new(_size: Size<i32, Logical>) -> Self {
+ Self {
+ gradient_format: GradientInterpolation {
+ color_space: GradientColorSpace::Oklch,
+ hue_interpolation: HueInterpolation::Longer,
+ },
+ }
+ }
+}
+
+impl TestCase for GradientOklchLonger {
+ fn render(
+ &mut self,
+ _renderer: &mut GlesRenderer,
+ size: Size<i32, Physical>,
+ ) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
+ let (a, b) = (size.w / 6, size.h / 3);
+ let size = (size.w - a * 2, size.h - b * 2);
+ let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
+
+ [BorderRenderElement::new(
+ area.size,
+ Rectangle::from_loc_and_size((0., 0.), area.size),
+ self.gradient_format,
+ Color::new_unpremul(1., 0., 0., 1.),
+ Color::new_unpremul(0., 1., 0., 1.),
+ 0.,
+ Rectangle::from_loc_and_size((0., 0.), area.size),
+ 0.,
+ CornerRadius::default(),
+ 1.,
+ )
+ .with_location(area.loc)]
+ .into_iter()
+ .map(|elem| Box::new(elem) as _)
+ .collect()
+ }
+}
diff --git a/niri-visual-tests/src/cases/gradient_oklch_shorter.rs b/niri-visual-tests/src/cases/gradient_oklch_shorter.rs
new file mode 100644
index 00000000..7cd412ab
--- /dev/null
+++ b/niri-visual-tests/src/cases/gradient_oklch_shorter.rs
@@ -0,0 +1,53 @@
+use niri::render_helpers::border::BorderRenderElement;
+use niri_config::{
+ Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
+};
+use smithay::backend::renderer::element::RenderElement;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::utils::{Logical, Physical, Rectangle, Size};
+
+use super::TestCase;
+
+pub struct GradientOklchShorter {
+ gradient_format: GradientInterpolation,
+}
+
+impl GradientOklchShorter {
+ pub fn new(_size: Size<i32, Logical>) -> Self {
+ Self {
+ gradient_format: GradientInterpolation {
+ color_space: GradientColorSpace::Oklch,
+ hue_interpolation: HueInterpolation::Shorter,
+ },
+ }
+ }
+}
+
+impl TestCase for GradientOklchShorter {
+ fn render(
+ &mut self,
+ _renderer: &mut GlesRenderer,
+ size: Size<i32, Physical>,
+ ) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
+ let (a, b) = (size.w / 6, size.h / 3);
+ let size = (size.w - a * 2, size.h - b * 2);
+ let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
+
+ [BorderRenderElement::new(
+ area.size,
+ Rectangle::from_loc_and_size((0., 0.), area.size),
+ self.gradient_format,
+ Color::new_unpremul(1., 0., 0., 1.),
+ Color::new_unpremul(0., 1., 0., 1.),