Wire plugin marketplace search to filter the grid
Replaces the static "Install plugin · N active" hero button with the design's pair: a glass search input + a primary Install action. The input is bound to a new InputState on ElyShell (plugin_search_input); on every render the catalog reads its current value and filters the plugin grid by case-insensitive contains on name, author, and description. An empty-needle render shows every installed plugin so the page still works at rest. When the search yields no matches, the grid swaps to an empty-state copy that quotes the user's needle. Install action moves into a trailing dark chip that calls choose_plugin_package, preserving the existing file-picker flow.
This commit is contained in:
@@ -6,7 +6,7 @@ use gpui::{
|
|||||||
StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient, px, rgb,
|
StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient, px, rgb,
|
||||||
rgba,
|
rgba,
|
||||||
};
|
};
|
||||||
use gpui_component::{IconName, scroll::ScrollableElement};
|
use gpui_component::{IconName, input::Input, scroll::ScrollableElement};
|
||||||
|
|
||||||
use super::plugin_editors_pick::{action_button, render_editors_pick};
|
use super::plugin_editors_pick::{action_button, render_editors_pick};
|
||||||
use super::{ElyShell, render_canvas_surface};
|
use super::{ElyShell, render_canvas_surface};
|
||||||
@@ -18,6 +18,9 @@ impl ElyShell {
|
|||||||
snapshot: &BrowserSnapshot,
|
snapshot: &BrowserSnapshot,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
|
let needle = self.plugin_search_input.read(cx).value().to_string();
|
||||||
|
let needle_lower = needle.trim().to_lowercase();
|
||||||
|
|
||||||
render_canvas_surface(
|
render_canvas_surface(
|
||||||
div()
|
div()
|
||||||
.size_full()
|
.size_full()
|
||||||
@@ -27,88 +30,122 @@ impl ElyShell {
|
|||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap(px(24.0))
|
.gap(px(24.0))
|
||||||
.child(render_hero(snapshot, cx))
|
.child(self.render_hero(snapshot, cx))
|
||||||
.child(render_summary(snapshot))
|
.child(render_summary(snapshot))
|
||||||
.child(render_grid(snapshot, cx)),
|
.child(render_grid(snapshot, &needle_lower, cx)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_hero(
|
||||||
|
&mut self,
|
||||||
|
snapshot: &BrowserSnapshot,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.grid()
|
||||||
|
.grid_cols(2)
|
||||||
|
.gap(px(28.0))
|
||||||
|
.child(self.render_hero_left(snapshot, cx))
|
||||||
|
.child(render_editors_pick(snapshot, cx))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_hero_left(
|
||||||
|
&mut self,
|
||||||
|
_snapshot: &BrowserSnapshot,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(10.0))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(11.0))
|
||||||
|
.text_color(rgb(colors::INK_4))
|
||||||
|
.child("PLUGINS · NATIVE TO ELY"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.font_family(SERIF_FAMILY)
|
||||||
|
.text_size(px(48.0))
|
||||||
|
.font_weight(FontWeight(400.0))
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child("Quiet tools. Everyday magic."),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.max_w(px(520.0))
|
||||||
|
.text_size(px(13.5))
|
||||||
|
.text_color(rgb(colors::INK_2))
|
||||||
|
.child(
|
||||||
|
"ELY plugins are sandboxed, theme-aware, and ship with their own \
|
||||||
|
controls in your sidebar — no Chrome extension framework required.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(self.render_search_row(cx))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_search_row(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(8.0))
|
||||||
|
.max_w(px(520.0))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.h(px(42.0))
|
||||||
|
.px(px(16.0))
|
||||||
|
.rounded(px(12.0))
|
||||||
|
.bg(rgba(SEARCH_BG))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(10.0))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.child(IconName::Search),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div().flex_1().child(
|
||||||
|
Input::new(&self.plugin_search_input)
|
||||||
|
.appearance(false)
|
||||||
|
.cleanable(true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id(SharedString::from("plugin-install-action"))
|
||||||
|
.h(px(42.0))
|
||||||
|
.px(px(16.0))
|
||||||
|
.rounded(px(12.0))
|
||||||
|
.bg(rgba(INSTALL_BG))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(8.0))
|
||||||
|
.text_size(px(13.0))
|
||||||
|
.font_weight(FontWeight(500.0))
|
||||||
|
.text_color(rgb(0xffffff))
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|style| style.opacity(0.92))
|
||||||
|
.active(|style| style.opacity(0.78))
|
||||||
|
.on_click(cx.listener(|shell, _, window, cx| {
|
||||||
|
shell.choose_plugin_package(window, cx);
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(rgb(0xffffff))
|
||||||
|
.child(IconName::Plus),
|
||||||
|
)
|
||||||
|
.child("Install"),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_hero(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
|
|
||||||
div()
|
|
||||||
.grid()
|
|
||||||
.grid_cols(2)
|
|
||||||
.gap(px(28.0))
|
|
||||||
.child(render_hero_left(snapshot, cx))
|
|
||||||
.child(render_editors_pick(snapshot, cx))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_hero_left(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
|
|
||||||
let total = snapshot.installed_plugins.len();
|
|
||||||
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap(px(10.0))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_size(px(11.0))
|
|
||||||
.text_color(rgb(colors::INK_4))
|
|
||||||
.child("PLUGINS · NATIVE TO ELY"),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.font_family(SERIF_FAMILY)
|
|
||||||
.text_size(px(48.0))
|
|
||||||
.font_weight(FontWeight(400.0))
|
|
||||||
.text_color(rgb(colors::INK))
|
|
||||||
.child("Quiet tools. Everyday magic."),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.max_w(px(520.0))
|
|
||||||
.text_size(px(13.5))
|
|
||||||
.text_color(rgb(colors::INK_2))
|
|
||||||
.child(
|
|
||||||
"ELY plugins are sandboxed, theme-aware, and ship with their own controls in \
|
|
||||||
your sidebar — no Chrome extension framework required.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(render_install_button(total, cx))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_install_button(total: usize, cx: &mut Context<ElyShell>) -> AnyElement {
|
|
||||||
div().flex().child(
|
|
||||||
div()
|
|
||||||
.id(SharedString::from("plugin-install"))
|
|
||||||
.h(px(42.0))
|
|
||||||
.px(px(16.0))
|
|
||||||
.rounded(px(12.0))
|
|
||||||
.bg(rgba(SEARCH_BG))
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap(px(10.0))
|
|
||||||
.text_size(px(13.0))
|
|
||||||
.text_color(rgb(colors::INK_3))
|
|
||||||
.cursor_pointer()
|
|
||||||
.hover(|style| style.bg(rgba(SEARCH_BG_HOVER)))
|
|
||||||
.active(|style| style.opacity(0.85))
|
|
||||||
.on_click(cx.listener(|shell, _, window, cx| {
|
|
||||||
shell.choose_plugin_package(window, cx);
|
|
||||||
}))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(rgb(colors::INK_3))
|
|
||||||
.child(IconName::Plus),
|
|
||||||
)
|
|
||||||
.child(format!(
|
|
||||||
"Install plugin · {} active",
|
|
||||||
total
|
|
||||||
)))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_summary(snapshot: &BrowserSnapshot) -> AnyElement {
|
fn render_summary(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||||
div()
|
div()
|
||||||
@@ -151,11 +188,25 @@ fn category_chip(label: &'static str, active: bool) -> AnyElement {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_grid(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
|
fn render_grid(
|
||||||
|
snapshot: &BrowserSnapshot,
|
||||||
|
needle: &str,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
if snapshot.installed_plugins.is_empty() {
|
if snapshot.installed_plugins.is_empty() {
|
||||||
return render_empty_state(cx);
|
return render_empty_state(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let matches: Vec<&InstalledPlugin> = snapshot
|
||||||
|
.installed_plugins
|
||||||
|
.iter()
|
||||||
|
.filter(|plugin| matches_plugin(plugin, needle))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if matches.is_empty() {
|
||||||
|
return render_no_match_state(needle);
|
||||||
|
}
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_h_0()
|
.min_h_0()
|
||||||
@@ -164,9 +215,8 @@ fn render_grid(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyEle
|
|||||||
.grid_cols(4)
|
.grid_cols(4)
|
||||||
.gap(px(14.0))
|
.gap(px(14.0))
|
||||||
.children(
|
.children(
|
||||||
snapshot
|
matches
|
||||||
.installed_plugins
|
.into_iter()
|
||||||
.iter()
|
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, plugin)| {
|
.map(|(index, plugin)| {
|
||||||
render_plugin_card(index, plugin, &snapshot.active_profile_kind, cx)
|
render_plugin_card(index, plugin, &snapshot.active_profile_kind, cx)
|
||||||
@@ -175,6 +225,38 @@ fn render_grid(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyEle
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn matches_plugin(plugin: &InstalledPlugin, needle: &str) -> bool {
|
||||||
|
if needle.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let manifest = plugin.manifest();
|
||||||
|
manifest.name().to_lowercase().contains(needle)
|
||||||
|
|| manifest.author().to_lowercase().contains(needle)
|
||||||
|
|| manifest.description().to_lowercase().contains(needle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_no_match_state(needle: &str) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(8.0))
|
||||||
|
.py(px(40.0))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(13.0))
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child(format!("No plugins match \"{needle}\".")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(11.5))
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.child("Try a different keyword."),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
fn render_empty_state(cx: &mut Context<ElyShell>) -> AnyElement {
|
fn render_empty_state(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -350,7 +432,7 @@ fn sandbox_chip_label(snapshot: &BrowserSnapshot) -> &'static str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SEARCH_BG: u32 = 0xffffffd9;
|
const SEARCH_BG: u32 = 0xffffffd9;
|
||||||
const SEARCH_BG_HOVER: u32 = 0xffffffeb;
|
const INSTALL_BG: u32 = 0x1d1c1aff;
|
||||||
const CARD_BG: u32 = 0xffffffc7;
|
const CARD_BG: u32 = 0xffffffc7;
|
||||||
const CARD_BG_HOVER: u32 = 0xffffffeb;
|
const CARD_BG_HOVER: u32 = 0xffffffeb;
|
||||||
const STATUS_BG: u32 = 0xffffffd9;
|
const STATUS_BG: u32 = 0xffffffd9;
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ pub struct ElyShell {
|
|||||||
state: ShellState,
|
state: ShellState,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
command_input: Entity<InputState>,
|
command_input: Entity<InputState>,
|
||||||
|
pub(crate) plugin_search_input: Entity<InputState>,
|
||||||
download_action_error: Option<String>,
|
download_action_error: Option<String>,
|
||||||
download_clear_confirmation: bool,
|
download_clear_confirmation: bool,
|
||||||
download_security_confirmation: Option<PendingDownloadFileAction>,
|
download_security_confirmation: Option<PendingDownloadFileAction>,
|
||||||
@@ -104,6 +105,8 @@ impl ElyShell {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let command_input =
|
let command_input =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("Search or enter address"));
|
cx.new(|cx| InputState::new(window, cx).placeholder("Search or enter address"));
|
||||||
|
let plugin_search_input =
|
||||||
|
cx.new(|cx| InputState::new(window, cx).placeholder("Search plugins…"));
|
||||||
|
|
||||||
let command_subscription = cx.subscribe_in(
|
let command_subscription = cx.subscribe_in(
|
||||||
&command_input,
|
&command_input,
|
||||||
@@ -151,6 +154,7 @@ impl ElyShell {
|
|||||||
state,
|
state,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
command_input,
|
command_input,
|
||||||
|
plugin_search_input,
|
||||||
download_action_error: None,
|
download_action_error: None,
|
||||||
download_clear_confirmation: false,
|
download_clear_confirmation: false,
|
||||||
download_security_confirmation: None,
|
download_security_confirmation: None,
|
||||||
|
|||||||
Reference in New Issue
Block a user