ui_prompt.rs

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