|
| 1 | +use std::cell::{Cell, RefCell}; |
| 2 | + |
| 3 | +use objc2::rc::Retained; |
| 4 | +use objc2::{define_class, msg_send, sel, AnyThread, DefinedClass, MainThreadOnly}; |
| 5 | +use objc2_app_kit::{NSButton, NSColor, NSEvent, NSTrackingArea, NSTrackingAreaOptions}; |
| 6 | +use objc2_foundation::{MainThreadMarker, NSObjectProtocol, NSString}; |
| 7 | + |
| 8 | +use crate::komorebi::CycleDirection; |
| 9 | + |
| 10 | +#[derive(Debug)] |
| 11 | +pub struct LayoutButtonIvars { |
| 12 | + workspace: crate::komorebi::Workspace, |
| 13 | + is_hovering: Cell<bool>, |
| 14 | + tracking_area: RefCell<Option<Retained<NSTrackingArea>>>, |
| 15 | +} |
| 16 | + |
| 17 | +impl LayoutButtonIvars { |
| 18 | + fn new(workspace: crate::komorebi::Workspace) -> Self { |
| 19 | + Self { |
| 20 | + workspace, |
| 21 | + is_hovering: Cell::new(false), |
| 22 | + tracking_area: RefCell::new(None), |
| 23 | + } |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +define_class!( |
| 28 | + /// A Custom button representing a workspace in the status bar. |
| 29 | + /// Displays an indicator for focused and non-empty workspaces. |
| 30 | + #[unsafe(super = NSButton)] |
| 31 | + #[thread_kind = MainThreadOnly] |
| 32 | + #[ivars = LayoutButtonIvars] |
| 33 | + #[derive(Debug)] |
| 34 | + pub struct LayoutButton; |
| 35 | + |
| 36 | + unsafe impl NSObjectProtocol for LayoutButton {} |
| 37 | + |
| 38 | + impl LayoutButton { |
| 39 | + #[unsafe(method(buttonClicked:))] |
| 40 | + fn button_clicked(&self, _sender: &NSButton) { |
| 41 | + crate::komorebi::cycle_layout(CycleDirection::Next); |
| 42 | + } |
| 43 | + |
| 44 | + #[unsafe(method(mouseEntered:))] |
| 45 | + fn mouse_entered(&self, _event: &NSEvent) { |
| 46 | + self.ivars().is_hovering.set(true); |
| 47 | + let layer = self.layer().unwrap(); |
| 48 | + let hover_color = NSColor::colorWithWhite_alpha(1.0, 0.1).CGColor(); |
| 49 | + let _: () = unsafe { msg_send![&layer, setBackgroundColor: &*hover_color] }; |
| 50 | + } |
| 51 | + |
| 52 | + #[unsafe(method(mouseExited:))] |
| 53 | + fn mouse_exited(&self, _event: &NSEvent) { |
| 54 | + if self.ivars().workspace.focused { |
| 55 | + return; |
| 56 | + } |
| 57 | + |
| 58 | + self.ivars().is_hovering.set(false); |
| 59 | + let layer = self.layer().unwrap(); |
| 60 | + let clear_color = NSColor::clearColor().CGColor(); |
| 61 | + let _: () = unsafe { msg_send![&layer, setBackgroundColor: &*clear_color] }; |
| 62 | + } |
| 63 | + |
| 64 | + #[unsafe(method(updateTrackingAreas))] |
| 65 | + fn update_tracking_areas(&self) { |
| 66 | + // Remove old tracking area if it exists |
| 67 | + if let Some(old_tracking_area) = self.ivars().tracking_area.borrow().as_ref() { |
| 68 | + self.removeTrackingArea(old_tracking_area); |
| 69 | + } |
| 70 | + |
| 71 | + // Create new tracking area |
| 72 | + let options = NSTrackingAreaOptions::MouseEnteredAndExited |
| 73 | + | NSTrackingAreaOptions::ActiveAlways; |
| 74 | + let tracking_area = unsafe { |
| 75 | + NSTrackingArea::initWithRect_options_owner_userInfo( |
| 76 | + NSTrackingArea::alloc(), |
| 77 | + self.bounds(), |
| 78 | + options, |
| 79 | + Some(self), |
| 80 | + None, |
| 81 | + ) |
| 82 | + }; |
| 83 | + self.addTrackingArea(&tracking_area); |
| 84 | + |
| 85 | + // Store tracking area |
| 86 | + *self.ivars().tracking_area.borrow_mut() = Some(tracking_area); |
| 87 | + } |
| 88 | + } |
| 89 | +); |
| 90 | + |
| 91 | +impl LayoutButton { |
| 92 | + pub fn new(mtm: MainThreadMarker, workspace: &crate::komorebi::Workspace) -> Retained<Self> { |
| 93 | + // Create button |
| 94 | + let this = Self::alloc(mtm).set_ivars(LayoutButtonIvars::new(workspace.clone())); |
| 95 | + // SAFETY: The signature of `NSButton`'s `init` method is correct. |
| 96 | + let this: Retained<Self> = unsafe { msg_send![super(this), init] }; |
| 97 | + |
| 98 | + // Configure button |
| 99 | + this.setTitle(&NSString::from_str(&workspace.layout)); |
| 100 | + |
| 101 | + // Set up action handler |
| 102 | + unsafe { this.setTarget(Some(&this)) }; |
| 103 | + unsafe { this.setAction(Some(sel!(buttonClicked:))) }; |
| 104 | + |
| 105 | + // Make button transparent |
| 106 | + this.setBordered(false); |
| 107 | + this.setWantsLayer(true); |
| 108 | + let layer = this.layer().unwrap(); |
| 109 | + let _: () = unsafe { msg_send![&layer, setCornerRadius: 4.0] }; |
| 110 | + |
| 111 | + // Add size constraints for padding (since bezel is removed) |
| 112 | + let height_constraint = this |
| 113 | + .heightAnchor() |
| 114 | + .constraintGreaterThanOrEqualToConstant(24.0); |
| 115 | + height_constraint.setActive(true); |
| 116 | + |
| 117 | + // Set background color based on focus state |
| 118 | + let bg_color = NSColor::clearColor().CGColor(); |
| 119 | + let _: () = unsafe { msg_send![&layer, setBackgroundColor: &*bg_color] }; |
| 120 | + |
| 121 | + this |
| 122 | + } |
| 123 | +} |
0 commit comments