1use crate::kernels::KernelSpecification;
2use crate::repl_store::ReplStore;
3use crate::KERNEL_DOCS_URL;
4
5use gpui::DismissEvent;
6
7use picker::Picker;
8use picker::PickerDelegate;
9
10use std::sync::Arc;
11use ui::ListItemSpacing;
12
13use gpui::SharedString;
14use gpui::Task;
15use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
16
17type OnSelect = Box<dyn Fn(KernelSpecification, &mut WindowContext)>;
18
19#[derive(IntoElement)]
20pub struct KernelSelector<T: PopoverTrigger> {
21 handle: Option<PopoverMenuHandle<Picker<KernelPickerDelegate>>>,
22 on_select: OnSelect,
23 trigger: T,
24 info_text: Option<SharedString>,
25 current_kernelspec: Option<KernelSpecification>,
26}
27
28pub struct KernelPickerDelegate {
29 all_kernels: Vec<KernelSpecification>,
30 filtered_kernels: Vec<KernelSpecification>,
31 selected_kernelspec: Option<KernelSpecification>,
32 on_select: OnSelect,
33}
34
35impl<T: PopoverTrigger> KernelSelector<T> {
36 pub fn new(
37 on_select: OnSelect,
38 current_kernelspec: Option<KernelSpecification>,
39 trigger: T,
40 ) -> Self {
41 KernelSelector {
42 on_select,
43 handle: None,
44 trigger,
45 info_text: None,
46 current_kernelspec,
47 }
48 }
49
50 pub fn with_handle(mut self, handle: PopoverMenuHandle<Picker<KernelPickerDelegate>>) -> Self {
51 self.handle = Some(handle);
52 self
53 }
54
55 pub fn with_info_text(mut self, text: impl Into<SharedString>) -> Self {
56 self.info_text = Some(text.into());
57 self
58 }
59}
60
61impl PickerDelegate for KernelPickerDelegate {
62 type ListItem = ListItem;
63
64 fn match_count(&self) -> usize {
65 self.filtered_kernels.len()
66 }
67
68 fn selected_index(&self) -> usize {
69 if let Some(kernelspec) = self.selected_kernelspec.as_ref() {
70 self.filtered_kernels
71 .iter()
72 .position(|k| k == kernelspec)
73 .unwrap_or(0)
74 } else {
75 0
76 }
77 }
78
79 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
80 self.selected_kernelspec = self.filtered_kernels.get(ix).cloned();
81 cx.notify();
82 }
83
84 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
85 "Select a kernel...".into()
86 }
87
88 fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
89 let all_kernels = self.all_kernels.clone();
90
91 if query.is_empty() {
92 self.filtered_kernels = all_kernels;
93 return Task::Ready(Some(()));
94 }
95
96 self.filtered_kernels = if query.is_empty() {
97 all_kernels
98 } else {
99 all_kernels
100 .into_iter()
101 .filter(|kernel| kernel.name().to_lowercase().contains(&query.to_lowercase()))
102 .collect()
103 };
104
105 return Task::Ready(Some(()));
106 }
107
108 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
109 if let Some(kernelspec) = &self.selected_kernelspec {
110 (self.on_select)(kernelspec.clone(), cx.window_context());
111 cx.emit(DismissEvent);
112 }
113 }
114
115 fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
116
117 fn render_match(
118 &self,
119 ix: usize,
120 selected: bool,
121 _cx: &mut ViewContext<Picker<Self>>,
122 ) -> Option<Self::ListItem> {
123 let kernelspec = self.filtered_kernels.get(ix)?;
124
125 let is_selected = self.selected_kernelspec.as_ref() == Some(kernelspec);
126
127 Some(
128 ListItem::new(ix)
129 .inset(true)
130 .spacing(ListItemSpacing::Sparse)
131 .selected(selected)
132 .child(
133 h_flex().w_full().justify_between().min_w(px(200.)).child(
134 h_flex()
135 .gap_1p5()
136 .child(Label::new(kernelspec.name()))
137 .child(
138 Label::new(kernelspec.type_name())
139 .size(LabelSize::XSmall)
140 .color(Color::Muted),
141 ),
142 ),
143 )
144 .end_slot(div().when(is_selected, |this| {
145 this.child(
146 Icon::new(IconName::Check)
147 .color(Color::Accent)
148 .size(IconSize::Small),
149 )
150 })),
151 )
152 }
153
154 fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
155 Some(
156 h_flex()
157 .w_full()
158 .border_t_1()
159 .border_color(cx.theme().colors().border_variant)
160 .p_1()
161 .gap_4()
162 .child(
163 Button::new("kernel-docs", "Kernel Docs")
164 .icon(IconName::ExternalLink)
165 .icon_size(IconSize::XSmall)
166 .icon_color(Color::Muted)
167 .icon_position(IconPosition::End)
168 .on_click(move |_, cx| cx.open_url(KERNEL_DOCS_URL)),
169 )
170 .into_any(),
171 )
172 }
173}
174
175impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
176 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
177 let store = ReplStore::global(cx).read(cx);
178 let all_kernels: Vec<KernelSpecification> =
179 store.kernel_specifications().cloned().collect();
180
181 let selected_kernelspec = self.current_kernelspec;
182
183 let delegate = KernelPickerDelegate {
184 on_select: self.on_select,
185 all_kernels: all_kernels.clone(),
186 filtered_kernels: all_kernels,
187 selected_kernelspec,
188 };
189
190 let picker_view = cx.new_view(|cx| {
191 let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
192 picker
193 });
194
195 PopoverMenu::new("kernel-switcher")
196 .menu(move |_cx| Some(picker_view.clone()))
197 .trigger(self.trigger)
198 .attach(gpui::AnchorCorner::BottomLeft)
199 .when_some(self.handle, |menu, handle| menu.with_handle(handle))
200 }
201}