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