gpui: Improve `window.prompt` to support ESC with non-English cancel text on macOS (#29538)

Jason Lee , Mikayla Maki , and Mikayla Maki created

Release Notes:

- N/A

----

The before version GPUI used `Cancel` for cancel text, if we use
non-English text (e.g.: "取消" in Chinese), then the press `Esc` to cancel
will not work.

So this PR to change it by use `PromptButton` to instead the `&str`,
then we can use `PromptButton::cancel("取消")` for the `Cancel` button.

Run `cargo run -p gpui --example window` to test.

---

Platform Test:

- [x] macOS
- [x] Windows
- [x] Linux (x11 and Wayland)

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>

Change summary

crates/gpui/examples/window.rs                   | 41 +++++++++++++
crates/gpui/src/app.rs                           | 12 ++--
crates/gpui/src/app/async_context.rs             | 13 ++-
crates/gpui/src/platform.rs                      | 54 +++++++++++++++++
crates/gpui/src/platform/linux/wayland/window.rs |  6 +-
crates/gpui/src/platform/linux/x11/window.rs     |  8 +-
crates/gpui/src/platform/mac/events.rs           |  2 
crates/gpui/src/platform/mac/window.rs           | 20 ++++--
crates/gpui/src/platform/test/platform.rs        |  8 +-
crates/gpui/src/platform/test/window.rs          |  6 +-
crates/gpui/src/platform/windows/window.rs       | 10 +-
crates/gpui/src/window.rs                        | 36 +++++++----
crates/gpui/src/window/prompts.rs                | 14 ++--
crates/ui_prompt/src/ui_prompt.rs                | 10 +-
14 files changed, 174 insertions(+), 66 deletions(-)

Detailed changes

crates/gpui/examples/window.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{
-    App, Application, Bounds, Context, KeyBinding, SharedString, Timer, Window, WindowBounds,
-    WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size,
+    App, Application, Bounds, Context, KeyBinding, PromptButton, PromptLevel, SharedString, Timer,
+    Window, WindowBounds, WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size,
 };
 
 struct SubWindow {
@@ -169,6 +169,42 @@ impl Render for WindowDemo {
                 let content_size = window.bounds().size;
                 window.resize(size(content_size.height, content_size.width));
             }))
+            .child(button("Prompt", |window, cx| {
+                let answer = window.prompt(
+                    PromptLevel::Info,
+                    "Are you sure?",
+                    None,
+                    &["Ok", "Cancel"],
+                    cx,
+                );
+
+                cx.spawn(async move |_| {
+                    if answer.await.unwrap() == 0 {
+                        println!("You have clicked Ok");
+                    } else {
+                        println!("You have clicked Cancel");
+                    }
+                })
+                .detach();
+            }))
+            .child(button("Prompt (non-English)", |window, cx| {
+                let answer = window.prompt(
+                    PromptLevel::Info,
+                    "Are you sure?",
+                    None,
+                    &[PromptButton::ok("确定"), PromptButton::cancel("取消")],
+                    cx,
+                );
+
+                cx.spawn(async move |_| {
+                    if answer.await.unwrap() == 0 {
+                        println!("You have clicked Ok");
+                    } else {
+                        println!("You have clicked Cancel");
+                    }
+                })
+                .detach();
+            }))
     }
 }
 
@@ -195,6 +231,7 @@ fn main() {
             },
         )
         .unwrap();
+
         cx.activate(true);
         cx.on_action(|_: &Quit, cx| cx.quit());
         cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);

crates/gpui/src/app.rs 🔗

@@ -37,10 +37,10 @@ use crate::{
     AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
     EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
     Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
-    PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptHandle, PromptLevel,
-    Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString,
-    SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
-    WindowHandle, WindowId, WindowInvalidator,
+    PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
+    PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
+    SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window,
+    WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
     colors::{Colors, GlobalColors},
     current_platform, hash, init_app_menus,
 };
@@ -1578,14 +1578,14 @@ impl App {
             PromptLevel,
             &str,
             Option<&str>,
-            &[&str],
+            &[PromptButton],
             PromptHandle,
             &mut Window,
             &mut App,
         ) -> RenderablePromptHandle
         + 'static,
     ) {
-        self.prompt_builder = Some(PromptBuilder::Custom(Box::new(renderer)))
+        self.prompt_builder = Some(PromptBuilder::Custom(Box::new(renderer)));
     }
 
     /// Reset the prompt builder to the default implementation.

crates/gpui/src/app/async_context.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, BorrowAppContext,
-    Entity, EventEmitter, Focusable, ForegroundExecutor, Global, PromptLevel, Render, Reservation,
-    Result, Subscription, Task, VisualContext, Window, WindowHandle,
+    Entity, EventEmitter, Focusable, ForegroundExecutor, Global, PromptButton, PromptLevel, Render,
+    Reservation, Result, Subscription, Task, VisualContext, Window, WindowHandle,
 };
 use anyhow::Context as _;
 use derive_more::{Deref, DerefMut};
@@ -314,13 +314,16 @@ impl AsyncWindowContext {
     /// Present a platform dialog.
     /// The provided message will be presented, along with buttons for each answer.
     /// When a button is clicked, the returned Receiver will receive the index of the clicked button.
-    pub fn prompt(
+    pub fn prompt<T>(
         &mut self,
         level: PromptLevel,
         message: &str,
         detail: Option<&str>,
-        answers: &[&str],
-    ) -> oneshot::Receiver<usize> {
+        answers: &[T],
+    ) -> oneshot::Receiver<usize>
+    where
+        T: Clone + Into<PromptButton>,
+    {
         self.window
             .update(self, |_, window, cx| {
                 window.prompt(level, message, detail, answers, cx)

crates/gpui/src/platform.rs 🔗

@@ -418,7 +418,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
         level: PromptLevel,
         msg: &str,
         detail: Option<&str>,
-        answers: &[&str],
+        answers: &[PromptButton],
     ) -> Option<oneshot::Receiver<usize>>;
     fn activate(&self);
     fn is_active(&self) -> bool;
@@ -1244,6 +1244,58 @@ pub enum PromptLevel {
     Critical,
 }
 
+/// Prompt Button
+#[derive(Clone, Debug, PartialEq)]
+pub enum PromptButton {
+    /// Ok button
+    Ok(SharedString),
+    /// Cancel button
+    Cancel(SharedString),
+    /// Other button
+    Other(SharedString),
+}
+
+impl PromptButton {
+    /// Create a button with label
+    pub fn new(label: impl Into<SharedString>) -> Self {
+        PromptButton::Other(label.into())
+    }
+
+    /// Create an Ok button
+    pub fn ok(label: impl Into<SharedString>) -> Self {
+        PromptButton::Ok(label.into())
+    }
+
+    /// Create a Cancel button
+    pub fn cancel(label: impl Into<SharedString>) -> Self {
+        PromptButton::Cancel(label.into())
+    }
+
+    #[allow(dead_code)]
+    pub(crate) fn is_cancel(&self) -> bool {
+        matches!(self, PromptButton::Cancel(_))
+    }
+
+    /// Returns the label of the button
+    pub fn label(&self) -> &SharedString {
+        match self {
+            PromptButton::Ok(label) => label,
+            PromptButton::Cancel(label) => label,
+            PromptButton::Other(label) => label,
+        }
+    }
+}
+
+impl From<&str> for PromptButton {
+    fn from(value: &str) -> Self {
+        match value.to_lowercase().as_str() {
+            "ok" => PromptButton::Ok("Ok".into()),
+            "cancel" => PromptButton::Cancel("Cancel".into()),
+            _ => PromptButton::Other(SharedString::from(value.to_owned())),
+        }
+    }
+}
+
 /// The style of the cursor (pointer)
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
 pub enum CursorStyle {

crates/gpui/src/platform/linux/wayland/window.rs 🔗

@@ -29,8 +29,8 @@ use crate::platform::{
 use crate::scene::Scene;
 use crate::{
     AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
-    PlatformDisplay, PlatformInput, Point, PromptLevel, RequestFrameOptions, ResizeEdge,
-    ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance,
+    PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
+    ResizeEdge, ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance,
     WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowParams, px,
     size,
 };
@@ -862,7 +862,7 @@ impl PlatformWindow for WaylandWindow {
         _level: PromptLevel,
         _msg: &str,
         _detail: Option<&str>,
-        _answers: &[&str],
+        _answers: &[PromptButton],
     ) -> Option<Receiver<usize>> {
         None
     }

crates/gpui/src/platform/linux/x11/window.rs 🔗

@@ -4,9 +4,9 @@ use crate::platform::blade::{BladeContext, BladeRenderer, BladeSurfaceConfig};
 use crate::{
     AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, GpuSpecs, Modifiers,
     Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
-    Point, PromptLevel, RequestFrameOptions, ResizeEdge, ScaledPixels, Scene, Size, Tiling,
-    WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
-    WindowParams, X11ClientStatePtr, px, size,
+    Point, PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, ScaledPixels, Scene, Size,
+    Tiling, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowDecorations,
+    WindowKind, WindowParams, X11ClientStatePtr, px, size,
 };
 
 use blade_graphics as gpu;
@@ -1227,7 +1227,7 @@ impl PlatformWindow for X11Window {
         _level: PromptLevel,
         _msg: &str,
         _detail: Option<&str>,
-        _answers: &[&str],
+        _answers: &[PromptButton],
     ) -> Option<futures::channel::oneshot::Receiver<usize>> {
         None
     }

crates/gpui/src/platform/mac/events.rs 🔗

@@ -21,7 +21,7 @@ const BACKSPACE_KEY: u16 = 0x7f;
 const SPACE_KEY: u16 = b' ' as u16;
 const ENTER_KEY: u16 = 0x0d;
 const NUMPAD_ENTER_KEY: u16 = 0x03;
-const ESCAPE_KEY: u16 = 0x1b;
+pub(crate) const ESCAPE_KEY: u16 = 0x1b;
 const TAB_KEY: u16 = 0x09;
 const SHIFT_TAB_KEY: u16 = 0x19;
 

crates/gpui/src/platform/mac/window.rs 🔗

@@ -3,8 +3,8 @@ use crate::{
     AnyWindowHandle, Bounds, DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor,
     KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
     MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
-    PlatformWindow, Point, PromptLevel, RequestFrameOptions, ScaledPixels, Size, Timer,
-    WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowKind, WindowParams,
+    PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, ScaledPixels, Size,
+    Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowKind, WindowParams,
     platform::PlatformInputHandler, point, px, size,
 };
 use block::ConcreteBlock;
@@ -902,7 +902,7 @@ impl PlatformWindow for MacWindow {
         level: PromptLevel,
         msg: &str,
         detail: Option<&str>,
-        answers: &[&str],
+        answers: &[PromptButton],
     ) -> Option<oneshot::Receiver<usize>> {
         // macOs applies overrides to modal window buttons after they are added.
         // Two most important for this logic are:
@@ -926,7 +926,7 @@ impl PlatformWindow for MacWindow {
             .iter()
             .enumerate()
             .rev()
-            .find(|(_, label)| **label != "Cancel")
+            .find(|(_, label)| !label.is_cancel())
             .filter(|&(label_index, _)| label_index > 0);
 
         unsafe {
@@ -948,11 +948,19 @@ impl PlatformWindow for MacWindow {
                 .enumerate()
                 .filter(|&(ix, _)| Some(ix) != latest_non_cancel_label.map(|(ix, _)| ix))
             {
-                let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
+                let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer.label())];
                 let _: () = msg_send![button, setTag: ix as NSInteger];
+
+                if answer.is_cancel() {
+                    // Bind Escape Key to Cancel Button
+                    if let Some(key) = std::char::from_u32(super::events::ESCAPE_KEY as u32) {
+                        let _: () =
+                            msg_send![button, setKeyEquivalent: ns_string(&key.to_string())];
+                    }
+                }
             }
             if let Some((ix, answer)) = latest_non_cancel_label {
-                let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
+                let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer.label())];
                 let _: () = msg_send![button, setTag: ix as NSInteger];
             }
 

crates/gpui/src/platform/test/platform.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
     AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
     ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
-    PlatformTextSystem, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, Size, Task,
-    TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
+    PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
+    Size, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
 };
 use anyhow::Result;
 use collections::VecDeque;
@@ -165,10 +165,10 @@ impl TestPlatform {
         &self,
         msg: &str,
         detail: Option<&str>,
-        answers: &[&str],
+        answers: &[PromptButton],
     ) -> oneshot::Receiver<usize> {
         let (tx, rx) = oneshot::channel();
-        let answers: Vec<String> = answers.iter().map(|&s| s.to_string()).collect();
+        let answers: Vec<String> = answers.iter().map(|s| s.label().to_string()).collect();
         self.background_executor()
             .set_waiting_hint(Some(format!("PROMPT: {:?} {:?}", msg, detail)));
         self.prompts

crates/gpui/src/platform/test/window.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
     AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DispatchEventResult, GpuSpecs,
     Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
-    Point, RequestFrameOptions, ScaledPixels, Size, TestPlatform, TileId, WindowAppearance,
-    WindowBackgroundAppearance, WindowBounds, WindowParams,
+    Point, PromptButton, RequestFrameOptions, ScaledPixels, Size, TestPlatform, TileId,
+    WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowParams,
 };
 use collections::HashMap;
 use parking_lot::Mutex;
@@ -164,7 +164,7 @@ impl PlatformWindow for TestWindow {
         _level: crate::PromptLevel,
         msg: &str,
         detail: Option<&str>,
-        answers: &[&str],
+        answers: &[PromptButton],
     ) -> Option<futures::channel::oneshot::Receiver<usize>> {
         Some(
             self.0

crates/gpui/src/platform/windows/window.rs 🔗

@@ -608,7 +608,7 @@ impl PlatformWindow for WindowsWindow {
         level: PromptLevel,
         msg: &str,
         detail: Option<&str>,
-        answers: &[&str],
+        answers: &[PromptButton],
     ) -> Option<Receiver<usize>> {
         let (done_tx, done_rx) = oneshot::channel();
         let msg = msg.to_string();
@@ -616,8 +616,8 @@ impl PlatformWindow for WindowsWindow {
             Some(info) => Some(info.to_string()),
             None => None,
         };
-        let answers = answers.iter().map(|s| s.to_string()).collect::<Vec<_>>();
         let handle = self.0.hwnd;
+        let answers = answers.to_vec();
         self.0
             .executor
             .spawn(async move {
@@ -653,9 +653,9 @@ impl PlatformWindow for WindowsWindow {
                     let mut button_id_map = Vec::with_capacity(answers.len());
                     let mut buttons = Vec::new();
                     let mut btn_encoded = Vec::new();
-                    for (index, btn_string) in answers.iter().enumerate() {
-                        let encoded = HSTRING::from(btn_string);
-                        let button_id = if btn_string == "Cancel" {
+                    for (index, btn) in answers.iter().enumerate() {
+                        let encoded = HSTRING::from(btn.label().as_ref());
+                        let button_id = if btn.is_cancel() {
                             IDCANCEL.0
                         } else {
                             index as i32 - 100

crates/gpui/src/window.rs 🔗

@@ -9,13 +9,13 @@ use crate::{
     KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId, LineLayoutIndex, Modifiers,
     ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent,
     Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
-    PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
-    RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR,
-    SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
-    SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
-    TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance,
-    WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
-    point, prelude::*, px, rems, size, transparent_black,
+    PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad, Render,
+    RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge,
+    SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size,
+    StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle,
+    TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, WindowAppearance,
+    WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions,
+    WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, transparent_black,
 };
 use anyhow::{Context as _, Result, anyhow};
 use collections::{FxHashMap, FxHashSet};
@@ -3821,28 +3821,36 @@ impl Window {
     /// Present a platform dialog.
     /// The provided message will be presented, along with buttons for each answer.
     /// When a button is clicked, the returned Receiver will receive the index of the clicked button.
-    pub fn prompt(
+    pub fn prompt<T>(
         &mut self,
         level: PromptLevel,
         message: &str,
         detail: Option<&str>,
-        answers: &[&str],
+        answers: &[T],
         cx: &mut App,
-    ) -> oneshot::Receiver<usize> {
+    ) -> oneshot::Receiver<usize>
+    where
+        T: Clone + Into<PromptButton>,
+    {
         let prompt_builder = cx.prompt_builder.take();
         let Some(prompt_builder) = prompt_builder else {
             unreachable!("Re-entrant window prompting is not supported by GPUI");
         };
 
+        let answers = answers
+            .iter()
+            .map(|answer| answer.clone().into())
+            .collect::<Vec<_>>();
+
         let receiver = match &prompt_builder {
             PromptBuilder::Default => self
                 .platform_window
-                .prompt(level, message, detail, answers)
+                .prompt(level, message, detail, &answers)
                 .unwrap_or_else(|| {
-                    self.build_custom_prompt(&prompt_builder, level, message, detail, answers, cx)
+                    self.build_custom_prompt(&prompt_builder, level, message, detail, &answers, cx)
                 }),
             PromptBuilder::Custom(_) => {
-                self.build_custom_prompt(&prompt_builder, level, message, detail, answers, cx)
+                self.build_custom_prompt(&prompt_builder, level, message, detail, &answers, cx)
             }
         };
 
@@ -3857,7 +3865,7 @@ impl Window {
         level: PromptLevel,
         message: &str,
         detail: Option<&str>,
-        answers: &[&str],
+        answers: &[PromptButton],
         cx: &mut App,
     ) -> oneshot::Receiver<usize> {
         let (sender, receiver) = oneshot::channel();

crates/gpui/src/window/prompts.rs 🔗

@@ -4,7 +4,7 @@ use futures::channel::oneshot;
 
 use crate::{
     AnyView, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable,
-    InteractiveElement, IntoElement, ParentElement, PromptLevel, Render,
+    InteractiveElement, IntoElement, ParentElement, PromptButton, PromptLevel, Render,
     StatefulInteractiveElement, Styled, div, opaque_grey, white,
 };
 
@@ -74,7 +74,7 @@ pub fn fallback_prompt_renderer(
     level: PromptLevel,
     message: &str,
     detail: Option<&str>,
-    actions: &[&str],
+    actions: &[PromptButton],
     handle: PromptHandle,
     window: &mut Window,
     cx: &mut App,
@@ -83,7 +83,7 @@ pub fn fallback_prompt_renderer(
         _level: level,
         message: message.to_string(),
         detail: detail.map(ToString::to_string),
-        actions: actions.iter().map(ToString::to_string).collect(),
+        actions: actions.to_vec(),
         focus: cx.focus_handle(),
     });
 
@@ -95,7 +95,7 @@ pub struct FallbackPromptRenderer {
     _level: PromptLevel,
     message: String,
     detail: Option<String>,
-    actions: Vec<String>,
+    actions: Vec<PromptButton>,
     focus: FocusHandle,
 }
 
@@ -138,7 +138,7 @@ impl Render for FallbackPromptRenderer {
                     .rounded_xs()
                     .cursor_pointer()
                     .text_sm()
-                    .child(action.clone())
+                    .child(action.label().clone())
                     .id(ix)
                     .on_click(cx.listener(move |_, _, _, cx| {
                         cx.emit(PromptResponse(ix));
@@ -202,7 +202,7 @@ pub(crate) enum PromptBuilder {
                 PromptLevel,
                 &str,
                 Option<&str>,
-                &[&str],
+                &[PromptButton],
                 PromptHandle,
                 &mut Window,
                 &mut App,
@@ -216,7 +216,7 @@ impl Deref for PromptBuilder {
         PromptLevel,
         &str,
         Option<&str>,
-        &[&str],
+        &[PromptButton],
         PromptHandle,
         &mut Window,
         &mut App,

crates/ui_prompt/src/ui_prompt.rs 🔗

@@ -1,8 +1,8 @@
 use gpui::{
     App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, FontWeight,
-    InteractiveElement, IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse,
-    Refineable, Render, RenderablePromptHandle, SharedString, Styled, TextStyleRefinement, Window,
-    div,
+    InteractiveElement, IntoElement, ParentElement, PromptButton, PromptHandle, PromptLevel,
+    PromptResponse, Refineable, Render, RenderablePromptHandle, SharedString, Styled,
+    TextStyleRefinement, Window, div,
 };
 use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 use settings::{Settings, SettingsStore};
@@ -35,7 +35,7 @@ fn zed_prompt_renderer(
     level: PromptLevel,
     message: &str,
     detail: Option<&str>,
-    actions: &[&str],
+    actions: &[PromptButton],
     handle: PromptHandle,
     window: &mut Window,
     cx: &mut App,
@@ -44,7 +44,7 @@ fn zed_prompt_renderer(
         |cx| ZedPromptRenderer {
             _level: level,
             message: message.to_string(),
-            actions: actions.iter().map(ToString::to_string).collect(),
+            actions: actions.iter().map(|a| a.label().to_string()).collect(),
             focus: cx.focus_handle(),
             active_action_id: 0,
             detail: detail