1use gpui::{
2 App, Context, EventEmitter, IntoElement, PlatformDisplay, Size, Window,
3 WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
4 linear_color_stop, linear_gradient, point,
5};
6use release_channel::ReleaseChannel;
7use std::rc::Rc;
8use ui::{Render, prelude::*};
9
10pub struct AgentNotification {
11 title: SharedString,
12 caption: SharedString,
13 icon: IconName,
14 project_name: Option<SharedString>,
15}
16
17impl AgentNotification {
18 pub fn new(
19 title: impl Into<SharedString>,
20 caption: impl Into<SharedString>,
21 icon: IconName,
22 project_name: Option<impl Into<SharedString>>,
23 ) -> Self {
24 Self {
25 title: title.into(),
26 caption: caption.into(),
27 icon,
28 project_name: project_name.map(|name| name.into()),
29 }
30 }
31
32 pub fn window_options(screen: Rc<dyn PlatformDisplay>, cx: &App) -> WindowOptions {
33 let size = Size {
34 width: px(450.),
35 height: px(72.),
36 };
37
38 let notification_margin_width = px(16.);
39 let notification_margin_height = px(-48.);
40
41 let bounds = gpui::Bounds::<Pixels> {
42 origin: screen.bounds().top_right()
43 - point(
44 size.width + notification_margin_width,
45 notification_margin_height,
46 ),
47 size,
48 };
49
50 let app_id = ReleaseChannel::global(cx).app_id();
51
52 WindowOptions {
53 window_bounds: Some(WindowBounds::Windowed(bounds)),
54 titlebar: None,
55 focus: false,
56 show: true,
57 kind: WindowKind::PopUp,
58 is_movable: false,
59 display_id: Some(screen.id()),
60 window_background: WindowBackgroundAppearance::Transparent,
61 app_id: Some(app_id.to_owned()),
62 window_min_size: None,
63 window_decorations: Some(WindowDecorations::Client),
64 tabbing_identifier: None,
65 ..Default::default()
66 }
67 }
68}
69
70pub enum AgentNotificationEvent {
71 Accepted,
72 Dismissed,
73}
74
75impl EventEmitter<AgentNotificationEvent> for AgentNotification {}
76
77impl AgentNotification {
78 pub fn accept(&mut self, cx: &mut Context<Self>) {
79 cx.emit(AgentNotificationEvent::Accepted);
80 }
81
82 pub fn dismiss(&mut self, cx: &mut Context<Self>) {
83 cx.emit(AgentNotificationEvent::Dismissed);
84 }
85}
86
87impl Render for AgentNotification {
88 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
89 let ui_font = theme_settings::setup_ui_font(window, cx);
90 let line_height = window.line_height();
91
92 let bg = cx.theme().colors().elevated_surface_background;
93 let gradient_overflow = || {
94 div()
95 .h_full()
96 .absolute()
97 .w_8()
98 .bottom_0()
99 .right_0()
100 .bg(linear_gradient(
101 90.,
102 linear_color_stop(bg, 1.),
103 linear_color_stop(bg.opacity(0.2), 0.),
104 ))
105 };
106
107 h_flex()
108 .id("agent-notification")
109 .size_full()
110 .p_3()
111 .gap_4()
112 .justify_between()
113 .elevation_3(cx)
114 .text_ui(cx)
115 .font(ui_font)
116 .border_color(cx.theme().colors().border)
117 .rounded_xl()
118 .child(
119 h_flex()
120 .items_start()
121 .gap_2()
122 .flex_1()
123 .child(
124 h_flex().h(line_height).justify_center().child(
125 Icon::new(self.icon)
126 .color(Color::Muted)
127 .size(IconSize::Small),
128 ),
129 )
130 .child(
131 v_flex()
132 .flex_1()
133 .max_w(px(300.))
134 .child(
135 div()
136 .relative()
137 .text_size(px(14.))
138 .text_color(cx.theme().colors().text)
139 .truncate()
140 .child(self.title.clone())
141 .child(gradient_overflow()),
142 )
143 .child(
144 h_flex()
145 .relative()
146 .gap_1p5()
147 .text_size(px(12.))
148 .text_color(cx.theme().colors().text_muted)
149 .truncate()
150 .when_some(
151 self.project_name.clone(),
152 |description, project_name| {
153 description.child(
154 h_flex()
155 .gap_1p5()
156 .child(
157 div()
158 .max_w_16()
159 .truncate()
160 .child(project_name),
161 )
162 .child(
163 div().size(px(3.)).rounded_full().bg(cx
164 .theme()
165 .colors()
166 .text
167 .opacity(0.5)),
168 ),
169 )
170 },
171 )
172 .child(self.caption.clone())
173 .child(gradient_overflow()),
174 ),
175 ),
176 )
177 .child(
178 v_flex()
179 .gap_1()
180 .items_center()
181 .child(
182 Button::new("open", "View Panel")
183 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
184 .full_width()
185 .on_click({
186 cx.listener(move |this, _event, _, cx| {
187 this.accept(cx);
188 })
189 }),
190 )
191 .child(Button::new("dismiss", "Dismiss").full_width().on_click({
192 cx.listener(move |this, _event, _, cx| {
193 this.dismiss(cx);
194 })
195 })),
196 )
197 }
198}