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