diff --git a/README.md b/README.md index 5935a00..a3b8e09 100644 --- a/README.md +++ b/README.md @@ -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 存于系统数据目录 diff --git a/gpui-app/src/store.rs b/gpui-app/src/store.rs index 455c711..09eeb10 100644 --- a/gpui-app/src/store.rs +++ b/gpui-app/src/store.rs @@ -229,6 +229,9 @@ pub struct UiState { pub open_tabs: Vec, #[serde(default)] pub active_tab: Option, + /// 连接侧栏抽屉是否收起(抽屉式折叠,Ctrl+B 切换) + #[serde(default)] + pub sidebar_collapsed: bool, } impl Default for Storage { diff --git a/gpui-app/src/ui/app/actions.rs b/gpui-app/src/ui/app/actions.rs index f793814..e94a7d0 100644 --- a/gpui-app/src/ui/app/actions.rs +++ b/gpui-app/src/ui/app/actions.rs @@ -106,9 +106,17 @@ pub(super) fn close_tab(&mut self, id: &str, cx: &mut Context) { 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.sidebar_collapsed = !self.sidebar_collapsed; + self.persist_ui_state(); + cx.notify(); + } + // ── 订阅 ────────────────────────────────────────────────────────────── pub fn add_subscription(&mut self, sub: Subscription) { diff --git a/gpui-app/src/ui/app/mod.rs b/gpui-app/src/ui/app/mod.rs index 587be08..4c9260c 100644 --- a/gpui-app/src/ui/app/mod.rs +++ b/gpui-app/src/ui/app/mod.rs @@ -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, _pump: gpui_kit::Task<()>, - // ── 持久化数据 ── pub connections: Vec, pub subscriptions: Vec, @@ -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::(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)), ) } diff --git a/gpui-app/src/ui/app/sidebar.rs b/gpui-app/src/ui/app/sidebar.rs index 1e099dc..9affcf3 100644 --- a/gpui-app/src/ui/app/sidebar.rs +++ b/gpui-app/src/ui/app/sidebar.rs @@ -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) -> impl IntoElement { - let query = self.search.read(cx).value().to_lowercase(); + /// 侧栏收起后的窄 rail:抽屉把手(拉回侧栏)+ 新建连接入口。 + pub(super) fn render_sidebar_rail(&mut self, cx: &mut Context) -> 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) -> 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) @@ -186,14 +217,28 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context, @@ -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() + }) } } diff --git a/gpui-app/src/ui/connection_view/publish.rs b/gpui-app/src/ui/connection_view/publish.rs index d747829..2a753ea 100644 --- a/gpui-app/src/ui/connection_view/publish.rs +++ b/gpui-app/src/ui/connection_view/publish.rs @@ -18,6 +18,37 @@ use crate::ui::widgets::{ field, KvEditor }; use super::*; impl ConnectionView { + /// 发布栏收起后的细条:抽屉把手。 + pub(super) fn render_publish_rail(&self, cx: &mut Context) -> 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) { @@ -295,6 +326,18 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context) -> 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(); + })), ), ); diff --git a/gpui-app/src/ui/connection_view/subscribe.rs b/gpui-app/src/ui/connection_view/subscribe.rs index 96b11a7..1c5800e 100644 --- a/gpui-app/src/ui/connection_view/subscribe.rs +++ b/gpui-app/src/ui/connection_view/subscribe.rs @@ -17,6 +17,31 @@ use crate::ui::widgets::field; use super::*; impl ConnectionView { + /// 订阅面板收起后的窄 rail:抽屉把手。 + pub(super) fn render_subs_rail(&self, cx: &mut Context) -> 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) -> 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)) }