zed: Open About window as standalone window (#52523)

Dario and Danilo Leal created

## Context

Replaces the native system prompt used for "About Zed" with a custom
standalone GPUI window which can open without the need for opening a Zed
editor window of none are currently open.

Closes #49157

## How to Review

Single file change in `crates/zed/src/zed.rs` — the `about` function is
replaced by `open_about_window`. The new window deduplicates itself,
matches the old layout (title, version info, Ok + Copy buttons), and
closes on either button or Escape.

## Screenshots

### Old Behavior

Opening the about window when no other Zed window is open will open a
new empty zed window and display the about window on top:
<img width="1710" height="1072" alt="image"
src="https://github.com/user-attachments/assets/10a22027-4aa8-4cc9-9d91-368d76bb6a91"
/>

### New Behavior

If there is no open Zed window we just open an instance of the new About
window:
<img width="1710" height="1076" alt="image"
src="https://github.com/user-attachments/assets/e55c92dc-18db-4556-83bd-1a78db754eff"
/>


If there is at least one open Zed window, we display the About window on
top of it:
<img width="1710" height="1075" alt="image"
src="https://github.com/user-attachments/assets/a653e402-a107-4b50-8d09-7d50f537b36c"
/>

In bright mode:
<img width="1710" height="1071" alt="image"
src="https://github.com/user-attachments/assets/c72f8856-7152-47a6-a630-0875372a270f"
/>

Additionally, the ESC button will now act like clicking the Ok button
and close the about window without copying its content.

## Self-Review Checklist

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
  - no new tests have been implemented
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Improved the About Zed dialog to open as a standalone window instead
of a native system prompt

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

crates/zed/src/zed.rs | 261 +++++++++++++++++++++++++++++++++++++-------
1 file changed, 217 insertions(+), 44 deletions(-)

Detailed changes

crates/zed/src/zed.rs 🔗

@@ -33,10 +33,11 @@ use git_ui::commit_view::CommitViewToolbar;
 use git_ui::git_panel::GitPanel;
 use git_ui::project_diff::{BranchDiffToolbar, ProjectDiffToolbar};
 use gpui::{
-    Action, App, AppContext as _, AsyncWindowContext, Context, DismissEvent, Element, Entity,
-    Focusable, KeyBinding, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString,
-    Task, TitlebarOptions, UpdateGlobal, WeakEntity, Window, WindowHandle, WindowKind,
-    WindowOptions, actions, image_cache, point, px, retain_all,
+    Action, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent,
+    Element, Entity, FocusHandle, Focusable, Image, ImageFormat, KeyBinding, ParentElement,
+    PathPromptOptions, PromptLevel, ReadGlobal, SharedString, Size, Task, TitlebarOptions,
+    UpdateGlobal, WeakEntity, Window, WindowBounds, WindowHandle, WindowKind, WindowOptions,
+    actions, image_cache, img, point, px, retain_all,
 };
 use image_viewer::ImageInfo;
 use language::Capability;
@@ -78,7 +79,7 @@ use std::{
 use terminal_view::terminal_panel::{self, TerminalPanel};
 use theme::{ActiveTheme, SystemAppearance, ThemeRegistry, deserialize_icon_theme};
 use theme_settings::{ThemeSettings, load_user_theme};
-use ui::{PopoverMenuHandle, prelude::*};
+use ui::{Navigable, NavigableEntry, PopoverMenuHandle, TintColor, prelude::*};
 use util::markdown::MarkdownString;
 use util::rel_path::RelPath;
 use util::{ResultExt, asset_str, maybe};
@@ -96,8 +97,8 @@ use workspace::{
 };
 use workspace::{Pane, notifications::DetachAndPromptErr};
 use zed_actions::{
-    OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettingsFile, OpenZedUrl,
-    Quit,
+    About, OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettingsFile,
+    OpenZedUrl, Quit,
 };
 
 actions!(
@@ -277,10 +278,8 @@ pub fn init(cx: &mut App) {
             );
         });
     })
-    .on_action(|_: &zed_actions::About, cx| {
-        with_active_or_new_workspace(cx, |workspace, window, cx| {
-            about(workspace, window, cx);
-        });
+    .on_action(|_: &About, cx| {
+        open_about_window(cx);
     });
 }
 
@@ -1249,44 +1248,218 @@ fn initialize_pane(
     });
 }
 
-fn about(_: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
-    use std::fmt::Write;
-    let release_channel = ReleaseChannel::global(cx).display_name();
-    let full_version = AppVersion::global(cx);
-    let version = env!("CARGO_PKG_VERSION");
-    let debug = if cfg!(debug_assertions) {
-        "(debug)"
-    } else {
-        ""
-    };
-    let message = format!("{release_channel} {version} {debug}");
+fn open_about_window(cx: &mut App) {
+    fn about_window_icon(release_channel: ReleaseChannel) -> Arc<Image> {
+        let bytes = match release_channel {
+            ReleaseChannel::Dev => include_bytes!("../resources/app-icon-dev.png").as_slice(),
+            ReleaseChannel::Nightly => {
+                include_bytes!("../resources/app-icon-nightly.png").as_slice()
+            }
+            ReleaseChannel::Preview => {
+                include_bytes!("../resources/app-icon-preview.png").as_slice()
+            }
+            ReleaseChannel::Stable => include_bytes!("../resources/app-icon.png").as_slice(),
+        };
 
-    let mut detail = AppCommitSha::try_global(cx)
-        .map(|sha| sha.full())
-        .unwrap_or_default();
-    if !detail.is_empty() {
-        detail.push('\n');
+        Arc::new(Image::from_bytes(ImageFormat::Png, bytes.to_vec()))
     }
-    _ = write!(&mut detail, "\n{full_version}");
 
-    let detail = Some(detail);
+    struct AboutWindow {
+        focus_handle: FocusHandle,
+        ok_entry: NavigableEntry,
+        copy_entry: NavigableEntry,
+        app_icon: Arc<Image>,
+        message: SharedString,
+        commit: Option<SharedString>,
+        full_version: SharedString,
+    }
 
-    let prompt = window.prompt(
-        PromptLevel::Info,
-        &message,
-        detail.as_deref(),
-        &["Copy", "OK"],
-        cx,
-    );
-    cx.spawn(async move |_, cx| {
-        if let Ok(0) = prompt.await {
-            let content = format!("{}\n{}", message, detail.as_deref().unwrap_or(""));
-            cx.update(|cx| {
-                cx.write_to_clipboard(gpui::ClipboardItem::new_string(content));
-            });
+    impl AboutWindow {
+        fn new(cx: &mut Context<Self>) -> Self {
+            let release_channel = ReleaseChannel::global(cx);
+            let release_channel_name = release_channel.display_name();
+            let full_version: SharedString = AppVersion::global(cx).to_string().into();
+            let version = env!("CARGO_PKG_VERSION");
+
+            let debug = if cfg!(debug_assertions) {
+                "(debug)"
+            } else {
+                ""
+            };
+            let message: SharedString = format!("{release_channel_name} {version} {debug}").into();
+            let commit = AppCommitSha::try_global(cx)
+                .map(|sha| sha.full())
+                .filter(|commit| !commit.is_empty())
+                .map(SharedString::from);
+
+            Self {
+                focus_handle: cx.focus_handle(),
+                ok_entry: NavigableEntry::focusable(cx),
+                copy_entry: NavigableEntry::focusable(cx),
+                app_icon: about_window_icon(release_channel),
+                message,
+                commit,
+                full_version,
+            }
         }
-    })
-    .detach();
+
+        fn copy_details(&self, window: &mut Window, cx: &mut Context<Self>) {
+            let content = match self.commit.as_ref() {
+                Some(commit) => {
+                    format!(
+                        "{}\nCommit: {}\nVersion: {}",
+                        self.message, commit, self.full_version
+                    )
+                }
+                None => format!("{}\nVersion: {}", self.message, self.full_version),
+            };
+            cx.write_to_clipboard(ClipboardItem::new_string(content));
+            window.remove_window();
+        }
+    }
+
+    impl Render for AboutWindow {
+        fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+            let ok_is_focused = self.ok_entry.focus_handle.contains_focused(window, cx);
+            let copy_is_focused = self.copy_entry.focus_handle.contains_focused(window, cx);
+
+            Navigable::new(
+                v_flex()
+                    .id("about-window")
+                    .track_focus(&self.focus_handle)
+                    .on_action(cx.listener(|_, _: &menu::Cancel, window, _cx| {
+                        window.remove_window();
+                    }))
+                    .min_w_0()
+                    .size_full()
+                    .bg(cx.theme().colors().editor_background)
+                    .text_color(cx.theme().colors().text)
+                    .p_4()
+                    .when(cfg!(target_os = "macos"), |this| this.pt_10())
+                    .gap_4()
+                    .text_center()
+                    .justify_between()
+                    .child(
+                        v_flex()
+                            .w_full()
+                            .gap_2()
+                            .items_center()
+                            .child(img(self.app_icon.clone()).size_16().flex_none())
+                            .child(Headline::new(self.message.clone()))
+                            .when_some(self.commit.clone(), |this, commit| {
+                                this.child(
+                                    Label::new("Commit")
+                                        .color(Color::Muted)
+                                        .size(LabelSize::XSmall),
+                                )
+                                .child(Label::new(commit).size(LabelSize::Small))
+                            })
+                            .child(
+                                Label::new("Version")
+                                    .color(Color::Muted)
+                                    .size(LabelSize::XSmall),
+                            )
+                            .child(Label::new(self.full_version.clone()).size(LabelSize::Small)),
+                    )
+                    .child(
+                        h_flex()
+                            .w_full()
+                            .gap_1()
+                            .child(
+                                div()
+                                    .flex_1()
+                                    .track_focus(&self.ok_entry.focus_handle)
+                                    .on_action(cx.listener(|_, _: &menu::Confirm, window, _cx| {
+                                        window.remove_window();
+                                    }))
+                                    .child(
+                                        Button::new("ok", "Ok")
+                                            .full_width()
+                                            .style(ButtonStyle::OutlinedGhost)
+                                            .toggle_state(ok_is_focused)
+                                            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+                                            .on_click(cx.listener(|_, _, window, _cx| {
+                                                window.remove_window();
+                                            })),
+                                    ),
+                            )
+                            .child(
+                                div()
+                                    .flex_1()
+                                    .track_focus(&self.copy_entry.focus_handle)
+                                    .on_action(cx.listener(
+                                        |this, _: &menu::Confirm, window, cx| {
+                                            this.copy_details(window, cx);
+                                        },
+                                    ))
+                                    .child(
+                                        Button::new("copy", "Copy")
+                                            .full_width()
+                                            .style(ButtonStyle::Tinted(TintColor::Accent))
+                                            .toggle_state(copy_is_focused)
+                                            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+                                            .on_click(cx.listener(|this, _event, window, cx| {
+                                                this.copy_details(window, cx);
+                                            })),
+                                    ),
+                            ),
+                    )
+                    .into_any_element(),
+            )
+            .entry(self.ok_entry.clone())
+            .entry(self.copy_entry.clone())
+        }
+    }
+
+    impl Focusable for AboutWindow {
+        fn focus_handle(&self, _cx: &App) -> FocusHandle {
+            self.ok_entry.focus_handle.clone()
+        }
+    }
+
+    // Don't open about window twice
+    if let Some(existing) = cx
+        .windows()
+        .into_iter()
+        .find_map(|w| w.downcast::<AboutWindow>())
+    {
+        existing
+            .update(cx, |about_window, window, cx| {
+                window.activate_window();
+                about_window.ok_entry.focus_handle.focus(window, cx);
+            })
+            .log_err();
+        return;
+    }
+
+    let window_size = Size {
+        width: px(440.),
+        height: px(300.),
+    };
+
+    cx.open_window(
+        WindowOptions {
+            titlebar: Some(TitlebarOptions {
+                title: Some("About Zed".into()),
+                appears_transparent: true,
+                traffic_light_position: Some(point(px(12.), px(12.))),
+            }),
+            window_bounds: Some(WindowBounds::centered(window_size, cx)),
+            is_resizable: false,
+            is_minimizable: false,
+            kind: WindowKind::Normal,
+            app_id: Some(ReleaseChannel::global(cx).app_id().to_owned()),
+            ..Default::default()
+        },
+        |window, cx| {
+            let about_window = cx.new(AboutWindow::new);
+            let focus_handle = about_window.read(cx).ok_entry.focus_handle.clone();
+            window.activate_window();
+            focus_handle.focus(window, cx);
+            about_window
+        },
+    )
+    .log_err();
 }
 
 #[cfg(not(target_os = "windows"))]