Docker 的标签改用可横向滚动的下划线标签,与系统服务一致
均分宽度放不下英文的 "Containers 12"。不管什么语言,标签都按文字和数量的宽度排, 放不下时横向滚动。去掉只给 Docker 用的 count_tabs 和分段控件的计数。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
82c2c8b277
commit
7b0dad9b41
8 files changed
+79
-135
No files matched your search
@@ -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
@@ -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"),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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()),
|
||||
)
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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:?}"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user