pub mod builder;
pub mod common;
pub mod event;
pub mod panel;
#[doc(hidden)]
pub use objc2;
#[doc(hidden)]
pub use objc2_app_kit;
#[doc(hidden)]
pub use objc2_foundation;
#[doc(hidden)]
pub use pastey;
use std::{
any::Any,
collections::HashMap,
fmt,
sync::{Arc, Mutex},
};
use objc2::runtime::ProtocolObject;
use objc2_app_kit::NSWindowDelegate;
use tauri::{
plugin::{Builder, TauriPlugin},
Manager, Runtime, WebviewWindow,
};
pub use builder::{
CollectionBehavior, PanelBuilder, PanelLevel, ResizeDirection, StyleMask, TrackingAreaOptions,
};
pub use objc2::runtime::AnyObject;
pub use objc2_app_kit::{NSPanel, NSResponder, NSView, NSWindow};
pub use objc2_foundation::{NSNotification, NSObject, NSPoint, NSRect, NSSize};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum StyleMaskError {
ObjectiveCException(String),
UnknownObjectiveCException,
}
impl fmt::Display for StyleMaskError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ObjectiveCException(reason) => {
write!(formatter, "AppKit rejected the panel style mask: {reason}")
}
Self::UnknownObjectiveCException => {
formatter.write_str("AppKit rejected the panel style mask")
}
}
}
}
impl std::error::Error for StyleMaskError {}
#[doc(hidden)]
pub fn catch_style_mask_exception(operation: impl FnOnce()) -> Result<(), StyleMaskError> {
match objc2::exception::catch(std::panic::AssertUnwindSafe(operation)) {
Ok(()) => Ok(()),
Err(Some(exception)) => Err(StyleMaskError::ObjectiveCException(exception.to_string())),
Err(None) => Err(StyleMaskError::UnknownObjectiveCException),
}
}
pub trait EventHandler {
fn as_delegate(&self) -> ProtocolObject<dyn NSWindowDelegate>;
}
pub trait Panel<R: tauri::Runtime = tauri::Wry>: Send + Sync {
fn show(&self);
fn hide(&self);
fn to_window(&self) -> Option<tauri::WebviewWindow<R>>;
fn as_panel(&self) -> &objc2_app_kit::NSPanel;
fn label(&self) -> &str;
fn as_any(&self) -> &dyn Any;
fn set_event_handler(&self, handler: Option<&ProtocolObject<dyn NSWindowDelegate>>);
fn is_visible(&self) -> bool;
fn is_floating_panel(&self) -> bool;
fn becomes_key_only_if_needed(&self) -> bool;
fn can_become_key_window(&self) -> bool;
fn can_become_main_window(&self) -> bool;
fn hides_on_deactivate(&self) -> bool;
fn make_key_window(&self);
fn make_main_window(&self);
fn resign_key_window(&self);
fn make_key_and_order_front(&self);
fn order_front_regardless(&self);
fn show_and_make_key(&self);
fn set_level(&self, level: i64);
fn set_floating_panel(&self, value: bool);
fn set_becomes_key_only_if_needed(&self, value: bool);
fn set_hides_on_deactivate(&self, value: bool);
fn set_works_when_modal(&self, value: bool);
fn set_alpha_value(&self, value: f64);
fn set_released_when_closed(&self, released: bool);
fn set_content_size(&self, width: f64, height: f64);
fn set_has_shadow(&self, value: bool);
fn set_opaque(&self, value: bool);
fn set_accepts_mouse_moved_events(&self, value: bool);
fn set_ignores_mouse_events(&self, value: bool);
fn set_movable_by_window_background(&self, value: bool);
fn set_collection_behavior(&self, behavior: objc2_app_kit::NSWindowCollectionBehavior);
fn content_view(&self) -> objc2::rc::Retained<objc2_app_kit::NSView>;
fn resign_main_window(&self);
fn set_style_mask(
&self,
style_mask: objc2_app_kit::NSWindowStyleMask,
) -> Result<(), StyleMaskError>;
fn add_style_mask(
&self,
style_mask: objc2_app_kit::NSWindowStyleMask,
) -> Result<(), StyleMaskError> {
self.set_style_mask(self.as_panel().styleMask() | style_mask)
}
fn make_first_responder(&self, responder: Option<&objc2_app_kit::NSResponder>) -> bool;
fn set_corner_radius(&self, radius: f64);
fn set_transparent(&self, transparent: bool);
}
pub trait FromWindow<R: Runtime>: Panel<R> + Sized {
fn from_window(window: WebviewWindow<R>, label: String) -> tauri::Result<Self>;
}
pub type PanelHandle<R> = Arc<dyn Panel<R>>;
pub struct Store<R: Runtime> {
panels: HashMap<String, PanelHandle<R>>,
}
impl<R: Runtime> Default for Store<R> {
fn default() -> Self {
Self {
panels: HashMap::new(),
}
}
}
pub struct WebviewPanelManager<R: Runtime>(pub Mutex<Store<R>>);
impl<R: Runtime> Default for WebviewPanelManager<R> {
fn default() -> Self {
Self(Mutex::new(Store::default()))
}
}
pub trait ManagerExt<R: Runtime> {
fn get_webview_panel(&self, label: &str) -> Result<PanelHandle<R>, Error>;
fn remove_webview_panel(&self, label: &str) -> Option<PanelHandle<R>>;
}
#[derive(Debug)]
pub enum Error {
PanelNotFound,
}
impl<R: Runtime, T: Manager<R>> ManagerExt<R> for T {
fn get_webview_panel(&self, label: &str) -> Result<PanelHandle<R>, Error> {
let manager = self.state::<self::WebviewPanelManager<R>>();
let manager = manager.0.lock().unwrap();
match manager.panels.get(label) {
Some(panel) => Ok(panel.clone()),
None => Err(Error::PanelNotFound),
}
}
fn remove_webview_panel(&self, label: &str) -> Option<PanelHandle<R>> {
self.state::<self::WebviewPanelManager<R>>()
.0
.lock()
.unwrap()
.panels
.remove(label)
}
}
pub trait WebviewWindowExt<R: Runtime> {
fn to_panel<P: FromWindow<R> + 'static>(&self) -> tauri::Result<PanelHandle<R>>;
}
impl<R: Runtime> WebviewWindowExt<R> for WebviewWindow<R> {
fn to_panel<P: FromWindow<R> + 'static>(&self) -> tauri::Result<PanelHandle<R>> {
let label = self.label().to_string();
let panel = P::from_window(self.clone(), label.clone())?;
let arc_panel = Arc::new(panel) as PanelHandle<R>;
let manager = self.state::<WebviewPanelManager<R>>();
manager
.0
.lock()
.unwrap()
.panels
.insert(label, arc_panel.clone());
Ok(arc_panel)
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("nspanel")
.setup(|app, _api| {
app.manage(self::WebviewPanelManager::<R>::default());
Ok(())
})
.build()
}
#[cfg(test)]
mod tests {
use objc2::rc::Retained;
use objc2_foundation::{NSException, NSInternalInconsistencyException, NSString};
use super::{catch_style_mask_exception, StyleMaskError};
#[test]
fn objective_c_exceptions_become_style_mask_errors() {
let reason = NSString::from_str("invalid test style mask");
let exception = unsafe {
NSException::exceptionWithName_reason_userInfo(
NSInternalInconsistencyException,
Some(&reason),
None,
)
};
let exception =
unsafe { Retained::cast_unchecked::<objc2::exception::Exception>(exception) };
let error = catch_style_mask_exception(|| objc2::exception::throw(exception))
.expect_err("the Objective-C exception should be returned as an error");
assert_eq!(
error,
StyleMaskError::ObjectiveCException("invalid test style mask".into())
);
}
}