ui_prompt.rs

  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}