use gpui::prelude::*;
use gpui::{
div, px, App, Context, EventEmitter, FocusHandle, Focusable, FontWeight, IntoElement,
KeyDownEvent, MouseButton, SharedString, Window, WindowControlArea,
};
use super::Updater;
use crate::devtools::Probed;
use crate::theme::{theme, Size};
use crate::{Button, Variant};
const TITLEBAR: f32 = 34.0;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpdateOutcome {
UpToDate,
Pending(String),
Failed(String),
}
impl UpdateOutcome {
pub fn lines(&self, app: &str, current: &str) -> (String, String) {
match self {
UpdateOutcome::UpToDate => (
"You're up to date".to_string(),
format!("{app} {current} is the latest version."),
),
UpdateOutcome::Pending(version) => (
format!("{app} {version} is on the way"),
"It is still building for this platform. Check again shortly.".to_string(),
),
UpdateOutcome::Failed(why) => ("Couldn't check for updates".to_string(), why.clone()),
}
}
}
#[derive(Debug, Clone)]
pub enum UpdateNoticeEvent {
Dismissed,
}
pub struct UpdateNotice {
updater: Updater,
outcome: UpdateOutcome,
window_root: bool,
focus: FocusHandle,
}
impl UpdateNotice {
pub fn new(updater: Updater, outcome: UpdateOutcome, cx: &mut Context<Self>) -> Self {
UpdateNotice {
updater,
outcome,
window_root: false,
focus: cx.focus_handle(),
}
}
pub fn window_root(mut self, window_root: bool) -> Self {
self.window_root = window_root;
self
}
pub fn outcome(&self) -> &UpdateOutcome {
&self.outcome
}
pub fn dismiss(&mut self, cx: &mut Context<Self>) {
cx.emit(UpdateNoticeEvent::Dismissed);
}
fn key_down(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
if event.keystroke.key == "escape" {
self.dismiss(cx);
}
}
}
impl Focusable for UpdateNotice {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus.clone()
}
}
impl EventEmitter<UpdateNoticeEvent> for UpdateNotice {}
impl Render for UpdateNotice {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let t = theme(cx);
let bg = t.body().hsla();
let text = t.text().hsla();
let dim = t.dimmed().hsla();
let pad = t.spacing(Size::Lg);
let gap = t.spacing(Size::Xs);
let headline_size = t.font_size(Size::Md);
let small = t.font_size(Size::Xs);
let (headline, detail) = self
.outcome
.lines(self.updater.app(), self.updater.version());
div()
.size_full()
.flex()
.flex_col()
.track_focus(&self.focus)
.on_key_down(cx.listener(Self::key_down))
.bg(bg)
.text_color(text)
.pt(px(if self.window_root { TITLEBAR } else { pad }))
.px(px(pad))
.pb(px(pad))
.gap(px(gap))
.when(self.window_root, |this| this.child(drag_strip()))
.child(
div()
.text_size(px(headline_size))
.font_weight(FontWeight::BOLD)
.child(SharedString::from(headline)),
)
.child(
div()
.text_size(px(small))
.text_color(dim)
.child(SharedString::from(detail)),
)
.child(div().flex_1())
.child(
div().flex().items_center().justify_end().child(
Button::new("guise-update-ok", "OK")
.variant(Variant::Filled)
.on_click(cx.listener(|this, _, _, cx| this.dismiss(cx))),
),
)
.probe("UpdateNotice")
}
}
fn drag_strip() -> impl IntoElement {
let lead = if cfg!(target_os = "macos") { 70.0 } else { 0.0 };
div()
.absolute()
.top_0()
.left(px(lead))
.right_0()
.h(px(TITLEBAR - 6.0))
.window_control_area(WindowControlArea::Drag)
.on_mouse_down(MouseButton::Left, |_, window, _| window.start_window_move())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_outcome_says_what_happened_and_why() {
for outcome in [
UpdateOutcome::UpToDate,
UpdateOutcome::Pending("1.32.0".into()),
UpdateOutcome::Failed("network unreachable".into()),
] {
let (headline, detail) = outcome.lines("Acme", "1.31.0");
assert!(!headline.trim().is_empty());
assert!(!detail.trim().is_empty());
}
}
#[test]
fn up_to_date_names_the_version_you_are_on() {
let (_, detail) = UpdateOutcome::UpToDate.lines("Acme", "1.31.0");
assert!(detail.contains("1.31.0"), "{detail}");
assert!(detail.contains("Acme"), "{detail}");
}
#[test]
fn a_pending_release_is_not_reported_as_up_to_date() {
let (headline, detail) = UpdateOutcome::Pending("1.32.0".into()).lines("Acme", "1.31.0");
assert!(headline.contains("1.32.0"), "{headline}");
assert!(!headline.contains("up to date"), "{headline}");
assert!(detail.contains("building"), "{detail}");
}
#[test]
fn a_failed_check_surfaces_its_reason() {
let (_, detail) = UpdateOutcome::Failed("network unreachable".into()).lines("Acme", "1.31.0");
assert_eq!(detail, "network unreachable");
}
}