use std::cell::{Cell, RefCell};
use std::ffi::c_void;
use std::rc::Rc;
use crate::signal::Signal;
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::{define_class, msg_send, sel, AllocAnyThread, DefinedClass, MainThreadOnly};
use objc2_app_kit::{
NSApplication, NSControlStateValueOff, NSControlStateValueOn, NSEventMask, NSEventType,
NSImage, NSMenu, NSMenuItem, NSStatusBar, NSStatusItem, NSVariableStatusItemLength, NSWindow,
};
use objc2_core_graphics::{
CGBitmapContextCreate, CGBitmapContextCreateImage, CGColorSpace, CGImageAlphaInfo,
};
use objc2_foundation::{MainThreadMarker, NSObject, NSObjectProtocol, NSSize, NSString};
type TrayFn = Box<dyn FnMut(&mut TrayCtx)>;
pub struct TrayCtx {
window: Retained<NSWindow>,
status_item: Retained<NSStatusItem>,
}
impl TrayCtx {
pub fn show_window(&self) {
if let Some(mtm) = MainThreadMarker::new() {
self.window.makeKeyAndOrderFront(None);
NSApplication::sharedApplication(mtm).activate();
}
}
pub fn hide_window(&self) {
self.window.orderOut(None);
}
pub fn quit(&self) {
if let Some(mtm) = MainThreadMarker::new() {
NSApplication::sharedApplication(mtm).terminate(None);
}
}
pub fn notify(&self, title: &str, body: &str) {
deliver_notification(title, body);
let _ = &self.status_item; }
}
#[allow(deprecated)]
fn deliver_notification(title: &str, body: &str) {
use objc2::ClassType;
use objc2_foundation::{NSUserNotification, NSUserNotificationCenter};
let center: Option<Retained<NSUserNotificationCenter>> = unsafe {
msg_send![
NSUserNotificationCenter::class(),
defaultUserNotificationCenter
]
};
let Some(center) = center else { return };
let note = NSUserNotification::new();
note.setTitle(Some(&NSString::from_str(title)));
note.setInformativeText(Some(&NSString::from_str(body)));
center.deliverNotification(¬e);
}
enum ItemKind {
Action {
label: String,
checked: Option<Rc<Cell<bool>>>,
enabled: Option<Signal<bool>>,
cb: TrayFn,
},
Separator,
}
pub struct TrayMenuItem {
kind: ItemKind,
}
impl TrayMenuItem {
pub fn item(label: impl Into<String>, cb: impl FnMut(&mut TrayCtx) + 'static) -> Self {
Self {
kind: ItemKind::Action {
label: label.into(),
checked: None,
enabled: None,
cb: Box::new(cb),
},
}
}
pub fn check(
label: impl Into<String>,
checked: Rc<Cell<bool>>,
cb: impl FnMut(&mut TrayCtx) + 'static,
) -> Self {
Self {
kind: ItemKind::Action {
label: label.into(),
checked: Some(checked),
enabled: None,
cb: Box::new(cb),
},
}
}
pub fn enabled(mut self, flag: Signal<bool>) -> Self {
if let ItemKind::Action { enabled, .. } = &mut self.kind {
*enabled = Some(flag);
}
self
}
pub fn separator() -> Self {
Self {
kind: ItemKind::Separator,
}
}
}
#[derive(Default)]
pub struct Tray {
tooltip: String,
icon: Option<(u32, u32, Vec<u8>)>,
on_left_click: Option<TrayFn>,
on_double_click: Option<TrayFn>,
items: Vec<TrayMenuItem>,
}
impl Tray {
pub fn new() -> Self {
Self::default()
}
pub fn tooltip(mut self, s: impl Into<String>) -> Self {
self.tooltip = s.into();
self
}
pub fn icon_rgba(mut self, w: u32, h: u32, rgba: &[u8]) -> Self {
self.icon = Some((w, h, rgba.to_vec()));
self
}
pub fn on_left_click(mut self, f: impl FnMut(&mut TrayCtx) + 'static) -> Self {
self.on_left_click = Some(Box::new(f));
self
}
pub fn on_double_click(mut self, f: impl FnMut(&mut TrayCtx) + 'static) -> Self {
self.on_double_click = Some(Box::new(f));
self
}
pub fn menu(mut self, items: Vec<TrayMenuItem>) -> Self {
self.items = items;
self
}
}
struct TargetIvars {
tray: RefCell<Tray>,
window: Retained<NSWindow>,
status_item: RefCell<Option<Retained<NSStatusItem>>>,
pending: Cell<isize>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "WindUiTrayTarget"]
#[ivars = TargetIvars]
struct TrayTarget;
unsafe impl NSObjectProtocol for TrayTarget {}
impl TrayTarget {
#[unsafe(method(statusClick:))]
fn status_click(&self, _sender: Option<&AnyObject>) {
let mtm = MainThreadMarker::from(self);
let app = NSApplication::sharedApplication(mtm);
let (ty, clicks, ctrl) = match app.currentEvent() {
Some(ev) => {
let flags = ev.modifierFlags();
(ev.r#type(), ev.clickCount(), flags.contains(objc2_app_kit::NSEventModifierFlags::Control))
}
None => (NSEventType::LeftMouseUp, 1, false),
};
let is_right = ty == NSEventType::RightMouseUp || ctrl;
if is_right {
self.pop_menu(mtm);
} else {
self.invoke_click(clicks >= 2);
}
}
#[unsafe(method(menuClick:))]
fn menu_click(&self, sender: &NSMenuItem) {
self.ivars().pending.set(sender.tag());
}
}
);
impl TrayTarget {
fn new(mtm: MainThreadMarker, tray: Tray, window: Retained<NSWindow>) -> Retained<Self> {
let ivars = TargetIvars {
tray: RefCell::new(tray),
window,
status_item: RefCell::new(None),
pending: Cell::new(-1),
};
let this = Self::alloc(mtm).set_ivars(ivars);
unsafe { msg_send![super(this), init] }
}
fn ctx(&self) -> TrayCtx {
let status_item = self.ivars().status_item.borrow().as_ref().unwrap().clone();
TrayCtx {
window: self.ivars().window.clone(),
status_item,
}
}
fn invoke_click(&self, double: bool) {
let mut ctx = self.ctx();
let mut tray = self.ivars().tray.borrow_mut();
let cb = if double {
tray.on_double_click.as_mut()
} else {
tray.on_left_click.as_mut()
};
if let Some(cb) = cb {
cb(&mut ctx);
}
}
#[allow(deprecated)]
fn pop_menu(&self, mtm: MainThreadMarker) {
let menu = NSMenu::new(mtm);
menu.setAutoenablesItems(false);
{
let tray = self.ivars().tray.borrow();
for (i, it) in tray.items.iter().enumerate() {
match &it.kind {
ItemKind::Separator => menu.addItem(&NSMenuItem::separatorItem(mtm)),
ItemKind::Action {
label,
checked,
enabled,
..
} => {
let item = unsafe {
NSMenuItem::initWithTitle_action_keyEquivalent(
NSMenuItem::alloc(mtm),
&NSString::from_str(label),
Some(sel!(menuClick:)),
&NSString::from_str(""),
)
};
item.setTag(i as isize);
unsafe { item.setTarget(Some(self)) };
let on = checked.as_ref().is_some_and(|c| c.get());
item.setState(if on {
NSControlStateValueOn
} else {
NSControlStateValueOff
});
let usable = enabled.map(|e| e.get()).unwrap_or(true);
item.setEnabled(usable);
menu.addItem(&item);
}
}
}
}
let si = match self.ivars().status_item.borrow().as_ref() {
Some(s) => s.clone(),
None => return,
};
self.ivars().pending.set(-1);
si.popUpStatusItemMenu(&menu); let idx = self.ivars().pending.replace(-1);
if idx >= 0 {
self.invoke_menu(idx as usize);
}
}
fn invoke_menu(&self, idx: usize) {
let mut ctx = self.ctx();
let mut tray = self.ivars().tray.borrow_mut();
if let Some(it) = tray.items.get_mut(idx) {
if let ItemKind::Action { cb, .. } = &mut it.kind {
cb(&mut ctx);
}
}
}
}
pub(crate) struct TrayState {
status_item: Retained<NSStatusItem>,
_target: Retained<TrayTarget>,
}
impl Drop for TrayState {
fn drop(&mut self) {
if let Some(mtm) = MainThreadMarker::new() {
NSStatusBar::systemStatusBar().removeStatusItem(&self.status_item);
let _ = mtm;
}
}
}
pub(crate) fn install(
mtm: MainThreadMarker,
window: Retained<NSWindow>,
tray: Tray,
) -> Option<TrayState> {
let icon = tray.icon.clone();
let tooltip = tray.tooltip.clone();
let target = TrayTarget::new(mtm, tray, window);
let status_item =
NSStatusBar::systemStatusBar().statusItemWithLength(NSVariableStatusItemLength);
let button = status_item.button(mtm)?;
if let Some((w, h, rgba)) = icon {
if let Some(img) = nsimage_from_rgba(w as i32, h as i32, &rgba) {
button.setImage(Some(&img));
}
}
if !tooltip.is_empty() {
button.setToolTip(Some(&NSString::from_str(&tooltip)));
}
unsafe {
button.setTarget(Some(&target));
button.setAction(Some(sel!(statusClick:)));
button.sendActionOn(NSEventMask::LeftMouseUp | NSEventMask::RightMouseUp);
}
*target.ivars().status_item.borrow_mut() = Some(status_item.clone());
Some(TrayState {
status_item,
_target: target,
})
}
fn nsimage_from_rgba(w: i32, h: i32, rgba: &[u8]) -> Option<Retained<NSImage>> {
if w <= 0 || h <= 0 || rgba.len() < (w * h * 4) as usize {
return None;
}
let mut buf = vec![0u8; (w * h * 4) as usize];
for i in 0..(w * h) as usize {
let s = i * 4;
let a = rgba[s + 3] as u32;
buf[s] = (rgba[s] as u32 * a / 255) as u8;
buf[s + 1] = (rgba[s + 1] as u32 * a / 255) as u8;
buf[s + 2] = (rgba[s + 2] as u32 * a / 255) as u8;
buf[s + 3] = a as u8;
}
let cs = CGColorSpace::new_device_rgb()?;
let image = unsafe {
let ctx = CGBitmapContextCreate(
buf.as_mut_ptr() as *mut c_void,
w as usize,
h as usize,
8,
(w * 4) as usize,
Some(&cs),
CGImageAlphaInfo::PremultipliedLast.0,
)?;
CGBitmapContextCreateImage(Some(&ctx))?
};
let size = NSSize {
width: w as f64,
height: h as f64,
};
let nsimage = NSImage::initWithCGImage_size(NSImage::alloc(), &image, size);
nsimage.setTemplate(false);
Some(nsimage)
}