Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/assets/assets/icons/pin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions crates/core/src/keybindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
63 changes: 63 additions & 0 deletions crates/core/src/tab_container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,10 @@ pub struct TabContainer {
tab_list: Option<Entity<ListState<TabListDelegate>>>,
closing_tabs: HashSet<SharedString>,
show_window_controls: bool,
/// 窗口置顶切换回调,由上层注入;为 None 时不渲染置顶按钮
on_toggle_always_on_top: Option<Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>>,
/// 当前窗口置顶状态读取器,由上层注入
is_always_on_top: Option<Arc<dyn Fn() -> bool + Send + Sync>>,
/// Pinned tab that stays fixed before the scrollable tab list
pinned_tab: Option<TabItem>,
/// Whether the pinned tab is currently active (showing its content)
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -787,6 +793,18 @@ impl TabContainer {
self
}

/// 注入窗口置顶切换逻辑:`on_toggle` 在用户点击置顶按钮时调用,
/// `is_active` 在每次渲染时被调用以决定按钮的视觉状态。
pub fn with_always_on_top_control(
mut self,
on_toggle: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
is_active: Arc<dyn Fn() -> 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<Self>) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -2051,6 +2077,43 @@ impl TabContainer {
})
.child(Icon::new(icon).with_size(Size::Small))
}

/// 渲染窗口置顶按钮,位于最小化按钮左侧。
/// 该按钮不声明系统窗口控制区,点击时由上层注入的回调完成切换。
fn render_always_on_top_button(
&self,
on_toggle: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
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 {
Expand Down
2 changes: 2 additions & 0 deletions crates/ui/src/icon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ pub enum IconName {
PanelRightClose,
PanelRightOpen,
Pause,
Pin,
Play,
Plus,
Redo,
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions main/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions main/locales/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: 切换面板缩放
Expand Down
91 changes: 90 additions & 1 deletion main/src/onetcli_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
};
use gpui_component::WindowExt;
use one_core::keybindings::{action_id, rebind_keybindings, shortcuts_for};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};

Check failure on line 8 in main/src/onetcli_app.rs

View workflow job for this annotation

GitHub Actions / Test (x86_64-linux-gnu, ubuntu-latest)

unused import: `RawWindowHandle`
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

static ALWAYS_ON_TOP: AtomicBool = AtomicBool::new(false);

actions!(
onetcli_app,
Expand All @@ -19,6 +24,7 @@
ActivateTab8,
ActivateTab9,
ToggleFullscreen,
ToggleAlwaysOnTop,
MinimizeWindow,
DuplicateTab,
QuitApp,
Expand Down Expand Up @@ -98,6 +104,57 @@
});
}

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<()> {

Check failure on line 121 in main/src/onetcli_app.rs

View workflow job for this annotation

GitHub Actions / Test (x86_64-linux-gnu, ubuntu-latest)

unused variable: `always_on_top`
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;
Expand Down Expand Up @@ -270,6 +327,15 @@
.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,
Expand Down Expand Up @@ -315,6 +381,13 @@
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,
Expand Down Expand Up @@ -343,6 +416,7 @@
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| {
Expand Down Expand Up @@ -413,7 +487,22 @@

#[cfg(not(target_os = "macos"))]
{
container = container.with_window_controls(true)
// 窗口置顶按钮注入:点击时切换置顶并刷新按钮视觉状态
let on_toggle: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync> =
Arc::new(|_window: &mut Window, cx: &mut App| {
toggle_always_on_top(cx);
if let Some(tab_container) = cx
.try_global::<GlobalTabContainer>()
.map(|global| global.tab_container.clone())
{
tab_container.update(cx, |_, cx| cx.notify());
}
});
let is_active: Arc<dyn Fn() -> 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
Expand Down
7 changes: 7 additions & 0 deletions main/src/setting_tab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
Loading