设置新增「应用 › 窗口」:记住窗口大小和位置

- 设置左栏在「关于」上面新增「应用」,「窗口」组两个开关,默认都打开:
  记住窗口大小(含最大化状态)、记住窗口位置(连同所在屏幕),下次启动
  时生效。
- 窗口移动、缩放停下后记进数据目录的 window.json,退出时再记一次。
- 恢复时那块屏幕不在了、或窗口大半在屏幕外,就放在屏幕中间;比屏幕大
  就缩到能放下。全屏不记,按退出全屏后的大小恢复。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XuzandClaude Opus 5.5 committed 2026-10-06 23:17:55 +08:00
1 parent 1bab1bff50
commit e9700d0e32
15 files changed
+541 -9

No files matched your search

+1
View File
@@ -26,6 +26,7 @@ on:
- "src/sftp/upload.rs"
- "src/credential/credential_dialog.rs"
- "src/settings/settings_panel.rs"
- "src/workspace/window_state.rs"
- "src/testing.rs"
- "tests/cli_bin.rs"
- "build.rs"
+5
View File
@@ -56,6 +56,7 @@ ShellRS(crate 与二进制都叫 `shellrs`)是 Xshell / WinSCP 式的 SSH
- 双击、回车、F4、右键「编辑」都打开它,本地文件也行;新建文件后直接打开。
- 只认 UTF-8(不做 GBK),上限 5 MB,约 15 种语言语法高亮。
- 远程文件原地改写:截断后写原文件,不走 `.filepart` 加改名。保存前核对,被改过就问「覆盖」。
- **设置 › 应用 › 窗口**:「记住窗口大小」(含最大化)和「记住窗口位置」两个开关,默认都开,下次启动生效。屏幕不在了或窗口不在屏幕上就居中,比屏幕大就缩小;全屏不记。
- **终端通知**:只做程序请求的(OSC 9 / 777)和响铃,不做 OSC 133 长命令通知:那要每台服务器装 shell 集成,收益不抵成本。不在前台发系统通知,在前台而终端在别的标签发应用内通知;响铃在终端就在眼前时不提醒。
- **主题**:「应用外观」(浅色 / 深色 / 跟随系统)决定深浅;「外观 › 主题」左右两栏里浅色、深色各选一套,按当前外观生效。组里第一项「界面跟随主题」默认关闭:界面是中性的黑白灰(gpui-kit 默认主题),只有终端按主题配色;打开后界面也按主题配色。用户试过默认跟随,觉得中性界面更合适,别改回去。页面「重置」对卡片和开关各管各的。
- 左栏只列浅色主题,右栏只列深色主题;卡片是名称加示例输出,选中的整张加底色,右上角是蓝底(主题的 `blue`)对勾。
@@ -143,6 +144,10 @@ cargo test -- --ignored # 会读写真实钥匙串的测试
- 改表要升版本、在 `STEPS` 里加一步,并加一个「用冻结的旧 schema 升级、再和新库比对」的测试。
- 加列直接 `ALTER TABLE ADD COLUMN`,在 `SCHEMA` 里也写在最后一列之后。
- 库只往前升:旧版本遇到新库直接报错、不动文件。迁移前先 `VACUUM INTO` 备份。
- **窗口的大小和位置存在 `window.json`,不进 `settings.json`**(`workspace/window_state.rs`):移动、缩放时 `observe_window_bounds` 记下,停 0.5 秒再写;退出时再写一次,用 App 级的 `on_app_quit`,因为 Windows / Linux 关窗口时工作区已经先没了。只有 `Workspace::new` 记,UI 测试不碰这个文件。
- 启动时 `main` 先读设置和 `window.json` 再开窗口,`window_options` 按两个开关算 `window_bounds` 和 `display_id`。
- 屏幕按 `PlatformDisplay::uuid` 认(`DisplayId` 重启会变)。GPUI 的窗口坐标在 macOS 上从所在屏幕左上角算,别处是整个桌面,原样存、原样交回,配上同一个 `display_id` 就能落回原处。
- macOS 的 `window_bounds()` 从不报最大化,靠 `is_maximized()`(大小等于屏幕可见区)判断,恢复时给可见区大小的普通窗口;别的平台用 `WindowBounds::Maximized`。
- **一个数据目录只跑一个 ShellRS。** 起图形界面之前先连外部 CLI 的套接字:连得上就发 `Request::Activate`(带启动链接)然后退出。为兼容已装的 CLI,不升 `PROTOCOL_VERSION`。
- **临时主机在 `HostStore` 的 `temporary` 里**:`host()` / `login()` 找得到,`hosts()` 看不到,也不写库。会写外键的检查用 `saved_host`。最后一个标签关掉就 `remove_temporary`。
+2 -1
View File
@@ -100,7 +100,7 @@ ShellRS 同一时间只运行一个:已经开着时再打开一次,会把已
## 设置
侧栏最下方的「设置」按钮(或 ⌘,,其他平台 Ctrl+,)在中间区打开「设置」标签页;已经打开时切换到该标签,不会重复打开。设置页分两栏:左边是分类(外观、终端、关键字高亮、外部 CLI、关于),上方可搜索,栏宽可拖动;右边是所选分类的设置。设置改动立即生效并保存在数据目录的 `settings.json` 里。
侧栏最下方的「设置」按钮(或 ⌘,,其他平台 Ctrl+,)在中间区打开「设置」标签页;已经打开时切换到该标签,不会重复打开。设置页分两栏:左边是分类(外观、终端、关键字高亮、外部 CLI、应用、关于),上方可搜索,栏宽可拖动;右边是所选分类的设置。设置改动立即生效并保存在数据目录的 `settings.json` 里。
- **外观 → 常规 → 界面语言**:跟随系统、简体中文、English,默认简体中文。英文界面尚在翻译中,目前只有 GPUI Kit 组件自带的文字(如搜索框提示)会切换。
- **外观 → 常规 → 应用外观**:深色、浅色、跟随系统,默认跟随系统;选「跟随系统」时随系统的深浅色切换。标题栏的主题按钮也会改写这一项。
@@ -108,6 +108,7 @@ ShellRS 同一时间只运行一个:已经开着时再打开一次,会把已
- **终端 → 字体配置**:字体(只列出本机安装的等宽字体,默认是系统的等宽字体,macOS 上为 Menlo)、字号(8–32 像素,默认 13)、行高(字号的 1.0–2.0 倍,默认 1.54,即原来的 20 像素行高)。改动立即作用于所有已打开的终端。下方的预览按终端的排法显示字母、数字、易混字符、符号、中文、制表符和一行命令。设置文件里的字体在本机没有安装时,使用默认字体。
- **关键字高亮**:总开关、预览和规则表,终端里按规则给文字上色、匹配时提醒,见「终端」一节。
- **外部 CLI**:见下一节。
- **应用 → 窗口**:「记住窗口大小」恢复上次的窗口大小和最大化状态,「记住窗口位置」把窗口放回上次所在的屏幕和位置,默认都打开,下次启动时生效。那块屏幕不在了、或窗口大半在屏幕外时,放在屏幕中间;窗口比屏幕大时缩到能放下。全屏不记,按退出全屏后的大小恢复。窗口的大小和位置随移动随时记在数据目录的 `window.json` 里。
- **关于**:「应用更新」一组:当前版本和检查更新、更新渠道、自动升级,见「下载与更新」。
## 外部 CLI
+1
View File
@@ -52,6 +52,7 @@ icon_assets!(
RotateCcwClock,
Activity,
Monitor,
AppWindow,
Network,
RadioTower,
ArrowDownLeft,
+1
View File
@@ -12,6 +12,7 @@ pub use app_icon::show_logo_when_unbundled;
pub use assets::{AppAssets, CatalogIcon, DOCKER_ICON};
pub use paths::{
cli_endpoint, cli_socket_path, data_dir, database_path, keys_dir, settings_path, updates_dir,
window_state_path,
};
pub use quit::{quit_held_back, set_quit_guard};
pub use window_hiding::{bring_forward, hide_when_closed};
+7
View File
@@ -50,6 +50,13 @@ pub fn settings_path() -> std::io::Result<PathBuf> {
Ok(dir.join("settings.json"))
}
/// The main window's last size, maximized state and place, for the next
/// launch (设置 › 应用 › 窗口). Kept apart from the settings: it changes
/// whenever the window moves.
pub fn window_state_path() -> PathBuf {
data_dir().join("window.json")
}
/// Where the running app listens for the `shellrs` command. The command
/// and the app both ask here, so both follow `SHELLRS_DATA_DIR`.
pub fn cli_socket_path() -> PathBuf {
+5 -2
View File
@@ -43,7 +43,11 @@ fn main() {
shellrs::app::show_logo_when_unbundled(cx);
cx.activate(true);
let options = shellrs::workspace::window_options(cx);
// Read before the window opens: 应用 › 窗口 decides where.
let (settings, settings_problem) = open_settings();
let saved = shellrs::workspace::WindowState::load(&shellrs::app::window_state_path());
let options =
shellrs::workspace::window_options(saved.as_ref(), settings.settings().window, cx);
cx.spawn(async move |cx| {
cx.open_window(options, |window, cx| {
window.activate_window();
@@ -51,7 +55,6 @@ fn main() {
shellrs::app::hide_when_closed(window, cx);
let (store, problem) = open_store();
let store = cx.new(|_| store);
let (settings, settings_problem) = open_settings();
let settings = cx.new(|_| settings);
let workspace =
cx.new(|cx| shellrs::workspace::Workspace::new(store, settings, window, cx));
+1
View File
@@ -12,6 +12,7 @@ pub use apply::apply;
pub use model::{
AppSettings, Appearance, Choice, ExternalCliSettings, InterfaceLanguage, NotificationSettings,
TerminalFontSettings, TerminalHighlightSettings, TerminalThemeSettings, UpdateSettings,
WindowSettings,
};
pub use settings_panel::{SettingsPanel, SettingsPanelEvent};
pub use store::{SettingsStore, SettingsStoreEvent};
+31
View File
@@ -32,6 +32,28 @@ pub struct AppSettings {
pub notifications: NotificationSettings,
/// 关键字高亮.
pub terminal_highlight: TerminalHighlightSettings,
/// 应用 → 窗口.
pub window: WindowSettings,
}
/// 应用 → 窗口: what the next launch restores of the main window. Both on
/// until the user turns them off.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct WindowSettings {
/// 记住窗口大小: its size, and whether it was maximized.
pub remember_size: bool,
/// 记住窗口位置: where it was, on which screen.
pub remember_position: bool,
}
impl Default for WindowSettings {
fn default() -> Self {
Self {
remember_size: true,
remember_position: true,
}
}
}
/// 外观 → 主题: by key, the theme used while the app is light and the one
@@ -531,6 +553,15 @@ mod tests {
assert!(!themes.is_default_choice());
}
#[test]
fn the_window_is_remembered_unless_the_file_says_otherwise() {
let settings: AppSettings = serde_json::from_str("{}").unwrap();
assert!(settings.window.remember_size && settings.window.remember_position);
let settings: AppSettings =
serde_json::from_str(r#"{"window":{"remember_position":false}}"#).unwrap();
assert!(settings.window.remember_size && !settings.window.remember_position);
}
#[test]
fn automatic_updates_are_on_unless_the_file_says_otherwise() {
let settings: AppSettings = serde_json::from_str("{}").unwrap();
+46 -2
View File
@@ -35,7 +35,7 @@ use crate::update::{Phase, Tone, UpdateSnapshot, UpdateStep, Updater, build_info
use super::highlight_rules::HighlightRulesEditor;
use super::terminal_themes::{app_follows_item, terminal_theme_item};
use super::{AppSettings, Choice, NotificationSettings, SettingsStore};
use super::{AppSettings, Choice, NotificationSettings, SettingsStore, WindowSettings};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SettingsPanelEvent {
@@ -230,7 +230,7 @@ struct Category {
type CategoryGroups = fn(&SettingsPanel, &App) -> Vec<SettingGroup>;
/// The categories, in the order the left column lists them.
const CATEGORIES: [Category; 5] = [
const CATEGORIES: [Category; 6] = [
Category {
title: "外观",
icon: CatalogIcon::Palette,
@@ -251,6 +251,11 @@ const CATEGORIES: [Category; 5] = [
icon: CatalogIcon::SquareTerminal,
groups: external_cli_groups,
},
Category {
title: "应用",
icon: CatalogIcon::AppWindow,
groups: application_groups,
},
Category {
title: "关于",
icon: CatalogIcon::Info,
@@ -501,6 +506,45 @@ fn highlight_preview_item() -> SettingItem {
.keywords(["预览", "高亮"])
}
/// 应用 → 窗口: what the next launch restores of the main window.
fn application_groups(panel: &SettingsPanel, _: &App) -> Vec<SettingGroup> {
let store = &panel.store;
let switch = |read: fn(&WindowSettings) -> bool, write: fn(&mut WindowSettings, bool)| {
let (reader, writer) = (store.clone(), store.clone());
SettingField::switch(
move |cx| read(&reader.read(cx).settings().window),
move |on, cx| {
writer.update(cx, |store, cx| {
store.update(|settings| write(&mut settings.window, on), cx)
});
},
)
.default_value(true)
};
vec![
SettingGroup::new().title("窗口").items([
SettingItem::new(
"记住窗口大小",
switch(
|window| window.remember_size,
|window, on| window.remember_size = on,
),
)
.description("下次启动时恢复上次关闭时的窗口大小和最大化状态。"),
SettingItem::new(
"记住窗口位置",
switch(
|window| window.remember_position,
|window, on| window.remember_position = on,
),
)
.description(
"下次启动时把窗口放回上次所在的屏幕和位置;那块屏幕不在时放在主屏幕中间。",
),
]),
]
}
/// 外部 CLI: whether the `shellrs` command may use the saved hosts, the
/// command itself, and the skill that teaches agents to use it.
fn external_cli_groups(panel: &SettingsPanel, cx: &App) -> Vec<SettingGroup> {
+2
View File
@@ -16,7 +16,9 @@ mod title_bar;
mod tool_sidebar;
mod tools;
mod updates;
mod window_state;
mod workspace_view;
pub use links::open_link_once_open;
pub use window_state::WindowState;
pub use workspace_view::{Workspace, notify_once_open, window_options};
+387
View File
@@ -0,0 +1,387 @@
//! 设置 › 应用 › 窗口: the main window's size, maximized state and place,
//! kept in `window.json` as they change, and where the next launch opens
//! the window from them.
use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::time::Duration;
use gpui_kit::*;
use serde::{Deserialize, Serialize};
use crate::settings::WindowSettings;
use super::Workspace;
/// How long a move or resize has to settle before it is written: a drag
/// reports every step.
const SAVE_DELAY: Duration = Duration::from_millis(500);
/// A new window's size, and the size of one whose size is not remembered.
fn default_size() -> Size<Pixels> {
size(px(1280.), px(800.))
}
/// How much of a window has to be on its screen for it to open where it
/// was: enough of the title bar to drag it by.
const VISIBLE_WIDTH: f32 = 120.;
const VISIBLE_HEIGHT: f32 = 40.;
/// The main window as it last was.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WindowState {
/// The screen it was on, as the system names it across restarts.
display: Option<String>,
/// Its size and place when neither maximized nor full screen, as GPUI
/// gives and takes them: from the screen's top left on macOS, the
/// desktop's elsewhere.
bounds: Bounds<Pixels>,
maximized: bool,
}
impl WindowState {
pub fn capture(window: &Window, cx: &App) -> Self {
// macOS never reports a window maximized, only its frame filling
// the screen; a full screen window comes back as it was before.
let (bounds, maximized) = match window.window_bounds() {
WindowBounds::Windowed(bounds) => (bounds, window.is_maximized()),
WindowBounds::Maximized(bounds) => (bounds, true),
WindowBounds::Fullscreen(bounds) => (bounds, false),
};
let display = window
.display(cx)
.and_then(|display| display.uuid().ok())
.map(|uuid| uuid.to_string());
Self {
display,
bounds,
maximized,
}
}
/// The state saved at `path`; none when there is none or it does not
/// read, and the window opens as a new one would.
pub fn load(path: &Path) -> Option<Self> {
serde_json::from_slice(&std::fs::read(path).ok()?).ok()
}
/// Written whole, through a temporary file, as the settings are.
pub fn save(&self, path: &Path) -> std::io::Result<()> {
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
let temporary = path.with_extension("json.tmp");
std::fs::write(&temporary, serde_json::to_vec_pretty(self)?)?;
std::fs::rename(temporary, path)
}
}
impl Workspace {
/// Keep the window's size and place at `path` as they change, for the
/// next launch. Always kept: 设置 › 应用 › 窗口 decides what of it the
/// next launch uses.
pub(super) fn remember_window(
&mut self,
path: PathBuf,
window: &mut Window,
cx: &mut Context<Self>,
) {
let latest: Rc<RefCell<Option<WindowState>>> = Rc::default();
// Written again on quitting, in case a change has not settled. The
// app's, not this view's: on Windows and Linux closing the window
// has dropped the view by then.
App::on_app_quit(cx, {
let (latest, path) = (latest.clone(), path.clone());
move |_| {
if let Some(state) = latest.borrow().as_ref() {
write(state, &path);
}
async {}
}
})
.detach();
let subscription = cx.observe_window_bounds(window, move |this, window, cx| {
let state = WindowState::capture(window, cx);
if latest.borrow().as_ref() == Some(&state) {
return;
}
*latest.borrow_mut() = Some(state.clone());
let path = path.clone();
this.window_state_save = Some(cx.spawn(async move |_, cx| {
cx.background_executor().timer(SAVE_DELAY).await;
cx.background_spawn(async move { write(&state, &path) })
.await;
}));
});
self._subscriptions.push(subscription);
}
}
/// Saving the window's place is a convenience: a failure is only logged.
fn write(state: &WindowState, path: &Path) {
if let Err(error) = state.save(path) {
eprintln!("shellrs: 无法保存窗口位置:{error}");
}
}
/// Where the main window opens: `saved`, as much of it as `settings`
/// remember, on the screens there are now.
pub fn window_placement(
saved: Option<&WindowState>,
settings: WindowSettings,
cx: &App,
) -> (WindowBounds, Option<DisplayId>) {
let screens: Vec<Screen> = cx
.displays()
.iter()
.map(|display| Screen {
id: display.id(),
uuid: display.uuid().ok().map(|uuid| uuid.to_string()),
bounds: display.bounds(),
visible: display.visible_bounds(),
})
.collect();
let primary = cx.primary_display().map(|display| display.id());
placement(
saved,
settings,
&screens,
primary,
cfg!(target_os = "macos"),
)
}
/// A screen the window could open on.
struct Screen {
id: DisplayId,
uuid: Option<String>,
bounds: Bounds<Pixels>,
/// Without the menu bar, Dock or taskbar.
visible: Bounds<Pixels>,
}
/// [`window_placement`] over given screens. `macos`: maximized is a frame
/// filling the screen's visible part, as macOS shows it, not a state.
fn placement(
saved: Option<&WindowState>,
settings: WindowSettings,
screens: &[Screen],
primary: Option<DisplayId>,
macos: bool,
) -> (WindowBounds, Option<DisplayId>) {
let primary = screens
.iter()
.find(|screen| Some(screen.id) == primary)
.or(screens.first());
let centered = |screen: Option<&Screen>, size: Size<Pixels>| match screen {
Some(screen) => Bounds::centered_at(screen.bounds.center(), size),
None => Bounds::new(point(px(0.), px(0.)), size),
};
let Some(saved) = saved.filter(|_| settings.remember_size || settings.remember_position) else {
return (
WindowBounds::Windowed(centered(primary, default_size())),
None,
);
};
// The screen it was on, if remembered and still there; else the main
// one.
let remembered = saved
.display
.as_ref()
.filter(|_| settings.remember_position)
.and_then(|uuid| {
screens
.iter()
.find(|screen| screen.uuid.as_ref() == Some(uuid))
});
let screen = remembered.or(primary);
let size = if settings.remember_size {
// Taken from a larger screen, it shrinks to fit this one.
match screen {
Some(screen) => size(
saved.bounds.size.width.min(screen.visible.size.width),
saved.bounds.size.height.min(screen.visible.size.height),
),
None => saved.bounds.size,
}
} else {
default_size()
};
let bounds = match remembered {
Some(screen) if reachable(Bounds::new(saved.bounds.origin, size), screen.bounds) => {
Bounds::new(saved.bounds.origin, size)
}
_ => centered(screen, size),
};
let display = remembered.map(|screen| screen.id);
if settings.remember_size && saved.maximized {
return match screen {
Some(screen) if macos => (WindowBounds::Windowed(screen.visible), display),
_ => (WindowBounds::Maximized(bounds), display),
};
}
(WindowBounds::Windowed(bounds), display)
}
/// Whether enough of `bounds` is on the screen to take hold of: its top
/// edge on the screen, and a stretch of title bar with it.
fn reachable(bounds: Bounds<Pixels>, screen: Bounds<Pixels>) -> bool {
let overlap = bounds.intersect(&screen);
bounds.top() >= screen.top()
&& bounds.top() <= screen.bottom() - px(VISIBLE_HEIGHT)
&& overlap.size.width >= px(VISIBLE_WIDTH)
&& overlap.size.height >= px(VISIBLE_HEIGHT)
}
#[cfg(test)]
mod tests {
use gpui_kit::{Bounds, DisplayId, Pixels, WindowBounds, point, px, size};
use super::{Screen, WindowState, default_size, placement};
use crate::settings::WindowSettings;
fn rect(x: f32, y: f32, width: f32, height: f32) -> Bounds<Pixels> {
Bounds::new(point(px(x), px(y)), size(px(width), px(height)))
}
/// A laptop screen and a larger one to its right, the laptop's menu
/// bar and Dock taken from its visible part.
fn screens() -> Vec<Screen> {
vec![
Screen {
id: DisplayId::new(1),
uuid: Some("laptop".into()),
bounds: rect(0., 0., 1512., 982.),
visible: rect(0., 33., 1512., 890.),
},
Screen {
id: DisplayId::new(2),
uuid: Some("monitor".into()),
bounds: rect(1512., 0., 2560., 1440.),
visible: rect(1512., 0., 2560., 1440.),
},
]
}
fn saved(display: &str, bounds: Bounds<Pixels>, maximized: bool) -> WindowState {
WindowState {
display: Some(display.into()),
bounds,
maximized,
}
}
fn remember(remember_size: bool, remember_position: bool) -> WindowSettings {
WindowSettings {
remember_size,
remember_position,
}
}
fn place(
saved: Option<&WindowState>,
settings: WindowSettings,
macos: bool,
) -> (WindowBounds, Option<DisplayId>) {
placement(saved, settings, &screens(), Some(DisplayId::new(1)), macos)
}
fn centered_on_laptop(size: gpui_kit::Size<Pixels>) -> WindowBounds {
WindowBounds::Windowed(Bounds::centered_at(
rect(0., 0., 1512., 982.).center(),
size,
))
}
#[test]
fn a_first_launch_opens_in_the_middle_of_the_main_screen() {
let opened = place(None, WindowSettings::default(), false);
assert_eq!(opened, (centered_on_laptop(default_size()), None));
}
#[test]
fn the_window_comes_back_where_it_was_on_the_screen_it_was_on() {
let was = rect(1700., 120., 1600., 1000.);
let state = saved("monitor", was, false);
assert_eq!(
place(Some(&state), WindowSettings::default(), false),
(WindowBounds::Windowed(was), Some(DisplayId::new(2)))
);
// Without its place, it keeps its size in the middle of the main
// screen, shrunk to fit there.
assert_eq!(
place(Some(&state), remember(true, false), false),
(centered_on_laptop(size(px(1512.), px(890.))), None)
);
// Without its size, a new window's size where it was.
assert_eq!(
place(Some(&state), remember(false, true), false),
(
WindowBounds::Windowed(Bounds::new(was.origin, default_size())),
Some(DisplayId::new(2))
)
);
// Neither: as a new window.
assert_eq!(
place(Some(&state), remember(false, false), false),
(centered_on_laptop(default_size()), None)
);
}
#[test]
fn a_window_whose_screen_is_gone_or_that_is_off_it_is_centered() {
let state = saved("projector", rect(100., 100., 1000., 700.), false);
assert_eq!(
place(Some(&state), WindowSettings::default(), false),
(centered_on_laptop(size(px(1000.), px(700.))), None)
);
// Dragged up past the top, or almost all off the side.
for was in [
rect(100., -200., 1000., 700.),
rect(1450., 100., 1000., 700.),
] {
let state = saved("laptop", was, false);
let (bounds, display) = place(Some(&state), WindowSettings::default(), false);
assert_eq!(bounds, centered_on_laptop(was.size));
assert_eq!(display, Some(DisplayId::new(1)));
}
}
#[test]
fn a_maximized_window_opens_maximized_if_its_size_is_remembered() {
let restored = rect(1700., 120., 1600., 1000.);
let state = saved("monitor", restored, true);
assert_eq!(
place(Some(&state), WindowSettings::default(), false),
(WindowBounds::Maximized(restored), Some(DisplayId::new(2)))
);
// On macOS, as a frame filling the screen but its menu bar and Dock.
let state = saved("laptop", rect(0., 33., 1512., 890.), true);
assert_eq!(
place(Some(&state), WindowSettings::default(), true),
(
WindowBounds::Windowed(rect(0., 33., 1512., 890.)),
Some(DisplayId::new(1))
)
);
assert!(matches!(
place(Some(&state), remember(false, true), true).0,
WindowBounds::Windowed(bounds) if bounds.size == default_size()
));
}
#[test]
fn the_state_is_kept_in_a_file_and_a_bad_one_is_ignored() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("data").join("window.json");
assert_eq!(WindowState::load(&path), None);
let state = saved("monitor", rect(1700., 120., 1600., 1000.), true);
state.save(&path).unwrap();
assert_eq!(WindowState::load(&path), Some(state));
std::fs::write(&path, "{\"bounds\": 7}").unwrap();
assert_eq!(WindowState::load(&path), None);
}
}
+17 -3
View File
@@ -53,6 +53,7 @@ use crate::host::{
};
use crate::settings::{
Appearance, SettingsPanel, SettingsPanelEvent, SettingsStore, SettingsStoreEvent,
WindowSettings,
};
use crate::sftp::{
SharedLocalDirectoryProvider, SharedSftpTransportProvider, SshSftpTransportProvider,
@@ -75,6 +76,7 @@ use super::{
title_bar::render_title_bar,
tool_sidebar::{ToolSidebar, render_tool_switch},
tools::{TOOL_SIDEBAR_WIDTH, set_right_dock_open},
window_state::{WindowState, window_placement},
};
const DOCK_ID: &str = "shellrs-dock";
@@ -106,11 +108,18 @@ pub fn notify_once_open(notification: Notification, window: &mut Window, cx: &mu
});
}
/// Window options for the main workspace window.
pub fn window_options(cx: &mut App) -> WindowOptions {
/// Window options for the main workspace window: where it was last, as much
/// of it as 设置 › 应用 › 窗口 remembers.
pub fn window_options(
saved: Option<&WindowState>,
settings: WindowSettings,
cx: &mut App,
) -> WindowOptions {
let (window_bounds, display_id) = window_placement(saved, settings, cx);
WindowOptions {
window_bounds: Some(window_bounds),
display_id,
// Window geometry is a platform boundary; `px` is the API's unit.
window_bounds: Some(WindowBounds::centered(size(px(1280.), px(800.)), cx)),
window_min_size: Some(gpui_kit::Size {
width: px(960.),
height: px(600.),
@@ -192,6 +201,9 @@ pub struct Workspace {
/// When each terminal last notified, of each kind, keyed by
/// `notices::notice_tag` and the kind.
pub(super) notice_times: HashMap<String, std::time::Instant>,
/// The pending write of the window's size and place, see
/// `window_state.rs`.
pub(super) window_state_save: Option<Task<()>>,
pub(super) _subscriptions: Vec<Subscription>,
}
@@ -250,6 +262,7 @@ impl Workspace {
),
}
this.sync_cli_server(cx);
this.remember_window(crate::app::window_state_path(), window, cx);
let bundle = cx.app_path().ok();
this.updater.update(cx, |updater, cx| {
updater.set_services(UpdateServices::system(bundle), cx);
@@ -548,6 +561,7 @@ impl Workspace {
editors: HashMap::new(),
next_editor_id: 1,
notice_times: HashMap::new(),
window_state_save: None,
_subscriptions: subscriptions,
}
}
+34
View File
@@ -436,6 +436,40 @@ fn terminal_themes_follow_the_appearance_and_a_card_chooses_one(cx: &mut TestApp
cx.update(|cx| assert_eq!(cx.theme().theme_name().as_ref(), "Default Light"));
}
#[gpui_kit::test]
fn the_window_settings_are_on_until_turned_off(cx: &mut TestAppContext) {
let (handle, workspace) = open_workspace(cx);
let window = |cx: &mut TestAppContext| {
cx.update(|cx| workspace.read(cx).settings().read(cx).settings().window)
};
in_frame(cx, handle, |window, cx| window.click("open-settings", cx));
// 应用 comes right before 关于.
in_frame(cx, handle, |window, cx| {
window.within("settings").click("0-4", cx)
});
let switch = |item: usize| {
move |window: &mut gpui_kit::Window| {
window
.within("settings")
.within("group-0")
.within(format!("item-{item}"))
.find("check")
.checked()
}
};
in_frame(cx, handle, |window, cx| {
assert_eq!(switch(0)(window), Some(true));
assert_eq!(switch(1)(window), Some(true));
window
.within("settings")
.within("group-0")
.within("item-1")
.click("check", cx);
});
let settings = window(cx);
assert!(settings.remember_size && !settings.remember_position);
}
/// The 关键字高亮 rules, as the settings hold them.
fn highlight_rules(
workspace: &Entity<Workspace>,
+1 -1
View File
@@ -230,7 +230,7 @@ fn serve_updates(
fn open_about_settings(cx: &mut TestAppContext, handle: WindowHandle<Root>) {
in_frame(cx, handle, |window, cx| window.click("open-settings", cx));
in_frame(cx, handle, |window, cx| {
window.within("settings").click("0-4", cx)
window.within("settings").click("0-5", cx)
});
}