Files
GitLite/src-tauri/src/badge.rs
T
Christoph 900a6fa230
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 18m21s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Failing after 1h43m55s
feat(core): improve platform compatibility and startup robustness
This update enhances cross-platform reliability by addressing potential hangs during application startup on Linux environments using WebKitGTK. Additionally, native taskbar badge functionality is improved for macOS and Linux, ensuring accurate display of sync status. Configuration settings are also updated to include more specific internal crate reads and web search capabilities.

- Added timeout fallback to waitForAnimationFrame in Svelte
- Improved set_sync_badge logic for macOS/Linux platforms
- Updated local configuration with detailed dependency reads
2026-07-10 20:24:48 +02:00

201 lines
6.7 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}"))?;
}
// Native badge count: works on Linux (via the desktop's launcher API, e.g. Unity's
// LauncherEntry, also honored by GNOME/KDE) and macOS (dock badge). Matched against the
// app's `<product-name>.desktop` file on Linux, so it's a silent no-op on launchers that
// don't implement the protocol.
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let Some(window) = app.get_webview_window("main") else {
return Ok(());
};
window
.set_badge_count(if count > 0 { Some(count as i64) } else { None })
.map_err(|err| format!("Could not set taskbar badge: {err}"))?;
}
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
{
let _ = (app, count);
}
Ok(())
}