ui: Add margin style methods to `Label` and `LabelLike` (#14032)

Marshall Bowers created

This PR adds margin style methods to the `Label` and `LabelLike`
components.

This allows for callers to provide a margin to these components without
needing to introduce a wrapping `div` to do so.

Release Notes:

- N/A

Change summary

crates/collab_ui/src/collab_panel.rs           |  3 
crates/editor/src/editor.rs                    | 11 +-
crates/search/src/project_search.rs            |  6 
crates/title_bar/src/collab.rs                 |  3 
crates/ui/src/components/button/button_like.rs |  2 
crates/ui/src/components/label/label.rs        | 13 +++
crates/ui/src/components/label/label_like.rs   | 17 +++
crates/vcs_menu/src/lib.rs                     | 76 +++++++++++--------
8 files changed, 81 insertions(+), 50 deletions(-)

Detailed changes

crates/collab_ui/src/collab_panel.rs 🔗

@@ -2547,9 +2547,8 @@ impl CollabPanel {
                     .take(FACEPILE_LIMIT)
                     .chain(if extra_count > 0 {
                         Some(
-                            div()
+                            Label::new(format!("+{extra_count}"))
                                 .ml_2()
-                                .child(Label::new(format!("+{extra_count}")))
                                 .into_any_element(),
                         )
                     } else {

crates/editor/src/editor.rs 🔗

@@ -1129,11 +1129,10 @@ impl CompletionsMenu {
                                     None
                                 } else {
                                     Some(
-                                        h_flex().ml_4().child(
-                                            Label::new(text.clone())
-                                                .size(LabelSize::Small)
-                                                .color(Color::Muted),
-                                        ),
+                                        Label::new(text.clone())
+                                            .ml_4()
+                                            .size(LabelSize::Small)
+                                            .color(Color::Muted),
                                     )
                                 }
                             } else {
@@ -1156,7 +1155,7 @@ impl CompletionsMenu {
                                     }
                                 }))
                                 .child(h_flex().overflow_hidden().child(completion_label))
-                                .end_slot::<Div>(documentation_label),
+                                .end_slot::<Label>(documentation_label),
                         )
                     })
                     .collect()

crates/search/src/project_search.rs 🔗

@@ -1458,9 +1458,9 @@ impl Render for ProjectSearchBar {
             )
             .when(limit_reached, |this| {
                 this.child(
-                    div()
-                        .child(Label::new("Search limit reached").color(Color::Warning))
-                        .ml_2(),
+                    Label::new("Search limit reached")
+                        .ml_2()
+                        .color(Color::Warning),
                 )
             });
 

crates/title_bar/src/collab.rs 🔗

@@ -262,9 +262,8 @@ impl TitleBar {
                         ))
                         .children(if extra_count > 0 {
                             Some(
-                                div()
+                                Label::new(format!("+{extra_count}"))
                                     .ml_1()
-                                    .child(Label::new(format!("+{extra_count}")))
                                     .into_any_element(),
                             )
                         } else {

crates/ui/src/components/button/button_like.rs 🔗

@@ -332,7 +332,7 @@ impl ButtonSize {
 /// This is also used to build the prebuilt buttons.
 #[derive(IntoElement)]
 pub struct ButtonLike {
-    pub base: Div,
+    pub(super) base: Div,
     id: ElementId,
     pub(super) style: ButtonStyle,
     pub(super) disabled: bool,

crates/ui/src/components/label/label.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::WindowContext;
+use gpui::{StyleRefinement, WindowContext};
 
 use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
 
@@ -70,6 +70,17 @@ impl Label {
     }
 }
 
+// Style methods.
+impl Label {
+    fn style(&mut self) -> &mut StyleRefinement {
+        self.base.base.style()
+    }
+
+    gpui::margin_style_methods!({
+        visibility: pub
+    });
+}
+
 impl LabelCommon for Label {
     /// Sets the size of the label using a [`LabelSize`].
     ///

crates/ui/src/components/label/label_like.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{relative, AnyElement, FontWeight, Styled};
+use gpui::{relative, AnyElement, FontWeight, StyleRefinement, Styled};
 use smallvec::SmallVec;
 
 use crate::prelude::*;
@@ -43,6 +43,7 @@ pub trait LabelCommon {
 
 #[derive(IntoElement)]
 pub struct LabelLike {
+    pub(super) base: Div,
     size: LabelSize,
     weight: FontWeight,
     line_height_style: LineHeightStyle,
@@ -55,6 +56,7 @@ pub struct LabelLike {
 impl LabelLike {
     pub fn new() -> Self {
         Self {
+            base: div(),
             size: LabelSize::Default,
             weight: FontWeight::default(),
             line_height_style: LineHeightStyle::default(),
@@ -66,6 +68,17 @@ impl LabelLike {
     }
 }
 
+// Style methods.
+impl LabelLike {
+    fn style(&mut self) -> &mut StyleRefinement {
+        self.base.style()
+    }
+
+    gpui::margin_style_methods!({
+        visibility: pub
+    });
+}
+
 impl LabelCommon for LabelLike {
     fn size(mut self, size: LabelSize) -> Self {
         self.size = size;
@@ -106,7 +119,7 @@ impl ParentElement for LabelLike {
 
 impl RenderOnce for LabelLike {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        div()
+        self.base
             .when(self.strikethrough, |this| {
                 this.relative().child(
                     div()

crates/vcs_menu/src/lib.rs 🔗

@@ -2,9 +2,9 @@ use anyhow::{Context, Result};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use git::repository::Branch;
 use gpui::{
-    actions, rems, AnyElement, AppContext, DismissEvent, Element, EventEmitter, FocusHandle,
-    FocusableView, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
-    Subscription, Task, View, ViewContext, VisualContext, WindowContext,
+    actions, rems, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
+    InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
+    Task, View, ViewContext, VisualContext, WindowContext,
 };
 use picker::{Picker, PickerDelegate};
 use std::{ops::Not, sync::Arc};
@@ -268,11 +268,13 @@ impl PickerDelegate for BranchListDelegate {
                 .start_slot(HighlightedLabel::new(shortened_branch_name, highlights)),
         )
     }
+
     fn render_header(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
         let label = if self.last_query.is_empty() {
-            h_flex()
+            Label::new("Recent Branches")
+                .size(LabelSize::Small)
                 .ml_3()
-                .child(Label::new("Recent Branches").size(LabelSize::Small))
+                .into_any_element()
         } else {
             let match_label = self.matches.is_empty().not().then(|| {
                 let suffix = if self.matches.len() == 1 { "" } else { "es" };
@@ -285,43 +287,51 @@ impl PickerDelegate for BranchListDelegate {
                 .justify_between()
                 .child(Label::new("Branches").size(LabelSize::Small))
                 .children(match_label)
+                .into_any_element()
         };
-        Some(label.mt_1().into_any())
+        Some(v_flex().mt_1().child(label).into_any_element())
     }
+
     fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
         if self.last_query.is_empty() {
             return None;
         }
 
         Some(
-            h_flex().mr_3().pb_2().child(h_flex().w_full()).child(
-            Button::new("branch-picker-create-branch-button", "Create branch").on_click(
-                cx.listener(|_, _, cx| {
-                    cx.spawn(|picker, mut cx| async move {
-                                        picker.update(&mut cx, |this, cx| {
-                                            let project = this.delegate.workspace.read(cx).project().read(cx);
-                                            let current_pick = &this.delegate.last_query;
-                                            let repo = project
-                                                .get_first_worktree_root_repo(cx)
-                                                .context("failed to get root repository for first worktree")?;
-                                            let status = repo
-                                                .create_branch(&current_pick);
-                                            if status.is_err() {
-                                                this.delegate.display_error_toast(format!("Failed to create branch '{current_pick}', check for conflicts or unstashed files"), cx);
-                                                status?;
-                                            }
-                                            let status = repo.change_branch(&current_pick);
-                                            if status.is_err() {
-                                                this.delegate.display_error_toast(format!("Failed to check branch '{current_pick}', check for conflicts or unstashed files"), cx);
-                                                status?;
-                                            }
-                                            this.cancel(&Default::default(), cx);
-                                            Ok::<(), anyhow::Error>(())
+            h_flex()
+                .mr_3()
+                .pb_2()
+                .child(h_flex().w_full())
+                .child(
+                    Button::new("branch-picker-create-branch-button", "Create branch")
+                        .on_click(cx.listener(|_, _, cx| {
+                            cx.spawn(|picker, mut cx| async move {
+                                picker.update(&mut cx, |this, cx| {
+                                    let project =
+                                        this.delegate.workspace.read(cx).project().read(cx);
+                                    let current_pick = &this.delegate.last_query;
+                                    let repo = project.get_first_worktree_root_repo(cx).context(
+                                        "failed to get root repository for first worktree",
+                                    )?;
+                                    let status = repo.create_branch(&current_pick);
+                                    if status.is_err() {
+                                        this.delegate.display_error_toast(format!("Failed to create branch '{current_pick}', check for conflicts or unstashed files"), cx);
+                                        status?;
+                                    }
+                                    let status = repo.change_branch(&current_pick);
+                                    if status.is_err() {
+                                        this.delegate.display_error_toast(format!("Failed to check branch '{current_pick}', check for conflicts or unstashed files"), cx);
+                                        status?;
+                                    }
+                                    this.cancel(&Default::default(), cx);
+                                    Ok::<(), anyhow::Error>(())
                                 })
-
-                    }).detach_and_log_err(cx);
-                }),
-            ).style(ui::ButtonStyle::Filled)).into_any_element(),
+                            })
+                            .detach_and_log_err(cx);
+                        }))
+                        .style(ui::ButtonStyle::Filled),
+                )
+                .into_any_element(),
         )
     }
 }