ui_prompt.rs

  1use gpui::{
  2    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    div,
  6};
  7use markdown::{Markdown, 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: &[&str],
 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(ToString::to_string).collect(),
 48            focus: cx.focus_handle(),
 49            active_action_id: 0,
 50            detail: detail.filter(|text| !text.is_empty()).map(|text| {
 51                cx.new(|cx| {
 52                    let settings = ThemeSettings::get_global(cx);
 53                    let mut base_text_style = window.text_style();
 54                    base_text_style.refine(&TextStyleRefinement {
 55                        font_family: Some(settings.ui_font.family.clone()),
 56                        font_size: Some(settings.ui_font_size(cx).into()),
 57                        color: Some(ui::Color::Muted.color(cx)),
 58                        ..Default::default()
 59                    });
 60                    let markdown_style = MarkdownStyle {
 61                        base_text_style,
 62                        selection_background_color: { cx.theme().players().local().selection },
 63                        ..Default::default()
 64                    };
 65                    Markdown::new(SharedString::new(text), markdown_style, None, None, cx)
 66                })
 67            }),
 68        }
 69    });
 70
 71    handle.with_view(renderer, window, cx)
 72}
 73
 74pub struct ZedPromptRenderer {
 75    _level: PromptLevel,
 76    message: String,
 77    actions: Vec<String>,
 78    focus: FocusHandle,
 79    active_action_id: usize,
 80    detail: Option<Entity<Markdown>>,
 81}
 82
 83impl ZedPromptRenderer {
 84    fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
 85        cx.emit(PromptResponse(self.active_action_id));
 86    }
 87
 88    fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
 89        if let Some(ix) = self.actions.iter().position(|a| a == "Cancel") {
 90            cx.emit(PromptResponse(ix));
 91        }
 92    }
 93
 94    fn select_first(
 95        &mut self,
 96        _: &menu::SelectFirst,
 97        _window: &mut Window,
 98        cx: &mut Context<Self>,
 99    ) {
100        self.active_action_id = self.actions.len().saturating_sub(1);
101        cx.notify();
102    }
103
104    fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
105        self.active_action_id = 0;
106        cx.notify();
107    }
108
109    fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
110        if self.active_action_id > 0 {
111            self.active_action_id -= 1;
112        } else {
113            self.active_action_id = self.actions.len().saturating_sub(1);
114        }
115        cx.notify();
116    }
117
118    fn select_previous(
119        &mut self,
120        _: &menu::SelectPrevious,
121        _window: &mut Window,
122        cx: &mut Context<Self>,
123    ) {
124        self.active_action_id = (self.active_action_id + 1) % self.actions.len();
125        cx.notify();
126    }
127}
128
129impl Render for ZedPromptRenderer {
130    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
131        let settings = ThemeSettings::get_global(cx);
132        let font_family = settings.ui_font.family.clone();
133        let prompt = v_flex()
134            .key_context("Prompt")
135            .cursor_default()
136            .track_focus(&self.focus)
137            .on_action(cx.listener(Self::confirm))
138            .on_action(cx.listener(Self::cancel))
139            .on_action(cx.listener(Self::select_next))
140            .on_action(cx.listener(Self::select_previous))
141            .on_action(cx.listener(Self::select_first))
142            .on_action(cx.listener(Self::select_last))
143            .elevation_3(cx)
144            .w_72()
145            .overflow_hidden()
146            .p_4()
147            .gap_4()
148            .font_family(font_family)
149            .child(
150                div()
151                    .w_full()
152                    .font_weight(FontWeight::BOLD)
153                    .child(self.message.clone())
154                    .text_color(ui::Color::Default.color(cx)),
155            )
156            .children(
157                self.detail
158                    .clone()
159                    .map(|detail| div().w_full().text_xs().child(detail)),
160            )
161            .child(h_flex().justify_end().gap_2().children(
162                self.actions.iter().enumerate().rev().map(|(ix, action)| {
163                    ui::Button::new(ix, action.clone())
164                        .label_size(LabelSize::Large)
165                        .style(ButtonStyle::Filled)
166                        .when(ix == self.active_action_id, |el| {
167                            el.style(ButtonStyle::Tinted(TintColor::Accent))
168                        })
169                        .layer(ElevationIndex::ModalSurface)
170                        .on_click(cx.listener(move |_, _, _window, cx| {
171                            cx.emit(PromptResponse(ix));
172                        }))
173                }),
174            ));
175
176        div().size_full().occlude().child(
177            div()
178                .size_full()
179                .absolute()
180                .top_0()
181                .left_0()
182                .flex()
183                .flex_col()
184                .justify_around()
185                .child(
186                    div()
187                        .w_full()
188                        .flex()
189                        .flex_row()
190                        .justify_around()
191                        .child(prompt),
192                ),
193        )
194    }
195}
196
197impl EventEmitter<PromptResponse> for ZedPromptRenderer {}
198
199impl Focusable for ZedPromptRenderer {
200    fn focus_handle(&self, _: &crate::App) -> FocusHandle {
201        self.focus.clone()
202    }
203}