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}