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(())
|
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() {
|
pub fn run() {
|
||||||
let port: u16 = 44548;
|
let port: u16 = 44548;
|
||||||
let context = tauri::generate_context!();
|
let context = tauri::generate_context!();
|
||||||
@@ -295,6 +360,8 @@ pub fn run() {
|
|||||||
builder = builder
|
builder = builder
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
set_badge_count,
|
set_badge_count,
|
||||||
|
set_tray_unread,
|
||||||
|
flash_window,
|
||||||
send_notification,
|
send_notification,
|
||||||
check_for_update,
|
check_for_update,
|
||||||
install_update,
|
install_update,
|
||||||
@@ -314,12 +381,13 @@ pub fn run() {
|
|||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
// --- System tray: keeps Lotus Chat running in the background so
|
// --- System tray: keeps Lotus Chat running in the background so
|
||||||
// notifications keep arriving after the window is closed-to-tray. ---
|
// 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 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 quit_item = MenuItem::with_id(app, "quit", "Quit Lotus Chat", true, None::<&str>)?;
|
||||||
let separator = PredefinedMenuItem::separator(app)?;
|
let separator = PredefinedMenuItem::separator(app)?;
|
||||||
let tray_menu = Menu::with_items(app, &[&open_item, &separator, &quit_item])?;
|
let tray_menu = Menu::with_items(app, &[&open_item, &separator, &quit_item])?;
|
||||||
let _tray = TrayIconBuilder::with_id("main-tray")
|
let tray = TrayIconBuilder::with_id("main-tray")
|
||||||
.icon(app.default_window_icon().cloned().unwrap())
|
.icon(base_icon.clone().expect("bundled window icon"))
|
||||||
.tooltip("Lotus Chat")
|
.tooltip("Lotus Chat")
|
||||||
.menu(&tray_menu)
|
.menu(&tray_menu)
|
||||||
.show_menu_on_left_click(false)
|
.show_menu_on_left_click(false)
|
||||||
@@ -347,6 +415,19 @@ pub fn run() {
|
|||||||
})
|
})
|
||||||
.build(app)?;
|
.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)]
|
#[cfg(debug_assertions)]
|
||||||
let window_url = WebviewUrl::App(Default::default());
|
let window_url = WebviewUrl::App(Default::default());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user