From db4e74efa8cd67820315825c3fd825f996e58e9f Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Sat, 3 Oct 2026 16:59:15 +0800 Subject: [PATCH 1/6] fix: retain ordinary window startup geometry --- .../2026-10-03-ordinary-window-geometry.md | 81 ++++++++++ nebula_app/src/gpui_shell/config.rs | 7 +- nebula_app/src/gpui_shell/workspace.rs | 15 +- .../src/gpui_shell/workspace/closing.rs | 2 +- .../src/gpui_shell/workspace/residency.rs | 1 + .../gpui_shell/workspace/session_recovery.rs | 10 ++ .../gpui_shell/workspace/terminal_activity.rs | 1 + .../src/gpui_shell/workspace/windowing.rs | 56 +++---- .../workspace/windowing/startup_geometry.rs | 147 ++++++++++++++++-- .../workspace/windowing/transfer_tests.rs | 18 +++ nebula_app/src/platform/startup.rs | 64 ++++++++ nebula_app/src/session.rs | 15 +- nebula_app/src/session/window_layout.rs | 32 ++++ .../src/window_context/session_snapshot.rs | 5 +- 14 files changed, 390 insertions(+), 64 deletions(-) create mode 100644 architecture/notes/nebula_app/gpui_shell/workspace/2026-10-03-ordinary-window-geometry.md diff --git a/architecture/notes/nebula_app/gpui_shell/workspace/2026-10-03-ordinary-window-geometry.md b/architecture/notes/nebula_app/gpui_shell/workspace/2026-10-03-ordinary-window-geometry.md new file mode 100644 index 000000000..20b40dc60 --- /dev/null +++ b/architecture/notes/nebula_app/gpui_shell/workspace/2026-10-03-ordinary-window-geometry.md @@ -0,0 +1,81 @@ +# Ordinary-window startup geometry + +## Status + +Proposed for review. + +## Context + +The GPUI startup path already reads explicit Lua grid dimensions, but ignores +`window.position`. Regular workspace snapshots omit `Session.window`, so the +last size and position disappear when the process exits. Quick Terminal already +owns an independent remembered size and monitor policy. + +## Evidence + +`workspace/windowing.rs` owns the registry and combined persistence boundary. +`session/window_layout.rs` preserves per-window metadata in the same atomic JSON +file as tabs. The pinned GPUI Windows backend (`fc05d637`) obtains normal bounds +with `GetWindowPlacement`; maximized and fullscreen bounds retain their restore +rectangle. Windows placement uses workspace coordinates, whereas display bounds +and configured positions use desktop coordinates. + +## Decision + +Attach normal native geometry at the ordinary-window persistence boundary, +including empty tab-close snapshots. Cold restore belongs only to the first +ordinary `RestoreOrDefault` window. New windows, Quick Terminal and isolated +administrator processes never consume this geometry. Geometry restoration is +independent of restoring shell tabs; the boot-attempt cutoff still prevents a +broken saved startup loop. Existing empty-session and background-close policies +remain owned by session persistence. + +Sizes remain logical GPUI pixels. Windows saved position is the physical desktop +client origin, paired with the stable display UUID. Restore first chooses the +configured physical monitor, otherwise the remembered display, otherwise the +nearest physical monitor, then the primary display. It clamps size and origin +within that display's work area. Working coordinates are translated only at the +Windows placement boundary. Fullscreen saves its normal rectangle but does not +force a fullscreen startup; maximized state may be restored. + +Explicit Lua dimensions override saved size; explicit Lua position overrides +saved position. Missing dimensions are not an implicit explicit default. +Configured Windows position describes the outer window in physical desktop +pixels, as the legacy configuration does. Apply it before first presentation +using `SetWindowPos` because native border insets are known only after creation. +Other platforms keep their existing position behavior pending an equivalent +public/native placement contract. + +## Rejected alternatives + +- Persisting zoom-derived rows/columns: couples layout to font state and loses + the user's actual native size. +- Saving maximized/fullscreen viewport size: destroys the normal restore size. +- Always recentering on primary: discards a still-attached secondary display. +- Reopening all ordinary windows for cold startup: changes tab restoration policy. +- A second geometry file: adds a competing save lifecycle and atomicity boundary. + +## Consequences + +New JSON fields are optional; older sessions remain readable. The shared session +format still contains flattened tabs and per-window layouts, without a version +bump. Quick Terminal settings remain separate. Native DPI conversion follows the +pinned backend and must be reviewed when that dependency changes. + +## Validation + +The added GPUI snapshot regression exercises a real registered workspace and +normal native bounds. Session tests atomically roundtrip multiwindow and empty +geometry, accept old metadata, and enforce the crash-loop cutoff. Startup tests +cover explicit grid priority and selected-display offscreen clamping. Execution +and platform acceptance are performed in GitHub Actions; local execution is not +part of this change's evidence. + +## Supersedes + +None. + +## Revisit when + +GPUI exposes a cross-platform display scale/position API or cold startup begins +restoring separate ordinary windows. diff --git a/nebula_app/src/gpui_shell/config.rs b/nebula_app/src/gpui_shell/config.rs index 7d9fa9888..940d2562c 100644 --- a/nebula_app/src/gpui_shell/config.rs +++ b/nebula_app/src/gpui_shell/config.rs @@ -469,7 +469,7 @@ fn default_font_family() -> &'static str { crate::font_install::REQUIRED_FONT_FAMILY } -/// `config.window.dimensions`:启动窗口的网格行列。 +/// 显式窗口网格、物理坐标与首个普通窗口的冷启动恢复值。 /// /// 单独驻留而不是并入 [`Settings`],因为生命周期不同:`Settings` 随设置页写入 /// 反复重载,启动网格按定义只在开窗时生效一次,而 Lua 配置求值不该出现在每次 @@ -478,6 +478,8 @@ fn default_font_family() -> &'static str { #[derive(Default)] pub(crate) struct StartupWindow { pub dimensions: Option, + pub position: Option>, + pub(crate) restored: Option, } impl Global for StartupWindow {} @@ -508,7 +510,7 @@ impl StartupWindow { /// `dimensions()` 自带「行列必须同时非零」的旧壳合同。 fn from_config(config: &crate::config::UiConfig) -> Self { - Self { dimensions: config.window.dimensions() } + Self { dimensions: config.window.dimensions(), position: config.window.position, restored: None } } } @@ -931,6 +933,7 @@ mod tests { ); let configured = |columns: usize, lines: usize| StartupWindow { dimensions: Some(Dimensions { columns, lines }), + ..Default::default() }; cx.update(|cx| { assert_eq!(startup_grid(cx), builtin); diff --git a/nebula_app/src/gpui_shell/workspace.rs b/nebula_app/src/gpui_shell/workspace.rs index d100f97f3..0c63f759b 100644 --- a/nebula_app/src/gpui_shell/workspace.rs +++ b/nebula_app/src/gpui_shell/workspace.rs @@ -840,7 +840,8 @@ impl NebulaWorkspace { window, cx, sidebar_width, - window_role == windowing::WindowRole::Regular, + window_role == windowing::WindowRole::Regular + && !windowing::startup_geometry::restored_size(runtime_window_id, &startup, cx), ); let this = cx.entity().downgrade(); let appearance_sub = window.observe_window_appearance(move |_, cx| { @@ -1545,15 +1546,6 @@ impl NebulaWorkspace { (0..self.tabs.len()).find_map(|tab_ix| self.busy_process_in_tab(tab_ix, None, cx)) } - fn save_clean_window_session(&mut self, cx: &mut App) -> std::io::Result<()> { - windowing::save_current_window_session( - self.runtime_window_id, - self.snapshot_session(cx), - session_persistence::SaveReason::WindowClose, - cx, - ) - } - /// 聚焦另一个 pane(点击上报或方向导航落点)。 fn focus_pane( &mut self, @@ -1698,6 +1690,7 @@ impl NebulaWorkspace { self.runtime_window_id, self.snapshot_session(cx), session_persistence::SaveReason::TabsClosed, + window, cx, ) { log::warn!("Could not save closed tabs: {error}"); @@ -2807,7 +2800,7 @@ impl Render for NebulaWorkspace { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { crate::gpui_shell::theme::sync_component_focus_ring(window, cx); if window.is_window_active() { - windowing::mark_active(self.runtime_window_id, cx); + windowing::mark_active(self.runtime_window_id, window, cx); } if !cx.has_active_drag() { self.cross_window_dock = None; diff --git a/nebula_app/src/gpui_shell/workspace/closing.rs b/nebula_app/src/gpui_shell/workspace/closing.rs index 0bb64cb74..970b8b749 100644 --- a/nebula_app/src/gpui_shell/workspace/closing.rs +++ b/nebula_app/src/gpui_shell/workspace/closing.rs @@ -197,7 +197,7 @@ impl NebulaWorkspace { cx.notify(); return; } - if workspace.save_clean_window_session(cx).is_err() { + if workspace.save_clean_window_session(window, cx).is_err() { workspace.window_close_pending = false; let language = crate::gpui_shell::config::ui_language(cx); crate::gpui_shell::toast::banner( diff --git a/nebula_app/src/gpui_shell/workspace/residency.rs b/nebula_app/src/gpui_shell/workspace/residency.rs index 568b73ce5..9b2994801 100644 --- a/nebula_app/src/gpui_shell/workspace/residency.rs +++ b/nebula_app/src/gpui_shell/workspace/residency.rs @@ -1014,6 +1014,7 @@ impl NebulaWorkspace { self.runtime_window_id, self.snapshot_session(cx), super::session_persistence::SaveReason::Checkpoint, + window, cx, ) { log::warn!("Could not checkpoint before hiding window: {error}"); diff --git a/nebula_app/src/gpui_shell/workspace/session_recovery.rs b/nebula_app/src/gpui_shell/workspace/session_recovery.rs index 1f925c9a6..3f10d5878 100644 --- a/nebula_app/src/gpui_shell/workspace/session_recovery.rs +++ b/nebula_app/src/gpui_shell/workspace/session_recovery.rs @@ -3,6 +3,16 @@ use super::*; impl NebulaWorkspace { + pub(super) fn save_clean_window_session(&mut self, window: &Window, cx: &mut App) -> std::io::Result<()> { + windowing::save_current_window_session( + self.runtime_window_id, + self.snapshot_session(cx), + session_persistence::SaveReason::WindowClose, + window, + cx, + ) + } + pub(super) fn restore_update_session( &mut self, session: &crate::session::Session, diff --git a/nebula_app/src/gpui_shell/workspace/terminal_activity.rs b/nebula_app/src/gpui_shell/workspace/terminal_activity.rs index c909180f3..2985dc319 100644 --- a/nebula_app/src/gpui_shell/workspace/terminal_activity.rs +++ b/nebula_app/src/gpui_shell/workspace/terminal_activity.rs @@ -24,6 +24,7 @@ impl NebulaWorkspace { self.runtime_window_id, self.snapshot_session(cx), session_persistence::SaveReason::Checkpoint, + window, cx, ) { log::warn!("Could not checkpoint native recovery identity: {error}"); diff --git a/nebula_app/src/gpui_shell/workspace/windowing.rs b/nebula_app/src/gpui_shell/workspace/windowing.rs index 0a3a301c9..07b6a4182 100644 --- a/nebula_app/src/gpui_shell/workspace/windowing.rs +++ b/nebula_app/src/gpui_shell/workspace/windowing.rs @@ -6,7 +6,7 @@ mod shutdown; pub(crate) use shutdown::{quit_all, quit_for_update}; -mod startup_geometry; +pub(super) mod startup_geometry; pub(super) use startup_geometry::prepare_initial_grid; #[cfg(windows)] @@ -69,6 +69,7 @@ struct WindowEntry { last_activated: u64, native_hwnd: isize, role: WindowRole, + configured_position_pending: bool, } struct QuickTerminalWindow { @@ -423,35 +424,10 @@ fn workspace_window_options( focus: bool, role: WindowRole, sidebar_width: f32, + id: u64, ) -> WindowOptions { match role { - WindowRole::Regular => { - let preferred = startup_geometry::preferred_size(cx, sidebar_width) - .unwrap_or_else(|| size(px(1080.0), px(720.0))); - let bounds = cx.primary_display().map_or_else( - || Bounds::centered(None, preferred, cx), - |display| { - let visible = display.visible_bounds(); - let fitted = preferred.min(&visible.size); - Bounds::new( - point( - visible.origin.x + (visible.size.width - fitted.width) / 2.0, - visible.origin.y + (visible.size.height - fitted.height) / 2.0, - ), - fitted, - ) - }, - ); - crate::platform::window_chrome::configure_options(WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - window_min_size: Some(size(px(760.0), px(540.0)).min(&bounds.size)), - titlebar: Some(TitleBar::title_bar_options()), - app_id: Some("pebrel".to_owned()), - window_background: crate::gpui_shell::wallpaper::initial_background_appearance(), - focus, - ..Default::default() - }) - }, + WindowRole::Regular => startup_geometry::regular_options(id, focus, sidebar_width, cx), WindowRole::QuickTerminal => { let (display_id, visible) = quick_terminal_anchor_display(cx) .map(|(id, bounds)| (Some(id), bounds)) @@ -510,12 +486,14 @@ fn open_workspace_window( let start_hidden = shell_events.is_some() && matches!(startup, WorkspaceStartup::RestoreOrDefault) && crate::platform::startup::start_hidden(&runtime); - let mut options = workspace_window_options(cx, focus, role, runtime.sidebar_width); + startup_geometry::load_restore(runtime_window_id, &startup, role, cx); + let mut options = workspace_window_options(cx, focus, role, runtime.sidebar_width, runtime_window_id); if start_hidden { options.show = false; options.focus = false; } let deferred_show = crate::platform::startup::first_frame::defer_show(&mut options); + let configured_position_pending = !options.show && role == WindowRole::Regular; let workspace_slot = Rc::new(RefCell::new(None)); let hwnd_slot = Rc::new(RefCell::new(0isize)); let workspace_out = workspace_slot.clone(); @@ -523,6 +501,7 @@ fn open_workspace_window( let handle = cx.open_window(options, move |window, cx| { window.set_window_title(crate::brand::NAME); crate::platform::window_chrome::configure(window); + if role == WindowRole::Regular && !configured_position_pending { startup_geometry::apply_configured_position(window, cx); } *hwnd_out.borrow_mut() = native_hwnd(window).unwrap_or_default(); #[cfg(windows)] crate::gpui_shell::set_native_window_icon(window); @@ -564,6 +543,7 @@ fn open_workspace_window( last_activated, native_hwnd, role, + configured_position_pending, }); crate::gpui_shell::wallpaper::refresh(cx); if deferred_show { @@ -611,7 +591,11 @@ fn open_runtime_window( Ok((window_id, pane_id)) } -pub(crate) fn mark_active(runtime_window_id: u64, cx: &mut App) { +pub(crate) fn mark_active(runtime_window_id: u64, window: &Window, cx: &mut App) { + if let Some(entry) = cx.global_mut::().entries.iter_mut().find(|entry| entry.runtime_window_id == runtime_window_id) + && std::mem::take(&mut entry.configured_position_pending) { + startup_geometry::apply_configured_position(window, cx); + } let registry = cx.global_mut::(); if !registry.entries.iter().any(|entry| { entry.runtime_window_id == runtime_window_id && entry.role == WindowRole::Regular @@ -1427,7 +1411,7 @@ pub(crate) fn autosave_tick(cx: &mut App) { fn combined_session( mut current: Option<(u64, crate::session::Session)>, - cx: &App, + cx: &mut App, ) -> Option { let mut entries = cx .global::() @@ -1449,7 +1433,9 @@ fn combined_session( current.take().expect("current window snapshot exists").1 } else { let Some(workspace) = entry.workspace.upgrade() else { continue }; - workspace.read(cx).snapshot_session(cx) + let mut session = workspace.read(cx).snapshot_session(cx); + session.window = entry.handle.update(cx, |_, window, cx| startup_geometry::capture_window(window, cx)).ok(); + session }; sessions.push((active_handle == Some(entry.handle), session)); } @@ -1464,8 +1450,9 @@ fn save_combined_session(cx: &mut App, clean: bool) -> std::io::Result<()> { pub(super) fn save_current_window_session( runtime_window_id: u64, - session: crate::session::Session, + mut session: crate::session::Session, reason: SaveReason, + window: &Window, cx: &mut App, ) -> std::io::Result<()> { if !cx.global::().entries.iter().any(|entry| { @@ -1473,6 +1460,7 @@ pub(super) fn save_current_window_session( }) { return Ok(()); } + session.window = Some(startup_geometry::capture_window(window, cx)); let session = combined_session(Some((runtime_window_id, session)), cx); cx.global_mut::().session_persistence.save(session, reason) } @@ -1548,7 +1536,7 @@ pub(super) fn close_saved_workspace_window( cx: &mut App, ) { if let Err(error) = - save_current_window_session(runtime_window_id, session, SaveReason::WindowClose, cx) + save_current_window_session(runtime_window_id, session, SaveReason::WindowClose, window, cx) { log::warn!("Could not checkpoint moved window: {error}"); } diff --git a/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs b/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs index c2e77ae71..748c200b1 100644 --- a/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs +++ b/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs @@ -4,7 +4,9 @@ //! platform exposes display DPI, supply the final size before the window is //! shown; otherwise keep the existing post-creation sizing fallback. -use gpui::{App, Pixels, Size, Window, px, size}; +use gpui::{App, Bounds, Pixels, Point, Size, Window, WindowBounds, WindowOptions, point, px, size}; +use super::{WindowRole, WorkspaceStartup}; +use crate::gpui_shell::config::StartupWindow; use crate::gpui_shell::terminal::view::TerminalView; @@ -24,11 +26,8 @@ fn default_size( display_limit.map_or(preferred, |limit| preferred.min(&limit)) } -fn display_limit(cx: &App) -> Option> { - cx.primary_display().map(|display| { - let bounds = display.bounds().size; - size(bounds.width * 0.95, bounds.height * 0.95) - }) +fn display_limit(window: &Window, cx: &App) -> Option> { + window.display(cx).map(|display| display.visible_bounds().size * 0.95) } fn fit_native_size(preferred: Size, visible: Option>) -> Size { @@ -49,12 +48,107 @@ pub(super) fn preferred_size(cx: &App, sidebar_width: f32) -> Option, origin: Option>, visible: Bounds) -> Bounds { + let fitted = fit_native_size(preferred, Some(visible.size)); + let origin = origin.unwrap_or_else(|| point( + visible.origin.x + (visible.size.width - fitted.width) / 2.0, + visible.origin.y + (visible.size.height - fitted.height) / 2.0, + )); + Bounds::new(point( + px(origin.x.as_f32().clamp(visible.left().as_f32(), (visible.right() - fitted.width).as_f32().max(visible.left().as_f32()))), + px(origin.y.as_f32().clamp(visible.top().as_f32(), (visible.bottom() - fitted.height).as_f32().max(visible.top().as_f32()))), + ), fitted) +} + +pub(super) fn load_restore(id: u64, startup: &WorkspaceStartup, role: WindowRole, cx: &mut App) { + if id != 1 || role != WindowRole::Regular || !matches!(startup, WorkspaceStartup::RestoreOrDefault) + || crate::platform::elevation::requires_isolation() { return; } + let restored = crate::session::load().and_then(|session| session.startup_window_state()); + if let Some(config) = cx.try_global_mut::() { config.restored = restored; } +} + +pub(in crate::gpui_shell::workspace) fn restored_size(id: u64, startup: &WorkspaceStartup, cx: &App) -> bool { + id == 1 && matches!(startup, WorkspaceStartup::RestoreOrDefault) + && cx.try_global::().is_some_and(|config| config.dimensions.is_none() && config.restored.is_some()) +} + +pub(super) fn regular_options(id: u64, focus: bool, sidebar_width: f32, cx: &mut App) -> WindowOptions { + let config = cx.try_global::(); + let restored = (id == 1).then(|| config.and_then(|config| config.restored)).flatten(); + let configured = config.and_then(|config| config.position).map(|position| (position.x, position.y)); + let physical = configured.or_else(|| restored.and_then(|window| window.position)); + let physical_display = configured.and_then(crate::platform::startup::physical_display); + let displays = cx.displays(); + let selected = physical_display.and_then(|(id, _)| displays.iter().find(|display| u64::from(display.id()) == id).cloned()) + .or_else(|| restored.and_then(|window| window.display).and_then(|uuid| displays.iter().find(|display| display.uuid().is_ok_and(|id| id.as_bytes() == &uuid)).cloned())) + .or_else(|| physical.and_then(crate::platform::startup::physical_display).and_then(|(id, _)| displays.iter().find(|display| u64::from(display.id()) == id).cloned())) + .or_else(|| cx.primary_display()); + let display_id = selected.as_ref().map(|display| display.id()); + let scale = display_id.and_then(|id| crate::platform::startup::display_scale(u64::from(id))); + let visible = selected.map(|display| display.visible_bounds()).unwrap_or_else(|| Bounds::centered(None, size(px(1080.0), px(720.0)), cx)); + let configured_dimensions = config.is_some_and(|config| config.dimensions.is_some()); + let preferred = if !configured_dimensions && let Some(restored) = restored { + size(px(restored.width as f32), px(restored.height as f32)) + } else if let Some(scale) = scale { + default_size(TerminalView::startup_cell_metrics_at_scale(scale, cx), crate::gpui_shell::config::startup_grid(cx), sidebar_width, Some(visible.size * 0.95)) + } else { preferred_size(cx, sidebar_width).unwrap_or_else(|| size(px(1080.0), px(720.0))) }; + let origin = physical.zip(scale).map(|((x, y), scale)| point(px(x as f32 / scale), px(y as f32 / scale))); + let mut bounds = fit_bounds(preferred, origin, visible); + if let Some(scale) = scale { + let offset = crate::platform::startup::placement_offset(u64::from(display_id.unwrap())); + bounds.origin -= point(px(offset.0 as f32 / scale), px(offset.1 as f32 / scale)); + } + let maximized = !configured_dimensions && configured.is_none() && restored.is_some_and(|window| window.maximized); + crate::platform::window_chrome::configure_options(WindowOptions { + window_bounds: Some(if maximized { WindowBounds::Maximized(bounds) } else { WindowBounds::Windowed(bounds) }), + window_min_size: Some(size(px(760.0), px(540.0)).min(&bounds.size)), + titlebar: Some(gpui_component::TitleBar::title_bar_options()), + app_id: Some("pebrel".to_owned()), + window_background: crate::gpui_shell::wallpaper::initial_background_appearance(), + focus, + display_id, + ..Default::default() + }) +} + +pub(super) fn capture_window(window: &Window, cx: &App) -> crate::session::WindowState { + let normal = window.window_bounds(); + let bounds = normal.get_bounds(); + let display = window.display(cx); + #[cfg(windows)] + let position = display.as_ref().map(|display| { + let offset = crate::platform::startup::placement_offset(u64::from(display.id())); + ((bounds.origin.x.as_f32() * window.scale_factor()).round() as i32 + offset.0, + (bounds.origin.y.as_f32() * window.scale_factor()).round() as i32 + offset.1) + }); + #[cfg(not(windows))] + let position = None; + crate::session::WindowState { + width: bounds.size.width.as_f32().round().max(1.0) as u32, + height: bounds.size.height.as_f32().round().max(1.0) as u32, + maximized: matches!(normal, WindowBounds::Maximized(_)), + position, + display: display.and_then(|display| display.uuid().ok()).map(|id| *id.as_bytes()), + } +} + +pub(super) fn apply_configured_position(window: &Window, cx: &App) { + #[cfg(windows)] + if let Some(position) = cx.try_global::().and_then(|config| config.position) + && let Some(hwnd) = super::native_hwnd(window) + && !crate::platform::startup::place_normal_window(hwnd, (position.x, position.y)) { + log::warn!("Could not apply configured window position"); + } + #[cfg(not(windows))] + let _ = (window, cx); +} + fn same_device_size(actual: Size, requested: Size, scale: f32) -> bool { // Window placement quantizes logical sizes to device pixels. A subpixel // round-trip difference must not schedule a second asynchronous SetWindowPos. @@ -75,10 +169,10 @@ pub(in crate::gpui_shell::workspace) fn prepare_initial_grid( TerminalView::startup_cell_metrics(window, cx), crate::gpui_shell::config::startup_grid(cx), sidebar_width, - display_limit(cx), + display_limit(window, cx), ); let requested = if crate::platform::startup::primary_display_scale().is_some() { - fit_preflight_size(requested, cx) + fit_native_size(requested, window.display(cx).map(|display| display.visible_bounds().size)) } else { // Keep the original sizing policy on platforms without preflight DPI. requested @@ -109,6 +203,41 @@ pub(in crate::gpui_shell::workspace) fn prepare_initial_grid( mod tests { use super::*; + #[test] + fn restoring_a_large_legal_window_does_not_shrink_it() { + let visible = Bounds::new(point(px(0.0), px(0.0)), size(px(1920.0), px(1040.0))); + let bounds = fit_bounds(size(px(1900.0), px(1030.0)), Some(point(px(10.0), px(5.0))), visible); + assert_eq!(bounds.size, size(px(1900.0), px(1030.0))); + assert_eq!(bounds.origin, point(px(10.0), px(5.0))); + } + + #[test] + fn restored_bounds_stay_visible_on_the_selected_negative_origin_display() { + let visible = Bounds::new(point(px(-1920.0), px(40.0)), size(px(1920.0), px(1040.0))); + let bounds = fit_bounds(size(px(1300.0), px(800.0)), Some(point(px(-1700.0), px(120.0))), visible); + assert_eq!(bounds.origin, point(px(-1700.0), px(120.0))); + assert_eq!(bounds.size, size(px(1300.0), px(800.0))); + let offscreen = fit_bounds(size(px(3000.0), px(2000.0)), Some(point(px(9000.0), px(-9000.0))), visible); + assert!(visible.contains(&offscreen.origin)); + assert!(offscreen.right() <= visible.right()); + assert!(offscreen.bottom() <= visible.bottom()); + } + + #[cfg(feature = "gpui-test-support")] + #[gpui::test] + fn restored_dimensions_apply_only_to_the_first_window_and_explicit_grid_wins(cx: &mut gpui::TestAppContext) { + cx.update(|cx| { + cx.set_global(StartupWindow { restored: Some(crate::session::WindowState { width: 900, height: 650, ..Default::default() }), ..Default::default() }); + assert!(restored_size(1, &WorkspaceStartup::RestoreOrDefault, cx)); + assert!(!restored_size(2, &WorkspaceStartup::RestoreOrDefault, cx)); + assert!(!restored_size(1, &WorkspaceStartup::Empty, cx)); + let options = regular_options(1, false, 230.0, cx); + assert_eq!(options.window_bounds.unwrap().get_bounds().size, size(px(900.0), px(650.0))); + cx.global_mut::().dimensions = Some(crate::config::window::Dimensions { columns: 155, lines: 43 }); + assert!(!restored_size(1, &WorkspaceStartup::RestoreOrDefault, cx)); + }); + } + #[test] fn initial_window_keeps_the_base_font_grid_and_fits_the_display() { let metrics = (px(8.0), px(20.0)); diff --git a/nebula_app/src/gpui_shell/workspace/windowing/transfer_tests.rs b/nebula_app/src/gpui_shell/workspace/windowing/transfer_tests.rs index 220c87da3..75691958b 100644 --- a/nebula_app/src/gpui_shell/workspace/windowing/transfer_tests.rs +++ b/nebula_app/src/gpui_shell/workspace/windowing/transfer_tests.rs @@ -110,3 +110,21 @@ fn moving_one_of_two_tabs_preserves_source_and_identity(cx: &mut TestAppContext) assert_eq!(combined_session(None, cx).unwrap().tabs.len(), 2); }); } + +#[gpui::test] +fn ordinary_window_geometry_is_in_the_durable_snapshot(cx: &mut TestAppContext) { + initialize_test(cx); + cx.update(|cx| { + let (id, _) = open_test_window(cx, 1); + let entry = entry_by_id(id, cx).unwrap(); + let expected = entry.handle.update(cx, |_, window, _| { + window.resize(size(px(1300.0), px(800.0))); + window.window_bounds().get_bounds().size + }).unwrap(); + let saved = combined_session(None, cx).unwrap().into_update_windows().unwrap(); + let geometry = saved[0].window.expect("ordinary window geometry must survive process exit"); + assert_eq!(geometry.width, expected.width.as_f32().round() as u32); + assert_eq!(geometry.height, expected.height.as_f32().round() as u32); + assert!(!geometry.maximized); + }); +} diff --git a/nebula_app/src/platform/startup.rs b/nebula_app/src/platform/startup.rs index 3aca26e20..9cb63aab8 100644 --- a/nebula_app/src/platform/startup.rs +++ b/nebula_app/src/platform/startup.rs @@ -162,3 +162,67 @@ mod tests { } } } + +/// GPUI Windows display ids are the borrowed HMONITOR value at the pinned backend. +pub(crate) fn physical_display(point: (i32, i32)) -> Option<(u64, f32)> { + #[cfg(windows)] + { + use windows_sys::Win32::Foundation::POINT; + use windows_sys::Win32::Graphics::Gdi::{MonitorFromPoint, MONITOR_DEFAULTTONEAREST}; + // SAFETY: this queries a borrowed monitor handle without retaining it. + let monitor = unsafe { MonitorFromPoint(POINT { x: point.0, y: point.1 }, MONITOR_DEFAULTTONEAREST) }; + display_scale(monitor as u64).map(|scale| (monitor as u64, scale)) + } + #[cfg(not(windows))] + { let _ = point; None } +} + +pub(crate) fn display_scale(id: u64) -> Option { + #[cfg(windows)] + { + use windows_sys::Win32::UI::HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI}; + let (mut x, mut y) = (0, 0); + let monitor = id as *mut core::ffi::c_void; + // SAFETY: the borrowed monitor and local DPI output pointers stay valid. + if monitor.is_null() || unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut x, &mut y) } < 0 || x == 0 || x != y { + None + } else { Some(x as f32 / 96.0) } + } + #[cfg(not(windows))] + { let _ = id; None } +} + +pub(crate) fn placement_offset(id: u64) -> (i32, i32) { + #[cfg(windows)] + { + use windows_sys::Win32::Graphics::Gdi::{GetMonitorInfoW, MONITORINFO}; + // SAFETY: MONITORINFO is a plain output structure; cbSize is set below. + let mut info: MONITORINFO = unsafe { std::mem::zeroed() }; + info.cbSize = std::mem::size_of::() as u32; + // SAFETY: the borrowed display handle and initialized output are valid. + if unsafe { GetMonitorInfoW(id as *mut core::ffi::c_void, &mut info) } != 0 { + return (info.rcWork.left - info.rcMonitor.left, info.rcWork.top - info.rcMonitor.top); + } + } + let _ = id; + (0, 0) +} + +#[cfg(windows)] +pub(crate) fn place_normal_window(hwnd: isize, desired: (i32, i32)) -> bool { + use windows_sys::Win32::Foundation::RECT; + use windows_sys::Win32::Graphics::Gdi::{GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTONEAREST}; + use windows_sys::Win32::UI::WindowsAndMessaging::{GetWindowRect, SetWindowPos, SWP_NOSIZE, SWP_NOACTIVATE, SWP_NOZORDER}; + let hwnd = hwnd as *mut core::ffi::c_void; + // SAFETY: both native output structures admit zero initialization. + let mut rect: RECT = unsafe { std::mem::zeroed() }; + let mut monitor: MONITORINFO = unsafe { std::mem::zeroed() }; + monitor.cbSize = std::mem::size_of::() as u32; + // SAFETY: the caller owns the live HWND; the local outputs remain valid. + if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 + || unsafe { GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &mut monitor) } == 0 { return false; } + let x = desired.0.clamp(monitor.rcWork.left, (monitor.rcWork.right - (rect.right - rect.left)).max(monitor.rcWork.left)); + let y = desired.1.clamp(monitor.rcWork.top, (monitor.rcWork.bottom - (rect.bottom - rect.top)).max(monitor.rcWork.top)); + // SAFETY: only the live window position changes; ownership and size are preserved. + unsafe { SetWindowPos(hwnd, std::ptr::null_mut(), x, y, 0, 0, SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOZORDER) != 0 } +} diff --git a/nebula_app/src/session.rs b/nebula_app/src/session.rs index 87117cba3..0762c83d9 100644 --- a/nebula_app/src/session.rs +++ b/nebula_app/src/session.rs @@ -219,16 +219,19 @@ impl TabSession { } /// Last normal (non-maximized, non-fullscreen) inner size in logical pixels. -/// Logical units keep the perceived size stable when the next launch lands on -/// a monitor with a different DPI scale factor. Write-only since v4: startup -/// always sizes the window from the configured column/line count, so this -/// record is diagnostic and forward-compat data, never replayed at boot. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +/// Logical units keep the perceived size stable across display DPI changes. +/// Windows position is the normal client origin in physical desktop pixels; +/// the stable display identity keeps an existing monitor as the restore target. +#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub struct WindowState { pub width: u32, pub height: u32, #[serde(default)] pub maximized: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub position: Option<(i32, i32)>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display: Option<[u8; 16]>, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -447,7 +450,7 @@ mod tests { #[test] fn window_state_round_trip_preserves_logical_size_and_maximize() { let mut session = Session::new(0, Vec::new()); - session.window = Some(WindowState { width: 1280, height: 720, maximized: true }); + session.window = Some(WindowState { width: 1280, height: 720, maximized: true, ..Default::default() }); let json = serde_json::to_string(&session).unwrap(); let restored: Session = serde_json::from_str(&json).unwrap(); assert_eq!(restored.window, session.window); diff --git a/nebula_app/src/session/window_layout.rs b/nebula_app/src/session/window_layout.rs index ee44a7801..97d512f3b 100644 --- a/nebula_app/src/session/window_layout.rs +++ b/nebula_app/src/session/window_layout.rs @@ -35,6 +35,16 @@ pub(crate) fn combine_sessions( } impl Session { + pub(crate) fn startup_window_state(&self) -> Option { + if self.boot_attempts >= super::MAX_BOOT_ATTEMPTS { + return None; + } + self.window + .or_else(|| self.window_layout.iter().find(|layout| layout.active).and_then(|layout| layout.window)) + .or_else(|| self.window_layout.first().and_then(|layout| layout.window)) + .filter(|window| window.width > 0 && window.height > 0) + } + /// Open the previously active window last so native activation restores it. /// Invalid boundaries are an error, never permission to drop or duplicate tabs. pub(crate) fn into_update_windows(self) -> std::io::Result> { @@ -90,6 +100,28 @@ mod tests { ) } + #[test] + fn startup_geometry_survives_atomic_empty_and_multiwindow_roundtrips() { + let first = WindowState { width: 1300, height: 800, position: Some((-1500, 120)), display: Some([7; 16]), ..Default::default() }; + let active = WindowState { width: 1100, height: 700, maximized: true, ..Default::default() }; + let mut left = window(&["/first"], 0); + left.window = Some(first); + let mut right = window(&[], 0); + right.window = Some(active); + let combined = combine_sessions([(false, left), (true, right)]).unwrap(); + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("session.json"); + crate::session::save_to(&path, &combined).unwrap(); + let mut loaded = crate::session::load_from(&path).unwrap(); + assert_eq!(loaded.startup_window_state(), Some(active)); + assert_eq!(loaded.clone().into_update_windows().unwrap()[0].window, Some(first)); + loaded.boot_attempts = super::super::MAX_BOOT_ATTEMPTS; + assert_eq!(loaded.startup_window_state(), None); + let legacy: WindowState = serde_json::from_str(r#"{"width":900,"height":650,"maximized":false}"#).unwrap(); + assert_eq!(legacy.position, None); + assert_eq!(legacy.display, None); + } + #[test] fn saved_workspace_retains_window_boundaries_and_active_tabs_for_update() { let first = window(&["/first", "/second"], 1); diff --git a/nebula_app/src/window_context/session_snapshot.rs b/nebula_app/src/window_context/session_snapshot.rs index b779e205c..363f0e9a6 100644 --- a/nebula_app/src/window_context/session_snapshot.rs +++ b/nebula_app/src/window_context/session_snapshot.rs @@ -26,6 +26,7 @@ impl WindowContext { width: self.windowed_size.width, height: self.windowed_size.height, maximized, + ..Default::default() } } else { // Normal state: take the current size straight from the window. @@ -34,7 +35,9 @@ impl WindowContext { // every relaunch. let logical: LogicalSize = self.display.window.inner_size().to_logical(self.display.window.scale_factor); - session::WindowState { width: logical.width, height: logical.height, maximized } + session::WindowState { + width: logical.width, height: logical.height, maximized, ..Default::default() + } }); session } From aa89066b95ffc3b0b11ccf75e92385ae6173143b Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:09:22 +0800 Subject: [PATCH 2/6] fix: preserve full work area and first-show placement ownership --- nebula_app/src/gpui_shell/config.rs | 6 +- .../gpui_shell/workspace/session_recovery.rs | 6 +- .../src/gpui_shell/workspace/windowing.rs | 78 ++++++------------- .../workspace/windowing/startup_geometry.rs | 68 ++++++++++++---- .../workspace/windowing/transfer_tests.rs | 32 ++++++-- nebula_app/src/platform/startup.rs | 49 ++++++++++++ nebula_app/src/session.rs | 3 +- nebula_app/src/session/window_layout.rs | 21 ++++- .../src/window_context/session_snapshot.rs | 5 +- 9 files changed, 186 insertions(+), 82 deletions(-) diff --git a/nebula_app/src/gpui_shell/config.rs b/nebula_app/src/gpui_shell/config.rs index 940d2562c..f4c7cace5 100644 --- a/nebula_app/src/gpui_shell/config.rs +++ b/nebula_app/src/gpui_shell/config.rs @@ -510,7 +510,11 @@ impl StartupWindow { /// `dimensions()` 自带「行列必须同时非零」的旧壳合同。 fn from_config(config: &crate::config::UiConfig) -> Self { - Self { dimensions: config.window.dimensions(), position: config.window.position, restored: None } + Self { + dimensions: config.window.dimensions(), + position: config.window.position, + restored: None, + } } } diff --git a/nebula_app/src/gpui_shell/workspace/session_recovery.rs b/nebula_app/src/gpui_shell/workspace/session_recovery.rs index 3f10d5878..637bcb7e2 100644 --- a/nebula_app/src/gpui_shell/workspace/session_recovery.rs +++ b/nebula_app/src/gpui_shell/workspace/session_recovery.rs @@ -3,7 +3,11 @@ use super::*; impl NebulaWorkspace { - pub(super) fn save_clean_window_session(&mut self, window: &Window, cx: &mut App) -> std::io::Result<()> { + pub(super) fn save_clean_window_session( + &mut self, + window: &Window, + cx: &mut App, + ) -> std::io::Result<()> { windowing::save_current_window_session( self.runtime_window_id, self.snapshot_session(cx), diff --git a/nebula_app/src/gpui_shell/workspace/windowing.rs b/nebula_app/src/gpui_shell/workspace/windowing.rs index 07b6a4182..dad6ed2cc 100644 --- a/nebula_app/src/gpui_shell/workspace/windowing.rs +++ b/nebula_app/src/gpui_shell/workspace/windowing.rs @@ -26,10 +26,9 @@ use std::time::Duration; use super::DockTarget; use gpui::{ AnyWindowHandle, App, AppContext as _, Bounds, Context, Entity, Global, IntoElement, Render, - Styled as _, Subscription, WeakEntity, Window, WindowBounds, WindowOptions, div, point, px, - size, + Styled as _, Subscription, WeakEntity, Window, div, px, }; -use gpui_component::{Root, TitleBar}; +use gpui_component::Root; use nebula_split::SplitTree; use serde_json::json; @@ -419,52 +418,6 @@ pub(super) fn open_recipe_window(session: crate::session::Session, cx: &mut App) }); } -fn workspace_window_options( - cx: &mut App, - focus: bool, - role: WindowRole, - sidebar_width: f32, - id: u64, -) -> WindowOptions { - match role { - WindowRole::Regular => startup_geometry::regular_options(id, focus, sidebar_width, cx), - WindowRole::QuickTerminal => { - let (display_id, visible) = quick_terminal_anchor_display(cx) - .map(|(id, bounds)| (Some(id), bounds)) - .or_else(|| { - cx.primary_display().map(|display| (Some(display.id()), display.bounds())) - }) - .unwrap_or_else(|| (None, Bounds::centered(None, size(px(1080.0), px(720.0)), cx))); - let remembered = nebula_settings::RuntimeSettings::load() - .quick_terminal_size - .map(|size| size.fit(visible.size.width.into(), visible.size.height.into())); - let width = remembered.map_or(visible.size.width, |size| px(size.width)); - let height = remembered.map_or_else( - || px((f32::from(visible.size.height) * 0.4).round().max(1.0)), - |size| px(size.height), - ); - let bounds = Bounds { - origin: point( - visible.origin.x + (visible.size.width - width) * 0.5, - if cfg!(windows) { visible.origin.y - height } else { visible.origin.y }, - ), - size: size(width, height), - }; - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - // 与旧壳一致:无系统标题栏,但保留 Nebula 自绘标题栏及三枚 - // 窗口按钮。TitleBar options 负责 Windows 客户区命中测试。 - titlebar: Some(TitleBar::title_bar_options()), - app_id: Some("pebrel-quick-terminal".to_owned()), - window_background: crate::gpui_shell::wallpaper::initial_background_appearance(), - focus: !cfg!(windows), - show: !cfg!(windows), - display_id, - ..Default::default() - } - }, - } -} fn allocate_window(cx: &mut App) -> (u64, crate::runtime_api::RuntimeHub) { let registry = cx.global_mut::(); @@ -487,7 +440,8 @@ fn open_workspace_window( && matches!(startup, WorkspaceStartup::RestoreOrDefault) && crate::platform::startup::start_hidden(&runtime); startup_geometry::load_restore(runtime_window_id, &startup, role, cx); - let mut options = workspace_window_options(cx, focus, role, runtime.sidebar_width, runtime_window_id); + let mut options = + startup_geometry::options(cx, focus, role, runtime.sidebar_width, runtime_window_id); if start_hidden { options.show = false; options.focus = false; @@ -501,7 +455,9 @@ fn open_workspace_window( let handle = cx.open_window(options, move |window, cx| { window.set_window_title(crate::brand::NAME); crate::platform::window_chrome::configure(window); - if role == WindowRole::Regular && !configured_position_pending { startup_geometry::apply_configured_position(window, cx); } + if role == WindowRole::Regular && !configured_position_pending { + startup_geometry::apply_configured_position(window, cx); + } *hwnd_out.borrow_mut() = native_hwnd(window).unwrap_or_default(); #[cfg(windows)] crate::gpui_shell::set_native_window_icon(window); @@ -592,8 +548,13 @@ fn open_runtime_window( } pub(crate) fn mark_active(runtime_window_id: u64, window: &Window, cx: &mut App) { - if let Some(entry) = cx.global_mut::().entries.iter_mut().find(|entry| entry.runtime_window_id == runtime_window_id) - && std::mem::take(&mut entry.configured_position_pending) { + if let Some(entry) = cx + .global_mut::() + .entries + .iter_mut() + .find(|entry| entry.runtime_window_id == runtime_window_id) + && std::mem::take(&mut entry.configured_position_pending) + { startup_geometry::apply_configured_position(window, cx); } let registry = cx.global_mut::(); @@ -1434,7 +1395,10 @@ fn combined_session( } else { let Some(workspace) = entry.workspace.upgrade() else { continue }; let mut session = workspace.read(cx).snapshot_session(cx); - session.window = entry.handle.update(cx, |_, window, cx| startup_geometry::capture_window(window, cx)).ok(); + session.window = entry + .handle + .update(cx, |_, window, cx| startup_geometry::capture_window(window, cx)) + .ok(); session }; sessions.push((active_handle == Some(entry.handle), session)); @@ -1557,7 +1521,11 @@ pub(crate) fn close_empty_workspace_window( let session = combined_session(None, cx); let reason = if session.is_some() { SaveReason::TabsClosed } else { SaveReason::WindowClose }; - let session = session.unwrap_or_else(|| crate::session::Session::new(0, Vec::new())); + let session = session.unwrap_or_else(|| { + let mut session = crate::session::Session::new(0, Vec::new()); + session.window = Some(startup_geometry::capture_window(window, cx)); + session + }); if let Err(error) = cx.global_mut::().session_persistence.save(Some(session), reason) { diff --git a/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs b/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs index 748c200b1..a726a31af 100644 --- a/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs +++ b/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs @@ -78,6 +78,54 @@ pub(in crate::gpui_shell::workspace) fn restored_size(id: u64, startup: &Workspa && cx.try_global::().is_some_and(|config| config.dimensions.is_none() && config.restored.is_some()) } +pub(super) fn options( + cx: &mut App, + focus: bool, + role: WindowRole, + sidebar_width: f32, + id: u64, +) -> WindowOptions { + match role { + WindowRole::Regular => regular_options(id, focus, sidebar_width, cx), + WindowRole::QuickTerminal => { + let (display_id, visible) = super::quick_terminal_anchor_display(cx) + .map(|(id, bounds)| (Some(id), bounds)) + .or_else(|| { + cx.primary_display().map(|display| (Some(display.id()), display.bounds())) + }) + .unwrap_or_else(|| (None, Bounds::centered(None, size(px(1080.0), px(720.0)), cx))); + let remembered = nebula_settings::RuntimeSettings::load() + .quick_terminal_size + .map(|size| size.fit(visible.size.width.into(), visible.size.height.into())); + let width = remembered.map_or(visible.size.width, |size| px(size.width)); + let height = remembered.map_or_else( + || px((f32::from(visible.size.height) * 0.4).round().max(1.0)), + |size| px(size.height), + ); + let bounds = Bounds { + origin: point( + visible.origin.x + (visible.size.width - width) * 0.5, + if cfg!(windows) { visible.origin.y - height } else { visible.origin.y }, + ), + size: size(width, height), + }; + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + // 与旧壳一致:无系统标题栏,但保留 Nebula 自绘标题栏及三枚 + // 窗口按钮。TitleBar options 负责 Windows 客户区命中测试。 + titlebar: Some(gpui_component::TitleBar::title_bar_options()), + app_id: Some("pebrel-quick-terminal".to_owned()), + window_background: crate::gpui_shell::wallpaper::initial_background_appearance(), + focus: !cfg!(windows), + show: !cfg!(windows), + display_id, + ..Default::default() + } + }, + } +} + + pub(super) fn regular_options(id: u64, focus: bool, sidebar_width: f32, cx: &mut App) -> WindowOptions { let config = cx.try_global::(); let restored = (id == 1).then(|| config.and_then(|config| config.restored)).flatten(); @@ -102,7 +150,10 @@ pub(super) fn regular_options(id: u64, focus: bool, sidebar_width: f32, cx: &mut let mut bounds = fit_bounds(preferred, origin, visible); if let Some(scale) = scale { let offset = crate::platform::startup::placement_offset(u64::from(display_id.unwrap())); - bounds.origin -= point(px(offset.0 as f32 / scale), px(offset.1 as f32 / scale)); + let desktop = (bounds.origin.x.as_f32(), bounds.origin.y.as_f32()); + let origin = crate::platform::startup::workspace_position( + ((desktop.0 * scale).round() as i32, (desktop.1 * scale).round() as i32), scale, offset); + bounds.origin = point(px(origin.0), px(origin.1)); } let maximized = !configured_dimensions && configured.is_none() && restored.is_some_and(|window| window.maximized); crate::platform::window_chrome::configure_options(WindowOptions { @@ -121,14 +172,7 @@ pub(super) fn capture_window(window: &Window, cx: &App) -> crate::session::Windo let normal = window.window_bounds(); let bounds = normal.get_bounds(); let display = window.display(cx); - #[cfg(windows)] - let position = display.as_ref().map(|display| { - let offset = crate::platform::startup::placement_offset(u64::from(display.id())); - ((bounds.origin.x.as_f32() * window.scale_factor()).round() as i32 + offset.0, - (bounds.origin.y.as_f32() * window.scale_factor()).round() as i32 + offset.1) - }); - #[cfg(not(windows))] - let position = None; + let position = crate::platform::startup::normal_position(bounds.origin, window.scale_factor(), display.as_ref().map(|display| u64::from(display.id()))); crate::session::WindowState { width: bounds.size.width.as_f32().round().max(1.0) as u32, height: bounds.size.height.as_f32().round().max(1.0) as u32, @@ -139,14 +183,10 @@ pub(super) fn capture_window(window: &Window, cx: &App) -> crate::session::Windo } pub(super) fn apply_configured_position(window: &Window, cx: &App) { - #[cfg(windows)] if let Some(position) = cx.try_global::().and_then(|config| config.position) - && let Some(hwnd) = super::native_hwnd(window) - && !crate::platform::startup::place_normal_window(hwnd, (position.x, position.y)) { + && !crate::platform::startup::place_configured_window(window, (position.x, position.y)) { log::warn!("Could not apply configured window position"); } - #[cfg(not(windows))] - let _ = (window, cx); } fn same_device_size(actual: Size, requested: Size, scale: f32) -> bool { diff --git a/nebula_app/src/gpui_shell/workspace/windowing/transfer_tests.rs b/nebula_app/src/gpui_shell/workspace/windowing/transfer_tests.rs index 75691958b..01760fd66 100644 --- a/nebula_app/src/gpui_shell/workspace/windowing/transfer_tests.rs +++ b/nebula_app/src/gpui_shell/workspace/windowing/transfer_tests.rs @@ -1,6 +1,6 @@ use super::*; use crate::gpui_shell::terminal::view::TerminalLaunch; -use gpui::TestAppContext; +use gpui::{TestAppContext, size}; fn initialize_test(cx: &mut TestAppContext) { cx.update(|cx| { @@ -117,10 +117,13 @@ fn ordinary_window_geometry_is_in_the_durable_snapshot(cx: &mut TestAppContext) cx.update(|cx| { let (id, _) = open_test_window(cx, 1); let entry = entry_by_id(id, cx).unwrap(); - let expected = entry.handle.update(cx, |_, window, _| { - window.resize(size(px(1300.0), px(800.0))); - window.window_bounds().get_bounds().size - }).unwrap(); + let expected = entry + .handle + .update(cx, |_, window, _| { + window.resize(size(px(1300.0), px(800.0))); + window.window_bounds().get_bounds().size + }) + .unwrap(); let saved = combined_session(None, cx).unwrap().into_update_windows().unwrap(); let geometry = saved[0].window.expect("ordinary window geometry must survive process exit"); assert_eq!(geometry.width, expected.width.as_f32().round() as u32); @@ -128,3 +131,22 @@ fn ordinary_window_geometry_is_in_the_durable_snapshot(cx: &mut TestAppContext) assert!(!geometry.maximized); }); } + +#[gpui::test] +fn ordinary_window_geometry_survives_closing_the_last_empty_window(cx: &mut TestAppContext) { + initialize_test(cx); + cx.update(|cx| { + let (id, _) = open_test_window(cx, 0); + let entry = entry_by_id(id, cx).unwrap(); + let expected = entry.handle.update(cx, |_, window, cx| { + let expected = window.window_bounds().get_bounds().size; + close_empty_workspace_window(id, window, cx); + expected + }).unwrap(); + let saved = cx.global::().session_persistence.update_windows().unwrap(); + assert!(saved[0].tabs.is_empty(), "closing the last tab must not resurrect a shell"); + let geometry = saved[0].window.expect("last empty window must preserve its native geometry"); + assert_eq!(geometry.width, expected.width.as_f32().round() as u32); + assert_eq!(geometry.height, expected.height.as_f32().round() as u32); + }); +} diff --git a/nebula_app/src/platform/startup.rs b/nebula_app/src/platform/startup.rs index 9cb63aab8..507af25d2 100644 --- a/nebula_app/src/platform/startup.rs +++ b/nebula_app/src/platform/startup.rs @@ -226,3 +226,52 @@ pub(crate) fn place_normal_window(hwnd: isize, desired: (i32, i32)) -> bool { // SAFETY: only the live window position changes; ownership and size are preserved. unsafe { SetWindowPos(hwnd, std::ptr::null_mut(), x, y, 0, 0, SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOZORDER) != 0 } } + +#[cfg(feature = "gpui-shell")] +pub(crate) fn normal_position(origin: gpui::Point, scale: f32, display: Option) -> Option<(i32, i32)> { + #[cfg(windows)] + return display.map(|display| { + let offset = placement_offset(display); + desktop_position((origin.x.as_f32(), origin.y.as_f32()), scale, offset) + }); + #[cfg(not(windows))] + { let _ = (origin, scale, display); None } +} + +#[cfg(feature = "gpui-shell")] +pub(crate) fn place_configured_window(window: &gpui::Window, desired: (i32, i32)) -> bool { + #[cfg(windows)] + { + use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle}; + let Ok(handle) = HasWindowHandle::window_handle(window) else { return false }; + let RawWindowHandle::Win32(handle) = handle.as_raw() else { return false }; + place_normal_window(handle.hwnd.get(), desired) + } + #[cfg(not(windows))] + { let _ = (window, desired); true } +} + +/// Convert the pinned Windows backend's client/workspace point to desktop pixels. +pub(crate) fn desktop_position(origin: (f32, f32), scale: f32, offset: (i32, i32)) -> (i32, i32) { + ((origin.0 * scale).round() as i32 + offset.0, (origin.1 * scale).round() as i32 + offset.1) +} + +/// Convert desktop pixels to the client/workspace point accepted by GPUI placement. +pub(crate) fn workspace_position(position: (i32, i32), scale: f32, offset: (i32, i32)) -> (f32, f32) { + ((position.0 - offset.0) as f32 / scale, (position.1 - offset.1) as f32 / scale) +} + +#[cfg(test)] +mod geometry_tests { + #[test] + fn windows_workspace_desktop_roundtrip_keeps_top_and_left_taskbars_at_fractional_dpi() { + for scale in [1.0, 1.25, 1.5, 2.0] { + for offset in [(0, 0), (0, 60), (80, 0)] { + for position in [(-1700, 120), (197, 102), (0, 0)] { + let origin = super::workspace_position(position, scale, offset); + assert_eq!(super::desktop_position(origin, scale, offset), position); + } + } + } + } +} diff --git a/nebula_app/src/session.rs b/nebula_app/src/session.rs index 0762c83d9..8e8e163f8 100644 --- a/nebula_app/src/session.rs +++ b/nebula_app/src/session.rs @@ -450,7 +450,8 @@ mod tests { #[test] fn window_state_round_trip_preserves_logical_size_and_maximize() { let mut session = Session::new(0, Vec::new()); - session.window = Some(WindowState { width: 1280, height: 720, maximized: true, ..Default::default() }); + session.window = + Some(WindowState { width: 1280, height: 720, maximized: true, ..Default::default() }); let json = serde_json::to_string(&session).unwrap(); let restored: Session = serde_json::from_str(&json).unwrap(); assert_eq!(restored.window, session.window); diff --git a/nebula_app/src/session/window_layout.rs b/nebula_app/src/session/window_layout.rs index 97d512f3b..c168920b0 100644 --- a/nebula_app/src/session/window_layout.rs +++ b/nebula_app/src/session/window_layout.rs @@ -40,7 +40,12 @@ impl Session { return None; } self.window - .or_else(|| self.window_layout.iter().find(|layout| layout.active).and_then(|layout| layout.window)) + .or_else(|| { + self.window_layout + .iter() + .find(|layout| layout.active) + .and_then(|layout| layout.window) + }) .or_else(|| self.window_layout.first().and_then(|layout| layout.window)) .filter(|window| window.width > 0 && window.height > 0) } @@ -102,8 +107,15 @@ mod tests { #[test] fn startup_geometry_survives_atomic_empty_and_multiwindow_roundtrips() { - let first = WindowState { width: 1300, height: 800, position: Some((-1500, 120)), display: Some([7; 16]), ..Default::default() }; - let active = WindowState { width: 1100, height: 700, maximized: true, ..Default::default() }; + let first = WindowState { + width: 1300, + height: 800, + position: Some((-1500, 120)), + display: Some([7; 16]), + ..Default::default() + }; + let active = + WindowState { width: 1100, height: 700, maximized: true, ..Default::default() }; let mut left = window(&["/first"], 0); left.window = Some(first); let mut right = window(&[], 0); @@ -117,7 +129,8 @@ mod tests { assert_eq!(loaded.clone().into_update_windows().unwrap()[0].window, Some(first)); loaded.boot_attempts = super::super::MAX_BOOT_ATTEMPTS; assert_eq!(loaded.startup_window_state(), None); - let legacy: WindowState = serde_json::from_str(r#"{"width":900,"height":650,"maximized":false}"#).unwrap(); + let legacy: WindowState = + serde_json::from_str(r#"{"width":900,"height":650,"maximized":false}"#).unwrap(); assert_eq!(legacy.position, None); assert_eq!(legacy.display, None); } diff --git a/nebula_app/src/window_context/session_snapshot.rs b/nebula_app/src/window_context/session_snapshot.rs index 363f0e9a6..181f25596 100644 --- a/nebula_app/src/window_context/session_snapshot.rs +++ b/nebula_app/src/window_context/session_snapshot.rs @@ -36,7 +36,10 @@ impl WindowContext { let logical: LogicalSize = self.display.window.inner_size().to_logical(self.display.window.scale_factor); session::WindowState { - width: logical.width, height: logical.height, maximized, ..Default::default() + width: logical.width, + height: logical.height, + maximized, + ..Default::default() } }); session From f99fcbac29a346d7df89e7b586b0156c506371e2 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:17:58 +0800 Subject: [PATCH 3/6] test: verify native window placement and durable empty geometry --- .../2026-10-03-ordinary-window-geometry.md | 6 +- nebula_app/src/gpui_shell/config.rs | 9 +- nebula_app/src/gpui_shell/workspace.rs | 5 +- .../workspace/session_persistence.rs | 7 +- .../src/gpui_shell/workspace/windowing.rs | 1 - .../workspace/windowing/startup_geometry.rs | 178 ++++++++++++++---- .../workspace/windowing/transfer_tests.rs | 17 +- nebula_app/src/platform/startup.rs | 54 ++++++ 8 files changed, 219 insertions(+), 58 deletions(-) diff --git a/architecture/notes/nebula_app/gpui_shell/workspace/2026-10-03-ordinary-window-geometry.md b/architecture/notes/nebula_app/gpui_shell/workspace/2026-10-03-ordinary-window-geometry.md index 20b40dc60..3bc36c259 100644 --- a/architecture/notes/nebula_app/gpui_shell/workspace/2026-10-03-ordinary-window-geometry.md +++ b/architecture/notes/nebula_app/gpui_shell/workspace/2026-10-03-ordinary-window-geometry.md @@ -41,8 +41,10 @@ force a fullscreen startup; maximized state may be restored. Explicit Lua dimensions override saved size; explicit Lua position overrides saved position. Missing dimensions are not an implicit explicit default. Configured Windows position describes the outer window in physical desktop -pixels, as the legacy configuration does. Apply it before first presentation -using `SetWindowPos` because native border insets are known only after creation. +pixels, as the legacy configuration does. Apply it using `SetWindowPos` because native border insets are known only after +creation. Hidden Windows startup retains a one-shot position until its first +active frame, after GPUI consumes its cached native placement; applying it +in the builder would be overwritten by activation. Other platforms keep their existing position behavior pending an equivalent public/native placement contract. diff --git a/nebula_app/src/gpui_shell/config.rs b/nebula_app/src/gpui_shell/config.rs index f4c7cace5..f7fc8133f 100644 --- a/nebula_app/src/gpui_shell/config.rs +++ b/nebula_app/src/gpui_shell/config.rs @@ -885,7 +885,7 @@ mod tests { &path, "local pebrel = require 'pebrel'\n\ local config = pebrel.config_builder()\n\ - config.window = { dimensions = { columns = 123, lines = 37 } }\n\ + config.window = { dimensions = { columns = 123, lines = 37 }, position = { x = 197, y = 102 } }\n\ return config\n", ) .unwrap(); @@ -896,10 +896,9 @@ mod tests { ]) .unwrap(); - assert_eq!( - StartupWindow::load(options.config_file).dimensions, - Some(Dimensions { columns: 123, lines: 37 }) - ); + let startup = StartupWindow::load(options.config_file); + assert_eq!(startup.dimensions, Some(Dimensions { columns: 123, lines: 37 })); + assert_eq!(startup.position, Some(crate::config::ui_config::Delta { x: 197, y: 102 })); } /// 用户写在 `pebrel.lua` 里的 `config.window.dimensions` 必须真的决定启动 diff --git a/nebula_app/src/gpui_shell/workspace.rs b/nebula_app/src/gpui_shell/workspace.rs index 0c63f759b..71ff2663f 100644 --- a/nebula_app/src/gpui_shell/workspace.rs +++ b/nebula_app/src/gpui_shell/workspace.rs @@ -836,12 +836,13 @@ impl NebulaWorkspace { let runtime = nebula_settings::RuntimeSettings::load(); let sidebar_width = runtime.sidebar_width; windowing::observe_window_bounds(runtime_window_id, window, cx); + let fit_default_grid = window_role == windowing::WindowRole::Regular + && !windowing::startup_geometry::restored_size(runtime_window_id, &startup, cx); let initial_grid = windowing::prepare_initial_grid( window, cx, sidebar_width, - window_role == windowing::WindowRole::Regular - && !windowing::startup_geometry::restored_size(runtime_window_id, &startup, cx), + fit_default_grid, ); let this = cx.entity().downgrade(); let appearance_sub = window.observe_window_appearance(move |_, cx| { diff --git a/nebula_app/src/gpui_shell/workspace/session_persistence.rs b/nebula_app/src/gpui_shell/workspace/session_persistence.rs index d262f5c94..82a912442 100644 --- a/nebula_app/src/gpui_shell/workspace/session_persistence.rs +++ b/nebula_app/src/gpui_shell/workspace/session_persistence.rs @@ -28,6 +28,11 @@ impl Default for SessionPersistence { } impl SessionPersistence { + #[cfg(test)] + pub(super) fn ordinary_for_test() -> Self { + Self { isolated: false, ..Self::default() } + } + pub(super) fn update_windows(&self) -> std::io::Result> { self.saved .clone() @@ -123,7 +128,7 @@ mod tests { fn ordinary_window() -> SessionPersistence { // Hosted Windows runners can be elevated. These tests exercise ordinary // window persistence; privileged isolation has its own negative test. - SessionPersistence { isolated: false, ..SessionPersistence::default() } + SessionPersistence::ordinary_for_test() } fn sample_session() -> Session { diff --git a/nebula_app/src/gpui_shell/workspace/windowing.rs b/nebula_app/src/gpui_shell/workspace/windowing.rs index dad6ed2cc..a2fbfc400 100644 --- a/nebula_app/src/gpui_shell/workspace/windowing.rs +++ b/nebula_app/src/gpui_shell/workspace/windowing.rs @@ -418,7 +418,6 @@ pub(super) fn open_recipe_window(session: crate::session::Session, cx: &mut App) }); } - fn allocate_window(cx: &mut App) -> (u64, crate::runtime_api::RuntimeHub) { let registry = cx.global_mut::(); let id = registry.next_window_id; diff --git a/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs b/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs index a726a31af..51ed351b4 100644 --- a/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs +++ b/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs @@ -4,9 +4,11 @@ //! platform exposes display DPI, supply the final size before the window is //! shown; otherwise keep the existing post-creation sizing fallback. -use gpui::{App, Bounds, Pixels, Point, Size, Window, WindowBounds, WindowOptions, point, px, size}; use super::{WindowRole, WorkspaceStartup}; use crate::gpui_shell::config::StartupWindow; +use gpui::{ + App, Bounds, Pixels, Point, Size, Window, WindowBounds, WindowOptions, point, px, size, +}; use crate::gpui_shell::terminal::view::TerminalView; @@ -26,8 +28,12 @@ fn default_size( display_limit.map_or(preferred, |limit| preferred.min(&limit)) } +fn default_display_limit(visible: Size) -> Size { + size(visible.width * 0.95, visible.height * 0.95) +} + fn display_limit(window: &Window, cx: &App) -> Option> { - window.display(cx).map(|display| display.visible_bounds().size * 0.95) + window.display(cx).map(|display| default_display_limit(display.visible_bounds().size)) } fn fit_native_size(preferred: Size, visible: Option>) -> Size { @@ -48,34 +54,63 @@ pub(super) fn preferred_size(cx: &App, sidebar_width: f32) -> Option, origin: Option>, visible: Bounds) -> Bounds { +fn fit_bounds( + preferred: Size, + origin: Option>, + visible: Bounds, +) -> Bounds { let fitted = fit_native_size(preferred, Some(visible.size)); - let origin = origin.unwrap_or_else(|| point( - visible.origin.x + (visible.size.width - fitted.width) / 2.0, - visible.origin.y + (visible.size.height - fitted.height) / 2.0, - )); - Bounds::new(point( - px(origin.x.as_f32().clamp(visible.left().as_f32(), (visible.right() - fitted.width).as_f32().max(visible.left().as_f32()))), - px(origin.y.as_f32().clamp(visible.top().as_f32(), (visible.bottom() - fitted.height).as_f32().max(visible.top().as_f32()))), - ), fitted) + let origin = origin.unwrap_or_else(|| { + point( + visible.origin.x + (visible.size.width - fitted.width) / 2.0, + visible.origin.y + (visible.size.height - fitted.height) / 2.0, + ) + }); + Bounds::new( + point( + px(origin.x.as_f32().clamp( + visible.left().as_f32(), + (visible.right() - fitted.width).as_f32().max(visible.left().as_f32()), + )), + px(origin.y.as_f32().clamp( + visible.top().as_f32(), + (visible.bottom() - fitted.height).as_f32().max(visible.top().as_f32()), + )), + ), + fitted, + ) } pub(super) fn load_restore(id: u64, startup: &WorkspaceStartup, role: WindowRole, cx: &mut App) { - if id != 1 || role != WindowRole::Regular || !matches!(startup, WorkspaceStartup::RestoreOrDefault) - || crate::platform::elevation::requires_isolation() { return; } + if id != 1 + || role != WindowRole::Regular + || !matches!(startup, WorkspaceStartup::RestoreOrDefault) + || crate::platform::elevation::requires_isolation() + { + return; + } let restored = crate::session::load().and_then(|session| session.startup_window_state()); - if let Some(config) = cx.try_global_mut::() { config.restored = restored; } + if cx.has_global::() { + cx.global_mut::().restored = restored; + } } -pub(in crate::gpui_shell::workspace) fn restored_size(id: u64, startup: &WorkspaceStartup, cx: &App) -> bool { - id == 1 && matches!(startup, WorkspaceStartup::RestoreOrDefault) - && cx.try_global::().is_some_and(|config| config.dimensions.is_none() && config.restored.is_some()) +pub(in crate::gpui_shell::workspace) fn restored_size( + id: u64, + startup: &WorkspaceStartup, + cx: &App, +) -> bool { + id == 1 + && matches!(startup, WorkspaceStartup::RestoreOrDefault) + && cx + .try_global::() + .is_some_and(|config| config.dimensions.is_none() && config.restored.is_some()) } pub(super) fn options( @@ -125,39 +160,76 @@ pub(super) fn options( } } - -pub(super) fn regular_options(id: u64, focus: bool, sidebar_width: f32, cx: &mut App) -> WindowOptions { +pub(super) fn regular_options( + id: u64, + focus: bool, + sidebar_width: f32, + cx: &mut App, +) -> WindowOptions { let config = cx.try_global::(); let restored = (id == 1).then(|| config.and_then(|config| config.restored)).flatten(); - let configured = config.and_then(|config| config.position).map(|position| (position.x, position.y)); + let configured = + config.and_then(|config| config.position).map(|position| (position.x, position.y)); let physical = configured.or_else(|| restored.and_then(|window| window.position)); let physical_display = configured.and_then(crate::platform::startup::physical_display); let displays = cx.displays(); - let selected = physical_display.and_then(|(id, _)| displays.iter().find(|display| u64::from(display.id()) == id).cloned()) - .or_else(|| restored.and_then(|window| window.display).and_then(|uuid| displays.iter().find(|display| display.uuid().is_ok_and(|id| id.as_bytes() == &uuid)).cloned())) - .or_else(|| physical.and_then(crate::platform::startup::physical_display).and_then(|(id, _)| displays.iter().find(|display| u64::from(display.id()) == id).cloned())) + let selected = physical_display + .and_then(|(id, _)| displays.iter().find(|display| u64::from(display.id()) == id).cloned()) + .or_else(|| { + restored.and_then(|window| window.display).and_then(|uuid| { + displays + .iter() + .find(|display| display.uuid().is_ok_and(|id| id.as_bytes() == &uuid)) + .cloned() + }) + }) + .or_else(|| { + physical.and_then(crate::platform::startup::physical_display).and_then(|(id, _)| { + displays.iter().find(|display| u64::from(display.id()) == id).cloned() + }) + }) .or_else(|| cx.primary_display()); let display_id = selected.as_ref().map(|display| display.id()); let scale = display_id.and_then(|id| crate::platform::startup::display_scale(u64::from(id))); - let visible = selected.map(|display| display.visible_bounds()).unwrap_or_else(|| Bounds::centered(None, size(px(1080.0), px(720.0)), cx)); + let visible = selected + .map(|display| display.visible_bounds()) + .unwrap_or_else(|| Bounds::centered(None, size(px(1080.0), px(720.0)), cx)); let configured_dimensions = config.is_some_and(|config| config.dimensions.is_some()); let preferred = if !configured_dimensions && let Some(restored) = restored { size(px(restored.width as f32), px(restored.height as f32)) } else if let Some(scale) = scale { - default_size(TerminalView::startup_cell_metrics_at_scale(scale, cx), crate::gpui_shell::config::startup_grid(cx), sidebar_width, Some(visible.size * 0.95)) - } else { preferred_size(cx, sidebar_width).unwrap_or_else(|| size(px(1080.0), px(720.0))) }; - let origin = physical.zip(scale).map(|((x, y), scale)| point(px(x as f32 / scale), px(y as f32 / scale))); + default_size( + TerminalView::startup_cell_metrics_at_scale(scale, cx), + crate::gpui_shell::config::startup_grid(cx), + sidebar_width, + Some(default_display_limit(visible.size)), + ) + } else { + preferred_size(cx, sidebar_width).unwrap_or_else(|| size(px(1080.0), px(720.0))) + }; + let origin = physical + .zip(scale) + .map(|((x, y), scale)| point(px(x as f32 / scale), px(y as f32 / scale))); let mut bounds = fit_bounds(preferred, origin, visible); if let Some(scale) = scale { let offset = crate::platform::startup::placement_offset(u64::from(display_id.unwrap())); let desktop = (bounds.origin.x.as_f32(), bounds.origin.y.as_f32()); let origin = crate::platform::startup::workspace_position( - ((desktop.0 * scale).round() as i32, (desktop.1 * scale).round() as i32), scale, offset); + ((desktop.0 * scale).round() as i32, (desktop.1 * scale).round() as i32), + scale, + offset, + ); bounds.origin = point(px(origin.0), px(origin.1)); } - let maximized = !configured_dimensions && configured.is_none() && restored.is_some_and(|window| window.maximized); + let maximized = !configured_dimensions + && configured.is_none() + && restored.is_some_and(|window| window.maximized); crate::platform::window_chrome::configure_options(WindowOptions { - window_bounds: Some(if maximized { WindowBounds::Maximized(bounds) } else { WindowBounds::Windowed(bounds) }), + window_bounds: Some(if maximized { + WindowBounds::Maximized(bounds) + } else { + WindowBounds::Windowed(bounds) + }), window_min_size: Some(size(px(760.0), px(540.0)).min(&bounds.size)), titlebar: Some(gpui_component::TitleBar::title_bar_options()), app_id: Some("pebrel".to_owned()), @@ -172,7 +244,11 @@ pub(super) fn capture_window(window: &Window, cx: &App) -> crate::session::Windo let normal = window.window_bounds(); let bounds = normal.get_bounds(); let display = window.display(cx); - let position = crate::platform::startup::normal_position(bounds.origin, window.scale_factor(), display.as_ref().map(|display| u64::from(display.id()))); + let position = crate::platform::startup::normal_position( + bounds.origin, + window.scale_factor(), + display.as_ref().map(|display| u64::from(display.id())), + ); crate::session::WindowState { width: bounds.size.width.as_f32().round().max(1.0) as u32, height: bounds.size.height.as_f32().round().max(1.0) as u32, @@ -184,7 +260,8 @@ pub(super) fn capture_window(window: &Window, cx: &App) -> crate::session::Windo pub(super) fn apply_configured_position(window: &Window, cx: &App) { if let Some(position) = cx.try_global::().and_then(|config| config.position) - && !crate::platform::startup::place_configured_window(window, (position.x, position.y)) { + && !crate::platform::startup::place_configured_window(window, (position.x, position.y)) + { log::warn!("Could not apply configured window position"); } } @@ -212,7 +289,10 @@ pub(in crate::gpui_shell::workspace) fn prepare_initial_grid( display_limit(window, cx), ); let requested = if crate::platform::startup::primary_display_scale().is_some() { - fit_native_size(requested, window.display(cx).map(|display| display.visible_bounds().size)) + fit_native_size( + requested, + window.display(cx).map(|display| display.visible_bounds().size), + ) } else { // Keep the original sizing policy on platforms without preflight DPI. requested @@ -246,7 +326,8 @@ mod tests { #[test] fn restoring_a_large_legal_window_does_not_shrink_it() { let visible = Bounds::new(point(px(0.0), px(0.0)), size(px(1920.0), px(1040.0))); - let bounds = fit_bounds(size(px(1900.0), px(1030.0)), Some(point(px(10.0), px(5.0))), visible); + let bounds = + fit_bounds(size(px(1900.0), px(1030.0)), Some(point(px(10.0), px(5.0))), visible); assert_eq!(bounds.size, size(px(1900.0), px(1030.0))); assert_eq!(bounds.origin, point(px(10.0), px(5.0))); } @@ -254,10 +335,12 @@ mod tests { #[test] fn restored_bounds_stay_visible_on_the_selected_negative_origin_display() { let visible = Bounds::new(point(px(-1920.0), px(40.0)), size(px(1920.0), px(1040.0))); - let bounds = fit_bounds(size(px(1300.0), px(800.0)), Some(point(px(-1700.0), px(120.0))), visible); + let bounds = + fit_bounds(size(px(1300.0), px(800.0)), Some(point(px(-1700.0), px(120.0))), visible); assert_eq!(bounds.origin, point(px(-1700.0), px(120.0))); assert_eq!(bounds.size, size(px(1300.0), px(800.0))); - let offscreen = fit_bounds(size(px(3000.0), px(2000.0)), Some(point(px(9000.0), px(-9000.0))), visible); + let offscreen = + fit_bounds(size(px(3000.0), px(2000.0)), Some(point(px(9000.0), px(-9000.0))), visible); assert!(visible.contains(&offscreen.origin)); assert!(offscreen.right() <= visible.right()); assert!(offscreen.bottom() <= visible.bottom()); @@ -265,15 +348,28 @@ mod tests { #[cfg(feature = "gpui-test-support")] #[gpui::test] - fn restored_dimensions_apply_only_to_the_first_window_and_explicit_grid_wins(cx: &mut gpui::TestAppContext) { + fn restored_dimensions_apply_only_to_the_first_window_and_explicit_grid_wins( + cx: &mut gpui::TestAppContext, + ) { cx.update(|cx| { - cx.set_global(StartupWindow { restored: Some(crate::session::WindowState { width: 900, height: 650, ..Default::default() }), ..Default::default() }); + cx.set_global(StartupWindow { + restored: Some(crate::session::WindowState { + width: 900, + height: 650, + ..Default::default() + }), + ..Default::default() + }); assert!(restored_size(1, &WorkspaceStartup::RestoreOrDefault, cx)); assert!(!restored_size(2, &WorkspaceStartup::RestoreOrDefault, cx)); assert!(!restored_size(1, &WorkspaceStartup::Empty, cx)); let options = regular_options(1, false, 230.0, cx); - assert_eq!(options.window_bounds.unwrap().get_bounds().size, size(px(900.0), px(650.0))); - cx.global_mut::().dimensions = Some(crate::config::window::Dimensions { columns: 155, lines: 43 }); + assert_eq!( + options.window_bounds.unwrap().get_bounds().size, + size(px(900.0), px(650.0)) + ); + cx.global_mut::().dimensions = + Some(crate::config::window::Dimensions { columns: 155, lines: 43 }); assert!(!restored_size(1, &WorkspaceStartup::RestoreOrDefault, cx)); }); } diff --git a/nebula_app/src/gpui_shell/workspace/windowing/transfer_tests.rs b/nebula_app/src/gpui_shell/workspace/windowing/transfer_tests.rs index 01760fd66..f816f068e 100644 --- a/nebula_app/src/gpui_shell/workspace/windowing/transfer_tests.rs +++ b/nebula_app/src/gpui_shell/workspace/windowing/transfer_tests.rs @@ -136,16 +136,21 @@ fn ordinary_window_geometry_is_in_the_durable_snapshot(cx: &mut TestAppContext) fn ordinary_window_geometry_survives_closing_the_last_empty_window(cx: &mut TestAppContext) { initialize_test(cx); cx.update(|cx| { + cx.global_mut::().session_persistence = SessionPersistence::ordinary_for_test(); let (id, _) = open_test_window(cx, 0); let entry = entry_by_id(id, cx).unwrap(); - let expected = entry.handle.update(cx, |_, window, cx| { - let expected = window.window_bounds().get_bounds().size; - close_empty_workspace_window(id, window, cx); - expected - }).unwrap(); + let expected = entry + .handle + .update(cx, |_, window, cx| { + let expected = window.window_bounds().get_bounds().size; + close_empty_workspace_window(id, window, cx); + expected + }) + .unwrap(); let saved = cx.global::().session_persistence.update_windows().unwrap(); assert!(saved[0].tabs.is_empty(), "closing the last tab must not resurrect a shell"); - let geometry = saved[0].window.expect("last empty window must preserve its native geometry"); + let geometry = + saved[0].window.expect("last empty window must preserve its native geometry"); assert_eq!(geometry.width, expected.width.as_f32().round() as u32); assert_eq!(geometry.height, expected.height.as_f32().round() as u32); }); diff --git a/nebula_app/src/platform/startup.rs b/nebula_app/src/platform/startup.rs index 507af25d2..7b3ed16e9 100644 --- a/nebula_app/src/platform/startup.rs +++ b/nebula_app/src/platform/startup.rs @@ -275,3 +275,57 @@ mod geometry_tests { } } } + +#[cfg(all(test, windows, feature = "gpui-shell"))] +mod native_geometry_tests { + use windows_sys::Win32::Foundation::RECT; + use windows_sys::Win32::Graphics::Gdi::{GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTONEAREST}; + use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW; + use windows_sys::Win32::UI::WindowsAndMessaging::{CreateWindowExW, DestroyWindow, GetClientRect, GetWindowRect, GetWindowPlacement, SetWindowPlacement, WINDOWPLACEMENT, WS_OVERLAPPEDWINDOW}; + + struct NativeWindow(isize); + impl Drop for NativeWindow { + fn drop(&mut self) { + // SAFETY: this test owns the native window until teardown. + assert_ne!(unsafe { DestroyWindow(self.0 as _) }, 0); + } + } + + #[test] + fn physical_outer_position_and_backend_normal_placement_roundtrip() { + // SAFETY: STATIC is a registered class; no pointer escapes the native call. + let hwnd = unsafe { CreateWindowExW(0, windows_core::w!("STATIC").as_ptr(), windows_core::w!("Pebrel geometry test").as_ptr(), WS_OVERLAPPEDWINDOW, 0, 0, 320, 240, std::ptr::null_mut(), std::ptr::null_mut(), GetModuleHandleW(std::ptr::null()), std::ptr::null()) }; + assert!(!hwnd.is_null()); + let _window = NativeWindow(hwnd as isize); + // SAFETY: plain native output structures admit zero initialization. + let (mut rect, mut client): (RECT, RECT) = unsafe { std::mem::zeroed() }; + let mut info: MONITORINFO = unsafe { std::mem::zeroed() }; + info.cbSize = std::mem::size_of::() as u32; + // SAFETY: the owned HWND and initialized output structure are valid. + let monitor = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }; + assert_ne!(unsafe { GetMonitorInfoW(monitor, &mut info) }, 0); + let desired = (info.rcWork.left + 197, info.rcWork.top + 102); + assert!(super::place_normal_window(hwnd as isize, desired)); + // SAFETY: all output pointers refer to initialized local structures. + assert_ne!(unsafe { GetWindowRect(hwnd, &mut rect) }, 0); + assert_ne!(unsafe { GetClientRect(hwnd, &mut client) }, 0); + assert_eq!((rect.left, rect.top), desired); + let before = (rect.left, rect.top, rect.right, rect.bottom); + let border = ((rect.right - rect.left - client.right) / 2, (rect.bottom - rect.top - client.bottom) / 2); + let mut placement: WINDOWPLACEMENT = unsafe { std::mem::zeroed() }; + placement.length = std::mem::size_of::() as u32; + assert_ne!(unsafe { GetWindowPlacement(hwnd, &mut placement) }, 0); + let scale = super::display_scale(monitor as u64).unwrap(); + let native = placement.rcNormalPosition; + let logical = ((native.left + border.0) as f32 / scale, (native.top + border.1) as f32 / scale); + let offset = super::placement_offset(monitor as u64); + let saved = super::desktop_position(logical, scale, offset); + let restored = super::workspace_position(saved, scale, offset); + let x = (restored.0 * scale).round() as i32 - border.0; + let y = (restored.1 * scale).round() as i32 - border.1; + placement.rcNormalPosition = RECT { left: x, top: y, right: x + native.right - native.left, bottom: y + native.bottom - native.top }; + assert_ne!(unsafe { SetWindowPlacement(hwnd, &placement) }, 0); + assert_ne!(unsafe { GetWindowRect(hwnd, &mut rect) }, 0); + assert_eq!((rect.left, rect.top, rect.right, rect.bottom), before); + } +} From 0afbbde9c7705121e0469143d83de89d41f3cbdb Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:23:34 +0800 Subject: [PATCH 4/6] style: apply remote formatting and tighten platform budget --- nebula_app/src/gpui_shell/workspace.rs | 8 +- .../workspace/windowing/startup_geometry.rs | 3 +- .../workspace/windowing/transfer_tests.rs | 3 +- nebula_app/src/platform/startup.rs | 125 +++++++++++++++--- scripts/platform_cfg_budget.txt | 2 +- 5 files changed, 110 insertions(+), 31 deletions(-) diff --git a/nebula_app/src/gpui_shell/workspace.rs b/nebula_app/src/gpui_shell/workspace.rs index 71ff2663f..4a6dd6667 100644 --- a/nebula_app/src/gpui_shell/workspace.rs +++ b/nebula_app/src/gpui_shell/workspace.rs @@ -838,12 +838,8 @@ impl NebulaWorkspace { windowing::observe_window_bounds(runtime_window_id, window, cx); let fit_default_grid = window_role == windowing::WindowRole::Regular && !windowing::startup_geometry::restored_size(runtime_window_id, &startup, cx); - let initial_grid = windowing::prepare_initial_grid( - window, - cx, - sidebar_width, - fit_default_grid, - ); + let initial_grid = + windowing::prepare_initial_grid(window, cx, sidebar_width, fit_default_grid); let this = cx.entity().downgrade(); let appearance_sub = window.observe_window_appearance(move |_, cx| { if let Some(workspace) = this.upgrade() { diff --git a/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs b/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs index 51ed351b4..731547b31 100644 --- a/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs +++ b/nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs @@ -54,7 +54,8 @@ pub(super) fn preferred_size(cx: &App, sidebar_width: f32) -> Option().session_persistence = SessionPersistence::ordinary_for_test(); + cx.global_mut::().session_persistence = + SessionPersistence::ordinary_for_test(); let (id, _) = open_test_window(cx, 0); let entry = entry_by_id(id, cx).unwrap(); let expected = entry diff --git a/nebula_app/src/platform/startup.rs b/nebula_app/src/platform/startup.rs index 7b3ed16e9..8799f3282 100644 --- a/nebula_app/src/platform/startup.rs +++ b/nebula_app/src/platform/startup.rs @@ -168,13 +168,17 @@ pub(crate) fn physical_display(point: (i32, i32)) -> Option<(u64, f32)> { #[cfg(windows)] { use windows_sys::Win32::Foundation::POINT; - use windows_sys::Win32::Graphics::Gdi::{MonitorFromPoint, MONITOR_DEFAULTTONEAREST}; + use windows_sys::Win32::Graphics::Gdi::{MONITOR_DEFAULTTONEAREST, MonitorFromPoint}; // SAFETY: this queries a borrowed monitor handle without retaining it. - let monitor = unsafe { MonitorFromPoint(POINT { x: point.0, y: point.1 }, MONITOR_DEFAULTTONEAREST) }; + let monitor = + unsafe { MonitorFromPoint(POINT { x: point.0, y: point.1 }, MONITOR_DEFAULTTONEAREST) }; display_scale(monitor as u64).map(|scale| (monitor as u64, scale)) } #[cfg(not(windows))] - { let _ = point; None } + { + let _ = point; + None + } } pub(crate) fn display_scale(id: u64) -> Option { @@ -184,12 +188,21 @@ pub(crate) fn display_scale(id: u64) -> Option { let (mut x, mut y) = (0, 0); let monitor = id as *mut core::ffi::c_void; // SAFETY: the borrowed monitor and local DPI output pointers stay valid. - if monitor.is_null() || unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut x, &mut y) } < 0 || x == 0 || x != y { + if monitor.is_null() + || unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut x, &mut y) } < 0 + || x == 0 + || x != y + { None - } else { Some(x as f32 / 96.0) } + } else { + Some(x as f32 / 96.0) + } } #[cfg(not(windows))] - { let _ = id; None } + { + let _ = id; + None + } } pub(crate) fn placement_offset(id: u64) -> (i32, i32) { @@ -211,8 +224,12 @@ pub(crate) fn placement_offset(id: u64) -> (i32, i32) { #[cfg(windows)] pub(crate) fn place_normal_window(hwnd: isize, desired: (i32, i32)) -> bool { use windows_sys::Win32::Foundation::RECT; - use windows_sys::Win32::Graphics::Gdi::{GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTONEAREST}; - use windows_sys::Win32::UI::WindowsAndMessaging::{GetWindowRect, SetWindowPos, SWP_NOSIZE, SWP_NOACTIVATE, SWP_NOZORDER}; + use windows_sys::Win32::Graphics::Gdi::{ + GetMonitorInfoW, MONITOR_DEFAULTTONEAREST, MONITORINFO, MonitorFromWindow, + }; + use windows_sys::Win32::UI::WindowsAndMessaging::{ + GetWindowRect, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, SetWindowPos, + }; let hwnd = hwnd as *mut core::ffi::c_void; // SAFETY: both native output structures admit zero initialization. let mut rect: RECT = unsafe { std::mem::zeroed() }; @@ -220,22 +237,50 @@ pub(crate) fn place_normal_window(hwnd: isize, desired: (i32, i32)) -> bool { monitor.cbSize = std::mem::size_of::() as u32; // SAFETY: the caller owns the live HWND; the local outputs remain valid. if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 - || unsafe { GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &mut monitor) } == 0 { return false; } - let x = desired.0.clamp(monitor.rcWork.left, (monitor.rcWork.right - (rect.right - rect.left)).max(monitor.rcWork.left)); - let y = desired.1.clamp(monitor.rcWork.top, (monitor.rcWork.bottom - (rect.bottom - rect.top)).max(monitor.rcWork.top)); + || unsafe { + GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &mut monitor) + } == 0 + { + return false; + } + let x = desired.0.clamp( + monitor.rcWork.left, + (monitor.rcWork.right - (rect.right - rect.left)).max(monitor.rcWork.left), + ); + let y = desired.1.clamp( + monitor.rcWork.top, + (monitor.rcWork.bottom - (rect.bottom - rect.top)).max(monitor.rcWork.top), + ); // SAFETY: only the live window position changes; ownership and size are preserved. - unsafe { SetWindowPos(hwnd, std::ptr::null_mut(), x, y, 0, 0, SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOZORDER) != 0 } + unsafe { + SetWindowPos( + hwnd, + std::ptr::null_mut(), + x, + y, + 0, + 0, + SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOZORDER, + ) != 0 + } } #[cfg(feature = "gpui-shell")] -pub(crate) fn normal_position(origin: gpui::Point, scale: f32, display: Option) -> Option<(i32, i32)> { +pub(crate) fn normal_position( + origin: gpui::Point, + scale: f32, + display: Option, +) -> Option<(i32, i32)> { #[cfg(windows)] return display.map(|display| { let offset = placement_offset(display); desktop_position((origin.x.as_f32(), origin.y.as_f32()), scale, offset) }); #[cfg(not(windows))] - { let _ = (origin, scale, display); None } + { + let _ = (origin, scale, display); + None + } } #[cfg(feature = "gpui-shell")] @@ -248,7 +293,10 @@ pub(crate) fn place_configured_window(window: &gpui::Window, desired: (i32, i32) place_normal_window(handle.hwnd.get(), desired) } #[cfg(not(windows))] - { let _ = (window, desired); true } + { + let _ = (window, desired); + true + } } /// Convert the pinned Windows backend's client/workspace point to desktop pixels. @@ -257,7 +305,11 @@ pub(crate) fn desktop_position(origin: (f32, f32), scale: f32, offset: (i32, i32 } /// Convert desktop pixels to the client/workspace point accepted by GPUI placement. -pub(crate) fn workspace_position(position: (i32, i32), scale: f32, offset: (i32, i32)) -> (f32, f32) { +pub(crate) fn workspace_position( + position: (i32, i32), + scale: f32, + offset: (i32, i32), +) -> (f32, f32) { ((position.0 - offset.0) as f32 / scale, (position.1 - offset.1) as f32 / scale) } @@ -279,9 +331,14 @@ mod geometry_tests { #[cfg(all(test, windows, feature = "gpui-shell"))] mod native_geometry_tests { use windows_sys::Win32::Foundation::RECT; - use windows_sys::Win32::Graphics::Gdi::{GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTONEAREST}; + use windows_sys::Win32::Graphics::Gdi::{ + GetMonitorInfoW, MONITOR_DEFAULTTONEAREST, MONITORINFO, MonitorFromWindow, + }; use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW; - use windows_sys::Win32::UI::WindowsAndMessaging::{CreateWindowExW, DestroyWindow, GetClientRect, GetWindowRect, GetWindowPlacement, SetWindowPlacement, WINDOWPLACEMENT, WS_OVERLAPPEDWINDOW}; + use windows_sys::Win32::UI::WindowsAndMessaging::{ + CreateWindowExW, DestroyWindow, GetClientRect, GetWindowPlacement, GetWindowRect, + SetWindowPlacement, WINDOWPLACEMENT, WS_OVERLAPPEDWINDOW, + }; struct NativeWindow(isize); impl Drop for NativeWindow { @@ -294,7 +351,22 @@ mod native_geometry_tests { #[test] fn physical_outer_position_and_backend_normal_placement_roundtrip() { // SAFETY: STATIC is a registered class; no pointer escapes the native call. - let hwnd = unsafe { CreateWindowExW(0, windows_core::w!("STATIC").as_ptr(), windows_core::w!("Pebrel geometry test").as_ptr(), WS_OVERLAPPEDWINDOW, 0, 0, 320, 240, std::ptr::null_mut(), std::ptr::null_mut(), GetModuleHandleW(std::ptr::null()), std::ptr::null()) }; + let hwnd = unsafe { + CreateWindowExW( + 0, + windows_core::w!("STATIC").as_ptr(), + windows_core::w!("Pebrel geometry test").as_ptr(), + WS_OVERLAPPEDWINDOW, + 0, + 0, + 320, + 240, + std::ptr::null_mut(), + std::ptr::null_mut(), + GetModuleHandleW(std::ptr::null()), + std::ptr::null(), + ) + }; assert!(!hwnd.is_null()); let _window = NativeWindow(hwnd as isize); // SAFETY: plain native output structures admit zero initialization. @@ -311,19 +383,28 @@ mod native_geometry_tests { assert_ne!(unsafe { GetClientRect(hwnd, &mut client) }, 0); assert_eq!((rect.left, rect.top), desired); let before = (rect.left, rect.top, rect.right, rect.bottom); - let border = ((rect.right - rect.left - client.right) / 2, (rect.bottom - rect.top - client.bottom) / 2); + let border = ( + (rect.right - rect.left - client.right) / 2, + (rect.bottom - rect.top - client.bottom) / 2, + ); let mut placement: WINDOWPLACEMENT = unsafe { std::mem::zeroed() }; placement.length = std::mem::size_of::() as u32; assert_ne!(unsafe { GetWindowPlacement(hwnd, &mut placement) }, 0); let scale = super::display_scale(monitor as u64).unwrap(); let native = placement.rcNormalPosition; - let logical = ((native.left + border.0) as f32 / scale, (native.top + border.1) as f32 / scale); + let logical = + ((native.left + border.0) as f32 / scale, (native.top + border.1) as f32 / scale); let offset = super::placement_offset(monitor as u64); let saved = super::desktop_position(logical, scale, offset); let restored = super::workspace_position(saved, scale, offset); let x = (restored.0 * scale).round() as i32 - border.0; let y = (restored.1 * scale).round() as i32 - border.1; - placement.rcNormalPosition = RECT { left: x, top: y, right: x + native.right - native.left, bottom: y + native.bottom - native.top }; + placement.rcNormalPosition = RECT { + left: x, + top: y, + right: x + native.right - native.left, + bottom: y + native.bottom - native.top, + }; assert_ne!(unsafe { SetWindowPlacement(hwnd, &placement) }, 0); assert_ne!(unsafe { GetWindowRect(hwnd, &mut rect) }, 0); assert_eq!((rect.left, rect.top, rect.right, rect.bottom), before); diff --git a/scripts/platform_cfg_budget.txt b/scripts/platform_cfg_budget.txt index 10232896e..5873851d3 100644 --- a/scripts/platform_cfg_budget.txt +++ b/scripts/platform_cfg_budget.txt @@ -1 +1 @@ -468 +467 From 5e6d5035b2bad1d1016df0b7e7c80ba568d2400e Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:29:50 +0800 Subject: [PATCH 5/6] test: follow startup geometry ownership for application identity --- scripts/tests/test_pebrel_branding.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/tests/test_pebrel_branding.py b/scripts/tests/test_pebrel_branding.py index fd9332731..18ae9da52 100644 --- a/scripts/tests/test_pebrel_branding.py +++ b/scripts/tests/test_pebrel_branding.py @@ -34,8 +34,12 @@ def test_visible_identity_and_cli_use_pebrel(self): self.assertIn('DEFAULT_CLASS: &str = "Pebrel";', window) gpui = self.source("nebula_app/src/gpui_shell/workspace/windowing.rs") self.assertIn('window.set_window_title(crate::brand::NAME)', gpui) - self.assertIn('app_id: Some("pebrel".to_owned())', gpui) - self.assertIn('app_id: Some("pebrel-quick-terminal".to_owned())', gpui) + self.assertIn("startup_geometry::options(", gpui) + geometry = self.source( + "nebula_app/src/gpui_shell/workspace/windowing/startup_geometry.rs" + ) + self.assertIn('app_id: Some("pebrel".to_owned())', geometry) + self.assertIn('app_id: Some("pebrel-quick-terminal".to_owned())', geometry) welcome = self.source("nebula_app/src/window_context/welcome.rs") self.assertIn('crate::brand::NAME', welcome) self.assertNotIn('Nebula Terminal', welcome) From 82592450b491c4bfa5d1c55b4b561adeb2467503 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:22:28 +0800 Subject: [PATCH 6/6] ci: retrigger after Windows git metadata timeout