1use gpui::{
2 App, Entity, EventEmitter, FocusHandle, Focusable, PromptButton, PromptHandle, PromptLevel,
3 PromptResponse, RenderablePromptHandle, SharedString, TextStyleRefinement, Window, div,
4 prelude::*,
5};
6use markdown::{Markdown, MarkdownElement, MarkdownStyle};
7use settings::{Settings, SettingsStore};
8use theme::ThemeSettings;
9use ui::{FluentBuilder, TintColor, prelude::*};
10use workspace::WorkspaceSettings;
11
12pub fn init(cx: &mut App) {
13 process_settings(cx);
14
15 cx.observe_global::<SettingsStore>(process_settings)
16 .detach();
17}
18
19fn process_settings(cx: &mut App) {
20 let settings = WorkspaceSettings::get_global(cx);
21 if settings.use_system_prompts && cfg!(not(any(target_os = "linux", target_os = "freebsd"))) {
22 cx.reset_prompt_builder();
23 } else {
24 cx.set_prompt_builder(zed_prompt_renderer);
25 }
26}
27
28/// Use this function in conjunction with [App::set_prompt_builder] to force
29/// GPUI to use the internal prompt system.
30fn zed_prompt_renderer(
31 level: PromptLevel,
32 message: &str,
33 detail: Option<&str>,
34 actions: &[PromptButton],
35 handle: PromptHandle,
36 window: &mut Window,
37 cx: &mut App,
38) -> RenderablePromptHandle {
39 let renderer = cx.new({
40 |cx| ZedPromptRenderer {
41 _level: level,
42 message: cx.new(|cx| Markdown::new(SharedString::new(message), None, None, cx)),
43 actions: actions.iter().map(|a| a.label().to_string()).collect(),
44 focus: cx.focus_handle(),
45 active_action_id: 0,
46 detail: detail
47 .filter(|text| !text.is_empty())
48 .map(|text| cx.new(|cx| Markdown::new(SharedString::new(text), None, None, cx))),
49 }
50 });
51
52 handle.with_view(renderer, window, cx)
53}
54
55pub struct ZedPromptRenderer {
56 _level: PromptLevel,
57 message: Entity<Markdown>,
58 actions: Vec<String>,
59 focus: FocusHandle,
60 active_action_id: usize,
61 detail: Option<Entity<Markdown>>,
62}
63
64impl ZedPromptRenderer {
65 fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
66 cx.emit(PromptResponse(self.active_action_id));
67 }
68
69 fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
70 if let Some(ix) = self.actions.iter().position(|a| a == "Cancel") {
71 cx.emit(PromptResponse(ix));
72 }
73 }
74
75 fn select_first(
76 &mut self,
77 _: &menu::SelectFirst,
78 _window: &mut Window,
79 cx: &mut Context<Self>,
80 ) {
81 self.active_action_id = self.actions.len().saturating_sub(1);
82 cx.notify();
83 }
84
85 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
86 self.active_action_id = 0;
87 cx.notify();
88 }
89
90 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
91 self.active_action_id = (self.active_action_id + 1) % self.actions.len();
92 cx.notify();
93 }
94
95 fn select_previous(
96 &mut self,
97 _: &menu::SelectPrevious,
98 _window: &mut Window,
99 cx: &mut Context<Self>,
100 ) {
101 if self.active_action_id > 0 {
102 self.active_action_id -= 1;
103 } else {
104 self.active_action_id = self.actions.len().saturating_sub(1);
105 }
106 cx.notify();
107 }
108}
109
110impl Render for ZedPromptRenderer {
111 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
112 let settings = ThemeSettings::get_global(cx);
113
114 let dialog = v_flex()
115 .key_context("Prompt")
116 .cursor_default()
117 .track_focus(&self.focus)
118 .on_action(cx.listener(Self::confirm))
119 .on_action(cx.listener(Self::cancel))
120 .on_action(cx.listener(Self::select_next))
121 .on_action(cx.listener(Self::select_previous))
122 .on_action(cx.listener(Self::select_first))
123 .on_action(cx.listener(Self::select_last))
124 .w_80()
125 .p_4()
126 .gap_4()
127 .elevation_3(cx)
128 .overflow_hidden()
129 .font_family(settings.ui_font.family.clone())
130 .child(div().w_full().child(MarkdownElement::new(
131 self.message.clone(),
132 markdown_style(true, window, cx),
133 )))
134 .children(self.detail.clone().map(|detail| {
135 div().w_full().text_xs().child(MarkdownElement::new(
136 detail,
137 markdown_style(false, window, cx),
138 ))
139 }))
140 .child(
141 v_flex()
142 .gap_1()
143 .children(self.actions.iter().enumerate().map(|(ix, action)| {
144 Button::new(ix, action.clone())
145 .full_width()
146 .style(ButtonStyle::Outlined)
147 .when(ix == self.active_action_id, |s| {
148 s.style(ButtonStyle::Tinted(TintColor::Accent))
149 })
150 .tab_index(ix as isize)
151 .on_click(cx.listener(move |_, _, _window, cx| {
152 cx.emit(PromptResponse(ix));
153 }))
154 })),
155 );
156
157 div()
158 .size_full()
159 .occlude()
160 .bg(gpui::black().opacity(0.2))
161 .child(
162 v_flex()
163 .size_full()
164 .absolute()
165 .top_0()
166 .left_0()
167 .items_center()
168 .justify_center()
169 .child(dialog),
170 )
171 }
172}
173
174fn markdown_style(main_message: bool, window: &Window, cx: &App) -> MarkdownStyle {
175 let mut base_text_style = window.text_style();
176 let settings = ThemeSettings::get_global(cx);
177 let font_size = settings.ui_font_size(cx).into();
178
179 let color = if main_message {
180 Color::Default.color(cx)
181 } else {
182 Color::Muted.color(cx)
183 };
184
185 base_text_style.refine(&TextStyleRefinement {
186 font_family: Some(settings.ui_font.family.clone()),
187 font_size: Some(font_size),
188 color: Some(color),
189 ..Default::default()
190 });
191
192 MarkdownStyle {
193 base_text_style,
194 selection_background_color: cx.theme().colors().element_selection_background,
195 ..Default::default()
196 }
197}
198
199impl EventEmitter<PromptResponse> for ZedPromptRenderer {}
200
201impl Focusable for ZedPromptRenderer {
202 fn focus_handle(&self, _: &crate::App) -> FocusHandle {
203 self.focus.clone()
204 }
205}