use std::sync::Arc;
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSPanel};
use objc2_foundation::{MainThreadMarker, NSSize};
use tauri::{AppHandle, Position, Runtime, Size, WebviewUrl, WebviewWindowBuilder};
use crate::{FromWindow, Panel, WebviewWindowExt};
type WindowConfigFn<'a, R> = Box<
dyn FnOnce(
WebviewWindowBuilder<'a, R, AppHandle<R>>,
) -> WebviewWindowBuilder<'a, R, AppHandle<R>>,
>;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ResizeDirection {
#[default]
Both,
Horizontal,
Vertical,
}
impl ResizeDirection {
fn apply(self, panel: &NSPanel) {
if self == Self::Both {
return;
}
let Some(content_view) = panel.contentView() else {
return;
};
let current_size = content_view.frame().size;
let (min_size, max_size) = resize_constraints(
self,
current_size,
panel.contentMinSize(),
panel.contentMaxSize(),
);
panel.setContentMinSize(min_size);
panel.setContentMaxSize(max_size);
}
}
fn resize_constraints(
direction: ResizeDirection,
current_size: NSSize,
mut min_size: NSSize,
mut max_size: NSSize,
) -> (NSSize, NSSize) {
match direction {
ResizeDirection::Both => {}
ResizeDirection::Horizontal => {
min_size.height = current_size.height;
max_size.height = current_size.height;
}
ResizeDirection::Vertical => {
min_size.width = current_size.width;
max_size.width = current_size.width;
}
}
(min_size, max_size)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PanelLevel {
Normal,
Submenu,
TornOffMenu,
Floating,
ModalPanel,
Utility,
Dock,
MainMenu,
Status,
PopUpMenu,
ScreenSaver,
Custom(i32),
}
impl PanelLevel {
pub fn value(&self) -> i64 {
match self {
PanelLevel::Normal => 0,
PanelLevel::Submenu => 3,
PanelLevel::TornOffMenu => 3,
PanelLevel::Floating => 4,
PanelLevel::ModalPanel => 8,
PanelLevel::Utility => 19,
PanelLevel::Dock => 20,
PanelLevel::MainMenu => 24,
PanelLevel::Status => 25,
PanelLevel::PopUpMenu => 101,
PanelLevel::ScreenSaver => 1000,
PanelLevel::Custom(value) => *value as i64,
}
}
}
impl From<PanelLevel> for i64 {
fn from(level: PanelLevel) -> Self {
level.value()
}
}
impl From<i32> for PanelLevel {
fn from(value: i32) -> Self {
PanelLevel::Custom(value)
}
}
impl From<i64> for PanelLevel {
fn from(value: i64) -> Self {
PanelLevel::Custom(value as i32)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CollectionBehavior(objc2_app_kit::NSWindowCollectionBehavior);
impl CollectionBehavior {
pub fn new() -> Self {
Self(objc2_app_kit::NSWindowCollectionBehavior::empty())
}
pub fn can_join_all_spaces(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowCollectionBehavior::CanJoinAllSpaces;
self
}
pub fn move_to_active_space(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowCollectionBehavior::MoveToActiveSpace;
self
}
pub fn managed(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowCollectionBehavior::Managed;
self
}
pub fn transient(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowCollectionBehavior::Transient;
self
}
pub fn stationary(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowCollectionBehavior::Stationary;
self
}
pub fn participates_in_cycle(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowCollectionBehavior::ParticipatesInCycle;
self
}
pub fn ignores_cycle(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowCollectionBehavior::IgnoresCycle;
self
}
pub fn full_screen_primary(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowCollectionBehavior::FullScreenPrimary;
self
}
pub fn full_screen_auxiliary(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowCollectionBehavior::FullScreenAuxiliary;
self
}
pub fn full_screen_none(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowCollectionBehavior::FullScreenNone;
self
}
pub fn full_screen_allows_tiling(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowCollectionBehavior::FullScreenAllowsTiling;
self
}
pub fn full_screen_disallows_tiling(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowCollectionBehavior::FullScreenDisallowsTiling;
self
}
pub fn from_raw(flags: objc2_app_kit::NSWindowCollectionBehavior) -> Self {
Self(flags)
}
pub fn value(&self) -> objc2_app_kit::NSWindowCollectionBehavior {
self.0
}
}
impl Default for CollectionBehavior {
fn default() -> Self {
Self::new()
}
}
impl From<CollectionBehavior> for objc2_app_kit::NSWindowCollectionBehavior {
fn from(behavior: CollectionBehavior) -> Self {
behavior.0
}
}
impl From<objc2_app_kit::NSWindowCollectionBehavior> for CollectionBehavior {
fn from(value: objc2_app_kit::NSWindowCollectionBehavior) -> Self {
CollectionBehavior(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TrackingAreaOptions(objc2_app_kit::NSTrackingAreaOptions);
impl TrackingAreaOptions {
pub fn new() -> Self {
Self(objc2_app_kit::NSTrackingAreaOptions::empty())
}
pub fn mouse_moved(mut self) -> Self {
self.0 |= objc2_app_kit::NSTrackingAreaOptions::MouseMoved;
self
}
pub fn mouse_entered_and_exited(mut self) -> Self {
self.0 |= objc2_app_kit::NSTrackingAreaOptions::MouseEnteredAndExited;
self
}
pub fn active_always(mut self) -> Self {
self.0 |= objc2_app_kit::NSTrackingAreaOptions::ActiveAlways;
self
}
pub fn active_in_active_app(mut self) -> Self {
self.0 |= objc2_app_kit::NSTrackingAreaOptions::ActiveInActiveApp;
self
}
pub fn active_in_key_window(mut self) -> Self {
self.0 |= objc2_app_kit::NSTrackingAreaOptions::ActiveInKeyWindow;
self
}
pub fn active_when_first_responder(mut self) -> Self {
self.0 |= objc2_app_kit::NSTrackingAreaOptions::ActiveWhenFirstResponder;
self
}
pub fn assume_inside(mut self) -> Self {
self.0 |= objc2_app_kit::NSTrackingAreaOptions::AssumeInside;
self
}
pub fn in_visible_rect(mut self) -> Self {
self.0 |= objc2_app_kit::NSTrackingAreaOptions::InVisibleRect;
self
}
pub fn cursor_update(mut self) -> Self {
self.0 |= objc2_app_kit::NSTrackingAreaOptions::CursorUpdate;
self
}
pub fn from_raw(flags: objc2_app_kit::NSTrackingAreaOptions) -> Self {
Self(flags)
}
pub fn value(&self) -> objc2_app_kit::NSTrackingAreaOptions {
self.0
}
}
impl Default for TrackingAreaOptions {
fn default() -> Self {
Self::new()
}
}
impl From<TrackingAreaOptions> for objc2_app_kit::NSTrackingAreaOptions {
fn from(options: TrackingAreaOptions) -> Self {
options.0
}
}
impl From<objc2_app_kit::NSTrackingAreaOptions> for TrackingAreaOptions {
fn from(value: objc2_app_kit::NSTrackingAreaOptions) -> Self {
TrackingAreaOptions(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StyleMask(objc2_app_kit::NSWindowStyleMask);
impl StyleMask {
pub fn new() -> Self {
Self(
objc2_app_kit::NSWindowStyleMask::Titled
| objc2_app_kit::NSWindowStyleMask::Closable
| objc2_app_kit::NSWindowStyleMask::Miniaturizable
| objc2_app_kit::NSWindowStyleMask::Resizable,
)
}
pub fn empty() -> Self {
Self(objc2_app_kit::NSWindowStyleMask::empty())
}
pub fn titled(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowStyleMask::Titled;
self
}
pub fn closable(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowStyleMask::Closable;
self
}
pub fn miniaturizable(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowStyleMask::Miniaturizable;
self
}
pub fn resizable(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowStyleMask::Resizable;
self
}
pub fn unified_title_and_toolbar(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowStyleMask::UnifiedTitleAndToolbar;
self
}
pub fn full_size_content_view(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowStyleMask::FullSizeContentView;
self
}
pub fn utility_window(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowStyleMask::UtilityWindow;
self
}
pub fn hud_window(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowStyleMask::HUDWindow;
self
}
pub fn nonactivating_panel(mut self) -> Self {
self.0 |= objc2_app_kit::NSWindowStyleMask::NonactivatingPanel;
self
}
pub fn borderless(mut self) -> Self {
self.0 = objc2_app_kit::NSWindowStyleMask::Borderless;
self
}
pub fn from_raw(flags: objc2_app_kit::NSWindowStyleMask) -> Self {
Self(flags)
}
pub fn value(&self) -> objc2_app_kit::NSWindowStyleMask {
self.0
}
}
impl Default for StyleMask {
fn default() -> Self {
Self::new()
}
}
impl From<StyleMask> for objc2_app_kit::NSWindowStyleMask {
fn from(mask: StyleMask) -> Self {
mask.0
}
}
impl From<objc2_app_kit::NSWindowStyleMask> for StyleMask {
fn from(value: objc2_app_kit::NSWindowStyleMask) -> Self {
StyleMask(value)
}
}
#[derive(Default)]
pub(crate) struct PanelConfig {
pub floating: Option<bool>,
pub level: Option<PanelLevel>,
pub has_shadow: Option<bool>,
pub opaque: Option<bool>,
pub alpha_value: Option<f64>,
pub hides_on_deactivate: Option<bool>,
pub becomes_key_only_if_needed: Option<bool>,
pub accepts_mouse_moved_events: Option<bool>,
pub ignores_mouse_events: Option<bool>,
pub movable_by_window_background: Option<bool>,
pub released_when_closed: Option<bool>,
pub works_when_modal: Option<bool>,
pub content_size: Option<Size>,
pub style_mask: Option<StyleMaskConfig>,
pub collection_behavior: Option<CollectionBehavior>,
pub no_activate: Option<bool>,
pub corner_radius: Option<f64>,
pub transparent: Option<bool>,
}
pub(crate) enum StyleMaskConfig {
Replace(StyleMask),
Add(StyleMask),
}
pub struct PanelBuilder<'a, R: Runtime, T: FromWindow<R> + 'static> {
handle: &'a AppHandle<R>,
label: String,
url: Option<WebviewUrl>,
title: Option<String>,
position: Option<Position>,
size: Option<Size>,
min_size: Option<Size>,
max_size: Option<Size>,
resizable: Option<bool>,
resize_direction: ResizeDirection,
pub(crate) panel_config: PanelConfig,
window_fn: Option<WindowConfigFn<'a, R>>,
_phantom: std::marker::PhantomData<T>,
}
impl<'a, R: Runtime + 'a, T: FromWindow<R> + 'static> PanelBuilder<'a, R, T> {
pub fn new(handle: &'a AppHandle<R>, label: impl Into<String>) -> Self {
Self {
handle,
label: label.into(),
url: None,
title: None,
position: None,
size: None,
min_size: None,
max_size: None,
resizable: None,
resize_direction: ResizeDirection::Both,
panel_config: PanelConfig::default(),
window_fn: None,
_phantom: std::marker::PhantomData,
}
}
pub fn url(mut self, url: WebviewUrl) -> Self {
self.url = Some(url);
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn position(mut self, position: Position) -> Self {
self.position = Some(position);
self
}
pub fn size(mut self, size: Size) -> Self {
self.size = Some(size);
self
}
pub fn min_size(mut self, size: Size) -> Self {
self.min_size = Some(size);
self
}
pub fn max_size(mut self, size: Size) -> Self {
self.max_size = Some(size);
self
}
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = Some(resizable);
self
}
pub fn resize_direction(mut self, direction: ResizeDirection) -> Self {
self.resize_direction = direction;
self
}
pub fn floating(mut self, floating: bool) -> Self {
self.panel_config.floating = Some(floating);
self
}
pub fn level(mut self, level: PanelLevel) -> Self {
self.panel_config.level = Some(level);
self
}
pub fn has_shadow(mut self, has_shadow: bool) -> Self {
self.panel_config.has_shadow = Some(has_shadow);
self
}
pub fn opaque(mut self, opaque: bool) -> Self {
self.panel_config.opaque = Some(opaque);
self
}
pub fn alpha_value(mut self, alpha: f64) -> Self {
self.panel_config.alpha_value = Some(alpha);
self
}
pub fn hides_on_deactivate(mut self, hides: bool) -> Self {
self.panel_config.hides_on_deactivate = Some(hides);
self
}
pub fn becomes_key_only_if_needed(mut self, value: bool) -> Self {
self.panel_config.becomes_key_only_if_needed = Some(value);
self
}
pub fn accepts_mouse_moved_events(mut self, value: bool) -> Self {
self.panel_config.accepts_mouse_moved_events = Some(value);
self
}
pub fn ignores_mouse_events(mut self, value: bool) -> Self {
self.panel_config.ignores_mouse_events = Some(value);
self
}
pub fn movable_by_window_background(mut self, value: bool) -> Self {
self.panel_config.movable_by_window_background = Some(value);
self
}
pub fn released_when_closed(mut self, value: bool) -> Self {
self.panel_config.released_when_closed = Some(value);
self
}
pub fn works_when_modal(mut self, value: bool) -> Self {
self.panel_config.works_when_modal = Some(value);
self
}
pub fn content_size(mut self, size: Size) -> Self {
self.panel_config.content_size = Some(size);
self
}
pub fn style_mask(mut self, style_mask: StyleMask) -> Self {
self.panel_config.style_mask = Some(StyleMaskConfig::Replace(style_mask));
self
}
pub fn add_style_mask(mut self, style_mask: StyleMask) -> Self {
self.panel_config.style_mask = Some(StyleMaskConfig::Add(style_mask));
self
}
pub fn collection_behavior(mut self, behavior: CollectionBehavior) -> Self {
self.panel_config.collection_behavior = Some(behavior);
self
}
pub fn no_activate(mut self, no_activate: bool) -> Self {
self.panel_config.no_activate = Some(no_activate);
self
}
pub fn corner_radius(mut self, radius: f64) -> Self {
self.panel_config.corner_radius = Some(radius);
self
}
pub fn transparent(mut self, transparent: bool) -> Self {
self.panel_config.transparent = Some(transparent);
self
}
pub fn with_window<F>(mut self, f: F) -> Self
where
F: FnOnce(
WebviewWindowBuilder<'a, R, AppHandle<R>>,
) -> WebviewWindowBuilder<'a, R, AppHandle<R>>
+ 'static,
{
self.window_fn = Some(Box::new(f) as WindowConfigFn<'a, R>);
self
}
pub fn build(self) -> tauri::Result<Arc<dyn Panel<R>>> {
let original_policy = if self.panel_config.no_activate.unwrap_or(false) {
MainThreadMarker::new().map(|mtm| {
let app = NSApplication::sharedApplication(mtm);
let current_policy = app.activationPolicy();
let _success = app.setActivationPolicy(NSApplicationActivationPolicy::Prohibited);
current_policy
})
} else {
None
};
let mut window_builder = WebviewWindowBuilder::new(
self.handle,
&self.label,
self.url.unwrap_or(WebviewUrl::App("index.html".into())),
);
if let Some(title) = self.title {
window_builder = window_builder.title(title);
}
if let Some(position) = self.position {
match position {
Position::Physical(pos) => {
window_builder = window_builder.position(pos.x as f64, pos.y as f64);
}
Position::Logical(pos) => {
window_builder = window_builder.position(pos.x, pos.y);
}
}
}
if let Some(size) = self.size {
match size {
Size::Physical(s) => {
window_builder = window_builder.inner_size(s.width as f64, s.height as f64);
}
Size::Logical(s) => {
window_builder = window_builder.inner_size(s.width, s.height);
}
}
}
if let Some(resizable) = self.resizable {
window_builder = window_builder.resizable(resizable);
}
if let Some(window_fn) = self.window_fn {
window_builder = window_fn(window_builder);
}
let window = window_builder.build();
if let Some(policy) = original_policy {
if let Some(mtm) = MainThreadMarker::new() {
let app = NSApplication::sharedApplication(mtm);
let _success = app.setActivationPolicy(policy);
}
}
let window = window?;
let panel = window.to_panel::<T>().unwrap();
if let Some(floating) = self.panel_config.floating {
panel.set_floating_panel(floating);
}
if let Some(level) = self.panel_config.level {
panel.set_level(level.value());
}
if let Some(has_shadow) = self.panel_config.has_shadow {
panel.set_has_shadow(has_shadow);
}
if let Some(opaque) = self.panel_config.opaque {
panel.set_opaque(opaque);
}
if let Some(alpha_value) = self.panel_config.alpha_value {
panel.set_alpha_value(alpha_value);
}
if let Some(hides) = self.panel_config.hides_on_deactivate {
panel.set_hides_on_deactivate(hides);
}
if let Some(value) = self.panel_config.becomes_key_only_if_needed {
panel.set_becomes_key_only_if_needed(value);
}
if let Some(value) = self.panel_config.accepts_mouse_moved_events {
panel.set_accepts_mouse_moved_events(value);
}
if let Some(value) = self.panel_config.ignores_mouse_events {
panel.set_ignores_mouse_events(value);
}
if let Some(value) = self.panel_config.movable_by_window_background {
panel.set_movable_by_window_background(value);
}
if let Some(value) = self.panel_config.released_when_closed {
panel.set_released_when_closed(value);
}
if let Some(value) = self.panel_config.works_when_modal {
panel.set_works_when_modal(value);
}
if let Some(style_mask) = self.panel_config.style_mask {
let result = match style_mask {
StyleMaskConfig::Replace(style_mask) => panel.set_style_mask(style_mask.0),
StyleMaskConfig::Add(style_mask) => panel.add_style_mask(style_mask.0),
};
if let Err(error) = result {
if let Some(window) = panel.to_window() {
let _ = window.close();
}
return Err(tauri::Error::Anyhow(error.into()));
}
}
if let Some(behavior) = self.panel_config.collection_behavior {
panel.set_collection_behavior(behavior.0);
}
if let Some(radius) = self.panel_config.corner_radius {
panel.set_corner_radius(radius);
}
if let Some(transparent) = self.panel_config.transparent {
panel.set_transparent(transparent);
}
if let Some(min_size) = self.min_size {
window.set_min_size(Some(min_size))?;
}
if let Some(max_size) = self.max_size {
window.set_max_size(Some(max_size))?;
}
self.resize_direction.apply(panel.as_panel());
Ok(panel)
}
}
#[cfg(test)]
mod tests {
use super::{resize_constraints, ResizeDirection};
use objc2_foundation::NSSize;
#[test]
fn horizontal_resize_fixes_only_the_height() {
let (min_size, max_size) = resize_constraints(
ResizeDirection::Horizontal,
NSSize::new(500.0, 300.0),
NSSize::new(200.0, 100.0),
NSSize::new(800.0, 600.0),
);
assert_eq!(min_size, NSSize::new(200.0, 300.0));
assert_eq!(max_size, NSSize::new(800.0, 300.0));
}
#[test]
fn vertical_resize_fixes_only_the_width() {
let (min_size, max_size) = resize_constraints(
ResizeDirection::Vertical,
NSSize::new(500.0, 300.0),
NSSize::new(200.0, 100.0),
NSSize::new(800.0, 600.0),
);
assert_eq!(min_size, NSSize::new(500.0, 100.0));
assert_eq!(max_size, NSSize::new(500.0, 600.0));
}
#[test]
fn both_resize_axes_preserve_existing_constraints() {
let min = NSSize::new(200.0, 100.0);
let max = NSSize::new(800.0, 600.0);
let constraints =
resize_constraints(ResizeDirection::Both, NSSize::new(500.0, 300.0), min, max);
assert_eq!(constraints, (min, max));
}
}