Fix spacing around hidden status bar items (#39992)

kitt created

This is a follow-up PR to
https://github.com/zed-industries/zed/pull/39609, and attempts to
address hidden status bar items still contributing to the layout and
creating extra spacing.

![before using display:none theres extra spaces, afterwords the buttons
are always evenly
spaced](https://github.com/user-attachments/assets/3bd07837-5f6f-4ca1-8985-9f3cb8b6893d)

- 203cbd634bfb1489b8afa4952d9594615a956b77 Adds a `.none()` method to
the `gpui::Styled` helper trait, so that status items can set their
display type to none inside their `render` method.

- 249f06e3de63b0ab32814f20e7105d8e2b642f02 Applies `.none()` to all the
status items.

- ~~499f564906c88336608c81615b11ebc9ab43d832~~ At first I was adding an
`is_visible` method to the `StatusBarView` trait, which would be used to
skip status bar items which would just render an empty div anyway, but I
felt duplicating the conditions for hiding the buttons between the
status items `is_visible` and `render` methods could be an attraction
for bugs, so I tried to find another approach. This commit contains
those changes, reverted immediately (if the `is_visible` approach is
preferred I can bring it back!)

- f37cb75f0519ceea1f3e1cc4f97087a5cb34b0fd (bonus!) Adds a condition to
the vim mode indicator to avoid a leading space when there are no
pending keys.

Release Notes:

- N/A

Change summary

crates/diagnostics/src/items.rs                             |  2 
crates/edit_prediction_button/src/edit_prediction_button.rs |  6 
crates/go_to_line/src/cursor_position.rs                    |  4 
crates/gpui/src/elements/div.rs                             | 27 +++-
crates/gpui/src/styled.rs                                   |  7 +
crates/image_viewer/src/image_info.rs                       |  2 
crates/language_selector/src/active_buffer_language.rs      |  5 
crates/language_tools/src/lsp_button.rs                     |  2 
crates/search/src/search_status_button.rs                   |  2 
crates/toolchain_selector/src/active_toolchain.rs           | 37 +++---
crates/vim/src/mode_indicator.rs                            |  4 
11 files changed, 59 insertions(+), 39 deletions(-)

Detailed changes

crates/diagnostics/src/items.rs 🔗

@@ -30,7 +30,7 @@ impl Render for DiagnosticIndicator {
     fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let indicator = h_flex().gap_2();
         if !ProjectSettings::get_global(cx).diagnostics.button {
-            return indicator;
+            return indicator.hidden();
         }
 
         let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {

crates/edit_prediction_button/src/edit_prediction_button.rs 🔗

@@ -72,17 +72,17 @@ impl Render for EditPredictionButton {
     fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         // Return empty div if AI is disabled
         if DisableAiSettings::get_global(cx).disable_ai {
-            return div();
+            return div().hidden();
         }
 
         let all_language_settings = all_language_settings(None, cx);
 
         match all_language_settings.edit_predictions.provider {
-            EditPredictionProvider::None => div(),
+            EditPredictionProvider::None => div().hidden(),
 
             EditPredictionProvider::Copilot => {
                 let Some(copilot) = Copilot::global(cx) else {
-                    return div();
+                    return div().hidden();
                 };
                 let status = copilot.read(cx).status();
 

crates/go_to_line/src/cursor_position.rs 🔗

@@ -1,5 +1,5 @@
 use editor::{Editor, MultiBufferSnapshot};
-use gpui::{App, Entity, FocusHandle, Focusable, Subscription, Task, WeakEntity};
+use gpui::{App, Entity, FocusHandle, Focusable, Styled, Subscription, Task, WeakEntity};
 use settings::Settings;
 use std::{fmt::Write, num::NonZeroU32, time::Duration};
 use text::{Point, Selection};
@@ -208,7 +208,7 @@ impl CursorPosition {
 impl Render for CursorPosition {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         if !StatusBarSettings::get_global(cx).cursor_position_button {
-            return div();
+            return div().hidden();
         }
 
         div().when_some(self.position, |el, position| {

crates/gpui/src/elements/div.rs 🔗

@@ -17,12 +17,13 @@
 
 use crate::{
     AbsoluteLength, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent,
-    DispatchPhase, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox,
-    HitboxBehavior, HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent,
-    KeyUpEvent, KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton,
-    MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow, ParentElement, Pixels,
-    Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task,
-    TooltipId, Visibility, Window, WindowControlArea, point, px, size,
+    DispatchPhase, Display, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId,
+    Hitbox, HitboxBehavior, HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext,
+    KeyDownEvent, KeyUpEvent, KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent,
+    MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow,
+    ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
+    StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px,
+    size,
 };
 use collections::HashMap;
 use refineable::Refineable;
@@ -1403,7 +1404,12 @@ impl Element for Div {
             content_size,
             window,
             cx,
-            |_style, scroll_offset, hitbox, window, cx| {
+            |style, scroll_offset, hitbox, window, cx| {
+                // skip children
+                if style.display == Display::None {
+                    return hitbox;
+                }
+
                 window.with_element_offset(scroll_offset, |window| {
                     for child in &mut self.children {
                         child.prepaint(window, cx);
@@ -1443,7 +1449,12 @@ impl Element for Div {
                 hitbox.as_ref(),
                 window,
                 cx,
-                |_style, window, cx| {
+                |style, window, cx| {
+                    // skip children
+                    if style.display == Display::None {
+                        return;
+                    }
+
                     for child in &mut self.children {
                         child.paint(window, cx);
                     }

crates/gpui/src/styled.rs 🔗

@@ -53,6 +53,13 @@ pub trait Styled: Sized {
         self
     }
 
+    /// Sets the display type of the element to `none`.
+    /// [Docs](https://tailwindcss.com/docs/display)
+    fn hidden(mut self) -> Self {
+        self.style().display = Some(Display::None);
+        self
+    }
+
     /// Sets the whitespace of the element to `normal`.
     /// [Docs](https://tailwindcss.com/docs/whitespace#normal)
     fn whitespace_normal(mut self) -> Self {

crates/image_viewer/src/image_info.rs 🔗

@@ -47,7 +47,7 @@ impl Render for ImageInfo {
         let settings = ImageViewerSettings::get_global(cx);
 
         let Some(metadata) = self.metadata.as_ref() else {
-            return div();
+            return div().hidden();
         };
 
         let mut components = Vec::new();

crates/language_selector/src/active_buffer_language.rs 🔗

@@ -1,6 +1,7 @@
 use editor::Editor;
 use gpui::{
-    Context, Entity, IntoElement, ParentElement, Render, Subscription, WeakEntity, Window, div,
+    Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window,
+    div,
 };
 use language::LanguageName;
 use settings::Settings as _;
@@ -41,7 +42,7 @@ impl ActiveBufferLanguage {
 impl Render for ActiveBufferLanguage {
     fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         if !StatusBarSettings::get_global(cx).active_language_button {
-            return div();
+            return div().hidden();
         }
 
         div().when_some(self.active_language.as_ref(), |el, active_language| {

crates/language_tools/src/lsp_button.rs 🔗

@@ -1011,7 +1011,7 @@ impl StatusItemView for LspButton {
 impl Render for LspButton {
     fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
         if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() {
-            return div();
+            return div().hidden();
         }
 
         let mut has_errors = false;

crates/search/src/search_status_button.rs 🔗

@@ -18,7 +18,7 @@ impl Render for SearchButton {
         let button = div();
 
         if !EditorSettings::get_global(cx).search.button {
-            return button.w_0().invisible();
+            return button.hidden();
         }
 
         button.child(

crates/toolchain_selector/src/active_toolchain.rs 🔗

@@ -2,12 +2,12 @@ use std::sync::Arc;
 
 use editor::Editor;
 use gpui::{
-    AsyncWindowContext, Context, Entity, IntoElement, ParentElement, Render, Subscription, Task,
-    WeakEntity, Window, div,
+    AsyncWindowContext, Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription,
+    Task, WeakEntity, Window, div,
 };
 use language::{Buffer, BufferEvent, LanguageName, Toolchain, ToolchainScope};
 use project::{Project, ProjectPath, Toolchains, WorktreeId, toolchain_store::ToolchainStoreEvent};
-use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip};
+use ui::{Button, ButtonCommon, Clickable, LabelSize, SharedString, Tooltip};
 use util::{maybe, rel_path::RelPath};
 use workspace::{StatusItemView, Workspace, item::ItemHandle};
 
@@ -230,21 +230,22 @@ impl ActiveToolchain {
 
 impl Render for ActiveToolchain {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        div().when_some(self.active_toolchain.as_ref(), |el, active_toolchain| {
-            let term = self.term.clone();
-            el.child(
-                Button::new("change-toolchain", active_toolchain.name.clone())
-                    .label_size(LabelSize::Small)
-                    .on_click(cx.listener(|this, _, window, cx| {
-                        if let Some(workspace) = this.workspace.upgrade() {
-                            workspace.update(cx, |workspace, cx| {
-                                ToolchainSelector::toggle(workspace, window, cx)
-                            });
-                        }
-                    }))
-                    .tooltip(Tooltip::text(format!("Select {}", &term))),
-            )
-        })
+        let Some(active_toolchain) = self.active_toolchain.as_ref() else {
+            return div().hidden();
+        };
+
+        div().child(
+            Button::new("change-toolchain", active_toolchain.name.clone())
+                .label_size(LabelSize::Small)
+                .on_click(cx.listener(|this, _, window, cx| {
+                    if let Some(workspace) = this.workspace.upgrade() {
+                        workspace.update(cx, |workspace, cx| {
+                            ToolchainSelector::toggle(workspace, window, cx)
+                        });
+                    }
+                }))
+                .tooltip(Tooltip::text(format!("Select {}", &self.term))),
+        )
     }
 }
 

crates/vim/src/mode_indicator.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{Context, Element, Entity, Render, Subscription, WeakEntity, Window, div};
+use gpui::{Context, Entity, Render, Subscription, WeakEntity, Window, div};
 use ui::text_for_keystrokes;
 use workspace::{StatusItemView, item::ItemHandle, ui::prelude::*};
 
@@ -89,7 +89,7 @@ impl Render for ModeIndicator {
     fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let vim = self.vim();
         let Some(vim) = vim else {
-            return div().into_any();
+            return div().hidden().into_any_element();
         };
 
         let vim_readable = vim.read(cx);