264 lines
7.1 KiB
Rust
264 lines
7.1 KiB
Rust
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<u8> = Rgba([0xF2, 0x3C, 0x34, 0xFF]);
|
|
const BADGE_BACKGROUND_COLOR_MUTED: Rgba<u8> = Rgba([0x88, 0x88, 0x88, 0xFF]);
|
|
const BADGE_TEXT_COLOR: Rgba<u8> = 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<u8> {
|
|
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<u8>,
|
|
) {
|
|
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,
|
|
);
|
|
}
|