From 91924466020ac817998507b86208b79b96ad9bb7 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 24 Apr 2025 11:40:47 +0200 Subject: [PATCH] icons: add icon infrastructure --- Cargo.lock | 44 +++++++ Cargo.toml | 1 + src/compositor.rs | 1 + src/config/handler.rs | 2 + src/gfx_api.rs | 2 + src/icons.rs | 284 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/renderer.rs | 3 + src/state.rs | 5 + src/theme.rs | 3 - 10 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 src/icons.rs diff --git a/Cargo.lock b/Cargo.lock index d53d97ff..1b7a9e56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,12 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" @@ -183,6 +189,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" + [[package]] name = "byteorder" version = "1.5.0" @@ -597,6 +609,7 @@ dependencies = [ "shaderc", "smallvec", "thiserror", + "tiny-skia", "tracy-client-sys", "uapi", ] @@ -1245,6 +1258,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "strsim" version = "0.11.1" @@ -1345,6 +1364,31 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinyvec" version = "1.8.1" diff --git a/Cargo.toml b/Cargo.toml index 86156da9..dd43f8d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ png = "0.17.13" rustc-demangle = { version = "0.1.24", optional = true } tracy-client-sys = { version = "0.24.1", features = ["ondemand", "manual-lifetime", "debuginfod", "demangle"], optional = true } kbvm = "0.1.4" +tiny-skia = { version = "0.11.4", default-features = false, features = ["std"] } [build-dependencies] repc = "0.1.1" diff --git a/src/compositor.rs b/src/compositor.rs index 82b5f8bb..f9d43f92 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -288,6 +288,7 @@ fn start_compositor2( color_management_enabled: Cell::new(false), color_manager, float_above_fullscreen: Cell::new(false), + icons: Default::default(), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); diff --git a/src/config/handler.rs b/src/config/handler.rs index 847a9fdc..d4accbe9 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1545,6 +1545,7 @@ impl ConfigProxyHandler { } self.state.root.clone().node_visit(&mut V); self.state.damage(self.state.root.extents.get()); + self.state.icons.update_sizes(&self.state); } fn colors_changed(&self) { @@ -1561,6 +1562,7 @@ impl ConfigProxyHandler { } self.state.root.clone().node_visit(&mut V); self.state.damage(self.state.root.extents.get()); + self.state.icons.clear(); } fn get_sized(&self, sized: Resizable) -> Result { diff --git a/src/gfx_api.rs b/src/gfx_api.rs index e442d4a0..303b63e9 100644 --- a/src/gfx_api.rs +++ b/src/gfx_api.rs @@ -618,6 +618,7 @@ impl dyn GfxFramebuffer { let (width, height) = self.logical_size(transform); Rect::new(0, 0, width, height).unwrap() }, + icons: None, }; cursor.render_hardware_cursor(&mut renderer); self.render( @@ -906,6 +907,7 @@ pub fn create_render_pass( let (width, height) = logical_size(physical_size, transform); Rect::new(0, 0, width, height).unwrap() }, + icons: state.icons.get(state, scale), }; node.node_render(&mut renderer, 0, 0, None); if let Some(rect) = cursor_rect { diff --git a/src/icons.rs b/src/icons.rs new file mode 100644 index 00000000..3bcda66c --- /dev/null +++ b/src/icons.rs @@ -0,0 +1,284 @@ +#![allow(clippy::excessive_precision)] + +use { + crate::{ + cmm::cmm_transfer_function::TransferFunction, + format::ARGB8888, + gfx_api::{GfxContext, GfxError, GfxTexture}, + scale::Scale, + state::State, + theme::Theme, + utils::{copyhashmap::CopyHashMap, windows::WindowsExt}, + }, + ahash::AHashSet, + linearize::{Linearize, StaticMap, static_map}, + std::{cell::Cell, f32::consts::PI, mem, rc::Rc, sync::LazyLock}, + thiserror::Error, + tiny_skia::{Color, FillRule, Paint, Path, PathBuilder, Pixmap, Transform}, +}; + +#[derive(Default)] +pub struct Icons { + icons: CopyHashMap>>, +} + +#[derive(Copy, Clone, Debug, Linearize)] +pub enum IconState { + Active, + Passive, +} + +#[expect(dead_code)] +pub struct SizedIcons { + pub pin_unfocused_title: StaticMap>, + pub pin_focused_title: StaticMap>, + pub pin_attention_requested: StaticMap>, +} + +#[derive(Debug, Error)] +pub enum IconsError { + #[error("Could not create a pixmap")] + CreatePixmap, + #[error("The requested icons size is non-positive")] + NonPositiveSize, + #[error("There is no gfx context")] + NoRenderContext, + #[error("Could not create texture")] + CreateTexture(#[source] GfxError), +} + +impl Icons { + pub fn update_sizes(&self, state: &State) { + let mut sizes = AHashSet::new(); + let height = state.theme.sizes.title_height.get(); + for &(scale, _) in &*state.scales.lock() { + let [size] = scale.pixel_size([height]); + if size > 0 { + sizes.insert(size); + } + } + self.icons.lock().retain(|size, _| sizes.contains(size)); + } + + pub fn clear(&self) { + self.icons.clear(); + } + + pub fn get(&self, state: &State, scale: Scale) -> Option> { + let [size] = scale.pixel_size([state.theme.sizes.title_height.get()]); + if let Some(icons) = self.icons.get(&size) { + return icons; + } + let icons = match self.create(state, size) { + Ok(i) => Some(i), + Err(e) => { + log::error!("Could not create icons: {}", e); + None + } + }; + self.icons.set(size, icons.clone()); + icons + } + + fn create(&self, state: &State, size: i32) -> Result, IconsError> { + let Some(ctx) = state.render_ctx.get() else { + return Err(IconsError::NoRenderContext); + }; + Ok(Rc::new(create_icons(size, &state.theme, &ctx)?)) + } +} + +pub fn create_icons( + size: i32, + theme: &Theme, + ctx: &Rc, +) -> Result { + if size <= 0 { + return Err(IconsError::NonPositiveSize); + } + let size = size as u32; + + let create_pins = |color: crate::theme::Color| { + let create_pin = |color: Color| { + let mut paint = Paint::default(); + paint.set_color(color); + let s = size as f32 / 100.0; + let transform = Transform::from_scale(s, s); + let mut pixmap = Pixmap::new(size, size).ok_or(IconsError::CreatePixmap)?; + pixmap.fill_path(&PIN_PATH, &paint, FillRule::EvenOdd, transform, None); + upload_pixmap(pixmap, ctx) + }; + let colors = calculate_accents(color); + Ok(static_map! { + IconState::Passive => create_pin(colors[0])?, + IconState::Active => create_pin(colors[1])?, + }) + }; + + Ok(SizedIcons { + pin_unfocused_title: create_pins(theme.colors.unfocused_title_background.get())?, + pin_focused_title: create_pins(theme.colors.focused_title_background.get())?, + pin_attention_requested: create_pins(theme.colors.attention_requested_background.get())?, + }) +} + +fn upload_pixmap( + pixmap: Pixmap, + ctx: &Rc, +) -> Result, IconsError> { + let width = pixmap.width(); + let height = pixmap.width(); + let bytes = unsafe { mem::transmute::, Vec>>(pixmap.take()) }; + for chunk in bytes.array_chunks_ext::<4>() { + let r = chunk[0].get(); + let b = chunk[2].get(); + chunk[0].set(b); + chunk[2].set(r); + } + let tex: Rc = ctx + .clone() + .shmem_texture( + None, + &bytes, + ARGB8888, + width as _, + height as _, + width as i32 * 4, + None, + ) + .map_err(IconsError::CreateTexture)?; + Ok(tex) +} + +static PIN_PATH: LazyLock = LazyLock::new(|| { + let cx = 50.0f32; + let cy = 40.0f32; + let r = 30.0f32; + let xx = cx; + let xy = 90.0f32; + let d = xy - cy; + let v1 = r / d * (d * d - r * r).sqrt(); + let v2 = 1.0 / d * (d * d - r * r); + + let mut path = PathBuilder::new(); + path.move_to(cx, cy - r); + path.arc_cw_to(cx, cy, cx + r, cy); + path.arc_cw_to(cx, cy, xx + v1, xy - v2); + path.line_to(xx, xy); + path.line_to(xx - v1, xy - v2); + path.arc_cw_to(cx, cy, cx - r, cy); + path.arc_cw_to(cx, cy, cx, cy - r); + path.close(); + path.push_circle(cx, cy, r / 2.5); + path.finish().unwrap() +}); + +#[test] +fn pin_path() { + let _path = &*PIN_PATH; +} + +trait PathBuilderExt { + fn arc_cw_to(&mut self, cx: f32, cy: f32, x: f32, y: f32); +} + +impl PathBuilderExt for PathBuilder { + fn arc_cw_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { + let (x0, y0) = match self.last_point() { + None => { + self.move_to(0.0, 0.0); + (0.0, 0.0) + } + Some(p) => (p.x, p.y), + }; + + let ux = x0 - cx; + let uy = y0 - cy; + let ul = (ux * ux + uy * uy).sqrt(); + let uxn = ux / ul; + let uyn = uy / ul; + let a1 = (uy / ux).atan(); + + let tx = x - cx; + let ty = y - cy; + let tl = (tx * tx + ty * ty).sqrt(); + let txn = tx / tl; + let tyn = ty / tl; + let a2 = (ty / tx).atan(); + + let c = 4.0 / 3.0 * ((a2 - a1 + PI) % PI / 4.0).tan(); + let uc = ul * c; + let tc = tl * c; + self.cubic_to( + x0 - uyn * uc, + y0 + uxn * uc, + x + tyn * tc, + y - txn * tc, + x, + y, + ); + } +} + +impl From for Color { + fn from(v: crate::theme::Color) -> Self { + let [r, g, b, a] = v.to_array(TransferFunction::Srgb); + 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 }; + let l1 = (l2 + l) / 2.0; + [ + lab_to_color([l1, a, b, alpha]), + lab_to_color([l2, a, b, alpha]), + ] +} + +fn srgb_to_lab(srgb: crate::theme::Color) -> [f32; 4] { + let [mut r, mut g, mut b, alpha] = srgb.to_array(TransferFunction::Srgb); + if alpha < 1.0 { + r /= alpha; + g /= alpha; + b /= alpha; + } + + 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(); + + [ + 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, + 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, + 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_, + alpha, + ] +} + +fn lab_to_color([l, a, b, alpha]: [f32; 4]) -> Color { + let l_ = l + 0.3963377774 * a + 0.2158037573 * b; + let m_ = l - 0.1055613458 * a - 0.0638541728 * b; + let s_ = l - 0.0894841775 * a - 1.2914855480 * b; + + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + + let mut c = Color::TRANSPARENT; + c.set_red(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s); + c.set_green(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s); + c.set_blue(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s); + c.set_alpha(alpha); + c +} diff --git a/src/main.rs b/src/main.rs index 6c9052e4..974d0441 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,6 +72,7 @@ mod format; mod gfx_api; mod gfx_apis; mod globals; +mod icons; mod ifs; mod io_uring; #[cfg(feature = "it")] diff --git a/src/renderer.rs b/src/renderer.rs index 16841c33..5289ffe4 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,6 +1,7 @@ use { crate::{ gfx_api::{AcquireSync, GfxApiOpt, ReleaseSync, SampleRect}, + icons::SizedIcons, ifs::wl_surface::{ SurfaceBuffer, WlSurface, x_surface::xwindow::Xwindow, @@ -27,6 +28,8 @@ pub struct Renderer<'a> { pub state: &'a State, pub logical_extents: Rect, pub pixel_extents: Rect, + #[expect(dead_code)] + pub icons: Option>, } impl Renderer<'_> { diff --git a/src/state.rs b/src/state.rs index b488ac28..05bde3db 100644 --- a/src/state.rs +++ b/src/state.rs @@ -33,6 +33,7 @@ use { }, gfx_apis::create_gfx_context, globals::{Globals, GlobalsError, RemovableWaylandGlobal, WaylandGlobal}, + icons::Icons, ifs::{ ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1, ext_idle_notification_v1::ExtIdleNotificationV1, @@ -237,6 +238,7 @@ pub struct State { pub color_management_enabled: Cell, pub color_manager: Rc, pub float_above_fullscreen: Cell, + pub icons: Icons, } // impl Drop for State { @@ -466,6 +468,7 @@ impl State { UpdateTextTexturesVisitor.visit_display(&self.root); self.reload_cursors(); self.update_xwayland_wire_scale(); + self.icons.update_sizes(self); } fn cursor_sizes_changed(&self) { @@ -501,6 +504,7 @@ impl State { self.render_ctx_version.fetch_add(1); self.cursors.set(None); self.drm_feedback.set(None); + self.icons.clear(); self.wait_for_sync_obj .set_ctx(ctx.as_ref().and_then(|c| c.sync_obj_ctx().cloned())); @@ -1044,6 +1048,7 @@ impl State { let (width, height) = target.logical_size(target_transform); Rect::new_sized(0, 0, width, height).unwrap() }, + icons: None, }; let mut sample_rect = SampleRect::identity(); sample_rect.buffer_transform = transform; diff --git a/src/theme.rs b/src/theme.rs index 8d78d814..3fba81b1 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -182,7 +182,6 @@ impl Color { Self::new(TransferFunction::Srgb, to_f32(r), to_f32(g), to_f32(b)) } - #[cfg_attr(not(feature = "it"), expect(dead_code))] pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { Self::new_premultiplied( TransferFunction::Srgb, @@ -220,7 +219,6 @@ impl Color { c } - #[cfg_attr(not(feature = "it"), expect(dead_code))] pub fn to_srgba_premultiplied(self) -> [u8; 4] { let [r, g, b, a] = self.to_array(TransferFunction::Srgb); [to_u8(r), to_u8(g), to_u8(b), to_u8(a)] @@ -332,7 +330,6 @@ impl Color { res } - #[cfg_attr(not(feature = "it"), expect(dead_code))] pub fn and_then(self, other: &Color) -> Color { Color { r: self.r * (1.0 - other.a) + other.r,