feat: Windows taskbar overlay badge via ITaskbarList3
Build Lotus Chat Desktop / prepare (push) Successful in 7s
Build Lotus Chat Desktop / build-windows (push) Failing after 1m31s
Build Lotus Chat Desktop / build-linux (push) Failing after 15m17s
Build Lotus Chat Desktop / update-manifest (push) Has been skipped

Adds set_badge_count Tauri command (Windows only). Uses CoCreateInstance
to get ITaskbarList3 and SetOverlayIcon to display a dynamically drawn
badge on the taskbar button. The badge is a 16x16 GDI bitmap: red circle,
white bold Segoe UI text, capped at "99+". Passing count=0 clears the
overlay. Uses windows = 0.61 (already a transitive dep via webview2-com).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 17:32:53 -04:00
parent f0100c0c0c
commit b4812e6659
2 changed files with 140 additions and 0 deletions
+6
View File
@@ -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"
+134
View File
@@ -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<u16> = 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::<BITMAPINFOHEADER>() 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())