feat(ui): 侧栏/订阅面板/发布栏抽屉式折叠
- 连接侧栏:Ctrl+B 或把手按钮收起为 40px 窄 rail(展开把手 + 新建连接), 状态随 ui-state.json 持久化;展开时标题行带收起按钮 - 订阅面板:标题行收起按钮,收起后左侧 32px rail 拉回 - 发布面板:工具条右端把手收起为细条(34px),细条上拉回; 收起后全局 Ctrl+Enter 发送仍可用
This commit is contained in:
@@ -22,7 +22,8 @@
|
||||
- **阿里云 IoT** — Token(GroupId@@@DeviceId + HMAC-SHA1 签名)与一机一密两种鉴权方式一键生成连接
|
||||
- **连接测试** — 表单内握手测试(超时可配置),保存前验证连通性
|
||||
- **日志面板** — 连接级事件日志(CONNACK/SUBACK/PUBACK/错误/重连等),按日落盘 `mqttx-YYYY-MM-DD.log`,设置内一键打开日志目录
|
||||
- **快捷键** — `Ctrl+N` 新建连接、`Ctrl+,` 设置、`Ctrl+Shift+E` 导出、`Ctrl+Shift+I` 导入、`Ctrl+Shift+R` 资源监控、`Ctrl+Enter` 发送消息(全局生效)、`Enter` 提交订阅、`Esc` 退出编辑/清空输入
|
||||
- **快捷键** — `Ctrl+N` 新建连接、`Ctrl+,` 设置、`Ctrl+Shift+E` 导出、`Ctrl+Shift+I` 导入、`Ctrl+Shift+R` 资源监控、`Ctrl+Enter` 发送消息(全局生效)、`Ctrl+B` 收起/展开连接侧栏、`Enter` 提交订阅、`Esc` 退出编辑/清空输入
|
||||
- **抽屉式布局** — 连接侧栏(持久化,Ctrl+B)、订阅面板、底部发布面板均可一键收起为窄条抽屉,最大化消息流空间
|
||||
- **主题** — Linear 风格配色(近黑/纯白扁平底 + 靛蓝 #5e6ad2 主色,14px 紧凑排版、6px 圆角),浅色 / 深色 / 跟随系统
|
||||
- **设置** — 自动检查更新开关、消息缓存条数、时间戳毫秒、数据目录展示与打开、关于页(版本 / 许可)
|
||||
- **本地持久化** — 连接、订阅、预设、变量、设置以 JSON 存于系统数据目录
|
||||
|
||||
@@ -229,6 +229,9 @@ pub struct UiState {
|
||||
pub open_tabs: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub active_tab: Option<String>,
|
||||
/// 连接侧栏抽屉是否收起(抽屉式折叠,Ctrl+B 切换)
|
||||
#[serde(default)]
|
||||
pub sidebar_collapsed: bool,
|
||||
}
|
||||
|
||||
impl Default for Storage {
|
||||
|
||||
@@ -106,9 +106,17 @@ pub(super) fn close_tab(&mut self, id: &str, cx: &mut Context<Self>) {
|
||||
self.storage.save_ui_state(&UiState {
|
||||
open_tabs: self.open_tabs.clone(),
|
||||
active_tab: self.active_tab.clone(),
|
||||
sidebar_collapsed: self.sidebar_collapsed,
|
||||
});
|
||||
}
|
||||
|
||||
/// 收起/展开连接侧栏抽屉(Ctrl+B 或 rail/标题栏按钮),状态随 ui-state 持久化。
|
||||
pub fn toggle_sidebar(&mut self, cx: &mut Context<Self>) {
|
||||
self.sidebar_collapsed = !self.sidebar_collapsed;
|
||||
self.persist_ui_state();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// ── 订阅 ──────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn add_subscription(&mut self, sub: Subscription) {
|
||||
|
||||
@@ -48,7 +48,7 @@ const MAX_LOGS: usize = 3000;
|
||||
const EVENT_QUEUE_CAPACITY: usize = 1024;
|
||||
|
||||
// 应用级快捷键动作(context=None:任意焦点状态下都匹配)。
|
||||
gpui_kit::actions!(mqttx, [NewConnection, OpenSettings, ExportConnections, ImportConnections, OpenResourceMonitor, PublishMessage]);
|
||||
gpui_kit::actions!(mqttx, [NewConnection, OpenSettings, ExportConnections, ImportConnections, OpenResourceMonitor, PublishMessage, ToggleSidebar]);
|
||||
|
||||
/// 侧边栏分组过滤;chips 只做列表过滤,分组重命名/删除通过编辑连接的分组字段完成。
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
@@ -80,7 +80,6 @@ pub struct MqttXApp {
|
||||
storage: Storage,
|
||||
pub engine: std::sync::Arc<MqttEngine>,
|
||||
_pump: gpui_kit::Task<()>,
|
||||
|
||||
// ── 持久化数据 ──
|
||||
pub connections: Vec<ConnectionConfig>,
|
||||
pub subscriptions: Vec<Subscription>,
|
||||
@@ -115,6 +114,8 @@ pub struct MqttXApp {
|
||||
_appearance_obs: gpui_kit::Subscription,
|
||||
/// 窗口置顶状态(标题栏钉子按钮切换)
|
||||
pinned: bool,
|
||||
/// 连接侧栏抽屉收起状态(Ctrl+B / rail 按钮切换,随 ui-state 持久化)
|
||||
sidebar_collapsed: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -321,6 +322,7 @@ impl MqttXApp {
|
||||
KeyBinding::new("ctrl-shift-i", ImportConnections, None),
|
||||
KeyBinding::new("ctrl-shift-r", OpenResourceMonitor, None),
|
||||
KeyBinding::new("ctrl-enter", PublishMessage, None),
|
||||
KeyBinding::new("ctrl-b", ToggleSidebar, None),
|
||||
]);
|
||||
let weak = cx.entity().downgrade();
|
||||
// Context 上有同名 on_action(绘制期注册),这里必须走 App 的全局注册
|
||||
@@ -392,6 +394,14 @@ impl MqttXApp {
|
||||
});
|
||||
});
|
||||
});
|
||||
// Ctrl+B 抽屉式收起/展开连接侧栏
|
||||
let weak = cx.entity().downgrade();
|
||||
App::on_action::<ToggleSidebar>(cx, move |_, cx| {
|
||||
let w = weak.clone();
|
||||
run_on_active_window(cx, w, |entity, _, cx| {
|
||||
entity.update(cx, |app, cx| app.toggle_sidebar(cx));
|
||||
});
|
||||
});
|
||||
|
||||
Self {
|
||||
storage,
|
||||
@@ -419,6 +429,7 @@ impl MqttXApp {
|
||||
log_file: None,
|
||||
_appearance_obs: appearance_obs,
|
||||
pinned: false,
|
||||
sidebar_collapsed: ui_state.sidebar_collapsed,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,7 +449,12 @@ impl Render for MqttXApp {
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.min_h(px(0.))
|
||||
.child(self.render_sidebar(window, cx))
|
||||
// 连接侧栏抽屉:收起时只剩窄 rail(Ctrl+B 或 rail 按钮拉回)
|
||||
.child(if self.sidebar_collapsed {
|
||||
self.render_sidebar_rail(cx).into_any_element()
|
||||
} else {
|
||||
self.render_sidebar(window, cx).into_any_element()
|
||||
})
|
||||
.child(self.render_main(window, cx)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,39 @@ use crate::ui::widgets::status_color;
|
||||
use super::*;
|
||||
|
||||
impl MqttXApp {
|
||||
pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let query = self.search.read(cx).value().to_lowercase();
|
||||
/// 侧栏收起后的窄 rail:抽屉把手(拉回侧栏)+ 新建连接入口。
|
||||
pub(super) fn render_sidebar_rail(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w(px(40.))
|
||||
.h_full()
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.pt_2()
|
||||
.gap_1()
|
||||
.bg(cx.theme().sidebar)
|
||||
.text_color(cx.theme().sidebar_foreground)
|
||||
.border_r_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
Button::new("sidebar-rail-expand")
|
||||
.icon(IconName::PanelLeftOpen)
|
||||
.ghost()
|
||||
.small()
|
||||
.tooltip("展开连接侧栏(Ctrl+B)")
|
||||
.on_click(cx.listener(|this, _, _, cx| this.toggle_sidebar(cx))),
|
||||
)
|
||||
.child(
|
||||
Button::new("sidebar-rail-add")
|
||||
.icon(IconName::Plus)
|
||||
.ghost()
|
||||
.small()
|
||||
.tooltip("新建连接")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.open_connection_form(None, window, cx);
|
||||
})),
|
||||
)
|
||||
}
|
||||
pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let query = self.search.read(cx).value().to_lowercase();
|
||||
let search = self.search.clone();
|
||||
// 分组被改名/删除后过滤器可能悬空(列表恒空),渲染前复位到「全部」
|
||||
let dangling = matches!(&self.group_filter, GroupFilter::Named(name)
|
||||
@@ -185,6 +216,19 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
||||
.child(total.to_string()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Button::new("sidebar-collapse")
|
||||
.icon(IconName::PanelLeftClose)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip("收起侧栏(Ctrl+B)")
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.toggle_sidebar(cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("sidebar-add")
|
||||
.icon(IconName::Plus)
|
||||
@@ -195,6 +239,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
||||
this.open_connection_form(None, window, cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(v_flex().px_2().pb_2().child(Input::new(&search).small().prefix(
|
||||
gpui_kit::component::Icon::new(IconName::Search).small(),
|
||||
|
||||
@@ -180,6 +180,10 @@ pub struct ConnectionView {
|
||||
detail_format: DetailFormat,
|
||||
/// 消息流暂停滚动:消息照常入环形缓冲,仅不再触发本视图重绘
|
||||
paused: bool,
|
||||
/// 订阅面板抽屉收起状态(会话内有效)
|
||||
subs_collapsed: bool,
|
||||
/// 底部发布栏抽屉收起状态(会话内有效)
|
||||
publish_collapsed: bool,
|
||||
|
||||
// 预设
|
||||
preset_name: Entity<InputState>,
|
||||
@@ -257,6 +261,8 @@ impl ConnectionView {
|
||||
msg_dir: DirFilter::All,
|
||||
detail_format: DetailFormat::Auto,
|
||||
paused: false,
|
||||
subs_collapsed: false,
|
||||
publish_collapsed: false,
|
||||
preset_name,
|
||||
show_vars: true,
|
||||
var_rows: Vec::new(),
|
||||
@@ -299,7 +305,12 @@ impl Render for ConnectionView {
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.min_h(px(0.))
|
||||
.child(self.render_subscriptions(cx))
|
||||
// 订阅面板抽屉:收起时只剩窄 rail
|
||||
.child(if self.subs_collapsed {
|
||||
self.render_subs_rail(cx).into_any_element()
|
||||
} else {
|
||||
self.render_subscriptions(cx).into_any_element()
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
@@ -334,7 +345,11 @@ impl Render for ConnectionView {
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(self.render_publish_bar(cx))
|
||||
.child(if self.publish_collapsed {
|
||||
self.render_publish_rail(cx).into_any_element()
|
||||
} else {
|
||||
self.render_publish_bar(cx).into_any_element()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,37 @@ use crate::ui::widgets::{ field, KvEditor };
|
||||
use super::*;
|
||||
|
||||
impl ConnectionView {
|
||||
/// 发布栏收起后的细条:抽屉把手。
|
||||
pub(super) fn render_publish_rail(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.h(px(34.))
|
||||
.px_3()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.flex_shrink_0()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().secondary)
|
||||
.child(
|
||||
Button::new("pub-rail-expand")
|
||||
.icon(IconName::PanelBottomOpen)
|
||||
.ghost()
|
||||
.small()
|
||||
.tooltip("展开发布面板")
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.publish_collapsed = false;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("发布面板已收起"),
|
||||
)
|
||||
}
|
||||
|
||||
/// 发布当前面板参数。入口有三:发送按钮、发布主题框内 Enter、
|
||||
/// 全局 Ctrl+Enter action(app/mod.rs 注册)。
|
||||
pub(crate) fn do_publish(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -295,6 +326,18 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
.disabled(!connected)
|
||||
.when(!connected, |b| b.tooltip("未连接,无法发送"))
|
||||
.on_click(cx.listener(|this, _, window, cx| this.do_publish(window, cx))),
|
||||
)
|
||||
// 发布栏抽屉把手:收起整个底部面板
|
||||
.child(
|
||||
Button::new("pub-collapse")
|
||||
.icon(IconName::PanelBottom)
|
||||
.ghost()
|
||||
.small()
|
||||
.tooltip("收起发布面板")
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.publish_collapsed = true;
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -17,6 +17,31 @@ use crate::ui::widgets::field;
|
||||
use super::*;
|
||||
|
||||
impl ConnectionView {
|
||||
/// 订阅面板收起后的窄 rail:抽屉把手。
|
||||
pub(super) fn render_subs_rail(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w(px(32.))
|
||||
.h_full()
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.pt_2()
|
||||
.bg(cx.theme().sidebar)
|
||||
.text_color(cx.theme().sidebar_foreground)
|
||||
.border_r_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
Button::new("subs-rail-expand")
|
||||
.icon(IconName::PanelLeftOpen)
|
||||
.ghost()
|
||||
.small()
|
||||
.tooltip("展开订阅面板")
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.subs_collapsed = false;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
/// 订阅按 (连接, 主题) 唯一:先摘掉旧条目再走 `add_subscription`,
|
||||
/// 借它入列 + `save_subscriptions` 落盘的逻辑完成 upsert(storage 字段是私有的)。
|
||||
/// 落盘顺序以 add 时为准,入列后移回原位以保持界面顺序稳定。
|
||||
@@ -743,7 +768,19 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
||||
.h_11()
|
||||
.px_3()
|
||||
.items_center()
|
||||
.child(div().text_sm().font_semibold().child("订阅")),
|
||||
.justify_between()
|
||||
.child(div().text_sm().font_semibold().child("订阅"))
|
||||
.child(
|
||||
Button::new("subs-collapse")
|
||||
.icon(IconName::PanelLeftClose)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip("收起订阅面板")
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.subs_collapsed = true;
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(div().flex_1().overflow_y_scrollbar().px_2().child(list))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user