use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Mutex;
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, POINT, WPARAM};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::Shell::{
Shell_NotifyIconW, NIF_ICON, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_DELETE, NIM_MODIFY,
NOTIFYICONDATAW,
};
use windows::Win32::UI::WindowsAndMessaging::{
AppendMenuW, CreatePopupMenu, CreateWindowExW, DefWindowProcW, DestroyMenu, DispatchMessageW,
GetCursorPos, GetMessageW, LoadIconW, LoadImageW, PostMessageW, PostQuitMessage,
RegisterClassW, RegisterWindowMessageW, SetForegroundWindow, TrackPopupMenu, TranslateMessage,
CW_USEDEFAULT, HICON, IDI_APPLICATION, IMAGE_ICON, LR_DEFAULTSIZE, LR_LOADFROMFILE, MF_GRAYED,
MF_SEPARATOR, MF_STRING, MSG, TPM_BOTTOMALIGN, TPM_RIGHTALIGN, WINDOW_EX_STYLE, WINDOW_STYLE,
WM_APP, WM_CLOSE, WM_COMMAND, WM_DESTROY, WM_LBUTTONUP, WM_RBUTTONUP, WNDCLASSW,
};
use super::{Chosen, Listed, Mood, Standing, Trouble};
const CALLBACK: u32 = WM_APP + 1;
const REFRESH: u32 = WM_APP + 2;
const QUIT: usize = 0xF000;
#[derive(Default)]
struct Shown {
lines: Vec<Listed>,
troubles: Vec<Trouble>,
mood: Mood,
}
struct Shared {
shown: Mutex<Shown>,
chosen: Mutex<Vec<Chosen>>,
}
thread_local! {
static STATE: std::cell::RefCell<Option<std::sync::Arc<Shared>>> =
const { std::cell::RefCell::new(None) };
}
pub struct Tray {
shared: std::sync::Arc<Shared>,
window: Mutex<Option<isize>>,
ready: Receiver<()>,
}
impl Tray {
pub fn show_up() -> windows::core::Result<Self> {
let shared = std::sync::Arc::new(Shared {
shown: Mutex::default(),
chosen: Mutex::default(),
});
let (ready_tx, ready) = channel();
let (window_tx, window_rx) = channel();
let theirs = std::sync::Arc::clone(&shared);
std::thread::Builder::new()
.name("slipcase-open tray".to_owned())
.spawn(move || pump(&theirs, &window_tx, &ready_tx))?;
let window = window_rx.recv().map_err(|_| {
windows::core::Error::new(
windows::Win32::Foundation::E_FAIL,
"the tray thread could not make its window",
)
})?;
Ok(Self {
shared,
window: Mutex::new(Some(window)),
ready,
})
}
}
impl Drop for Tray {
fn drop(&mut self) {
if let Ok(mut window) = self.window.lock() {
if let Some(hwnd) = window.take() {
#[allow(unsafe_code)]
unsafe {
let _ =
PostMessageW(Some(HWND(hwnd as *mut _)), WM_CLOSE, WPARAM(0), LPARAM(0));
}
}
}
let _ = self
.ready
.recv_timeout(std::time::Duration::from_millis(500));
}
}
impl Standing for Tray {
fn show(&self, sessions: &[Listed], troubles: &[Trouble], mood: Mood) {
if let Ok(mut shown) = self.shared.shown.lock() {
sessions.clone_into(&mut shown.lines);
troubles.clone_into(&mut shown.troubles);
shown.mood = mood;
}
if let Ok(window) = self.window.lock() {
if let Some(hwnd) = *window {
#[allow(unsafe_code)]
unsafe {
let _ = PostMessageW(Some(HWND(hwnd as *mut _)), REFRESH, WPARAM(0), LPARAM(0));
}
}
}
}
fn taken(&self) -> Vec<Chosen> {
self.shared
.chosen
.lock()
.map_or_else(|_| Vec::new(), |mut c| std::mem::take(&mut *c))
}
fn holding(&self) -> bool {
true
}
}
#[allow(unsafe_code)]
fn redress(hwnd: HWND, shown: &Shown) {
let mut data = icon_data(hwnd, Some(icon(shown.mood)));
let text = match (shown.troubles.first(), shown.lines.len()) {
(Some(first), _) => format!("Slipcase Open - {}", first.summary),
(None, 0) => "Slipcase Open - nothing open".to_owned(),
(None, 1) => {
"Slipcase Open - 1 content file open; saves go back into its container".to_owned()
}
(None, n) => {
format!("Slipcase Open - {n} content files open; saves go back into their containers")
}
};
for (i, c) in text.encode_utf16().enumerate().take(127) {
data.szTip[i] = c;
}
data.uFlags = NIF_ICON | NIF_TIP;
unsafe {
let _ = Shell_NotifyIconW(NIM_MODIFY, &raw const data);
}
}
fn icon_data(hwnd: HWND, icon: Option<HICON>) -> NOTIFYICONDATAW {
NOTIFYICONDATAW {
cbSize: u32::try_from(std::mem::size_of::<NOTIFYICONDATAW>()).unwrap_or(0),
hWnd: hwnd,
uID: 1,
uCallbackMessage: CALLBACK,
hIcon: icon.unwrap_or_default(),
..Default::default()
}
}
const fn artwork(mood: Mood) -> &'static str {
match mood {
Mood::Settled => "slipcase-open.ico",
Mood::Working => "slipcase-open-working.ico",
Mood::Look => "slipcase-open-yellow.ico",
Mood::AtRisk => "slipcase-open-orange.ico",
Mood::Danger => "slipcase-open-red.ico",
}
}
thread_local! {
static ICONS: std::cell::RefCell<std::collections::HashMap<&'static str, HICON>> =
std::cell::RefCell::new(std::collections::HashMap::new());
}
#[allow(unsafe_code)]
fn icon(mood: Mood) -> HICON {
let name = artwork(mood);
ICONS.with(|cache| {
if let Some(had) = cache.borrow().get(name) {
return *had;
}
let loaded = load(name);
cache.borrow_mut().insert(name, loaded);
loaded
})
}
#[allow(unsafe_code)]
fn load(name: &str) -> HICON {
if let Ok(exe) = std::env::current_exe() {
let beside = exe.with_file_name(name);
if beside.exists() {
use std::os::windows::ffi::OsStrExt as _;
let wide: Vec<u16> = beside
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let loaded = unsafe {
LoadImageW(
None,
PCWSTR(wide.as_ptr()),
IMAGE_ICON,
0,
0,
LR_LOADFROMFILE | LR_DEFAULTSIZE,
)
};
if let Ok(handle) = loaded {
return HICON(handle.0);
}
}
}
unsafe { LoadIconW(None, IDI_APPLICATION) }.unwrap_or_default()
}
#[allow(unsafe_code)]
fn pump(shared: &std::sync::Arc<Shared>, window: &Sender<isize>, ready: &Sender<()>) {
STATE.with(|s| *s.borrow_mut() = Some(std::sync::Arc::clone(shared)));
unsafe {
let Ok(instance) = GetModuleHandleW(None) else {
return;
};
let class = w!("slipcase-open-tray");
let wc = WNDCLASSW {
lpfnWndProc: Some(procedure),
hInstance: instance.into(),
lpszClassName: class,
..Default::default()
};
let _ = RegisterClassW(&raw const wc);
let Ok(hwnd) = CreateWindowExW(
WINDOW_EX_STYLE(0),
class,
w!("slipcase-open"),
WINDOW_STYLE(0),
CW_USEDEFAULT,
CW_USEDEFAULT,
0,
0,
None,
None,
Some(instance.into()),
None,
) else {
return;
};
let mut data = icon_data(hwnd, Some(icon(Mood::Settled)));
data.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
for (i, c) in "Slipcase Open".encode_utf16().enumerate().take(127) {
data.szTip[i] = c;
}
if !Shell_NotifyIconW(NIM_ADD, &raw const data).as_bool() {
return;
}
if window.send(hwnd.0 as isize).is_err() {
let _ = Shell_NotifyIconW(NIM_DELETE, &raw const data);
return;
}
let restarted = RegisterWindowMessageW(w!("TaskbarCreated"));
let mut msg = MSG::default();
while GetMessageW(&raw mut msg, None, 0, 0).as_bool() {
if msg.message == restarted && restarted != 0 {
let _ = Shell_NotifyIconW(NIM_ADD, &raw const data);
}
let _ = TranslateMessage(&raw const msg);
DispatchMessageW(&raw const msg);
}
let _ = Shell_NotifyIconW(NIM_DELETE, &raw const data);
}
let _ = ready.send(());
}
#[allow(unsafe_code)]
unsafe extern "system" fn procedure(hwnd: HWND, msg: u32, w: WPARAM, l: LPARAM) -> LRESULT {
unsafe {
match msg {
CALLBACK => {
let what = u32::try_from(l.0).unwrap_or(0);
if what == WM_RBUTTONUP || what == WM_LBUTTONUP {
offer(hwnd);
}
LRESULT(0)
}
REFRESH => {
STATE.with(|s| {
let Some(shared) = s.borrow().as_ref().map(std::sync::Arc::clone) else {
return;
};
let shown = shared.shown.lock();
if let Ok(shown) = shown {
redress(hwnd, &shown);
}
});
LRESULT(0)
}
WM_COMMAND => {
let picked = w.0 & 0xFFFF;
STATE.with(|s| {
let Some(shared) = s.borrow().as_ref().map(std::sync::Arc::clone) else {
return;
};
let what = if picked == QUIT {
Some(Chosen::Quit)
} else {
shared
.shown
.lock()
.ok()
.and_then(|shown| shown.troubles.get(picked.wrapping_sub(1)).cloned())
.map(|trouble| Chosen::Dismiss(trouble.id))
};
if let Some(what) = what {
if let Ok(mut chosen) = shared.chosen.lock() {
chosen.push(what);
}
}
});
LRESULT(0)
}
WM_CLOSE | WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, w, l),
}
}
}
#[allow(unsafe_code)]
unsafe fn offer(hwnd: HWND) {
unsafe {
let Ok(menu) = CreatePopupMenu() else {
return;
};
let shown = STATE.with(|s| {
s.borrow()
.as_ref()
.and_then(|shared| {
shared.shown.lock().ok().map(|shown| Shown {
lines: shown.lines.clone(),
troubles: shown.troubles.clone(),
mood: shown.mood,
})
})
.unwrap_or_default()
});
if !shown.troubles.is_empty() {
let _ = AppendMenuW(
menu,
MF_STRING | MF_GRAYED,
0,
w!("Needs a look - click one to clear it"),
);
for (at, trouble) in shown.troubles.iter().enumerate() {
let text: Vec<u16> = trouble
.summary
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let _ = AppendMenuW(menu, MF_STRING, at + 1, PCWSTR(text.as_ptr()));
}
let _ = AppendMenuW(menu, MF_SEPARATOR, 0, PCWSTR::null());
}
let lines: Vec<&Listed> = shown
.lines
.iter()
.filter(|l| l.live || l.needs_a_person)
.collect();
if lines.is_empty() {
if shown.troubles.is_empty() {
let _ = AppendMenuW(
menu,
MF_STRING | MF_GRAYED,
0,
w!("Nothing open - double-click a .slpc container"),
);
}
} else {
for listed in lines {
let label = match (listed.needs_a_person, listed.write_backs) {
(true, _) => {
format!("{} - left behind, needs a decision", listed.content_name)
}
(false, Some(0u64)) => {
format!("{} - open, nothing saved yet", listed.content_name)
}
(false, Some(1u64)) => format!("{} - saved once", listed.content_name),
(false, Some(n)) => format!("{} - saved {n} times", listed.content_name),
(false, None) => listed.content_name.clone(),
};
let text: Vec<u16> = label.encode_utf16().chain(std::iter::once(0)).collect();
let _ = AppendMenuW(menu, MF_STRING | MF_GRAYED, 0, PCWSTR(text.as_ptr()));
}
}
let _ = AppendMenuW(menu, MF_SEPARATOR, 0, PCWSTR::null());
let _ = AppendMenuW(menu, MF_STRING, QUIT, w!("Quit"));
let mut at = POINT::default();
let _ = GetCursorPos(&raw mut at);
let _ = SetForegroundWindow(hwnd);
let _ = TrackPopupMenu(
menu,
TPM_RIGHTALIGN | TPM_BOTTOMALIGN,
at.x,
at.y,
None,
hwnd,
None,
);
let _ = DestroyMenu(menu);
}
}