diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2fcaa81..f0cb128 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -44,6 +44,12 @@ tauri-plugin-updater = "2" [target.'cfg(target_os = "windows")'.dependencies] webview2-com = "0.38" +windows = { version = "0.61", features = [ + "Win32_Graphics_Gdi", + "Win32_System_Com", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging", +] } [lib] name = "app_lib" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 732129d..2583639 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,12 +6,146 @@ use tauri::{webview::{NewWindowResponse, WebviewWindowBuilder}, WebviewUrl}; use tauri_plugin_opener::OpenerExt; +#[tauri::command] +fn set_badge_count(count: u32, window: tauri::Window) -> Result<(), String> { + #[cfg(target_os = "windows")] + { + use windows::{ + core::PCWSTR, + Win32::{ + Foundation::{BOOL, COLORREF, HWND, RECT}, + Graphics::Gdi::{ + CreateCompatibleDC, CreateDIBSection, CreatePen, CreateSolidBrush, + DeleteDC, DeleteObject, DIB_RGB_COLORS, DrawTextW, Ellipse, + ReleaseDC, SelectObject, SetBkMode, SetTextColor, BITMAPINFO, + BITMAPINFOHEADER, BI_RGB, DT_CENTER, DT_SINGLELINE, DT_VCENTER, + PS_NULL, TRANSPARENT, CreateFontW, DEFAULT_CHARSET, DEFAULT_PITCH, + DEFAULT_QUALITY, CLIP_DEFAULT_PRECIS, FW_BOLD, FF_DONTCARE, + OUT_DEFAULT_PRECIS, + }, + UI::{ + Shell::{ITaskbarList3, TaskbarList}, + WindowsAndMessaging::{CreateBitmap, CreateIconIndirect, DestroyIcon, ICONINFO}, + }, + System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER}, + }, + }; + + let hwnd = HWND(window.hwnd().map_err(|e| e.to_string())?.0 as _); + + let hicon = if count > 0 { + let label = if count > 99 { + "99+".to_string() + } else { + count.to_string() + }; + let label_wide: Vec = label.encode_utf16().chain(std::iter::once(0)).collect(); + + unsafe { + let size = 16i32; + let hdc_screen = windows::Win32::Graphics::Gdi::GetDC(HWND(std::ptr::null_mut())); + let hdc = CreateCompatibleDC(hdc_screen); + + let bmi = BITMAPINFO { + bmiHeader: BITMAPINFOHEADER { + biSize: std::mem::size_of::() as u32, + biWidth: size, + biHeight: -size, + biPlanes: 1, + biBitCount: 32, + biCompression: BI_RGB.0, + ..Default::default() + }, + ..Default::default() + }; + let mut bits: *mut std::ffi::c_void = std::ptr::null_mut(); + let hbm_color = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, &mut bits, None, 0) + .map_err(|e| e.to_string())?; + let old_bm = SelectObject(hdc, hbm_color); + + // Red circle (COLORREF is 0x00BBGGRR) + let hbrush = CreateSolidBrush(COLORREF(0x003030DD)); + let old_brush = SelectObject(hdc, hbrush); + let hpen = CreatePen(PS_NULL, 0, COLORREF(0)); + let old_pen = SelectObject(hdc, hpen); + let _ = Ellipse(hdc, 0, 0, size, size); + + // White bold text centered + let hfont = CreateFontW( + 11, 0, 0, 0, FW_BOLD.0 as i32, 0, 0, 0, + DEFAULT_CHARSET.0 as u32, + OUT_DEFAULT_PRECIS.0 as u32, + CLIP_DEFAULT_PRECIS.0 as u32, + DEFAULT_QUALITY.0 as u32, + (DEFAULT_PITCH.0 | FF_DONTCARE.0) as u32, + windows::core::w!("Segoe UI"), + ); + let old_font = SelectObject(hdc, hfont); + SetTextColor(hdc, COLORREF(0x00FFFFFF)); + let _ = SetBkMode(hdc, TRANSPARENT); + let mut rect = RECT { left: 0, top: 0, right: size, bottom: size }; + let _ = DrawTextW( + hdc, + &label_wide[..label_wide.len() - 1], + &mut rect, + DT_CENTER | DT_VCENTER | DT_SINGLELINE, + ); + + SelectObject(hdc, old_brush); + SelectObject(hdc, old_pen); + SelectObject(hdc, old_font); + SelectObject(hdc, old_bm); + let _ = DeleteObject(hbrush); + let _ = DeleteObject(hpen); + let _ = DeleteObject(hfont); + + let hbm_mask = CreateBitmap(size, size, 1, 1, None) + .map_err(|e| { let _ = DeleteObject(hbm_color); e.to_string() })?; + + let icon_info = ICONINFO { + fIcon: BOOL(1), + xHotspot: 0, + yHotspot: 0, + hbmMask: hbm_mask, + hbmColor: hbm_color, + }; + let hicon = CreateIconIndirect(&icon_info) + .map_err(|e| { let _ = DeleteObject(hbm_color); let _ = DeleteObject(hbm_mask); e.to_string() })?; + + let _ = DeleteObject(hbm_color); + let _ = DeleteObject(hbm_mask); + let _ = DeleteDC(hdc); + let _ = ReleaseDC(HWND(std::ptr::null_mut()), hdc_screen); + + Some(hicon) + } + } else { + None + }; + + unsafe { + let taskbar: ITaskbarList3 = + CoCreateInstance(&TaskbarList, None, CLSCTX_INPROC_SERVER) + .map_err(|e| e.to_string())?; + taskbar.HrInit().map_err(|e| e.to_string())?; + taskbar + .SetOverlayIcon(hwnd, hicon, PCWSTR::null()) + .map_err(|e| e.to_string())?; + if let Some(icon) = hicon { + let _ = DestroyIcon(icon); + } + } + } + Ok(()) +} + pub fn run() { let port: u16 = 44548; let context = tauri::generate_context!(); let builder = tauri::Builder::default(); builder + .invoke_handler(tauri::generate_handler![set_badge_count]) .plugin(tauri_plugin_localhost::Builder::new(port).build()) .plugin(tauri_plugin_window_state::Builder::default().build()) .plugin(tauri_plugin_opener::init())