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"],