From e35c2f854b83d795c4b2626180be4a5a9fa4de24 Mon Sep 17 00:00:00 2001 From: 414522569 <414522569@qq.com> Date: Tue, 23 Jun 2026 13:49:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AA=97=E5=8F=A3=E7=BD=AE?= =?UTF-8?q?=E9=A1=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/assets/assets/icons/pin.svg | 1 + crates/core/src/keybindings.rs | 1 + crates/core/src/tab_container.rs | 63 +++++++++++++++++++++ crates/ui/src/icon.rs | 2 + main/Cargo.toml | 2 + main/locales/main.yml | 4 ++ main/src/onetcli_app.rs | 91 +++++++++++++++++++++++++++++- main/src/setting_tab.rs | 7 +++ 8 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 crates/assets/assets/icons/pin.svg diff --git a/crates/assets/assets/icons/pin.svg b/crates/assets/assets/icons/pin.svg new file mode 100644 index 0000000000..ff27e430f6 --- /dev/null +++ b/crates/assets/assets/icons/pin.svg @@ -0,0 +1 @@ + diff --git a/crates/core/src/keybindings.rs b/crates/core/src/keybindings.rs index acaed4b070..c7b8c1fada 100644 --- a/crates/core/src/keybindings.rs +++ b/crates/core/src/keybindings.rs @@ -10,6 +10,7 @@ pub mod action_id { pub const WINDOW_TOGGLE_ZOOM: &str = "window.toggle_zoom"; pub const WINDOW_CLOSE_PANEL: &str = "window.close_panel"; pub const WINDOW_TOGGLE_FULLSCREEN: &str = "window.toggle_fullscreen"; + pub const WINDOW_TOGGLE_ALWAYS_ON_TOP: &str = "window.toggle_always_on_top"; pub const APP_DUPLICATE_TAB: &str = "app.duplicate_tab"; pub const APP_QUIT: &str = "app.quit"; pub const HOME_QUICK_OPEN: &str = "home.quick_open"; diff --git a/crates/core/src/tab_container.rs b/crates/core/src/tab_container.rs index cf40baddf6..26b970f581 100644 --- a/crates/core/src/tab_container.rs +++ b/crates/core/src/tab_container.rs @@ -701,6 +701,10 @@ pub struct TabContainer { tab_list: Option>>, closing_tabs: HashSet, show_window_controls: bool, + /// 窗口置顶切换回调,由上层注入;为 None 时不渲染置顶按钮 + on_toggle_always_on_top: Option>, + /// 当前窗口置顶状态读取器,由上层注入 + is_always_on_top: Option bool + Send + Sync>>, /// Pinned tab that stays fixed before the scrollable tab list pinned_tab: Option, /// Whether the pinned tab is currently active (showing its content) @@ -732,6 +736,8 @@ impl TabContainer { tab_list: None, closing_tabs: HashSet::new(), show_window_controls: false, + on_toggle_always_on_top: None, + is_always_on_top: None, pinned_tab: None, pinned_tab_active: false, } @@ -787,6 +793,18 @@ impl TabContainer { self } + /// 注入窗口置顶切换逻辑:`on_toggle` 在用户点击置顶按钮时调用, + /// `is_active` 在每次渲染时被调用以决定按钮的视觉状态。 + pub fn with_always_on_top_control( + mut self, + on_toggle: Arc, + is_active: Arc bool + Send + Sync>, + ) -> Self { + self.on_toggle_always_on_top = Some(on_toggle); + self.is_always_on_top = Some(is_active); + self + } + /// Set a pinned tab that stays fixed before the scrollable tab list. /// The pinned tab is always visible and cannot be scrolled away. pub fn set_pinned_tab(&mut self, tab: TabItem, cx: &mut Context) { @@ -1966,6 +1984,14 @@ impl TabContainer { .items_center() .flex_shrink_0() .h_full() + .when_some(self.on_toggle_always_on_top.clone(), |el, on_toggle| { + let is_active = self + .is_always_on_top + .as_ref() + .map(|probe| probe()) + .unwrap_or(false); + el.child(self.render_always_on_top_button(on_toggle, is_active)) + }) .child(self.render_control_button( "minimize", IconName::WindowMinimize, @@ -2051,6 +2077,43 @@ impl TabContainer { }) .child(Icon::new(icon).with_size(Size::Small)) } + + /// 渲染窗口置顶按钮,位于最小化按钮左侧。 + /// 该按钮不声明系统窗口控制区,点击时由上层注入的回调完成切换。 + fn render_always_on_top_button( + &self, + on_toggle: Arc, + is_active: bool, + ) -> impl IntoElement { + // 置顶激活时用琥珀色高亮,提示当前窗口已置顶 + let icon_color = if is_active { + gpui::rgb(0xfbbf24) + } else { + gpui::rgb(0xffffff) + }; + + div() + .id("always-on-top") + .flex() + .w(px(34.0)) + .h_full() + .flex_shrink_0() + .justify_center() + .content_center() + .items_center() + .text_color(icon_color) + .hover(move |style| style.bg(gpui::rgb(0x3a3a3a)).text_color(icon_color)) + .active(move |style| style.bg(gpui::rgb(0x2a2a2a)).text_color(icon_color)) + .on_mouse_down(MouseButton::Left, move |_, window, cx| { + window.prevent_default(); + cx.stop_propagation(); + }) + .on_click(move |_, window, cx| { + cx.stop_propagation(); + on_toggle(window, cx); + }) + .child(Icon::new(IconName::Pin).with_size(Size::Small)) + } } impl Focusable for TabContainer { diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 38852f33f3..28ecf801ea 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -109,6 +109,7 @@ pub enum IconName { PanelRightClose, PanelRightOpen, Pause, + Pin, Play, Plus, Redo, @@ -304,6 +305,7 @@ impl IconNamed for IconName { Self::PanelRightClose => "icons/panel-right-close.svg", Self::PanelRightOpen => "icons/panel-right-open.svg", Self::Pause => "icons/pause.svg", + Self::Pin => "icons/pin.svg", Self::Play => "icons/play.svg", Self::Plus => "icons/plus.svg", Self::Redo => "icons/redo.svg", diff --git a/main/Cargo.toml b/main/Cargo.toml index 7dc01e931b..c369861dfa 100644 --- a/main/Cargo.toml +++ b/main/Cargo.toml @@ -46,9 +46,11 @@ global-hotkey.workspace = true sha2 = { workspace = true } flate2 = "1" tar = "0.4" +raw-window-handle = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] zip = { version = "2", default-features = false, features = ["deflate"] } +windows = { version = "0.62", features = ["Win32_Foundation", "Win32_UI_WindowsAndMessaging"] } [target.'cfg(target_os = "windows")'.build-dependencies] winresource = "0.1" diff --git a/main/locales/main.yml b/main/locales/main.yml index 218b42b20e..de204e3a0a 100644 --- a/main/locales/main.yml +++ b/main/locales/main.yml @@ -982,6 +982,10 @@ Settings: en: Toggle Fullscreen zh-CN: 切换全屏 zh-HK: 切換全螢幕 + toggle_always_on_top: + en: Toggle Always On Top + zh-CN: 切换窗口置顶 + zh-HK: 切換視窗置頂 toggle_zoom: en: Toggle Panel Zoom zh-CN: 切换面板缩放 diff --git a/main/src/onetcli_app.rs b/main/src/onetcli_app.rs index ebbf148247..2b9dbfec4e 100644 --- a/main/src/onetcli_app.rs +++ b/main/src/onetcli_app.rs @@ -5,6 +5,11 @@ use gpui::{ }; use gpui_component::WindowExt; use one_core::keybindings::{action_id, rebind_keybindings, shortcuts_for}; +use raw_window_handle::{HasWindowHandle, RawWindowHandle}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +static ALWAYS_ON_TOP: AtomicBool = AtomicBool::new(false); actions!( onetcli_app, @@ -19,6 +24,7 @@ actions!( ActivateTab8, ActivateTab9, ToggleFullscreen, + ToggleAlwaysOnTop, MinimizeWindow, DuplicateTab, QuitApp, @@ -98,6 +104,57 @@ fn toggle_fullscreen(cx: &mut App) { }); } +fn toggle_always_on_top(cx: &mut App) { + let Some(active_window) = cx.active_window() else { + return; + }; + cx.defer(move |cx| { + _ = active_window.update(cx, |_, window, _| { + let next = !ALWAYS_ON_TOP.load(Ordering::Relaxed); + if set_window_always_on_top(window, next).is_ok() { + ALWAYS_ON_TOP.store(next, Ordering::Relaxed); + } + }); + }); +} + +fn set_window_always_on_top(window: &Window, always_on_top: bool) -> anyhow::Result<()> { + let handle = HasWindowHandle::window_handle(window) + .map_err(|err| anyhow::anyhow!("获取窗口句柄失败: {err:?}"))? + .as_raw(); + match handle { + #[cfg(target_os = "windows")] + RawWindowHandle::Win32(handle) => set_windows_always_on_top(handle.hwnd.get(), always_on_top), + _ => Err(anyhow::anyhow!("当前平台暂不支持窗口置顶")), + } +} + +#[cfg(target_os = "windows")] +fn set_windows_always_on_top(hwnd: isize, always_on_top: bool) -> anyhow::Result<()> { + use windows::Win32::Foundation::HWND; + use windows::Win32::UI::WindowsAndMessaging::{ + HWND_NOTOPMOST, HWND_TOPMOST, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SetWindowPos, + }; + + let insert_after = if always_on_top { + HWND_TOPMOST + } else { + HWND_NOTOPMOST + }; + unsafe { + SetWindowPos( + HWND(hwnd as *mut _), + Some(insert_after), + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, + )?; + } + Ok(()) +} + fn duplicate_tab(cx: &mut App) { let Some(active_window) = cx.active_window() else { return; @@ -270,6 +327,15 @@ fn init_keybindings(cx: &App) -> Vec { .into_iter() .map(|key| KeyBinding::new(&key, ToggleFullscreen, None)), ); + keybindings.extend( + shortcuts_for( + cx, + action_id::WINDOW_TOGGLE_ALWAYS_ON_TOP, + &[default_shortcut("ctrl-cmd-t", "ctrl-alt-t")], + ) + .into_iter() + .map(|key| KeyBinding::new(&key, ToggleAlwaysOnTop, None)), + ); keybindings.extend( shortcuts_for( cx, @@ -315,6 +381,13 @@ fn refreshable_keybindings(cx: &App) -> Vec { None, ToggleFullscreen, )); + keybindings.extend(rebind_keybindings( + cx, + action_id::WINDOW_TOGGLE_ALWAYS_ON_TOP, + &[default_shortcut("ctrl-cmd-t", "ctrl-alt-t")], + None, + ToggleAlwaysOnTop, + )); keybindings.extend(rebind_keybindings( cx, action_id::APP_DUPLICATE_TAB, @@ -343,6 +416,7 @@ fn init_action_handlers(cx: &mut App) { cx.on_action(|_: &ActivateTab8, cx| activate_tab_by_number(8, cx)); cx.on_action(|_: &ActivateTab9, cx| activate_tab_by_number(9, cx)); cx.on_action(|_: &ToggleFullscreen, cx| toggle_fullscreen(cx)); + cx.on_action(|_: &ToggleAlwaysOnTop, cx| toggle_always_on_top(cx)); cx.on_action(|_: &DuplicateTab, cx| duplicate_tab(cx)); cx.on_action(|_: &QuitApp, cx| quit_app(cx)); cx.on_action(|_: &OpenConnectionQuickOpen, cx| { @@ -413,7 +487,22 @@ impl OnetCliApp { #[cfg(not(target_os = "macos"))] { - container = container.with_window_controls(true) + // 窗口置顶按钮注入:点击时切换置顶并刷新按钮视觉状态 + let on_toggle: Arc = + Arc::new(|_window: &mut Window, cx: &mut App| { + toggle_always_on_top(cx); + if let Some(tab_container) = cx + .try_global::() + .map(|global| global.tab_container.clone()) + { + tab_container.update(cx, |_, cx| cx.notify()); + } + }); + let is_active: Arc bool + Send + Sync> = + Arc::new(|| ALWAYS_ON_TOP.load(Ordering::Relaxed)); + container = container + .with_window_controls(true) + .with_always_on_top_control(on_toggle, is_active); } container diff --git a/main/src/setting_tab.rs b/main/src/setting_tab.rs index f5e1d76cf5..842aacfccf 100644 --- a/main/src/setting_tab.rs +++ b/main/src/setting_tab.rs @@ -1237,6 +1237,13 @@ const WINDOW_SHORTCUTS: &[ShortcutEntry] = &[ action_id: Some(action_id::WINDOW_TOGGLE_FULLSCREEN), system_hotkey: false, }, + ShortcutEntry { + keys_macos: &["ctrl-cmd-t"], + keys_other: &["ctrl-alt-t"], + label_key: "Settings.Shortcuts.toggle_always_on_top", + action_id: Some(action_id::WINDOW_TOGGLE_ALWAYS_ON_TOP), + system_hotkey: false, + }, ShortcutEntry { keys_macos: &["shift-escape"], keys_other: &["shift-escape"],