use ab_glyph::{FontRef, PxScale}; use image::{Rgba, RgbaImage, imageops}; use imageproc::drawing::{draw_filled_circle_mut, draw_filled_rect_mut, draw_text_mut, text_size}; use imageproc::filter::gaussian_blur_f32; use imageproc::rect::Rect; use std::io::Cursor; use tauri::image::Image; static FONT: &[u8] = include_bytes!("../../fonts/Roboto-Bold.ttf"); const BADGE_BACKGROUND_COLOR: Rgba = Rgba([0xF2, 0x3C, 0x34, 0xFF]); const BADGE_BACKGROUND_COLOR_MUTED: Rgba = Rgba([0x88, 0x88, 0x88, 0xFF]); const BADGE_TEXT_COLOR: Rgba = Rgba([0xFF, 0xFF, 0xFF, 0xFF]); pub fn set_badge_count_icon(window: &tauri::WebviewWindow, amount: i32, is_muted: bool) { if amount == 0 { window.set_overlay_icon(None).unwrap_or_default(); if let Ok(tray_opt) = super::TRAY_HANDLE.lock() { if let Some(tray) = tray_opt.as_ref() { let _ = tray.set_icon(Some(super::TRAY_BASE_ICON.clone())); } } } else { let png = generate_counter_png(48, amount, is_muted); let converted = Image::from_bytes(&png); if let Ok(converted) = converted { window.set_overlay_icon(Some(converted)).unwrap_or_default(); } else { log::error!("Failed to convert notification icon: {:?}", converted.err()); window.set_overlay_icon(None).unwrap_or_default(); } // Update tray icon with counter overlay if let Ok(tray_opt) = super::TRAY_HANDLE.lock() { if let Some(tray) = tray_opt.as_ref() { let base_icon = &super::TRAY_BASE_ICON; let counter_size = (base_icon.width() as f32 * 0.6).floor() as u32; let counter_icon = generate_counter_png(counter_size, amount, is_muted); let counter_icon = Image::from_bytes(&counter_icon).unwrap(); let overlay_icon = overlay_tray_icon(base_icon, &counter_icon); let _ = tray.set_icon(Some(overlay_icon)); } } } } pub fn generate_counter_png(size: u32, count: i32, is_muted: bool) -> Vec { let background_color = if is_muted { BADGE_BACKGROUND_COLOR_MUTED } else { BADGE_BACKGROUND_COLOR }; // Prepare text properties let (text, font, scale, text_width, text_height) = if count >= 0 { let text = if count < 100 { count.to_string() } else { format!("..{:02}", count % 100) }; let font = FontRef::try_from_slice(FONT).expect("Invalid font"); let scale = { let base = if text.len() < 3 { 0.9 } else { 0.75 }; let calculated_scale = (base * size as f32).ceil(); PxScale::from(calculated_scale) }; let (text_width, text_height) = text_size(scale, &font, &text); (Some(text), Some(font), Some(scale), text_width, text_height) } else { (None, None, None, 0, 0) }; // Calculate badge dimensions let (badge_width, badge_height) = if count >= 0 { let padding = size / 10; let min_dimension = size; let content_width = text_width + padding * 2; let content_height = text_height + padding * 2; let width = content_width.max(min_dimension); let height = content_height.max(min_dimension); (width, height) } else { (size, size) }; let edge_space = if let Some(scale) = scale { ((scale.y / 10.0).ceil() as u32).max(1) } else { 1 }; let img_width = badge_width + edge_space * 2; let img_height = badge_height + edge_space * 2; let mut img = RgbaImage::from_pixel(img_width, img_height, Rgba([0, 0, 0, 0])); let corner_radius = if count < 0 { badge_width.min(badge_height) / 2 } else { badge_height / 2 }; draw_rounded_rect( &mut img, edge_space, edge_space, badge_width, badge_height, corner_radius, background_color, ); // Apply gaussian blur for antialiasing effect img = gaussian_blur_f32(&img, 0.75); if let (Some(text), Some(font), Some(scale)) = (text, font, scale) { let x = edge_space as f32 + ((badge_width as f32 - text_width as f32) / 2.0).ceil(); let y = edge_space as f32 + ((badge_height as f32 - text_height as f32) / 2.0).ceil(); draw_text_mut( &mut img, BADGE_TEXT_COLOR, x as i32, y as i32, scale, &font, &text, ); } let mut buffer = Vec::new(); let mut cursor = Cursor::new(&mut buffer); img .write_to(&mut cursor, image::ImageFormat::Png) .expect("PNG encode failed"); buffer } pub fn overlay_tray_icon(icon: &Image, counter: &Image) -> Image<'static> { let icon_rgba = icon.rgba(); let counter_rgba = counter.rgba(); let icon_img = image::RgbaImage::from_raw(icon.width(), icon.height(), icon_rgba.to_vec()) .expect("Failed to create RgbaImage from icon data"); let counter_img = image::RgbaImage::from_raw(counter.width(), counter.height(), counter_rgba.to_vec()) .expect("Failed to create RgbaImage from counter data"); let mut result = icon_img.clone(); let icon_width = result.width(); let icon_height = result.height(); let counter_width = counter_img.width(); let counter_height = counter_img.height(); let padding = 0; let x = icon_width.saturating_sub(counter_width + padding); let y = icon_height.saturating_sub(counter_height + padding); // Overlay the counter image onto the icon imageops::overlay(&mut result, &counter_img, x.into(), y.into()); // Convert back to tauri Image let mut buffer = Vec::new(); let mut cursor = Cursor::new(&mut buffer); result .write_to(&mut cursor, image::ImageFormat::Png) .expect("PNG encode failed"); Image::from_bytes(&buffer).expect("Failed to create Image from bytes") } fn draw_rounded_rect( img: &mut RgbaImage, x: u32, y: u32, width: u32, height: u32, radius: u32, color: Rgba, ) { let radius = radius.min(width / 2).min(height / 2); if radius == 0 { draw_filled_rect_mut( img, Rect::at(x as i32, y as i32).of_size(width, height), color, ); return; } if width > 2 * radius && height > 2 * radius { draw_filled_rect_mut( img, Rect::at((x + radius) as i32, (y + radius) as i32) .of_size(width - 2 * radius, height - 2 * radius), color, ); } if width > 2 * radius { draw_filled_rect_mut( img, Rect::at((x + radius) as i32, y as i32).of_size(width - 2 * radius, radius), color, ); draw_filled_rect_mut( img, Rect::at((x + radius) as i32, (y + height - radius) as i32) .of_size(width - 2 * radius, radius), color, ); } if height > 2 * radius { draw_filled_rect_mut( img, Rect::at(x as i32, (y + radius) as i32).of_size(radius, height - 2 * radius), color, ); draw_filled_rect_mut( img, Rect::at((x + width - radius) as i32, (y + radius) as i32) .of_size(radius, height - 2 * radius), color, ); } let radius_i32 = radius as i32; draw_filled_circle_mut( img, ((x + radius) as i32, (y + radius) as i32), radius_i32, color, ); draw_filled_circle_mut( img, ((x + width - radius) as i32, (y + radius) as i32), radius_i32, color, ); draw_filled_circle_mut( img, ((x + radius) as i32, (y + height - radius) as i32), radius_i32, color, ); draw_filled_circle_mut( img, ((x + width - radius) as i32, (y + height - radius) as i32), radius_i32, color, ); }