extensions_ui: Add design changes to expose the filters more (#29582)

Closes https://github.com/zed-industries/zed/issues/28086

The main motivator for this change is to have the "MCP Servers" filter
more clearly visible. And because of this, all other filters end up more
visible, as they're not in a dropdown menu anymore. Ended up pushing
some other small changes here and there as well. This is our final
product:

<img
src="https://github.com/user-attachments/assets/16ac78b6-72d9-4a8a-801b-b4b992221331"
width="700"/>

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2025-04-29 07:54:39 -03:00 committed by GitHub
parent 756fcd0733
commit 9b37206147
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 75 additions and 102 deletions

View file

@ -40,7 +40,7 @@ impl RenderOnce for ExtensionCard {
.bg(cx.theme().colors().elevated_surface_background)
.border_1()
.border_color(cx.theme().colors().border)
.rounded_sm()
.rounded_md()
.children(self.children)
.when(self.overridden_by_dev_extension, |card| {
card.child(

View file

@ -1,6 +1,6 @@
use gpui::{AnyElement, Div, StyleRefinement};
use smallvec::SmallVec;
use ui::{ButtonLike, prelude::*};
use ui::prelude::*;
#[derive(IntoElement)]
pub struct FeatureUpsell {
@ -46,21 +46,20 @@ impl FeatureUpsell {
impl RenderOnce for FeatureUpsell {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
self.base
.p_4()
.py_2()
.px_4()
.justify_between()
.border_color(cx.theme().colors().border)
.child(v_flex().overflow_hidden().child(Label::new(self.text)))
.flex_wrap()
.border_color(cx.theme().colors().border_variant)
.child(Label::new(self.text))
.child(h_flex().gap_2().children(self.children).when_some(
self.docs_url,
|el, docs_url| {
el.child(
ButtonLike::new("open_docs")
.child(
h_flex()
.gap_2()
.child(Label::new("View docs"))
.child(Icon::new(IconName::ArrowUpRight)),
)
Button::new("open_docs", "View Documentation")
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.on_click({
let docs_url = docs_url.clone();
move |_event, _window, cx| {

View file

@ -165,7 +165,7 @@ fn extension_provides_label(provides: ExtensionProvides) -> &'static str {
ExtensionProvides::Languages => "Languages",
ExtensionProvides::Grammars => "Grammars",
ExtensionProvides::LanguageServers => "Language Servers",
ExtensionProvides::ContextServers => "Context Servers",
ExtensionProvides::ContextServers => "MCP Servers",
ExtensionProvides::SlashCommands => "Slash Commands",
ExtensionProvides::IndexedDocsProviders => "Indexed Docs Providers",
ExtensionProvides::Snippets => "Snippets",
@ -573,6 +573,7 @@ impl ExtensionsPage {
extension.authors.join(", ")
))
.size(LabelSize::Small)
.color(Color::Muted)
.truncate(),
)
.child(Label::new("<>").size(LabelSize::Small)),
@ -594,7 +595,6 @@ impl ExtensionsPage {
)
.icon_color(Color::Accent)
.icon_size(IconSize::Small)
.style(ButtonStyle::Filled)
.on_click(cx.listener({
let repository_url = repository_url.clone();
move |_, _, _, cx| {
@ -701,6 +701,7 @@ impl ExtensionsPage {
extension.manifest.authors.join(", ")
))
.size(LabelSize::Small)
.color(Color::Muted)
.truncate(),
)
.child(
@ -731,7 +732,6 @@ impl ExtensionsPage {
)
.icon_color(Color::Accent)
.icon_size(IconSize::Small)
.style(ButtonStyle::Filled)
.on_click(cx.listener({
let repository_url = repository_url.clone();
move |_, _, _, cx| {
@ -751,8 +751,7 @@ impl ExtensionsPage {
IconName::Ellipsis,
)
.icon_color(Color::Accent)
.icon_size(IconSize::Small)
.style(ButtonStyle::Filled),
.icon_size(IconSize::Small),
)
.menu(move |window, cx| {
Some(Self::render_remote_extension_context_menu(
@ -950,19 +949,20 @@ impl ExtensionsPage {
cx.theme().colors().border
};
h_flex().w_full().gap_2().key_context(key_context).child(
h_flex()
.flex_1()
.px_2()
.py_1()
.gap_2()
.border_1()
.border_color(editor_border)
.min_w(rems_from_px(384.))
.rounded_lg()
.child(Icon::new(IconName::MagnifyingGlass))
.child(self.render_text_input(&self.query_editor, cx)),
)
h_flex()
.key_context(key_context)
.h_8()
.flex_1()
.min_w(rems_from_px(384.))
.pl_1p5()
.pr_2()
.py_1()
.gap_2()
.border_1()
.border_color(editor_border)
.rounded_lg()
.child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted))
.child(self.render_text_input(&self.query_editor, cx))
}
fn render_text_input(
@ -1193,52 +1193,6 @@ impl ExtensionsPage {
upsell.when(ix < upsells_count, |upsell| upsell.border_b_1())
}))
}
fn build_extension_provides_filter_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
let this = cx.entity();
ContextMenu::build(window, cx, |mut menu, _window, _cx| {
menu = menu.header("Extension Category").toggleable_entry(
"All",
self.provides_filter.is_none(),
IconPosition::End,
None,
{
let this = this.clone();
move |_window, cx| {
this.update(cx, |this, cx| {
this.change_provides_filter(None, cx);
});
}
},
);
for provides in ExtensionProvides::iter() {
let label = extension_provides_label(provides);
menu = menu.toggleable_entry(
label,
self.provides_filter == Some(provides),
IconPosition::End,
None,
{
let this = this.clone();
move |_window, cx| {
this.update(cx, |this, cx| {
this.change_provides_filter(Some(provides), cx);
this.provides_filter = Some(provides);
});
}
},
)
}
menu
})
}
}
impl Render for ExtensionsPage {
@ -1249,9 +1203,8 @@ impl Render for ExtensionsPage {
.child(
v_flex()
.gap_4()
.p_4()
.border_b_1()
.border_color(cx.theme().colors().border)
.pt_4()
.px_4()
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
@ -1271,29 +1224,9 @@ impl Render for ExtensionsPage {
.child(
h_flex()
.w_full()
.gap_2()
.justify_between()
.child(h_flex().gap_2().child(self.render_search(cx)).child({
let this = cx.entity().clone();
PopoverMenu::new("extension-provides-filter")
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| {
this.build_extension_provides_filter_menu(window, cx)
}))
})
.trigger_with_tooltip(
Button::new(
"extension-provides-filter-button",
self.provides_filter
.map(extension_provides_label)
.unwrap_or("All"),
)
.icon(IconName::Filter)
.icon_position(IconPosition::Start),
Tooltip::text("Filter extensions by category"),
)
.anchor(gpui::Corner::TopLeft)
}))
.gap_4()
.flex_wrap()
.child(self.render_search(cx))
.child(
h_flex()
.child(
@ -1343,6 +1276,47 @@ impl Render for ExtensionsPage {
),
),
)
.child(
h_flex()
.id("filter-row")
.gap_2()
.py_2p5()
.px_4()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.overflow_x_scroll()
.child(
Button::new("filter-all-categories", "All")
.when(self.provides_filter.is_none(), |button| {
button.style(ButtonStyle::Filled)
})
.when(self.provides_filter.is_some(), |button| {
button.style(ButtonStyle::Subtle)
})
.toggle_state(self.provides_filter.is_none())
.on_click(cx.listener(|this, _event, _, cx| {
this.change_provides_filter(None, cx);
})),
)
.children(ExtensionProvides::iter().map(|provides| {
let label = extension_provides_label(provides);
Button::new(
SharedString::from(format!("filter-category-{}", label)),
label,
)
.style(if self.provides_filter == Some(provides) {
ButtonStyle::Filled
} else {
ButtonStyle::Subtle
})
.toggle_state(self.provides_filter == Some(provides))
.on_click({
cx.listener(move |this, _event, _, cx| {
this.change_provides_filter(Some(provides), cx);
})
})
})),
)
.child(self.render_feature_upsells(cx))
.child(
v_flex()