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: message.to_string(),
 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: String,
 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_family = settings.ui_font.family.clone();
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(font_family)
134            .child(
135                div()
136                    .w_full()
137                    .font_weight(FontWeight::BOLD)
138                    .child(self.message.clone())
139                    .text_color(ui::Color::Default.color(cx)),
140            )
141            .children(self.detail.clone().map(|detail| {
142                div()
143                    .w_full()
144                    .text_xs()
145                    .child(MarkdownElement::new(detail, {
146                        let settings = ThemeSettings::get_global(cx);
147                        let mut base_text_style = window.text_style();
148                        base_text_style.refine(&TextStyleRefinement {
149                            font_family: Some(settings.ui_font.family.clone()),
150                            font_size: Some(settings.ui_font_size(cx).into()),
151                            color: Some(ui::Color::Muted.color(cx)),
152                            ..Default::default()
153                        });
154                        MarkdownStyle {
155                            base_text_style,
156                            selection_background_color: cx
157                                .theme()
158                                .colors()
159                                .element_selection_background,
160                            ..Default::default()
161                        }
162                    }))
163            }))
164            .child(h_flex().justify_end().gap_2().children(
165                self.actions.iter().enumerate().rev().map(|(ix, action)| {
166                    ui::Button::new(ix, action.clone())
167                        .label_size(LabelSize::Large)
168                        .style(ButtonStyle::Filled)
169                        .when(ix == self.active_action_id, |el| {
170                            el.style(ButtonStyle::Tinted(TintColor::Accent))
171                        })
172                        .layer(ElevationIndex::ModalSurface)
173                        .on_click(cx.listener(move |_, _, _window, cx| {
174                            cx.emit(PromptResponse(ix));
175                        }))
176                }),
177            ));
178
179        div().size_full().occlude().child(
180            div()
181                .size_full()
182                .absolute()
183                .top_0()
184                .left_0()
185                .flex()
186                .flex_col()
187                .justify_around()
188                .child(
189                    div()
190                        .w_full()
191                        .flex()
192                        .flex_row()
193                        .justify_around()
194                        .child(prompt),
195                ),
196        )
197    }
198}
199
200impl EventEmitter<PromptResponse> for ZedPromptRenderer {}
201
202impl Focusable for ZedPromptRenderer {
203    fn focus_handle(&self, _: &crate::App) -> FocusHandle {
204        self.focus.clone()
205    }
206}