core_animation/
window.rs

1//! Test window for running examples.
2//!
3//! For production use, integrate layers with your own window management.
4
5use std::time::Duration;
6
7use crate::color::Color;
8use crate::shape_layer_builder::CAShapeLayerBuilder;
9use crate::text_layer_builder::CATextLayerBuilder;
10use objc2::rc::Retained;
11use objc2::{MainThreadMarker, MainThreadOnly};
12use objc2_app_kit::{
13    NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSColor, NSScreen, NSWindow,
14    NSWindowStyleMask,
15};
16use objc2_core_foundation::{kCFRunLoopDefaultMode, CFRunLoop, CFTimeInterval};
17use objc2_core_graphics::CGColor;
18use objc2_foundation::{NSPoint, NSRect, NSSize, NSString};
19use objc2_quartz_core::{CALayer, CAShapeLayer, CATextLayer};
20
21/// Specifies which screen to use for window placement.
22#[derive(Clone, Debug, Default)]
23pub enum Screen {
24    /// The main screen (default). This is typically the screen with the menu bar.
25    #[default]
26    Main,
27    /// A specific screen by index (0-based). Index 0 is typically the main screen.
28    Index(usize),
29}
30
31/// Window level determining the z-order of the overlay window.
32///
33/// Controls where the window appears in the window stack relative to other windows.
34///
35/// # Common levels
36///
37/// - `Normal` (0): Standard application windows
38/// - `Floating` (3): Floating palettes, tool windows
39/// - `ModalPanel` (8): Modal panels that block interaction
40/// - `ScreenSaver` (1000): Screen saver level
41/// - `AboveAll` (1001): Above all windows including fullscreen apps
42///
43/// # Example
44///
45/// ```ignore
46/// use core_animation::prelude::*;
47///
48/// let window = WindowBuilder::new()
49///     .size(200.0, 200.0)
50///     .level(WindowLevel::AboveAll)
51///     .build();
52/// ```
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
54pub enum WindowLevel {
55    /// Normal window level (0), appears with regular application windows.
56    Normal,
57    /// Floating window level (3), appears above normal windows.
58    Floating,
59    /// Modal panel level (8), appears above floating windows.
60    ModalPanel,
61    /// Screen saver level (1000).
62    ScreenSaver,
63    /// Above all other windows (1001), including fullscreen apps and the Dock.
64    #[default]
65    AboveAll,
66    /// Custom window level value (platform-specific).
67    Custom(isize),
68}
69
70impl WindowLevel {
71    /// Returns the raw window level value for NSWindow.
72    pub fn raw_level(&self) -> isize {
73        match self {
74            WindowLevel::Normal => 0,
75            WindowLevel::Floating => 3,
76            WindowLevel::ModalPanel => 8,
77            WindowLevel::ScreenSaver => 1000,
78            WindowLevel::AboveAll => 1001,
79            WindowLevel::Custom(level) => *level,
80        }
81    }
82}
83
84/// Window style options.
85#[derive(Clone, Debug)]
86pub struct WindowStyle {
87    pub titled: bool,
88    pub closable: bool,
89    pub resizable: bool,
90    pub miniaturizable: bool,
91    pub borderless: bool,
92}
93
94impl Default for WindowStyle {
95    fn default() -> Self {
96        Self {
97            titled: true,
98            closable: true,
99            resizable: true,
100            miniaturizable: true,
101            borderless: false,
102        }
103    }
104}
105
106/// Builder for test windows.
107///
108/// ```ignore
109/// let window = WindowBuilder::new()
110///     .title("Demo")
111///     .size(640.0, 480.0)
112///     .centered()
113///     .background_color(Color::gray(0.1))
114///     .build();
115///
116/// window.container().add_sublayer(&my_layer);
117/// window.show_for(5.seconds());
118/// ```
119pub struct WindowBuilder {
120    title: String,
121    size: (f64, f64),
122    position: Option<(f64, f64)>,
123    centered: bool,
124    screen: Screen,
125    style: WindowStyle,
126    background: Option<Color>,
127    activation_policy: NSApplicationActivationPolicy,
128    transparent: bool,
129    corner_radius: Option<f64>,
130    level: Option<WindowLevel>,
131    border_color: Option<Color>,
132    layers: Vec<(String, Retained<CAShapeLayer>)>,
133    text_layers: Vec<(String, Retained<CATextLayer>)>,
134    non_activating: bool,
135    ignores_mouse: bool,
136}
137
138impl WindowBuilder {
139    /// Create a new window builder with default settings.
140    pub fn new() -> Self {
141        Self {
142            title: String::from("Window"),
143            size: (640.0, 480.0),
144            position: None,
145            centered: false,
146            screen: Screen::Main,
147            style: WindowStyle::default(),
148            background: None,
149            activation_policy: NSApplicationActivationPolicy::Accessory,
150            transparent: false,
151            corner_radius: None,
152            level: None,
153            border_color: None,
154            layers: Vec::new(),
155            text_layers: Vec::new(),
156            non_activating: false,
157            ignores_mouse: false,
158        }
159    }
160
161    /// Set the window title.
162    pub fn title(mut self, title: impl Into<String>) -> Self {
163        self.title = title.into();
164        self
165    }
166
167    /// Set the window size in points.
168    pub fn size(mut self, width: f64, height: f64) -> Self {
169        self.size = (width, height);
170        self
171    }
172
173    /// Set the window position in screen coordinates.
174    /// This is mutually exclusive with `centered()`.
175    pub fn position(mut self, x: f64, y: f64) -> Self {
176        self.position = Some((x, y));
177        self.centered = false;
178        self
179    }
180
181    /// Center the window on the selected screen.
182    /// This is mutually exclusive with `position()`.
183    pub fn centered(mut self) -> Self {
184        self.centered = true;
185        self.position = None;
186        self
187    }
188
189    /// Select which screen to place the window on.
190    /// Defaults to `Screen::Main`.
191    pub fn on_screen(mut self, screen: Screen) -> Self {
192        self.screen = screen;
193        self
194    }
195
196    /// Set the background color of the root container layer.
197    ///
198    /// Accepts any type that implements `Into<Color>`, including:
199    /// - `Color::RED`, `Color::rgb(0.1, 0.1, 0.2)`
200    /// - `Color::WHITE.with_alpha(0.5)`
201    pub fn background_color(mut self, color: impl Into<Color>) -> Self {
202        self.background = Some(color.into());
203        self
204    }
205
206    /// Set the background color of the root container layer (RGBA, 0.0-1.0).
207    pub fn background_rgba(mut self, r: f64, g: f64, b: f64, a: f64) -> Self {
208        self.background = Some(Color::rgba(r, g, b, a));
209        self
210    }
211
212    /// Set the background color of the root container layer (RGB with alpha=1.0).
213    pub fn background_rgb(mut self, r: f64, g: f64, b: f64) -> Self {
214        self.background = Some(Color::rgb(r, g, b));
215        self
216    }
217
218    /// Configure window style.
219    pub fn style(mut self, style: WindowStyle) -> Self {
220        self.style = style;
221        self
222    }
223
224    /// Make the window borderless (no title bar, not resizable).
225    pub fn borderless(mut self) -> Self {
226        self.style.borderless = true;
227        self.style.titled = false;
228        self
229    }
230
231    /// Set whether the window has a title bar.
232    pub fn titled(mut self, titled: bool) -> Self {
233        self.style.titled = titled;
234        self
235    }
236
237    /// Set whether the window is closable.
238    pub fn closable(mut self, closable: bool) -> Self {
239        self.style.closable = closable;
240        self
241    }
242
243    /// Set whether the window is resizable.
244    pub fn resizable(mut self, resizable: bool) -> Self {
245        self.style.resizable = resizable;
246        self
247    }
248
249    /// Set the application activation policy.
250    /// Defaults to `Accessory` (no dock icon, no menu bar).
251    pub fn activation_policy(mut self, policy: NSApplicationActivationPolicy) -> Self {
252        self.activation_policy = policy;
253        self
254    }
255
256    /// Make the window fully transparent for overlay effects.
257    ///
258    /// When enabled, this sets the NSWindow to be non-opaque with a clear background,
259    /// allowing the content to be drawn on a fully transparent background.
260    /// This is useful for creating floating overlay windows.
261    ///
262    /// # Example
263    ///
264    /// ```ignore
265    /// use core_animation::prelude::*;
266    ///
267    /// let window = WindowBuilder::new()
268    ///     .size(200.0, 200.0)
269    ///     .transparent()
270    ///     .borderless()
271    ///     .build();
272    /// ```
273    pub fn transparent(mut self) -> Self {
274        self.transparent = true;
275        self
276    }
277
278    /// Set the corner radius on the container layer.
279    ///
280    /// This applies rounded corners to the container layer that holds your content.
281    /// Combine with `.background_color()` for a rounded panel effect.
282    ///
283    /// # Arguments
284    ///
285    /// * `radius` - The corner radius in points
286    ///
287    /// # Example
288    ///
289    /// ```ignore
290    /// use core_animation::prelude::*;
291    ///
292    /// let window = WindowBuilder::new()
293    ///     .size(200.0, 200.0)
294    ///     .transparent()
295    ///     .borderless()
296    ///     .corner_radius(20.0)
297    ///     .background_color(Color::gray(0.1).with_alpha(0.85))
298    ///     .build();
299    /// ```
300    pub fn corner_radius(mut self, radius: f64) -> Self {
301        self.corner_radius = Some(radius);
302        self
303    }
304
305    /// Set the window level (z-order).
306    ///
307    /// Controls where the window appears in the window stack relative to other windows.
308    /// Higher levels appear above lower levels.
309    ///
310    /// # Arguments
311    ///
312    /// * `level` - The window level to set
313    ///
314    /// # Example
315    ///
316    /// ```ignore
317    /// use core_animation::prelude::*;
318    ///
319    /// // Create an overlay that floats above all windows
320    /// let window = WindowBuilder::new()
321    ///     .size(200.0, 200.0)
322    ///     .level(WindowLevel::AboveAll)
323    ///     .build();
324    ///
325    /// // Or use a custom level
326    /// let window = WindowBuilder::new()
327    ///     .level(WindowLevel::Custom(500))
328    ///     .build();
329    /// ```
330    pub fn level(mut self, level: WindowLevel) -> Self {
331        self.level = Some(level);
332        self
333    }
334
335    /// Set the border color on the container layer.
336    ///
337    /// When set, this also applies a border width of 1.0 to the container layer.
338    /// This creates a subtle visible border around the window content.
339    ///
340    /// # Arguments
341    ///
342    /// * `color` - The border color
343    ///
344    /// # Example
345    ///
346    /// ```ignore
347    /// use core_animation::prelude::*;
348    ///
349    /// // Create a window with a subtle gray border
350    /// let window = WindowBuilder::new()
351    ///     .size(200.0, 200.0)
352    ///     .transparent()
353    ///     .borderless()
354    ///     .corner_radius(20.0)
355    ///     .background_color(Color::gray(0.1).with_alpha(0.85))
356    ///     .border_color(Color::rgba(0.3, 0.3, 0.35, 0.5))
357    ///     .build();
358    /// ```
359    pub fn border_color(mut self, color: impl Into<Color>) -> Self {
360        self.border_color = Some(color.into());
361        self
362    }
363
364    /// Make the window non-activating (won't steal focus from other apps).
365    ///
366    /// When enabled, showing the window will not make it the key window or
367    /// activate the application. The previously focused app remains focused.
368    ///
369    /// This is ideal for OSD-style overlays that should appear above other
370    /// windows without interrupting the user's workflow.
371    ///
372    /// # Example
373    ///
374    /// ```ignore
375    /// use core_animation::prelude::*;
376    ///
377    /// // Create an OSD that doesn't steal focus
378    /// let osd = WindowBuilder::new()
379    ///     .size(200.0, 80.0)
380    ///     .transparent()
381    ///     .borderless()
382    ///     .level(WindowLevel::ScreenSaver)
383    ///     .non_activating()
384    ///     .build();
385    ///
386    /// osd.show_for(2.seconds()); // Shows without stealing focus
387    /// ```
388    pub fn non_activating(mut self) -> Self {
389        self.non_activating = true;
390        self
391    }
392
393    /// Make the window ignore mouse events (click-through).
394    ///
395    /// When enabled, mouse clicks pass through the window to whatever is
396    /// behind it. Useful for pure display overlays.
397    ///
398    /// # Example
399    ///
400    /// ```ignore
401    /// use core_animation::prelude::*;
402    ///
403    /// // Create a click-through overlay
404    /// let overlay = WindowBuilder::new()
405    ///     .size(200.0, 80.0)
406    ///     .transparent()
407    ///     .borderless()
408    ///     .non_activating()
409    ///     .ignores_mouse_events()
410    ///     .build();
411    /// ```
412    pub fn ignores_mouse_events(mut self) -> Self {
413        self.ignores_mouse = true;
414        self
415    }
416
417    /// Add a shape layer to the window.
418    ///
419    /// The closure receives a [`CAShapeLayerBuilder`] for configuration.
420    /// The built layer will be added to [`Window::container()`] when [`build()`](Self::build) is called.
421    ///
422    /// This allows configuring shape layers inline in the fluent API,
423    /// creating a fully fluent flow from window to layers to animations.
424    ///
425    /// # Arguments
426    ///
427    /// * `name` - A unique identifier for this layer
428    /// * `configure` - A closure that configures the layer builder
429    ///
430    /// # Examples
431    ///
432    /// ```ignore
433    /// use core_animation::prelude::*;
434    ///
435    /// let window = WindowBuilder::new()
436    ///     .title("Animation Demo")
437    ///     .size(400.0, 400.0)
438    ///     .centered()
439    ///     .transparent()
440    ///     .borderless()
441    ///     .background_color(Color::rgba(0.1, 0.1, 0.15, 0.85))
442    ///     .layer("circle", |s| {
443    ///         s.circle(80.0)
444    ///             .position(CGPoint::new(200.0, 200.0))
445    ///             .fill_color(Color::CYAN)
446    ///             .animate("pulse", KeyPath::TransformScale, |a| {
447    ///                 a.values(0.85, 1.15)
448    ///                     .duration(2.seconds())
449    ///                     .autoreverses()
450    ///                     .repeat(Repeat::Forever)
451    ///             })
452    ///     })
453    ///     .build();
454    ///
455    /// window.show_for(10.seconds());
456    /// ```
457    ///
458    /// Multiple layers can be added:
459    ///
460    /// ```ignore
461    /// WindowBuilder::new()
462    ///     .size(400.0, 400.0)
463    ///     .layer("background_ring", |s| {
464    ///         s.circle(200.0)
465    ///             .position(CGPoint::new(200.0, 200.0))
466    ///             .fill_color(Color::TRANSPARENT)
467    ///             .stroke_color(Color::WHITE.with_alpha(0.3))
468    ///             .line_width(2.0)
469    ///     })
470    ///     .layer("main_circle", |s| {
471    ///         s.circle(80.0)
472    ///             .position(CGPoint::new(200.0, 200.0))
473    ///             .fill_color(Color::CYAN)
474    ///     })
475    ///     .build();
476    /// ```
477    pub fn layer<F>(mut self, name: &str, configure: F) -> Self
478    where
479        F: FnOnce(CAShapeLayerBuilder) -> CAShapeLayerBuilder,
480    {
481        let builder = CAShapeLayerBuilder::new();
482        let configured = configure(builder);
483        let layer = configured.build();
484        self.layers.push((name.to_string(), layer));
485        self
486    }
487
488    /// Add a text layer to the window.
489    ///
490    /// The closure receives a [`CATextLayerBuilder`] for configuration.
491    /// The built layer will be added to [`Window::container()`] when [`build()`](Self::build) is called.
492    ///
493    /// # Arguments
494    ///
495    /// * `name` - A unique identifier for this layer
496    /// * `configure` - A closure that configures the text layer builder
497    ///
498    /// # Examples
499    ///
500    /// ```ignore
501    /// use core_animation::prelude::*;
502    ///
503    /// let window = WindowBuilder::new()
504    ///     .title("Text Demo")
505    ///     .size(400.0, 200.0)
506    ///     .centered()
507    ///     .transparent()
508    ///     .borderless()
509    ///     .background_color(Color::rgba(0.1, 0.1, 0.15, 0.85))
510    ///     .text_layer("label", |t| {
511    ///         t.text("Hello, World!")
512    ///             .font_size(24.0)
513    ///             .foreground_color(Color::WHITE)
514    ///             .alignment(TextAlign::Center)
515    ///             .position(CGPoint::new(200.0, 100.0))
516    ///     })
517    ///     .build();
518    ///
519    /// window.show_for(5.seconds());
520    /// ```
521    ///
522    /// Combining text with shape layers:
523    ///
524    /// ```ignore
525    /// WindowBuilder::new()
526    ///     .size(200.0, 100.0)
527    ///     .layer("background", |s| {
528    ///         s.circle(80.0)
529    ///             .position(CGPoint::new(50.0, 50.0))
530    ///             .fill_color(Color::BLUE)
531    ///     })
532    ///     .text_layer("label", |t| {
533    ///         t.text("85%")
534    ///             .font_size(16.0)
535    ///             .foreground_color(Color::WHITE)
536    ///             .position(CGPoint::new(150.0, 50.0))
537    ///     })
538    ///     .build();
539    /// ```
540    pub fn text_layer<F>(mut self, name: &str, configure: F) -> Self
541    where
542        F: FnOnce(CATextLayerBuilder) -> CATextLayerBuilder,
543    {
544        let builder = CATextLayerBuilder::new();
545        let configured = configure(builder);
546        let layer = configured.build();
547        self.text_layers.push((name.to_string(), layer));
548        self
549    }
550
551    /// Build the window.
552    ///
553    /// # Panics
554    ///
555    /// Panics if not called from the main thread.
556    pub fn build(self) -> Window {
557        let mtm = MainThreadMarker::new()
558            .expect("WindowBuilder::build() must be called from the main thread");
559
560        // Initialize application
561        let app = NSApplication::sharedApplication(mtm);
562        app.setActivationPolicy(self.activation_policy);
563
564        // Get the target screen
565        let screen = self.get_screen(mtm);
566        let screen_frame = screen.frame();
567
568        // Calculate window frame
569        let window_size = NSSize::new(self.size.0, self.size.1);
570        let window_origin = if self.centered {
571            NSPoint::new(
572                (screen_frame.size.width - window_size.width) / 2.0 + screen_frame.origin.x,
573                (screen_frame.size.height - window_size.height) / 2.0 + screen_frame.origin.y,
574            )
575        } else if let Some((x, y)) = self.position {
576            NSPoint::new(x, y)
577        } else {
578            // Default: top-left with some margin
579            NSPoint::new(
580                screen_frame.origin.x + 100.0,
581                screen_frame.origin.y + screen_frame.size.height - window_size.height - 100.0,
582            )
583        };
584        let content_rect = NSRect::new(window_origin, window_size);
585
586        // Build style mask
587        let mut style_mask = NSWindowStyleMask::empty();
588        if self.style.borderless {
589            style_mask |= NSWindowStyleMask::Borderless;
590        } else {
591            if self.style.titled {
592                style_mask |= NSWindowStyleMask::Titled;
593            }
594            if self.style.closable {
595                style_mask |= NSWindowStyleMask::Closable;
596            }
597            if self.style.resizable {
598                style_mask |= NSWindowStyleMask::Resizable;
599            }
600            if self.style.miniaturizable {
601                style_mask |= NSWindowStyleMask::Miniaturizable;
602            }
603        }
604
605        // Create window
606        let ns_window = unsafe {
607            let window = NSWindow::alloc(mtm);
608            let window = NSWindow::initWithContentRect_styleMask_backing_defer(
609                window,
610                content_rect,
611                style_mask,
612                NSBackingStoreType::Buffered,
613                false,
614            );
615            window.setReleasedWhenClosed(false);
616            window
617        };
618
619        // Set title
620        let title = NSString::from_str(&self.title);
621        ns_window.setTitle(&title);
622
623        // Apply transparency settings
624        if self.transparent {
625            ns_window.setOpaque(false);
626            let clear_color = NSColor::clearColor();
627            ns_window.setBackgroundColor(Some(&clear_color));
628        }
629
630        // Apply window level
631        if let Some(level) = self.level {
632            ns_window.setLevel(level.raw_level());
633        }
634
635        // Enable layer backing
636        let content_view = ns_window.contentView().expect("Window has no content view");
637        content_view.setWantsLayer(true);
638
639        let root_layer = content_view.layer().expect("View has no layer");
640
641        // Create container layer with background
642        let container = CALayer::new();
643        container.setBounds(objc2_core_foundation::CGRect::new(
644            objc2_core_foundation::CGPoint::new(0.0, 0.0),
645            objc2_core_foundation::CGSize::new(self.size.0, self.size.1),
646        ));
647        container.setPosition(objc2_core_foundation::CGPoint::new(
648            self.size.0 / 2.0,
649            self.size.1 / 2.0,
650        ));
651
652        if let Some(color) = self.background {
653            let cgcolor: objc2_core_foundation::CFRetained<CGColor> = color.into();
654            container.setBackgroundColor(Some(&cgcolor));
655        }
656
657        // Apply corner radius to container layer
658        if let Some(radius) = self.corner_radius {
659            container.setCornerRadius(radius);
660        }
661
662        // Apply border color and width to container layer
663        if let Some(color) = self.border_color {
664            let cgcolor: objc2_core_foundation::CFRetained<CGColor> = color.into();
665            container.setBorderColor(Some(&cgcolor));
666            container.setBorderWidth(1.0);
667        }
668
669        root_layer.addSublayer(&container);
670
671        // Add all configured shape layers to the container
672        for (_name, layer) in &self.layers {
673            container.addSublayer(layer);
674        }
675
676        // Add all configured text layers to the container
677        for (_name, layer) in &self.text_layers {
678            container.addSublayer(layer);
679        }
680
681        // Apply mouse event handling
682        if self.ignores_mouse {
683            ns_window.setIgnoresMouseEvents(true);
684        }
685
686        Window {
687            ns_window,
688            container,
689            size: self.size,
690            mtm,
691            non_activating: self.non_activating,
692        }
693    }
694
695    /// Get the NSScreen for the selected screen.
696    fn get_screen(&self, mtm: MainThreadMarker) -> Retained<NSScreen> {
697        match &self.screen {
698            Screen::Main => NSScreen::mainScreen(mtm).expect("No main screen available"),
699            Screen::Index(idx) => {
700                let screens = NSScreen::screens(mtm);
701                if *idx < screens.len() {
702                    screens.objectAtIndex(*idx)
703                } else {
704                    // Fall back to main screen if index is out of bounds
705                    NSScreen::mainScreen(mtm).expect("No main screen available")
706                }
707            }
708        }
709    }
710}
711
712impl Default for WindowBuilder {
713    fn default() -> Self {
714        Self::new()
715    }
716}
717
718/// A window with a layer container for adding sublayers.
719pub struct Window {
720    ns_window: Retained<NSWindow>,
721    container: Retained<CALayer>,
722    size: (f64, f64),
723    mtm: MainThreadMarker,
724    non_activating: bool,
725}
726
727impl Window {
728    /// Get the container layer.
729    ///
730    /// Add your layers as sublayers of this container.
731    pub fn container(&self) -> &CALayer {
732        &self.container
733    }
734
735    /// Get the window size in points.
736    pub fn size(&self) -> (f64, f64) {
737        self.size
738    }
739
740    /// Get the underlying NSWindow.
741    pub fn ns_window(&self) -> &NSWindow {
742        &self.ns_window
743    }
744
745    /// Returns the CGWindowID for this window.
746    ///
747    /// This is useful for screen recording with t-rec's HeadlessRecorder.
748    ///
749    /// # Example
750    ///
751    /// ```ignore
752    /// use core_animation::prelude::*;
753    /// use t_rec::HeadlessRecorder;
754    ///
755    /// let window = WindowBuilder::new()
756    ///     .title("Demo")
757    ///     .size(400.0, 300.0)
758    ///     .build();
759    ///
760    /// let mut recorder = HeadlessRecorder::builder()
761    ///     .window_id(window.window_id())
762    ///     .fps(30)
763    ///     .output_gif("demo.gif")
764    ///     .build()?;
765    /// ```
766    pub fn window_id(&self) -> u64 {
767        // NSWindow.windowNumber returns NSInteger (isize on macOS)
768        // CGWindowID is u32 but we use u64 for t-rec compatibility
769        self.ns_window.windowNumber() as u64
770    }
771
772    /// Show the window and bring it to the front.
773    ///
774    /// If the window was created with `.non_activating()`, it will appear
775    /// without stealing focus from the currently active application.
776    pub fn show(&self) {
777        if self.non_activating {
778            // Show without stealing focus
779            self.ns_window.orderFrontRegardless();
780        } else {
781            // Traditional behavior: make key and activate
782            self.ns_window.makeKeyAndOrderFront(None);
783            #[allow(deprecated)]
784            NSApplication::sharedApplication(self.mtm).activateIgnoringOtherApps(true);
785        }
786    }
787
788    /// Show the window for a specified duration, then hide it.
789    ///
790    /// This shows the window, processes events for the given duration,
791    /// and then hides the window before returning.
792    ///
793    /// # Example
794    ///
795    /// ```ignore
796    /// use core_animation::prelude::*;
797    ///
798    /// window.show_for(5.seconds());
799    /// window.show_for(500.millis());
800    /// window.show_for(1.5.seconds());
801    /// ```
802    pub fn show_for(&self, duration: Duration) {
803        self.show();
804
805        let start = std::time::Instant::now();
806        while start.elapsed() < duration {
807            self.run_loop_tick();
808        }
809
810        self.hide();
811        self.close();
812    }
813
814    /// Hide the window.
815    ///
816    /// The window is removed from the screen but not destroyed.
817    /// It can be shown again with `show()`.
818    ///
819    /// This method runs a brief event loop tick after ordering out
820    /// to ensure the hide takes effect immediately, which is important
821    /// when called from within another run loop (like t-rec's presenter).
822    pub fn hide(&self) {
823        self.ns_window.orderOut(None);
824        // Run a brief event loop tick to ensure the hide takes effect
825        self.run_loop_tick();
826    }
827
828    /// Close and release the window.
829    ///
830    /// After calling this, the window should not be used again.
831    /// Runs an event loop tick to ensure the close takes effect immediately.
832    pub fn close(&self) {
833        self.ns_window.close();
834        self.run_loop_tick();
835    }
836
837    /// Returns whether the window is currently visible.
838    pub fn is_visible(&self) -> bool {
839        self.ns_window.isVisible()
840    }
841
842    /// Run a single iteration of the event loop.
843    ///
844    /// This processes pending events and returns immediately.
845    /// Useful for custom animation loops.
846    pub fn run_loop_tick(&self) {
847        let mode = unsafe { kCFRunLoopDefaultMode };
848        CFRunLoop::run_in_mode(mode, 1.0 / 60.0 as CFTimeInterval, false);
849    }
850
851    /// Run the event loop indefinitely until the window is closed.
852    pub fn run(&self) {
853        self.show();
854
855        // Run until window is closed
856        while self.ns_window.isVisible() {
857            self.run_loop_tick();
858        }
859    }
860}