feat(workspace): add option for moving the tab close button to the left (#2739)

Mikayla Maki created

Fixes https://github.com/zed-industries/community/issues/1760

Release Notes:

- Add option for chosing where the close button should be displayed on
editor tabs

Change summary

assets/settings/default.json |   7 +
crates/theme/src/theme.rs    |   4 
crates/workspace/src/item.rs |  19 ++++
crates/workspace/src/pane.rs | 149 ++++++++++++++++++++-----------------
4 files changed, 107 insertions(+), 72 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -128,9 +128,12 @@
   // 4. Save when idle for a certain amount of time:
   //     "autosave": { "after_delay": {"milliseconds": 500} },
   "autosave": "off",
-  // Color tab titles based on the git status of the buffer.
+  // Settings related to the editor's tabs
   "tabs": {
-    "git_status": false
+    // Show git status colors in the editor tabs.
+    "git_status": false,
+    // Position of the close button on the editor tabs.
+    "close_position": "right"
   },
   // Whether or not to remove any trailing whitespace from lines of a buffer
   // before saving it.

crates/theme/src/theme.rs 🔗

@@ -723,12 +723,12 @@ pub struct Scrollbar {
     pub thumb: ContainerStyle,
     pub width: f32,
     pub min_height_factor: f32,
-    pub git: FileGitDiffColors,
+    pub git: BufferGitDiffColors,
     pub selections: Color,
 }
 
 #[derive(Clone, Deserialize, Default, JsonSchema)]
-pub struct FileGitDiffColors {
+pub struct BufferGitDiffColors {
     pub inserted: Color,
     pub modified: Color,
     pub deleted: Color,

crates/workspace/src/item.rs 🔗

@@ -33,11 +33,30 @@ use theme::Theme;
 #[derive(Deserialize)]
 pub struct ItemSettings {
     pub git_status: bool,
+    pub close_position: ClosePosition,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum ClosePosition {
+    Left,
+    #[default]
+    Right,
+}
+
+impl ClosePosition {
+    pub fn right(&self) -> bool {
+        match self {
+            ClosePosition::Left => false,
+            ClosePosition::Right => true,
+        }
+    }
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct ItemSettingsContent {
     git_status: Option<bool>,
+    close_position: Option<ClosePosition>,
 }
 
 impl Setting for ItemSettings {

crates/workspace/src/pane.rs 🔗

@@ -1370,81 +1370,94 @@ impl Pane {
             container.border.left = false;
         }
 
-        Flex::row()
-            .with_child({
-                let diameter = 7.0;
-                let icon_color = if item.has_conflict(cx) {
-                    Some(tab_style.icon_conflict)
-                } else if item.is_dirty(cx) {
-                    Some(tab_style.icon_dirty)
-                } else {
-                    None
-                };
+        let buffer_jewel_element = {
+            let diameter = 7.0;
+            let icon_color = if item.has_conflict(cx) {
+                Some(tab_style.icon_conflict)
+            } else if item.is_dirty(cx) {
+                Some(tab_style.icon_dirty)
+            } else {
+                None
+            };
 
-                Canvas::new(move |scene, bounds, _, _, _| {
-                    if let Some(color) = icon_color {
-                        let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
-                        scene.push_quad(Quad {
-                            bounds: square,
-                            background: Some(color),
-                            border: Default::default(),
-                            corner_radius: diameter / 2.,
-                        });
-                    }
-                })
-                .constrained()
-                .with_width(diameter)
-                .with_height(diameter)
-                .aligned()
+            Canvas::new(move |scene, bounds, _, _, _| {
+                if let Some(color) = icon_color {
+                    let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
+                    scene.push_quad(Quad {
+                        bounds: square,
+                        background: Some(color),
+                        border: Default::default(),
+                        corner_radius: diameter / 2.,
+                    });
+                }
             })
-            .with_child(title.aligned().contained().with_style(ContainerStyle {
-                margin: Margin {
-                    left: tab_style.spacing,
-                    right: tab_style.spacing,
-                    ..Default::default()
-                },
+            .constrained()
+            .with_width(diameter)
+            .with_height(diameter)
+            .aligned()
+        };
+
+        let title_element = title.aligned().contained().with_style(ContainerStyle {
+            margin: Margin {
+                left: tab_style.spacing,
+                right: tab_style.spacing,
                 ..Default::default()
-            }))
-            .with_child(
-                if hovered {
-                    let item_id = item.id();
-                    enum TabCloseButton {}
-                    let icon = Svg::new("icons/x_mark_8.svg");
-                    MouseEventHandler::<TabCloseButton, _>::new(item_id, cx, |mouse_state, _| {
-                        if mouse_state.hovered() {
-                            icon.with_color(tab_style.icon_close_active)
-                        } else {
-                            icon.with_color(tab_style.icon_close)
-                        }
-                    })
-                    .with_padding(Padding::uniform(4.))
-                    .with_cursor_style(CursorStyle::PointingHand)
-                    .on_click(MouseButton::Left, {
-                        let pane = pane.clone();
-                        move |_, _, cx| {
-                            let pane = pane.clone();
-                            cx.window_context().defer(move |cx| {
-                                if let Some(pane) = pane.upgrade(cx) {
-                                    pane.update(cx, |pane, cx| {
-                                        pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
-                                    });
-                                }
+            },
+            ..Default::default()
+        });
+
+        let close_element = if hovered {
+            let item_id = item.id();
+            enum TabCloseButton {}
+            let icon = Svg::new("icons/x_mark_8.svg");
+            MouseEventHandler::<TabCloseButton, _>::new(item_id, cx, |mouse_state, _| {
+                if mouse_state.hovered() {
+                    icon.with_color(tab_style.icon_close_active)
+                } else {
+                    icon.with_color(tab_style.icon_close)
+                }
+            })
+            .with_padding(Padding::uniform(4.))
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, {
+                let pane = pane.clone();
+                move |_, _, cx| {
+                    let pane = pane.clone();
+                    cx.window_context().defer(move |cx| {
+                        if let Some(pane) = pane.upgrade(cx) {
+                            pane.update(cx, |pane, cx| {
+                                pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
                             });
                         }
-                    })
-                    .into_any_named("close-tab-icon")
-                    .constrained()
-                } else {
-                    Empty::new().constrained()
+                    });
                 }
-                .with_width(tab_style.close_icon_width)
-                .aligned(),
-            )
-            .contained()
-            .with_style(container)
+            })
+            .into_any_named("close-tab-icon")
             .constrained()
-            .with_height(tab_style.height)
-            .into_any()
+        } else {
+            Empty::new().constrained()
+        }
+        .with_width(tab_style.close_icon_width)
+        .aligned();
+
+        let close_right = settings::get::<ItemSettings>(cx).close_position.right();
+
+        if close_right {
+            Flex::row()
+                .with_child(buffer_jewel_element)
+                .with_child(title_element)
+                .with_child(close_element)
+        } else {
+            Flex::row()
+                .with_child(close_element)
+                .with_child(title_element)
+                .with_child(buffer_jewel_element)
+        }
+        .contained()
+        .with_style(container)
+        .constrained()
+        .with_height(tab_style.height)
+        .into_any()
     }
 
     pub fn render_tab_bar_button<