Docker 的标签改用可横向滚动的下划线标签,与系统服务一致

均分宽度放不下英文的 "Containers 12"。不管什么语言,标签都按文字和数量的宽度排,
放不下时横向滚动。去掉只给 Docker 用的 count_tabs 和分段控件的计数。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XuzandClaude Opus 5.5 committed 2026-10-07 22:42:23 +08:00
1 parent 82c2c8b277
commit 7b0dad9b41
8 files changed
+79 -135

No files matched your search

+1 -1
View File
@@ -49,7 +49,7 @@ ShellRS(crate 与二进制都叫 `shellrs`)是 Xshell / WinSCP 式的 SSH
- 系统监控每 2 秒读一次(运行时长随读数刷新,不自己走),磁盘每 30 秒,只在显示时读。≥ 70% 黄、≥ 90% 红。
- 网络连接、系统服务、Docker、历史命令的列表不自己刷新:打开、换终端、点刷新、执行命令之后才读。进程管理每 15 秒读一次。
- 结束进程,停止 / 重启服务,停止 / 重启 / 删除容器都先确认。不是 root 时用 `sudo -n`,要密码就报「需要 root 权限…」。
- 系统服务的标签用 gpui-kit 的下划线 `TabBar`(均分放不下「已停止 202」),Docker 用均分的 `shared::count_tabs`。
- 系统服务和 Docker 的标签都用 gpui-kit 的下划线 `TabBar`:每个标签按文字和数量的宽度排,放不下时横向滚动,不截断。均分放不下「已停止 202」,英文的 "Containers 12" 也放不下,不管什么语言都这样。
- 历史命令读主机的 `~/.bash_history`,不自己记录;点一条是放进输入行、不执行,另有执行按钮。
- 命令片段所有主机共用(库里预留了「适用范围」列);「点击时自动执行」新建时默认勾上;执行按钮鼠标移上去才出现,位置一直留着。
- **在线升级**:
+75 -49
View File
@@ -8,6 +8,7 @@ use gpui_kit::component::{
h_flex,
scroll::ScrollableElement as _,
spinner::Spinner,
tab::{Tab, TabBar},
tag::Tag,
v_flex,
};
@@ -25,7 +26,7 @@ use crate::app::{
ToggleDockerProject,
};
use crate::i18n::t;
use crate::shared::{count_tabs, soft_tag, tinted};
use crate::shared::{soft_tag, tinted};
use crate::terminal::{ExecResult, ExecTarget, RemoteTerminalId, exec_answer};
/// How long a terminal not connected is left before it is asked again.
@@ -249,55 +250,80 @@ impl DockerPanel {
self.sync_list(&rows);
let summary = table.summary();
let refresh = self.dispatch.clone();
let header = v_flex()
.flex_shrink_0()
.px_3()
.pt_3()
.pb_2()
.gap_2()
.child(
h_flex()
.gap_2()
.child(
div()
.id("docker-summary")
.test_support()
.aria_label(summary.clone())
.flex_1()
.min_w_0()
.truncate()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(summary),
)
.child(
Button::new("docker-refresh")
.ghost()
.xsmall()
.icon(Icon::new(CatalogIcon::RefreshCw))
.loading(self.loading)
.tooltip(t!("tools.refresh"))
.accessibility_label(t!("tools.refresh"))
.on_click(move |_, window, cx| {
refresh.dispatch_action(&RefreshDocker, window, cx)
}),
let header =
v_flex()
.flex_shrink_0()
.px_3()
.pt_3()
.pb_2()
.gap_2()
.child(
h_flex()
.gap_2()
.child(
div()
.id("docker-summary")
.test_support()
.aria_label(summary.clone())
.flex_1()
.min_w_0()
.truncate()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(summary),
)
.child(
Button::new("docker-refresh")
.ghost()
.xsmall()
.icon(Icon::new(CatalogIcon::RefreshCw))
.loading(self.loading)
.tooltip(t!("tools.refresh"))
.accessibility_label(t!("tools.refresh"))
.on_click(move |_, window, cx| {
refresh.dispatch_action(&RefreshDocker, window, cx)
}),
),
)
.when_some(problem, |header, problem| {
header.child(render_problem(&problem, cx))
})
.child(
// gpui-kit's underlined tabs, as 系统服务's: each as wide as
// its label and count, and where they do not all fit the bar
// scrolls sideways rather than cut a label short, whatever
// the language ("Containers 12" has no room in a quarter of
// the panel).
div().id("docker-tabs").test_support().child(
TabBar::new("docker-tab-bar")
.underline()
.small()
.selected_index(
DockerTab::ALL
.iter()
.position(|tab| *tab == self.tab)
.unwrap_or(0),
)
.on_click(cx.listener(|this, index: &usize, _, cx| {
this.show(DockerTab::ALL[*index], cx)
}))
.children(DockerTab::ALL.iter().zip(table.counts()).map(
|(tab, count)| {
Tab::new()
.label(tab.label())
.aria_label(format!("{} {count}", tab.label()))
// Beside the label: the suffix would sit
// outside the label's box.
.child(
div()
.ml_1()
.text_color(cx.theme().muted_foreground)
.child(count.to_string()),
)
},
)),
),
)
.when_some(problem, |header, problem| {
header.child(render_problem(&problem, cx))
})
.child(count_tabs(
"docker-tabs",
DockerTab::ALL
.iter()
.map(|tab| tab.label())
.zip(table.counts()),
DockerTab::ALL
.iter()
.position(|tab| *tab == self.tab)
.unwrap_or(0),
cx.listener(|this, index: &usize, _, cx| this.show(DockerTab::ALL[*index], cx)),
));
);
let empty = match self.tab {
DockerTab::Containers => t!("docker.panel.no_containers"),
+1 -1
View File
@@ -295,7 +295,7 @@ impl ServicePanel {
// count: 「已停止 202」 does not fit an equal share of the
// panel, and where they all do not fit the bar scrolls
// sideways rather than cut a label short (the user's
// choice; Docker's shorter labels share the width).
// choice, Docker's too).
div().id("services-tabs").test_support().child(
TabBar::new("services-tab-bar")
.underline()
-36
View File
@@ -1,36 +0,0 @@
//! The tabs over a right-sidebar tool's list.
use gpui_kit::component::Sizable as _;
use gpui_kit::*;
use super::{Segment, SegmentedControl};
/// What a right-sidebar tool's list shows, as a small segmented control
/// across the panel, its segments sharing the width equally, each with how
/// many it holds beside its label: 「容器 9」. For short labels: Docker's.
/// Longer ones (系统服务's 「已停止 202」) are cut short in an equal share of
/// the panel's 320 px, and keep gpui-kit's underlined tabs, which scroll
/// sideways instead; so do a dialog's sections.
///
/// Not gpui-kit's `TabBar`: its tabs take their labels' width whatever
/// they are told (the segmented one wraps each in a box of its own for its
/// sliding marker), so they bunch up at the start of the bar. `id` names
/// the box around the control, whose segments are found in it by their
/// place: `within(id).click(1usize)`.
pub fn count_tabs(
id: &'static str,
tabs: impl IntoIterator<Item = (impl Into<SharedString>, usize)>,
selected: usize,
on_click: impl Fn(&usize, &mut Window, &mut App) + 'static,
) -> impl IntoElement {
div().id(id).test_support().child(
SegmentedControl::new(SharedString::from(format!("{id}-control")))
.small()
.selected_index(Some(selected))
.on_change(on_click)
.segments(
tabs.into_iter()
.map(|(label, count)| Segment::new(label).count(count)),
),
)
}
-2
View File
@@ -2,7 +2,6 @@
mod closable_tab;
mod command_line;
mod count_tabs;
mod dialog;
mod format;
mod host_mark;
@@ -15,7 +14,6 @@ mod tint;
pub use closable_tab::ClosableTabTitle;
pub use command_line::{command_tooltip, one_line};
pub use count_tabs::count_tabs;
pub use dialog::{
DeleteHandler, commit_footer, confirm_danger, confirm_delete, dismiss_form_error,
form_error_notification, parse_port,
+2 -26
View File
@@ -9,9 +9,7 @@ type ChangeHandler = Rc<dyn Fn(&usize, &mut Window, &mut App)>;
/// One choice out of a few, side by side in one track, the chosen one
/// standing out as a raised block: a segmented control, for a form's choice
/// between ways of doing the same thing (密码 / 使用凭据 / 无密码), and, small
/// and with counts, for what a right-sidebar tool's list shows (容器 9 / 卷
/// 0 …, see `count_tabs`).
/// between ways of doing the same thing (密码 / 使用凭据 / 无密码).
///
/// Behavior comes from gpui-base's radio group: activation by pointer, Enter
/// or Space, a tab stop per segment, and the radio group's semantics for
@@ -30,8 +28,6 @@ pub struct SegmentedControl {
/// One segment of a [`SegmentedControl`].
pub struct Segment {
label: SharedString,
/// How many there are of what it shows, quieter beside the label.
count: Option<usize>,
disabled: bool,
/// Why a disabled segment cannot be chosen.
tooltip: Option<SharedString>,
@@ -41,18 +37,11 @@ impl Segment {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
count: None,
disabled: false,
tooltip: None,
}
}
/// How many there are of what it shows: 「容器 9」.
pub fn count(mut self, count: usize) -> Self {
self.count = Some(count);
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
@@ -120,7 +109,6 @@ impl RenderOnce for SegmentedControl {
let total = self.segments.len();
let selected = self.selected;
let on_change = self.on_change;
let muted = theme.muted_foreground;
RadioGroup::new(self.id)
.axis(Axis::Horizontal)
@@ -142,10 +130,7 @@ impl RenderOnce for SegmentedControl {
Radio::new(ix)
.checked(checked)
.disabled(segment.disabled)
.accessibility_label(match segment.count {
Some(count) => format!("{} {count}", segment.label).into(),
None => segment.label.clone(),
})
.accessibility_label(segment.label.clone())
.set_position(ix + 1, total)
// Equal shares of the track, whatever the labels.
.flex_1()
@@ -194,15 +179,6 @@ impl RenderOnce for SegmentedControl {
})
})
.child(div().truncate().child(segment.label))
.when_some(segment.count, |radio, count| {
radio.child(
div()
.flex_shrink_0()
.ml_1()
.text_color(muted)
.child(count.to_string()),
)
})
}))
}
}
-1
View File
@@ -135,7 +135,6 @@ async fn containers_show_by_project_and_the_rest_on_their_own(cx: &mut TestAppCo
window.within("docker-tabs").find(0usize).label(),
Some("容器 4")
);
assert_tabs_share_the_width(window, "docker-tabs", 4);
});
// Unfold the stopped project, fold the running one.
-19
View File
@@ -212,22 +212,3 @@ pub fn in_frame<R>(
cx.run_until_parked();
result
}
/// That the tabs of a tool's segmented bar, in the box `id`, share its
/// whole width evenly: `shared::count_tabs`.
pub fn assert_tabs_share_the_width(window: &mut gpui_kit::Window, id: &'static str, tabs: usize) {
let bar = window.find(id).bounds();
let widths: Vec<f32> = (0..tabs)
.map(|index| f32::from(window.within(id).find(index).bounds().size.width))
.collect();
let first = window.within(id).find(0usize).bounds();
let last = window.within(id).find(tabs - 1).bounds();
assert!(
f32::from(first.left() - bar.left()) < 8. && f32::from(bar.right() - last.right()) < 8.,
"the tabs do not span the bar: {first:?} … {last:?} in {bar:?}"
);
assert!(
widths.iter().all(|width| (width - widths[0]).abs() < 1.),
"the tabs are not as wide as each other: {widths:?}"
);
}