virtual_desktop_manager/
tray_icons.rs

1//! Generate tray icons as needed.
2//!
3//! # References
4//!
5//! The idea for this feature came from other projects that use the tray icon to
6//! display the current virtual desktop index:
7//! - [m0ngr31/VirtualDesktopManager](https://github.com/m0ngr31/VirtualDesktopManager)
8//!   - This is also where the icons used in this project was copied from.
9//!   - Creates new icons on demand to support up to 999 desktops.
10//! - [mzomparelli/zVirtualDesktop: Windows 10 Virtual Desktop Hotkeys, System
11//!   Tray Icon, Wallpapers, and Task View
12//!   replacement](https://github.com/mzomparelli/zVirtualDesktop?tab=readme-ov-file)
13//!   - Hard coded to 9 images?
14//! - [lutz/VirtualDesktopNameDeskband: Deskband for the windows taskbar to show
15//!   the name of current virtual
16//!   desktop](https://github.com/lutz/VirtualDesktopNameDeskband)
17//!   - Shows the desktop index in a "deskband", a small widget shown next to
18//!     the taskbar (next to where windows 10 would show the weather).
19//! - [dankrusi/WindowsVirtualDesktopHelper: App to help manage Virtual Desktops
20//!   for Windows 10 and Windows
21//!   11](https://github.com/dankrusi/WindowsVirtualDesktopHelper)
22//!   - This generates icons on demand and so can likely support "infinitely"
23//!     many desktops.
24//! - [sdias/win-10-virtual-desktop-enhancer: An application that enhances the
25//!   Windows 10 multiple desktops feature by adding additional keyboard
26//!   shortcuts and support for multiple
27//!   wallpapers.](https://github.com/sdias/win-10-virtual-desktop-enhancer)
28//!   - Supports icon packs for specifying tray icon on different desktops.
29
30#![allow(dead_code)]
31
32#[allow(unused_imports)]
33use std::{borrow::Cow, io::Cursor, sync::OnceLock};
34
35#[cfg(feature = "tray_icon_hardcoded")]
36mod hardcoded {
37    pub static ICON1: &[u8] = include_bytes!("icons/triangle1.ico");
38    pub static ICON2: &[u8] = include_bytes!("icons/triangle2.ico");
39    pub static ICON3: &[u8] = include_bytes!("icons/triangle3.ico");
40    pub static ICON4: &[u8] = include_bytes!("icons/triangle4.ico");
41    pub static ICON5: &[u8] = include_bytes!("icons/triangle5.ico");
42    pub static ICON6: &[u8] = include_bytes!("icons/triangle6.ico");
43    pub static ICON7: &[u8] = include_bytes!("icons/triangle7.ico");
44    pub static ICON8: &[u8] = include_bytes!("icons/triangle8.ico");
45    pub static ICON9: &[u8] = include_bytes!("icons/triangle9.ico");
46}
47#[cfg(feature = "tray_icon_hardcoded")]
48pub use hardcoded::*;
49
50pub static ICON_EMPTY: &[u8] = include_bytes!("icons/triangleEmpty.ico");
51#[cfg(feature = "tray_icon_with_background")]
52pub static IMAGE_EMPTY: &[u8] = include_bytes!("icons/triangleEmptyImage.png");
53
54/// This font is only guaranteed to work on numbers.
55///
56/// # Font
57///
58/// - [Open Sans - Google Fonts](https://fonts.google.com/specimen/Open+Sans/about)
59///   - Links to: <https://github.com/googlefonts/opensans>
60///   - License:  Open Font License.
61/// - Alternatively we could use [`DejaVuSans.ttf`](https://github.com/image-rs/imageproc/blob/4e6a5dc65485cd58c74f1d120657676831106c57/examples/DejaVuSans.ttf)
62/// - The `text-to-png` includes a font if you don't chose one yourself, we could use that one.
63///
64/// # Minimize size
65///
66/// We only need to render digits so we remove unnecessary stuff, see:
67/// [filesize - Way to reduce size of .ttf fonts? - Stack Overflow](https://stackoverflow.com/questions/2635423/way-to-reduce-size-of-ttf-fonts)
68#[cfg(any(
69    feature = "tray_icon_with_background",
70    feature = "tray_icon_text_only",
71    feature = "tray_icon_text_only_alt"
72))]
73static NUMBER_FONT: &[u8] = include_bytes!("./OpenSans-Bold-DigitsOnly.ttf");
74
75pub fn get_included_icon(_number: u32) -> Option<&'static [u8]> {
76    #[cfg(feature = "tray_icon_hardcoded")]
77    {
78        Some(match _number {
79            1 => ICON1,
80            2 => ICON2,
81            3 => ICON3,
82            4 => ICON4,
83            5 => ICON5,
84            6 => ICON6,
85            7 => ICON7,
86            8 => ICON8,
87            9 => ICON9,
88            _ => return None,
89        })
90    }
91    #[cfg(not(feature = "tray_icon_hardcoded"))]
92    {
93        None
94    }
95}
96
97pub enum IconType {
98    WithBackground {
99        allow_hardcoded: bool,
100        light_theme: bool,
101    },
102    NoBackground {
103        light_theme: bool,
104    },
105    NoBackgroundAlt,
106}
107impl IconType {
108    // TODO: maybe return errors from this in case image generation fails.
109    pub fn generate_icon(&self, number: u32) -> Cow<'static, [u8]> {
110        match self {
111            // TODO: support light theme with hardcoded icons
112            Self::WithBackground {
113                allow_hardcoded: true,
114                light_theme,
115            } => match get_included_icon(number).filter(|_| !light_theme) {
116                Some(d) => Cow::Borrowed(d),
117                #[cfg(feature = "tray_icon_with_background")]
118                None => Cow::Owned(generate_icon_with_background(number, *light_theme)),
119                #[cfg(not(feature = "tray_icon_with_background"))]
120                None => Cow::Borrowed(ICON_EMPTY),
121            },
122            Self::WithBackground {
123                allow_hardcoded: false,
124                light_theme,
125            } => {
126                #[cfg(feature = "tray_icon_with_background")]
127                {
128                    generate_icon_with_background(number, *light_theme).into()
129                }
130                #[cfg(not(feature = "tray_icon_with_background"))]
131                {
132                    Cow::Borrowed(ICON_EMPTY)
133                }
134            }
135            Self::NoBackground { light_theme } => {
136                #[cfg(feature = "tray_icon_text_only")]
137                {
138                    generate_icon_without_background(number, *light_theme).into()
139                }
140                #[cfg(not(feature = "tray_icon_text_only"))]
141                {
142                    Cow::Borrowed(ICON_EMPTY)
143                }
144            }
145            Self::NoBackgroundAlt => {
146                #[cfg(feature = "tray_icon_text_only_alt")]
147                {
148                    generate_icon_without_background_alt(number).into()
149                }
150                #[cfg(not(feature = "tray_icon_text_only_alt"))]
151                {
152                    Cow::Borrowed(ICON_EMPTY)
153                }
154            }
155        }
156    }
157}
158
159#[cfg(any(feature = "tray_icon_with_background", feature = "tray_icon_text_only"))]
160fn get_number_font() -> &'static ab_glyph::FontRef<'static> {
161    static CACHED: OnceLock<ab_glyph::FontRef<'static>> = OnceLock::new();
162    CACHED.get_or_init(|| {
163        ab_glyph::FontRef::try_from_slice(NUMBER_FONT).expect("Valid font embedded in binary")
164    })
165}
166
167#[cfg(feature = "tray_icon_with_background")]
168fn get_empty_image() -> &'static image::DynamicImage {
169    static CACHED: OnceLock<image::DynamicImage> = OnceLock::new();
170    CACHED.get_or_init(|| {
171        let image =
172            image::ImageReader::with_format(Cursor::new(IMAGE_EMPTY), image::ImageFormat::Png)
173                .decode()
174                .expect("Failed to load embedded PNG");
175        // Embedded image is 258 pixels but the `image` crate can only convert
176        // to ico when the initial image is max 256x256.
177        image.crop_imm(0, 0, 256, 256)
178    })
179}
180
181/// Generate an icon with a background using the `imageproc` crate to draw text.
182#[cfg(feature = "tray_icon_with_background")]
183pub fn generate_icon_with_background(number: u32, light_theme: bool) -> Vec<u8> {
184    let text = number.to_string();
185
186    let font = get_number_font();
187    let mut canvas = get_empty_image().clone();
188    if light_theme {
189        canvas.invert();
190    }
191    imageproc::drawing::draw_text_mut(
192        &mut canvas,
193        imageproc::image::Rgba(if light_theme {
194            [0, 0, 0, 255]
195        } else {
196            [255, 255, 255, 255]
197        }),
198        if text.len() >= 2 { 110 } else { 130 },
199        56,
200        ab_glyph::PxScale { x: 150.0, y: 180.0 },
201        font,
202        &text,
203    );
204    // canvas = image::imageops::contrast(&canvas, 10.0).into();
205    let mut data = Vec::new();
206    canvas
207        .write_to(&mut Cursor::new(&mut data), image::ImageFormat::Ico)
208        .expect("Failed to convert generated tray image to ICO format");
209    data
210}
211
212/// Generate icon without any background using the `imageproc` crate to draw text.
213#[cfg(feature = "tray_icon_text_only")]
214pub fn generate_icon_without_background(number: u32, light_theme: bool) -> Vec<u8> {
215    let text = number.to_string();
216
217    let font = get_number_font();
218    let mut canvas = image::ImageBuffer::from_pixel(256, 256, image::Rgba([0_u8, 0, 0, 0]));
219
220    imageproc::drawing::draw_text_mut(
221        &mut canvas,
222        imageproc::image::Rgba(if light_theme {
223            [0, 0, 0, 255]
224        } else {
225            [255, 255, 255, 255]
226        }),
227        if text.len() >= 2 { -8 } else { 0 },
228        -130,
229        ab_glyph::PxScale {
230            x: match text.len() {
231                0 | 1 => 660.0,
232                2 => 330.0,
233                _ => 210.0,
234            },
235            y: 490.0,
236        },
237        font,
238        &text,
239    );
240    // canvas = image::imageops::contrast(&canvas, 10.0).into();
241    let mut data = Vec::new();
242    canvas
243        .write_to(&mut Cursor::new(&mut data), image::ImageFormat::Ico)
244        .expect("Failed to convert generated tray image to ICO format");
245    data
246}
247
248/// Generate icon without any background using the `text-to-png` crate to draw
249/// text.
250#[cfg(feature = "tray_icon_text_only_alt")]
251pub fn generate_icon_without_background_alt(number: u32) -> Vec<u8> {
252    let renderer = text_to_png::TextRenderer::try_new_with_ttf_font_data(NUMBER_FONT)
253        .expect("Failed to load embedded font");
254
255    let text_png = renderer
256        .render_text_to_png_data(number.to_string(), 128, "Dark Turquoise")
257        .expect("Failed to render text to PNG");
258
259    // Convert from PNG to ICO:
260    let mut data = Vec::new();
261    image::io::Reader::with_format(Cursor::new(&text_png.data), image::ImageFormat::Png)
262        .decode()
263        .expect("Failed to read generated PNG")
264        .write_to(&mut Cursor::new(&mut data), image::ImageFormat::Ico)
265        .expect("Failed to convert tray image to ICO format");
266    data
267}