While building Zed with nightly rustc I've noticed it doesn't compile because of good old pathfinder_simd. It also emits a bunch of warnings about use of f64 literals where f32 is expected, so I've fixed them - it should make future upgrades more straightforward.
454 lines
17 KiB
Rust
454 lines
17 KiB
Rust
use gpui::{KeyContext, canvas};
|
|
use settings::Settings;
|
|
use theme_settings::ThemeSettings;
|
|
use ui::{
|
|
ActiveTheme, Color, Context, Disableable, DocumentationAside, DocumentationSide, FluentBuilder,
|
|
InteractiveElement, IntoElement, Label, LabelCommon, ListItem, ListItemSpacing, ParentElement,
|
|
Render, ScrollAxes, Scrollbars, Styled, StyledExt, Window, WithScrollbar, div, h_flex,
|
|
rems_from_px, utils::WithRemSize, v_flex,
|
|
};
|
|
|
|
use crate::shape::Shape;
|
|
use crate::{
|
|
ElementContainer, Picker, PickerDelegate, PickerEditorPosition, Preview, ToggleMultiSelect,
|
|
head::Head,
|
|
preview::Layout,
|
|
render::window_controls::{Bottom, Left, LeftCorner, Middle, Right, RightCorner},
|
|
};
|
|
use crate::{persistence, preview};
|
|
use gpui::Action as _;
|
|
use gpui::Focusable as _;
|
|
use std::sync::Arc;
|
|
use ui::{Divider, Tooltip, prelude::*};
|
|
use ui_input::ErasedEditor;
|
|
|
|
pub mod window_controls;
|
|
|
|
impl<D: PickerDelegate> Render for Picker<D> {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
self.finish_any_completed_resize(window, cx);
|
|
// toggle between BelowForced and Right based on whether it'd clamp if
|
|
// horizontal
|
|
let rendered_layout = self.preview_layout_rendered(window);
|
|
let content = match &self.preview {
|
|
Some(
|
|
preview @ Preview {
|
|
layout: Layout::Below,
|
|
..
|
|
},
|
|
) => self
|
|
.render_with_preview_below(preview, window, cx)
|
|
.into_any_element(),
|
|
// render sideways based on bounds
|
|
Some(
|
|
preview @ Preview {
|
|
layout: Layout::Right,
|
|
..
|
|
},
|
|
) if rendered_layout == Some(Layout::Below) => self
|
|
.render_with_preview_below(preview, window, cx)
|
|
.into_any_element(),
|
|
Some(
|
|
preview @ Preview {
|
|
layout: Layout::Right,
|
|
..
|
|
},
|
|
) => self
|
|
.render_with_preview_right(preview, window, cx)
|
|
.into_any_element(),
|
|
Some(Preview {
|
|
layout: Layout::Hidden,
|
|
..
|
|
})
|
|
| None => self.render_results(window, cx).into_any_element(),
|
|
};
|
|
|
|
// The border, background and rounding wrap the whole picker (results and
|
|
// preview together). When there's a preview, clip the contents so its
|
|
// corners follow the rounded border. Avoid clipping otherwise, so a
|
|
// documentation aside (which is positioned outside the picker) isn't cut
|
|
// off.
|
|
let has_preview = self.preview.is_some();
|
|
let content = div()
|
|
.when(self.draws_own_container(), |this| this.elevation_3(cx))
|
|
.when(has_preview, |this| this.overflow_hidden())
|
|
.child(content);
|
|
|
|
let layout = self
|
|
.preview_layout_rendered(window)
|
|
.unwrap_or(Layout::Hidden);
|
|
|
|
div()
|
|
.relative()
|
|
.child(content)
|
|
.when(self.is_resizable(), |this| {
|
|
this.left(self.shape.horizontal_offset(window))
|
|
.child(self.render_resize(Left, window, cx))
|
|
.child(self.render_resize(Right(layout), window, cx))
|
|
.child(self.render_resize(Bottom(layout), window, cx))
|
|
.child(self.render_resize(LeftCorner(layout), window, cx))
|
|
.child(self.render_resize(RightCorner(layout), window, cx))
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<D: PickerDelegate> Picker<D> {
|
|
fn render_editor(
|
|
&self,
|
|
editor: &Arc<dyn ErasedEditor>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> gpui::Div {
|
|
if let Some(custom) = self.delegate.render_editor(editor, window, cx) {
|
|
return custom;
|
|
}
|
|
let editor_position = self.delegate.editor_position();
|
|
|
|
v_flex()
|
|
.when(editor_position == PickerEditorPosition::End, |this| {
|
|
this.child(Divider::horizontal())
|
|
})
|
|
.child(
|
|
h_flex()
|
|
.h_9()
|
|
.px_2p5()
|
|
.flex_none()
|
|
.overflow_hidden()
|
|
.child(div().flex_1().child(editor.render(window, cx)))
|
|
.children(self.delegate.searchbar_trailer(window, cx))
|
|
.when(self.delegate.supports_multi_select(), |this| {
|
|
this.child(self.render_multi_select_toggle(cx))
|
|
}),
|
|
)
|
|
.when(editor_position == PickerEditorPosition::Start, |this| {
|
|
this.child(Divider::horizontal())
|
|
})
|
|
}
|
|
|
|
/// The multi-select toggle is picker-owned so it can reflect the mode,
|
|
/// which delegates don't know about.
|
|
fn render_multi_select_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let active = self.select_instead_of_open;
|
|
let focus_handle = self.focus_handle(cx);
|
|
IconButton::new("picker-multi-select-toggle", IconName::FileMultiple)
|
|
.icon_size(IconSize::Small)
|
|
.toggle_state(active)
|
|
.tooltip(move |_window, cx| {
|
|
Tooltip::for_action_in("Toggle Multi Select", &ToggleMultiSelect, &focus_handle, cx)
|
|
})
|
|
.on_click(cx.listener(|_, _, window, cx| {
|
|
window.dispatch_action(ToggleMultiSelect.boxed_clone(), cx);
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn render_results(
|
|
&self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> impl IntoElement {
|
|
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
|
|
let window_size = window.viewport_size();
|
|
let rem_size = window.rem_size();
|
|
let is_wide_window = window_size.width / rem_size > rems_from_px(800_f32).0;
|
|
|
|
let aside = self.delegate.documentation_aside(window, cx);
|
|
|
|
let editor_position = self.delegate.editor_position();
|
|
let picker_bounds = self.picker_bounds.clone();
|
|
|
|
let mut key_context = KeyContext::default();
|
|
key_context.add("Picker");
|
|
if self.preview.is_some() {
|
|
key_context.add("with_preview");
|
|
}
|
|
|
|
let menu = v_flex()
|
|
.key_context(key_context)
|
|
.relative()
|
|
.map(|this| {
|
|
self.shape.apply_results_size(
|
|
self.preview_layout_rendered(window),
|
|
&self.size_bounds,
|
|
self.fill_height(),
|
|
this,
|
|
window,
|
|
)
|
|
})
|
|
.child(
|
|
canvas(
|
|
move |bounds, _window, _cx| {
|
|
picker_bounds.set(Some(bounds));
|
|
},
|
|
|_bounds, _state, _window, _cx| {},
|
|
)
|
|
.size_full()
|
|
.absolute()
|
|
.top_0()
|
|
.left_0(),
|
|
)
|
|
.on_action(cx.listener(Self::select_next))
|
|
.on_action(cx.listener(Self::select_previous))
|
|
.on_action(cx.listener(Self::editor_move_down))
|
|
.on_action(cx.listener(Self::editor_move_up))
|
|
.on_action(cx.listener(Self::select_first))
|
|
.on_action(cx.listener(Self::select_last))
|
|
.on_action(cx.listener(Self::select_child))
|
|
.on_action(cx.listener(Self::select_parent))
|
|
.on_action(cx.listener(Self::cancel))
|
|
.on_action(cx.listener(Self::confirm))
|
|
.on_action(cx.listener(Self::secondary_confirm))
|
|
.on_action(cx.listener(Self::confirm_completion))
|
|
.on_action(cx.listener(Self::confirm_input))
|
|
.on_action(cx.listener(Self::toggle_preview))
|
|
.on_action(cx.listener(Self::set_preview_right))
|
|
.on_action(cx.listener(Self::set_preview_below))
|
|
.on_action(cx.listener(Self::set_preview_hidden))
|
|
.on_action(cx.listener(Self::toggle_actions_menu))
|
|
.on_action(cx.listener(Self::toggle_multi_select))
|
|
.on_action(cx.listener(Self::multi_select_next))
|
|
.children(match &self.head {
|
|
Head::Editor(editor) => {
|
|
if editor_position == PickerEditorPosition::Start {
|
|
let editor = editor.clone();
|
|
Some(self.render_editor(&editor, window, cx))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
Head::Empty(empty_head) => Some(h_flex().child(empty_head.clone())),
|
|
})
|
|
.when(self.delegate.match_count() > 0, |el| {
|
|
el.child(
|
|
v_flex()
|
|
.id("element-container")
|
|
.relative()
|
|
.flex_grow_1()
|
|
.min_h_0()
|
|
.when_some(
|
|
self.shape.results_max_height(
|
|
&self.size_bounds,
|
|
self.fill_height(),
|
|
window,
|
|
),
|
|
|this, max_height| this.max_h(max_height),
|
|
)
|
|
.overflow_hidden()
|
|
.children(self.delegate.render_header(window, cx))
|
|
.child(self.render_element_container(cx))
|
|
.when(self.show_scrollbar, |this| {
|
|
let base_scrollbar_config = Scrollbars::new(ScrollAxes::Vertical);
|
|
|
|
this.map(|this| match &self.element_container {
|
|
ElementContainer::List(state) => this.custom_scrollbars(
|
|
base_scrollbar_config.tracked_scroll_handle(state),
|
|
window,
|
|
cx,
|
|
),
|
|
ElementContainer::UniformList(state) => this.custom_scrollbars(
|
|
base_scrollbar_config.tracked_scroll_handle(state),
|
|
window,
|
|
cx,
|
|
),
|
|
})
|
|
}),
|
|
)
|
|
})
|
|
.when(self.delegate.match_count() == 0, |el| {
|
|
el.when_some(self.delegate.no_matches_text(window, cx), |el, text| {
|
|
el.child(
|
|
v_flex().flex_grow_1().py_2().child(
|
|
ListItem::new("empty_state")
|
|
.inset(true)
|
|
.spacing(ListItemSpacing::Sparse)
|
|
.disabled(true)
|
|
.child(Label::new(text).color(Color::Muted)),
|
|
),
|
|
)
|
|
})
|
|
})
|
|
.children(self.render_footer(window, cx))
|
|
.children(match &self.head {
|
|
Head::Editor(editor) => {
|
|
if editor_position == PickerEditorPosition::End {
|
|
let editor = editor.clone();
|
|
Some(self.render_editor(&editor, window, cx))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
|
|
});
|
|
|
|
let Some(aside) = aside else {
|
|
return menu;
|
|
};
|
|
|
|
let render_aside = |aside: DocumentationAside, cx: &mut Context<Self>| {
|
|
WithRemSize::new(ui_font_size)
|
|
.occlude()
|
|
.elevation_2(cx)
|
|
.w_full()
|
|
.p_2()
|
|
.overflow_hidden()
|
|
.when(is_wide_window, |this| this.max_w_96())
|
|
.when(!is_wide_window, |this| this.max_w_48())
|
|
.child((aside.render)(cx))
|
|
};
|
|
|
|
if is_wide_window {
|
|
let aside_index = self.delegate.documentation_aside_index();
|
|
let picker_bounds = self.picker_bounds.get();
|
|
let item_bounds =
|
|
aside_index.and_then(|ix| self.item_bounds.borrow().get(&ix).copied());
|
|
|
|
let item_position = match (picker_bounds, item_bounds) {
|
|
(Some(picker_bounds), Some(item_bounds)) => {
|
|
let relative_top = item_bounds.origin.y - picker_bounds.origin.y;
|
|
let height = item_bounds.size.height;
|
|
Some((relative_top, height))
|
|
}
|
|
_ => None,
|
|
};
|
|
|
|
div()
|
|
.relative()
|
|
.child(menu)
|
|
// Only render the aside once we have bounds to avoid flicker
|
|
.when_some(item_position, |this, (top, height)| {
|
|
this.child(
|
|
h_flex()
|
|
.absolute()
|
|
.when(aside.side == DocumentationSide::Left, |el| {
|
|
el.right_full().mr_1()
|
|
})
|
|
.when(aside.side == DocumentationSide::Right, |el| {
|
|
el.left_full().ml_1()
|
|
})
|
|
.top(top)
|
|
.h(height)
|
|
.child(render_aside(aside, cx)),
|
|
)
|
|
})
|
|
} else {
|
|
v_flex()
|
|
.w_full()
|
|
.gap_1()
|
|
.justify_end()
|
|
.child(render_aside(aside, cx))
|
|
.child(menu)
|
|
}
|
|
}
|
|
|
|
fn render_with_preview_below(
|
|
&self,
|
|
preview: &Preview,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> impl IntoElement {
|
|
h_flex()
|
|
.relative()
|
|
.child(
|
|
v_flex()
|
|
.h(self
|
|
.shape
|
|
.height(Some(preview::Layout::Below), &self.size_bounds, window))
|
|
.child(
|
|
div()
|
|
.h(self.shape.results_height(
|
|
preview::Layout::Below,
|
|
&self.size_bounds,
|
|
window,
|
|
))
|
|
.overflow_hidden()
|
|
.child(self.render_results(window, cx)),
|
|
)
|
|
.child(
|
|
div()
|
|
.h(self.shape.preview_height(
|
|
preview::Layout::Below,
|
|
&self.size_bounds,
|
|
window,
|
|
))
|
|
.border_t_1()
|
|
.border_color(cx.theme().colors().border_variant)
|
|
.child(preview.render(cx)),
|
|
),
|
|
)
|
|
.when(self.is_resizable(), |this| {
|
|
this.child(self.render_resize(
|
|
window_controls::Middle(preview::Layout::Below),
|
|
window,
|
|
cx,
|
|
))
|
|
})
|
|
}
|
|
|
|
pub(crate) fn render_with_preview_right(
|
|
&self,
|
|
preview: &Preview,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> impl IntoElement {
|
|
v_flex()
|
|
.relative()
|
|
.child(
|
|
h_flex()
|
|
.h(self
|
|
.shape
|
|
.height(Some(Layout::Right), &self.size_bounds, window))
|
|
.child(
|
|
div()
|
|
.flex_1()
|
|
.h_full()
|
|
.overflow_hidden()
|
|
.child(self.render_results(window, cx)),
|
|
)
|
|
.child(
|
|
div()
|
|
.w(self
|
|
.shape
|
|
.preview_width(Layout::Right, &self.size_bounds, window))
|
|
.map(|this| {
|
|
self.shape.apply_height(
|
|
Some(Layout::Right),
|
|
&self.size_bounds,
|
|
this,
|
|
window,
|
|
)
|
|
})
|
|
.border_l_1()
|
|
.border_color(cx.theme().colors().border_variant)
|
|
.overflow_hidden()
|
|
.child(preview.render(cx)),
|
|
),
|
|
)
|
|
.when(self.is_resizable(), |this| {
|
|
this.child(self.render_resize(Middle(Layout::Right), window, cx))
|
|
})
|
|
}
|
|
|
|
fn finish_any_completed_resize(
|
|
&mut self,
|
|
window: &mut Window,
|
|
cx: &mut Context<'_, Picker<D>>,
|
|
) {
|
|
if let Shape::Resizing(pos) = self.shape
|
|
&& !cx.has_active_drag()
|
|
{
|
|
let centered =
|
|
Shape::centered_and_relative(pos, self.preview_layout_rendered(window), window);
|
|
persistence::store_shape_for_this_layout(
|
|
D::name(),
|
|
self.preview_layout(),
|
|
centered,
|
|
window,
|
|
cx,
|
|
);
|
|
self.shape = Shape::HorizontallyCentered(centered);
|
|
if let Some(preview) = &mut self.preview {
|
|
preview.adjust_to_new_size(window, cx);
|
|
}
|
|
}
|
|
}
|
|
}
|