diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index d2343ff..879b2c9 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -422,6 +422,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)] @@ -431,6 +433,30 @@ pub enum GradientRelativeTo { WorkspaceView, } +#[derive(Default, Debug, Clone, Copy, PartialEq)] +pub struct GradientInterpolation { + pub color_space: GradientColorSpace, + pub hue_interpol: 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)] @@ -503,7 +529,8 @@ impl Color { impl From 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.); - [r * a, g * a, b * a, a] + //[r * a, g * a, b * a, a] + [r, g, b, a] } } @@ -1423,6 +1450,55 @@ impl CornerRadius { } } +impl FromStr for GradientInterpolation { + type Err = miette::Error; + + fn from_str(s: &str) -> Result { + let mut iter = s.split_whitespace(); + let in_part1 = iter.next(); + let in_part2 = iter.next(); + let in_part3 = iter.next(); + + let color = if in_part1 != None { + let in_str = in_part1.unwrap(); + match in_str { + "srgb" => GradientColorSpace::Srgb, + "srgb-linear" => GradientColorSpace::SrgbLinear, + "oklab" => GradientColorSpace::Oklab, + "oklch" => GradientColorSpace::Oklch, + &_ => return Err(miette!("Invalid color-space: {in_str}")) + } + } else { + GradientColorSpace::Srgb + }; + + let interpolation = if in_part2 != None { + let in_str = in_part2.unwrap(); + if color != GradientColorSpace::Oklch { + return Err(miette!("There's a value: {in_str} after a non polar colorspace")) + } + if in_part3 == None || in_part3.unwrap() != "hue" { + return Err(miette!("Invalid hue-interpolation: {in_str} you may be missing 'hue' at the end.")) + } else if iter.next() == None { + match in_str { + "shorter" => HueInterpolation::Shorter, + "longer" => HueInterpolation::Longer, + "increasing" => HueInterpolation::Increasing, + "decreasing" => HueInterpolation::Decreasing, + &_ => return Err(miette!("Invalid hue-interpolation: {in_str}")) + } + } else { + // this is a placeholder and should be changed if anything is added to in + return Err(miette!("Theres a missing indicator ’hue’ from ’in’ ")) + } + } else { + HueInterpolation::Shorter + }; + + Ok( Self { color_space: color, hue_interpol: interpolation } ) + } +} + impl FromStr for Color { type Err = miette::Error; @@ -2778,6 +2854,10 @@ mod tests { to: Color::new(0, 128, 255, 255), angle: 180, relative_to: GradientRelativeTo::WorkspaceView, + in_ : GradientInterpolation { + color_space: GradientColorSpace::Srgb, + hue_interpol: HueInterpolation::Shorter, + } }), inactive_gradient: None, }, diff --git a/niri-visual-tests/src/cases/gradient_angle.rs b/niri-visual-tests/src/cases/gradient_angle.rs index 7d0b754..08a7f4d 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::{CornerRadius, GradientInterpolation}; use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::GlesRenderer; use smithay::utils::{Logical, Physical, Rectangle, Size}; @@ -64,6 +64,7 @@ impl TestCase for GradientAngle { [BorderRenderElement::new( area.size, Rectangle::from_loc_and_size((0., 0.), area.size), + GradientInterpolation::default(), [1., 0., 0., 1.], [0., 1., 0., 1.], self.angle - FRAC_PI_2, diff --git a/niri-visual-tests/src/cases/gradient_area.rs b/niri-visual-tests/src/cases/gradient_area.rs index 13f50d4..1c8a457 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}; @@ -104,6 +104,7 @@ impl TestCase for GradientArea { [BorderRenderElement::new( area.size, g_area, + GradientInterpolation::default(), [1., 0., 0., 1.], [0., 1., 0., 1.], FRAC_PI_4, 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 0000000..bd8014f --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_oklab.rs @@ -0,0 +1,51 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{CornerRadius, GradientInterpolation, HueInterpolation, GradientColorSpace}; +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) -> Self { + Self { + gradient_format: GradientInterpolation{ + color_space: GradientColorSpace::Oklab, + hue_interpol: HueInterpolation::Shorter + } + } + } +} + +impl TestCase for GradientOklab { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + 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, + [1., 0., 0., 1.], + [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_alpha.rs b/niri-visual-tests/src/cases/gradient_oklch_alpha.rs new file mode 100644 index 0000000..6d518e6 --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_oklch_alpha.rs @@ -0,0 +1,51 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{CornerRadius, GradientInterpolation, HueInterpolation, GradientColorSpace}; +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) -> Self { + Self { + gradient_format: GradientInterpolation{ + color_space: GradientColorSpace::Oklch, + hue_interpol: HueInterpolation::Longer + } + } + } +} + +impl TestCase for GradientOklchAlpha { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + 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, + [1., 0., 0., 1.], + [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 0000000..a3a734d --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_oklch_decreasing.rs @@ -0,0 +1,51 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{CornerRadius, GradientInterpolation, HueInterpolation, GradientColorSpace}; +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) -> Self { + Self { + gradient_format: GradientInterpolation{ + color_space: GradientColorSpace::Oklch, + hue_interpol: HueInterpolation::Decreasing + } + } + } +} + +impl TestCase for GradientOklchDecreasing { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + 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, + [1., 0., 0., 1.], + [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 0000000..89a75cb --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_oklch_increasing.rs @@ -0,0 +1,51 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{CornerRadius, GradientInterpolation, HueInterpolation, GradientColorSpace}; +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) -> Self { + Self { + gradient_format: GradientInterpolation{ + color_space: GradientColorSpace::Oklch, + hue_interpol: HueInterpolation::Increasing + } + } + } +} + +impl TestCase for GradientOklchIncreasing { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + 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, + [1., 0., 0., 1.], + [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 0000000..46c9f2b --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_oklch_longer.rs @@ -0,0 +1,51 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{CornerRadius, GradientInterpolation, HueInterpolation, GradientColorSpace}; +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) -> Self { + Self { + gradient_format: GradientInterpolation{ + color_space: GradientColorSpace::Oklch, + hue_interpol: HueInterpolation::Longer + } + } + } +} + +impl TestCase for GradientOklchLonger { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + 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, + [1., 0., 0., 1.], + [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 0000000..f9d2636 --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_oklch_shorter.rs @@ -0,0 +1,51 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{CornerRadius, GradientInterpolation, HueInterpolation, GradientColorSpace}; +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) -> Self { + Self { + gradient_format: GradientInterpolation{ + color_space: GradientColorSpace::Oklch, + hue_interpol: HueInterpolation::Shorter + } + } + } +} + +impl TestCase for GradientOklchShorter { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + 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, + [1., 0., 0., 1.], + [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_srgb.rs b/niri-visual-tests/src/cases/gradient_srgb.rs new file mode 100644 index 0000000..9b80124 --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_srgb.rs @@ -0,0 +1,51 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{CornerRadius, GradientInterpolation, HueInterpolation, GradientColorSpace}; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Physical, Rectangle, Size}; + +use super::TestCase; + +pub struct GradientSrgb { + gradient_format: GradientInterpolation, +} + +impl GradientSrgb { + pub fn new(_size: Size) -> Self { + Self { + gradient_format: GradientInterpolation{ + color_space: GradientColorSpace::Srgb, + hue_interpol: HueInterpolation::Shorter + } + } + } +} + +impl TestCase for GradientSrgb { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + 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, + [1., 0., 0., 1.], + [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_srgblinear.rs b/niri-visual-tests/src/cases/gradient_srgblinear.rs new file mode 100644 index 0000000..225ce64 --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_srgblinear.rs @@ -0,0 +1,51 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{CornerRadius, GradientInterpolation, HueInterpolation, GradientColorSpace}; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Physical, Rectangle, Size}; + +use super::TestCase; + +pub struct GradientSrgbLinear { + gradient_format: GradientInterpolation, +} + +impl GradientSrgbLinear { + pub fn new(_size: Size) -> Self { + Self { + gradient_format: GradientInterpolation{ + color_space: GradientColorSpace::SrgbLinear, + hue_interpol: HueInterpolation::Shorter + } + } + } +} + +impl TestCase for GradientSrgbLinear { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + 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, + [1., 0., 0., 1.], + [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/mod.rs b/niri-visual-tests/src/cases/mod.rs index 25aa1e3..4b01e33 100644 --- a/niri-visual-tests/src/cases/mod.rs +++ b/niri-visual-tests/src/cases/mod.rs @@ -6,6 +6,14 @@ use smithay::utils::{Physical, Size}; pub mod gradient_angle; pub mod gradient_area; +pub mod gradient_srgb; +pub mod gradient_srgblinear; +pub mod gradient_oklab; +pub mod gradient_oklch_shorter; +pub mod gradient_oklch_longer; +pub mod gradient_oklch_increasing; +pub mod gradient_oklch_decreasing; +pub mod gradient_oklch_alpha; pub mod layout; pub mod tile; pub mod window; diff --git a/niri-visual-tests/src/main.rs b/niri-visual-tests/src/main.rs index 771c501..89358b8 100644 --- a/niri-visual-tests/src/main.rs +++ b/niri-visual-tests/src/main.rs @@ -5,6 +5,7 @@ use std::env; use std::sync::atomic::Ordering; use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt}; +use cases::gradient_oklch_alpha::GradientOklchAlpha; use cases::tile::Tile; use cases::window::Window; use gtk::prelude::{ @@ -18,6 +19,13 @@ use tracing_subscriber::EnvFilter; use crate::cases::gradient_angle::GradientAngle; use crate::cases::gradient_area::GradientArea; +use crate::cases::gradient_srgb::GradientSrgb; +use crate::cases::gradient_srgblinear::GradientSrgbLinear; +use crate::cases::gradient_oklab::GradientOklab; +use crate::cases::gradient_oklch_shorter::GradientOklchShorter; +use crate::cases::gradient_oklch_longer::GradientOklchLonger; +use crate::cases::gradient_oklch_increasing::GradientOklchIncreasing; +use crate::cases::gradient_oklch_decreasing::GradientOklchDecreasing; use crate::cases::layout::Layout; use crate::cases::TestCase; @@ -112,6 +120,14 @@ fn build_ui(app: &adw::Application) { s.add(GradientAngle::new, "Gradient - Angle"); s.add(GradientArea::new, "Gradient - Area"); + s.add(GradientSrgb::new, "Gradient - Srgb"); + s.add(GradientSrgbLinear::new, "Gradient - SrgbLinear"); + s.add(GradientOklab::new, "Gradient - Oklab"); + s.add(GradientOklchShorter::new, "Gradient - Oklch Shorter"); + s.add(GradientOklchLonger::new, "Gradient - Oklch Longer"); + s.add(GradientOklchIncreasing::new, "Gradient - Oklch Increasing"); + s.add(GradientOklchDecreasing::new, "Gradient - Oklch Decreasing"); + s.add(GradientOklchAlpha::new, "Gradient - Alpha"); let content_headerbar = adw::HeaderBar::new(); diff --git a/resources/default-config.kdl b/resources/default-config.kdl index 1d8ebb6..ed2d4f0 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -153,6 +153,7 @@ layout { // The angle is the same as in linear-gradient, and is optional, // defaulting to 180 (top-to-bottom gradient). // You can use any CSS linear-gradient tool on the web to set these up. + // Changing the color space is also supported, check the wiki for more info. // // active-gradient from="#80c8ff" to="#bbddff" angle=45 diff --git a/src/layout/focus_ring.rs b/src/layout/focus_ring.rs index 97a1b75..af1af21 100644 --- a/src/layout/focus_ring.rs +++ b/src/layout/focus_ring.rs @@ -1,7 +1,7 @@ use std::iter::zip; use arrayvec::ArrayVec; -use niri_config::{CornerRadius, Gradient, GradientRelativeTo}; +use niri_config::{CornerRadius, Gradient, GradientRelativeTo, GradientInterpolation}; use smithay::backend::renderer::element::Kind; use smithay::utils::{Logical, Point, Rectangle, Size}; @@ -91,6 +91,7 @@ impl FocusRing { to: color, angle: 0, relative_to: GradientRelativeTo::Window, + in_: GradientInterpolation::default() }); let full_rect = Rectangle::from_loc_and_size((-width, -width), self.full_size); @@ -178,6 +179,7 @@ impl FocusRing { border.update( size, Rectangle::from_loc_and_size(gradient_area.loc - loc, gradient_area.size), + gradient.in_, gradient.from.into(), gradient.to.into(), ((gradient.angle as f32) - 90.).to_radians(), @@ -198,6 +200,7 @@ impl FocusRing { gradient_area.loc - self.locations[0], gradient_area.size, ), + gradient.in_, gradient.from.into(), gradient.to.into(), ((gradient.angle as f32) - 90.).to_radians(), diff --git a/src/layout/tile.rs b/src/layout/tile.rs index 710add8..ec98525 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use std::time::Duration; -use niri_config::CornerRadius; +use niri_config::{CornerRadius, GradientInterpolation}; use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::{Element, Kind}; use smithay::backend::renderer::gles::GlesRenderer; @@ -757,6 +757,7 @@ impl Tile { return BorderRenderElement::new( geo.size, Rectangle::from_loc_and_size((0., 0.), geo.size), + GradientInterpolation::default(), elem.color(), elem.color(), 0., diff --git a/src/render_helpers/border.rs b/src/render_helpers/border.rs index 7e36c44..6f49aa2 100644 --- a/src/render_helpers/border.rs +++ b/src/render_helpers/border.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use glam::{Mat3, Vec2}; -use niri_config::CornerRadius; +use niri_config::{CornerRadius, GradientInterpolation, GradientColorSpace, HueInterpolation}; use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage}; use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform}; use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions}; @@ -28,6 +28,7 @@ pub struct BorderRenderElement { struct Parameters { size: Size, gradient_area: Rectangle, + gradient_format: GradientInterpolation, color_from: [f32; 4], color_to: [f32; 4], angle: f32, @@ -43,6 +44,7 @@ impl BorderRenderElement { pub fn new( size: Size, gradient_area: Rectangle, + gradient_format: GradientInterpolation, color_from: [f32; 4], color_to: [f32; 4], angle: f32, @@ -57,6 +59,7 @@ impl BorderRenderElement { params: Parameters { size, gradient_area, + gradient_format, color_from, color_to, angle, @@ -77,6 +80,7 @@ impl BorderRenderElement { params: Parameters { size: Default::default(), gradient_area: Default::default(), + gradient_format: GradientInterpolation::default(), color_from: Default::default(), color_to: Default::default(), angle: 0., @@ -97,6 +101,7 @@ impl BorderRenderElement { &mut self, size: Size, gradient_area: Rectangle, + gradient_format: GradientInterpolation, color_from: [f32; 4], color_to: [f32; 4], angle: f32, @@ -108,6 +113,7 @@ impl BorderRenderElement { let params = Parameters { size, gradient_area, + gradient_format, color_from, color_to, angle, @@ -128,6 +134,7 @@ impl BorderRenderElement { let Parameters { size, gradient_area, + gradient_format, color_from, color_to, angle, @@ -162,11 +169,27 @@ impl BorderRenderElement { let input_to_geo = Mat3::from_scale(area_size) * Mat3::from_translation(-geo_loc / area_size); + let colorspace = match gradient_format.color_space { + GradientColorSpace::Srgb => 0., + GradientColorSpace::SrgbLinear => 1., + GradientColorSpace::Oklab => 2., + GradientColorSpace::Oklch => 3., + }; + + let hue_interpolation = match gradient_format.hue_interpol { + HueInterpolation::Shorter => 0., + HueInterpolation::Longer => 1., + HueInterpolation::Increasing => 2., + HueInterpolation::Decreasing => 3., + }; + self.inner.update( size, None, scale, vec![ + Uniform::new("colorspace", colorspace), + Uniform::new("hue_interpolation", hue_interpolation), Uniform::new("color_from", color_from), Uniform::new("color_to", color_to), Uniform::new("grad_offset", grad_offset.to_array()), diff --git a/src/render_helpers/shaders/border.frag b/src/render_helpers/shaders/border.frag index fe12103..3f92554 100644 --- a/src/render_helpers/shaders/border.frag +++ b/src/render_helpers/shaders/border.frag @@ -10,6 +10,8 @@ uniform float niri_scale; uniform vec2 niri_size; varying vec2 niri_v_coords; +uniform float colorspace; +uniform float hue_interpolation; uniform vec4 color_from; uniform vec4 color_to; uniform vec2 grad_offset; @@ -21,6 +23,173 @@ uniform vec2 geo_size; uniform vec4 outer_radius; uniform float border_width; +float srgb_to_linear(float color) { + return pow(color, 2.2); +} + +float linear_to_srgb(float color) { + return pow(color, 1.0 / 2.2); +} + +vec3 lab_to_lch(vec3 color) { + float c = sqrt(pow(color.y, 2.0) + pow(color.z, 2.0)); + float h = degrees(atan(color.z, color.y)) ; + h += h <= 0.0 ? + 360.0 : + 0.0 ; + return vec3( + color.x, + c, + h + ); +} + +vec3 lch_to_lab(vec3 color) { + float a = color.y * clamp(cos(radians(color.z)), -1.0, 1.0); + float b = color.y * clamp(sin(radians(color.z)), -1.0, 1.0); + return vec3( + color.x, + a, + b + ); +} + +vec3 linear_to_oklab(vec3 color){ + mat3 rgb_to_lms = mat3( + vec3(0.4122214708, 0.5363325363, 0.0514459929), + vec3(0.2119034982, 0.6806995451, 0.1073969566), + vec3(0.0883024619, 0.2817188376, 0.6299787005) + ); + mat3 lms_to_oklab = mat3( + vec3(0.2104542553, 0.7936177850, -0.0040720468), + vec3(1.9779984951, -2.4285922050, 0.4505937099), + vec3(0.0259040371, 0.7827717662, -0.8086757660) + ); + vec3 lms = color * rgb_to_lms; + + lms = vec3( + pow(lms.x, 1.0 / 3.0), + pow(lms.y, 1.0 / 3.0), + pow(lms.z, 1.0 / 3.0) + ); + return lms * lms_to_oklab; +} + +vec3 oklab_to_linear(vec3 color){ + mat3 oklab_to_lms = mat3( + vec3(1.0, 0.3963377774, 0.2158037573), + vec3(1.0, -0.1055613458, -0.0638541728), + vec3(1.0, -0.0894841775, -1.2914855480) + ); + mat3 lms_to_rgb = mat3( + vec3(4.0767416621, -3.3077115913, 0.2309699292), + vec3(-1.2684380046, 2.6097574011, -0.3413193965), + vec3(-0.0041960863, -0.7034186147, 1.7076147010) + ); + vec3 lms = color * oklab_to_lms; + + lms = vec3( + pow(lms.x, 3.0), + pow(lms.y, 3.0), + pow(lms.z, 3.0) + ); + return lms * lms_to_rgb; +} + +vec4 color_mix(vec4 color1, vec4 color2, float color_ratio) { + // srgb + + vec4 color_out; + + if (colorspace == 0.0) { + color_out = mix(color1, color2, color_ratio); + color_out.rgb = color_out.rgb * color_out.a; + return color_out; + } + + color1.rgb = vec3( + srgb_to_linear(color1.r), + srgb_to_linear(color1.g), + srgb_to_linear(color1.b) + ); + color2.rgb = vec3( + srgb_to_linear(color2.r), + srgb_to_linear(color2.g), + srgb_to_linear(color2.b) + ); + // srgb-linear + if (colorspace == 1.0) { + color_out = mix( + color1, + color2, + color_ratio + ); + // oklab + } else if (colorspace == 2.0) { + color1.xyz = linear_to_oklab(color1.rgb); + color2.xyz = linear_to_oklab(color2.rgb); + color_out = mix( + color1, + color2, + color_ratio + ); + color_out.rgb = oklab_to_linear(color_out.xyz); + // oklch + } else if (colorspace == 3.0) { + color1.xyz = lab_to_lch(linear_to_oklab(color1.rgb)); + color2.xyz = lab_to_lch(linear_to_oklab(color2.rgb)); + color_out = mix(color1, color2, color_ratio); + + float min_hue = min(color1.z, color2.z); + float max_hue = max(color1.z, color2.z); + float path_direct_distance = (max_hue - min_hue) * color_ratio; + float path_mod_distance = (360.0 - max_hue + min_hue) * color_ratio; + + float path_mod = + color1.z == min_hue ? + mod(color1.z - path_mod_distance, 360.0) : + mod(color1.z + path_mod_distance, 360.0) ; + float path_direct = + color1.z == min_hue ? + color1.z + path_direct_distance : + color1.z - path_direct_distance ; + + // shorter + if (hue_interpolation == 0.0) { + color_out.z = + max_hue - min_hue > 360.0 - max_hue + min_hue ? + path_mod : + path_direct ; + // longer + } else if (hue_interpolation == 1.0) { + color_out.z = + max_hue - min_hue <= 360.0 - max_hue + min_hue ? + path_mod : + path_direct ; + // increasing + } else if (hue_interpolation == 2.0) { + color_out.z = + color1.z > color2.z ? + path_mod : + path_direct ; + // decreasing + } else if (hue_interpolation == 3.0) { + color_out.z = + color1.z <= color2.z ? + path_mod : + path_direct ; + } + color_out.rgb = clamp(oklab_to_linear(lch_to_lab(color_out.xyz)), 0.0, 1.0); + } + + return vec4( + linear_to_srgb(color_out.r) * color_out.a, + linear_to_srgb(color_out.g) * color_out.a, + linear_to_srgb(color_out.b) * color_out.a, + color_out.a + ); +} + vec4 gradient_color(vec2 coords) { coords = coords + grad_offset; @@ -33,7 +202,7 @@ vec4 gradient_color(vec2 coords) { frac += 1.0; frac = clamp(frac, 0.0, 1.0); - return mix(color_from, color_to, frac); + return color_mix(color_from, color_to, frac); } float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) { diff --git a/src/render_helpers/shaders/mod.rs b/src/render_helpers/shaders/mod.rs index b482493..91ba32d 100644 --- a/src/render_helpers/shaders/mod.rs +++ b/src/render_helpers/shaders/mod.rs @@ -34,6 +34,8 @@ impl Shaders { renderer, include_str!("border.frag"), &[ + UniformName::new("colorspace", UniformType::_1f), + UniformName::new("hue_interpolation", UniformType::_1f), UniformName::new("color_from", UniformType::_4f), UniformName::new("color_to", UniformType::_4f), UniformName::new("grad_offset", UniformType::_2f), diff --git a/src/window/mapped.rs b/src/window/mapped.rs index 178b0a3..c9841c6 100644 --- a/src/window/mapped.rs +++ b/src/window/mapped.rs @@ -2,7 +2,7 @@ use std::cell::{Cell, RefCell}; use std::cmp::{max, min}; use std::time::Duration; -use niri_config::{CornerRadius, WindowRule}; +use niri_config::{CornerRadius, GradientInterpolation, WindowRule}; use smithay::backend::renderer::element::surface::render_elements_from_surface_tree; use smithay::backend::renderer::element::{Id, Kind}; use smithay::backend::renderer::gles::GlesRenderer; @@ -289,6 +289,7 @@ impl Mapped { return BorderRenderElement::new( geo.size, Rectangle::from_loc_and_size((0., 0.), geo.size), + GradientInterpolation::default(), elem.color(), elem.color(), 0., diff --git a/wiki/Configuration:-Layout.md b/wiki/Configuration:-Layout.md index 04fe04b..5211723 100644 --- a/wiki/Configuration:-Layout.md +++ b/wiki/Configuration:-Layout.md @@ -32,7 +32,7 @@ layout { active-color "#ffc87f" inactive-color "#505050" // active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view" - // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" + // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" in="srgb-linear" } struts { @@ -169,7 +169,7 @@ layout { inactive-color "#505050" // active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view" - // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" + // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" in="srgb-linear" } } ``` @@ -221,6 +221,9 @@ layout { } ``` +Gradients can be rendered with different kinds of color interpolation, this doesen't mean that the arguments the gradient takes are any different. +Except for an optional `in` argument which can take both a colorspace and a hue interpolation method if the color space is polar. + Gradients can be colored relative to windows individually (the default), or to the whole view of the workspace. To do that, set `relative-to="workspace-view"`. Here's a visual example: