feat(desktop): tray unread overlay + taskbar flash
- set_tray_unread command re-renders the tray icon with a red unread dot composited onto the base icon (handle + base pixels kept in managed state). - flash_window command flashes the taskbar via request_user_attention. - Register both commands; bump cinny submodule to 107921e0 (web wiring). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
Submodule cinny updated: 053b364a44...107921e0d0
+83
-2
@@ -272,6 +272,71 @@ fn set_badge_count(count: u32, window: tauri::Window) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Held in managed state so the tray's unread overlay can be updated at runtime.
|
||||
/// Keeping the TrayIcon handle here also keeps the tray alive.
|
||||
struct TrayUnreadState {
|
||||
tray: tauri::tray::TrayIcon,
|
||||
base_rgba: Vec<u8>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
/// Paint a small white-ringed red "unread" dot into the bottom-right corner of
|
||||
/// an RGBA buffer, in place. Cross-platform (operates on raw pixels).
|
||||
fn draw_unread_dot(rgba: &mut [u8], width: u32, height: u32) {
|
||||
let w = width as i32;
|
||||
let h = height as i32;
|
||||
if w <= 0 || h <= 0 {
|
||||
return;
|
||||
}
|
||||
let r = ((w.min(h) as f32) * 0.30) as i32;
|
||||
let ring = ((r as f32) * 0.18).max(1.0) as i32;
|
||||
let cx = w - r - ((w as f32) * 0.06) as i32;
|
||||
let cy = h - r - ((h as f32) * 0.06) as i32;
|
||||
for y in (cy - r - ring).max(0)..(cy + r + ring).min(h) {
|
||||
for x in (cx - r - ring).max(0)..(cx + r + ring).min(w) {
|
||||
let dx = x - cx;
|
||||
let dy = y - cy;
|
||||
let dist2 = dx * dx + dy * dy;
|
||||
let idx = ((y * w + x) as usize) * 4;
|
||||
if idx + 3 >= rgba.len() {
|
||||
continue;
|
||||
}
|
||||
if dist2 <= r * r {
|
||||
rgba[idx] = 0xDD;
|
||||
rgba[idx + 1] = 0x30;
|
||||
rgba[idx + 2] = 0x30;
|
||||
rgba[idx + 3] = 0xFF;
|
||||
} else if dist2 <= (r + ring) * (r + ring) {
|
||||
rgba[idx] = 0xFF;
|
||||
rgba[idx + 1] = 0xFF;
|
||||
rgba[idx + 2] = 0xFF;
|
||||
rgba[idx + 3] = 0xFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Overlay (or clear) the unread dot on the tray icon.
|
||||
#[tauri::command]
|
||||
fn set_tray_unread(unread: bool, state: tauri::State<'_, TrayUnreadState>) -> Result<(), String> {
|
||||
let mut rgba = state.base_rgba.clone();
|
||||
if unread {
|
||||
draw_unread_dot(&mut rgba, state.width, state.height);
|
||||
}
|
||||
let icon = tauri::image::Image::new_owned(rgba, state.width, state.height);
|
||||
state.tray.set_icon(Some(icon)).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Flash the taskbar button to draw attention (e.g. a new mention while the
|
||||
/// window is unfocused). Clears automatically once the window is focused.
|
||||
#[tauri::command]
|
||||
fn flash_window(window: tauri::Window) -> Result<(), String> {
|
||||
window
|
||||
.request_user_attention(Some(tauri::UserAttentionType::Informational))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
let port: u16 = 44548;
|
||||
let context = tauri::generate_context!();
|
||||
@@ -295,6 +360,8 @@ pub fn run() {
|
||||
builder = builder
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
set_badge_count,
|
||||
set_tray_unread,
|
||||
flash_window,
|
||||
send_notification,
|
||||
check_for_update,
|
||||
install_update,
|
||||
@@ -314,12 +381,13 @@ pub fn run() {
|
||||
.setup(move |app| {
|
||||
// --- System tray: keeps Lotus Chat running in the background so
|
||||
// notifications keep arriving after the window is closed-to-tray. ---
|
||||
let base_icon = app.default_window_icon().cloned();
|
||||
let open_item = MenuItem::with_id(app, "open", "Open Lotus Chat", true, None::<&str>)?;
|
||||
let quit_item = MenuItem::with_id(app, "quit", "Quit Lotus Chat", true, None::<&str>)?;
|
||||
let separator = PredefinedMenuItem::separator(app)?;
|
||||
let tray_menu = Menu::with_items(app, &[&open_item, &separator, &quit_item])?;
|
||||
let _tray = TrayIconBuilder::with_id("main-tray")
|
||||
.icon(app.default_window_icon().cloned().unwrap())
|
||||
let tray = TrayIconBuilder::with_id("main-tray")
|
||||
.icon(base_icon.clone().expect("bundled window icon"))
|
||||
.tooltip("Lotus Chat")
|
||||
.menu(&tray_menu)
|
||||
.show_menu_on_left_click(false)
|
||||
@@ -347,6 +415,19 @@ pub fn run() {
|
||||
})
|
||||
.build(app)?;
|
||||
|
||||
// Keep the tray handle (and base icon pixels) in managed state so
|
||||
// set_tray_unread can re-render the icon at runtime.
|
||||
if let Some(img) = base_icon {
|
||||
let base_rgba = img.rgba().to_vec();
|
||||
let (width, height) = (img.width(), img.height());
|
||||
app.manage(TrayUnreadState {
|
||||
tray,
|
||||
base_rgba,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let window_url = WebviewUrl::App(Default::default());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user