ui_prompt.rs

  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}