The Windows overlay badge now renders a single combined number and includes an additional “changes” component in the total. This makes the badge more informative while keeping the icon compact and legible across small sizes. - Simplify badge rendering to one circle with centered text - Extend the Tauri command and frontend call to pass changes count
187 lines
6.0 KiB
Rust
187 lines
6.0 KiB
Rust
//! 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<Image<'static>> {
|
|
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(())
|
|
}
|