use std::sync::mpsc::{self, Receiver, Sender};
use std::time::Duration;
use crate::glyph::TrayBitmap;
pub const MENU_OPEN: &str = "costroid.open";
pub const MENU_REFRESH: &str = "costroid.refresh";
pub const MENU_QUIT: &str = "costroid.quit";
const EVENT_POLL: Duration = Duration::from_millis(120);
#[cfg(target_os = "linux")]
const COMMAND_POLL: Duration = Duration::from_millis(150);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrayAction {
Toggle,
Show,
Refresh,
Quit,
}
pub struct TrayController {
#[cfg(target_os = "linux")]
cmd_tx: Option<Sender<TrayCommand>>,
#[cfg(not(target_os = "linux"))]
tray: Option<tray_icon::TrayIcon>,
}
impl TrayController {
pub fn update(&self, bitmap: &TrayBitmap, tooltip: &str) {
#[cfg(target_os = "linux")]
if let Some(tx) = &self.cmd_tx {
let _ = tx.send(TrayCommand::Update {
rgba: bitmap.rgba.clone(),
width: bitmap.width,
height: bitmap.height,
tooltip: tooltip.to_owned(),
});
}
#[cfg(not(target_os = "linux"))]
if let Some(tray) = &self.tray {
if let Ok(icon) =
tray_icon::Icon::from_rgba(bitmap.rgba.clone(), bitmap.width, bitmap.height)
{
let _ = tray.set_icon(Some(icon));
}
let _ = tray.set_tooltip(Some(tooltip));
}
}
pub fn shutdown(&self) {
#[cfg(target_os = "linux")]
if let Some(tx) = &self.cmd_tx {
let _ = tx.send(TrayCommand::Quit);
}
}
pub fn is_active(&self) -> bool {
#[cfg(target_os = "linux")]
let active = self.cmd_tx.is_some();
#[cfg(not(target_os = "linux"))]
let active = self.tray.is_some();
active
}
}
pub fn spawn(initial: &TrayBitmap, tooltip: &str) -> TrayController {
#[cfg(target_os = "linux")]
{
spawn_linux(initial, tooltip)
}
#[cfg(not(target_os = "linux"))]
{
spawn_native(initial, tooltip)
}
}
pub fn spawn_event_bridge(ctx: egui::Context) -> Receiver<TrayAction> {
let (tx, rx) = mpsc::channel::<TrayAction>();
let spawned = std::thread::Builder::new()
.name("costroid-bar-tray-events".to_owned())
.spawn(move || event_bridge_loop(&ctx, &tx));
if let Err(err) = spawned {
eprintln!("costroid-bar: could not start the tray event bridge: {err}");
}
rx
}
fn event_bridge_loop(ctx: &egui::Context, tx: &Sender<TrayAction>) {
use tray_icon::menu::MenuEvent;
use tray_icon::{MouseButton, MouseButtonState, TrayIconEvent};
let menu_rx = MenuEvent::receiver();
let tray_rx = TrayIconEvent::receiver();
loop {
let mut woke = false;
while let Ok(event) = menu_rx.try_recv() {
let action = match event.id.0.as_str() {
MENU_OPEN => Some(TrayAction::Show),
MENU_REFRESH => Some(TrayAction::Refresh),
MENU_QUIT => Some(TrayAction::Quit),
_ => None,
};
if let Some(action) = action {
if tx.send(action).is_err() {
return;
}
woke = true;
}
}
while let Ok(event) = tray_rx.try_recv() {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
if tx.send(TrayAction::Toggle).is_err() {
return;
}
woke = true;
}
}
if woke {
ctx.request_repaint();
}
std::thread::sleep(EVENT_POLL);
}
}
fn build_tray(
rgba: &[u8],
width: u32,
height: u32,
tooltip: &str,
) -> anyhow::Result<tray_icon::TrayIcon> {
use tray_icon::menu::{Menu, MenuItem, PredefinedMenuItem};
let menu = Menu::new();
menu.append(&MenuItem::with_id(MENU_OPEN, "Open Costroid", true, None))?;
menu.append(&PredefinedMenuItem::separator())?;
menu.append(&MenuItem::with_id(MENU_REFRESH, "Refresh now", true, None))?;
menu.append(&MenuItem::with_id(MENU_QUIT, "Quit", true, None))?;
let icon = tray_icon::Icon::from_rgba(rgba.to_vec(), width, height)?;
let tray = tray_icon::TrayIconBuilder::new()
.with_menu(Box::new(menu))
.with_tooltip(tooltip)
.with_title("costroid")
.with_icon(icon)
.build()?;
Ok(tray)
}
#[cfg(target_os = "linux")]
enum TrayCommand {
Update {
rgba: Vec<u8>,
width: u32,
height: u32,
tooltip: String,
},
Quit,
}
#[cfg(target_os = "linux")]
struct TrayInit {
rgba: Vec<u8>,
width: u32,
height: u32,
tooltip: String,
}
#[cfg(target_os = "linux")]
fn spawn_linux(initial: &TrayBitmap, tooltip: &str) -> TrayController {
let (cmd_tx, cmd_rx) = mpsc::channel::<TrayCommand>();
let init = TrayInit {
rgba: initial.rgba.clone(),
width: initial.width,
height: initial.height,
tooltip: tooltip.to_owned(),
};
let spawned = std::thread::Builder::new()
.name("costroid-bar-tray".to_owned())
.spawn(move || run_linux_tray(init, cmd_rx));
match spawned {
Ok(_handle) => TrayController {
cmd_tx: Some(cmd_tx),
},
Err(err) => {
eprintln!("costroid-bar: could not start the tray thread: {err}; running window-only.");
TrayController { cmd_tx: None }
}
}
}
#[cfg(target_os = "linux")]
fn run_linux_tray(init: TrayInit, cmd_rx: Receiver<TrayCommand>) {
use gtk::glib;
if let Err(err) = gtk::init() {
eprintln!(
"costroid-bar: GTK init failed ({err}); the Linux tray is unavailable — running window-only."
);
return;
}
let tray = match build_tray(&init.rgba, init.width, init.height, &init.tooltip) {
Ok(tray) => tray,
Err(err) => {
eprintln!("costroid-bar: could not create the tray ({err}); running window-only.");
return;
}
};
glib::timeout_add_local(COMMAND_POLL, move || loop {
match cmd_rx.try_recv() {
Ok(TrayCommand::Update {
rgba,
width,
height,
tooltip,
}) => {
if let Ok(icon) = tray_icon::Icon::from_rgba(rgba, width, height) {
let _ = tray.set_icon(Some(icon));
}
let _ = tray.set_tooltip(Some(&tooltip));
}
Ok(TrayCommand::Quit) => {
gtk::main_quit();
return glib::ControlFlow::Break;
}
Err(mpsc::TryRecvError::Empty) => return glib::ControlFlow::Continue,
Err(mpsc::TryRecvError::Disconnected) => {
gtk::main_quit();
return glib::ControlFlow::Break;
}
}
});
gtk::main();
}
#[cfg(not(target_os = "linux"))]
fn spawn_native(initial: &TrayBitmap, tooltip: &str) -> TrayController {
match build_tray(&initial.rgba, initial.width, initial.height, tooltip) {
Ok(tray) => TrayController { tray: Some(tray) },
Err(err) => {
eprintln!("costroid-bar: could not create the tray ({err}); running window-only.");
TrayController { tray: None }
}
}
}