From 639326fcc31a95a0c7a9a5bb56f1ed4d53530f26 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 11 Nov 2025 23:02:57 +0100 Subject: [PATCH] feat(icon): optimize & bundle icons with crabtime for non-unix platforms --- .gitmodules | 3 ++ Cargo.toml | 7 ++- cosmic-icons | 1 + res/icons/close-menu-symbolic.svg | 4 -- res/icons/go-next-symbolic.svg | 3 -- res/icons/go-previous-symbolic.svg | 3 -- res/icons/list-add-symbolic.svg | 3 -- res/icons/list-remove-symbolic.svg | 3 -- res/icons/navbar-closed-symbolic.svg | 10 ----- res/icons/navbar-open-symbolic.svg | 8 ---- res/icons/open-menu-symbolic.svg | 3 -- res/icons/window-close-symbolic.svg | 3 -- res/icons/window-maximize-symbolic.svg | 4 -- res/icons/window-minimize-symbolic.svg | 3 -- res/icons/window-restore-symbolic.svg | 4 -- src/widget/button/icon.rs | 4 -- src/widget/button/text.rs | 22 ++++----- src/widget/dropdown/multi/widget.rs | 4 -- src/widget/dropdown/widget.rs | 4 -- src/widget/header_bar.rs | 13 ------ src/widget/icon/bundle.rs | 62 ++++++++++++++++++++++++++ src/widget/icon/handle.rs | 6 +-- src/widget/icon/mod.rs | 55 ++--------------------- src/widget/icon/named.rs | 16 ++++++- src/widget/nav_bar_toggle.rs | 12 ++--- src/widget/spin_button.rs | 48 ++++++++++---------- src/widget/warning.rs | 9 ---- 27 files changed, 128 insertions(+), 189 deletions(-) create mode 160000 cosmic-icons delete mode 100644 res/icons/close-menu-symbolic.svg delete mode 100644 res/icons/go-next-symbolic.svg delete mode 100644 res/icons/go-previous-symbolic.svg delete mode 100644 res/icons/list-add-symbolic.svg delete mode 100644 res/icons/list-remove-symbolic.svg delete mode 100644 res/icons/navbar-closed-symbolic.svg delete mode 100644 res/icons/navbar-open-symbolic.svg delete mode 100644 res/icons/open-menu-symbolic.svg delete mode 100644 res/icons/window-close-symbolic.svg delete mode 100644 res/icons/window-maximize-symbolic.svg delete mode 100644 res/icons/window-minimize-symbolic.svg delete mode 100644 res/icons/window-restore-symbolic.svg create mode 100644 src/widget/icon/bundle.rs diff --git a/.gitmodules b/.gitmodules index 367f7f2..fdaf8ab 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = iced url = https://github.com/pop-os/iced.git branch = master +[submodule "icon-theme"] + path = cosmic-icons + url = https://github.com/pop-os/cosmic-icons diff --git a/Cargo.toml b/Cargo.toml index 430af23..4d74212 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,6 +107,8 @@ cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-c chrono = "0.4.42" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } +# Compile-time generation of code +crabtime = "1.1.4" # Internationalization i18n-embed = { version = "0.16.0", features = [ "fluent-system", @@ -152,6 +154,10 @@ freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://githu freedesktop-desktop-entry = { version = "0.7.14", optional = true } shlex = { version = "1.3.0", optional = true } +[target.'cfg(not(unix))'.dependencies] +# Used to embed bundled icons for non-unix platforms. +phf = { version = "0.13.1", features = ["macros"] } + [dependencies.cosmic-theme] path = "cosmic-theme" @@ -222,4 +228,3 @@ dirs = "6.0.0" [dev-dependencies] tempfile = "3.13.0" - diff --git a/cosmic-icons b/cosmic-icons new file mode 160000 index 0000000..70b0758 --- /dev/null +++ b/cosmic-icons @@ -0,0 +1 @@ +Subproject commit 70b07582e24ec2114672256b9657ca80670bca8a diff --git a/res/icons/close-menu-symbolic.svg b/res/icons/close-menu-symbolic.svg deleted file mode 100644 index caf00d3..0000000 --- a/res/icons/close-menu-symbolic.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/icons/go-next-symbolic.svg b/res/icons/go-next-symbolic.svg deleted file mode 100644 index 3aed371..0000000 --- a/res/icons/go-next-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/icons/go-previous-symbolic.svg b/res/icons/go-previous-symbolic.svg deleted file mode 100644 index 4957cff..0000000 --- a/res/icons/go-previous-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/icons/list-add-symbolic.svg b/res/icons/list-add-symbolic.svg deleted file mode 100644 index 59b2fb0..0000000 --- a/res/icons/list-add-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/icons/list-remove-symbolic.svg b/res/icons/list-remove-symbolic.svg deleted file mode 100644 index 5b9ded7..0000000 --- a/res/icons/list-remove-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/icons/navbar-closed-symbolic.svg b/res/icons/navbar-closed-symbolic.svg deleted file mode 100644 index 46f35e1..0000000 --- a/res/icons/navbar-closed-symbolic.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/res/icons/navbar-open-symbolic.svg b/res/icons/navbar-open-symbolic.svg deleted file mode 100644 index c1f3216..0000000 --- a/res/icons/navbar-open-symbolic.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/res/icons/open-menu-symbolic.svg b/res/icons/open-menu-symbolic.svg deleted file mode 100644 index efae2a2..0000000 --- a/res/icons/open-menu-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/icons/window-close-symbolic.svg b/res/icons/window-close-symbolic.svg deleted file mode 100644 index 2533639..0000000 --- a/res/icons/window-close-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/icons/window-maximize-symbolic.svg b/res/icons/window-maximize-symbolic.svg deleted file mode 100644 index ef66334..0000000 --- a/res/icons/window-maximize-symbolic.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/icons/window-minimize-symbolic.svg b/res/icons/window-minimize-symbolic.svg deleted file mode 100644 index fdcf99b..0000000 --- a/res/icons/window-minimize-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/icons/window-restore-symbolic.svg b/res/icons/window-restore-symbolic.svg deleted file mode 100644 index bcb506f..0000000 --- a/res/icons/window-restore-symbolic.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index 0bb3c84..754bc43 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -132,10 +132,6 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { let mut content = Vec::with_capacity(2); - if let icon::Data::Name(ref mut named) = builder.variant.handle.data { - named.size = Some(builder.icon_size); - } - content.push( crate::widget::icon(builder.variant.handle.clone()) .size(builder.icon_size) diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs index e5dea9f..3f58c93 100644 --- a/src/widget/button/text.rs +++ b/src/widget/button/text.rs @@ -91,21 +91,15 @@ impl Button<'_, Message> { impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { - let trailing_icon = builder.variant.trailing_icon.map(|mut i| { - if let icon::Data::Name(ref mut named) = i.data { - named.size = Some(builder.icon_size); - } + let trailing_icon = builder + .variant + .trailing_icon + .map(crate::widget::icon::Handle::icon); - i.icon() - }); - - let leading_icon = builder.variant.leading_icon.map(|mut i| { - if let icon::Data::Name(ref mut named) = i.data { - named.size = Some(builder.icon_size); - } - - i.icon() - }); + let leading_icon = builder + .variant + .leading_icon + .map(crate::widget::icon::Handle::icon); let label: Option> = (!builder.label.is_empty()).then(|| { let font = crate::font::Font { diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 79b1a6b..458cf5e 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -230,10 +230,6 @@ impl State { pub fn new() -> Self { Self { icon: match icon::from_name("pan-down-symbolic").size(16).handle().data { - icon::Data::Name(named) => named - .path() - .filter(|path| path.extension().is_some_and(|ext| ext == OsStr::new("svg"))) - .map(iced_core::svg::Handle::from_path), icon::Data::Svg(handle) => Some(handle), icon::Data::Image(_) => None, }, diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index a5612cc..d4a9bc8 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -407,10 +407,6 @@ impl State { pub fn new() -> Self { Self { icon: match icon::from_name("pan-down-symbolic").size(16).handle().data { - icon::Data::Name(named) => named - .path() - .filter(|path| path.extension().is_some_and(|ext| ext == OsStr::new("svg"))) - .map(iced_core::svg::Handle::from_path), icon::Data::Svg(handle) => Some(handle), icon::Data::Image(_) => None, }, diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 01a8d55..d500bde 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -449,25 +449,12 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { fn window_controls(&mut self) -> Element<'a, Message> { macro_rules! icon { ($name:expr, $size:expr, $on_press:expr) => {{ - #[cfg(target_os = "linux")] let icon = { widget::icon::from_name($name) .apply(widget::button::icon) .padding(8) }; - #[cfg(not(target_os = "linux"))] - let icon = { - widget::icon::from_svg_bytes(include_bytes!(concat!( - "../../res/icons/", - $name, - ".svg" - ))) - .symbolic(true) - .apply(widget::button::icon) - .padding(8) - }; - icon.class(crate::theme::Button::HeaderBar) .selected(self.focused) .icon_size($size) diff --git a/src/widget/icon/bundle.rs b/src/widget/icon/bundle.rs new file mode 100644 index 0000000..0e1fdc1 --- /dev/null +++ b/src/widget/icon/bundle.rs @@ -0,0 +1,62 @@ +// Copyright 2025 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Embedded icons for platforms which do not support icon themes yet. + +/// Icon bundling is not enabled on unix platforms. +pub fn get(icon_name: &str) -> Option { + None +} + +#[cfg(not(unix))] +/// Get a bundled icon on non-unix platforms. +pub fn get(icon_name: &str) -> Option { + ICONS + .get(icon_name) + .map(|bytes| super::Data::Svg(crate::iced::widget::svg::Handle::from_memory(*bytes))) +} + +#[cfg(not(unix))] +#[crabtime::expression] +fn comptime_icon_bundler() -> String { + let manifest_dir = std::path::Path::new(crabtime::WORKSPACE_PATH); + let icon_paths = [ + "cosmic-icons/freedesktop/scalable", + "cosmic-icons/extra/scalable", + ]; + + let key_value_assignments = icon_paths + .into_iter() + .map(|path| manifest_dir.join(path)) + .inspect(|icon_path| assert!(icon_path.exists(), "path = {icon_path:?}")) + .map(|icon_path| std::fs::read_dir(icon_path).unwrap()) + .flat_map(|dir| { + dir.flat_map(|entry| entry.unwrap().path().read_dir().unwrap()) + .map(|entry| { + let entry = entry.unwrap(); + let path = entry.path().canonicalize().unwrap(); + let file_name = path.file_stem().unwrap().to_str().unwrap().to_owned(); + let path = path.into_os_string().into_string().unwrap(); + (file_name, path) + }) + }) + .fold( + std::collections::BTreeMap::new(), + |mut set, (name, path)| { + set.insert(name, path); + set + }, + ) + .into_iter() + .fold(String::new(), |mut output, (name, path)| { + output.push_str(&format!(" \"{name}\" => include_bytes!(\"{path}\"),\n")); + output + }); + + ["phf::phf_map!(\n", &key_value_assignments, ")"].concat() +} + +#[cfg(not(unix))] +static ICONS: phf::Map<&'static str, &'static [u8]> = { + comptime_icon_bundler! {} +}; diff --git a/src/widget/icon/handle.rs b/src/widget/icon/handle.rs index 1fa2d85..a4ddd36 100644 --- a/src/widget/icon/handle.rs +++ b/src/widget/icon/handle.rs @@ -1,7 +1,7 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use super::{Icon, Named}; +use super::Icon; use crate::widget::{image, svg}; use std::borrow::Cow; use std::ffi::OsStr; @@ -26,7 +26,7 @@ impl Handle { #[must_use] #[derive(Clone, Debug, Hash)] pub enum Data { - Name(Named), + // Name(Named), Image(image::Handle), Svg(svg::Handle), } @@ -94,7 +94,7 @@ pub fn from_raster_pixels( /// Create a SVG handle from memory. pub fn from_svg_bytes(bytes: impl Into>) -> Handle { Handle { - symbolic: false, + symbolic: true, data: Data::Svg(svg::Handle::from_memory(bytes)), } } diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 20e8bf2..6c6a9f0 100644 --- a/src/widget/icon/mod.rs +++ b/src/widget/icon/mod.rs @@ -3,8 +3,8 @@ //! Lazily-generated SVG icon widget for Iced. +mod bundle; mod named; -use std::ffi::OsStr; use std::sync::Arc; pub use named::{IconFallback, Named}; @@ -58,14 +58,6 @@ impl Icon { #[must_use] pub fn into_svg_handle(self) -> Option { match self.handle.data { - Data::Name(named) => { - if let Some(path) = named.path() { - if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) { - return Some(iced_core::svg::Handle::from_path(path)); - } - } - } - Data::Image(_) => (), Data::Svg(handle) => return Some(handle), } @@ -76,12 +68,6 @@ impl Icon { #[must_use] pub fn size(mut self, size: u16) -> Self { self.size = size; - // ensures correct icon size variant selection - if let Data::Name(named) = &self.handle.data { - let mut new_named = named.clone(); - new_named.size = Some(size); - self.handle = new_named.handle(); - } self } @@ -120,19 +106,6 @@ impl Icon { }; match self.handle.data { - Data::Name(named) => { - if let Some(path) = named.path() { - if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) { - from_svg(iced_core::svg::Handle::from_path(path)) - } else { - from_image(iced_core::image::Handle::from_path(path)) - } - } else { - let bytes: &'static [u8] = &[]; - from_svg(iced_core::svg::Handle::from_memory(bytes)) - } - } - Data::Image(handle) => from_image(handle), Data::Svg(handle) => from_svg(handle), } @@ -147,32 +120,14 @@ impl<'a, Message: 'a> From for Element<'a, Message> { /// Draw an icon in the given bounds via the runtime's renderer. pub fn draw(renderer: &mut crate::Renderer, handle: &Handle, icon_bounds: Rectangle) { - enum IcedHandle { - Svg(iced_core::svg::Handle), - Image(iced_core::image::Handle), - } - - let iced_handle = match handle.clone().data { - Data::Name(named) => named.path().map(|path| { - if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) { - IcedHandle::Svg(iced_core::svg::Handle::from_path(path)) - } else { - IcedHandle::Image(iced_core::image::Handle::from_path(path)) - } - }), - - Data::Image(handle) => Some(IcedHandle::Image(handle)), - Data::Svg(handle) => Some(IcedHandle::Svg(handle)), - }; - - match iced_handle { - Some(IcedHandle::Svg(handle)) => iced_core::svg::Renderer::draw_svg( + match handle.clone().data { + Data::Svg(handle) => iced_core::svg::Renderer::draw_svg( renderer, iced_core::svg::Svg::new(handle), icon_bounds, ), - Some(IcedHandle::Image(handle)) => { + Data::Image(handle) => { iced_core::image::Renderer::draw_image( renderer, handle, @@ -183,7 +138,5 @@ pub fn draw(renderer: &mut crate::Renderer, handle: &Handle, icon_bounds: Rectan [0.0; 4], ); } - - None => {} } } diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index e1c5350..8405e08 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 use super::{Handle, Icon}; -use std::{borrow::Cow, path::PathBuf, sync::Arc}; +use std::{borrow::Cow, ffi::OsStr, path::PathBuf, sync::Arc}; #[derive(Debug, Clone, Default, Hash)] /// Fallback icon to use if the icon was not found. @@ -116,9 +116,21 @@ impl Named { #[inline] pub fn handle(self) -> Handle { + let name = self.name.clone(); Handle { symbolic: self.symbolic, - data: super::Data::Name(self), + data: if let Some(path) = self.path() { + if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) { + super::Data::Svg(iced_core::svg::Handle::from_path(path)) + } else { + super::Data::Image(iced_core::image::Handle::from_path(path)) + } + } else { + super::bundle::get(&name).unwrap_or_else(|| { + let bytes: &'static [u8] = &[]; + super::Data::Svg(iced_core::svg::Handle::from_memory(bytes)) + }) + }, } } diff --git a/src/widget/nav_bar_toggle.rs b/src/widget/nav_bar_toggle.rs index 23495e3..b0849dd 100644 --- a/src/widget/nav_bar_toggle.rs +++ b/src/widget/nav_bar_toggle.rs @@ -28,18 +28,12 @@ pub const fn nav_bar_toggle() -> NavBarToggle { impl From> for Element<'_, Message> { fn from(nav_bar_toggle: NavBarToggle) -> Self { let icon = if nav_bar_toggle.active { - widget::icon::from_svg_bytes( - &include_bytes!("../../res/icons/navbar-open-symbolic.svg")[..], - ) - .symbolic(true) + "navbar-open-symbolic" } else { - widget::icon::from_svg_bytes( - &include_bytes!("../../res/icons/navbar-closed-symbolic.svg")[..], - ) - .symbolic(true) + "navbar-closed-symbolic" }; - widget::button::icon(icon) + widget::button::icon(widget::icon::from_name(icon)) .padding([8, 16]) .on_press_maybe(nav_bar_toggle.on_toggle) .selected(nav_bar_toggle.selected) diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index a93f2ee..6f4a4de 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -115,7 +115,7 @@ where } } -fn increment(value: T, step: T, min: T, max: T) -> T +fn increment(value: T, step: T, _min: T, max: T) -> T where T: Copy + Sub + Add + PartialOrd, { @@ -126,7 +126,7 @@ where } } -fn decrement(value: T, step: T, min: T, max: T) -> T +fn decrement(value: T, step: T, min: T, _max: T) -> T where T: Copy + Sub + Add + PartialOrd, { @@ -149,25 +149,25 @@ where } } } -macro_rules! make_button { - ($spin_button:expr, $icon:expr, $operation:expr) => {{ - #[cfg(target_os = "linux")] - let button = icon::from_name($icon); - #[cfg(not(target_os = "linux"))] - let button = - icon::from_svg_bytes(include_bytes!(concat!["../../res/icons/", $icon, ".svg"])) - .symbolic(true); - - button - .apply(button::icon) - .on_press(($spin_button.on_press)($operation( - $spin_button.value, - $spin_button.step, - $spin_button.min, - $spin_button.max, - ))) - }}; +fn make_button<'a, T, Message>( + spin_button: &SpinButton<'a, T, Message>, + icon: &'static str, + operation: fn(T, T, T, T) -> T, +) -> Element<'a, Message> +where + Message: Clone + 'static, + T: Copy + Sub + Add + PartialOrd, +{ + icon::from_name(icon) + .apply(button::icon) + .on_press((spin_button.on_press)(operation( + spin_button.value, + spin_button.step, + spin_button.min, + spin_button.max, + ))) + .into() } fn horizontal_variant(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message> @@ -175,8 +175,8 @@ where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - let decrement_button = make_button!(spin_button, "list-remove-symbolic", decrement); - let increment_button = make_button!(spin_button, "list-add-symbolic", increment); + let decrement_button = make_button(&spin_button, "list-remove-symbolic", decrement); + let increment_button = make_button(&spin_button, "list-add-symbolic", increment); let label = text::body(spin_button.label) .apply(container) @@ -198,8 +198,8 @@ where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - let decrement_button = make_button!(spin_button, "list-remove-symbolic", decrement); - let increment_button = make_button!(spin_button, "list-add-symbolic", increment); + let decrement_button = make_button(&spin_button, "list-remove-symbolic", decrement); + let increment_button = make_button(&spin_button, "list-add-symbolic", increment); let label = text::body(spin_button.label) .apply(container) diff --git a/src/widget/warning.rs b/src/widget/warning.rs index 3e3a1ad..942ffb8 100644 --- a/src/widget/warning.rs +++ b/src/widget/warning.rs @@ -33,20 +33,11 @@ impl<'a, Message: 'static + Clone> Warning<'a, Message> { pub fn into_widget(self) -> widget::Container<'a, Message, crate::Theme, Renderer> { let label = widget::container(crate::widget::text(self.message)).width(Length::Fill); - #[cfg(target_os = "linux")] let close_button = icon::from_name("window-close-symbolic") .size(16) .apply(widget::button::icon) .on_press_maybe(self.on_close); - #[cfg(not(target_os = "linux"))] - let close_button = - icon::from_svg_bytes(include_bytes!("../../res/icons/window-close-symbolic.svg")) - .symbolic(true) - .apply(widget::button::icon) - .icon_size(16) - .on_press_maybe(self.on_close); - widget::row::with_capacity(2) .push(label) .push(close_button)