Skip to main content

guise/
about.rs

1//! `About` — the small centered card every desktop app owes its users: icon,
2//! name, version, what kind of build this is, and a link home.
3//!
4//! Ported from sinclair, where the interesting part was never the layout. It
5//! was [`BuildKind`]: a build made from some commit that merely carries the
6//! version number is not the release, and saying "Released 2026-08-18" on one
7//! is a small lie that costs a bug report. So the line says what the build
8//! actually is, and the type makes it hard to say otherwise.
9//!
10//! The card is a `RenderOnce` builder, so it goes wherever you want it — its
11//! own window, a modal, a settings page.
12
13use gpui::prelude::*;
14use gpui::{div, px, AnyElement, App, FontWeight, IntoElement, SharedString, Window};
15
16use crate::devtools::Probed;
17use crate::theme::{theme, Size};
18
19/// What kind of build this is, which decides how the dated line reads.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum BuildKind {
22    /// Built from the tag matching its version. Only this may call its date a
23    /// release date.
24    Released,
25    /// Any other build, whatever version number it carries.
26    #[default]
27    Development,
28}
29
30impl BuildKind {
31    /// The dated line for a build of this kind.
32    ///
33    /// Outside a git checkout there is often no date to qualify, so an unknown
34    /// date reads as what the build is rather than as a date it hasn't got.
35    pub fn line(self, date: &str) -> String {
36        match (self, date.trim()) {
37            (BuildKind::Released, "") | (BuildKind::Released, "unknown") => {
38                "Released build".to_string()
39            }
40            (BuildKind::Released, date) => format!("Released {date}"),
41            (BuildKind::Development, "") | (BuildKind::Development, "unknown") => {
42                "Development build".to_string()
43            }
44            (BuildKind::Development, date) => format!("Development build · {date}"),
45        }
46    }
47}
48
49/// The About card.
50#[derive(IntoElement)]
51pub struct About {
52    name: SharedString,
53    version: Option<SharedString>,
54    icon: Option<AnyElement>,
55    build_date: Option<SharedString>,
56    kind: BuildKind,
57    tagline: Option<SharedString>,
58    credits: Option<SharedString>,
59    links: Vec<AnyElement>,
60}
61
62impl About {
63    pub fn new(name: impl Into<SharedString>) -> Self {
64        About {
65            name: name.into(),
66            version: None,
67            icon: None,
68            build_date: None,
69            kind: BuildKind::default(),
70            tagline: None,
71            credits: None,
72            links: Vec::new(),
73        }
74    }
75
76    /// The version string, shown as "Version 1.0.0". Usually
77    /// `env!("CARGO_PKG_VERSION")`.
78    pub fn version(mut self, version: impl Into<SharedString>) -> Self {
79        self.version = Some(version.into());
80        self
81    }
82
83    /// The app icon — an `img(..)`, an [`Icon`](crate::Icon), anything.
84    pub fn icon(mut self, icon: impl IntoElement) -> Self {
85        self.icon = Some(icon.into_any_element());
86        self
87    }
88
89    /// The build date, and whether this build is the release of its version.
90    /// Both come from the build script; see [`BuildKind`].
91    pub fn build(mut self, kind: BuildKind, date: impl Into<SharedString>) -> Self {
92        self.kind = kind;
93        self.build_date = Some(date.into());
94        self
95    }
96
97    /// One line under the name.
98    pub fn tagline(mut self, tagline: impl Into<SharedString>) -> Self {
99        self.tagline = Some(tagline.into());
100        self
101    }
102
103    /// The copyright or acknowledgement line at the foot.
104    pub fn credits(mut self, credits: impl Into<SharedString>) -> Self {
105        self.credits = Some(credits.into());
106        self
107    }
108
109    /// A link, usually an [`Anchor`](crate::Anchor). Several may be added.
110    pub fn link(mut self, link: impl IntoElement) -> Self {
111        self.links.push(link.into_any_element());
112        self
113    }
114}
115
116impl RenderOnce for About {
117    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
118        let t = theme(cx);
119        let body = t.body().hsla();
120        let text = t.text().hsla();
121        let dimmed = t.dimmed().hsla();
122        let font_lg = t.font_size(Size::Lg);
123        let font_sm = t.font_size(Size::Sm);
124        let font_xs = t.font_size(Size::Xs);
125        let gap = t.spacing(Size::Sm);
126
127        let mut card = div()
128            .flex()
129            .flex_col()
130            .items_center()
131            .size_full()
132            .px(px(28.0))
133            .py(px(32.0))
134            .gap(px(gap))
135            .bg(body)
136            .text_color(text)
137            .children(self.icon)
138            .child(
139                div()
140                    .text_size(px(font_lg))
141                    .font_weight(FontWeight::SEMIBOLD)
142                    .child(self.name.clone()),
143            );
144
145        if let Some(tagline) = self.tagline {
146            card = card.child(
147                div()
148                    .text_size(px(font_sm))
149                    .text_color(dimmed)
150                    .child(tagline),
151            );
152        }
153        if let Some(version) = self.version {
154            card = card.child(
155                div()
156                    .text_size(px(font_sm))
157                    .text_color(dimmed)
158                    .child(SharedString::from(format!("Version {version}"))),
159            );
160        }
161        if let Some(date) = self.build_date {
162            card = card.child(
163                div()
164                    .text_size(px(font_xs))
165                    .text_color(dimmed)
166                    .child(SharedString::from(self.kind.line(date.as_ref()))),
167            );
168        }
169        if !self.links.is_empty() {
170            card = card.child(
171                div()
172                    .flex()
173                    .items_center()
174                    .gap(px(gap))
175                    .pt(px(gap))
176                    .children(self.links),
177            );
178        }
179
180        card = card.child(div().flex_1());
181
182        if let Some(credits) = self.credits {
183            card = card.child(
184                div()
185                    .text_size(px(font_xs))
186                    .text_color(dimmed)
187                    .child(credits),
188            );
189        }
190
191        card.probe("About").attr("name", self.name)
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn only_a_released_build_may_call_its_date_a_release_date() {
201        assert_eq!(
202            BuildKind::Released.line("2026-08-18"),
203            "Released 2026-08-18"
204        );
205        assert_eq!(
206            BuildKind::Development.line("2026-08-18"),
207            "Development build · 2026-08-18"
208        );
209    }
210
211    #[test]
212    fn an_unknown_date_reads_as_the_build_kind_alone() {
213        assert_eq!(BuildKind::Development.line("unknown"), "Development build");
214        assert_eq!(BuildKind::Development.line(""), "Development build");
215        assert_eq!(BuildKind::Released.line("unknown"), "Released build");
216    }
217
218    #[test]
219    fn whitespace_is_not_a_date() {
220        assert_eq!(BuildKind::Development.line("   "), "Development build");
221    }
222
223    #[test]
224    fn a_build_is_a_development_build_until_proven_otherwise() {
225        assert_eq!(BuildKind::default(), BuildKind::Development);
226    }
227}