@@ -24,8 +24,8 @@ use std::{
sync::{Arc, atomic::AtomicBool},
};
use ui::{
- ContextMenu, Divider, DropdownMenu, DropdownStyle, Switch, SwitchColor, TreeViewItem,
- prelude::*,
+ ContextMenu, Divider, DropdownMenu, DropdownStyle, IconButtonShape, Switch, SwitchColor,
+ TreeViewItem, WithScrollbar, prelude::*,
};
use ui_input::{NumericStepper, NumericStepperType};
use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
@@ -2896,6 +2896,7 @@ pub struct SettingsWindow {
/// If this is empty the selected page is rendered,
/// otherwise the last sub page gets rendered.
sub_page_stack: Vec<SubPage>,
+ scroll_handle: ScrollHandle,
}
struct SubPage {
@@ -2970,10 +2971,14 @@ impl SettingsPageItem {
.gap_2()
.flex_wrap()
.justify_between()
- .when(!is_last, |this| {
- this.pb_4()
- .border_b_1()
- .border_color(cx.theme().colors().border_variant)
+ .map(|this| {
+ if is_last {
+ this.pb_6()
+ } else {
+ this.pb_4()
+ .border_b_1()
+ .border_color(cx.theme().colors().border_variant)
+ }
})
.child(
v_flex()
@@ -2983,10 +2988,7 @@ impl SettingsPageItem {
h_flex()
.w_full()
.gap_4()
- .child(
- Label::new(SharedString::new_static(setting_item.title))
- .size(LabelSize::Default),
- )
+ .child(Label::new(SharedString::new_static(setting_item.title)))
.when_some(
file_set_in.filter(|file_set_in| file_set_in != &file),
|elem, file_set_in| {
@@ -3027,15 +3029,18 @@ impl SettingsPageItem {
.border_color(cx.theme().colors().border_variant)
})
.child(
- v_flex().max_w_1_2().flex_shrink().child(
- Label::new(SharedString::new_static(sub_page_link.title))
- .size(LabelSize::Default),
- ),
+ v_flex()
+ .max_w_1_2()
+ .flex_shrink()
+ .child(Label::new(SharedString::new_static(sub_page_link.title))),
)
.child(
Button::new(("sub-page".into(), sub_page_link.title), "Configure")
- .icon(Some(IconName::ChevronRight))
- .icon_position(Some(IconPosition::End))
+ .size(ButtonSize::Medium)
+ .icon(IconName::ChevronRight)
+ .icon_position(IconPosition::End)
+ .icon_color(Color::Muted)
+ .icon_size(IconSize::Small)
.style(ButtonStyle::Outlined),
)
.on_click({
@@ -3161,6 +3166,7 @@ impl SettingsWindow {
search_task: None,
search_matches: vec![],
sub_page_stack: vec![],
+ scroll_handle: ScrollHandle::new(),
};
this.fetch_files(cx);
@@ -3487,22 +3493,27 @@ impl SettingsWindow {
.child(Label::new(last))
}
- fn render_page(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) -> Div {
+ fn render_page(
+ &mut self,
+ window: &mut Window,
+ cx: &mut Context<SettingsWindow>,
+ ) -> impl IntoElement {
let mut page = v_flex()
.w_full()
.pt_4()
+ .pb_6()
.px_6()
.gap_4()
- .bg(cx.theme().colors().editor_background);
+ .bg(cx.theme().colors().editor_background)
+ .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx);
+
let mut page_content = v_flex()
.id("settings-ui-page")
+ .size_full()
.gap_4()
.overflow_y_scroll()
- .track_scroll(
- window
- .use_state(cx, |_, _| ScrollHandle::default())
- .read(cx),
- );
+ .track_scroll(&self.scroll_handle);
+
if self.sub_page_stack.len() == 0 {
page = page.child(self.render_files(window, cx));
@@ -3510,29 +3521,69 @@ impl SettingsWindow {
let items_len = items.len();
let mut section_header = None;
- page_content =
- page_content.children(items.into_iter().enumerate().map(|(index, item)| {
- let is_last = index == items_len - 1;
- if let SettingsPageItem::SectionHeader(header) = item {
- section_header = Some(*header);
- }
- item.render(
- self.current_file.clone(),
- section_header.expect("All items rendered after a section header"),
- is_last,
- window,
- cx,
- )
- }))
+ let search_query = self.search_bar.read(cx).text(cx);
+ let has_active_search = !search_query.is_empty();
+ let has_no_results = items_len == 0 && has_active_search;
+
+ if has_no_results {
+ page_content = page_content.child(
+ v_flex()
+ .size_full()
+ .items_center()
+ .justify_center()
+ .gap_1()
+ .child(div().child("No Results"))
+ .child(
+ div()
+ .text_sm()
+ .text_color(cx.theme().colors().text_muted)
+ .child(format!("No settings match \"{}\"", search_query)),
+ ),
+ )
+ } else {
+ let last_non_header_index = items
+ .iter()
+ .enumerate()
+ .rev()
+ .find(|(_, item)| !matches!(item, SettingsPageItem::SectionHeader(_)))
+ .map(|(index, _)| index);
+
+ page_content = page_content.children(items.clone().into_iter().enumerate().map(
+ |(index, item)| {
+ let no_bottom_border = items
+ .get(index + 1)
+ .map(|next_item| {
+ matches!(next_item, SettingsPageItem::SectionHeader(_))
+ })
+ .unwrap_or(false);
+ let is_last = Some(index) == last_non_header_index;
+
+ if let SettingsPageItem::SectionHeader(header) = item {
+ section_header = Some(*header);
+ }
+ item.render(
+ self.current_file.clone(),
+ section_header.expect("All items rendered after a section header"),
+ no_bottom_border || is_last,
+ window,
+ cx,
+ )
+ },
+ ))
+ }
} else {
page = page.child(
h_flex()
- .gap_2()
- .child(IconButton::new("back-btn", IconName::ChevronLeft).on_click(
- cx.listener(|this, _, _, cx| {
- this.pop_sub_page(cx);
- }),
- ))
+ .ml_neg_1p5()
+ .gap_1()
+ .child(
+ IconButton::new("back-btn", IconName::ArrowLeft)
+ .icon_size(IconSize::Small)
+ .shape(IconButtonShape::Square)
+ .on_click(cx.listener(|this, _, _, cx| {
+ this.pop_sub_page(cx);
+ })),
+ )
.child(self.render_sub_page_breadcrumbs()),
);
@@ -3768,7 +3819,12 @@ where
menu
}),
)
+ .trigger_size(ButtonSize::Medium)
.style(DropdownStyle::Outlined)
+ .offset(gpui::Point {
+ x: px(0.0),
+ y: px(2.0),
+ })
.into_any_element()
}
@@ -3942,6 +3998,7 @@ mod test {
search_matches: vec![],
search_task: None,
sub_page_stack: vec![],
+ scroll_handle: ScrollHandle::new(),
};
settings_window.build_search_matches();
@@ -1,4 +1,4 @@
-use gpui::{ClickEvent, Corner, CursorStyle, Entity, Hsla, MouseButton};
+use gpui::{Corner, Entity, Pixels, Point};
use crate::{ContextMenu, PopoverMenu, prelude::*};
@@ -21,11 +21,14 @@ enum LabelKind {
pub struct DropdownMenu {
id: ElementId,
label: LabelKind,
+ trigger_size: ButtonSize,
style: DropdownStyle,
menu: Entity<ContextMenu>,
full_width: bool,
disabled: bool,
handle: Option<PopoverMenuHandle<ContextMenu>>,
+ attach: Option<Corner>,
+ offset: Option<Point<Pixels>>,
}
impl DropdownMenu {
@@ -37,11 +40,14 @@ impl DropdownMenu {
Self {
id: id.into(),
label: LabelKind::Text(label.into()),
+ trigger_size: ButtonSize::Default,
style: DropdownStyle::default(),
menu,
full_width: false,
disabled: false,
handle: None,
+ attach: None,
+ offset: None,
}
}
@@ -53,14 +59,22 @@ impl DropdownMenu {
Self {
id: id.into(),
label: LabelKind::Element(label),
+ trigger_size: ButtonSize::Default,
style: DropdownStyle::default(),
menu,
full_width: false,
disabled: false,
handle: None,
+ attach: None,
+ offset: None,
}
}
+ pub fn trigger_size(mut self, size: ButtonSize) -> Self {
+ self.trigger_size = size;
+ self
+ }
+
pub fn style(mut self, style: DropdownStyle) -> Self {
self.style = style;
self
@@ -75,6 +89,18 @@ impl DropdownMenu {
self.handle = Some(handle);
self
}
+
+ /// Defines which corner of the handle to attach the menu's anchor to.
+ pub fn attach(mut self, attach: Corner) -> Self {
+ self.attach = Some(attach);
+ self
+ }
+
+ /// Offsets the position of the menu by that many pixels.
+ pub fn offset(mut self, offset: Point<Pixels>) -> Self {
+ self.offset = Some(offset);
+ self
+ }
}
impl Disableable for DropdownMenu {
@@ -86,17 +112,46 @@ impl Disableable for DropdownMenu {
impl RenderOnce for DropdownMenu {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
- PopoverMenu::new(self.id)
+ let button_style = match self.style {
+ DropdownStyle::Solid => ButtonStyle::Filled,
+ DropdownStyle::Outlined => ButtonStyle::Outlined,
+ DropdownStyle::Ghost => ButtonStyle::Transparent,
+ };
+
+ let full_width = self.full_width;
+ let trigger_size = self.trigger_size;
+
+ let button = match self.label {
+ LabelKind::Text(text) => Button::new(self.id.clone(), text)
+ .style(button_style)
+ .icon(IconName::ChevronUpDown)
+ .icon_position(IconPosition::End)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .when(full_width, |this| this.full_width())
+ .size(trigger_size)
+ .disabled(self.disabled),
+ LabelKind::Element(_element) => Button::new(self.id.clone(), "")
+ .style(button_style)
+ .icon(IconName::ChevronUpDown)
+ .icon_position(IconPosition::End)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .when(full_width, |this| this.full_width())
+ .size(trigger_size)
+ .disabled(self.disabled),
+ };
+
+ PopoverMenu::new((self.id.clone(), "popover"))
.full_width(self.full_width)
.menu(move |_window, _cx| Some(self.menu.clone()))
- .trigger(
- DropdownMenuTrigger::new(self.label)
- .full_width(self.full_width)
- .disabled(self.disabled)
- .style(self.style),
- )
- .attach(Corner::BottomLeft)
- .when_some(self.handle, |el, handle| el.with_handle(handle))
+ .trigger(button)
+ .attach(match self.attach {
+ Some(attach) => attach,
+ None => Corner::BottomRight,
+ })
+ .when_some(self.offset, |this, offset| this.offset(offset))
+ .when_some(self.handle, |this, handle| this.with_handle(handle))
}
}
@@ -179,149 +234,3 @@ impl Component for DropdownMenu {
)
}
}
-
-#[derive(Debug, Clone, Copy)]
-pub struct DropdownTriggerStyle {
- pub bg: Hsla,
-}
-
-impl DropdownTriggerStyle {
- pub fn for_style(style: DropdownStyle, cx: &App) -> Self {
- let colors = cx.theme().colors();
-
- let bg = match style {
- DropdownStyle::Solid => colors.editor_background,
- DropdownStyle::Outlined => colors.surface_background,
- DropdownStyle::Ghost => colors.ghost_element_background,
- };
-
- Self { bg }
- }
-}
-
-#[derive(IntoElement)]
-struct DropdownMenuTrigger {
- label: LabelKind,
- full_width: bool,
- selected: bool,
- disabled: bool,
- style: DropdownStyle,
- cursor_style: CursorStyle,
- on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
-}
-
-impl DropdownMenuTrigger {
- pub fn new(label: LabelKind) -> Self {
- Self {
- label,
- full_width: false,
- selected: false,
- disabled: false,
- style: DropdownStyle::default(),
- cursor_style: CursorStyle::default(),
- on_click: None,
- }
- }
-
- pub fn full_width(mut self, full_width: bool) -> Self {
- self.full_width = full_width;
- self
- }
-
- pub fn style(mut self, style: DropdownStyle) -> Self {
- self.style = style;
- self
- }
-}
-
-impl Disableable for DropdownMenuTrigger {
- fn disabled(mut self, disabled: bool) -> Self {
- self.disabled = disabled;
- self
- }
-}
-
-impl Toggleable for DropdownMenuTrigger {
- fn toggle_state(mut self, selected: bool) -> Self {
- self.selected = selected;
- self
- }
-}
-
-impl Clickable for DropdownMenuTrigger {
- fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
- self.on_click = Some(Box::new(handler));
- self
- }
-
- fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
- self.cursor_style = cursor_style;
- self
- }
-}
-
-impl RenderOnce for DropdownMenuTrigger {
- fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
- let disabled = self.disabled;
-
- let style = DropdownTriggerStyle::for_style(self.style, cx);
- let is_outlined = matches!(self.style, DropdownStyle::Outlined);
-
- h_flex()
- .id("dropdown-menu-trigger")
- .min_w_20()
- .pl_2()
- .pr_1p5()
- .py_0p5()
- .gap_2()
- .justify_between()
- .rounded_sm()
- .map(|this| {
- if self.full_width {
- this.w_full()
- } else {
- this.flex_none().w_auto()
- }
- })
- .when(is_outlined, |this| {
- this.border_1()
- .border_color(cx.theme().colors().border)
- .overflow_hidden()
- })
- .map(|this| {
- if disabled {
- this.cursor_not_allowed()
- .bg(cx.theme().colors().element_disabled)
- } else {
- this.bg(style.bg)
- .hover(|s| s.bg(cx.theme().colors().element_hover))
- }
- })
- .child(match self.label {
- LabelKind::Text(text) => Label::new(text)
- .color(if disabled {
- Color::Disabled
- } else {
- Color::Default
- })
- .into_any_element(),
- LabelKind::Element(element) => element,
- })
- .child(
- Icon::new(IconName::ChevronUpDown)
- .size(IconSize::XSmall)
- .color(if disabled {
- Color::Disabled
- } else {
- Color::Muted
- }),
- )
- .when_some(self.on_click.filter(|_| !disabled), |el, on_click| {
- el.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
- .on_click(move |event, window, cx| {
- cx.stop_propagation();
- (on_click)(event, window, cx)
- })
- })
- }
-}