From 37ec1a4a3f2da67788b5c65f5e3e760d54a18a41 Mon Sep 17 00:00:00 2001 From: kossLAN Date: Fri, 29 May 2026 11:37:04 -0400 Subject: [PATCH] theme: move shared state into workspace crate --- Cargo.lock | 18 + Cargo.toml | 4 + gfx-types/Cargo.toml | 7 + gfx-types/src/lib.rs | 7 + src/gfx_api.rs | 10 +- src/gfx_apis/vulkan/alpha_modes.rs | 8 +- src/gfx_apis/vulkan/pipeline.rs | 4 +- src/icons.rs | 12 - src/theme.rs | 919 +---------------------------- theme/Cargo.toml | 14 + theme/src/lib.rs | 916 ++++++++++++++++++++++++++++ 11 files changed, 977 insertions(+), 942 deletions(-) create mode 100644 gfx-types/Cargo.toml create mode 100644 gfx-types/src/lib.rs create mode 100644 theme/Cargo.toml create mode 100644 theme/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 9424f67f..ee3a9112 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -710,6 +710,7 @@ dependencies = [ "jay-eventfd-cache", "jay-formats", "jay-geometry", + "jay-gfx-types", "jay-io-uring", "jay-layout-animation", "jay-libinput", @@ -717,6 +718,7 @@ dependencies = [ "jay-pango", "jay-pr-caps", "jay-sighand", + "jay-theme", "jay-time", "jay-toml-config", "jay-tracy", @@ -850,6 +852,10 @@ dependencies = [ "smallvec", ] +[[package]] +name = "jay-gfx-types" +version = "0.1.0" + [[package]] name = "jay-io-uring" version = "0.1.0" @@ -936,6 +942,18 @@ dependencies = [ "uapi", ] +[[package]] +name = "jay-theme" +version = "0.1.0" +dependencies = [ + "jay-cmm", + "jay-config", + "jay-gfx-types", + "jay-utils", + "linearize", + "num-traits", +] + [[package]] name = "jay-time" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index a83a58dc..6fa6ee7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,8 @@ members = [ "bugs", "logger", "video-types", + "gfx-types", + "theme", "pango", "libinput", "toml-config", @@ -89,6 +91,8 @@ jay-pr-caps = { version = "0.1.0", path = "pr-caps" } jay-bugs = { version = "0.1.0", path = "bugs" } jay-logger = { version = "0.1.0", path = "logger" } jay-video-types = { version = "0.1.0", path = "video-types" } +jay-gfx-types = { version = "0.1.0", path = "gfx-types" } +jay-theme = { version = "0.1.0", path = "theme" } jay-pango = { version = "0.1.0", path = "pango" } jay-libinput = { version = "0.1.0", path = "libinput" } diff --git a/gfx-types/Cargo.toml b/gfx-types/Cargo.toml new file mode 100644 index 00000000..ad1c75dc --- /dev/null +++ b/gfx-types/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "jay-gfx-types" +version = "0.1.0" +edition = "2024" +license = "GPL-3.0-only" + +[dependencies] diff --git a/gfx-types/src/lib.rs b/gfx-types/src/lib.rs new file mode 100644 index 00000000..28bb50e3 --- /dev/null +++ b/gfx-types/src/lib.rs @@ -0,0 +1,7 @@ +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default)] +pub enum AlphaMode { + #[default] + PremultipliedElectrical, + PremultipliedOptical, + Straight, +} diff --git a/src/gfx_api.rs b/src/gfx_api.rs index d4c0f19b..f5518b2e 100644 --- a/src/gfx_api.rs +++ b/src/gfx_api.rs @@ -47,6 +47,8 @@ use { uapi::{OwnedFd, c}, }; +pub use jay_gfx_types::AlphaMode; + #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Linearize)] pub enum GfxApi { OpenGl, @@ -409,14 +411,6 @@ pub enum ResetStatus { Other(u32), } -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default)] -pub enum AlphaMode { - #[default] - PremultipliedElectrical, - PremultipliedOptical, - Straight, -} - pub trait GfxBlendBuffer: Any + Debug {} pub trait GfxFramebuffer: Debug { diff --git a/src/gfx_apis/vulkan/alpha_modes.rs b/src/gfx_apis/vulkan/alpha_modes.rs index 9480c2b0..98c458f3 100644 --- a/src/gfx_apis/vulkan/alpha_modes.rs +++ b/src/gfx_apis/vulkan/alpha_modes.rs @@ -4,8 +4,12 @@ pub const AM_PREMULTIPLIED_ELECTRICAL: u32 = 0; pub const AM_PREMULTIPLIED_OPTICAL: u32 = 1; pub const AM_STRAIGHT: u32 = 2; -impl AlphaMode { - pub fn to_vulkan(self) -> u32 { +pub trait AlphaModeExt { + fn to_vulkan(self) -> u32; +} + +impl AlphaModeExt for AlphaMode { + fn to_vulkan(self) -> u32 { match self { AlphaMode::PremultipliedElectrical => AM_PREMULTIPLIED_ELECTRICAL, AlphaMode::PremultipliedOptical => AM_PREMULTIPLIED_OPTICAL, diff --git a/src/gfx_apis/vulkan/pipeline.rs b/src/gfx_apis/vulkan/pipeline.rs index 5351a483..6d670e26 100644 --- a/src/gfx_apis/vulkan/pipeline.rs +++ b/src/gfx_apis/vulkan/pipeline.rs @@ -2,8 +2,8 @@ use { crate::{ gfx_api::AlphaMode, gfx_apis::vulkan::{ - VulkanError, descriptor::VulkanDescriptorSetLayout, device::VulkanDevice, - shaders::VulkanShader, + VulkanError, alpha_modes::AlphaModeExt, descriptor::VulkanDescriptorSetLayout, + device::VulkanDevice, shaders::VulkanShader, }, }, arrayvec::ArrayVec, diff --git a/src/icons.rs b/src/icons.rs index f4537352..624e45d8 100644 --- a/src/icons.rs +++ b/src/icons.rs @@ -222,18 +222,6 @@ impl PathBuilderExt for PathBuilder { } } -impl From for Color { - fn from(v: crate::theme::Color) -> Self { - let [r, g, b, a] = v.to_array(Eotf::Gamma22); - let mut c = Self::TRANSPARENT; - c.set_red(r / a); - c.set_green(g / a); - c.set_blue(b / a); - c.set_alpha(a); - c - } -} - fn calculate_accents(srgb: crate::theme::Color) -> [Color; 2] { let [l, a, b, alpha] = srgb_to_lab(srgb); let l2 = if l < 0.65 { 0.9 } else { l - 0.4 }; diff --git a/src/theme.rs b/src/theme.rs index 056d88f2..48b30b6b 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,918 +1 @@ -#![expect(clippy::excessive_precision)] - -use { - crate::{ - cmm::cmm_eotf::{Eotf, bt1886_eotf_args, bt1886_inv_eotf_args}, - gfx_api::AlphaMode, - utils::{clonecell::CloneCell, static_text::StaticText}, - }, - jay_config::theme::BarPosition as ConfigBarPosition, - linearize::Linearize, - num_traits::Float, - std::{ - cell::Cell, - cmp::Ordering, - ops::{Add, Div, Mul}, - sync::Arc, - }, -}; - -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct Color { - r: f32, - g: f32, - b: f32, - a: f32, -} - -impl Eq for Color {} - -impl Ord for Color { - fn cmp(&self, other: &Self) -> Ordering { - self.r - .total_cmp(&other.r) - .then_with(|| self.g.total_cmp(&other.g)) - .then_with(|| self.b.total_cmp(&other.b)) - .then_with(|| self.a.total_cmp(&other.a)) - } -} - -impl Mul for Color { - type Output = Self; - - fn mul(self, rhs: f32) -> Self::Output { - Self { - r: self.r * rhs, - g: self.g * rhs, - b: self.b * rhs, - a: self.a * rhs, - } - } -} - -impl PartialOrd for Color { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -fn to_f32(c: u8) -> f32 { - c as f32 / 255f32 -} - -fn to_u8(c: f32) -> u8 { - (c * 255f32).round() as u8 -} - -impl Color { - pub const TRANSPARENT: Self = Self { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.0, - }; - - pub const SOLID_BLACK: Self = Self { - r: 0.0, - g: 0.0, - b: 0.0, - a: 1.0, - }; - - pub fn new( - eotf: Eotf, - alpha_mode: AlphaMode, - mut r: f32, - mut g: f32, - mut b: f32, - a: f32, - ) -> Self { - if eotf == Eotf::Linear { - if alpha_mode == AlphaMode::Straight && a < 1.0 { - for c in [&mut r, &mut g, &mut b] { - *c *= a; - } - } - return Self { r, g, b, a }; - } - if alpha_mode == AlphaMode::PremultipliedElectrical && a < 1.0 && a > 0.0 { - for c in [&mut r, &mut g, &mut b] { - *c /= a; - } - } - #[inline(always)] - fn linear(c: f32) -> f32 { - c - } - fn st2084_pq(c: f32) -> f32 { - let cp = c.powf(1.0 / 78.84375); - let num = (cp - 0.8359375).max(0.0); - let den = 18.8515625 - 18.6875 * cp; - (num / den).powf(1.0 / 0.1593017578125) - } - fn st240(c: f32) -> f32 { - if c < 0.0913 { - c / 4.0 - } else { - ((c + 0.1115) / 1.1115).powf(1.0 / 0.45) - } - } - fn log100(c: f32) -> f32 { - 10.0.powf(2.0 * (c - 1.0)) - } - fn log316(c: f32) -> f32 { - 10.0.powf(2.5 * (c - 1.0)) - } - fn st428(c: f32) -> f32 { - c.powf(2.6) * 52.37 / 48.0 - } - fn gamma22(c: f32) -> f32 { - c.signum() * c.abs().powf(2.2) - } - fn gamma24(c: f32) -> f32 { - c.signum() * c.abs().powf(2.4) - } - fn gamma28(c: f32) -> f32 { - c.signum() * c.abs().powf(2.8) - } - fn compound_power_2_4(c: f32) -> f32 { - if c < 0.04045 { - c / 12.92 - } else { - ((c + 0.055) / 1.055).powf(2.4) - } - } - macro_rules! convert { - ($tf:ident) => {{ - r = $tf(r); - g = $tf(g); - b = $tf(b); - }}; - } - match eotf { - Eotf::Linear => convert!(linear), - Eotf::St2084Pq => convert!(st2084_pq), - Eotf::Bt1886(c) => { - let [a1, a2, a3, a4] = bt1886_eotf_args(c); - let bt1886 = |c: f32| -> f32 { a1 * ((a2 * c + a3).powf(2.4) - a4) }; - convert!(bt1886) - } - Eotf::Gamma22 => convert!(gamma22), - Eotf::Gamma24 => convert!(gamma24), - Eotf::Gamma28 => convert!(gamma28), - Eotf::St240 => convert!(st240), - Eotf::Log100 => convert!(log100), - Eotf::Log316 => convert!(log316), - Eotf::St428 => convert!(st428), - Eotf::Pow(n) => { - let e = n.eotf_f32(); - let pow = |c: f32| -> f32 { c.signum() * c.abs().powf(e) }; - convert!(pow) - } - Eotf::CompoundPower24 => convert!(compound_power_2_4), - } - if alpha_mode != AlphaMode::PremultipliedOptical && a < 1.0 { - for c in [&mut r, &mut g, &mut b] { - *c *= a; - } - } - Self { r, g, b, a } - } - - pub fn is_opaque(&self) -> bool { - self.a >= 1.0 - } - - pub fn from_gray_srgb(g: u8) -> Self { - Self::from_srgb(g, g, g) - } - - pub fn from_srgb(r: u8, g: u8, b: u8) -> Self { - Self::new( - Eotf::Gamma22, - AlphaMode::PremultipliedOptical, - to_f32(r), - to_f32(g), - to_f32(b), - 1.0, - ) - } - - pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { - Self::new( - Eotf::Gamma22, - AlphaMode::PremultipliedElectrical, - to_f32(r), - to_f32(g), - to_f32(b), - to_f32(a), - ) - } - - pub fn from_u32(eotf: Eotf, alpha_mode: AlphaMode, r: u32, g: u32, b: u32, a: u32) -> Self { - fn to_f32(c: u32) -> f32 { - ((c as f64) / (u32::MAX as f64)) as f32 - } - Self::new(eotf, alpha_mode, to_f32(r), to_f32(g), to_f32(b), to_f32(a)) - } - - pub fn from_srgba_straight(r: u8, g: u8, b: u8, a: u8) -> Self { - Self::new( - Eotf::Gamma22, - AlphaMode::Straight, - to_f32(r), - to_f32(g), - to_f32(b), - to_f32(a), - ) - } - - pub fn to_srgba_premultiplied(self) -> [u8; 4] { - let [r, g, b, a] = self.to_array(Eotf::Gamma22); - [to_u8(r), to_u8(g), to_u8(b), to_u8(a)] - } - - pub fn to_array(self, eotf: Eotf) -> [f32; 4] { - self.to_array2(eotf, None) - } - - pub fn to_array2(self, eotf: Eotf, alpha: Option) -> [f32; 4] { - let mut res = [self.r, self.g, self.b, self.a]; - fn linear(c: f32) -> f32 { - c - } - fn st2084_pq(c: f32) -> f32 { - let c = c.clamp(0.0, 1.0); - let num = 0.8359375 + 18.8515625 * c.powf(0.1593017578125); - let den = 1.0 + 18.6875 * c.powf(0.1593017578125); - (num / den).powf(78.84375) - } - fn st240(c: f32) -> f32 { - if c < 0.0228 { - 4.0 * c - } else { - 1.1115 * c.powf(0.45) - 0.1115 - } - } - fn log100(c: f32) -> f32 { - let c = c.clamp(0.0, 1.0); - if c < 0.01 { 0.0 } else { 1.0 + c.log10() / 2.0 } - } - fn log316(c: f32) -> f32 { - let c = c.clamp(0.0, 1.0); - if c < 10.0.sqrt() / 1000.0 { - 0.0 - } else { - 1.0 + c.log10() / 2.5 - } - } - fn st428(c: f32) -> f32 { - (48.0 * c / 52.37).powf(1.0 / 2.6) - } - fn gamma22(c: f32) -> f32 { - c.signum() * c.abs().powf(1.0 / 2.2) - } - fn gamma24(c: f32) -> f32 { - c.signum() * c.abs().powf(1.0 / 2.4) - } - fn gamma28(c: f32) -> f32 { - c.signum() * c.abs().powf(1.0 / 2.8) - } - fn compound_power_2_4(c: f32) -> f32 { - if c < 0.0031308 { - 12.92 * c - } else { - 1.055 * c.powf(1.0 / 2.4) - 0.055 - } - } - macro_rules! convert { - ($tf:ident) => {{ - for c in &mut res[..3] { - *c = $tf(*c); - } - }}; - } - if eotf != Eotf::Linear { - if self.a < 1.0 && self.a > 0.0 { - for c in &mut res[..3] { - *c /= self.a; - } - } - match eotf { - Eotf::Linear => convert!(linear), - Eotf::St2084Pq => convert!(st2084_pq), - Eotf::Bt1886(c) => { - let [a1, a2, a3, a4] = bt1886_inv_eotf_args(c); - let bt1886 = |c: f32| -> f32 { a1 * ((a2 * c + a3).powf(1.0 / 2.4) - a4) }; - convert!(bt1886) - } - Eotf::Gamma22 => convert!(gamma22), - Eotf::Gamma24 => convert!(gamma24), - Eotf::Gamma28 => convert!(gamma28), - Eotf::St240 => convert!(st240), - Eotf::Log100 => convert!(log100), - Eotf::Log316 => convert!(log316), - Eotf::St428 => convert!(st428), - Eotf::Pow(n) => { - let e = n.inv_eotf_f32(); - let pow = |c: f32| -> f32 { c.signum() * c.abs().powf(e) }; - convert!(pow) - } - Eotf::CompoundPower24 => convert!(compound_power_2_4), - } - if self.a < 1.0 { - for c in &mut res[..3] { - *c *= self.a; - } - } - } - if let Some(a) = alpha { - for c in &mut res { - *c *= a; - } - } - res - } - - pub fn and_then(self, other: &Color) -> Color { - Color { - r: self.r * (1.0 - other.a) + other.r, - g: self.g * (1.0 - other.a) + other.g, - b: self.b * (1.0 - other.a) + other.b, - a: self.a * (1.0 - other.a) + other.a, - } - } - - pub fn srgb_to_oklab(self) -> Oklab { - if self.a == 0.0 { - return Oklab { - l: 0.0, - a: 0.0, - b: 0.0, - }; - } - - let [r, g, b, _] = self.to_array2(Eotf::Linear, Some(1.0 / self.a)); - - let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b; - let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b; - let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b; - - let l_ = l.cbrt(); - let m_ = m.cbrt(); - let s_ = s.cbrt(); - - let l = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_; - let a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_; - let b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_; - - Oklab { l, a, b } - } -} - -impl From for Color { - fn from(f: jay_config::theme::Color) -> Self { - let [r, g, b, a] = f.to_f32_premultiplied(); - Self::new( - Eotf::Gamma22, - AlphaMode::PremultipliedElectrical, - r, - g, - b, - a, - ) - } -} - -macro_rules! colors { - ($($name:ident = $colors:tt,)*) => { - pub struct ThemeColors { - $( - pub $name: Cell, - )* - } - - #[derive(Copy, Clone, Debug, Linearize)] - #[expect(non_camel_case_types)] - pub enum ThemeColor { - $( - $name, - )* - } - - impl ThemeColor { - pub fn field(self, theme: &Theme) -> &Cell { - let colors = &theme.colors; - match self { - $( - Self::$name => &colors.$name, - )* - } - } - } - - impl ThemeColors { - pub fn reset(&self) { - let default = Self::default(); - $( - self.$name.set(default.$name.get()); - )* - } - } - - impl Default for ThemeColors { - fn default() -> Self { - Self { - $( - $name: Cell::new(colors!(@colors $colors)), - )* - } - } - } - }; - (@colors ($r:expr, $g:expr, $b:expr)) => { - Color::from_srgb($r, $g, $b) - }; - (@colors ($r:expr, $g:expr, $b:expr, $a:expr)) => { - Color::from_srgba_straight($r, $g, $b, $a) - }; -} - -colors! { - background = (0x00, 0x10, 0x19), - unfocused_title_background = (0x22, 0x22, 0x22), - focused_title_background = (0x28, 0x55, 0x77), - captured_unfocused_title_background = (0x22, 0x03, 0x03), - captured_focused_title_background = (0x77, 0x28, 0x31), - focused_inactive_title_background = (0x5f, 0x67, 0x6a), - unfocused_title_text = (0x88, 0x88, 0x88), - focused_title_text = (0xff, 0xff, 0xff), - focused_inactive_title_text = (0xff, 0xff, 0xff), - separator = (0x33, 0x33, 0x33), - border = (0x3f, 0x47, 0x4a), - active_border = (0x28, 0x55, 0x77), - bar_background = (0x00, 0x00, 0x00), - bar_text = (0xff, 0xff, 0xff), - attention_requested_background = (0x23, 0x09, 0x2c), - highlight = (0x9d, 0x28, 0xc6, 0x7f), - tab_active_background = (0x4c, 0x78, 0x99), - tab_active_border = (0x28, 0x55, 0x77), - tab_inactive_background = (0x22, 0x22, 0x22), - tab_inactive_border = (0x33, 0x33, 0x33), - tab_active_text = (0xff, 0xff, 0xff), - tab_inactive_text = (0x88, 0x88, 0x88), - tab_bar_background = (0x00, 0x00, 0x00, 0x00), - tab_attention_background = (0x23, 0x09, 0x2c), -} - -impl StaticText for ThemeColor { - fn text(&self) -> &'static str { - match self { - ThemeColor::background => "Background", - ThemeColor::unfocused_title_background => "Title Background (unfocused)", - ThemeColor::focused_title_background => "Title Background (focused)", - ThemeColor::captured_unfocused_title_background => { - "Title Background (unfocused, captured)" - } - ThemeColor::captured_focused_title_background => "Title Background (focused, captured)", - ThemeColor::focused_inactive_title_background => "Title Background (focused, inactive)", - ThemeColor::unfocused_title_text => "Title Text (unfocused)", - ThemeColor::focused_title_text => "Title Text (focused)", - ThemeColor::focused_inactive_title_text => "Title Text (focused, inactive)", - ThemeColor::separator => "Separator", - ThemeColor::border => "Border", - ThemeColor::active_border => "Border (active)", - ThemeColor::bar_background => "Bar Background", - ThemeColor::bar_text => "Bar Text", - ThemeColor::attention_requested_background => "Attention Requested", - ThemeColor::highlight => "Highlight", - ThemeColor::tab_active_background => "Tab Background (active)", - ThemeColor::tab_active_border => "Tab Border (active)", - ThemeColor::tab_inactive_background => "Tab Background (inactive)", - ThemeColor::tab_inactive_border => "Tab Border (inactive)", - ThemeColor::tab_active_text => "Tab Text (active)", - ThemeColor::tab_inactive_text => "Tab Text (inactive)", - ThemeColor::tab_bar_background => "Tab Bar Background", - ThemeColor::tab_attention_background => "Tab Attention Background", - } - } -} - -pub struct ThemeSize { - pub val: Cell, - pub set: Cell, -} - -impl ThemeSize { - pub fn get(&self) -> i32 { - self.val.get() - } -} - -macro_rules! sizes { - ($($name:ident = ($min:expr, $max:expr, $def:expr),)*) => { - pub struct ThemeSizes { - $( - pub $name: ThemeSize, - )* - } - - #[derive(Copy, Clone, Debug, Linearize)] - #[expect(non_camel_case_types)] - pub enum ThemeSized { - $( - $name, - )* - } - - impl ThemeSized { - pub fn min(self) -> i32 { - match self { - $( - Self::$name => $min, - )* - } - } - - pub fn max(self) -> i32 { - match self { - $( - Self::$name => $max, - )* - } - } - - pub fn field(self, theme: &Theme) -> &ThemeSize { - let sizes = &theme.sizes; - match self { - $( - Self::$name => &sizes.$name, - )* - } - } - - pub fn name(self) -> &'static str { - match self { - $( - Self::$name => stringify!($name), - )* - } - } - } - - impl ThemeSizes { - pub fn reset(&self) { - let default = Self::default(); - $( - self.$name.val.set(default.$name.val.get()); - self.$name.set.set(false); - )* - } - } - - impl Default for ThemeSizes { - fn default() -> Self { - Self { - $( - $name: ThemeSize { - val: Cell::new($def), - set: Cell::new(false), - }, - )* - } - } - } - } -} - -impl ThemeSizes { - pub fn bar_height(&self) -> i32 { - if self.bar_height.set.get() { - self.bar_height.val.get() - } else { - self.title_height.val.get() - } - } - - pub fn bar_separator_width(&self) -> i32 { - self.bar_separator_width.get() - } -} - -sizes! { - title_height = (0, 1000, 17), - bar_height = (0, 1000, 17), - border_width = (0, 1000, 4), - bar_separator_width = (0, 1000, 1), - gap = (0, 1000, 0), - title_gap = (0, 1000, 5), - tab_bar_height = (0, 1000, 22), - tab_bar_padding = (0, 1000, 6), - tab_bar_radius = (0, 1000, 6), - tab_bar_border_width = (0, 1000, 2), - tab_bar_text_padding = (0, 1000, 4), - tab_bar_gap = (0, 1000, 4), -} - -impl StaticText for ThemeSized { - fn text(&self) -> &'static str { - match self { - ThemeSized::title_height => "Title Height", - ThemeSized::bar_height => "Bar Height", - ThemeSized::border_width => "Border Width", - ThemeSized::bar_separator_width => "Bar Separator Width", - ThemeSized::gap => "Gap", - ThemeSized::title_gap => "Title Gap", - ThemeSized::tab_bar_height => "Tab Bar Height", - ThemeSized::tab_bar_padding => "Tab Bar Padding", - ThemeSized::tab_bar_radius => "Tab Bar Radius", - ThemeSized::tab_bar_border_width => "Tab Bar Border Width", - ThemeSized::tab_bar_text_padding => "Tab Bar Text Padding", - ThemeSized::tab_bar_gap => "Tab Bar Gap", - } - } -} - -pub const DEFAULT_FONT: &str = "monospace 8"; - -#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Default, Linearize)] -pub enum BarPosition { - #[default] - Top, - Bottom, -} - -impl StaticText for BarPosition { - fn text(&self) -> &'static str { - match self { - BarPosition::Top => "Top", - BarPosition::Bottom => "Bottom", - } - } -} - -impl TryFrom for BarPosition { - type Error = (); - - fn try_from(value: ConfigBarPosition) -> Result { - let v = match value { - ConfigBarPosition::Top => Self::Top, - ConfigBarPosition::Bottom => Self::Bottom, - _ => return Err(()), - }; - Ok(v) - } -} - -impl Into for BarPosition { - fn into(self) -> ConfigBarPosition { - match self { - BarPosition::Top => ConfigBarPosition::Top, - BarPosition::Bottom => ConfigBarPosition::Bottom, - } - } -} - -/// Per-corner radius for rounded rectangles. -/// -/// Each field specifies the radius (in logical pixels) for one corner. -/// A radius of 0 means a square corner. -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub struct CornerRadius { - pub top_left: f32, - pub top_right: f32, - pub bottom_right: f32, - pub bottom_left: f32, -} - -impl From for CornerRadius { - fn from(value: f32) -> Self { - Self { - top_left: value, - top_right: value, - bottom_right: value, - bottom_left: value, - } - } -} - -impl From for [f32; 4] { - fn from(cr: CornerRadius) -> Self { - [cr.top_left, cr.top_right, cr.bottom_right, cr.bottom_left] - } -} - -impl CornerRadius { - /// Shrink or grow all radii by `width`. Radii that are 0 stay 0 (square - /// corners remain square). Negative `width` shrinks; the result is clamped - /// to 0. - pub fn expanded_by(mut self, width: f32) -> Self { - if self.top_left > 0.0 { - self.top_left = (self.top_left + width).max(0.0); - } - if self.top_right > 0.0 { - self.top_right = (self.top_right + width).max(0.0); - } - if self.bottom_right > 0.0 { - self.bottom_right = (self.bottom_right + width).max(0.0); - } - if self.bottom_left > 0.0 { - self.bottom_left = (self.bottom_left + width).max(0.0); - } - self - } - - /// Scale all radii by a factor (e.g. for HiDPI). - pub fn scaled_by(self, scale: f32) -> Self { - Self { - top_left: self.top_left * scale, - top_right: self.top_right * scale, - bottom_right: self.bottom_right * scale, - bottom_left: self.bottom_left * scale, - } - } - - /// Reduce all radii proportionally so that adjacent corners don't overlap, - /// following the CSS spec algorithm. - pub fn fit_to(self, width: f32, height: f32) -> Self { - let reduction = f32::min( - f32::min( - width / (self.top_left + self.top_right), - width / (self.bottom_left + self.bottom_right), - ), - f32::min( - height / (self.top_left + self.bottom_left), - height / (self.top_right + self.bottom_right), - ), - ); - let reduction = f32::min(1.0, reduction); - Self { - top_left: self.top_left * reduction, - top_right: self.top_right * reduction, - bottom_right: self.bottom_right * reduction, - bottom_left: self.bottom_left * reduction, - } - } - - pub fn is_zero(&self) -> bool { - self.top_left == 0.0 - && self.top_right == 0.0 - && self.bottom_right == 0.0 - && self.bottom_left == 0.0 - } -} - -/// Horizontal alignment of title text inside tab buttons. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum TabTitleAlign { - #[default] - Start, - Center, - End, -} - -pub struct Theme { - pub colors: ThemeColors, - pub sizes: ThemeSizes, - pub font: CloneCell>, - pub bar_font: CloneCell>>, - pub title_font: CloneCell>>, - pub default_font: Arc, - #[allow(dead_code)] - pub show_titles: Cell, - #[allow(dead_code)] - pub floating_titles: Cell, - pub bar_position: Cell, - pub corner_radius: Cell, - pub autotile_enabled: Cell, - pub tab_title_align: Cell, -} - -impl Default for Theme { - fn default() -> Self { - let default_font = Arc::new(DEFAULT_FONT.to_string()); - Self { - colors: Default::default(), - sizes: Default::default(), - font: CloneCell::new(default_font.clone()), - bar_font: Default::default(), - title_font: Default::default(), - default_font, - show_titles: Cell::new(true), - floating_titles: Cell::new(false), - bar_position: Default::default(), - corner_radius: Cell::new(CornerRadius::default()), - autotile_enabled: Cell::new(false), - tab_title_align: Cell::new(TabTitleAlign::default()), - } - } -} - -impl Theme { - pub fn bar_font(&self) -> Arc { - self.bar_font.get().unwrap_or_else(|| self.font.get()) - } - - pub fn title_font(&self) -> Arc { - self.title_font.get().unwrap_or_else(|| self.font.get()) - } - - pub fn title_height(&self) -> i32 { - 0 - } - - pub fn title_plus_underline_height(&self) -> i32 { - 0 - } -} - -#[derive(Copy, Clone, Debug)] -pub struct Oklch { - pub l: f32, - pub c: f32, - pub h: f32, -} - -#[derive(Copy, Clone, Debug)] -pub struct Oklab { - pub l: f32, - pub a: f32, - pub b: f32, -} - -impl Oklab { - pub fn to_srgb(self) -> Color { - let l_ = self.l + 0.3963377774 * self.a + 0.2158037573 * self.b; - let m_ = self.l - 0.1055613458 * self.a - 0.0638541728 * self.b; - let s_ = self.l - 0.0894841775 * self.a - 1.2914855480 * self.b; - - let l = l_ * l_ * l_; - let m = m_ * m_ * m_; - let s = s_ * s_ * s_; - - let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s; - let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s; - let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s; - - Color::new( - Eotf::Linear, - AlphaMode::PremultipliedElectrical, - r, - g, - b, - 1.0, - ) - } - - pub fn to_oklch(self) -> Oklch { - let c = (self.a * self.a + self.b * self.b).sqrt(); - let h = self.b.atan2(self.a); - - Oklch { l: self.l, c, h } - } -} - -impl Oklch { - pub fn to_oklab(self) -> Oklab { - let a = self.c * self.h.cos(); - let b = self.c * self.h.sin(); - - Oklab { l: self.l, a, b } - } -} - -impl Add for Oklab { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Self { - l: self.l + rhs.l, - a: self.a + rhs.a, - b: self.b + rhs.b, - } - } -} - -impl Mul for Oklab { - type Output = Self; - - fn mul(self, rhs: f32) -> Self::Output { - Self { - l: self.l * rhs, - a: self.a * rhs, - b: self.b * rhs, - } - } -} - -impl Div for Oklab { - type Output = Self; - - fn div(self, rhs: f32) -> Self::Output { - Self { - l: self.l / rhs, - a: self.a / rhs, - b: self.b / rhs, - } - } -} +pub use jay_theme::*; diff --git a/theme/Cargo.toml b/theme/Cargo.toml new file mode 100644 index 00000000..b02d32fd --- /dev/null +++ b/theme/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "jay-theme" +version = "0.1.0" +edition = "2024" +license = "GPL-3.0-only" + +[dependencies] +jay-cmm = { version = "0.1.0", path = "../cmm" } +jay-config = { version = "1.10.0", path = "../jay-config" } +jay-gfx-types = { version = "0.1.0", path = "../gfx-types" } +jay-utils = { version = "0.1.0", path = "../utils" } + +linearize = { version = "0.1.3", features = ["derive"] } +num-traits = "0.2.17" diff --git a/theme/src/lib.rs b/theme/src/lib.rs new file mode 100644 index 00000000..94fde7b4 --- /dev/null +++ b/theme/src/lib.rs @@ -0,0 +1,916 @@ +#![expect(clippy::excessive_precision)] + +use { + jay_cmm::cmm_eotf::{Eotf, bt1886_eotf_args, bt1886_inv_eotf_args}, + jay_config::theme::BarPosition as ConfigBarPosition, + jay_gfx_types::AlphaMode, + jay_utils::{clonecell::CloneCell, static_text::StaticText}, + linearize::Linearize, + num_traits::Float, + std::{ + cell::Cell, + cmp::Ordering, + ops::{Add, Div, Mul}, + sync::Arc, + }, +}; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Color { + r: f32, + g: f32, + b: f32, + a: f32, +} + +impl Eq for Color {} + +impl Ord for Color { + fn cmp(&self, other: &Self) -> Ordering { + self.r + .total_cmp(&other.r) + .then_with(|| self.g.total_cmp(&other.g)) + .then_with(|| self.b.total_cmp(&other.b)) + .then_with(|| self.a.total_cmp(&other.a)) + } +} + +impl Mul for Color { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self { + r: self.r * rhs, + g: self.g * rhs, + b: self.b * rhs, + a: self.a * rhs, + } + } +} + +impl PartialOrd for Color { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +fn to_f32(c: u8) -> f32 { + c as f32 / 255f32 +} + +fn to_u8(c: f32) -> u8 { + (c * 255f32).round() as u8 +} + +impl Color { + pub const TRANSPARENT: Self = Self { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }; + + pub const SOLID_BLACK: Self = Self { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }; + + pub fn new( + eotf: Eotf, + alpha_mode: AlphaMode, + mut r: f32, + mut g: f32, + mut b: f32, + a: f32, + ) -> Self { + if eotf == Eotf::Linear { + if alpha_mode == AlphaMode::Straight && a < 1.0 { + for c in [&mut r, &mut g, &mut b] { + *c *= a; + } + } + return Self { r, g, b, a }; + } + if alpha_mode == AlphaMode::PremultipliedElectrical && a < 1.0 && a > 0.0 { + for c in [&mut r, &mut g, &mut b] { + *c /= a; + } + } + #[inline(always)] + fn linear(c: f32) -> f32 { + c + } + fn st2084_pq(c: f32) -> f32 { + let cp = c.powf(1.0 / 78.84375); + let num = (cp - 0.8359375).max(0.0); + let den = 18.8515625 - 18.6875 * cp; + (num / den).powf(1.0 / 0.1593017578125) + } + fn st240(c: f32) -> f32 { + if c < 0.0913 { + c / 4.0 + } else { + ((c + 0.1115) / 1.1115).powf(1.0 / 0.45) + } + } + fn log100(c: f32) -> f32 { + 10.0.powf(2.0 * (c - 1.0)) + } + fn log316(c: f32) -> f32 { + 10.0.powf(2.5 * (c - 1.0)) + } + fn st428(c: f32) -> f32 { + c.powf(2.6) * 52.37 / 48.0 + } + fn gamma22(c: f32) -> f32 { + c.signum() * c.abs().powf(2.2) + } + fn gamma24(c: f32) -> f32 { + c.signum() * c.abs().powf(2.4) + } + fn gamma28(c: f32) -> f32 { + c.signum() * c.abs().powf(2.8) + } + fn compound_power_2_4(c: f32) -> f32 { + if c < 0.04045 { + c / 12.92 + } else { + ((c + 0.055) / 1.055).powf(2.4) + } + } + macro_rules! convert { + ($tf:ident) => {{ + r = $tf(r); + g = $tf(g); + b = $tf(b); + }}; + } + match eotf { + Eotf::Linear => convert!(linear), + Eotf::St2084Pq => convert!(st2084_pq), + Eotf::Bt1886(c) => { + let [a1, a2, a3, a4] = bt1886_eotf_args(c); + let bt1886 = |c: f32| -> f32 { a1 * ((a2 * c + a3).powf(2.4) - a4) }; + convert!(bt1886) + } + Eotf::Gamma22 => convert!(gamma22), + Eotf::Gamma24 => convert!(gamma24), + Eotf::Gamma28 => convert!(gamma28), + Eotf::St240 => convert!(st240), + Eotf::Log100 => convert!(log100), + Eotf::Log316 => convert!(log316), + Eotf::St428 => convert!(st428), + Eotf::Pow(n) => { + let e = n.eotf_f32(); + let pow = |c: f32| -> f32 { c.signum() * c.abs().powf(e) }; + convert!(pow) + } + Eotf::CompoundPower24 => convert!(compound_power_2_4), + } + if alpha_mode != AlphaMode::PremultipliedOptical && a < 1.0 { + for c in [&mut r, &mut g, &mut b] { + *c *= a; + } + } + Self { r, g, b, a } + } + + pub fn is_opaque(&self) -> bool { + self.a >= 1.0 + } + + pub fn from_gray_srgb(g: u8) -> Self { + Self::from_srgb(g, g, g) + } + + pub fn from_srgb(r: u8, g: u8, b: u8) -> Self { + Self::new( + Eotf::Gamma22, + AlphaMode::PremultipliedOptical, + to_f32(r), + to_f32(g), + to_f32(b), + 1.0, + ) + } + + pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { + Self::new( + Eotf::Gamma22, + AlphaMode::PremultipliedElectrical, + to_f32(r), + to_f32(g), + to_f32(b), + to_f32(a), + ) + } + + pub fn from_u32(eotf: Eotf, alpha_mode: AlphaMode, r: u32, g: u32, b: u32, a: u32) -> Self { + fn to_f32(c: u32) -> f32 { + ((c as f64) / (u32::MAX as f64)) as f32 + } + Self::new(eotf, alpha_mode, to_f32(r), to_f32(g), to_f32(b), to_f32(a)) + } + + pub fn from_srgba_straight(r: u8, g: u8, b: u8, a: u8) -> Self { + Self::new( + Eotf::Gamma22, + AlphaMode::Straight, + to_f32(r), + to_f32(g), + to_f32(b), + to_f32(a), + ) + } + + pub fn to_srgba_premultiplied(self) -> [u8; 4] { + let [r, g, b, a] = self.to_array(Eotf::Gamma22); + [to_u8(r), to_u8(g), to_u8(b), to_u8(a)] + } + + pub fn to_array(self, eotf: Eotf) -> [f32; 4] { + self.to_array2(eotf, None) + } + + pub fn to_array2(self, eotf: Eotf, alpha: Option) -> [f32; 4] { + let mut res = [self.r, self.g, self.b, self.a]; + fn linear(c: f32) -> f32 { + c + } + fn st2084_pq(c: f32) -> f32 { + let c = c.clamp(0.0, 1.0); + let num = 0.8359375 + 18.8515625 * c.powf(0.1593017578125); + let den = 1.0 + 18.6875 * c.powf(0.1593017578125); + (num / den).powf(78.84375) + } + fn st240(c: f32) -> f32 { + if c < 0.0228 { + 4.0 * c + } else { + 1.1115 * c.powf(0.45) - 0.1115 + } + } + fn log100(c: f32) -> f32 { + let c = c.clamp(0.0, 1.0); + if c < 0.01 { 0.0 } else { 1.0 + c.log10() / 2.0 } + } + fn log316(c: f32) -> f32 { + let c = c.clamp(0.0, 1.0); + if c < 10.0.sqrt() / 1000.0 { + 0.0 + } else { + 1.0 + c.log10() / 2.5 + } + } + fn st428(c: f32) -> f32 { + (48.0 * c / 52.37).powf(1.0 / 2.6) + } + fn gamma22(c: f32) -> f32 { + c.signum() * c.abs().powf(1.0 / 2.2) + } + fn gamma24(c: f32) -> f32 { + c.signum() * c.abs().powf(1.0 / 2.4) + } + fn gamma28(c: f32) -> f32 { + c.signum() * c.abs().powf(1.0 / 2.8) + } + fn compound_power_2_4(c: f32) -> f32 { + if c < 0.0031308 { + 12.92 * c + } else { + 1.055 * c.powf(1.0 / 2.4) - 0.055 + } + } + macro_rules! convert { + ($tf:ident) => {{ + for c in &mut res[..3] { + *c = $tf(*c); + } + }}; + } + if eotf != Eotf::Linear { + if self.a < 1.0 && self.a > 0.0 { + for c in &mut res[..3] { + *c /= self.a; + } + } + match eotf { + Eotf::Linear => convert!(linear), + Eotf::St2084Pq => convert!(st2084_pq), + Eotf::Bt1886(c) => { + let [a1, a2, a3, a4] = bt1886_inv_eotf_args(c); + let bt1886 = |c: f32| -> f32 { a1 * ((a2 * c + a3).powf(1.0 / 2.4) - a4) }; + convert!(bt1886) + } + Eotf::Gamma22 => convert!(gamma22), + Eotf::Gamma24 => convert!(gamma24), + Eotf::Gamma28 => convert!(gamma28), + Eotf::St240 => convert!(st240), + Eotf::Log100 => convert!(log100), + Eotf::Log316 => convert!(log316), + Eotf::St428 => convert!(st428), + Eotf::Pow(n) => { + let e = n.inv_eotf_f32(); + let pow = |c: f32| -> f32 { c.signum() * c.abs().powf(e) }; + convert!(pow) + } + Eotf::CompoundPower24 => convert!(compound_power_2_4), + } + if self.a < 1.0 { + for c in &mut res[..3] { + *c *= self.a; + } + } + } + if let Some(a) = alpha { + for c in &mut res { + *c *= a; + } + } + res + } + + pub fn and_then(self, other: &Color) -> Color { + Color { + r: self.r * (1.0 - other.a) + other.r, + g: self.g * (1.0 - other.a) + other.g, + b: self.b * (1.0 - other.a) + other.b, + a: self.a * (1.0 - other.a) + other.a, + } + } + + pub fn srgb_to_oklab(self) -> Oklab { + if self.a == 0.0 { + return Oklab { + l: 0.0, + a: 0.0, + b: 0.0, + }; + } + + let [r, g, b, _] = self.to_array2(Eotf::Linear, Some(1.0 / self.a)); + + let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b; + let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b; + let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b; + + let l_ = l.cbrt(); + let m_ = m.cbrt(); + let s_ = s.cbrt(); + + let l = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_; + let a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_; + let b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_; + + Oklab { l, a, b } + } +} + +impl From for Color { + fn from(f: jay_config::theme::Color) -> Self { + let [r, g, b, a] = f.to_f32_premultiplied(); + Self::new( + Eotf::Gamma22, + AlphaMode::PremultipliedElectrical, + r, + g, + b, + a, + ) + } +} + +macro_rules! colors { + ($($name:ident = $colors:tt,)*) => { + pub struct ThemeColors { + $( + pub $name: Cell, + )* + } + + #[derive(Copy, Clone, Debug, Linearize)] + #[expect(non_camel_case_types)] + pub enum ThemeColor { + $( + $name, + )* + } + + impl ThemeColor { + pub fn field(self, theme: &Theme) -> &Cell { + let colors = &theme.colors; + match self { + $( + Self::$name => &colors.$name, + )* + } + } + } + + impl ThemeColors { + pub fn reset(&self) { + let default = Self::default(); + $( + self.$name.set(default.$name.get()); + )* + } + } + + impl Default for ThemeColors { + fn default() -> Self { + Self { + $( + $name: Cell::new(colors!(@colors $colors)), + )* + } + } + } + }; + (@colors ($r:expr, $g:expr, $b:expr)) => { + Color::from_srgb($r, $g, $b) + }; + (@colors ($r:expr, $g:expr, $b:expr, $a:expr)) => { + Color::from_srgba_straight($r, $g, $b, $a) + }; +} + +colors! { + background = (0x00, 0x10, 0x19), + unfocused_title_background = (0x22, 0x22, 0x22), + focused_title_background = (0x28, 0x55, 0x77), + captured_unfocused_title_background = (0x22, 0x03, 0x03), + captured_focused_title_background = (0x77, 0x28, 0x31), + focused_inactive_title_background = (0x5f, 0x67, 0x6a), + unfocused_title_text = (0x88, 0x88, 0x88), + focused_title_text = (0xff, 0xff, 0xff), + focused_inactive_title_text = (0xff, 0xff, 0xff), + separator = (0x33, 0x33, 0x33), + border = (0x3f, 0x47, 0x4a), + active_border = (0x28, 0x55, 0x77), + bar_background = (0x00, 0x00, 0x00), + bar_text = (0xff, 0xff, 0xff), + attention_requested_background = (0x23, 0x09, 0x2c), + highlight = (0x9d, 0x28, 0xc6, 0x7f), + tab_active_background = (0x4c, 0x78, 0x99), + tab_active_border = (0x28, 0x55, 0x77), + tab_inactive_background = (0x22, 0x22, 0x22), + tab_inactive_border = (0x33, 0x33, 0x33), + tab_active_text = (0xff, 0xff, 0xff), + tab_inactive_text = (0x88, 0x88, 0x88), + tab_bar_background = (0x00, 0x00, 0x00, 0x00), + tab_attention_background = (0x23, 0x09, 0x2c), +} + +impl StaticText for ThemeColor { + fn text(&self) -> &'static str { + match self { + ThemeColor::background => "Background", + ThemeColor::unfocused_title_background => "Title Background (unfocused)", + ThemeColor::focused_title_background => "Title Background (focused)", + ThemeColor::captured_unfocused_title_background => { + "Title Background (unfocused, captured)" + } + ThemeColor::captured_focused_title_background => "Title Background (focused, captured)", + ThemeColor::focused_inactive_title_background => "Title Background (focused, inactive)", + ThemeColor::unfocused_title_text => "Title Text (unfocused)", + ThemeColor::focused_title_text => "Title Text (focused)", + ThemeColor::focused_inactive_title_text => "Title Text (focused, inactive)", + ThemeColor::separator => "Separator", + ThemeColor::border => "Border", + ThemeColor::active_border => "Border (active)", + ThemeColor::bar_background => "Bar Background", + ThemeColor::bar_text => "Bar Text", + ThemeColor::attention_requested_background => "Attention Requested", + ThemeColor::highlight => "Highlight", + ThemeColor::tab_active_background => "Tab Background (active)", + ThemeColor::tab_active_border => "Tab Border (active)", + ThemeColor::tab_inactive_background => "Tab Background (inactive)", + ThemeColor::tab_inactive_border => "Tab Border (inactive)", + ThemeColor::tab_active_text => "Tab Text (active)", + ThemeColor::tab_inactive_text => "Tab Text (inactive)", + ThemeColor::tab_bar_background => "Tab Bar Background", + ThemeColor::tab_attention_background => "Tab Attention Background", + } + } +} + +pub struct ThemeSize { + pub val: Cell, + pub set: Cell, +} + +impl ThemeSize { + pub fn get(&self) -> i32 { + self.val.get() + } +} + +macro_rules! sizes { + ($($name:ident = ($min:expr, $max:expr, $def:expr),)*) => { + pub struct ThemeSizes { + $( + pub $name: ThemeSize, + )* + } + + #[derive(Copy, Clone, Debug, Linearize)] + #[expect(non_camel_case_types)] + pub enum ThemeSized { + $( + $name, + )* + } + + impl ThemeSized { + pub fn min(self) -> i32 { + match self { + $( + Self::$name => $min, + )* + } + } + + pub fn max(self) -> i32 { + match self { + $( + Self::$name => $max, + )* + } + } + + pub fn field(self, theme: &Theme) -> &ThemeSize { + let sizes = &theme.sizes; + match self { + $( + Self::$name => &sizes.$name, + )* + } + } + + pub fn name(self) -> &'static str { + match self { + $( + Self::$name => stringify!($name), + )* + } + } + } + + impl ThemeSizes { + pub fn reset(&self) { + let default = Self::default(); + $( + self.$name.val.set(default.$name.val.get()); + self.$name.set.set(false); + )* + } + } + + impl Default for ThemeSizes { + fn default() -> Self { + Self { + $( + $name: ThemeSize { + val: Cell::new($def), + set: Cell::new(false), + }, + )* + } + } + } + } +} + +impl ThemeSizes { + pub fn bar_height(&self) -> i32 { + if self.bar_height.set.get() { + self.bar_height.val.get() + } else { + self.title_height.val.get() + } + } + + pub fn bar_separator_width(&self) -> i32 { + self.bar_separator_width.get() + } +} + +sizes! { + title_height = (0, 1000, 17), + bar_height = (0, 1000, 17), + border_width = (0, 1000, 4), + bar_separator_width = (0, 1000, 1), + gap = (0, 1000, 0), + title_gap = (0, 1000, 5), + tab_bar_height = (0, 1000, 22), + tab_bar_padding = (0, 1000, 6), + tab_bar_radius = (0, 1000, 6), + tab_bar_border_width = (0, 1000, 2), + tab_bar_text_padding = (0, 1000, 4), + tab_bar_gap = (0, 1000, 4), +} + +impl StaticText for ThemeSized { + fn text(&self) -> &'static str { + match self { + ThemeSized::title_height => "Title Height", + ThemeSized::bar_height => "Bar Height", + ThemeSized::border_width => "Border Width", + ThemeSized::bar_separator_width => "Bar Separator Width", + ThemeSized::gap => "Gap", + ThemeSized::title_gap => "Title Gap", + ThemeSized::tab_bar_height => "Tab Bar Height", + ThemeSized::tab_bar_padding => "Tab Bar Padding", + ThemeSized::tab_bar_radius => "Tab Bar Radius", + ThemeSized::tab_bar_border_width => "Tab Bar Border Width", + ThemeSized::tab_bar_text_padding => "Tab Bar Text Padding", + ThemeSized::tab_bar_gap => "Tab Bar Gap", + } + } +} + +pub const DEFAULT_FONT: &str = "monospace 8"; + +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Default, Linearize)] +pub enum BarPosition { + #[default] + Top, + Bottom, +} + +impl StaticText for BarPosition { + fn text(&self) -> &'static str { + match self { + BarPosition::Top => "Top", + BarPosition::Bottom => "Bottom", + } + } +} + +impl TryFrom for BarPosition { + type Error = (); + + fn try_from(value: ConfigBarPosition) -> Result { + let v = match value { + ConfigBarPosition::Top => Self::Top, + ConfigBarPosition::Bottom => Self::Bottom, + _ => return Err(()), + }; + Ok(v) + } +} + +impl Into for BarPosition { + fn into(self) -> ConfigBarPosition { + match self { + BarPosition::Top => ConfigBarPosition::Top, + BarPosition::Bottom => ConfigBarPosition::Bottom, + } + } +} + +/// Per-corner radius for rounded rectangles. +/// +/// Each field specifies the radius (in logical pixels) for one corner. +/// A radius of 0 means a square corner. +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct CornerRadius { + pub top_left: f32, + pub top_right: f32, + pub bottom_right: f32, + pub bottom_left: f32, +} + +impl From for CornerRadius { + fn from(value: f32) -> Self { + Self { + top_left: value, + top_right: value, + bottom_right: value, + bottom_left: value, + } + } +} + +impl From for [f32; 4] { + fn from(cr: CornerRadius) -> Self { + [cr.top_left, cr.top_right, cr.bottom_right, cr.bottom_left] + } +} + +impl CornerRadius { + /// Shrink or grow all radii by `width`. Radii that are 0 stay 0 (square + /// corners remain square). Negative `width` shrinks; the result is clamped + /// to 0. + pub fn expanded_by(mut self, width: f32) -> Self { + if self.top_left > 0.0 { + self.top_left = (self.top_left + width).max(0.0); + } + if self.top_right > 0.0 { + self.top_right = (self.top_right + width).max(0.0); + } + if self.bottom_right > 0.0 { + self.bottom_right = (self.bottom_right + width).max(0.0); + } + if self.bottom_left > 0.0 { + self.bottom_left = (self.bottom_left + width).max(0.0); + } + self + } + + /// Scale all radii by a factor (e.g. for HiDPI). + pub fn scaled_by(self, scale: f32) -> Self { + Self { + top_left: self.top_left * scale, + top_right: self.top_right * scale, + bottom_right: self.bottom_right * scale, + bottom_left: self.bottom_left * scale, + } + } + + /// Reduce all radii proportionally so that adjacent corners don't overlap, + /// following the CSS spec algorithm. + pub fn fit_to(self, width: f32, height: f32) -> Self { + let reduction = f32::min( + f32::min( + width / (self.top_left + self.top_right), + width / (self.bottom_left + self.bottom_right), + ), + f32::min( + height / (self.top_left + self.bottom_left), + height / (self.top_right + self.bottom_right), + ), + ); + let reduction = f32::min(1.0, reduction); + Self { + top_left: self.top_left * reduction, + top_right: self.top_right * reduction, + bottom_right: self.bottom_right * reduction, + bottom_left: self.bottom_left * reduction, + } + } + + pub fn is_zero(&self) -> bool { + self.top_left == 0.0 + && self.top_right == 0.0 + && self.bottom_right == 0.0 + && self.bottom_left == 0.0 + } +} + +/// Horizontal alignment of title text inside tab buttons. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum TabTitleAlign { + #[default] + Start, + Center, + End, +} + +pub struct Theme { + pub colors: ThemeColors, + pub sizes: ThemeSizes, + pub font: CloneCell>, + pub bar_font: CloneCell>>, + pub title_font: CloneCell>>, + pub default_font: Arc, + #[allow(dead_code)] + pub show_titles: Cell, + #[allow(dead_code)] + pub floating_titles: Cell, + pub bar_position: Cell, + pub corner_radius: Cell, + pub autotile_enabled: Cell, + pub tab_title_align: Cell, +} + +impl Default for Theme { + fn default() -> Self { + let default_font = Arc::new(DEFAULT_FONT.to_string()); + Self { + colors: Default::default(), + sizes: Default::default(), + font: CloneCell::new(default_font.clone()), + bar_font: Default::default(), + title_font: Default::default(), + default_font, + show_titles: Cell::new(true), + floating_titles: Cell::new(false), + bar_position: Default::default(), + corner_radius: Cell::new(CornerRadius::default()), + autotile_enabled: Cell::new(false), + tab_title_align: Cell::new(TabTitleAlign::default()), + } + } +} + +impl Theme { + pub fn bar_font(&self) -> Arc { + self.bar_font.get().unwrap_or_else(|| self.font.get()) + } + + pub fn title_font(&self) -> Arc { + self.title_font.get().unwrap_or_else(|| self.font.get()) + } + + pub fn title_height(&self) -> i32 { + 0 + } + + pub fn title_plus_underline_height(&self) -> i32 { + 0 + } +} + +#[derive(Copy, Clone, Debug)] +pub struct Oklch { + pub l: f32, + pub c: f32, + pub h: f32, +} + +#[derive(Copy, Clone, Debug)] +pub struct Oklab { + pub l: f32, + pub a: f32, + pub b: f32, +} + +impl Oklab { + pub fn to_srgb(self) -> Color { + let l_ = self.l + 0.3963377774 * self.a + 0.2158037573 * self.b; + let m_ = self.l - 0.1055613458 * self.a - 0.0638541728 * self.b; + let s_ = self.l - 0.0894841775 * self.a - 1.2914855480 * self.b; + + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + + let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s; + let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s; + let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s; + + Color::new( + Eotf::Linear, + AlphaMode::PremultipliedElectrical, + r, + g, + b, + 1.0, + ) + } + + pub fn to_oklch(self) -> Oklch { + let c = (self.a * self.a + self.b * self.b).sqrt(); + let h = self.b.atan2(self.a); + + Oklch { l: self.l, c, h } + } +} + +impl Oklch { + pub fn to_oklab(self) -> Oklab { + let a = self.c * self.h.cos(); + let b = self.c * self.h.sin(); + + Oklab { l: self.l, a, b } + } +} + +impl Add for Oklab { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self { + l: self.l + rhs.l, + a: self.a + rhs.a, + b: self.b + rhs.b, + } + } +} + +impl Mul for Oklab { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self { + l: self.l * rhs, + a: self.a * rhs, + b: self.b * rhs, + } + } +} + +impl Div for Oklab { + type Output = Self; + + fn div(self, rhs: f32) -> Self::Output { + Self { + l: self.l / rhs, + a: self.a / rhs, + b: self.b / rhs, + } + } +}