feat(gpui): add the filter menu at measured sizes
The popover matches the old computed styles: 256px wide, white/95, 16px radius, 14px padding, xl shadow, a three-way status grid with counts and dots, the amber favourite chip, role chips and dynamic tag chips. The theme now names Segoe UI explicitly so text metrics stay stable.
This commit is contained in:
1 parent
b4f1b05034
commit
aa0a05d352
3 files changed
+282
-11
No files matched your search
@@ -202,5 +202,6 @@
|
||||
"color": "#C5060B"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"font.family": "Segoe UI"
|
||||
}
|
||||
+273
-10
@@ -84,16 +84,24 @@ pub fn render(workspace: &Workspace, this: &Entity<Workspace>, cx: &App) -> impl
|
||||
let active = workspace.has_active_filter();
|
||||
let this_filter = this.clone();
|
||||
let label = filter_summary(workspace);
|
||||
pill_button(active)
|
||||
.id("nav-filter")
|
||||
.on_click(move |_, _, cx| {
|
||||
let _ = this_filter.update(cx, |workspace, cx| {
|
||||
workspace.filter_open = !workspace.filter_open;
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.child(icon(IconName::Funnel, 14., cx))
|
||||
.child(label)
|
||||
let mut wrapper = div()
|
||||
.relative()
|
||||
.child(
|
||||
pill_button(active)
|
||||
.id("nav-filter")
|
||||
.on_click(move |_, _, cx| {
|
||||
let _ = this_filter.update(cx, |workspace, cx| {
|
||||
workspace.filter_open = !workspace.filter_open;
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.child(icon(IconName::Funnel, 14., cx))
|
||||
.child(label),
|
||||
);
|
||||
if workspace.filter_open {
|
||||
wrapper = wrapper.child(filter_panel(workspace, this, cx));
|
||||
}
|
||||
wrapper
|
||||
});
|
||||
|
||||
if workspace.page == Page::Workspace {
|
||||
@@ -375,6 +383,261 @@ pub fn icon(name: IconName, size: f32, _cx: &App) -> impl IntoElement {
|
||||
div().text_size(px(size)).child(Icon::new(name))
|
||||
}
|
||||
|
||||
/// 筛选下拉面板(真值:256px 宽、白 95%、圆角 16、14px 内边距、Tailwind shadow-xl)。
|
||||
fn filter_panel(workspace: &Workspace, this: &Entity<Workspace>, cx: &App) -> AnyElement {
|
||||
use crate::workspace::{RoleFilter, StatusFilter};
|
||||
|
||||
let mut panel = v_flex()
|
||||
.absolute()
|
||||
.top(px(37.))
|
||||
.right(px(0.))
|
||||
.w(px(256.))
|
||||
.rounded_2xl()
|
||||
.border_1()
|
||||
.border_color(Hsla::from(rgb(0xe4e4e7)).opacity(0.9))
|
||||
.bg(Hsla::from(rgb(0xffffff)).opacity(0.95))
|
||||
.p(px(14.))
|
||||
.shadow(vec![
|
||||
BoxShadow {
|
||||
color: Hsla::from(rgb(0x000000)).opacity(0.1),
|
||||
offset: point(px(0.), px(20.)),
|
||||
blur_radius: px(25.),
|
||||
spread_radius: px(-5.),
|
||||
inset: false,
|
||||
},
|
||||
BoxShadow {
|
||||
color: Hsla::from(rgb(0x000000)).opacity(0.1),
|
||||
offset: point(px(0.), px(8.)),
|
||||
blur_radius: px(10.),
|
||||
spread_radius: px(-6.),
|
||||
inset: false,
|
||||
},
|
||||
]);
|
||||
|
||||
// ---- 在线状态 ----
|
||||
let mut head = h_flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.text_color(rgb(0xa1a1aa))
|
||||
.child("在线状态"),
|
||||
);
|
||||
if workspace.has_active_filter() {
|
||||
let this_reset = this.clone();
|
||||
head = head.child(
|
||||
div()
|
||||
.id("filter-reset")
|
||||
.text_size(px(11.))
|
||||
.text_color(rgb(0x71717a))
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.text_color(rgb(0x18181b)))
|
||||
.on_click(move |_, _, cx| {
|
||||
let _ = this_reset.update(cx, |workspace, cx| {
|
||||
workspace.reset_filters();
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.child("重置全部"),
|
||||
);
|
||||
}
|
||||
panel = panel.child(head);
|
||||
|
||||
let mut grid = h_flex().mt(px(6.)).gap(px(4.));
|
||||
let statuses: [(&'static str, StatusFilter, Option<Hsla>); 3] = [
|
||||
("全部", StatusFilter::All, None),
|
||||
("在线", StatusFilter::Online, Some(Hsla::from(rgb(0x10b981)))),
|
||||
("离线", StatusFilter::Offline, Some(Hsla::from(rgb(0xef4444)))),
|
||||
];
|
||||
for (label, filter, dot) in statuses {
|
||||
let active = workspace.status_filter == filter;
|
||||
let count = match filter {
|
||||
StatusFilter::Online => Some(workspace.online_count()),
|
||||
StatusFilter::Offline => Some(workspace.offline_count()),
|
||||
StatusFilter::All => None,
|
||||
};
|
||||
let text = match count {
|
||||
Some(count) => format!("{label} ({count})"),
|
||||
None => label.to_owned(),
|
||||
};
|
||||
let this_status = this.clone();
|
||||
let mut chip = filter_chip(active)
|
||||
.id(SharedString::from(format!("filter-status-{}", label)))
|
||||
.on_click(move |_, _, cx| {
|
||||
let _ = this_status.update(cx, |workspace, cx| {
|
||||
workspace.status_filter = filter;
|
||||
cx.notify();
|
||||
});
|
||||
});
|
||||
if let Some(dot_color) = dot {
|
||||
chip = chip.child(div().size(px(8.)).rounded_full().bg(dot_color));
|
||||
}
|
||||
grid = grid.child(chip.child(text));
|
||||
}
|
||||
panel = panel.child(grid);
|
||||
|
||||
// ---- 收藏与分类 ----
|
||||
panel = panel.child(
|
||||
v_flex()
|
||||
.mt(px(12.))
|
||||
.pt(px(12.))
|
||||
.border_t_1()
|
||||
.border_color(rgb(0xf1f2f4))
|
||||
.gap(px(6.))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.text_color(rgb(0xa1a1aa))
|
||||
.child("收藏与分类"),
|
||||
),
|
||||
);
|
||||
|
||||
let mut chips = h_flex().flex_wrap().gap(px(4.));
|
||||
// 收藏
|
||||
{
|
||||
let active = workspace.favorite_only;
|
||||
let this_fav = this.clone();
|
||||
let base = div()
|
||||
.id("filter-favorite")
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap(px(4.))
|
||||
.h(px(24.))
|
||||
.px(px(10.))
|
||||
.rounded_lg()
|
||||
.text_size(px(12.))
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.cursor_pointer()
|
||||
.on_click(move |_, _, cx| {
|
||||
let _ = this_fav.update(cx, |workspace, cx| {
|
||||
workspace.favorite_only = !workspace.favorite_only;
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.child(format!("★ 收藏 ({})", workspace.favorite_count()));
|
||||
chips = chips.child(if active {
|
||||
base.bg(rgb(0xf59e0b)).text_color(rgb(0xffffff))
|
||||
} else {
|
||||
base.bg(rgb(0xf4f4f5)).text_color(rgb(0x52525b))
|
||||
});
|
||||
}
|
||||
// 分类
|
||||
let roles: [(&'static str, RoleFilter); 5] = [
|
||||
("全部分类", RoleFilter::All),
|
||||
("主力", RoleFilter::Primary),
|
||||
("代理", RoleFilter::Proxy),
|
||||
("测试", RoleFilter::Test),
|
||||
("归档", RoleFilter::Archived),
|
||||
];
|
||||
for (label, role) in roles {
|
||||
let active = workspace.role_filter == role;
|
||||
let this_role = this.clone();
|
||||
let base = div()
|
||||
.id(SharedString::from(format!("filter-role-{}", label)))
|
||||
.flex()
|
||||
.items_center()
|
||||
.h(px(24.))
|
||||
.px(px(10.))
|
||||
.rounded_lg()
|
||||
.text_size(px(12.))
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.cursor_pointer()
|
||||
.on_click(move |_, _, cx| {
|
||||
let _ = this_role.update(cx, |workspace, cx| {
|
||||
workspace.role_filter = role;
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.child(label);
|
||||
chips = chips.child(if active {
|
||||
base.bg(rgb(0x18181b)).text_color(rgb(0xffffff))
|
||||
} else {
|
||||
base.bg(rgb(0xf4f4f5)).text_color(rgb(0x52525b))
|
||||
});
|
||||
}
|
||||
panel = panel.child(chips);
|
||||
|
||||
// ---- 标签 ----
|
||||
let tags = workspace.all_tags();
|
||||
if !tags.is_empty() {
|
||||
let mut tag_chips = h_flex().flex_wrap().gap(px(4.));
|
||||
for tag in tags {
|
||||
let active = workspace.tag_filter == tag;
|
||||
let this_tag = this.clone();
|
||||
let value = tag.clone();
|
||||
let base = div()
|
||||
.id(SharedString::from(format!("filter-tag-{}", tag)))
|
||||
.h(px(20.))
|
||||
.px(px(8.))
|
||||
.rounded_lg()
|
||||
.text_size(px(11.))
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.cursor_pointer()
|
||||
.on_click(move |_, _, cx| {
|
||||
let value = value.clone();
|
||||
let _ = this_tag.update(cx, |workspace, cx| {
|
||||
workspace.tag_filter = if workspace.tag_filter == value {
|
||||
String::new()
|
||||
} else {
|
||||
value
|
||||
};
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.child(format!("#{tag}"));
|
||||
tag_chips = tag_chips.child(if active {
|
||||
base.bg(rgb(0x18181b)).text_color(rgb(0xffffff))
|
||||
} else {
|
||||
base.bg(rgb(0xf4f4f5)).text_color(rgb(0x52525b))
|
||||
});
|
||||
}
|
||||
panel = panel.child(
|
||||
v_flex()
|
||||
.mt(px(12.))
|
||||
.pt(px(12.))
|
||||
.border_t_1()
|
||||
.border_color(rgb(0xf1f2f4))
|
||||
.gap(px(6.))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.text_color(rgb(0xa1a1aa))
|
||||
.child("标签"),
|
||||
)
|
||||
.child(tag_chips),
|
||||
);
|
||||
}
|
||||
|
||||
panel.into_any_element()
|
||||
}
|
||||
|
||||
/// 筛选面板里的状态按钮(真值:圆角 12、12px/500、padding 6/8,激活黑底白字)。
|
||||
fn filter_chip(active: bool) -> Div {
|
||||
let base = div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap(px(4.))
|
||||
.py(px(6.))
|
||||
.px(px(8.))
|
||||
.rounded_lg()
|
||||
.text_size(px(12.))
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.cursor_pointer();
|
||||
if active {
|
||||
base.bg(rgb(0x18181b)).text_color(rgb(0xffffff))
|
||||
} else {
|
||||
base.bg(rgb(0xf4f4f5))
|
||||
.text_color(rgb(0x52525b))
|
||||
.hover(|style| style.bg(rgb(0xe4e4e7)))
|
||||
}
|
||||
}
|
||||
|
||||
/// 品牌图标:纯黑圆角方块 + 白色滑块(旧 `BrandLogo` 的简化等价物)。
|
||||
fn brand_logo() -> impl IntoElement {
|
||||
div()
|
||||
|
||||
@@ -229,6 +229,13 @@ impl Workspace {
|
||||
|| !self.tag_filter.is_empty()
|
||||
}
|
||||
|
||||
pub fn reset_filters(&mut self) {
|
||||
self.status_filter = StatusFilter::All;
|
||||
self.favorite_only = false;
|
||||
self.role_filter = RoleFilter::All;
|
||||
self.tag_filter.clear();
|
||||
}
|
||||
|
||||
pub fn filtered_hosts(&self) -> Vec<&Host> {
|
||||
let query = self.search_query.trim().to_lowercase();
|
||||
self.hosts
|
||||
|
||||
Reference in new issue
Block a user