use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, NSObject, NSObjectProtocol};
use objc2::{define_class, msg_send, sel, AnyThread, DefinedClass, MainThreadOnly};
use objc2_app_kit::{
NSApplication, NSEventMask, NSEventType, NSImage, NSStatusBar, NSStatusItem,
NSVariableStatusItemLength,
};
use objc2_foundation::{MainThreadMarker, NSData, NSString};
use crate::backend::{BackendError, ServiceResult};
use crate::event::{MouseButton, UiEvent};
use crate::primitives::context_menu::ContextMenu;
use crate::primitives::image::ImageSource;
use super::menu_bar_install::{build_ns_menu, MenuKind, QuadraMenuTarget};
#[derive(Default)]
pub(crate) struct MacTrayState {
status_item: Option<Retained<NSStatusItem>>,
click_target: Option<Retained<QuadraTrayTarget>>,
menu_target: Option<Retained<QuadraMenuTarget>>,
}
impl MacTrayState {
fn ensure_item(
&mut self,
mtm: MainThreadMarker,
events: &Rc<RefCell<VecDeque<UiEvent>>>,
) -> Retained<NSStatusItem> {
if let Some(item) = &self.status_item {
return item.clone();
}
let item = NSStatusBar::systemStatusBar().statusItemWithLength(NSVariableStatusItemLength);
if let Some(button) = item.button(mtm) {
let target = QuadraTrayTarget::new(mtm, events.clone());
unsafe { button.setAction(Some(sel!(quadraTrayAction:))) };
let target_obj: &AnyObject = unsafe { &*(&*target as *const _ as *const AnyObject) };
unsafe { button.setTarget(Some(target_obj)) };
button.sendActionOn(NSEventMask::LeftMouseUp | NSEventMask::RightMouseUp);
self.click_target = Some(target);
}
self.status_item = Some(item.clone());
item
}
}
struct QuadraTrayTargetIvars {
events: Rc<RefCell<VecDeque<UiEvent>>>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[ivars = QuadraTrayTargetIvars]
struct QuadraTrayTarget;
unsafe impl NSObjectProtocol for QuadraTrayTarget {}
impl QuadraTrayTarget {
#[unsafe(method(quadraTrayAction:))]
fn quadra_tray_action(&self, _sender: &AnyObject) {
let mtm = MainThreadMarker::from(self);
let button = mouse_button_from_current_event(mtm);
self.ivars()
.events
.borrow_mut()
.push_back(UiEvent::TrayClicked { button });
let app = NSApplication::sharedApplication(mtm);
if let Some(window) = app.keyWindow() {
if let Some(view) = window.contentView() {
view.setNeedsDisplay(true);
}
}
}
}
);
impl QuadraTrayTarget {
fn new(mtm: MainThreadMarker, events: Rc<RefCell<VecDeque<UiEvent>>>) -> Retained<Self> {
let this = mtm.alloc::<Self>();
let this = this.set_ivars(QuadraTrayTargetIvars { events });
unsafe { msg_send![super(this), init] }
}
}
fn mouse_button_from_current_event(mtm: MainThreadMarker) -> MouseButton {
let app = NSApplication::sharedApplication(mtm);
match app.currentEvent().map(|event| event.r#type()) {
Some(NSEventType::RightMouseUp) => MouseButton::Right,
Some(NSEventType::OtherMouseUp) => MouseButton::Middle,
_ => MouseButton::Left,
}
}
pub(crate) fn decode_ns_image(source: &ImageSource) -> Option<Retained<NSImage>> {
match source {
ImageSource::Bytes(bytes) => {
let data = NSData::with_bytes(bytes);
NSImage::initWithData(NSImage::alloc(), &data)
}
ImageSource::Path(path) => {
let path_str = path.to_str()?;
NSImage::initWithContentsOfFile(NSImage::alloc(), &NSString::from_str(path_str))
}
}
}
pub(crate) fn set_icon(
state: &mut MacTrayState,
events: Rc<RefCell<VecDeque<UiEvent>>>,
icon: ImageSource,
) -> ServiceResult<()> {
let mtm = MainThreadMarker::new().ok_or(BackendError::Unsupported)?;
let image = decode_ns_image(&icon).ok_or_else(|| BackendError::PlatformFailure {
context: "NSImage failed to decode the tray icon source".to_string(),
})?;
let item = state.ensure_item(mtm, &events);
if let Some(button) = item.button(mtm) {
button.setImage(Some(&image));
}
Ok(())
}
pub(crate) fn set_tooltip(
state: &mut MacTrayState,
events: Rc<RefCell<VecDeque<UiEvent>>>,
tooltip: &str,
) -> ServiceResult<()> {
let mtm = MainThreadMarker::new().ok_or(BackendError::Unsupported)?;
let item = state.ensure_item(mtm, &events);
if let Some(button) = item.button(mtm) {
button.setToolTip(Some(&NSString::from_str(tooltip)));
}
Ok(())
}
pub(crate) fn set_menu(
state: &mut MacTrayState,
events: Rc<RefCell<VecDeque<UiEvent>>>,
menu: &ContextMenu,
) -> ServiceResult<()> {
let mtm = MainThreadMarker::new().ok_or(BackendError::Unsupported)?;
let item = state.ensure_item(mtm, &events);
if menu.items.is_empty() {
item.setMenu(None);
state.menu_target = None;
return Ok(());
}
let target = QuadraMenuTarget::new(mtm, events, MenuKind::Context);
let ns_menu = build_ns_menu(mtm, &menu.items, &target);
item.setMenu(Some(&ns_menu));
state.menu_target = Some(target);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn events() -> Rc<RefCell<VecDeque<UiEvent>>> {
Rc::new(RefCell::new(VecDeque::new()))
}
#[test]
fn set_icon_off_main_thread_returns_unsupported_not_panic() {
let mut state = MacTrayState::default();
let result = set_icon(&mut state, events(), ImageSource::Bytes(Vec::new()));
assert_eq!(result, Err(BackendError::Unsupported));
}
#[test]
fn set_tooltip_off_main_thread_returns_unsupported_not_panic() {
let mut state = MacTrayState::default();
let result = set_tooltip(&mut state, events(), "hello");
assert_eq!(result, Err(BackendError::Unsupported));
}
#[test]
fn set_menu_off_main_thread_returns_unsupported_not_panic() {
let mut state = MacTrayState::default();
let menu = ContextMenu {
id: crate::types::WidgetId::new("tray-menu"),
items: Vec::new(),
selected_idx: 0,
bg: None,
placement: crate::primitives::context_menu::ContextMenuPlacement::default(),
};
let result = set_menu(&mut state, events(), &menu);
assert_eq!(result, Err(BackendError::Unsupported));
}
}