#![cfg_attr( all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" )] 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()) .setup(move |app| { #[cfg(debug_assertions)] let window_url = WebviewUrl::App(Default::default()); #[cfg(not(debug_assertions))] let window_url = { let url = format!("http://localhost:{}", port).parse().unwrap(); WebviewUrl::External(url) }; let app_handle = app.handle().clone(); let window = WebviewWindowBuilder::new(app, "main".to_string(), window_url) .title("Lotus Chat") .disable_drag_drop_handler() .on_new_window(move |url, _features| { let _ = app_handle.opener().open_url(url.as_str(), None::<&str>); NewWindowResponse::Deny }) .build()?; // Grant camera and microphone to WebView2 automatically. // Windows requires an explicit PermissionRequested COM event handler. #[cfg(target_os = "windows")] window.with_webview(|webview| { use webview2_com::{ Microsoft::Web::WebView2::Win32::{ COREWEBVIEW2_PERMISSION_KIND, COREWEBVIEW2_PERMISSION_KIND_CAMERA, COREWEBVIEW2_PERMISSION_KIND_MICROPHONE, COREWEBVIEW2_PERMISSION_STATE_ALLOW, }, PermissionRequestedEventHandler, }; let controller = webview.controller(); if let Ok(core) = unsafe { controller.CoreWebView2() } { let handler = PermissionRequestedEventHandler::create(Box::new( |_sender, args| { if let Some(args) = args { let mut kind = COREWEBVIEW2_PERMISSION_KIND(0); unsafe { args.PermissionKind(&mut kind) }?; if kind == COREWEBVIEW2_PERMISSION_KIND_MICROPHONE || kind == COREWEBVIEW2_PERMISSION_KIND_CAMERA { unsafe { args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW) }?; } } Ok(()) }, )); let mut token = Default::default(); let _ = unsafe { core.add_PermissionRequested(&handler, &mut token) }; } })?; Ok(()) }) .run(context) .expect("error while building tauri application"); }