mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
settings ui: Add scrollbar and other design details (#39504)
Release Notes: - N/A
This commit is contained in:
parent
d2b91eb2bc
commit
79a8986cb7
3 changed files with 169 additions and 201 deletions
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,8 +122,9 @@ impl RenderOnce for TreeViewItem {
|
|||
let selected_border = cx.theme().colors().border.opacity(0.6);
|
||||
let focused_border = cx.theme().colors().border_focused;
|
||||
let transparent_border = cx.theme().colors().border_transparent;
|
||||
let item_size = rems_from_px(28.);
|
||||
|
||||
let indentation_line = h_flex().size_7().flex_none().justify_center().child(
|
||||
let indentation_line = h_flex().size(item_size).flex_none().justify_center().child(
|
||||
div()
|
||||
.w_px()
|
||||
.h_full()
|
||||
|
|
@ -143,7 +144,8 @@ impl RenderOnce for TreeViewItem {
|
|||
.map(|this| {
|
||||
let label = self.label;
|
||||
if self.root_item {
|
||||
this.px_1()
|
||||
this.h(item_size)
|
||||
.px_1()
|
||||
.mb_1()
|
||||
.gap_2p5()
|
||||
.rounded_sm()
|
||||
|
|
|
|||
Loading…
Reference in a new issue