Add setting for enabling server-side decorations (#39250)

Be and Conrad Irwin created

Previously, this was controllable via the undocumented
ZED_WINDOW_DECORATIONS environment variable (added in #13866). Using an
environment variable for this is inconvenient because it requires users
to set that environment variable somehow before starting Zed, such as in
the .desktop file or persistently in their shell. Controlling this via a
Zed setting is more convenient.

This does not modify the design of the titlebar in any way. It only
moves the existing option from an environment variable to a Zed setting.

Fixes #14165

Client-side decorations (default):
<img width="3840" height="2160" alt="image"
src="https://github.com/user-attachments/assets/525feb92-2f60-47d3-b0ca-47c98770fa8c"
/>


Server-side decorations in KDE Plasma:
<img width="3840" height="2160" alt="image"
src="https://github.com/user-attachments/assets/7379c7c8-e5e3-47ba-a3ea-4191fec9434d"
/>

Release Notes:

- Changed option for Wayland server-side decorations from an environment
variable to settings.json field

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

assets/settings/default.json                      | 10 ++++++
crates/rules_library/src/rules_library.rs         |  7 +++-
crates/settings/src/settings_content/workspace.rs | 25 +++++++++++++++++
crates/settings/src/vscode_import.rs              |  1 
crates/settings_ui/src/page_data.rs               | 15 ++++++++++
crates/settings_ui/src/settings_ui.rs             |  1 
crates/workspace/src/workspace_settings.rs        |  2 +
crates/zed/src/zed.rs                             |  5 ++
8 files changed, 63 insertions(+), 3 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -175,6 +175,16 @@
   //
   // Default: true
   "zoomed_padding": true,
+  // What draws Zed's window decorations (titlebar):
+  // 1. Client application (Zed) draws its own window decorations
+  //    "client"
+  // 2. Display server draws the window decorations. Not supported by GNOME Wayland.
+  //    "server"
+  //
+  // This requires restarting Zed for changes to take effect.
+  //
+  // Default: "client"
+  "window_decorations": "client",
   // Whether to use the system provided dialogs for Open and Save As.
   // When set to false, Zed will use the built-in keyboard-first pickers.
   "use_system_path_prompts": true,

crates/rules_library/src/rules_library.rs 🔗

@@ -25,7 +25,7 @@ use ui::{
     Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Render, Tooltip, prelude::*,
 };
 use util::{ResultExt, TryFutureExt};
-use workspace::{Workspace, client_side_decorations};
+use workspace::{Workspace, WorkspaceSettings, client_side_decorations};
 use zed_actions::assistant::InlineAssist;
 
 use prompt_store::*;
@@ -122,7 +122,10 @@ pub fn open_rules_library(
             let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
                 Ok(val) if val == "server" => gpui::WindowDecorations::Server,
                 Ok(val) if val == "client" => gpui::WindowDecorations::Client,
-                _ => gpui::WindowDecorations::Client,
+                _ => match WorkspaceSettings::get_global(cx).window_decorations {
+                    settings::WindowDecorations::Server => gpui::WindowDecorations::Server,
+                    settings::WindowDecorations::Client => gpui::WindowDecorations::Client,
+                },
             };
             cx.open_window(
                 WindowOptions {

crates/settings/src/settings_content/workspace.rs 🔗

@@ -109,6 +109,9 @@ pub struct WorkspaceSettingsContent {
     ///
     /// Default: true
     pub zoomed_padding: Option<bool>,
+    /// What draws window decorations/titlebar, the client application (Zed) or display server
+    /// Default: client
+    pub window_decorations: Option<WindowDecorations>,
 }
 
 #[with_fallible_options]
@@ -290,6 +293,28 @@ pub enum BottomDockLayout {
     RightAligned,
 }
 
+#[derive(
+    Copy,
+    Clone,
+    Default,
+    Debug,
+    Serialize,
+    Deserialize,
+    PartialEq,
+    JsonSchema,
+    MergeFrom,
+    strum::VariantArray,
+    strum::VariantNames,
+)]
+#[serde(rename_all = "snake_case")]
+pub enum WindowDecorations {
+    /// Zed draws its own window decorations/titlebar (client-side decoration)
+    #[default]
+    Client,
+    /// Show system's window titlebar (server-side decoration; not supported by GNOME Wayland)
+    Server,
+}
+
 #[derive(
     Copy,
     Clone,

crates/settings/src/vscode_import.rs 🔗

@@ -843,6 +843,7 @@ impl VsCodeSettings {
             resize_all_panels_in_dock: None,
             restore_on_file_reopen: self.read_bool("workbench.editor.restoreViewState"),
             restore_on_startup: None,
+            window_decorations: None,
             show_call_status_icon: None,
             use_system_path_prompts: self.read_bool("files.simpleDialog.enable"),
             use_system_prompts: None,

crates/settings_ui/src/page_data.rs 🔗

@@ -3264,6 +3264,21 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Window Decorations",
+                    description: "(Linux only) whether Zed or your compositor should draw window decorations.",
+                    field: Box::new(SettingField {
+                        json_path: Some("window_decorations"),
+                        pick: |settings_content| {
+                            settings_content.workspace.window_decorations.as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content.workspace.window_decorations = value;
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
                 SettingsPageItem::SectionHeader("Pane Modifiers"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Inactive Opacity",

crates/settings_ui/src/settings_ui.rs 🔗

@@ -513,6 +513,7 @@ fn init_renderers(cx: &mut App) {
         .add_basic_renderer::<settings::ShellDiscriminants>(render_dropdown)
         .add_basic_renderer::<settings::EditPredictionsMode>(render_dropdown)
         .add_basic_renderer::<settings::RelativeLineNumbers>(render_dropdown)
+        .add_basic_renderer::<settings::WindowDecorations>(render_dropdown)
         // please semicolon stay on next line
         ;
 }

crates/workspace/src/workspace_settings.rs 🔗

@@ -31,6 +31,7 @@ pub struct WorkspaceSettings {
     pub close_on_file_delete: bool,
     pub use_system_window_tabs: bool,
     pub zoomed_padding: bool,
+    pub window_decorations: settings::WindowDecorations,
 }
 
 #[derive(Copy, Clone, PartialEq, Debug, Default)]
@@ -105,6 +106,7 @@ impl Settings for WorkspaceSettings {
             close_on_file_delete: workspace.close_on_file_delete.unwrap(),
             use_system_window_tabs: workspace.use_system_window_tabs.unwrap(),
             zoomed_padding: workspace.zoomed_padding.unwrap(),
+            window_decorations: workspace.window_decorations.unwrap(),
         }
     }
 }

crates/zed/src/zed.rs 🔗

@@ -307,7 +307,10 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowO
     let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
         Ok(val) if val == "server" => gpui::WindowDecorations::Server,
         Ok(val) if val == "client" => gpui::WindowDecorations::Client,
-        _ => gpui::WindowDecorations::Client,
+        _ => match WorkspaceSettings::get_global(cx).window_decorations {
+            settings::WindowDecorations::Server => gpui::WindowDecorations::Server,
+            settings::WindowDecorations::Client => gpui::WindowDecorations::Client,
+        },
     };
 
     let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs;