//! P5-44 — Taskbar thumbnail toolbar (call controls). //! //! While a voice/video call is active the web client calls `set_thumbbar` from //! `useTauriThumbbar`, which mirrors the call-embed atom + mic/sound state onto //! three buttons on the taskbar thumbnail toolbar: **Mute/Unmute**, //! **Deafen/Undeafen** and **End Call**. Clicking a button pushes a //! `thumbbar-action` DOM event back to the web side (`"mute" | "deafen" | "end"`) //! which drives the real call controls. //! //! Windows: `ITaskbarList3::ThumbBarAddButtons` (first call for the window) then //! `ThumbBarUpdateButtons` (subsequent calls) — mirrors the COM + GDI/HICON idiom //! in `set_badge_count`. Thumb-button clicks arrive as `WM_COMMAND` with //! `HIWORD(wParam) == THBN_CLICKED`, so we subclass the main window (installed //! once in `setup`) to catch them. The main window HWND comes from the "main" //! webview window; the "buttons added" flag lives in managed `ThumbbarState` //! (like lib.rs's `TrayUnreadState`) so add-vs-update works across calls. //! //! Other platforms are a no-op — the command stays cross-platform so the web //! side is unconditional. use tauri::{AppHandle, Manager}; /// Managed state shared with lib.rs (registered in `setup`). Only the Windows /// path reads `added`; kept cross-platform so `set_thumbbar` can inject it. #[derive(Default)] pub struct ThumbbarState { #[allow(dead_code)] added: std::sync::atomic::AtomicBool, } // Thumb-button ids (LOWORD of wParam on WM_COMMAND / THBN_CLICKED). #[cfg(target_os = "windows")] const BTN_MUTE: u32 = 1; #[cfg(target_os = "windows")] const BTN_DEAFEN: u32 = 2; #[cfg(target_os = "windows")] const BTN_END: u32 = 3; /// HIWORD(wParam) value on a thumb-button click (CommCtrl `THBN_CLICKED`). #[cfg(target_os = "windows")] const THBN_CLICKED: u16 = 0x1800; /// uIdSubclass passed to SetWindowSubclass — identifies our subclass instance. #[cfg(target_os = "windows")] const SUBCLASS_ID: usize = 1; /// Payload emitted to the web on a thumb-button click. #[cfg(target_os = "windows")] #[derive(serde::Serialize)] struct Action<'a> { action: &'a str, } /// Build a single THUMBBUTTON, attaching an icon (and the THB_ICON mask) when one /// was created. Always carries a tooltip and enabled/hidden flags. #[cfg(target_os = "windows")] fn thumb_button( id: u32, hidden: bool, tip: &str, icon: Option, ) -> windows::Win32::UI::Shell::THUMBBUTTON { use windows::Win32::UI::Shell::{ THUMBBUTTON, THUMBBUTTONFLAGS, THUMBBUTTONMASK, THBF_ENABLED, THBF_HIDDEN, THB_FLAGS, THB_ICON, THB_TOOLTIP, }; use windows::Win32::UI::WindowsAndMessaging::HICON; let mut mask: THUMBBUTTONMASK = THB_TOOLTIP | THB_FLAGS; let flags: THUMBBUTTONFLAGS = if hidden { THBF_HIDDEN } else { THBF_ENABLED }; let mut hicon = HICON::default(); if let Some(i) = icon { mask = mask | THB_ICON; hicon = i; } let mut sz_tip = [0u16; 260]; for (dst, ch) in sz_tip.iter_mut().zip(tip.encode_utf16().take(259)) { *dst = ch; } THUMBBUTTON { dwMask: mask, iId: id, iBitmap: 0, hIcon: hicon, szTip: sz_tip, dwFlags: flags, } } /// Update (or hide) the three thumb-toolbar buttons for the given call state. #[tauri::command] pub fn set_thumbbar( app: AppHandle, state: tauri::State<'_, ThumbbarState>, active: bool, muted: bool, deafened: bool, ) -> Result<(), String> { #[cfg(target_os = "windows")] { use std::sync::atomic::Ordering; use windows::Win32::{ Foundation::HWND, System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER}, UI::{ Shell::{ITaskbarList3, TaskbarList}, WindowsAndMessaging::DestroyIcon, }, }; // Nothing to do (and nothing to hide) if a toolbar was never added. if !active && !state.added.load(Ordering::SeqCst) { return Ok(()); } let window = app .get_webview_window("main") .ok_or_else(|| "no main window".to_string())?; let hwnd = HWND(window.hwnd().map_err(|e| e.to_string())?.0 as _); let mic_icon = make_icon(Glyph::Mic, muted); let deaf_icon = make_icon(Glyph::Head, deafened); let end_icon = make_icon(Glyph::End, false); let buttons = [ thumb_button(BTN_MUTE, !active, if muted { "Unmute" } else { "Mute" }, mic_icon), thumb_button( BTN_DEAFEN, !active, if deafened { "Undeafen" } else { "Deafen" }, deaf_icon, ), thumb_button(BTN_END, !active, "End Call", end_icon), ]; let result = unsafe { let taskbar: ITaskbarList3 = CoCreateInstance(&TaskbarList, None, CLSCTX_INPROC_SERVER) .map_err(|e| e.to_string())?; taskbar.HrInit().map_err(|e| e.to_string())?; let r = if state.added.load(Ordering::SeqCst) { taskbar.ThumbBarUpdateButtons(hwnd, &buttons) } else { let r = taskbar.ThumbBarAddButtons(hwnd, &buttons); if r.is_ok() { state.added.store(true, Ordering::SeqCst); } r }; r.map_err(|e| e.to_string()) }; // The shell copies the icons on add/update, so release ours (mirrors the // DestroyIcon after SetOverlayIcon in set_badge_count). for icon in [mic_icon, deaf_icon, end_icon].into_iter().flatten() { unsafe { let _ = DestroyIcon(icon); } } result?; } #[cfg(not(target_os = "windows"))] { // No-op elsewhere; bind args so the signature stays identical and no // unused warnings fire. let _ = (&app, &state, active, muted, deafened); } Ok(()) } /// Which glyph a thumb-button icon draws. #[cfg(target_os = "windows")] #[derive(Clone, Copy)] enum Glyph { Mic, Head, End, } /// Draw a simple white monochrome glyph onto a 32x32 32-bpp DIB and wrap it in an /// `HICON`. Mirrors the CreateDIBSection → alpha-fixup → CreateIconIndirect idiom /// in `set_badge_count`. Returns `None` on any GDI failure (the button is then /// added tooltip-only). `slashed` overlays a transparent diagonal cut to signal /// the muted / deafened state. #[cfg(target_os = "windows")] fn make_icon( glyph: Glyph, slashed: bool, ) -> Option { use windows::core::BOOL; use windows::Win32::Foundation::COLORREF; use windows::Win32::Graphics::Gdi::{ Arc, CreateBitmap, CreateCompatibleDC, CreateDIBSection, CreatePen, CreateSolidBrush, DeleteDC, DeleteObject, Ellipse, GetDC, LineTo, MoveToEx, ReleaseDC, RoundRect, SelectObject, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, PS_SOLID, }; use windows::Win32::UI::WindowsAndMessaging::{CreateIconIndirect, ICONINFO}; unsafe { let size = 32i32; let hdc_screen = GetDC(None); let hdc = CreateCompatibleDC(Some(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(Some(hdc), &bmi, DIB_RGB_COLORS, &mut bits, None, 0).ok()?; if !bits.is_null() { std::ptr::write_bytes(bits as *mut u8, 0, (size * size * 4) as usize); } let old_bm = SelectObject(hdc, hbm_color.into()); let white = COLORREF(0x00FF_FFFF); let hbrush = CreateSolidBrush(white); let old_brush = SelectObject(hdc, hbrush.into()); let hpen = CreatePen(PS_SOLID, 2, white); let old_pen = SelectObject(hdc, hpen.into()); match glyph { Glyph::Mic => { // Capsule mic head + stand. let _ = RoundRect(hdc, 13, 5, 19, 19, 6, 6); let _ = MoveToEx(hdc, 16, 19, None); let _ = LineTo(hdc, 16, 25); let _ = MoveToEx(hdc, 11, 25, None); let _ = LineTo(hdc, 21, 25); } Glyph::Head => { // Headphone band + two ear cups. let _ = Arc(hdc, 6, 7, 26, 27, 6, 17, 26, 17); let _ = RoundRect(hdc, 6, 16, 11, 26, 2, 2); let _ = RoundRect(hdc, 21, 16, 26, 26, 2, 2); } Glyph::End => { // Filled disc (end-call button). let _ = Ellipse(hdc, 6, 6, 26, 26); } } if slashed { // Draw the slash in black (pixel 0), which the alpha fixup below // leaves fully transparent — carving a visible diagonal gap. let black_pen = CreatePen(PS_SOLID, 4, COLORREF(0)); let prev = SelectObject(hdc, black_pen.into()); let _ = MoveToEx(hdc, 6, 6, None); let _ = LineTo(hdc, 26, 26); SelectObject(hdc, prev); let _ = DeleteObject(black_pen.into()); } SelectObject(hdc, old_brush); SelectObject(hdc, old_pen); SelectObject(hdc, old_bm); let _ = DeleteObject(hbrush.into()); let _ = DeleteObject(hpen.into()); // GDI leaves alpha at 0; mark every painted pixel opaque so Windows uses // per-pixel alpha instead of the opaque mask (same fix as set_badge_count). let pixel_count = (size * size) as usize; let pixels = std::slice::from_raw_parts_mut(bits as *mut u32, pixel_count); for pixel in pixels.iter_mut() { if *pixel != 0 { *pixel |= 0xFF00_0000u32; } } let hbm_mask = CreateBitmap(size, size, 1, 1, None); if hbm_mask.0 as usize == 0 { let _ = DeleteObject(hbm_color.into()); let _ = DeleteDC(hdc); let _ = ReleaseDC(None, hdc_screen); return None; } let icon_info = ICONINFO { fIcon: BOOL(1), xHotspot: 0, yHotspot: 0, hbmMask: hbm_mask, hbmColor: hbm_color, }; let hicon = CreateIconIndirect(&icon_info).ok(); let _ = DeleteObject(hbm_color.into()); let _ = DeleteObject(hbm_mask.into()); let _ = DeleteDC(hdc); let _ = ReleaseDC(None, hdc_screen); hicon } } /// Window subclass proc: catches thumb-button clicks (`WM_COMMAND` / /// `THBN_CLICKED`) and forwards them to the web as `thumbbar-action`. `dwrefdata` /// is a leaked `Box` installed by `setup`; it is reclaimed on /// `WM_NCDESTROY`. #[cfg(target_os = "windows")] unsafe extern "system" fn subclass_proc( hwnd: windows::Win32::Foundation::HWND, umsg: u32, wparam: windows::Win32::Foundation::WPARAM, lparam: windows::Win32::Foundation::LPARAM, _uidsubclass: usize, dwrefdata: usize, ) -> windows::Win32::Foundation::LRESULT { use windows::Win32::Foundation::LRESULT; use windows::Win32::UI::Shell::{DefSubclassProc, RemoveWindowSubclass}; use windows::Win32::UI::WindowsAndMessaging::{WM_COMMAND, WM_NCDESTROY}; match umsg { WM_COMMAND => { let w = wparam.0; let notif = ((w >> 16) & 0xFFFF) as u16; let id = (w & 0xFFFF) as u32; if notif == THBN_CLICKED { let action = match id { BTN_MUTE => Some("mute"), BTN_DEAFEN => Some("deafen"), BTN_END => Some("end"), _ => None, }; if let Some(action) = action { if dwrefdata != 0 { // Borrow (do not take ownership of) the leaked AppHandle. let app = &*(dwrefdata as *const AppHandle); let detail = serde_json::to_string(&Action { action }).unwrap_or_default(); super::emit_to_web(app, "thumbbar-action", &detail); } return LRESULT(0); } } DefSubclassProc(hwnd, umsg, wparam, lparam) } WM_NCDESTROY => { let _ = RemoveWindowSubclass(hwnd, Some(subclass_proc), SUBCLASS_ID); if dwrefdata != 0 { drop(Box::from_raw(dwrefdata as *mut AppHandle)); } DefSubclassProc(hwnd, umsg, wparam, lparam) } _ => DefSubclassProc(hwnd, umsg, wparam, lparam), } } /// Called once from `native::setup`. Registers `ThumbbarState` and, on Windows, /// subclasses the main window so thumb-button clicks reach the web client. pub fn setup(app: &AppHandle) -> tauri::Result<()> { app.manage(ThumbbarState::default()); #[cfg(target_os = "windows")] { use windows::Win32::Foundation::HWND; use windows::Win32::UI::Shell::SetWindowSubclass; if let Some(window) = app.get_webview_window("main") { if let Ok(handle) = window.hwnd() { let hwnd = HWND(handle.0 as _); // Leak an AppHandle for the proc; reclaimed on WM_NCDESTROY. let refdata = Box::into_raw(Box::new(app.clone())) as usize; unsafe { let _ = SetWindowSubclass(hwnd, Some(subclass_proc), SUBCLASS_ID, refdata); } } } } Ok(()) }