wayland: Fix window bounds restoration (#12581)

apricotbucket28 created

Fixes multiple issues that prevented window bounds restoration to not
work on Wayland.

Note: Since the display uuid depends on the `wl_output.name` field, this
only works properly on KDE 5.26+ or Gnome 44+ ([kwin
commit](https://github.com/KDE/kwin/commit/330a02d862329893957532f46655c52c755f3731),
[mutter](https://github.com/GNOME/mutter/commit/7e838b1115f195ba4c7b06169ade3407d29c66d0)).

Release Notes:

- N/A

Change summary

crates/gpui/src/geometry.rs                       |  2 
crates/gpui/src/platform/linux/wayland/client.rs  | 47 +++++---
crates/gpui/src/platform/linux/wayland/display.rs |  7 +
crates/gpui/src/platform/linux/wayland/window.rs  | 88 ++++++++--------
4 files changed, 82 insertions(+), 62 deletions(-)

Detailed changes

crates/gpui/src/geometry.rs 🔗

@@ -712,7 +712,7 @@ impl Size<Length> {
 /// assert_eq!(bounds.origin, origin);
 /// assert_eq!(bounds.size, size);
 /// ```
-#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
+#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Hash)]
 #[refineable(Debug)]
 #[repr(C)]
 pub struct Bounds<T: Clone + Default + Debug> {

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -139,8 +139,9 @@ impl Globals {
     }
 }
 
-#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
 pub struct InProgressOutput {
+    name: Option<String>,
     scale: Option<i32>,
     position: Option<Point<DevicePixels>>,
     size: Option<Size<DevicePixels>>,
@@ -151,6 +152,7 @@ impl InProgressOutput {
         if let Some((position, size)) = self.position.zip(self.size) {
             let scale = self.scale.unwrap_or(1);
             Some(Output {
+                name: self.name.clone(),
                 scale,
                 bounds: Bounds::new(position, size),
             })
@@ -160,22 +162,13 @@ impl InProgressOutput {
     }
 }
 
-#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
 pub struct Output {
+    pub name: Option<String>,
     pub scale: i32,
     pub bounds: Bounds<DevicePixels>,
 }
 
-impl Hash for Output {
-    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-        state.write_i32(self.scale);
-        state.write_i32(self.bounds.origin.x.0);
-        state.write_i32(self.bounds.origin.y.0);
-        state.write_i32(self.bounds.size.width.0);
-        state.write_i32(self.bounds.size.height.0);
-    }
-}
-
 pub(crate) struct WaylandClientState {
     serial_tracker: SerialTracker,
     globals: Globals,
@@ -337,7 +330,6 @@ impl Drop for WaylandClient {
 }
 
 const WL_DATA_DEVICE_MANAGER_VERSION: u32 = 3;
-const WL_OUTPUT_VERSION: u32 = 2;
 
 fn wl_seat_version(version: u32) -> u32 {
     // We rely on the wl_pointer.frame event
@@ -354,6 +346,20 @@ fn wl_seat_version(version: u32) -> u32 {
     version.clamp(WL_SEAT_MIN_VERSION, WL_SEAT_MAX_VERSION)
 }
 
+fn wl_output_version(version: u32) -> u32 {
+    const WL_OUTPUT_MIN_VERSION: u32 = 2;
+    const WL_OUTPUT_MAX_VERSION: u32 = 4;
+
+    if version < WL_OUTPUT_MIN_VERSION {
+        panic!(
+            "wl_output below required version: {} < {}",
+            version, WL_OUTPUT_MIN_VERSION
+        );
+    }
+
+    version.clamp(WL_OUTPUT_MIN_VERSION, WL_OUTPUT_MAX_VERSION)
+}
+
 impl WaylandClient {
     pub(crate) fn new() -> Self {
         let conn = Connection::connect_to_env().unwrap();
@@ -378,7 +384,7 @@ impl WaylandClient {
                     "wl_output" => {
                         let output = globals.registry().bind::<wl_output::WlOutput, _, _>(
                             global.name,
-                            WL_OUTPUT_VERSION,
+                            wl_output_version(global.version),
                             &qh,
                             (),
                         );
@@ -517,6 +523,7 @@ impl LinuxClient for WaylandClient {
             .map(|(id, output)| {
                 Rc::new(WaylandDisplay {
                     id: id.clone(),
+                    name: output.name.clone(),
                     bounds: output.bounds,
                 }) as Rc<dyn PlatformDisplay>
             })
@@ -532,6 +539,7 @@ impl LinuxClient for WaylandClient {
                 (object_id.protocol_id() == id.0).then(|| {
                     Rc::new(WaylandDisplay {
                         id: object_id.clone(),
+                        name: output.name.clone(),
                         bounds: output.bounds,
                     }) as Rc<dyn PlatformDisplay>
                 })
@@ -716,8 +724,12 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
                     );
                 }
                 "wl_output" => {
-                    let output =
-                        registry.bind::<wl_output::WlOutput, _, _>(name, WL_OUTPUT_VERSION, qh, ());
+                    let output = registry.bind::<wl_output::WlOutput, _, _>(
+                        name,
+                        wl_output_version(version),
+                        qh,
+                        (),
+                    );
 
                     state
                         .in_progress_outputs
@@ -821,6 +833,9 @@ impl Dispatch<wl_output::WlOutput, ()> for WaylandClientStatePtr {
         };
 
         match event {
+            wl_output::Event::Name { name } => {
+                in_progress_output.name = Some(name);
+            }
             wl_output::Event::Scale { factor } => {
                 in_progress_output.scale = Some(factor);
             }

crates/gpui/src/platform/linux/wayland/display.rs 🔗

@@ -12,6 +12,7 @@ use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay};
 pub(crate) struct WaylandDisplay {
     /// The ID of the wl_output object
     pub id: ObjectId,
+    pub name: Option<String>,
     pub bounds: Bounds<DevicePixels>,
 }
 
@@ -27,7 +28,11 @@ impl PlatformDisplay for WaylandDisplay {
     }
 
     fn uuid(&self) -> anyhow::Result<Uuid> {
-        Err(anyhow::anyhow!("Display UUID is not supported on Wayland"))
+        if let Some(name) = &self.name {
+            Ok(Uuid::new_v5(&Uuid::NAMESPACE_DNS, name.as_bytes()))
+        } else {
+            Err(anyhow::anyhow!("Wayland display does not have a name"))
+        }
     }
 
     fn bounds(&self) -> Bounds<DevicePixels> {

crates/gpui/src/platform/linux/wayland/window.rs 🔗

@@ -81,8 +81,8 @@ pub struct WaylandWindowState {
     input_handler: Option<PlatformInputHandler>,
     decoration_state: WaylandDecorationState,
     fullscreen: bool,
-    restore_bounds: Bounds<DevicePixels>,
     maximized: bool,
+    windowed_bounds: Bounds<DevicePixels>,
     client: WaylandClientStatePtr,
     handle: AnyWindowHandle,
     active: bool,
@@ -158,8 +158,8 @@ impl WaylandWindowState {
             input_handler: None,
             decoration_state: WaylandDecorationState::Client,
             fullscreen: false,
-            restore_bounds: Bounds::default(),
             maximized: false,
+            windowed_bounds: options.bounds,
             client,
             appearance,
             handle,
@@ -230,6 +230,7 @@ impl WaylandWindow {
             .wm_base
             .get_xdg_surface(&surface, &globals.qh, surface.id());
         let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
+        toplevel.set_min_size(200, 200);
 
         if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
             fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
@@ -348,23 +349,32 @@ impl WaylandWindowStatePtr {
     pub fn handle_toplevel_event(&self, event: xdg_toplevel::Event) -> bool {
         match event {
             xdg_toplevel::Event::Configure {
-                width,
-                height,
+                mut width,
+                mut height,
                 states,
             } => {
-                let width = NonZeroU32::new(width as u32);
-                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));
+
                 let mut state = self.state.borrow_mut();
-                state.maximized = maximized;
+                let got_unmaximized = state.maximized && !maximized;
                 state.fullscreen = fullscreen;
-                if fullscreen || maximized {
-                    state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32));
+                state.maximized = maximized;
+
+                if got_unmaximized {
+                    width = state.windowed_bounds.size.width.0;
+                    height = state.windowed_bounds.size.height.0;
+                } else if width != 0 && height != 0 && !fullscreen && !maximized {
+                    state.windowed_bounds = Bounds {
+                        origin: Point::default(),
+                        size: size(width.into(), height.into()),
+                    };
                 }
+
+                let width = NonZeroU32::new(width as u32);
+                let height = NonZeroU32::new(height as u32);
                 drop(state);
                 self.resize(width, height);
-                self.set_fullscreen(fullscreen);
 
                 false
             }
@@ -393,49 +403,44 @@ impl WaylandWindowStatePtr {
     ) {
         let mut state = self.state.borrow_mut();
 
-        // We use `WpFractionalScale` instead to set the scale if it's available
-        if state.globals.fractional_scale_manager.is_some() {
-            return;
-        }
-
         match event {
             wl_surface::Event::Enter { output } => {
-                // We use `PreferredBufferScale` instead to set the scale if it's available
-                if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
-                    return;
-                }
                 let id = output.id();
 
                 let Some(output) = outputs.get(&id) else {
                     return;
                 };
 
-                state.outputs.insert(id, *output);
+                state.outputs.insert(id, output.clone());
 
                 let scale = primary_output_scale(&mut state);
 
-                state.surface.set_buffer_scale(scale);
-                drop(state);
-                self.rescale(scale as f32);
-            }
-            wl_surface::Event::Leave { output } => {
                 // We use `PreferredBufferScale` instead to set the scale if it's available
-                if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
-                    return;
+                if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
+                    state.surface.set_buffer_scale(scale);
+                    drop(state);
+                    self.rescale(scale as f32);
                 }
-
+            }
+            wl_surface::Event::Leave { output } => {
                 state.outputs.remove(&output.id());
 
                 let scale = primary_output_scale(&mut state);
 
-                state.surface.set_buffer_scale(scale);
-                drop(state);
-                self.rescale(scale as f32);
+                // We use `PreferredBufferScale` instead to set the scale if it's available
+                if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
+                    state.surface.set_buffer_scale(scale);
+                    drop(state);
+                    self.rescale(scale as f32);
+                }
             }
             wl_surface::Event::PreferredBufferScale { factor } => {
-                state.surface.set_buffer_scale(factor);
-                drop(state);
-                self.rescale(factor as f32);
+                // We use `WpFractionalScale` instead to set the scale if it's available
+                if state.globals.fractional_scale_manager.is_none() {
+                    state.surface.set_buffer_scale(factor);
+                    drop(state);
+                    self.rescale(factor as f32);
+                }
             }
             _ => {}
         }
@@ -537,11 +542,6 @@ impl WaylandWindowStatePtr {
         self.set_size_and_scale(None, None, Some(scale));
     }
 
-    pub fn set_fullscreen(&self, fullscreen: bool) {
-        let mut state = self.state.borrow_mut();
-        state.fullscreen = fullscreen;
-    }
-
     /// Notifies the window of the state of the decorations.
     ///
     /// # Note
@@ -602,10 +602,10 @@ fn primary_output_scale(state: &mut RefMut<WaylandWindowState>) -> i32 {
     for (id, output) in state.outputs.iter() {
         if let Some((_, output_data)) = &current_output {
             if output.scale > output_data.scale {
-                current_output = Some((id.clone(), *output));
+                current_output = Some((id.clone(), output.clone()));
             }
         } else {
-            current_output = Some((id.clone(), *output));
+            current_output = Some((id.clone(), output.clone()));
         }
         scale = scale.max(output.scale);
     }
@@ -636,9 +636,9 @@ impl PlatformWindow for WaylandWindow {
     fn window_bounds(&self) -> WindowBounds {
         let state = self.borrow();
         if state.fullscreen {
-            WindowBounds::Fullscreen(state.restore_bounds)
+            WindowBounds::Fullscreen(state.windowed_bounds)
         } else if state.maximized {
-            WindowBounds::Maximized(state.restore_bounds)
+            WindowBounds::Maximized(state.windowed_bounds)
         } else {
             WindowBounds::Windowed(state.bounds.map(|p| DevicePixels(p as i32)))
         }
@@ -664,6 +664,7 @@ impl PlatformWindow for WaylandWindow {
         self.borrow().display.as_ref().map(|(id, display)| {
             Rc::new(WaylandDisplay {
                 id: id.clone(),
+                name: display.name.clone(),
                 bounds: display.bounds,
             }) as Rc<dyn PlatformDisplay>
         })
@@ -779,7 +780,6 @@ impl PlatformWindow for WaylandWindow {
 
     fn toggle_fullscreen(&self) {
         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 {