1use gpui::prelude::*;
11use gpui::{
12 div, px, App, Context, EventEmitter, FocusHandle, Focusable, FontWeight, IntoElement,
13 KeyDownEvent, MouseButton, SharedString, Window, WindowControlArea,
14};
15
16use super::Updater;
17use crate::devtools::Probed;
18use crate::theme::{theme, Size};
19use crate::{Button, Variant};
20
21const TITLEBAR: f32 = 34.0;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum UpdateOutcome {
29 UpToDate,
31 Pending(String),
33 Failed(String),
35}
36
37impl UpdateOutcome {
38 pub fn lines(&self, app: &str, current: &str) -> (String, String) {
41 match self {
42 UpdateOutcome::UpToDate => (
43 "You're up to date".to_string(),
44 format!("{app} {current} is the latest version."),
45 ),
46 UpdateOutcome::Pending(version) => (
47 format!("{app} {version} is on the way"),
48 "It is still building for this platform. Check again shortly.".to_string(),
49 ),
50 UpdateOutcome::Failed(why) => ("Couldn't check for updates".to_string(), why.clone()),
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
57pub enum UpdateNoticeEvent {
58 Dismissed,
61}
62
63pub struct UpdateNotice {
65 updater: Updater,
66 outcome: UpdateOutcome,
67 window_root: bool,
68 focus: FocusHandle,
69}
70
71impl UpdateNotice {
72 pub fn new(updater: Updater, outcome: UpdateOutcome, cx: &mut Context<Self>) -> Self {
73 UpdateNotice {
74 updater,
75 outcome,
76 window_root: false,
77 focus: cx.focus_handle(),
78 }
79 }
80
81 pub fn window_root(mut self, window_root: bool) -> Self {
85 self.window_root = window_root;
86 self
87 }
88
89 pub fn outcome(&self) -> &UpdateOutcome {
91 &self.outcome
92 }
93
94 pub fn dismiss(&mut self, cx: &mut Context<Self>) {
96 cx.emit(UpdateNoticeEvent::Dismissed);
97 }
98
99 fn key_down(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
100 if event.keystroke.key == "escape" {
101 self.dismiss(cx);
102 }
103 }
104}
105
106impl Focusable for UpdateNotice {
107 fn focus_handle(&self, _cx: &App) -> FocusHandle {
108 self.focus.clone()
109 }
110}
111
112impl EventEmitter<UpdateNoticeEvent> for UpdateNotice {}
113
114impl Render for UpdateNotice {
115 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
116 let t = theme(cx);
117 let bg = t.body().hsla();
118 let text = t.text().hsla();
119 let dim = t.dimmed().hsla();
120 let pad = t.spacing(Size::Lg);
121 let gap = t.spacing(Size::Xs);
122 let headline_size = t.font_size(Size::Md);
123 let small = t.font_size(Size::Xs);
124 let (headline, detail) = self
125 .outcome
126 .lines(self.updater.app(), self.updater.version());
127
128 div()
129 .size_full()
130 .flex()
131 .flex_col()
132 .track_focus(&self.focus)
133 .on_key_down(cx.listener(Self::key_down))
134 .bg(bg)
135 .text_color(text)
136 .pt(px(if self.window_root { TITLEBAR } else { pad }))
137 .px(px(pad))
138 .pb(px(pad))
139 .gap(px(gap))
140 .when(self.window_root, |this| this.child(drag_strip()))
141 .child(
142 div()
143 .text_size(px(headline_size))
144 .font_weight(FontWeight::BOLD)
145 .child(SharedString::from(headline)),
146 )
147 .child(
148 div()
149 .text_size(px(small))
150 .text_color(dim)
151 .child(SharedString::from(detail)),
152 )
153 .child(div().flex_1())
154 .child(
155 div().flex().items_center().justify_end().child(
156 Button::new("guise-update-ok", "OK")
157 .variant(Variant::Filled)
158 .on_click(cx.listener(|this, _, _, cx| this.dismiss(cx))),
159 ),
160 )
161 .probe("UpdateNotice")
162 }
163}
164
165fn drag_strip() -> impl IntoElement {
168 let lead = if cfg!(target_os = "macos") { 70.0 } else { 0.0 };
169 div()
170 .absolute()
171 .top_0()
172 .left(px(lead))
173 .right_0()
174 .h(px(TITLEBAR - 6.0))
175 .window_control_area(WindowControlArea::Drag)
176 .on_mouse_down(MouseButton::Left, |_, window, _| window.start_window_move())
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
186 fn every_outcome_says_what_happened_and_why() {
187 for outcome in [
188 UpdateOutcome::UpToDate,
189 UpdateOutcome::Pending("1.32.0".into()),
190 UpdateOutcome::Failed("network unreachable".into()),
191 ] {
192 let (headline, detail) = outcome.lines("Acme", "1.31.0");
193 assert!(!headline.trim().is_empty());
194 assert!(!detail.trim().is_empty());
195 }
196 }
197
198 #[test]
199 fn up_to_date_names_the_version_you_are_on() {
200 let (_, detail) = UpdateOutcome::UpToDate.lines("Acme", "1.31.0");
201 assert!(detail.contains("1.31.0"), "{detail}");
202 assert!(detail.contains("Acme"), "{detail}");
203 }
204
205 #[test]
208 fn a_pending_release_is_not_reported_as_up_to_date() {
209 let (headline, detail) = UpdateOutcome::Pending("1.32.0".into()).lines("Acme", "1.31.0");
210 assert!(headline.contains("1.32.0"), "{headline}");
211 assert!(!headline.contains("up to date"), "{headline}");
212 assert!(detail.contains("building"), "{detail}");
213 }
214
215 #[test]
217 fn a_failed_check_surfaces_its_reason() {
218 let (_, detail) = UpdateOutcome::Failed("network unreachable".into()).lines("Acme", "1.31.0");
219 assert_eq!(detail, "network unreachable");
220 }
221}