From 7b0dad9b4100e0c224716a46009ed90fd7e9d308 Mon Sep 17 00:00:00 2001 From: Xuz Date: Wed, 7 Oct 2026 22:42:23 +0800 Subject: [PATCH] =?UTF-8?q?Docker=20=E7=9A=84=E6=A0=87=E7=AD=BE=E6=94=B9?= =?UTF-8?q?=E7=94=A8=E5=8F=AF=E6=A8=AA=E5=90=91=E6=BB=9A=E5=8A=A8=E7=9A=84?= =?UTF-8?q?=E4=B8=8B=E5=88=92=E7=BA=BF=E6=A0=87=E7=AD=BE=EF=BC=8C=E4=B8=8E?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E6=9C=8D=E5=8A=A1=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 均分宽度放不下英文的 "Containers 12"。不管什么语言,标签都按文字和数量的宽度排, 放不下时横向滚动。去掉只给 Docker 用的 count_tabs 和分段控件的计数。 Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- src/docker/docker_panel.rs | 124 +++++++++++++++++++------------- src/services/service_panel.rs | 2 +- src/shared/count_tabs.rs | 36 ---------- src/shared/mod.rs | 2 - src/shared/segmented_control.rs | 28 +------- tests/ui/docker.rs | 1 - tests/ui/support/mod.rs | 19 ----- 8 files changed, 79 insertions(+), 135 deletions(-) delete mode 100644 src/shared/count_tabs.rs diff --git a/CLAUDE.md b/CLAUDE.md index ffce3e1..676db4b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`,不自己记录;点一条是放进输入行、不执行,另有执行按钮。 - 命令片段所有主机共用(库里预留了「适用范围」列);「点击时自动执行」新建时默认勾上;执行按钮鼠标移上去才出现,位置一直留着。 - **在线升级**: diff --git a/src/docker/docker_panel.rs b/src/docker/docker_panel.rs index 46687d9..5ceddd2 100644 --- a/src/docker/docker_panel.rs +++ b/src/docker/docker_panel.rs @@ -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"), diff --git a/src/services/service_panel.rs b/src/services/service_panel.rs index 7459f91..909e827 100644 --- a/src/services/service_panel.rs +++ b/src/services/service_panel.rs @@ -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() diff --git a/src/shared/count_tabs.rs b/src/shared/count_tabs.rs deleted file mode 100644 index e36dabc..0000000 --- a/src/shared/count_tabs.rs +++ /dev/null @@ -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, 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)), - ), - ) -} diff --git a/src/shared/mod.rs b/src/shared/mod.rs index c528476..d1a1bb6 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -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, diff --git a/src/shared/segmented_control.rs b/src/shared/segmented_control.rs index 817a45d..cc58840 100644 --- a/src/shared/segmented_control.rs +++ b/src/shared/segmented_control.rs @@ -9,9 +9,7 @@ type ChangeHandler = Rc; /// 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, disabled: bool, /// Why a disabled segment cannot be chosen. tooltip: Option, @@ -41,18 +37,11 @@ impl Segment { pub fn new(label: impl Into) -> 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()), - ) - }) })) } } diff --git a/tests/ui/docker.rs b/tests/ui/docker.rs index 10aa22e..33e1f78 100644 --- a/tests/ui/docker.rs +++ b/tests/ui/docker.rs @@ -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. diff --git a/tests/ui/support/mod.rs b/tests/ui/support/mod.rs index c72bd1e..0ca110d 100644 --- a/tests/ui/support/mod.rs +++ b/tests/ui/support/mod.rs @@ -212,22 +212,3 @@ pub fn in_frame( 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 = (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:?}" - ); -}