"""Core glass effect implementation for macOS Liquid Glass."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from ._bridge import get_nsview_from_widget
from ._platform import HAS_GLASS_EFFECT, IS_MACOS, platform_guard
from ._types import BlendingMode, GlassOptions
if TYPE_CHECKING:
from ._compat import QtWidgets
__all__ = ["apply_glass_to_widget", "apply_glass_to_window", "remove_glass_effect"]
_effect_registry: dict[int, tuple[Any, Any]] = {}
_next_effect_id: int = 0
_NS_WINDOW_BELOW: int = -1
_NS_VIEW_WIDTH_SIZABLE: int = 2
_NS_VIEW_HEIGHT_SIZABLE: int = 16
_NS_VIEW_MAX_X_MARGIN: int = 4
_NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK: int = 1 << 15
def _create_glass_view(frame: Any, options: GlassOptions) -> Any | None: # noqa: ANN401
"""
Create an NSGlassEffectView or NSVisualEffectView.
Attempts to use NSGlassEffectView on macOS 26+, falling back to
NSVisualEffectView on older versions.
Args:
frame: NSRect for the view's frame.
options: Glass effect configuration.
Returns:
The created view, or None on failure.
"""
import objc # noqa: PLC0415 # ty: ignore
if HAS_GLASS_EFFECT:
try:
glass_cls = objc.lookUpClass("NSGlassEffectView") # ty: ignore
glass = glass_cls.alloc().initWithFrame_(frame) # ty: ignore
if options.corner_radius > 0:
glass.setCornerRadius_(options.corner_radius) # ty: ignore
except objc.nosuchclass_error: # ty: ignore
pass
else:
return glass
try:
from AppKit import ( # noqa: PLC0415 # ty: ignore
NSVisualEffectBlendingModeBehindWindow,
NSVisualEffectBlendingModeWithinWindow,
NSVisualEffectStateActive,
NSVisualEffectView,
)
glass = NSVisualEffectView.alloc().initWithFrame_(frame) # ty: ignore
glass.setMaterial_(options.material.value) # ty: ignore
blending = (
NSVisualEffectBlendingModeBehindWindow # ty: ignore
if options.blending_mode == BlendingMode.BEHIND_WINDOW
else NSVisualEffectBlendingModeWithinWindow # ty: ignore
)
glass.setBlendingMode_(blending) # ty: ignore
glass.setState_(NSVisualEffectStateActive) # ty: ignore
except Exception: # noqa: BLE001
return None
else:
return glass
def _configure_window_for_glass(ns_window: Any) -> None: # noqa: ANN401
"""Configure NSWindow properties for full window glass effect rendering."""
from AppKit import NSColor # noqa: PLC0415 # ty: ignore
ns_window.setOpaque_(False) # ty: ignore
ns_window.setBackgroundColor_(NSColor.clearColor()) # ty: ignore
current_mask = ns_window.styleMask() # ty: ignore
ns_window.setStyleMask_(current_mask | _NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK) # ty: ignore
ns_window.setTitlebarAppearsTransparent_(True) # ty: ignore
def _configure_titlebar_for_glass(ns_window: Any) -> None: # noqa: ANN401
"""Configure only titlebar for widget-level glass (no window transparency)."""
current_mask = ns_window.styleMask() # ty: ignore
ns_window.setStyleMask_(current_mask | _NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK) # ty: ignore
ns_window.setTitlebarAppearsTransparent_(True) # ty: ignore
[docs]
@platform_guard
def apply_glass_to_window(
window: QtWidgets.QWidget, options: GlassOptions | None = None
) -> int | None:
"""
Apply glass effect to an entire window.
Creates an NSGlassEffectView (macOS 26+) or NSVisualEffectView (fallback)
that fills the window's content area behind all Qt content.
Uses one of three strategies based on window configuration:
1. Sibling Injection: If root view has a superview, adds glass as sibling
2. Content Swap: For frameless windows, creates a container and swaps
3. Child Fallback: Adds glass inside root view at bottom of z-order
Args:
window: A top-level QWidget (typically QMainWindow).
options: Glass effect configuration. Uses defaults if None.
Returns:
An effect ID for later removal, or None if the effect could not
be applied.
Note:
The window should be shown before calling this function.
"""
if options is None:
options = GlassOptions.window()
root_view = get_nsview_from_widget(window)
if root_view is None:
return None
ns_window = root_view.window() # ty: ignore
if ns_window is None:
return None
from AppKit import NSView # noqa: PLC0415 # ty: ignore
from Foundation import NSMakeRect # noqa: PLC0415 # ty: ignore
superview = root_view.superview() # ty: ignore
content_view = ns_window.contentView() # ty: ignore
container: Any = None
performed_swap = False
if superview is not None:
container = superview
elif root_view == content_view:
frame = root_view.frame() # ty: ignore
new_container = NSView.alloc().initWithFrame_(frame) # ty: ignore
new_container.setAutoresizingMask_( # ty: ignore
_NS_VIEW_WIDTH_SIZABLE | _NS_VIEW_HEIGHT_SIZABLE
)
new_container.setWantsLayer_(True) # ty: ignore
ns_window.setContentView_(new_container) # ty: ignore
root_view.setFrame_(new_container.bounds()) # ty: ignore
root_view.setAutoresizingMask_( # ty: ignore
_NS_VIEW_WIDTH_SIZABLE | _NS_VIEW_HEIGHT_SIZABLE
)
new_container.addSubview_(root_view) # ty: ignore
container = root_view.superview() # ty: ignore
performed_swap = True
else:
container = root_view
_configure_window_for_glass(ns_window)
if container == root_view.superview(): # ty: ignore
frame_rect = root_view.frame() # ty: ignore
else:
frame_rect = root_view.bounds() # ty: ignore
if performed_swap:
frame_rect = container.bounds() # ty: ignore
pad_left, pad_top, pad_right, pad_bottom = options.padding
frame_rect = NSMakeRect( # ty: ignore
frame_rect.origin.x + pad_left, # ty: ignore
frame_rect.origin.y + pad_bottom, # ty: ignore
frame_rect.size.width - pad_left - pad_right, # ty: ignore
frame_rect.size.height - pad_top - pad_bottom, # ty: ignore
)
glass = _create_glass_view(frame_rect, options)
if glass is None:
return None
glass.setAutoresizingMask_(_NS_VIEW_WIDTH_SIZABLE | _NS_VIEW_HEIGHT_SIZABLE) # ty: ignore
if container == root_view.superview(): # ty: ignore
container.addSubview_positioned_relativeTo_( # ty: ignore
glass, _NS_WINDOW_BELOW, root_view
)
else:
container.addSubview_positioned_relativeTo_( # ty: ignore
glass, _NS_WINDOW_BELOW, None
)
global _next_effect_id # noqa: PLW0603
effect_id = _next_effect_id
_next_effect_id += 1
_effect_registry[effect_id] = (glass, container)
window._glass_view = glass # type: ignore[attr-defined] # noqa: SLF001 # ty: ignore
return effect_id
[docs]
def remove_glass_effect(effect_id: int) -> bool:
"""
Remove a previously applied glass effect.
Args:
effect_id: The identifier returned by apply_glass_to_window or
apply_glass_to_widget.
Returns:
True if the effect was successfully removed, False if the effect
ID was not found.
"""
if effect_id not in _effect_registry:
return False
glass_view, _ = _effect_registry.pop(effect_id)
if IS_MACOS:
try:
glass_view.removeFromSuperview() # ty: ignore
except Exception: # noqa: BLE001
return False
return True