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") => "Released build".to_string(),
38      (BuildKind::Released, date) => format!("Released {date}"),
39      (BuildKind::Development, "") | (BuildKind::Development, "unknown") => {
40        "Development build".to_string()
41      }
42      (BuildKind::Development, date) => format!("Development build · {date}"),
43    }
44  }
45}
46
47/// The About card.
48#[derive(IntoElement)]
49pub struct About {
50  name: SharedString,
51  version: Option<SharedString>,
52  icon: Option<AnyElement>,
53  build_date: Option<SharedString>,
54  kind: BuildKind,
55  tagline: Option<SharedString>,
56  credits: Option<SharedString>,
57  links: Vec<AnyElement>,
58}
59
60impl About {
61  pub fn new(name: impl Into<SharedString>) -> Self {
62    About {
63      name: name.into(),
64      version: None,
65      icon: None,
66      build_date: None,
67      kind: BuildKind::default(),
68      tagline: None,
69      credits: None,
70      links: Vec::new(),
71    }
72  }
73
74  /// The version string, shown as "Version 1.0.0". Usually
75  /// `env!("CARGO_PKG_VERSION")`.
76  pub fn version(mut self, version: impl Into<SharedString>) -> Self {
77    self.version = Some(version.into());
78    self
79  }
80
81  /// The app icon — an `img(..)`, an [`Icon`](crate::Icon), anything.
82  pub fn icon(mut self, icon: impl IntoElement) -> Self {
83    self.icon = Some(icon.into_any_element());
84    self
85  }
86
87  /// The build date, and whether this build is the release of its version.
88  /// Both come from the build script; see [`BuildKind`].
89  pub fn build(mut self, kind: BuildKind, date: impl Into<SharedString>) -> Self {
90    self.kind = kind;
91    self.build_date = Some(date.into());
92    self
93  }
94
95  /// One line under the name.
96  pub fn tagline(mut self, tagline: impl Into<SharedString>) -> Self {
97    self.tagline = Some(tagline.into());
98    self
99  }
100
101  /// The copyright or acknowledgement line at the foot.
102  pub fn credits(mut self, credits: impl Into<SharedString>) -> Self {
103    self.credits = Some(credits.into());
104    self
105  }
106
107  /// A link, usually an [`Anchor`](crate::Anchor). Several may be added.
108  pub fn link(mut self, link: impl IntoElement) -> Self {
109    self.links.push(link.into_any_element());
110    self
111  }
112}
113
114impl RenderOnce for About {
115  fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
116    let t = theme(cx);
117    let body = t.body().hsla();
118    let text = t.text().hsla();
119    let dimmed = t.dimmed().hsla();
120    let font_lg = t.font_size(Size::Lg);
121    let font_sm = t.font_size(Size::Sm);
122    let font_xs = t.font_size(Size::Xs);
123    let gap = t.spacing(Size::Sm);
124
125    let mut card = div()
126      .flex()
127      .flex_col()
128      .items_center()
129      .size_full()
130      .px(px(28.0))
131      .py(px(32.0))
132      .gap(px(gap))
133      .bg(body)
134      .text_color(text)
135      .children(self.icon)
136      .child(
137        div()
138          .text_size(px(font_lg))
139          .font_weight(FontWeight::SEMIBOLD)
140          .child(self.name.clone()),
141      );
142
143    if let Some(tagline) = self.tagline {
144      card = card.child(
145        div()
146          .text_size(px(font_sm))
147          .text_color(dimmed)
148          .child(tagline),
149      );
150    }
151    if let Some(version) = self.version {
152      card = card.child(
153        div()
154          .text_size(px(font_sm))
155          .text_color(dimmed)
156          .child(SharedString::from(format!("Version {version}"))),
157      );
158    }
159    if let Some(date) = self.build_date {
160      card = card.child(
161        div()
162          .text_size(px(font_xs))
163          .text_color(dimmed)
164          .child(SharedString::from(self.kind.line(date.as_ref()))),
165      );
166    }
167    if !self.links.is_empty() {
168      card = card.child(
169        div()
170          .flex()
171          .items_center()
172          .gap(px(gap))
173          .pt(px(gap))
174          .children(self.links),
175      );
176    }
177
178    card = card.child(div().flex_1());
179
180    if let Some(credits) = self.credits {
181      card = card.child(
182        div()
183          .text_size(px(font_xs))
184          .text_color(dimmed)
185          .child(credits),
186      );
187    }
188
189    card.probe("About").attr("name", self.name)
190  }
191}
192
193#[cfg(test)]
194mod tests {
195  use super::*;
196
197  #[test]
198  fn only_a_released_build_may_call_its_date_a_release_date() {
199    assert_eq!(
200      BuildKind::Released.line("2026-08-18"),
201      "Released 2026-08-18"
202    );
203    assert_eq!(
204      BuildKind::Development.line("2026-08-18"),
205      "Development build · 2026-08-18"
206    );
207  }
208
209  #[test]
210  fn an_unknown_date_reads_as_the_build_kind_alone() {
211    assert_eq!(BuildKind::Development.line("unknown"), "Development build");
212    assert_eq!(BuildKind::Development.line(""), "Development build");
213    assert_eq!(BuildKind::Released.line("unknown"), "Released build");
214  }
215
216  #[test]
217  fn whitespace_is_not_a_date() {
218    assert_eq!(BuildKind::Development.line("   "), "Development build");
219  }
220
221  #[test]
222  fn a_build_is_a_development_build_until_proven_otherwise() {
223    assert_eq!(BuildKind::default(), BuildKind::Development);
224  }
225}