ui: Clean up `PlatformTitlebar` implementation (#9413)

Marshall Bowers created

This PR cleans up the implementation of the `PlatformTitlebar` component
to better match our conventions for building UI components.

Release Notes:

- N/A

Change summary

crates/collab_ui/src/collab_titlebar_item.rs          |   9 
crates/storybook/src/story_selector.rs                |   2 
crates/ui/src/components/platform_titlebar.rs         | 221 ++++++------
crates/ui/src/components/stories.rs                   |   2 
crates/ui/src/components/stories/platform_titlebar.rs |  46 ++
5 files changed, 158 insertions(+), 122 deletions(-)

Detailed changes

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -13,8 +13,8 @@ use rpc::proto;
 use std::sync::Arc;
 use theme::ActiveTheme;
 use ui::{
-    h_flex, platform_titlebar, popover_menu, prelude::*, Avatar, AvatarAudioStatusIndicator,
-    Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconButton, IconName, TintColor, Tooltip,
+    h_flex, popover_menu, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike,
+    ButtonStyle, ContextMenu, Icon, IconButton, IconName, PlatformTitlebar, TintColor, Tooltip,
 };
 use util::ResultExt;
 use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
@@ -58,8 +58,8 @@ impl Render for CollabTitlebarItem {
         let project_id = self.project.read(cx).remote_id();
         let workspace = self.workspace.upgrade();
 
-        platform_titlebar("collab-titlebar")
-            .titlebar_bg(cx.theme().colors().title_bar_background)
+        PlatformTitlebar::new("collab-titlebar")
+            .background(cx.theme().colors().title_bar_background)
             // note: on windows titlebar behaviour is handled by the platform implementation
             .when(cfg!(not(windows)), |this| {
                 this.on_click(|event, cx| {
@@ -68,7 +68,6 @@ impl Render for CollabTitlebarItem {
                     }
                 })
             })
-            .justify_between()
             // left side
             .child(
                 h_flex()

crates/storybook/src/story_selector.rs 🔗

@@ -29,6 +29,7 @@ pub enum ComponentStory {
     ListHeader,
     ListItem,
     OverflowScroll,
+    PlatformTitlebar,
     Scroll,
     Tab,
     TabBar,
@@ -60,6 +61,7 @@ impl ComponentStory {
             Self::ListHeader => cx.new_view(|_| ui::ListHeaderStory).into(),
             Self::ListItem => cx.new_view(|_| ui::ListItemStory).into(),
             Self::OverflowScroll => cx.new_view(|_| crate::stories::OverflowScrollStory).into(),
+            Self::PlatformTitlebar => cx.new_view(|_| ui::PlatformTitlebarStory).into(),
             Self::Scroll => ScrollStory::view(cx).into(),
             Self::Text => TextStory::view(cx).into(),
             Self::Tab => cx.new_view(|_| ui::TabStory).into(),

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

@@ -1,18 +1,7 @@
-// allowing due to multiple platform conditional code
-#![allow(unused_imports)]
-
-use gpui::{
-    div,
-    prelude::FluentBuilder,
-    px, transparent_black, AnyElement, Div, Element, ElementId, Fill, InteractiveElement,
-    Interactivity, IntoElement, ParentElement, Pixels, RenderOnce, Rgba, Stateful,
-    StatefulInteractiveElement, StyleRefinement, Styled,
-    WindowAppearance::{Dark, Light, VibrantDark, VibrantLight},
-    WindowContext,
-};
+use gpui::{transparent_black, AnyElement, Fill, Interactivity, Rgba, Stateful, WindowAppearance};
 use smallvec::SmallVec;
 
-use crate::h_flex;
+use crate::prelude::*;
 
 pub enum PlatformStyle {
     Linux,
@@ -47,27 +36,38 @@ impl PlatformStyle {
 #[derive(IntoElement)]
 pub struct PlatformTitlebar {
     platform: PlatformStyle,
-    titlebar_bg: Fill,
+    background: Fill,
     content: Stateful<Div>,
     children: SmallVec<[AnyElement; 2]>,
 }
 
-impl Styled for PlatformTitlebar {
-    fn style(&mut self) -> &mut StyleRefinement {
-        self.content.style()
-    }
-}
-
 impl PlatformTitlebar {
-    /// Change the platform style used
-    pub fn with_platform_style(self, style: PlatformStyle) -> Self {
+    pub fn new(id: impl Into<ElementId>) -> Self {
         Self {
-            platform: style,
-            ..self
+            platform: PlatformStyle::platform(),
+            background: transparent_black().into(),
+            content: div().id(id.into()),
+            children: SmallVec::new(),
         }
     }
 
-    fn titlebar_top_padding(&self, cx: &WindowContext) -> Pixels {
+    /// Sets the platform style.
+    pub fn platform_style(mut self, style: PlatformStyle) -> Self {
+        self.platform = style;
+        self
+    }
+
+    /// Sets the background color of the titlebar.
+    pub fn background<F>(mut self, fill: F) -> Self
+    where
+        F: Into<Fill>,
+        Self: Sized,
+    {
+        self.background = fill.into();
+        self
+    }
+
+    fn top_padding(&self, cx: &WindowContext) -> Pixels {
         if self.platform.windows() && cx.is_maximized() {
             // todo(windows): get padding from win32 api, need HWND from window context somehow
             // should be GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) * 2
@@ -84,106 +84,91 @@ impl PlatformTitlebar {
     }
 
     fn render_window_controls_right(&self, cx: &mut WindowContext) -> impl Element {
-        if self.platform.windows() {
-            let btn_height = titlebar_height(cx) - self.titlebar_top_padding(cx);
-            let close_btn_hover_color = Rgba {
-                r: 232.0 / 255.0,
-                g: 17.0 / 255.0,
-                b: 32.0 / 255.0,
-                a: 1.0,
-            };
-
-            let btn_hover_color = match cx.appearance() {
-                Light | VibrantLight => Rgba {
-                    r: 0.1,
-                    g: 0.1,
-                    b: 0.1,
-                    a: 0.2,
-                },
-                Dark | VibrantDark => Rgba {
-                    r: 0.9,
-                    g: 0.9,
-                    b: 0.9,
-                    a: 0.1,
-                },
-            };
-
-            fn windows_caption_btn(
-                id: &'static str,
-                icon_text: &'static str,
-                hover_color: Rgba,
-                cx: &WindowContext,
-            ) -> Stateful<Div> {
-                let mut active_color = hover_color;
-                active_color.a *= 0.2;
-                h_flex()
-                    .id(id)
-                    .h_full()
-                    .justify_center()
-                    .content_center()
-                    .items_center()
-                    .w(PlatformTitlebar::windows_caption_button_width(cx))
-                    .hover(|style| style.bg(hover_color))
-                    .active(|style| style.bg(active_color))
-                    .child(icon_text)
-            }
-
-            div()
-                .id("caption-buttons-windows")
-                .flex()
-                .flex_row()
+        if !self.platform.windows() {
+            return div().id("caption-buttons-windows");
+        }
+
+        let button_height = titlebar_height(cx) - self.top_padding(cx);
+        let close_button_hover_color = Rgba {
+            r: 232.0 / 255.0,
+            g: 17.0 / 255.0,
+            b: 32.0 / 255.0,
+            a: 1.0,
+        };
+
+        let button_hover_color = match cx.appearance() {
+            WindowAppearance::Light | WindowAppearance::VibrantLight => Rgba {
+                r: 0.1,
+                g: 0.1,
+                b: 0.1,
+                a: 0.2,
+            },
+            WindowAppearance::Dark | WindowAppearance::VibrantDark => Rgba {
+                r: 0.9,
+                g: 0.9,
+                b: 0.9,
+                a: 0.1,
+            },
+        };
+
+        fn windows_caption_button(
+            id: &'static str,
+            icon_text: &'static str,
+            hover_color: Rgba,
+            cx: &WindowContext,
+        ) -> Stateful<Div> {
+            let mut active_color = hover_color;
+            active_color.a *= 0.2;
+            h_flex()
+                .id(id)
+                .h_full()
                 .justify_center()
-                .content_stretch()
-                .max_h(btn_height)
-                .min_h(btn_height)
-                .font("Segoe Fluent Icons")
-                .text_size(px(10.0))
-                .children(vec![
-                    windows_caption_btn("minimize", "\u{e921}", btn_hover_color, cx), // minimize icon
-                    windows_caption_btn(
-                        "maximize",
-                        if cx.is_maximized() {
-                            "\u{e923}" // restore icon
-                        } else {
-                            "\u{e922}" // maximize icon
-                        },
-                        btn_hover_color,
-                        cx,
-                    ),
-                    windows_caption_btn("close", "\u{e8bb}", close_btn_hover_color, cx), // close icon
-                ])
-        } else {
-            div().id("caption-buttons-windows")
+                .content_center()
+                .items_center()
+                .w(PlatformTitlebar::windows_caption_button_width(cx))
+                .hover(|style| style.bg(hover_color))
+                .active(|style| style.bg(active_color))
+                .child(icon_text)
         }
-    }
 
-    /// Sets the background color of titlebar.
-    pub fn titlebar_bg<F>(mut self, fill: F) -> Self
-    where
-        F: Into<Fill>,
-        Self: Sized,
-    {
-        self.titlebar_bg = fill.into();
-        self
-    }
-}
+        const MINIMIZE_ICON: &str = "\u{e921}";
+        const RESTORE_ICON: &str = "\u{e923}";
+        const MAXIMIZE_ICON: &str = "\u{e922}";
+        const CLOSE_ICON: &str = "\u{e8bb}";
 
-pub fn platform_titlebar(id: impl Into<ElementId>) -> PlatformTitlebar {
-    let id = id.into();
-    PlatformTitlebar {
-        platform: PlatformStyle::platform(),
-        titlebar_bg: transparent_black().into(),
-        content: div().id(id.clone()),
-        children: SmallVec::new(),
+        div()
+            .id("caption-buttons-windows")
+            .flex()
+            .flex_row()
+            .justify_center()
+            .content_stretch()
+            .max_h(button_height)
+            .min_h(button_height)
+            .font("Segoe Fluent Icons")
+            .text_size(px(10.0))
+            .children(vec![
+                windows_caption_button("minimize", MINIMIZE_ICON, button_hover_color, cx),
+                windows_caption_button(
+                    "maximize",
+                    if cx.is_maximized() {
+                        RESTORE_ICON
+                    } else {
+                        MAXIMIZE_ICON
+                    },
+                    button_hover_color,
+                    cx,
+                ),
+                windows_caption_button("close", CLOSE_ICON, close_button_hover_color, cx),
+            ])
     }
 }
 
 impl RenderOnce for PlatformTitlebar {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
         let titlebar_height = titlebar_height(cx);
-        let titlebar_top_padding = self.titlebar_top_padding(cx);
+        let titlebar_top_padding = self.top_padding(cx);
         let window_controls_right = self.render_window_controls_right(cx);
-        let macos = self.platform.macos();
+
         h_flex()
             .id("titlebar")
             .w_full()
@@ -192,7 +177,7 @@ impl RenderOnce for PlatformTitlebar {
             .map(|this| {
                 if cx.is_fullscreen() {
                     this.pl_2()
-                } else if macos {
+                } else if self.platform.macos() {
                     // Use pixels here instead of a rem-based size because the macOS traffic
                     // lights are a static size, and don't scale with the rest of the UI.
                     this.pl(px(80.))
@@ -200,14 +185,15 @@ impl RenderOnce for PlatformTitlebar {
                     this.pl_2()
                 }
             })
-            .bg(self.titlebar_bg)
+            .bg(self.background)
             .content_stretch()
             .child(
                 self.content
+                    .id("titlebar-content")
                     .flex()
                     .flex_row()
+                    .justify_between()
                     .w_full()
-                    .id("titlebar-content")
                     .children(self.children),
             )
             .child(window_controls_right)
@@ -219,6 +205,7 @@ impl InteractiveElement for PlatformTitlebar {
         self.content.interactivity()
     }
 }
+
 impl StatefulInteractiveElement for PlatformTitlebar {}
 
 impl ParentElement for PlatformTitlebar {

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

@@ -10,6 +10,7 @@ mod label;
 mod list;
 mod list_header;
 mod list_item;
+mod platform_titlebar;
 mod tab;
 mod tab_bar;
 mod toggle_button;
@@ -26,6 +27,7 @@ pub use label::*;
 pub use list::*;
 pub use list_header::*;
 pub use list_item::*;
+pub use platform_titlebar::*;
 pub use tab::*;
 pub use tab_bar::*;
 pub use toggle_button::*;

crates/ui/src/components/stories/platform_titlebar.rs 🔗

@@ -0,0 +1,46 @@
+use gpui::Render;
+use story::{StoryContainer, StoryItem, StorySection};
+
+use crate::{prelude::*, PlatformStyle, PlatformTitlebar};
+
+pub struct PlatformTitlebarStory;
+
+impl Render for PlatformTitlebarStory {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        StoryContainer::new(
+            "Platform Titlebar",
+            "crates/ui/src/components/stories/platform_titlebar.rs",
+        )
+        .child(
+            StorySection::new().child(
+                StoryItem::new(
+                    "Default (macOS)",
+                    PlatformTitlebar::new("macos").platform_style(PlatformStyle::MacOs),
+                )
+                .description("")
+                .usage(""),
+            ),
+        )
+        .child(
+            StorySection::new().child(
+                StoryItem::new(
+                    "Default (Linux)",
+                    PlatformTitlebar::new("linux").platform_style(PlatformStyle::Linux),
+                )
+                .description("")
+                .usage(""),
+            ),
+        )
+        .child(
+            StorySection::new().child(
+                StoryItem::new(
+                    "Default (Windows)",
+                    PlatformTitlebar::new("windows").platform_style(PlatformStyle::Windows),
+                )
+                .description("")
+                .usage(""),
+            ),
+        )
+        .into_element()
+    }
+}