use std::cell::Cell;
#[cfg(feature = "icon-style")]
use std::cell::RefCell;
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::{DefinedClass, MainThreadOnly, define_class, msg_send, sel};
use objc2_app_kit::{
NSAppearance, NSAppearanceCustomization, NSBackingStoreType, NSColor, NSView, NSWindow,
NSWindowStyleMask, NSWindowTabbingMode, NSWindowTitleVisibility,
};
use objc2_foundation::{MainThreadMarker, NSObjectProtocol, NSPoint, NSRect, NSSize, NSString};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum Chrome {
Borderless,
#[default]
Titled,
}
impl Chrome {
fn style_mask(self) -> NSWindowStyleMask {
match self {
Self::Borderless => NSWindowStyleMask::Borderless | NSWindowStyleMask::Resizable,
Self::Titled => {
NSWindowStyleMask::Titled
| NSWindowStyleMask::FullSizeContentView
| NSWindowStyleMask::Closable
| NSWindowStyleMask::Miniaturizable
| NSWindowStyleMask::Resizable
}
}
}
}
#[derive(Debug, Default)]
struct WindowState {
close_on_escape: Cell<bool>,
#[cfg(feature = "icon-style")]
style_observer: RefCell<Option<crate::icon_style::StyleObserver>>,
}
fn checked_size(size: NSSize, what: &str) -> NSSize {
assert!(
size.width.is_finite() && size.height.is_finite(),
"{what}: window size must be finite, got {size:?}"
);
size
}
define_class!(
#[unsafe(super(NSWindow))]
#[thread_kind = MainThreadOnly]
#[ivars = WindowState]
#[derive(Debug)]
struct PaneWindow;
impl PaneWindow {
#[unsafe(method(canBecomeKeyWindow))]
fn can_become_key_window(&self) -> bool {
true
}
#[unsafe(method(canBecomeMainWindow))]
fn can_become_main_window(&self) -> bool {
true
}
#[unsafe(method(cancelOperation:))]
fn cancel_operation(&self, _sender: Option<&AnyObject>) {
if !self.ivars().close_on_escape.get() {
return;
}
if let Some(delegate) = self.delegate() {
if delegate.respondsToSelector(sel!(windowShouldClose:)) {
let should: bool = unsafe { msg_send![&*delegate, windowShouldClose: self] };
if !should {
return;
}
}
}
self.close();
}
}
);
#[must_use = "a GlassWindow releases its NSWindow when dropped; bind it for as long as the window should live"]
#[derive(Debug)]
pub struct GlassWindow {
window: Retained<PaneWindow>,
chrome: Chrome,
}
impl Clone for GlassWindow {
fn clone(&self) -> Self {
Self {
window: self.window.clone(),
chrome: self.chrome,
}
}
}
impl GlassWindow {
pub fn new(mtm: MainThreadMarker, size: NSSize, title: &str) -> Self {
Self::with_chrome(mtm, size, title, Chrome::Titled)
}
pub fn borderless(mtm: MainThreadMarker, size: NSSize, title: &str) -> Self {
Self::with_chrome(mtm, size, title, Chrome::Borderless)
}
fn with_chrome(mtm: MainThreadMarker, size: NSSize, title: &str, chrome: Chrome) -> Self {
let frame = NSRect::new(NSPoint::new(0.0, 0.0), checked_size(size, "GlassWindow"));
let style = chrome.style_mask();
let this = PaneWindow::alloc(mtm).set_ivars(WindowState::default());
let window: Retained<PaneWindow> = unsafe {
msg_send![
super(this),
initWithContentRect: frame,
styleMask: style,
backing: NSBackingStoreType::Buffered,
defer: false,
]
};
window.setOpaque(false);
window.setBackgroundColor(Some(&NSColor::clearColor()));
window.setTitle(&NSString::from_str(title));
match chrome {
Chrome::Borderless => window.setMovableByWindowBackground(true),
Chrome::Titled => {
window.setTitlebarAppearsTransparent(true);
window.setTitleVisibility(NSWindowTitleVisibility::Hidden);
window.setMovableByWindowBackground(false);
}
}
unsafe { window.setReleasedWhenClosed(false) };
window.setTabbingMode(NSWindowTabbingMode::Disallowed);
window.setRestorable(false);
Self { window, chrome }
}
pub fn is_borderless(&self) -> bool {
self.chrome == Chrome::Borderless
}
pub fn center(&self) {
self.window.center();
}
pub fn set_origin(&self, origin: NSPoint) {
self.window.setFrameOrigin(origin);
}
pub fn set_has_shadow(&self, has_shadow: bool) {
if self.window.hasShadow() == has_shadow {
return;
}
self.window.setHasShadow(has_shadow);
self.window.invalidateShadow();
}
pub fn show(&self) {
self.window.makeKeyAndOrderFront(None::<&AnyObject>);
}
pub fn set_close_on_escape(&self, enabled: bool) {
self.window.ivars().close_on_escape.set(enabled);
}
pub fn set_appearance(&self, appearance: Option<&NSAppearance>) {
self.window.setAppearance(appearance);
}
#[must_use]
pub fn appearance(&self) -> Option<Retained<NSAppearance>> {
self.window.appearance()
}
#[must_use]
pub fn effective_appearance(&self) -> Retained<NSAppearance> {
self.window.effectiveAppearance()
}
#[must_use]
pub fn is_dark(&self) -> bool {
crate::is_dark(&self.effective_appearance())
}
pub fn set_content_view(&self, view: &NSView) {
self.window.setContentView(Some(view));
}
#[must_use]
pub fn content_view(&self) -> Option<Retained<NSView>> {
self.window.contentView()
}
#[must_use]
pub fn size(&self) -> NSSize {
self.window
.contentRectForFrameRect(self.window.frame())
.size
}
pub fn set_size(&self, size: NSSize) {
self.window
.setContentSize(checked_size(size, "GlassWindow::set_size"));
}
#[must_use]
pub fn origin(&self) -> NSPoint {
self.window.frame().origin
}
#[must_use]
pub fn has_shadow(&self) -> bool {
self.window.hasShadow()
}
#[must_use]
pub fn close_on_escape(&self) -> bool {
self.window.ivars().close_on_escape.get()
}
#[must_use]
pub fn title(&self) -> String {
self.window.title().to_string()
}
pub fn set_title(&self, title: &str) {
self.window.setTitle(&NSString::from_str(title));
}
#[must_use]
pub fn is_visible(&self) -> bool {
self.window.isVisible()
}
pub fn hide(&self) {
self.window.orderOut(None::<&AnyObject>);
}
pub fn close(&self) {
self.window.close();
}
#[must_use]
pub fn mtm(&self) -> MainThreadMarker {
self.window.mtm()
}
pub fn make_first_responder(&self, view: &NSView) -> bool {
self.window.makeFirstResponder(Some(view))
}
pub fn set_min_size(&self, size: NSSize) {
self.window.setMinSize(size);
}
pub fn set_accepts_mouse_moved(&self, accepts: bool) {
self.window.setAcceptsMouseMovedEvents(accepts);
}
#[must_use]
pub fn ns_window(&self) -> &NSWindow {
&self.window
}
}
#[cfg(feature = "icon-style")]
impl GlassWindow {
pub fn follow_icon_style(&self) {
let weak = objc2::rc::Weak::from_retained(&self.window);
let observer = crate::icon_style::StyleObserver::new(self.mtm(), move |style| {
if let Some(window) = weak.load() {
window.setAppearance(style.appearance().as_deref());
}
});
self.window.ivars().style_observer.replace(Some(observer));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn borderless_mask_is_resizable_alone() {
assert_eq!(
Chrome::Borderless.style_mask(),
NSWindowStyleMask::Resizable
);
}
#[test]
fn titled_mask_carries_the_bits_the_behaviours_need() {
let m = Chrome::Titled.style_mask();
for (bit, why) in [
(
NSWindowStyleMask::Titled,
"the theme frame every behaviour lives in",
),
(
NSWindowStyleMask::FullSizeContentView,
"content under the titlebar",
),
(NSWindowStyleMask::Closable, "the close button"),
(
NSWindowStyleMask::Miniaturizable,
"minimise, which borderless cannot do",
),
(NSWindowStyleMask::Resizable, "zoom and edge resize"),
] {
assert!(m.contains(bit), "titled mask must contain {bit:?}: {why}");
}
}
#[test]
fn titled_is_the_default_chrome() {
assert_eq!(Chrome::default(), Chrome::Titled);
}
}