use core::fmt;
use objc2::rc::Retained;
use objc2::runtime::AnyClass;
use objc2::{MainThreadOnly, msg_send};
use objc2_app_kit::{NSColor, NSGlassEffectView, NSGlassEffectViewStyle, NSView};
use objc2_core_foundation::CGFloat;
use objc2_foundation::{MainThreadMarker, NSObjectProtocol, NSRect};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum GlassStyle {
Regular,
Clear,
}
impl From<GlassStyle> for NSGlassEffectViewStyle {
fn from(s: GlassStyle) -> Self {
match s {
GlassStyle::Regular => NSGlassEffectViewStyle::Regular,
GlassStyle::Clear => NSGlassEffectViewStyle::Clear,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum GlassError {
Unsupported,
}
impl fmt::Display for GlassError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unsupported => f.write_str(
"NSGlassEffectView is not available on this system; \
Liquid Glass requires macOS 26 or later",
),
}
}
}
impl core::error::Error for GlassError {}
#[must_use]
pub fn is_supported() -> bool {
AnyClass::get(c"NSGlassEffectView").is_some()
}
fn checked_frame(frame: NSRect) -> NSRect {
assert!(
!(frame.origin.x.is_nan()
|| frame.origin.y.is_nan()
|| frame.size.width.is_nan()
|| frame.size.height.is_nan()),
"GlassSurface frame must not contain NaN, got {frame:?}"
);
frame
}
#[must_use = "a GlassSurface does nothing until it is installed as a content view; \
dropping it releases the underlying NSGlassEffectView"]
#[derive(Debug)]
pub struct GlassSurface {
view: Retained<NSGlassEffectView>,
}
impl Clone for GlassSurface {
fn clone(&self) -> Self {
Self {
view: self.view.clone(),
}
}
}
impl GlassSurface {
#[must_use = "a GlassSurface does nothing until it is installed as a content view"]
pub fn new(
mtm: MainThreadMarker,
frame: NSRect,
style: GlassStyle,
corner_radius: CGFloat,
) -> Result<Self, GlassError> {
if !is_supported() {
return Err(GlassError::Unsupported);
}
let view =
NSGlassEffectView::initWithFrame(NSGlassEffectView::alloc(mtm), checked_frame(frame));
view.setStyle(style.into());
view.setCornerRadius(corner_radius);
view.setWantsLayer(true);
Ok(Self { view })
}
pub fn set_content_view(&self, content: &NSView) {
self.view.setContentView(Some(content));
}
pub fn set_tint_color(&self, tint: Option<&NSColor>) {
self.view.setTintColor(tint);
}
#[must_use]
pub fn tint_color(&self) -> Option<Retained<NSColor>> {
self.view.tintColor()
}
#[must_use]
pub fn corner_radius(&self) -> CGFloat {
self.view.cornerRadius()
}
pub fn set_corner_radius(&self, radius: CGFloat) {
self.view.setCornerRadius(radius);
}
#[must_use]
pub fn content_view(&self) -> Option<Retained<NSView>> {
self.view.contentView()
}
#[must_use]
pub fn effect_is_interactive(&self) -> Option<bool> {
if !self
.view
.respondsToSelector(objc2::sel!(effectIsInteractive))
{
return None;
}
Some(unsafe { msg_send![&*self.view, effectIsInteractive] })
}
#[must_use = "returns whether the property was actually set; it is absent before macOS 27"]
pub fn set_effect_interactive(&self, interactive: bool) -> bool {
if !self
.view
.respondsToSelector(objc2::sel!(setEffectIsInteractive:))
{
return false;
}
unsafe { msg_send![&*self.view, setEffectIsInteractive: interactive] }
true
}
pub fn set_style(&self, style: GlassStyle) {
self.view.setStyle(style.into());
}
#[must_use]
pub fn style(&self) -> Option<GlassStyle> {
match self.view.style() {
NSGlassEffectViewStyle::Regular => Some(GlassStyle::Regular),
NSGlassEffectViewStyle::Clear => Some(GlassStyle::Clear),
_ => None,
}
}
#[must_use]
pub fn view(&self) -> &NSView {
&self.view
}
}