Skip to repository content454 lines · 17.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:33:31.594Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
render.rs
1use gpui::{KeyContext, canvas};
2use settings::Settings;
3use theme_settings::ThemeSettings;
4use ui::{
5 ActiveTheme, Color, Context, Disableable, DocumentationAside, DocumentationSide, FluentBuilder,
6 InteractiveElement, IntoElement, Label, LabelCommon, ListItem, ListItemSpacing, ParentElement,
7 Render, ScrollAxes, Scrollbars, Styled, StyledExt, Window, WithScrollbar, div, h_flex,
8 rems_from_px, utils::WithRemSize, v_flex,
9};
10
11use crate::shape::Shape;
12use crate::{
13 ElementContainer, Picker, PickerDelegate, PickerEditorPosition, Preview, ToggleMultiSelect,
14 head::Head,
15 preview::Layout,
16 render::window_controls::{Bottom, Left, LeftCorner, Middle, Right, RightCorner},
17};
18use crate::{persistence, preview};
19use gpui::Action as _;
20use gpui::Focusable as _;
21use std::sync::Arc;
22use ui::{Divider, Tooltip, prelude::*};
23use ui_input::ErasedEditor;
24
25pub mod window_controls;
26
27impl<D: PickerDelegate> Render for Picker<D> {
28 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
29 self.finish_any_completed_resize(window, cx);
30 // toggle between BelowForced and Right based on whether it'd clamp if
31 // horizontal
32 let rendered_layout = self.preview_layout_rendered(window);
33 let content = match &self.preview {
34 Some(
35 preview @ Preview {
36 layout: Layout::Below,
37 ..
38 },
39 ) => self
40 .render_with_preview_below(preview, window, cx)
41 .into_any_element(),
42 // render sideways based on bounds
43 Some(
44 preview @ Preview {
45 layout: Layout::Right,
46 ..
47 },
48 ) if rendered_layout == Some(Layout::Below) => self
49 .render_with_preview_below(preview, window, cx)
50 .into_any_element(),
51 Some(
52 preview @ Preview {
53 layout: Layout::Right,
54 ..
55 },
56 ) => self
57 .render_with_preview_right(preview, window, cx)
58 .into_any_element(),
59 Some(Preview {
60 layout: Layout::Hidden,
61 ..
62 })
63 | None => self.render_results(window, cx).into_any_element(),
64 };
65
66 // The border, background and rounding wrap the whole picker (results and
67 // preview together). When there's a preview, clip the contents so its
68 // corners follow the rounded border. Avoid clipping otherwise, so a
69 // documentation aside (which is positioned outside the picker) isn't cut
70 // off.
71 let has_preview = self.preview.is_some();
72 let content = div()
73 .when(self.draws_own_container(), |this| this.elevation_3(cx))
74 .when(has_preview, |this| this.overflow_hidden())
75 .child(content);
76
77 let layout = self
78 .preview_layout_rendered(window)
79 .unwrap_or(Layout::Hidden);
80
81 div()
82 .relative()
83 .child(content)
84 .when(self.is_resizable(), |this| {
85 this.left(self.shape.horizontal_offset(window))
86 .child(self.render_resize(Left, window, cx))
87 .child(self.render_resize(Right(layout), window, cx))
88 .child(self.render_resize(Bottom(layout), window, cx))
89 .child(self.render_resize(LeftCorner(layout), window, cx))
90 .child(self.render_resize(RightCorner(layout), window, cx))
91 })
92 }
93}
94
95impl<D: PickerDelegate> Picker<D> {
96 fn render_editor(
97 &self,
98 editor: &Arc<dyn ErasedEditor>,
99 window: &mut Window,
100 cx: &mut Context<Self>,
101 ) -> gpui::Div {
102 if let Some(custom) = self.delegate.render_editor(editor, window, cx) {
103 return custom;
104 }
105 let editor_position = self.delegate.editor_position();
106
107 v_flex()
108 .when(editor_position == PickerEditorPosition::End, |this| {
109 this.child(Divider::horizontal())
110 })
111 .child(
112 h_flex()
113 .h_9()
114 .px_2p5()
115 .flex_none()
116 .overflow_hidden()
117 .child(div().flex_1().child(editor.render(window, cx)))
118 .children(self.delegate.searchbar_trailer(window, cx))
119 .when(self.delegate.supports_multi_select(), |this| {
120 this.child(self.render_multi_select_toggle(cx))
121 }),
122 )
123 .when(editor_position == PickerEditorPosition::Start, |this| {
124 this.child(Divider::horizontal())
125 })
126 }
127
128 /// The multi-select toggle is picker-owned so it can reflect the mode,
129 /// which delegates don't know about.
130 fn render_multi_select_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
131 let active = self.select_instead_of_open;
132 let focus_handle = self.focus_handle(cx);
133 IconButton::new("picker-multi-select-toggle", IconName::FileMultiple)
134 .icon_size(IconSize::Small)
135 .toggle_state(active)
136 .tooltip(move |_window, cx| {
137 Tooltip::for_action_in("Toggle Multi Select", &ToggleMultiSelect, &focus_handle, cx)
138 })
139 .on_click(cx.listener(|_, _, window, cx| {
140 window.dispatch_action(ToggleMultiSelect.boxed_clone(), cx);
141 }))
142 }
143
144 pub(crate) fn render_results(
145 &self,
146 window: &mut Window,
147 cx: &mut Context<Self>,
148 ) -> impl IntoElement {
149 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
150 let window_size = window.viewport_size();
151 let rem_size = window.rem_size();
152 let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0;
153
154 let aside = self.delegate.documentation_aside(window, cx);
155
156 let editor_position = self.delegate.editor_position();
157 let picker_bounds = self.picker_bounds.clone();
158
159 let mut key_context = KeyContext::default();
160 key_context.add("Picker");
161 if self.preview.is_some() {
162 key_context.add("with_preview");
163 }
164
165 let menu = v_flex()
166 .key_context(key_context)
167 .relative()
168 .map(|this| {
169 self.shape.apply_results_size(
170 self.preview_layout_rendered(window),
171 &self.size_bounds,
172 self.fill_height(),
173 this,
174 window,
175 )
176 })
177 .child(
178 canvas(
179 move |bounds, _window, _cx| {
180 picker_bounds.set(Some(bounds));
181 },
182 |_bounds, _state, _window, _cx| {},
183 )
184 .size_full()
185 .absolute()
186 .top_0()
187 .left_0(),
188 )
189 .on_action(cx.listener(Self::select_next))
190 .on_action(cx.listener(Self::select_previous))
191 .on_action(cx.listener(Self::editor_move_down))
192 .on_action(cx.listener(Self::editor_move_up))
193 .on_action(cx.listener(Self::select_first))
194 .on_action(cx.listener(Self::select_last))
195 .on_action(cx.listener(Self::select_child))
196 .on_action(cx.listener(Self::select_parent))
197 .on_action(cx.listener(Self::cancel))
198 .on_action(cx.listener(Self::confirm))
199 .on_action(cx.listener(Self::secondary_confirm))
200 .on_action(cx.listener(Self::confirm_completion))
201 .on_action(cx.listener(Self::confirm_input))
202 .on_action(cx.listener(Self::toggle_preview))
203 .on_action(cx.listener(Self::set_preview_right))
204 .on_action(cx.listener(Self::set_preview_below))
205 .on_action(cx.listener(Self::set_preview_hidden))
206 .on_action(cx.listener(Self::toggle_actions_menu))
207 .on_action(cx.listener(Self::toggle_multi_select))
208 .on_action(cx.listener(Self::multi_select_next))
209 .children(match &self.head {
210 Head::Editor(editor) => {
211 if editor_position == PickerEditorPosition::Start {
212 let editor = editor.clone();
213 Some(self.render_editor(&editor, window, cx))
214 } else {
215 None
216 }
217 }
218 Head::Empty(empty_head) => Some(h_flex().child(empty_head.clone())),
219 })
220 .when(self.delegate.match_count() > 0, |el| {
221 el.child(
222 v_flex()
223 .id("element-container")
224 .relative()
225 .flex_grow_1()
226 .min_h_0()
227 .when_some(
228 self.shape.results_max_height(
229 &self.size_bounds,
230 self.fill_height(),
231 window,
232 ),
233 |this, max_height| this.max_h(max_height),
234 )
235 .overflow_hidden()
236 .children(self.delegate.render_header(window, cx))
237 .child(self.render_element_container(cx))
238 .when(self.show_scrollbar, |this| {
239 let base_scrollbar_config = Scrollbars::new(ScrollAxes::Vertical);
240
241 this.map(|this| match &self.element_container {
242 ElementContainer::List(state) => this.custom_scrollbars(
243 base_scrollbar_config.tracked_scroll_handle(state),
244 window,
245 cx,
246 ),
247 ElementContainer::UniformList(state) => this.custom_scrollbars(
248 base_scrollbar_config.tracked_scroll_handle(state),
249 window,
250 cx,
251 ),
252 })
253 }),
254 )
255 })
256 .when(self.delegate.match_count() == 0, |el| {
257 el.when_some(self.delegate.no_matches_text(window, cx), |el, text| {
258 el.child(
259 v_flex().flex_grow_1().py_2().child(
260 ListItem::new("empty_state")
261 .inset(true)
262 .spacing(ListItemSpacing::Sparse)
263 .disabled(true)
264 .child(Label::new(text).color(Color::Muted)),
265 ),
266 )
267 })
268 })
269 .children(self.render_footer(window, cx))
270 .children(match &self.head {
271 Head::Editor(editor) => {
272 if editor_position == PickerEditorPosition::End {
273 let editor = editor.clone();
274 Some(self.render_editor(&editor, window, cx))
275 } else {
276 None
277 }
278 }
279 Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
280 });
281
282 let Some(aside) = aside else {
283 return menu;
284 };
285
286 let render_aside = |aside: DocumentationAside, cx: &mut Context<Self>| {
287 WithRemSize::new(ui_font_size)
288 .occlude()
289 .elevation_2(cx)
290 .w_full()
291 .p_2()
292 .overflow_hidden()
293 .when(is_wide_window, |this| this.max_w_96())
294 .when(!is_wide_window, |this| this.max_w_48())
295 .child((aside.render)(cx))
296 };
297
298 if is_wide_window {
299 let aside_index = self.delegate.documentation_aside_index();
300 let picker_bounds = self.picker_bounds.get();
301 let item_bounds =
302 aside_index.and_then(|ix| self.item_bounds.borrow().get(&ix).copied());
303
304 let item_position = match (picker_bounds, item_bounds) {
305 (Some(picker_bounds), Some(item_bounds)) => {
306 let relative_top = item_bounds.origin.y - picker_bounds.origin.y;
307 let height = item_bounds.size.height;
308 Some((relative_top, height))
309 }
310 _ => None,
311 };
312
313 div()
314 .relative()
315 .child(menu)
316 // Only render the aside once we have bounds to avoid flicker
317 .when_some(item_position, |this, (top, height)| {
318 this.child(
319 h_flex()
320 .absolute()
321 .when(aside.side == DocumentationSide::Left, |el| {
322 el.right_full().mr_1()
323 })
324 .when(aside.side == DocumentationSide::Right, |el| {
325 el.left_full().ml_1()
326 })
327 .top(top)
328 .h(height)
329 .child(render_aside(aside, cx)),
330 )
331 })
332 } else {
333 v_flex()
334 .w_full()
335 .gap_1()
336 .justify_end()
337 .child(render_aside(aside, cx))
338 .child(menu)
339 }
340 }
341
342 fn render_with_preview_below(
343 &self,
344 preview: &Preview,
345 window: &mut Window,
346 cx: &mut Context<Self>,
347 ) -> impl IntoElement {
348 h_flex()
349 .relative()
350 .child(
351 v_flex()
352 .h(self
353 .shape
354 .height(Some(preview::Layout::Below), &self.size_bounds, window))
355 .child(
356 div()
357 .h(self.shape.results_height(
358 preview::Layout::Below,
359 &self.size_bounds,
360 window,
361 ))
362 .overflow_hidden()
363 .child(self.render_results(window, cx)),
364 )
365 .child(
366 div()
367 .h(self.shape.preview_height(
368 preview::Layout::Below,
369 &self.size_bounds,
370 window,
371 ))
372 .border_t_1()
373 .border_color(cx.theme().colors().border_variant)
374 .child(preview.render(cx)),
375 ),
376 )
377 .when(self.is_resizable(), |this| {
378 this.child(self.render_resize(
379 window_controls::Middle(preview::Layout::Below),
380 window,
381 cx,
382 ))
383 })
384 }
385
386 pub(crate) fn render_with_preview_right(
387 &self,
388 preview: &Preview,
389 window: &mut Window,
390 cx: &mut Context<Self>,
391 ) -> impl IntoElement {
392 v_flex()
393 .relative()
394 .child(
395 h_flex()
396 .h(self
397 .shape
398 .height(Some(Layout::Right), &self.size_bounds, window))
399 .child(
400 div()
401 .flex_1()
402 .h_full()
403 .overflow_hidden()
404 .child(self.render_results(window, cx)),
405 )
406 .child(
407 div()
408 .w(self
409 .shape
410 .preview_width(Layout::Right, &self.size_bounds, window))
411 .map(|this| {
412 self.shape.apply_height(
413 Some(Layout::Right),
414 &self.size_bounds,
415 this,
416 window,
417 )
418 })
419 .border_l_1()
420 .border_color(cx.theme().colors().border_variant)
421 .overflow_hidden()
422 .child(preview.render(cx)),
423 ),
424 )
425 .when(self.is_resizable(), |this| {
426 this.child(self.render_resize(Middle(Layout::Right), window, cx))
427 })
428 }
429
430 fn finish_any_completed_resize(
431 &mut self,
432 window: &mut Window,
433 cx: &mut Context<'_, Picker<D>>,
434 ) {
435 if let Shape::Resizing(pos) = self.shape
436 && !cx.has_active_drag()
437 {
438 let centered =
439 Shape::centered_and_relative(pos, self.preview_layout_rendered(window), window);
440 persistence::store_shape_for_this_layout(
441 D::name(),
442 self.preview_layout(),
443 centered,
444 window,
445 cx,
446 );
447 self.shape = Shape::HorizontallyCentered(centered);
448 if let Some(preview) = &mut self.preview {
449 preview.adjust_to_new_size(window, cx);
450 }
451 }
452 }
453}
454