Skip to content

Commit 8180421

Browse files
committed
macos notification enhancement and serialized arg passing
1 parent 87d5310 commit 8180421

15 files changed

+377
-163
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/notification-interface/src/lib.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,30 @@ pub enum NotificationEvent {
66
Timeout,
77
}
88

9+
#[derive(
10+
Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize, specta::Type,
11+
)]
12+
pub enum ParticipantStatus {
13+
#[default]
14+
Accepted,
15+
Maybe,
16+
Declined,
17+
}
18+
19+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, specta::Type)]
20+
pub struct Participant {
21+
pub name: Option<String>,
22+
pub email: String,
23+
pub status: ParticipantStatus,
24+
}
25+
26+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, specta::Type)]
27+
pub struct EventDetails {
28+
pub what: String,
29+
pub timezone: Option<String>,
30+
pub location: Option<String>,
31+
}
32+
933
#[derive(Debug, Clone)]
1034
pub struct NotificationContext {
1135
pub key: String,
@@ -20,6 +44,9 @@ pub struct Notification {
2044
pub timeout: Option<std::time::Duration>,
2145
pub event_id: Option<String>,
2246
pub start_time: Option<i64>,
47+
pub participants: Option<Vec<Participant>>,
48+
pub event_details: Option<EventDetails>,
49+
pub action_label: Option<String>,
2350
}
2451

2552
impl Notification {
@@ -36,6 +63,9 @@ pub struct NotificationBuilder {
3663
timeout: Option<std::time::Duration>,
3764
event_id: Option<String>,
3865
start_time: Option<i64>,
66+
participants: Option<Vec<Participant>>,
67+
event_details: Option<EventDetails>,
68+
action_label: Option<String>,
3969
}
4070

4171
impl NotificationBuilder {
@@ -69,6 +99,21 @@ impl NotificationBuilder {
6999
self
70100
}
71101

102+
pub fn participants(mut self, participants: Vec<Participant>) -> Self {
103+
self.participants = Some(participants);
104+
self
105+
}
106+
107+
pub fn event_details(mut self, event_details: EventDetails) -> Self {
108+
self.event_details = Some(event_details);
109+
self
110+
}
111+
112+
pub fn action_label(mut self, action_label: impl Into<String>) -> Self {
113+
self.action_label = Some(action_label.into());
114+
self
115+
}
116+
72117
pub fn build(self) -> Notification {
73118
Notification {
74119
key: self.key,
@@ -77,6 +122,9 @@ impl NotificationBuilder {
77122
timeout: self.timeout,
78123
event_id: self.event_id,
79124
start_time: self.start_time,
125+
participants: self.participants,
126+
event_details: self.event_details,
127+
action_label: self.action_label,
80128
}
81129
}
82130
}

crates/notification-macos/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ swift-rs = { workspace = true, features = ["build"] }
2121
hypr-notification-interface = { workspace = true }
2222
swift-rs = { workspace = true }
2323

24+
serde = { workspace = true, features = ["derive"] }
25+
serde_json = { workspace = true }
26+
2427
[target.'cfg(target_os = "macos")'.dev-dependencies]
2528
objc2 = { workspace = true }
2629
objc2-app-kit = { workspace = true }

crates/notification-macos/examples/test_notification_with_event.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,54 @@ fn main() {
2020
println!("timeout: {}", id);
2121
});
2222

23+
let participants = vec![
24+
Participant {
25+
name: None,
26+
email: "[email protected]".to_string(),
27+
status: ParticipantStatus::Accepted,
28+
},
29+
Participant {
30+
name: Some("John Jeong".to_string()),
31+
email: "[email protected]".to_string(),
32+
status: ParticipantStatus::Accepted,
33+
},
34+
Participant {
35+
name: Some("Yujong Lee".to_string()),
36+
email: "[email protected]".to_string(),
37+
status: ParticipantStatus::Maybe,
38+
},
39+
Participant {
40+
name: Some("Tony Stark".to_string()),
41+
email: "[email protected]".to_string(),
42+
status: ParticipantStatus::Declined,
43+
},
44+
];
45+
46+
let event_details = EventDetails {
47+
what: "Discovery call - Apple <> Hyprnote".to_string(),
48+
timezone: Some("America/Cupertino".to_string()),
49+
location: Some("https://zoom.us/j/123456789".to_string()),
50+
};
51+
52+
let start_time = std::time::SystemTime::now()
53+
.duration_since(std::time::UNIX_EPOCH)
54+
.unwrap()
55+
.as_secs() as i64
56+
+ 120;
57+
2358
let notification = Notification::builder()
2459
.key("test_notification")
2560
.title("Test Notification")
26-
.message("Hover/click should now react")
61+
.message("Meeting starting soon")
2762
.timeout(Duration::from_secs(30))
63+
.participants(participants)
64+
.event_details(event_details)
65+
.action_label("Join Zoom & Start listening")
66+
.start_time(start_time)
2867
.build();
2968

3069
show(&notification);
31-
std::thread::sleep(Duration::from_secs(30));
70+
std::thread::sleep(Duration::from_secs(60));
3271
std::process::exit(0);
3372
});
3473
}

crates/notification-macos/src/lib.rs

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,7 @@ use swift_rs::{Bool, SRString, swift};
66

77
pub use hypr_notification_interface::*;
88

9-
swift!(fn _show_notification(
10-
key: &SRString,
11-
title: &SRString,
12-
message: &SRString,
13-
timeout_seconds: f64,
14-
start_time: i64
15-
) -> Bool);
9+
swift!(fn _show_notification(json_payload: &SRString) -> Bool);
1610

1711
swift!(fn _dismiss_all_notifications() -> Bool);
1812

@@ -93,20 +87,42 @@ pub unsafe extern "C" fn rust_on_notification_timeout(key_ptr: *const c_char) {
9387
}
9488
}
9589

90+
#[derive(serde::Serialize)]
91+
#[serde(rename_all = "camelCase")]
92+
struct NotificationPayload<'a> {
93+
key: &'a str,
94+
title: &'a str,
95+
message: &'a str,
96+
timeout_seconds: f64,
97+
start_time: Option<i64>,
98+
participants: Option<&'a [Participant]>,
99+
event_details: Option<&'a EventDetails>,
100+
action_label: Option<&'a str>,
101+
}
102+
96103
pub fn show(notification: &hypr_notification_interface::Notification) {
104+
let key = notification
105+
.key
106+
.as_deref()
107+
.unwrap_or(notification.title.as_str());
108+
let timeout_seconds = notification.timeout.map(|d| d.as_secs_f64()).unwrap_or(5.0);
109+
110+
let payload = NotificationPayload {
111+
key,
112+
title: &notification.title,
113+
message: &notification.message,
114+
timeout_seconds,
115+
start_time: notification.start_time,
116+
participants: notification.participants.as_deref(),
117+
event_details: notification.event_details.as_ref(),
118+
action_label: notification.action_label.as_deref(),
119+
};
120+
121+
let json = serde_json::to_string(&payload).unwrap();
122+
let json_str = SRString::from(json.as_str());
123+
97124
unsafe {
98-
let key = SRString::from(
99-
notification
100-
.key
101-
.as_deref()
102-
.unwrap_or(notification.title.as_str()),
103-
);
104-
let title = SRString::from(notification.title.as_str());
105-
let message = SRString::from(notification.message.as_str());
106-
let timeout_seconds = notification.timeout.map(|d| d.as_secs_f64()).unwrap_or(5.0);
107-
let start_time = notification.start_time.unwrap_or(0);
108-
109-
_show_notification(&key, &title, &message, timeout_seconds, start_time);
125+
_show_notification(&json_str);
110126
}
111127
}
112128

crates/notification-macos/swift-lib/src/NotificationButtons.swift

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,12 @@ class CloseButton: NSButton {
8787
}
8888
}
8989

90-
class ActionButton: NSButton {
90+
class NotificationButton: NSButton {
9191
weak var notification: NotificationInstance?
9292

93+
private static let normalBg = NSColor(calibratedWhite: 0.95, alpha: 0.9).cgColor
94+
private static let pressedBg = NSColor(calibratedWhite: 0.85, alpha: 0.9).cgColor
95+
9396
override init(frame frameRect: NSRect) {
9497
super.init(frame: frameRect)
9598
setup()
@@ -114,7 +117,7 @@ class ActionButton: NSButton {
114117
}
115118

116119
layer?.cornerRadius = 8
117-
layer?.backgroundColor = NSColor(calibratedWhite: 0.95, alpha: 0.9).cgColor
120+
layer?.backgroundColor = Self.normalBg
118121
layer?.borderColor = NSColor(calibratedWhite: 0.7, alpha: 0.5).cgColor
119122
layer?.borderWidth = 0.5
120123

@@ -131,22 +134,40 @@ class ActionButton: NSButton {
131134
return s
132135
}
133136

134-
override func mouseDown(with event: NSEvent) {
135-
layer?.backgroundColor = NSColor(calibratedWhite: 0.85, alpha: 0.9).cgColor
137+
func animatePress() {
138+
layer?.backgroundColor = Self.pressedBg
136139
DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) {
137-
self.layer?.backgroundColor = NSColor(calibratedWhite: 0.95, alpha: 0.9).cgColor
140+
self.layer?.backgroundColor = Self.normalBg
138141
}
139-
if let notification = notification {
140-
notification.key.withCString { keyPtr in
141-
rustOnNotificationAccept(keyPtr)
142-
}
143-
notification.dismiss()
142+
}
143+
144+
func performAction() {}
145+
146+
override func mouseDown(with event: NSEvent) {
147+
animatePress()
148+
performAction()
149+
}
150+
}
151+
152+
class ActionButton: NotificationButton {
153+
override func performAction() {
154+
guard let notification = notification else { return }
155+
notification.key.withCString { keyPtr in
156+
rustOnNotificationAccept(keyPtr)
144157
}
158+
notification.dismiss()
145159
}
146160
}
147161

148-
class DetailsButton: NSButton {
162+
class DetailsButton: NotificationButton {
163+
override func performAction() {
164+
notification?.toggleExpansion()
165+
}
166+
}
167+
168+
class CollapseButton: NSButton {
149169
weak var notification: NotificationInstance?
170+
var trackingArea: NSTrackingArea?
150171

151172
override init(frame frameRect: NSRect) {
152173
super.init(frame: frameRect)
@@ -161,39 +182,50 @@ class DetailsButton: NSButton {
161182
private func setup() {
162183
wantsLayer = true
163184
isBordered = false
164-
bezelStyle = .rounded
165-
controlSize = .small
166-
font = NSFont.systemFont(ofSize: 12, weight: .medium)
167-
focusRingType = .none
168-
169-
contentTintColor = NSColor(calibratedWhite: 0.1, alpha: 1.0)
170-
if #available(macOS 11.0, *) {
171-
bezelColor = NSColor(calibratedWhite: 0.9, alpha: 1.0)
172-
}
173-
174-
layer?.cornerRadius = 8
175-
layer?.backgroundColor = NSColor(calibratedWhite: 0.95, alpha: 0.9).cgColor
176-
layer?.borderColor = NSColor(calibratedWhite: 0.7, alpha: 0.5).cgColor
177-
layer?.borderWidth = 0.5
178-
179-
layer?.shadowColor = NSColor(calibratedWhite: 0.0, alpha: 0.5).cgColor
180-
layer?.shadowOpacity = 0.2
181-
layer?.shadowRadius = 2
182-
layer?.shadowOffset = CGSize(width: 0, height: 1)
185+
bezelStyle = .regularSquare
186+
imagePosition = .noImage
187+
title = "Show less"
188+
font = NSFont.systemFont(ofSize: 11, weight: .regular)
189+
contentTintColor = NSColor.secondaryLabelColor
190+
layer?.backgroundColor = NSColor.clear.cgColor
183191
}
184192

185193
override var intrinsicContentSize: NSSize {
186194
var s = super.intrinsicContentSize
187-
s.width += 12
188-
s.height = max(24, s.height + 2)
195+
s.height = max(16, s.height)
189196
return s
190197
}
191198

199+
override func updateTrackingAreas() {
200+
super.updateTrackingAreas()
201+
if let area = trackingArea { removeTrackingArea(area) }
202+
let area = NSTrackingArea(
203+
rect: bounds,
204+
options: [.activeAlways, .mouseEnteredAndExited, .inVisibleRect],
205+
owner: self,
206+
userInfo: nil
207+
)
208+
addTrackingArea(area)
209+
trackingArea = area
210+
}
211+
192212
override func mouseDown(with event: NSEvent) {
193-
layer?.backgroundColor = NSColor(calibratedWhite: 0.85, alpha: 0.9).cgColor
213+
contentTintColor = NSColor.tertiaryLabelColor
194214
DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) {
195-
self.layer?.backgroundColor = NSColor(calibratedWhite: 0.95, alpha: 0.9).cgColor
215+
self.contentTintColor = NSColor.secondaryLabelColor
196216
}
197217
notification?.toggleExpansion()
198218
}
219+
220+
override func mouseEntered(with event: NSEvent) {
221+
super.mouseEntered(with: event)
222+
NSCursor.pointingHand.push()
223+
contentTintColor = NSColor.labelColor
224+
}
225+
226+
override func mouseExited(with event: NSEvent) {
227+
super.mouseExited(with: event)
228+
NSCursor.pop()
229+
contentTintColor = NSColor.secondaryLabelColor
230+
}
199231
}

0 commit comments

Comments
 (0)