tauri_nspanel/
builder.rs

1use std::sync::Arc;
2
3use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy};
4use objc2_foundation::MainThreadMarker;
5use tauri::{AppHandle, Position, Runtime, Size, WebviewUrl, WebviewWindowBuilder};
6
7use crate::{FromWindow, Panel, WebviewWindowExt};
8
9/// Type alias for window configuration function
10type WindowConfigFn<'a, R> = Box<
11    dyn FnOnce(
12        WebviewWindowBuilder<'a, R, AppHandle<R>>,
13    ) -> WebviewWindowBuilder<'a, R, AppHandle<R>>,
14>;
15
16/// Window level constants for NSPanel
17/// Based on NSWindow.Level constants from macOS
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum PanelLevel {
20    /// Normal window level (0)
21    Normal,
22    /// Submenu window level (3)
23    Submenu,
24    /// Torn-off menu window level (3)
25    TornOffMenu,
26    /// Floating window level (4)
27    Floating,
28    /// Modal panel window level (8)
29    ModalPanel,
30    /// Utility window level (19)
31    Utility,
32    /// Dock window level (20)
33    Dock,
34    /// Main menu window level (24)
35    MainMenu,
36    /// Status window level (25)
37    Status,
38    /// Pop-up menu window level (101)
39    PopUpMenu,
40    /// Screen saver window level (1000)
41    ScreenSaver,
42    /// Custom level value
43    Custom(i32),
44}
45
46impl PanelLevel {
47    /// Convert to the raw i64 value used by NSWindow
48    pub fn value(&self) -> i64 {
49        match self {
50            PanelLevel::Normal => 0,
51            PanelLevel::Submenu => 3,
52            PanelLevel::TornOffMenu => 3,
53            PanelLevel::Floating => 4,
54            PanelLevel::ModalPanel => 8,
55            PanelLevel::Utility => 19,
56            PanelLevel::Dock => 20,
57            PanelLevel::MainMenu => 24,
58            PanelLevel::Status => 25,
59            PanelLevel::PopUpMenu => 101,
60            PanelLevel::ScreenSaver => 1000,
61            PanelLevel::Custom(value) => *value as i64,
62        }
63    }
64}
65
66impl From<PanelLevel> for i64 {
67    fn from(level: PanelLevel) -> Self {
68        level.value()
69    }
70}
71
72impl From<i32> for PanelLevel {
73    fn from(value: i32) -> Self {
74        PanelLevel::Custom(value)
75    }
76}
77
78impl From<i64> for PanelLevel {
79    fn from(value: i64) -> Self {
80        PanelLevel::Custom(value as i32)
81    }
82}
83
84/// Window collection behavior builder for NSPanel
85///
86/// Allows combining multiple collection behaviors using the builder pattern.
87/// Collection behaviors control how a window participates in Spaces, Exposé, and fullscreen mode.
88///
89/// # Example
90/// ```rust
91/// use tauri_nspanel::{CollectionBehavior, PanelBuilder};
92///
93/// // Create a panel that appears on all spaces and ignores Cmd+Tab cycling
94/// let behavior = CollectionBehavior::new()
95///     .can_join_all_spaces()
96///     .ignores_cycle();
97///
98/// // Use with PanelBuilder
99/// PanelBuilder::new(&app, "my-panel")
100///     .collection_behavior(behavior)
101///     .build();
102/// ```
103#[derive(Debug, Clone, Copy, PartialEq)]
104pub struct CollectionBehavior(objc2_app_kit::NSWindowCollectionBehavior);
105
106impl CollectionBehavior {
107    /// Create an empty collection behavior
108    pub fn new() -> Self {
109        Self(objc2_app_kit::NSWindowCollectionBehavior::empty())
110    }
111
112    /// Window can be shown on another space
113    pub fn can_join_all_spaces(mut self) -> Self {
114        self.0 |= objc2_app_kit::NSWindowCollectionBehavior::CanJoinAllSpaces;
115        self
116    }
117
118    /// Window appears in all spaces
119    pub fn move_to_active_space(mut self) -> Self {
120        self.0 |= objc2_app_kit::NSWindowCollectionBehavior::MoveToActiveSpace;
121        self
122    }
123
124    /// Window is managed by Spaces
125    pub fn managed(mut self) -> Self {
126        self.0 |= objc2_app_kit::NSWindowCollectionBehavior::Managed;
127        self
128    }
129
130    /// Window participates in Spaces and Expose
131    pub fn transient(mut self) -> Self {
132        self.0 |= objc2_app_kit::NSWindowCollectionBehavior::Transient;
133        self
134    }
135
136    /// Window does not participate in Spaces or Expose
137    pub fn stationary(mut self) -> Self {
138        self.0 |= objc2_app_kit::NSWindowCollectionBehavior::Stationary;
139        self
140    }
141
142    /// Window participates in cycling
143    pub fn participates_in_cycle(mut self) -> Self {
144        self.0 |= objc2_app_kit::NSWindowCollectionBehavior::ParticipatesInCycle;
145        self
146    }
147
148    /// Window ignores cycling commands
149    pub fn ignores_cycle(mut self) -> Self {
150        self.0 |= objc2_app_kit::NSWindowCollectionBehavior::IgnoresCycle;
151        self
152    }
153
154    /// Window can be shown in full screen
155    pub fn full_screen_primary(mut self) -> Self {
156        self.0 |= objc2_app_kit::NSWindowCollectionBehavior::FullScreenPrimary;
157        self
158    }
159
160    /// Window can be shown alongside full screen window
161    pub fn full_screen_auxiliary(mut self) -> Self {
162        self.0 |= objc2_app_kit::NSWindowCollectionBehavior::FullScreenAuxiliary;
163        self
164    }
165
166    /// Window does not allow full screen
167    pub fn full_screen_none(mut self) -> Self {
168        self.0 |= objc2_app_kit::NSWindowCollectionBehavior::FullScreenNone;
169        self
170    }
171
172    /// Window can be shown in full screen for this space only
173    pub fn full_screen_allows_tiling(mut self) -> Self {
174        self.0 |= objc2_app_kit::NSWindowCollectionBehavior::FullScreenAllowsTiling;
175        self
176    }
177
178    /// Window does not allow full screen and hides on app deactivation
179    pub fn full_screen_disallows_tiling(mut self) -> Self {
180        self.0 |= objc2_app_kit::NSWindowCollectionBehavior::FullScreenDisallowsTiling;
181        self
182    }
183
184    /// Create from raw NSWindowCollectionBehavior flags
185    pub fn from_raw(flags: objc2_app_kit::NSWindowCollectionBehavior) -> Self {
186        Self(flags)
187    }
188
189    /// Get the raw NSWindowCollectionBehavior flags
190    pub fn value(&self) -> objc2_app_kit::NSWindowCollectionBehavior {
191        self.0
192    }
193}
194
195impl Default for CollectionBehavior {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201impl From<CollectionBehavior> for objc2_app_kit::NSWindowCollectionBehavior {
202    fn from(behavior: CollectionBehavior) -> Self {
203        behavior.0
204    }
205}
206
207impl From<objc2_app_kit::NSWindowCollectionBehavior> for CollectionBehavior {
208    fn from(value: objc2_app_kit::NSWindowCollectionBehavior) -> Self {
209        CollectionBehavior(value)
210    }
211}
212
213/// Tracking area options builder for NSPanel
214///
215/// Allows combining multiple tracking area options using the builder pattern.
216/// Tracking areas enable mouse event tracking within a specific region of a view.
217///
218/// # Example
219/// ```rust
220/// use tauri_nspanel::{TrackingAreaOptions, PanelBuilder};
221///
222/// // Track mouse movement and enter/exit events, active in any application state
223/// let options = TrackingAreaOptions::new()
224///     .active_always()
225///     .mouse_entered_and_exited()
226///     .mouse_moved();
227///
228/// // Use with panel macro
229/// panel!(MyPanel {
230///     with: {
231///         tracking_area: {
232///             options: options,
233///             auto_resize: true
234///         }
235///     }
236/// });
237///
238/// // Or use with PanelBuilder
239/// PanelBuilder::new(&app, "my-panel")
240///     .tracking_area(options, true)
241///     .build();
242/// ```
243#[derive(Debug, Clone, Copy, PartialEq)]
244pub struct TrackingAreaOptions(objc2_app_kit::NSTrackingAreaOptions);
245
246impl TrackingAreaOptions {
247    /// Create empty tracking area options
248    pub fn new() -> Self {
249        Self(objc2_app_kit::NSTrackingAreaOptions::empty())
250    }
251
252    /// Track mouse moved events
253    pub fn mouse_moved(mut self) -> Self {
254        self.0 |= objc2_app_kit::NSTrackingAreaOptions::MouseMoved;
255        self
256    }
257
258    /// Track mouse entered and exited events
259    pub fn mouse_entered_and_exited(mut self) -> Self {
260        self.0 |= objc2_app_kit::NSTrackingAreaOptions::MouseEnteredAndExited;
261        self
262    }
263
264    /// Track when mouse is active in any application
265    pub fn active_always(mut self) -> Self {
266        self.0 |= objc2_app_kit::NSTrackingAreaOptions::ActiveAlways;
267        self
268    }
269
270    /// Track when mouse is active in this application
271    pub fn active_in_active_app(mut self) -> Self {
272        self.0 |= objc2_app_kit::NSTrackingAreaOptions::ActiveInActiveApp;
273        self
274    }
275
276    /// Track when mouse is active in key window
277    pub fn active_in_key_window(mut self) -> Self {
278        self.0 |= objc2_app_kit::NSTrackingAreaOptions::ActiveInKeyWindow;
279        self
280    }
281
282    /// Track when window is key
283    pub fn active_when_first_responder(mut self) -> Self {
284        self.0 |= objc2_app_kit::NSTrackingAreaOptions::ActiveWhenFirstResponder;
285        self
286    }
287
288    /// Assumes tracking area is active
289    pub fn assume_inside(mut self) -> Self {
290        self.0 |= objc2_app_kit::NSTrackingAreaOptions::AssumeInside;
291        self
292    }
293
294    /// Tracking area is in visibleRect coordinates
295    pub fn in_visible_rect(mut self) -> Self {
296        self.0 |= objc2_app_kit::NSTrackingAreaOptions::InVisibleRect;
297        self
298    }
299
300    /// Enable cursor update events
301    pub fn cursor_update(mut self) -> Self {
302        self.0 |= objc2_app_kit::NSTrackingAreaOptions::CursorUpdate;
303        self
304    }
305
306    /// Create from raw NSTrackingAreaOptions flags
307    pub fn from_raw(flags: objc2_app_kit::NSTrackingAreaOptions) -> Self {
308        Self(flags)
309    }
310
311    /// Get the raw NSTrackingAreaOptions flags
312    pub fn value(&self) -> objc2_app_kit::NSTrackingAreaOptions {
313        self.0
314    }
315}
316
317impl Default for TrackingAreaOptions {
318    fn default() -> Self {
319        Self::new()
320    }
321}
322
323impl From<TrackingAreaOptions> for objc2_app_kit::NSTrackingAreaOptions {
324    fn from(options: TrackingAreaOptions) -> Self {
325        options.0
326    }
327}
328
329impl From<objc2_app_kit::NSTrackingAreaOptions> for TrackingAreaOptions {
330    fn from(value: objc2_app_kit::NSTrackingAreaOptions) -> Self {
331        TrackingAreaOptions(value)
332    }
333}
334
335/// Window style mask builder for NSPanel
336///
337/// Allows combining multiple style masks using the builder pattern.
338/// Style masks control the appearance and behavior of the window frame.
339///
340/// # Example
341/// ```rust
342/// use tauri_nspanel::{StyleMask, PanelBuilder};
343///
344/// // Create a borderless panel that doesn't activate the app
345/// let style = StyleMask::new()
346///     .borderless()
347///     .nonactivating_panel();
348///
349/// // Use with PanelBuilder
350/// PanelBuilder::new(&app, "my-panel")
351///     .style_mask(style)
352///     .build();
353/// ```
354#[derive(Debug, Clone, Copy, PartialEq)]
355pub struct StyleMask(objc2_app_kit::NSWindowStyleMask);
356
357impl StyleMask {
358    /// Create with default style mask (Titled | Closable | Miniaturizable | Resizable)
359    pub fn new() -> Self {
360        Self(
361            objc2_app_kit::NSWindowStyleMask::Titled
362                | objc2_app_kit::NSWindowStyleMask::Closable
363                | objc2_app_kit::NSWindowStyleMask::Miniaturizable
364                | objc2_app_kit::NSWindowStyleMask::Resizable,
365        )
366    }
367
368    /// Create an empty style mask
369    pub fn empty() -> Self {
370        Self(objc2_app_kit::NSWindowStyleMask::empty())
371    }
372
373    /// Window has a title bar
374    pub fn titled(mut self) -> Self {
375        self.0 |= objc2_app_kit::NSWindowStyleMask::Titled;
376        self
377    }
378
379    /// Window has a close button
380    pub fn closable(mut self) -> Self {
381        self.0 |= objc2_app_kit::NSWindowStyleMask::Closable;
382        self
383    }
384
385    /// Window has a minimize button
386    pub fn miniaturizable(mut self) -> Self {
387        self.0 |= objc2_app_kit::NSWindowStyleMask::Miniaturizable;
388        self
389    }
390
391    /// Window can be resized
392    pub fn resizable(mut self) -> Self {
393        self.0 |= objc2_app_kit::NSWindowStyleMask::Resizable;
394        self
395    }
396
397    /// Window uses unified title and toolbar
398    pub fn unified_title_and_toolbar(mut self) -> Self {
399        self.0 |= objc2_app_kit::NSWindowStyleMask::UnifiedTitleAndToolbar;
400        self
401    }
402
403    /// Window uses full size content view
404    pub fn full_size_content_view(mut self) -> Self {
405        self.0 |= objc2_app_kit::NSWindowStyleMask::FullSizeContentView;
406        self
407    }
408
409    /// Window is a utility window
410    pub fn utility_window(mut self) -> Self {
411        self.0 |= objc2_app_kit::NSWindowStyleMask::UtilityWindow;
412        self
413    }
414
415    /// Window is a HUD window
416    pub fn hud_window(mut self) -> Self {
417        self.0 |= objc2_app_kit::NSWindowStyleMask::HUDWindow;
418        self
419    }
420
421    /// Window is a non-activating panel
422    pub fn nonactivating_panel(mut self) -> Self {
423        self.0 |= objc2_app_kit::NSWindowStyleMask::NonactivatingPanel;
424        self
425    }
426
427    /// Window has no title bar or border
428    pub fn borderless(mut self) -> Self {
429        self.0 = objc2_app_kit::NSWindowStyleMask::Borderless;
430        self
431    }
432
433    /// Create from raw NSWindowStyleMask flags
434    pub fn from_raw(flags: objc2_app_kit::NSWindowStyleMask) -> Self {
435        Self(flags)
436    }
437
438    /// Get the raw NSWindowStyleMask flags
439    pub fn value(&self) -> objc2_app_kit::NSWindowStyleMask {
440        self.0
441    }
442}
443
444impl Default for StyleMask {
445    fn default() -> Self {
446        Self::new()
447    }
448}
449
450impl From<StyleMask> for objc2_app_kit::NSWindowStyleMask {
451    fn from(mask: StyleMask) -> Self {
452        mask.0
453    }
454}
455
456impl From<objc2_app_kit::NSWindowStyleMask> for StyleMask {
457    fn from(value: objc2_app_kit::NSWindowStyleMask) -> Self {
458        StyleMask(value)
459    }
460}
461
462#[derive(Default)]
463pub(crate) struct PanelConfig {
464    pub floating: Option<bool>,
465    pub level: Option<PanelLevel>,
466    pub has_shadow: Option<bool>,
467    pub opaque: Option<bool>,
468    pub alpha_value: Option<f64>,
469    pub hides_on_deactivate: Option<bool>,
470    pub becomes_key_only_if_needed: Option<bool>,
471    pub accepts_mouse_moved_events: Option<bool>,
472    pub ignores_mouse_events: Option<bool>,
473    pub movable_by_window_background: Option<bool>,
474    pub released_when_closed: Option<bool>,
475    pub works_when_modal: Option<bool>,
476    pub content_size: Option<Size>,
477    pub style_mask: Option<StyleMask>,
478    pub collection_behavior: Option<CollectionBehavior>,
479    pub no_activate: Option<bool>,
480    pub corner_radius: Option<f64>,
481    pub transparent: Option<bool>,
482}
483
484/// Builder for creating panels with Tauri-like API
485///
486/// PanelBuilder provides a fluent interface that creates a Tauri window,
487/// converts it to an NSPanel, and applies panel-specific configurations.
488/// It can work with both the default panel type or custom panel classes
489/// created with the `panel!` macro.
490///
491/// # Type Parameters
492/// - `R`: The Tauri runtime type
493/// - `T`: The panel type (must implement `FromWindow<R>`)
494///
495/// # Example
496/// ```rust
497/// use tauri_nspanel::{panel, PanelBuilder, PanelLevel};
498///
499/// // Using default panel type
500/// let panel = PanelBuilder::new(&app, "my-panel")
501///     .url(WebviewUrl::App("panel.html".into()))
502///     .title("Tool Panel")
503///     .level(PanelLevel::Floating)
504///     .build()?;
505///
506/// // Using custom panel type
507/// panel!(CustomPanel {
508///     config: {
509///         can_become_key_window: false
510///     }
511/// });
512///
513/// let custom = PanelBuilder::<_, CustomPanel>::new(&app, "custom")
514///     .url(WebviewUrl::App("custom.html".into()))
515///     .build()?;
516/// ```
517pub struct PanelBuilder<'a, R: Runtime, T: FromWindow<R> + 'static> {
518    handle: &'a AppHandle<R>,
519    label: String,
520    url: Option<WebviewUrl>,
521    title: Option<String>,
522    position: Option<Position>,
523    size: Option<Size>,
524    pub(crate) panel_config: PanelConfig,
525    window_fn: Option<WindowConfigFn<'a, R>>,
526    _phantom: std::marker::PhantomData<T>,
527}
528
529impl<'a, R: Runtime + 'a, T: FromWindow<R> + 'static> PanelBuilder<'a, R, T> {
530    /// Create a new PanelBuilder
531    pub fn new(handle: &'a AppHandle<R>, label: impl Into<String>) -> Self {
532        Self {
533            handle,
534            label: label.into(),
535            url: None,
536            title: None,
537            position: None,
538            size: None,
539            panel_config: PanelConfig::default(),
540            window_fn: None,
541            _phantom: std::marker::PhantomData,
542        }
543    }
544
545    /// Set the webview URL
546    pub fn url(mut self, url: WebviewUrl) -> Self {
547        self.url = Some(url);
548        self
549    }
550
551    /// Set the window title
552    pub fn title(mut self, title: impl Into<String>) -> Self {
553        self.title = Some(title.into());
554        self
555    }
556
557    /// Set the window position
558    pub fn position(mut self, position: Position) -> Self {
559        self.position = Some(position);
560        self
561    }
562
563    /// Set the window size
564    pub fn size(mut self, size: Size) -> Self {
565        self.size = Some(size);
566        self
567    }
568
569    /// Set whether the panel floats above other windows
570    pub fn floating(mut self, floating: bool) -> Self {
571        self.panel_config.floating = Some(floating);
572        self
573    }
574
575    /// Set the window level
576    ///
577    /// The window level determines the panel's position in the window hierarchy.
578    /// Higher levels appear above lower levels.
579    ///
580    /// # Example
581    /// ```rust
582    /// use tauri_nspanel::{PanelBuilder, PanelLevel};
583    /// // Create a panel that floats above normal windows
584    /// PanelBuilder::new(&app, "floating")
585    ///     .level(PanelLevel::Floating)
586    ///     .build();
587    ///
588    /// // Create a status-level panel (appears above floating panels)
589    /// PanelBuilder::new(&app, "status")
590    ///     .level(PanelLevel::Status)
591    ///     .build();
592    /// ```
593    pub fn level(mut self, level: PanelLevel) -> Self {
594        self.panel_config.level = Some(level);
595        self
596    }
597
598    /// Set whether the panel has a shadow
599    pub fn has_shadow(mut self, has_shadow: bool) -> Self {
600        self.panel_config.has_shadow = Some(has_shadow);
601        self
602    }
603
604    /// Set whether the panel is opaque
605    pub fn opaque(mut self, opaque: bool) -> Self {
606        self.panel_config.opaque = Some(opaque);
607        self
608    }
609
610    /// Set the alpha value (transparency)
611    pub fn alpha_value(mut self, alpha: f64) -> Self {
612        self.panel_config.alpha_value = Some(alpha);
613        self
614    }
615
616    /// Set whether the panel hides when the app is deactivated
617    pub fn hides_on_deactivate(mut self, hides: bool) -> Self {
618        self.panel_config.hides_on_deactivate = Some(hides);
619        self
620    }
621
622    /// Set whether the panel becomes key window only if needed
623    pub fn becomes_key_only_if_needed(mut self, value: bool) -> Self {
624        self.panel_config.becomes_key_only_if_needed = Some(value);
625        self
626    }
627
628    /// Set whether the panel accepts mouse moved events
629    pub fn accepts_mouse_moved_events(mut self, value: bool) -> Self {
630        self.panel_config.accepts_mouse_moved_events = Some(value);
631        self
632    }
633
634    /// Set whether the panel ignores mouse events
635    pub fn ignores_mouse_events(mut self, value: bool) -> Self {
636        self.panel_config.ignores_mouse_events = Some(value);
637        self
638    }
639
640    /// Set whether the panel is movable by its background
641    pub fn movable_by_window_background(mut self, value: bool) -> Self {
642        self.panel_config.movable_by_window_background = Some(value);
643        self
644    }
645
646    /// Set whether the panel is released when closed
647    pub fn released_when_closed(mut self, value: bool) -> Self {
648        self.panel_config.released_when_closed = Some(value);
649        self
650    }
651
652    /// Set whether the panel works when modal dialogs are displayed
653    pub fn works_when_modal(mut self, value: bool) -> Self {
654        self.panel_config.works_when_modal = Some(value);
655        self
656    }
657
658    /// Set the content size (inner size excluding window decorations)
659    pub fn content_size(mut self, size: Size) -> Self {
660        self.panel_config.content_size = Some(size);
661        self
662    }
663
664    /// Set the window style mask
665    ///
666    /// Style masks control the appearance and behavior of the window frame.
667    ///
668    /// # Example
669    /// ```rust
670    /// use tauri_nspanel::{PanelBuilder, StyleMask};
671    /// // Create a borderless panel
672    /// PanelBuilder::new(&app, "borderless")
673    ///     .style_mask(StyleMask::empty().borderless())
674    ///     .build();
675    ///
676    /// // Create a HUD-style panel
677    /// PanelBuilder::new(&app, "hud")
678    ///     .style_mask(
679    ///         StyleMask::empty()
680    ///             .hud_window()
681    ///             .titled()
682    ///             .closable()
683    ///     )
684    ///     .build();
685    /// ```
686    pub fn style_mask(mut self, style_mask: StyleMask) -> Self {
687        self.panel_config.style_mask = Some(style_mask);
688        self
689    }
690
691    /// Set the collection behavior
692    ///
693    /// Collection behaviors control how the panel participates in Spaces, Exposé,
694    /// and fullscreen mode on macOS.
695    ///
696    /// # Example
697    /// ```rust
698    /// use tauri_nspanel::{CollectionBehavior, PanelBuilder};
699    /// // Create a panel that appears on all spaces and doesn't participate in cycling
700    /// PanelBuilder::new(&app, "tool-panel")
701    ///     .collection_behavior(
702    ///         CollectionBehavior::new()
703    ///             .can_join_all_spaces()
704    ///             .ignores_cycle()
705    ///     )
706    ///     .build();
707    /// ```
708    pub fn collection_behavior(mut self, behavior: CollectionBehavior) -> Self {
709        self.panel_config.collection_behavior = Some(behavior);
710        self
711    }
712
713    /// Prevent focus stealing during window creation
714    ///
715    /// Since PanelBuilder creates a regular window before converting it to a panel,
716    /// the window creation phase can steal focus. When set to true, the application's
717    /// activation policy is temporarily set to Prohibited during window creation,
718    /// preventing this focus interruption.
719    ///
720    /// This works particularly well with apps that use `ActivationPolicy::Accessory`,
721    /// ensuring the window is created silently before being converted to a panel.
722    ///
723    /// # Example
724    /// ```rust
725    /// use tauri_nspanel::{PanelBuilder, PanelLevel};
726    /// use tauri::WebviewUrl;
727    /// // Create a utility panel that doesn't steal focus
728    /// PanelBuilder::new(&app, "utility")
729    ///     .url(WebviewUrl::App("utility.html".into()))
730    ///     .no_activate(true)
731    ///     .level(PanelLevel::Floating)
732    ///     .build();
733    /// ```
734    pub fn no_activate(mut self, no_activate: bool) -> Self {
735        self.panel_config.no_activate = Some(no_activate);
736        self
737    }
738
739    /// Set the corner radius for rounded corners
740    ///
741    /// This enables the layer-backed view and sets the corner radius on the panel's layer,
742    /// giving the panel rounded corners with the specified radius.
743    ///
744    /// # Example
745    /// ```rust
746    /// use tauri_nspanel::PanelBuilder;
747    /// use tauri::WebviewUrl;
748    /// PanelBuilder::new(&app, "rounded-panel")
749    ///     .url(WebviewUrl::App("index.html".into()))
750    ///     .corner_radius(10.0)  // 10pt corner radius
751    ///     .build();
752    /// ```
753    pub fn corner_radius(mut self, radius: f64) -> Self {
754        self.panel_config.corner_radius = Some(radius);
755        self
756    }
757
758    /// Set the panel background to be transparent
759    ///
760    /// This sets the window background color to clear and makes the panel non-opaque,
761    /// allowing content behind the panel to show through.
762    ///
763    /// # Example
764    /// ```rust
765    /// use tauri_nspanel::PanelBuilder;
766    /// use tauri::WebviewUrl;
767    /// PanelBuilder::new(&app, "transparent-panel")
768    ///     .url(WebviewUrl::App("index.html".into()))
769    ///     .transparent(true)  // Transparent background
770    ///     .build();
771    /// ```
772    pub fn transparent(mut self, transparent: bool) -> Self {
773        self.panel_config.transparent = Some(transparent);
774        self
775    }
776
777    /// Apply a custom configuration function to the WebviewWindowBuilder
778    ///
779    /// This allows access to any Tauri window configuration not exposed by the panel builder.
780    /// The closure receives the WebviewWindowBuilder and should return it after applying
781    /// any desired configurations.
782    ///
783    /// # Example
784    /// ```rust
785    /// use tauri_nspanel::PanelBuilder;
786    /// use tauri::WebviewUrl;
787    /// PanelBuilder::new(&app, "my-panel")
788    ///     .url(WebviewUrl::App("index.html".into()))
789    ///     .with_window(|window| {
790    ///         window
791    ///             .min_inner_size(300.0, 200.0)
792    ///             .max_inner_size(800.0, 600.0)
793    ///             .resizable(false)
794    ///             .decorations(false)
795    ///             .always_on_top(true)
796    ///             .skip_taskbar(true)
797    ///     })
798    ///     .build()
799    /// ```
800    pub fn with_window<F>(mut self, f: F) -> Self
801    where
802        F: FnOnce(
803                WebviewWindowBuilder<'a, R, AppHandle<R>>,
804            ) -> WebviewWindowBuilder<'a, R, AppHandle<R>>
805            + 'static,
806    {
807        self.window_fn = Some(Box::new(f) as WindowConfigFn<'a, R>);
808        self
809    }
810
811    /// Build the panel
812    ///
813    /// Creates a Tauri window using the configured properties, converts it to
814    /// an NSPanel, and applies all panel-specific settings.
815    pub fn build(self) -> tauri::Result<Arc<dyn Panel<R>>> {
816        // Handle no_activate option by temporarily changing activation policy
817        let original_policy = if self.panel_config.no_activate.unwrap_or(false) {
818            MainThreadMarker::new().map(|mtm| unsafe {
819                let app = NSApplication::sharedApplication(mtm);
820                let current_policy = app.activationPolicy();
821                let _success = app.setActivationPolicy(NSApplicationActivationPolicy::Prohibited);
822                current_policy
823            })
824        } else {
825            None
826        };
827
828        // Create a window first
829        let mut window_builder = WebviewWindowBuilder::new(
830            self.handle,
831            &self.label,
832            self.url.unwrap_or(WebviewUrl::App("index.html".into())),
833        );
834
835        if let Some(title) = self.title {
836            window_builder = window_builder.title(title);
837        }
838
839        if let Some(position) = self.position {
840            match position {
841                Position::Physical(pos) => {
842                    window_builder = window_builder.position(pos.x as f64, pos.y as f64);
843                }
844                Position::Logical(pos) => {
845                    window_builder = window_builder.position(pos.x, pos.y);
846                }
847            }
848        }
849
850        if let Some(size) = self.size {
851            match size {
852                Size::Physical(s) => {
853                    window_builder = window_builder.inner_size(s.width as f64, s.height as f64);
854                }
855                Size::Logical(s) => {
856                    window_builder = window_builder.inner_size(s.width, s.height);
857                }
858            }
859        }
860
861        // Apply custom configuration if provided
862        if let Some(window_fn) = self.window_fn {
863            window_builder = window_fn(window_builder);
864        }
865
866        // Build the window
867        let window = window_builder.build()?;
868
869        // Convert to panel
870        let panel = window.to_panel::<T>().unwrap();
871
872        // Apply panel configuration using the Panel trait methods
873        if let Some(floating) = self.panel_config.floating {
874            panel.set_floating_panel(floating);
875        }
876        if let Some(level) = self.panel_config.level {
877            panel.set_level(level.value());
878        }
879        if let Some(has_shadow) = self.panel_config.has_shadow {
880            panel.set_has_shadow(has_shadow);
881        }
882        if let Some(opaque) = self.panel_config.opaque {
883            panel.set_opaque(opaque);
884        }
885        if let Some(alpha_value) = self.panel_config.alpha_value {
886            panel.set_alpha_value(alpha_value);
887        }
888        if let Some(hides) = self.panel_config.hides_on_deactivate {
889            panel.set_hides_on_deactivate(hides);
890        }
891        if let Some(value) = self.panel_config.becomes_key_only_if_needed {
892            panel.set_becomes_key_only_if_needed(value);
893        }
894        if let Some(value) = self.panel_config.accepts_mouse_moved_events {
895            panel.set_accepts_mouse_moved_events(value);
896        }
897        if let Some(value) = self.panel_config.ignores_mouse_events {
898            panel.set_ignores_mouse_events(value);
899        }
900        if let Some(value) = self.panel_config.movable_by_window_background {
901            panel.set_movable_by_window_background(value);
902        }
903        if let Some(value) = self.panel_config.released_when_closed {
904            panel.set_released_when_closed(value);
905        }
906        if let Some(value) = self.panel_config.works_when_modal {
907            panel.set_works_when_modal(value);
908        }
909        if let Some(style_mask) = self.panel_config.style_mask {
910            panel.set_style_mask(style_mask.0);
911        }
912        if let Some(behavior) = self.panel_config.collection_behavior {
913            panel.set_collection_behavior(behavior.0);
914        }
915        if let Some(radius) = self.panel_config.corner_radius {
916            panel.set_corner_radius(radius);
917        }
918        if let Some(transparent) = self.panel_config.transparent {
919            panel.set_transparent(transparent);
920        }
921
922        // Restore original activation policy if we changed it
923        if let Some(policy) = original_policy {
924            if let Some(mtm) = MainThreadMarker::new() {
925                let app = NSApplication::sharedApplication(mtm);
926                let _success = app.setActivationPolicy(policy);
927            }
928        }
929
930        Ok(panel)
931    }
932}