use std::time::Duration;
use crate::color::Color;
use crate::shape_layer_builder::CAShapeLayerBuilder;
use crate::text_layer_builder::CATextLayerBuilder;
use objc2::rc::Retained;
use objc2::{MainThreadMarker, MainThreadOnly};
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSColor, NSScreen, NSWindow,
NSWindowStyleMask,
};
use objc2_core_foundation::{kCFRunLoopDefaultMode, CFRunLoop, CFTimeInterval};
use objc2_core_graphics::CGColor;
use objc2_foundation::{NSPoint, NSRect, NSSize, NSString};
use objc2_quartz_core::{CALayer, CAShapeLayer, CATextLayer};
#[derive(Clone, Debug, Default)]
pub enum Screen {
#[default]
Main,
Index(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WindowLevel {
Normal,
Floating,
ModalPanel,
ScreenSaver,
#[default]
AboveAll,
Custom(isize),
}
impl WindowLevel {
pub fn raw_level(&self) -> isize {
match self {
WindowLevel::Normal => 0,
WindowLevel::Floating => 3,
WindowLevel::ModalPanel => 8,
WindowLevel::ScreenSaver => 1000,
WindowLevel::AboveAll => 1001,
WindowLevel::Custom(level) => *level,
}
}
}
#[derive(Clone, Debug)]
pub struct WindowStyle {
pub titled: bool,
pub closable: bool,
pub resizable: bool,
pub miniaturizable: bool,
pub borderless: bool,
}
impl Default for WindowStyle {
fn default() -> Self {
Self {
titled: true,
closable: true,
resizable: true,
miniaturizable: true,
borderless: false,
}
}
}
pub struct WindowBuilder {
title: String,
size: (f64, f64),
position: Option<(f64, f64)>,
centered: bool,
screen: Screen,
style: WindowStyle,
background: Option<Color>,
activation_policy: NSApplicationActivationPolicy,
transparent: bool,
corner_radius: Option<f64>,
level: Option<WindowLevel>,
border_color: Option<Color>,
layers: Vec<(String, Retained<CAShapeLayer>)>,
text_layers: Vec<(String, Retained<CATextLayer>)>,
non_activating: bool,
ignores_mouse: bool,
}
impl WindowBuilder {
pub fn new() -> Self {
Self {
title: String::from("Window"),
size: (640.0, 480.0),
position: None,
centered: false,
screen: Screen::Main,
style: WindowStyle::default(),
background: None,
activation_policy: NSApplicationActivationPolicy::Accessory,
transparent: false,
corner_radius: None,
level: None,
border_color: None,
layers: Vec::new(),
text_layers: Vec::new(),
non_activating: false,
ignores_mouse: false,
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn size(mut self, width: f64, height: f64) -> Self {
self.size = (width, height);
self
}
pub fn position(mut self, x: f64, y: f64) -> Self {
self.position = Some((x, y));
self.centered = false;
self
}
pub fn centered(mut self) -> Self {
self.centered = true;
self.position = None;
self
}
pub fn on_screen(mut self, screen: Screen) -> Self {
self.screen = screen;
self
}
pub fn background_color(mut self, color: impl Into<Color>) -> Self {
self.background = Some(color.into());
self
}
pub fn background_rgba(mut self, r: f64, g: f64, b: f64, a: f64) -> Self {
self.background = Some(Color::rgba(r, g, b, a));
self
}
pub fn background_rgb(mut self, r: f64, g: f64, b: f64) -> Self {
self.background = Some(Color::rgb(r, g, b));
self
}
pub fn style(mut self, style: WindowStyle) -> Self {
self.style = style;
self
}
pub fn borderless(mut self) -> Self {
self.style.borderless = true;
self.style.titled = false;
self
}
pub fn titled(mut self, titled: bool) -> Self {
self.style.titled = titled;
self
}
pub fn closable(mut self, closable: bool) -> Self {
self.style.closable = closable;
self
}
pub fn resizable(mut self, resizable: bool) -> Self {
self.style.resizable = resizable;
self
}
pub fn activation_policy(mut self, policy: NSApplicationActivationPolicy) -> Self {
self.activation_policy = policy;
self
}
pub fn transparent(mut self) -> Self {
self.transparent = true;
self
}
pub fn corner_radius(mut self, radius: f64) -> Self {
self.corner_radius = Some(radius);
self
}
pub fn level(mut self, level: WindowLevel) -> Self {
self.level = Some(level);
self
}
pub fn border_color(mut self, color: impl Into<Color>) -> Self {
self.border_color = Some(color.into());
self
}
pub fn non_activating(mut self) -> Self {
self.non_activating = true;
self
}
pub fn ignores_mouse_events(mut self) -> Self {
self.ignores_mouse = true;
self
}
pub fn layer<F>(mut self, name: &str, configure: F) -> Self
where
F: FnOnce(CAShapeLayerBuilder) -> CAShapeLayerBuilder,
{
let builder = CAShapeLayerBuilder::new();
let configured = configure(builder);
let layer = configured.build();
self.layers.push((name.to_string(), layer));
self
}
pub fn text_layer<F>(mut self, name: &str, configure: F) -> Self
where
F: FnOnce(CATextLayerBuilder) -> CATextLayerBuilder,
{
let builder = CATextLayerBuilder::new();
let configured = configure(builder);
let layer = configured.build();
self.text_layers.push((name.to_string(), layer));
self
}
pub fn build(self) -> Window {
let mtm = MainThreadMarker::new()
.expect("WindowBuilder::build() must be called from the main thread");
let app = NSApplication::sharedApplication(mtm);
app.setActivationPolicy(self.activation_policy);
let screen = self.get_screen(mtm);
let screen_frame = screen.frame();
let window_size = NSSize::new(self.size.0, self.size.1);
let window_origin = if self.centered {
NSPoint::new(
(screen_frame.size.width - window_size.width) / 2.0 + screen_frame.origin.x,
(screen_frame.size.height - window_size.height) / 2.0 + screen_frame.origin.y,
)
} else if let Some((x, y)) = self.position {
NSPoint::new(x, y)
} else {
NSPoint::new(
screen_frame.origin.x + 100.0,
screen_frame.origin.y + screen_frame.size.height - window_size.height - 100.0,
)
};
let content_rect = NSRect::new(window_origin, window_size);
let mut style_mask = NSWindowStyleMask::empty();
if self.style.borderless {
style_mask |= NSWindowStyleMask::Borderless;
} else {
if self.style.titled {
style_mask |= NSWindowStyleMask::Titled;
}
if self.style.closable {
style_mask |= NSWindowStyleMask::Closable;
}
if self.style.resizable {
style_mask |= NSWindowStyleMask::Resizable;
}
if self.style.miniaturizable {
style_mask |= NSWindowStyleMask::Miniaturizable;
}
}
let ns_window = unsafe {
let window = NSWindow::alloc(mtm);
let window = NSWindow::initWithContentRect_styleMask_backing_defer(
window,
content_rect,
style_mask,
NSBackingStoreType::Buffered,
false,
);
window.setReleasedWhenClosed(false);
window
};
let title = NSString::from_str(&self.title);
ns_window.setTitle(&title);
if self.transparent {
ns_window.setOpaque(false);
let clear_color = NSColor::clearColor();
ns_window.setBackgroundColor(Some(&clear_color));
}
if let Some(level) = self.level {
ns_window.setLevel(level.raw_level());
}
let content_view = ns_window.contentView().expect("Window has no content view");
content_view.setWantsLayer(true);
let root_layer = content_view.layer().expect("View has no layer");
let container = CALayer::new();
container.setBounds(objc2_core_foundation::CGRect::new(
objc2_core_foundation::CGPoint::new(0.0, 0.0),
objc2_core_foundation::CGSize::new(self.size.0, self.size.1),
));
container.setPosition(objc2_core_foundation::CGPoint::new(
self.size.0 / 2.0,
self.size.1 / 2.0,
));
if let Some(color) = self.background {
let cgcolor: objc2_core_foundation::CFRetained<CGColor> = color.into();
container.setBackgroundColor(Some(&cgcolor));
}
if let Some(radius) = self.corner_radius {
container.setCornerRadius(radius);
}
if let Some(color) = self.border_color {
let cgcolor: objc2_core_foundation::CFRetained<CGColor> = color.into();
container.setBorderColor(Some(&cgcolor));
container.setBorderWidth(1.0);
}
root_layer.addSublayer(&container);
for (_name, layer) in &self.layers {
container.addSublayer(layer);
}
for (_name, layer) in &self.text_layers {
container.addSublayer(layer);
}
if self.ignores_mouse {
ns_window.setIgnoresMouseEvents(true);
}
Window {
ns_window,
container,
size: self.size,
mtm,
non_activating: self.non_activating,
}
}
fn get_screen(&self, mtm: MainThreadMarker) -> Retained<NSScreen> {
match &self.screen {
Screen::Main => NSScreen::mainScreen(mtm).expect("No main screen available"),
Screen::Index(idx) => {
let screens = NSScreen::screens(mtm);
if *idx < screens.len() {
screens.objectAtIndex(*idx)
} else {
NSScreen::mainScreen(mtm).expect("No main screen available")
}
}
}
}
}
impl Default for WindowBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct Window {
ns_window: Retained<NSWindow>,
container: Retained<CALayer>,
size: (f64, f64),
mtm: MainThreadMarker,
non_activating: bool,
}
impl Window {
pub fn container(&self) -> &CALayer {
&self.container
}
pub fn size(&self) -> (f64, f64) {
self.size
}
pub fn ns_window(&self) -> &NSWindow {
&self.ns_window
}
pub fn window_id(&self) -> u64 {
self.ns_window.windowNumber() as u64
}
pub fn show(&self) {
if self.non_activating {
self.ns_window.orderFrontRegardless();
} else {
self.ns_window.makeKeyAndOrderFront(None);
#[allow(deprecated)]
NSApplication::sharedApplication(self.mtm).activateIgnoringOtherApps(true);
}
}
pub fn show_for(&self, duration: Duration) {
self.show();
let start = std::time::Instant::now();
while start.elapsed() < duration {
self.run_loop_tick();
}
self.hide();
self.close();
}
pub fn hide(&self) {
self.ns_window.orderOut(None);
self.run_loop_tick();
}
pub fn close(&self) {
self.ns_window.close();
self.run_loop_tick();
}
pub fn is_visible(&self) -> bool {
self.ns_window.isVisible()
}
pub fn run_loop_tick(&self) {
let mode = unsafe { kCFRunLoopDefaultMode };
CFRunLoop::run_in_mode(mode, 1.0 / 60.0 as CFTimeInterval, false);
}
pub fn run(&self) {
self.show();
while self.ns_window.isVisible() {
self.run_loop_tick();
}
}
}