//! Renders the combined repository attention count as a taskbar badge and applies it as the //! Windows taskbar overlay. Windows has no native numeric taskbar badge (unlike //! macOS/Linux, which support `Window::set_badge_count`), so the numbers have to be drawn //! into a plain RGBA icon ourselves and set via `Window::set_overlay_icon`. use tauri::Manager; use tauri::image::Image; const GLYPH_WIDTH: usize = 3; const GLYPH_HEIGHT: usize = 5; /// A minimal 3x5 pixel-grid font, just enough to draw digits and "+" legibly at badge size. fn glyph_rows(ch: char) -> [&'static str; GLYPH_HEIGHT] { match ch { '0' => ["111", "101", "101", "101", "111"], '1' => ["010", "110", "010", "010", "111"], '2' => ["111", "001", "111", "100", "111"], '3' => ["111", "001", "111", "001", "111"], '4' => ["101", "101", "111", "001", "001"], '5' => ["111", "100", "111", "001", "111"], '6' => ["111", "100", "111", "101", "111"], '7' => ["111", "001", "010", "010", "010"], '8' => ["111", "101", "111", "101", "111"], '9' => ["111", "101", "111", "001", "111"], '+' => ["000", "010", "111", "010", "000"], _ => ["000", "000", "000", "000", "000"], } } const BADGE_FILL: [u8; 4] = [224, 160, 64, 255]; const BADGE_BORDER: [u8; 4] = [176, 118, 40, 255]; const TEXT_FILL: [u8; 4] = [255, 255, 255, 255]; const TEXT_SHADOW: [u8; 4] = [10, 12, 24, 190]; const CIRCLE_BORDER_WIDTH: f32 = 3.0; /// Caps the displayed text at three characters ("99+") so it always fits legibly. fn cap_text(count: u32) -> String { if count > 99 { "99+".to_string() } else { count.to_string() } } /// Draws a filled circle (with a slightly darker rim) centered at `(cx, cy)`. fn draw_circle( rgba: &mut [u8], size: usize, cx: f32, cy: f32, radius: f32, fill: [u8; 4], border: [u8; 4], ) { for y in 0..size { for x in 0..size { let dx = x as f32 + 0.5 - cx; let dy = y as f32 + 0.5 - cy; let dist = (dx * dx + dy * dy).sqrt(); if dist <= radius { let color = if dist >= radius - CIRCLE_BORDER_WIDTH { border } else { fill }; let idx = (y * size + x) * 4; rgba[idx..idx + 4].copy_from_slice(&color); } } } } fn draw_text_layer( rgba: &mut [u8], size: usize, text: &str, cx: f32, cy: f32, scale: usize, offset: (isize, isize), color: [u8; 4], ) { let glyphs: Vec<[&'static str; GLYPH_HEIGHT]> = text.chars().map(glyph_rows).collect(); let glyph_px_w = GLYPH_WIDTH * scale; let glyph_px_h = GLYPH_HEIGHT * scale; let gap = scale.max(1); let total_w = glyphs.len() * glyph_px_w + gap * glyphs.len().saturating_sub(1); let start_x = (cx - total_w as f32 / 2.0).round() as isize + offset.0; let start_y = (cy - glyph_px_h as f32 / 2.0).round() as isize + offset.1; for (gi, rows) in glyphs.iter().enumerate() { let glyph_x = start_x + (gi * (glyph_px_w + gap)) as isize; for (gy, row) in rows.iter().enumerate() { for (gx, pixel) in row.chars().enumerate() { if pixel != '1' { continue; } for py in 0..scale { for px in 0..scale { let x = glyph_x + (gx * scale + px) as isize; let y = start_y + (gy * scale + py) as isize; if x >= 0 && y >= 0 && (x as usize) < size && (y as usize) < size { let idx = (y as usize * size + x as usize) * 4; rgba[idx..idx + 4].copy_from_slice(&color); } } } } } } } /// Draws white `text` centered at `(cx, cy)`, scaling each font pixel up by `scale`. fn draw_text(rgba: &mut [u8], size: usize, text: &str, cx: f32, cy: f32, scale: usize) { for offset in [(-1, 0), (1, 0), (0, -1), (0, 1)] { draw_text_layer(rgba, size, text, cx, cy, scale, offset, TEXT_SHADOW); } draw_text_layer(rgba, size, text, cx, cy, scale, (0, 0), TEXT_FILL); } fn text_scale(text: &str) -> usize { match text.len() { 0 | 1 => 7, 2 => 5, _ => 4, } } /// Renders a single large badge with `count`. Returns `None` when there's nothing to show, /// so the caller can clear the overlay icon. fn render_badge_icon(count: u32) -> Option> { if count == 0 { return None; } const SIZE: usize = 64; const RADIUS: f32 = 29.0; let mut rgba = vec![0u8; SIZE * SIZE * 4]; let center = SIZE as f32 / 2.0; let text = cap_text(count); draw_circle( &mut rgba, SIZE, center, center, RADIUS, BADGE_FILL, BADGE_BORDER, ); draw_text(&mut rgba, SIZE, &text, center, center, text_scale(&text)); Some(Image::new_owned(rgba, SIZE as u32, SIZE as u32)) } /// Sets the taskbar badge to `ahead + behind + changes` (0 clears it). Windows-only: Windows /// has no native numeric badge API, so this draws and applies a small overlay icon instead. /// No-op on other platforms: non-Windows desktops should use `Window::set_badge_count` /// for a real native badge instead, which this app doesn't currently wire up. #[tauri::command] pub fn set_sync_badge( app: tauri::AppHandle, ahead: u32, behind: u32, changes: u32, ) -> Result<(), String> { let count = ahead.saturating_add(behind).saturating_add(changes); #[cfg(target_os = "windows")] { let Some(window) = app.get_webview_window("main") else { return Ok(()); }; let icon = render_badge_icon(count); window .set_overlay_icon(icon) .map_err(|err| format!("Could not set taskbar badge: {err}"))?; } #[cfg(not(target_os = "windows"))] { let _ = (app, count); } Ok(()) }