use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send};
use objc2_app_kit::{
NSBackingStoreType, NSBezierPath, NSColor, NSCompositingOperation, NSFloatingWindowLevel,
NSGraphicsContext, NSScreen, NSView, NSWindow, NSWindowCollectionBehavior, NSWindowStyleMask,
};
use objc2_core_foundation::CGFloat;
use objc2_foundation::{NSPoint, NSRect, NSSize};
#[derive(Clone, PartialEq)]
pub struct BorderParams {
pub color: (f64, f64, f64),
pub opacity: f64,
pub width: f64,
pub radius: f64,
}
#[derive(Clone, PartialEq)]
pub struct DimParams {
pub opacity: f32,
pub color: (f64, f64, f64),
pub cutout: Option<NSRect>,
pub border: Option<BorderParams>,
}
#[derive(Debug, Clone)]
struct DimViewIvars {
opacity: f32,
dim_r: f64,
dim_g: f64,
dim_b: f64,
cutout_x: f64,
cutout_y: f64,
cutout_w: f64,
cutout_h: f64,
has_cutout: bool,
has_border: bool,
border_r: f64,
border_g: f64,
border_b: f64,
border_opacity: f64,
border_width: f64,
border_radius: f64,
}
define_class!(
#[unsafe(super(NSView))]
#[thread_kind = MainThreadOnly]
#[name = "PaneruDimView"]
#[ivars = DimViewIvars]
#[derive(Debug)]
struct DimView;
impl DimView {
#[unsafe(method(drawRect:))]
fn draw_rect(&self, _dirty_rect: NSRect) {
let ivars = self.ivars();
let bounds = self.bounds();
let dim_color = NSColor::colorWithSRGBRed_green_blue_alpha(
ivars.dim_r as CGFloat,
ivars.dim_g as CGFloat,
ivars.dim_b as CGFloat,
CGFloat::from(ivars.opacity),
);
dim_color.setFill();
NSBezierPath::fillRect(bounds);
if ivars.has_cutout {
let half = if ivars.has_border { ivars.border_width / 2.0 } else { 0.0 };
let radius = ivars.border_radius as CGFloat;
let cutout = NSRect::new(
NSPoint::new(ivars.cutout_x - half, ivars.cutout_y - half),
NSSize::new(ivars.cutout_w + ivars.border_width, ivars.cutout_h + ivars.border_width),
);
if let Some(ctx) = NSGraphicsContext::currentContext() {
ctx.setCompositingOperation(NSCompositingOperation::Clear);
let hole = NSBezierPath::bezierPathWithRoundedRect_xRadius_yRadius(
cutout, radius, radius,
);
hole.fill();
ctx.setCompositingOperation(NSCompositingOperation::SourceOver);
}
if ivars.has_border {
let border_rect = NSRect::new(
NSPoint::new(ivars.cutout_x, ivars.cutout_y),
NSSize::new(ivars.cutout_w, ivars.cutout_h),
);
let path = NSBezierPath::bezierPathWithRoundedRect_xRadius_yRadius(
border_rect, radius, radius,
);
path.setLineWidth(ivars.border_width as CGFloat);
let border_color = NSColor::colorWithSRGBRed_green_blue_alpha(
ivars.border_r as CGFloat,
ivars.border_g as CGFloat,
ivars.border_b as CGFloat,
ivars.border_opacity as CGFloat,
);
border_color.setStroke();
path.stroke();
}
}
}
#[unsafe(method(isFlipped))]
fn is_flipped(&self) -> bool {
true
}
}
);
impl DimView {
fn new(mtm: MainThreadMarker, frame: NSRect, params: &DimParams) -> Retained<Self> {
let (has_cutout, cx, cy, cw, ch) = params.cutout.map_or((false, 0.0, 0.0, 0.0, 0.0), |r| {
(true, r.origin.x, r.origin.y, r.size.width, r.size.height)
});
let (has_border, br, bg, bb, bo, bw, brad) =
params
.border
.as_ref()
.map_or((false, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), |b| {
(
true, b.color.0, b.color.1, b.color.2, b.opacity, b.width, b.radius,
)
});
let this = Self::alloc(mtm).set_ivars(DimViewIvars {
opacity: params.opacity,
dim_r: params.color.0,
dim_g: params.color.1,
dim_b: params.color.2,
cutout_x: cx,
cutout_y: cy,
cutout_w: cw,
cutout_h: ch,
has_cutout,
has_border,
border_r: br,
border_g: bg,
border_b: bb,
border_opacity: bo,
border_width: bw,
border_radius: brad,
});
unsafe { msg_send![super(this), initWithFrame: frame] }
}
}
fn cg_abs_to_cocoa(frame: NSRect, primary_screen_height: f64) -> NSRect {
let cocoa_y = primary_screen_height - frame.origin.y - frame.size.height;
NSRect::new(NSPoint::new(frame.origin.x, cocoa_y), frame.size)
}
fn primary_screen_height(mtm: MainThreadMarker) -> f64 {
let screens = NSScreen::screens(mtm);
if screens.is_empty() {
return 0.0;
}
screens.objectAtIndex(0).frame().size.height
}
fn full_screen_rect(mtm: MainThreadMarker) -> NSRect {
let screens = NSScreen::screens(mtm);
if screens.is_empty() {
return NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0));
}
let mut min_x = f64::MAX;
let mut min_y = f64::MAX;
let mut max_x = f64::MIN;
let mut max_y = f64::MIN;
for screen in &screens {
let f = screen.frame();
min_x = min_x.min(f.origin.x);
min_y = min_y.min(f.origin.y);
max_x = max_x.max(f.origin.x + f.size.width);
max_y = max_y.max(f.origin.y + f.size.height);
}
NSRect::new(
NSPoint::new(min_x, min_y),
NSSize::new(max_x - min_x, max_y - min_y),
)
}
fn make_overlay_window(mtm: MainThreadMarker, cocoa_frame: NSRect) -> Retained<NSWindow> {
let window = unsafe {
NSWindow::initWithContentRect_styleMask_backing_defer(
NSWindow::alloc(mtm),
cocoa_frame,
NSWindowStyleMask::Borderless,
NSBackingStoreType::Buffered,
false,
)
};
window.setOpaque(false);
window.setBackgroundColor(Some(&NSColor::clearColor()));
window.setIgnoresMouseEvents(true);
window.setHasShadow(false);
window.setLevel(NSFloatingWindowLevel);
window.setCollectionBehavior(
NSWindowCollectionBehavior::Transient
| NSWindowCollectionBehavior::IgnoresCycle
| NSWindowCollectionBehavior::CanJoinAllSpaces
| NSWindowCollectionBehavior::Stationary
| NSWindowCollectionBehavior::FullScreenNone,
);
window
}
pub struct OverlayManager {
mtm: MainThreadMarker,
overlay: Option<(Retained<NSWindow>, DimParams)>,
hidden: bool,
}
impl OverlayManager {
pub fn new(mtm: MainThreadMarker) -> Self {
Self {
mtm,
overlay: None,
hidden: false,
}
}
pub fn update(
&mut self,
dim_opacity: f32,
dim_color: (f64, f64, f64),
focused_abs_cg: Option<NSRect>,
border: Option<&BorderParams>,
) {
let screen_h = primary_screen_height(self.mtm);
let screen_rect = full_screen_rect(self.mtm);
let cutout_local = focused_abs_cg.map(|cg_frame| {
let cocoa = cg_abs_to_cocoa(cg_frame, screen_h);
NSRect::new(
NSPoint::new(
cocoa.origin.x - screen_rect.origin.x,
(screen_rect.origin.y + screen_rect.size.height)
- (cocoa.origin.y + cocoa.size.height),
),
cocoa.size,
)
});
let params = DimParams {
opacity: dim_opacity,
color: dim_color,
cutout: cutout_local,
border: border.cloned(),
};
if let Some((window, stored)) = &mut self.overlay {
if *stored != params {
let view = DimView::new(self.mtm, screen_rect, ¶ms);
window.setContentView(Some(&view));
window.setFrame_display(screen_rect, true);
}
if self.hidden {
window.orderFront(None::<&AnyObject>);
self.hidden = false;
}
*stored = params;
} else {
let window = make_overlay_window(self.mtm, screen_rect);
let view = DimView::new(self.mtm, screen_rect, ¶ms);
window.setContentView(Some(&view));
window.orderFront(None::<&AnyObject>);
self.overlay = Some((window, params));
self.hidden = false;
}
}
pub fn remove_all(&mut self) {
if let Some((window, _)) = self.overlay.take() {
window.orderOut(None::<&AnyObject>);
}
self.hidden = false;
}
pub fn hide_all(&mut self) {
if self.hidden {
return;
}
if let Some((window, _)) = &self.overlay {
window.orderOut(None::<&AnyObject>);
}
self.hidden = true;
}
}