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

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>
This commit is contained in:
Jason Lee 2025-05-30 23:26:27 +08:00 committed by GitHub
parent 1d5d3de85c
commit 047e7eacec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 174 additions and 66 deletions

View file

@ -1,6 +1,6 @@
use gpui::{ use gpui::{
App, Application, Bounds, Context, KeyBinding, SharedString, Timer, Window, WindowBounds, App, Application, Bounds, Context, KeyBinding, PromptButton, PromptLevel, SharedString, Timer,
WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size, Window, WindowBounds, WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size,
}; };
struct SubWindow { struct SubWindow {
@ -169,6 +169,42 @@ impl Render for WindowDemo {
let content_size = window.bounds().size; let content_size = window.bounds().size;
window.resize(size(content_size.height, content_size.width)); 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(); .unwrap();
cx.activate(true); cx.activate(true);
cx.on_action(|_: &Quit, cx| cx.quit()); cx.on_action(|_: &Quit, cx| cx.quit());
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);

View file

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

View file

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

View file

@ -418,7 +418,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
level: PromptLevel, level: PromptLevel,
msg: &str, msg: &str,
detail: Option<&str>, detail: Option<&str>,
answers: &[&str], answers: &[PromptButton],
) -> Option<oneshot::Receiver<usize>>; ) -> Option<oneshot::Receiver<usize>>;
fn activate(&self); fn activate(&self);
fn is_active(&self) -> bool; fn is_active(&self) -> bool;
@ -1244,6 +1244,58 @@ pub enum PromptLevel {
Critical, 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) /// The style of the cursor (pointer)
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub enum CursorStyle { pub enum CursorStyle {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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