diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index d116bb2e62..94f7c7ba32 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -14,7 +14,7 @@ pub use collab_panel::CollabPanel; pub use collab_titlebar_item::CollabTitlebarItem; use gpui::{ actions, point, AppContext, DevicePixels, Pixels, PlatformDisplay, Size, Task, - WindowBackgroundAppearance, WindowContext, WindowKind, WindowOptions, + WindowBackgroundAppearance, WindowBounds, WindowContext, WindowKind, WindowOptions, }; use panel_settings::MessageEditorSettings; pub use panel_settings::{ @@ -117,14 +117,13 @@ fn notification_window_options( let app_id = ReleaseChannel::global(cx).app_id(); WindowOptions { - bounds: Some(bounds), + window_bounds: Some(WindowBounds::Windowed(bounds)), titlebar: None, focus: false, show: true, kind: WindowKind::PopUp, is_movable: false, display_id: Some(screen.id()), - fullscreen: false, window_background: WindowBackgroundAppearance::default(), app_id: Some(app_id.to_owned()), } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index be2f8d68fa..06c79bf556 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -9,7 +9,7 @@ use crate::{ JoinLines, }; use futures::StreamExt; -use gpui::{div, TestAppContext, VisualTestContext, WindowOptions}; +use gpui::{div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions}; use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, @@ -7493,10 +7493,10 @@ async fn test_following(cx: &mut gpui::TestAppContext) { let follower = cx.update(|cx| { cx.open_window( WindowOptions { - bounds: Some(Bounds::from_corners( + window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners( gpui::Point::new(0.into(), 0.into()), gpui::Point::new(10.into(), 80.into()), - )), + ))), ..Default::default() }, |cx| cx.new_view(|cx| build_editor(buffer.clone(), cx)), diff --git a/crates/gpui/examples/animation.rs b/crates/gpui/examples/animation.rs index b0e2c07df0..f3d8f56eda 100644 --- a/crates/gpui/examples/animation.rs +++ b/crates/gpui/examples/animation.rs @@ -63,7 +63,11 @@ fn main() { .with_assets(Assets {}) .run(|cx: &mut AppContext| { let options = WindowOptions { - bounds: Some(Bounds::centered(None, size(px(300.), px(300.)), cx)), + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(px(300.), px(300.)), + cx, + ))), ..Default::default() }; cx.open_window(options, |cx| { diff --git a/crates/gpui/examples/hello_world.rs b/crates/gpui/examples/hello_world.rs index 018c6097cf..6a91f6ab41 100644 --- a/crates/gpui/examples/hello_world.rs +++ b/crates/gpui/examples/hello_world.rs @@ -26,7 +26,7 @@ fn main() { let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx); cx.open_window( WindowOptions { - bounds: Some(bounds), + window_bounds: Some(WindowBounds::Windowed(bounds)), ..Default::default() }, |cx| { diff --git a/crates/gpui/examples/image/image.rs b/crates/gpui/examples/image/image.rs index 32bf205e83..e1e82d8d4a 100644 --- a/crates/gpui/examples/image/image.rs +++ b/crates/gpui/examples/image/image.rs @@ -79,10 +79,10 @@ fn main() { ..Default::default() }), - bounds: Some(Bounds { + window_bounds: Some(WindowBounds::Windowed(Bounds { size: size(px(1100.), px(600.)).into(), origin: Point::new(DevicePixels::from(200), DevicePixels::from(200)), - }), + })), ..Default::default() }; diff --git a/crates/gpui/examples/window_positioning.rs b/crates/gpui/examples/window_positioning.rs index aa70236e3c..da8742330b 100644 --- a/crates/gpui/examples/window_positioning.rs +++ b/crates/gpui/examples/window_positioning.rs @@ -43,7 +43,7 @@ fn main() { WindowOptions { // Set the bounds of the window in screen coordinates - bounds: Some(bounds), + window_bounds: Some(WindowBounds::Windowed(bounds)), // Specify the display_id to ensure the window is created on the correct screen display_id: Some(screen.id()), @@ -53,7 +53,6 @@ fn main() { show: true, kind: WindowKind::PopUp, is_movable: false, - fullscreen: false, app_id: None, } }; diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 0c6fb888e1..c66958c3c4 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -4,8 +4,8 @@ use crate::{ Element, Empty, Entity, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher, - TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext, WindowContext, - WindowHandle, WindowOptions, + TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext, WindowBounds, + WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{channel::oneshot, Stream, StreamExt}; @@ -188,7 +188,7 @@ impl TestAppContext { let bounds = Bounds::maximized(None, &mut cx); cx.open_window( WindowOptions { - bounds: Some(bounds), + window_bounds: Some(WindowBounds::Windowed(bounds)), ..Default::default() }, |cx| cx.new_view(build_window), @@ -201,7 +201,7 @@ impl TestAppContext { let bounds = Bounds::maximized(None, &mut cx); let window = cx.open_window( WindowOptions { - bounds: Some(bounds), + window_bounds: Some(WindowBounds::Windowed(bounds)), ..Default::default() }, |cx| cx.new_view(|_| Empty), @@ -224,7 +224,7 @@ impl TestAppContext { let bounds = Bounds::maximized(None, &mut cx); let window = cx.open_window( WindowOptions { - bounds: Some(bounds), + window_bounds: Some(WindowBounds::Windowed(bounds)), ..Default::default() }, |cx| cx.new_view(build_root_view), diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index cc4f23cff2..1312b66bbe 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -184,7 +184,7 @@ unsafe impl Send for DisplayId {} pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn bounds(&self) -> Bounds; fn is_maximized(&self) -> bool; - fn is_minimized(&self) -> bool; + fn window_bounds(&self) -> WindowBounds; fn content_size(&self) -> Size; fn scale_factor(&self) -> f32; fn appearance(&self) -> WindowAppearance; @@ -515,9 +515,10 @@ pub trait InputHandler: 'static { /// The variables that can be configured when creating a new window #[derive(Debug)] pub struct WindowOptions { - /// The bounds of the window in screen coordinates. - /// None -> inherit, Some(bounds) -> set bounds - pub bounds: Option>, + /// Specifies the state and bounds of the window in screen coordinates. + /// - `None`: Inherit the bounds. + /// - `Some(WindowBounds)`: Open a window with corresponding state and its restore size. + pub window_bounds: Option, /// The titlebar configuration of the window pub titlebar: Option, @@ -528,9 +529,6 @@ pub struct WindowOptions { /// Whether the window should be shown when created pub show: bool, - /// Whether the window should be fullscreen when created - pub fullscreen: bool, - /// The kind of window to create pub kind: WindowKind, @@ -571,10 +569,40 @@ pub(crate) struct WindowParams { pub window_background: WindowBackgroundAppearance, } +/// Represents the status of how a window should be opened. +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum WindowBounds { + /// Indicates that the window should open in a windowed state with the given bounds. + Windowed(Bounds), + /// Indicates that the window should open in a maximized state. + /// The bounds provided here represent the restore size of the window. + Maximized(Bounds), + /// Indicates that the window should open in fullscreen mode. + /// The bounds provided here represent the restore size of the window. + Fullscreen(Bounds), +} + +impl Default for WindowBounds { + fn default() -> Self { + WindowBounds::Windowed(Bounds::default()) + } +} + +impl WindowBounds { + /// Retrieve the inner bounds + pub fn get_bounds(&self) -> Bounds { + match self { + WindowBounds::Windowed(bounds) => *bounds, + WindowBounds::Maximized(bounds) => *bounds, + WindowBounds::Fullscreen(bounds) => *bounds, + } + } +} + impl Default for WindowOptions { fn default() -> Self { Self { - bounds: None, + window_bounds: None, titlebar: Some(TitlebarOptions { title: Default::default(), appears_transparent: Default::default(), @@ -585,7 +613,6 @@ impl Default for WindowOptions { kind: WindowKind::Normal, is_movable: true, display_id: None, - fullscreen: false, window_background: WindowBackgroundAppearance::default(), app_id: None, } diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 8924a322b5..58e4641d64 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -28,7 +28,7 @@ use crate::scene::Scene; use crate::{ px, size, Bounds, DevicePixels, Globals, Modifiers, Pixels, PlatformDisplay, PlatformInput, Point, PromptLevel, Size, WaylandClientState, WaylandClientStatePtr, WindowAppearance, - WindowBackgroundAppearance, WindowParams, + WindowBackgroundAppearance, WindowBounds, WindowParams, }; #[derive(Default)] @@ -79,6 +79,7 @@ pub struct WaylandWindowState { input_handler: Option, decoration_state: WaylandDecorationState, fullscreen: bool, + restore_bounds: Bounds, maximized: bool, client: WaylandClientStatePtr, callbacks: Callbacks, @@ -151,6 +152,7 @@ impl WaylandWindowState { input_handler: None, decoration_state: WaylandDecorationState::Client, fullscreen: false, + restore_bounds: Bounds::default(), maximized: false, callbacks: Callbacks::default(), client, @@ -332,10 +334,15 @@ impl WaylandWindowStatePtr { let height = NonZeroU32::new(height as u32); let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8)); let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8)); - self.resize(width, height); - self.set_fullscreen(fullscreen); let mut state = self.state.borrow_mut(); state.maximized = maximized; + state.fullscreen = fullscreen; + if fullscreen || maximized { + state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32)); + } + drop(state); + self.resize(width, height); + self.set_fullscreen(fullscreen); false } @@ -545,9 +552,17 @@ impl PlatformWindow for WaylandWindow { self.borrow().maximized } - fn is_minimized(&self) -> bool { - // This cannot be determined by the client - false + // todo(linux) + // check if it is right + fn window_bounds(&self) -> WindowBounds { + let state = self.borrow(); + if state.fullscreen { + WindowBounds::Fullscreen(state.restore_bounds) + } else if state.maximized { + WindowBounds::Maximized(state.restore_bounds) + } else { + WindowBounds::Windowed(state.bounds.map(|p| DevicePixels(p as i32))) + } } fn content_size(&self) -> Size { @@ -679,7 +694,8 @@ impl PlatformWindow for WaylandWindow { } fn toggle_fullscreen(&self) { - let state = self.borrow(); + let mut state = self.borrow_mut(); + state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32)); if !state.fullscreen { state.toplevel.set_fullscreen(None); } else { diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 1f23dcd959..77c050ac58 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -5,8 +5,8 @@ use crate::{ platform::blade::{BladeRenderer, BladeSurfaceConfig}, size, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels, Platform, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel, - Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowOptions, WindowParams, - X11Client, X11ClientState, X11ClientStatePtr, + Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions, + WindowParams, X11Client, X11ClientState, X11ClientStatePtr, }; use blade_graphics as gpu; use parking_lot::Mutex; @@ -526,8 +526,9 @@ impl PlatformWindow for X11Window { } // todo(linux) - fn is_minimized(&self) -> bool { - false + fn window_bounds(&self) -> WindowBounds { + let state = self.0.state.borrow(); + WindowBounds::Windowed(state.bounds.map(|p| DevicePixels(p))) } fn content_size(&self) -> Size { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 570eb9024a..09f5f3241a 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -4,7 +4,8 @@ use crate::{ DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel, - Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowKind, WindowParams, + Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowKind, + WindowParams, }; use block::ConcreteBlock; use cocoa::{ @@ -259,6 +260,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C sel!(windowDidChangeOcclusionState:), window_did_change_occlusion_state as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(windowWillEnterFullScreen:), + window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id), + ); decl.add_method( sel!(windowDidMove:), window_did_move as extern "C" fn(&Object, Sel, id), @@ -302,14 +307,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C sel!(concludeDragOperation:), conclude_drag_operation as extern "C" fn(&Object, Sel, id), ); - decl.add_method( - sel!(windowDidMiniaturize:), - window_did_miniaturize as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidDeminiaturize:), - window_did_deminiaturize as extern "C" fn(&Object, Sel, id), - ); decl.register() } @@ -348,7 +345,7 @@ struct MacWindowState { external_files_dragged: bool, // Whether the next left-mouse click is also the focusing click. first_mouse: bool, - minimized: bool, + fullscreen_restore_bounds: Bounds, } impl MacWindowState { @@ -435,10 +432,6 @@ impl MacWindowState { } } - fn is_minimized(&self) -> bool { - self.minimized - } - fn is_fullscreen(&self) -> bool { unsafe { let style_mask = self.native_window.styleMask(); @@ -494,6 +487,14 @@ impl MacWindowState { px((frame.size.height - content_layout_rect.size.height) as f32) } } + + fn window_bounds(&self) -> WindowBounds { + if self.is_fullscreen() { + WindowBounds::Fullscreen(self.fullscreen_restore_bounds) + } else { + WindowBounds::Windowed(self.bounds()) + } + } } unsafe impl Send for MacWindowState {} @@ -639,7 +640,7 @@ impl MacWindow { previous_keydown_inserted_text: None, external_files_dragged: false, first_mouse: false, - minimized: false, + fullscreen_restore_bounds: Bounds::default(), }))); (*native_window).set_ivar( @@ -775,12 +776,12 @@ impl PlatformWindow for MacWindow { self.0.as_ref().lock().bounds() } - fn is_maximized(&self) -> bool { - self.0.as_ref().lock().is_maximized() + fn window_bounds(&self) -> WindowBounds { + self.0.as_ref().lock().window_bounds() } - fn is_minimized(&self) -> bool { - self.0.as_ref().lock().is_minimized() + fn is_maximized(&self) -> bool { + self.0.as_ref().lock().is_maximized() } fn content_size(&self) -> Size { @@ -1466,6 +1467,12 @@ extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) { window_state.as_ref().lock().move_traffic_light(); } +extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + lock.fullscreen_restore_bounds = lock.bounds(); +} + extern "C" fn window_did_move(this: &Object, _: Sel, _: id) { let window_state = unsafe { get_window_state(this) }; let mut lock = window_state.as_ref().lock(); @@ -1863,18 +1870,6 @@ extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) { ); } -extern "C" fn window_did_miniaturize(this: &Object, _: Sel, _: id) { - let window_state = unsafe { get_window_state(this) }; - - window_state.lock().minimized = true; -} - -extern "C" fn window_did_deminiaturize(this: &Object, _: Sel, _: id) { - let window_state = unsafe { get_window_state(this) }; - - window_state.lock().minimized = false; -} - async fn synthetic_drag( window_state: Weak>, drag_id: usize, diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index bc9349e017..1b9654c561 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -2,7 +2,7 @@ use crate::{ AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DevicePixels, DispatchEventResult, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, Size, TestPlatform, TileId, WindowAppearance, - WindowBackgroundAppearance, WindowParams, + WindowBackgroundAppearance, WindowBounds, WindowParams, }; use collections::HashMap; use parking_lot::Mutex; @@ -112,11 +112,11 @@ impl PlatformWindow for TestWindow { self.0.lock().bounds } - fn is_maximized(&self) -> bool { - false + fn window_bounds(&self) -> WindowBounds { + WindowBounds::Windowed(self.bounds()) } - fn is_minimized(&self) -> bool { + fn is_maximized(&self) -> bool { false } diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index b16adf6126..753abe56d8 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -35,6 +35,7 @@ pub(crate) struct WindowsWindow(pub Rc); pub struct WindowsWindowState { pub origin: Point, pub physical_size: Size, + pub fullscreen_restore_bounds: Bounds, pub scale_factor: f32, pub callbacks: Callbacks, @@ -71,6 +72,10 @@ impl WindowsWindowState { ) -> Self { let origin = point(cs.x.into(), cs.y.into()); let physical_size = size(cs.cx.into(), cs.cy.into()); + let fullscreen_restore_bounds = Bounds { + origin, + size: physical_size, + }; let scale_factor = { let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32; monitor_dpi / USER_DEFAULT_SCREEN_DPI as f32 @@ -84,6 +89,7 @@ impl WindowsWindowState { Self { origin, physical_size, + fullscreen_restore_bounds, scale_factor, callbacks, input_handler, @@ -113,6 +119,35 @@ impl WindowsWindowState { } } + fn window_bounds(&self) -> WindowBounds { + let placement = unsafe { + let mut placement = WINDOWPLACEMENT { + length: std::mem::size_of::() as u32, + ..Default::default() + }; + GetWindowPlacement(self.hwnd, &mut placement).log_err(); + placement + }; + let bounds = Bounds { + origin: point( + DevicePixels(placement.rcNormalPosition.left), + DevicePixels(placement.rcNormalPosition.top), + ), + size: size( + DevicePixels(placement.rcNormalPosition.right - placement.rcNormalPosition.left), + DevicePixels(placement.rcNormalPosition.bottom - placement.rcNormalPosition.top), + ), + }; + + if self.is_fullscreen() { + WindowBounds::Fullscreen(self.fullscreen_restore_bounds) + } else if placement.showCmd == SW_SHOWMAXIMIZED.0 as u32 { + WindowBounds::Maximized(bounds) + } else { + WindowBounds::Windowed(bounds) + } + } + /// get the logical size of the app's drawable area. /// /// Currently, GPUI uses logical size of the app to handle mouse interactions (such as @@ -176,10 +211,6 @@ impl WindowsWindowStatePtr { main_receiver: context.main_receiver.clone(), }) } - - fn is_minimized(&self) -> bool { - unsafe { IsIconic(self.hwnd) }.as_bool() - } } #[derive(Default)] @@ -209,7 +240,7 @@ struct WindowCreateContext { impl WindowsWindow { pub(crate) fn new( handle: AnyWindowHandle, - options: WindowParams, + params: WindowParams, icon: HICON, executor: ForegroundExecutor, main_receiver: flume::Receiver, @@ -217,13 +248,13 @@ impl WindowsWindow { current_cursor: HCURSOR, ) -> Self { let classname = register_wnd_class(icon); - let hide_title_bar = options + let hide_title_bar = params .titlebar .as_ref() .map(|titlebar| titlebar.appears_transparent) .unwrap_or(false); let windowname = HSTRING::from( - options + params .titlebar .as_ref() .and_then(|titlebar| titlebar.title.as_ref()) @@ -231,12 +262,6 @@ impl WindowsWindow { .unwrap_or(""), ); let dwstyle = WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX; - let x = options.bounds.origin.x.0; - let y = options.bounds.origin.y.0; - let nwidth = options.bounds.size.width.0; - let nheight = options.bounds.size.height.0; - let hwndparent = HWND::default(); - let hmenu = HMENU::default(); let hinstance = get_module_handle(); let mut context = WindowCreateContext { inner: None, @@ -245,7 +270,7 @@ impl WindowsWindow { // todo(windows) move window to target monitor // options.display_id display: WindowsDisplay::primary_monitor().unwrap(), - transparent: options.window_background != WindowBackgroundAppearance::Opaque, + transparent: params.window_background != WindowBackgroundAppearance::Opaque, executor, main_receiver, mouse_wheel_settings, @@ -258,12 +283,12 @@ impl WindowsWindow { classname, &windowname, dwstyle, - x, - y, - nwidth, - nheight, - hwndparent, - hmenu, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + None, + None, hinstance, lpparam, ) @@ -272,6 +297,18 @@ impl WindowsWindow { register_drag_drop(state_ptr.clone()); let wnd = Self(state_ptr); + unsafe { + let mut placement = WINDOWPLACEMENT { + length: std::mem::size_of::() as u32, + ..Default::default() + }; + GetWindowPlacement(raw_hwnd, &mut placement).log_err(); + placement.rcNormalPosition.left = params.bounds.left().0; + placement.rcNormalPosition.right = params.bounds.right().0; + placement.rcNormalPosition.top = params.bounds.top().0; + placement.rcNormalPosition.bottom = params.bounds.bottom().0; + SetWindowPlacement(raw_hwnd, &placement).log_err(); + } unsafe { ShowWindow(raw_hwnd, SW_SHOW) }; wnd @@ -321,8 +358,8 @@ impl PlatformWindow for WindowsWindow { self.0.state.borrow().is_maximized() } - fn is_minimized(&self) -> bool { - self.0.is_minimized() + fn window_bounds(&self) -> WindowBounds { + self.0.state.borrow().window_bounds() } /// get the logical size of the app's drawable area. @@ -493,6 +530,10 @@ impl PlatformWindow for WindowsWindow { .executor .spawn(async move { let mut lock = state_ptr.state.borrow_mut(); + lock.fullscreen_restore_bounds = Bounds { + origin: lock.origin, + size: lock.physical_size, + }; let StyleAndBounds { style, x, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index d7e9863cfd..f7063d17c3 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -12,8 +12,8 @@ use crate::{ RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, View, VisualContext, WeakView, - WindowAppearance, WindowBackgroundAppearance, WindowOptions, WindowParams, WindowTextSystem, - SUBPIXEL_VARIANTS, + WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions, WindowParams, + WindowTextSystem, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; use collections::{FxHashMap, FxHashSet}; @@ -565,7 +565,7 @@ fn default_bounds(display_id: Option, cx: &mut AppContext) -> Bounds< const DEFAULT_WINDOW_OFFSET: Point = point(DevicePixels(0), DevicePixels(35)); cx.active_window() - .and_then(|w| w.update(cx, |_, cx| cx.window_bounds()).ok()) + .and_then(|w| w.update(cx, |_, cx| cx.bounds()).ok()) .map(|bounds| bounds.map_origin(|origin| origin + DEFAULT_WINDOW_OFFSET)) .unwrap_or_else(|| { let display = display_id @@ -592,19 +592,20 @@ impl Window { cx: &mut AppContext, ) -> Self { let WindowOptions { - bounds, + window_bounds, titlebar, focus, show, kind, is_movable, display_id, - fullscreen, window_background, app_id, } = options; - let bounds = bounds.unwrap_or_else(|| default_bounds(display_id, cx)); + let bounds = window_bounds + .map(|bounds| bounds.get_bounds()) + .unwrap_or_else(|| default_bounds(display_id, cx)); let mut platform_window = cx.platform.open_window( handle, WindowParams { @@ -632,8 +633,12 @@ impl Window { let next_frame_callbacks: Rc>> = Default::default(); let last_input_timestamp = Rc::new(Cell::new(Instant::now())); - if fullscreen { - platform_window.toggle_fullscreen(); + if let Some(ref window_open_state) = window_bounds { + match window_open_state { + WindowBounds::Fullscreen(_) => platform_window.toggle_fullscreen(), + WindowBounds::Maximized(_) => platform_window.zoom(), + WindowBounds::Windowed(_) => {} + } } platform_window.on_close(Box::new({ @@ -691,7 +696,7 @@ impl Window { let mut cx = cx.to_async(); move |_, _| { handle - .update(&mut cx, |_, cx| cx.window_bounds_changed()) + .update(&mut cx, |_, cx| cx.bounds_changed()) .log_err(); } })); @@ -699,7 +704,7 @@ impl Window { let mut cx = cx.to_async(); move || { handle - .update(&mut cx, |_, cx| cx.window_bounds_changed()) + .update(&mut cx, |_, cx| cx.bounds_changed()) .log_err(); } })); @@ -943,10 +948,10 @@ impl<'a> WindowContext<'a> { self.window.platform_window.is_maximized() } - /// Check if the platform window is minimized - /// On some platforms (namely Windows) the position is incorrect when minimized - pub fn is_minimized(&self) -> bool { - self.window.platform_window.is_minimized() + /// Return the `WindowBounds` to indicate that how a window should be opened + /// after it has been closed + pub fn window_bounds(&self) -> WindowBounds { + self.window.platform_window.window_bounds() } /// Dispatch the given action on the currently focused element. @@ -1075,7 +1080,7 @@ impl<'a> WindowContext<'a> { .spawn(|app| f(AsyncWindowContext::new(app, self.window.handle))) } - fn window_bounds_changed(&mut self) { + fn bounds_changed(&mut self) { self.window.scale_factor = self.window.platform_window.scale_factor(); self.window.viewport_size = self.window.platform_window.content_size(); self.window.display_id = self.window.platform_window.display().id(); @@ -1088,7 +1093,7 @@ impl<'a> WindowContext<'a> { } /// Returns the bounds of the current window in the global coordinate space, which could span across multiple displays. - pub fn window_bounds(&self) -> Bounds { + pub fn bounds(&self) -> Bounds { self.window.platform_window.bounds() } diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index 70853267ca..cb3498c179 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -7,7 +7,8 @@ mod story_selector; use clap::Parser; use dialoguer::FuzzySelect; use gpui::{ - div, px, size, AnyView, AppContext, Bounds, Render, ViewContext, VisualContext, WindowOptions, + div, px, size, AnyView, AppContext, Bounds, Render, ViewContext, VisualContext, WindowBounds, + WindowOptions, }; use log::LevelFilter; use project::Project; @@ -85,7 +86,7 @@ fn main() { let bounds = Bounds::centered(None, size, cx); let _window = cx.open_window( WindowOptions { - bounds: Some(bounds), + window_bounds: Some(WindowBounds::Windowed(bounds)), ..Default::default() }, move |cx| { diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 04639b5011..e977166b00 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -5,7 +5,7 @@ use std::path::Path; use anyhow::{anyhow, bail, Context, Result}; use client::DevServerProjectId; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; -use gpui::{point, size, Axis, Bounds}; +use gpui::{point, size, Axis, Bounds, WindowBounds}; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, @@ -59,50 +59,99 @@ impl sqlez::bindable::Column for SerializedAxis { } } -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct SerializedWindowsBounds(pub(crate) Bounds); +#[derive(Copy, Clone, Debug, PartialEq, Default)] +pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds); -impl StaticColumnCount for SerializedWindowsBounds { +impl StaticColumnCount for SerializedWindowBounds { fn column_count() -> usize { 5 } } -impl Bind for SerializedWindowsBounds { +impl Bind for SerializedWindowBounds { fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = statement.bind(&"Fixed", start_index)?; - - statement.bind( - &( - SerializedDevicePixels(self.0.origin.x), - SerializedDevicePixels(self.0.origin.y), - SerializedDevicePixels(self.0.size.width), - SerializedDevicePixels(self.0.size.height), - ), - next_index, - ) + match self.0 { + WindowBounds::Windowed(bounds) => { + let next_index = statement.bind(&"Windowed", start_index)?; + statement.bind( + &( + SerializedDevicePixels(bounds.origin.x), + SerializedDevicePixels(bounds.origin.y), + SerializedDevicePixels(bounds.size.width), + SerializedDevicePixels(bounds.size.height), + ), + next_index, + ) + } + WindowBounds::Maximized(bounds) => { + let next_index = statement.bind(&"Maximized", start_index)?; + statement.bind( + &( + SerializedDevicePixels(bounds.origin.x), + SerializedDevicePixels(bounds.origin.y), + SerializedDevicePixels(bounds.size.width), + SerializedDevicePixels(bounds.size.height), + ), + next_index, + ) + } + WindowBounds::Fullscreen(bounds) => { + let next_index = statement.bind(&"FullScreen", start_index)?; + statement.bind( + &( + SerializedDevicePixels(bounds.origin.x), + SerializedDevicePixels(bounds.origin.y), + SerializedDevicePixels(bounds.size.width), + SerializedDevicePixels(bounds.size.height), + ), + next_index, + ) + } + } } } -impl Column for SerializedWindowsBounds { +impl Column for SerializedWindowBounds { fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { let (window_state, next_index) = String::column(statement, start_index)?; - let bounds = match window_state.as_str() { - "Fixed" => { + let status = match window_state.as_str() { + "Windowed" | "Fixed" => { let ((x, y, width, height), _) = Column::column(statement, next_index)?; let x: i32 = x; let y: i32 = y; let width: i32 = width; let height: i32 = height; - SerializedWindowsBounds(Bounds { + SerializedWindowBounds(WindowBounds::Windowed(Bounds { origin: point(x.into(), y.into()), size: size(width.into(), height.into()), - }) + })) + } + "Maximized" => { + let ((x, y, width, height), _) = Column::column(statement, next_index)?; + let x: i32 = x; + let y: i32 = y; + let width: i32 = width; + let height: i32 = height; + SerializedWindowBounds(WindowBounds::Maximized(Bounds { + origin: point(x.into(), y.into()), + size: size(width.into(), height.into()), + })) + } + "FullScreen" => { + let ((x, y, width, height), _) = Column::column(statement, next_index)?; + let x: i32 = x; + let y: i32 = y; + let width: i32 = width; + let height: i32 = height; + SerializedWindowBounds(WindowBounds::Fullscreen(Bounds { + origin: point(x.into(), y.into()), + size: size(width.into(), height.into()), + })) } _ => bail!("Window State did not have a valid string"), }; - Ok((bounds, next_index + 4)) + Ok((status, next_index + 4)) } } @@ -279,6 +328,8 @@ define_connection! { ALTER TABLE pane_groups ADD COLUMN flexes TEXT; ), // Add fullscreen field to workspace + // Deprecated, `WindowBounds` holds the fullscreen state now. + // Preserving so users can downgrade Zed. sql!( ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool ), @@ -328,19 +379,17 @@ impl WorkspaceDb { workspace_id, local_paths, dev_server_project_id, - bounds, + window_bounds, display, - fullscreen, centered_layout, docks, ): ( WorkspaceId, Option, Option, - Option, + Option, Option, Option, - Option, DockStructure, ) = self .select_row_bound(sql! { @@ -354,7 +403,6 @@ impl WorkspaceDb { window_width, window_height, display, - fullscreen, centered_layout, left_dock_visible, left_dock_active_panel, @@ -398,8 +446,7 @@ impl WorkspaceDb { .get_center_pane_group(workspace_id) .context("Getting center group") .log_err()?, - bounds: bounds.map(|bounds| bounds.0), - fullscreen: fullscreen.unwrap_or(false), + window_bounds, centered_layout: centered_layout.unwrap_or(false), display, docks, @@ -549,13 +596,12 @@ impl WorkspaceDb { pub(crate) fn last_window( &self, - ) -> anyhow::Result<(Option, Option, Option)> { + ) -> anyhow::Result<(Option, Option)> { let mut prepared_query = - self.select::<(Option, Option, Option)>(sql!( + self.select::<(Option, Option)>(sql!( SELECT display, - window_state, window_x, window_y, window_width, window_height, - fullscreen + window_state, window_x, window_y, window_width, window_height FROM workspaces WHERE local_paths IS NOT NULL @@ -563,10 +609,7 @@ impl WorkspaceDb { LIMIT 1 ))?; let result = prepared_query()?; - Ok(result - .into_iter() - .next() - .unwrap_or_else(|| (None, None, None))) + Ok(result.into_iter().next().unwrap_or_else(|| (None, None))) } query! { @@ -829,7 +872,7 @@ impl WorkspaceDb { } query! { - pub(crate) async fn set_window_bounds(workspace_id: WorkspaceId, bounds: SerializedWindowsBounds, display: Uuid) -> Result<()> { + pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> { UPDATE workspaces SET window_state = ?2, window_x = ?3, @@ -841,14 +884,6 @@ impl WorkspaceDb { } } - query! { - pub(crate) async fn set_fullscreen(workspace_id: WorkspaceId, fullscreen: bool) -> Result<()> { - UPDATE workspaces - SET fullscreen = ?2 - WHERE workspace_id = ?1 - } - } - query! { pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> { UPDATE workspaces @@ -938,10 +973,9 @@ mod tests { id: WorkspaceId(1), location: LocalPaths::new(["/tmp", "/tmp2"]).into(), center_group: Default::default(), - bounds: Default::default(), + window_bounds: Default::default(), display: Default::default(), docks: Default::default(), - fullscreen: false, centered_layout: false, }; @@ -949,10 +983,9 @@ mod tests { id: WorkspaceId(2), location: LocalPaths::new(["/tmp"]).into(), center_group: Default::default(), - bounds: Default::default(), + window_bounds: Default::default(), display: Default::default(), docks: Default::default(), - fullscreen: false, centered_layout: false, }; @@ -1049,10 +1082,9 @@ mod tests { id: WorkspaceId(5), location: LocalPaths::new(["/tmp", "/tmp2"]).into(), center_group, - bounds: Default::default(), + window_bounds: Default::default(), display: Default::default(), docks: Default::default(), - fullscreen: false, centered_layout: false, }; @@ -1079,10 +1111,9 @@ mod tests { id: WorkspaceId(1), location: LocalPaths::new(["/tmp", "/tmp2"]).into(), center_group: Default::default(), - bounds: Default::default(), + window_bounds: Default::default(), display: Default::default(), docks: Default::default(), - fullscreen: false, centered_layout: false, }; @@ -1090,10 +1121,9 @@ mod tests { id: WorkspaceId(2), location: LocalPaths::new(["/tmp"]).into(), center_group: Default::default(), - bounds: Default::default(), + window_bounds: Default::default(), display: Default::default(), docks: Default::default(), - fullscreen: false, centered_layout: false, }; @@ -1128,10 +1158,9 @@ mod tests { id: WorkspaceId(3), location: LocalPaths::new(&["/tmp", "/tmp2"]).into(), center_group: Default::default(), - bounds: Default::default(), + window_bounds: Default::default(), display: Default::default(), docks: Default::default(), - fullscreen: false, centered_layout: false, }; @@ -1163,10 +1192,9 @@ mod tests { id: WorkspaceId(4), location: LocalPaths::new(workspace_id).into(), center_group: center_group.clone(), - bounds: Default::default(), + window_bounds: Default::default(), display: Default::default(), docks: Default::default(), - fullscreen: false, centered_layout: false, } } diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index aef9e04e5d..08fe507afc 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -1,4 +1,4 @@ -use super::SerializedAxis; +use super::{SerializedAxis, SerializedWindowBounds}; use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId}; use anyhow::{Context, Result}; use async_recursion::async_recursion; @@ -7,7 +7,7 @@ use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; -use gpui::{AsyncWindowContext, Bounds, DevicePixels, Model, Task, View, WeakView}; +use gpui::{AsyncWindowContext, Model, Task, View, WeakView}; use project::Project; use serde::{Deserialize, Serialize}; use std::{ @@ -110,8 +110,7 @@ pub(crate) struct SerializedWorkspace { pub(crate) id: WorkspaceId, pub(crate) location: SerializedWorkspaceLocation, pub(crate) center_group: SerializedPaneGroup, - pub(crate) bounds: Option>, - pub(crate) fullscreen: bool, + pub(crate) window_bounds: Option, pub(crate) centered_layout: bool, pub(crate) display: Option, pub(crate) docks: DockStructure, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 098ac3d28b..20f88ab0ef 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -32,7 +32,7 @@ use gpui::{ ElementId, Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global, GlobalElementId, KeyContext, Keystroke, LayoutId, ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel, Render, Size, Subscription, Task, View, WeakView, - WindowHandle, WindowOptions, + WindowBounds, WindowHandle, WindowOptions, }; use item::{ FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings, @@ -46,7 +46,7 @@ use node_runtime::NodeRuntime; use notifications::{simple_message_notification::MessageNotification, NotificationHandle}; pub use pane::*; pub use pane_group::*; -use persistence::{model::SerializedWorkspace, SerializedWindowsBounds, DB}; +use persistence::{model::SerializedWorkspace, SerializedWindowBounds, DB}; pub use persistence::{ model::{ItemId, LocalPaths, SerializedDevServerProject, SerializedWorkspaceLocation}, WorkspaceDb, DB as WORKSPACE_DB, @@ -785,29 +785,15 @@ impl Workspace { .await; this.update(&mut cx, |this, cx| { if let Some(display) = cx.display() { - let window_bounds = cx.window_bounds(); - let fullscreen = cx.is_fullscreen(); - if let Some(display_uuid) = display.uuid().log_err() { - // Only update the window bounds when not full screen, - // so we can remember the last non-fullscreen bounds - // across restarts - if fullscreen { - cx.background_executor() - .spawn(DB.set_fullscreen(workspace_id, true)) - .detach_and_log_err(cx); - } else if !cx.is_minimized() { - cx.background_executor() - .spawn(DB.set_fullscreen(workspace_id, false)) - .detach_and_log_err(cx); - cx.background_executor() - .spawn(DB.set_window_bounds( - workspace_id, - SerializedWindowsBounds(window_bounds), - display_uuid, - )) - .detach_and_log_err(cx); - } + let window_bounds = cx.window_bounds(); + cx.background_executor() + .spawn(DB.set_window_open_status( + workspace_id, + SerializedWindowBounds(window_bounds), + display_uuid, + )) + .detach_and_log_err(cx); } } this.bounds_save_task_queued.take(); @@ -947,30 +933,27 @@ impl Workspace { } else { let window_bounds_override = window_bounds_env_override(); - let (bounds, display, fullscreen) = if let Some(bounds) = window_bounds_override { - (Some(bounds), None, false) + let (window_bounds, display) = if let Some(bounds) = window_bounds_override { + (Some(WindowBounds::Windowed(bounds)), None) } else { let restorable_bounds = serialized_workspace .as_ref() - .and_then(|workspace| { - Some((workspace.display?, workspace.bounds?, workspace.fullscreen)) - }) + .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?))) .or_else(|| { - let (display, bounds, fullscreen) = DB.last_window().log_err()?; - Some((display?, bounds?.0, fullscreen.unwrap_or(false))) + let (display, window_bounds) = DB.last_window().log_err()?; + Some((display?, window_bounds?)) }); - if let Some((serialized_display, bounds, fullscreen)) = restorable_bounds { - (Some(bounds), Some(serialized_display), fullscreen) + if let Some((serialized_display, serialized_status)) = restorable_bounds { + (Some(serialized_status.0), Some(serialized_display)) } else { - (None, None, false) + (None, None) } }; // Use the serialized workspace to construct the new window let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?; - options.bounds = bounds; - options.fullscreen = fullscreen; + options.window_bounds = window_bounds; let centered_layout = serialized_workspace .as_ref() .map(|w| w.centered_layout) @@ -3667,14 +3650,14 @@ impl Workspace { if let Some(location) = location { let center_group = build_serialized_pane_group(&self.center.root, cx); let docks = build_serialized_docks(self, cx); + let window_bounds = Some(SerializedWindowBounds(cx.window_bounds())); let serialized_workspace = SerializedWorkspace { id: self.database_id, location, center_group, - bounds: Default::default(), + window_bounds, display: Default::default(), docks, - fullscreen: cx.is_fullscreen(), centered_layout: self.centered_layout, }; return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace)); @@ -4867,7 +4850,8 @@ pub fn join_hosted_project( let window_bounds_override = window_bounds_env_override(); cx.update(|cx| { let mut options = (app_state.build_window_options)(None, cx); - options.bounds = window_bounds_override; + options.window_bounds = + window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds)); cx.open_window(options, |cx| { cx.new_view(|cx| { Workspace::new(Default::default(), project, app_state.clone(), cx) @@ -4931,7 +4915,8 @@ pub fn join_dev_server_project( let window_bounds_override = window_bounds_env_override(); cx.update(|cx| { let mut options = (app_state.build_window_options)(None, cx); - options.bounds = window_bounds_override; + options.window_bounds = + window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds)); cx.open_window(options, |cx| { cx.new_view(|cx| { Workspace::new(Default::default(), project, app_state.clone(), cx) @@ -4993,7 +4978,8 @@ pub fn join_in_room_project( let window_bounds_override = window_bounds_env_override(); cx.update(|cx| { let mut options = (app_state.build_window_options)(None, cx); - options.bounds = window_bounds_override; + options.window_bounds = + window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds)); cx.open_window(options, |cx| { cx.new_view(|cx| { Workspace::new(Default::default(), project, app_state.clone(), cx) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 05eae13153..63bf427b5a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -96,13 +96,12 @@ pub fn build_window_options(display_uuid: Option, cx: &mut AppContext) -> appears_transparent: true, traffic_light_position: Some(point(px(9.0), px(9.0))), }), - bounds: None, + window_bounds: None, focus: false, show: false, kind: WindowKind::Normal, is_movable: true, display_id: display.map(|display| display.id()), - fullscreen: false, window_background: cx.theme().window_background_appearance(), app_id: Some(app_id.to_owned()), }