Update Platform Controls (#13751)

Nate Butler created

Continuing from #13597, this PR refactors platform controls to extract a
generic set of platform controls that can be used for any platform that
does not define it's own/we don't use the system ones.

In the future, these controls will likely be used as a fallback on
windows as well when the windows icon font isn't available.

Release Notes:

- Added updated window controls on Linux

Change summary

assets/icons/generic_close.svg                     |   4 
assets/icons/generic_maximize.svg                  |   3 
assets/icons/generic_minimize.svg                  |   3 
assets/icons/generic_restore.svg                   |   4 
crates/title_bar/src/platforms.rs                  |   1 
crates/title_bar/src/platforms/platform_generic.rs |  47 ++++
crates/title_bar/src/platforms/platform_linux.rs   | 131 ------------
crates/title_bar/src/title_bar.rs                  |  54 +++--
crates/title_bar/src/window_controls.rs            | 164 ++++++++++++++++
crates/ui/src/components/icon.rs                   |   8 
crates/ui/src/styles/color.rs                      |   2 
11 files changed, 272 insertions(+), 149 deletions(-)

Detailed changes

assets/icons/generic_close.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.5 4.5L4.5 11.5" stroke="black" stroke-linecap="square" stroke-linejoin="round"/>
+<path d="M4.5 4.5L11.5 11.5" stroke="black" stroke-linecap="square" stroke-linejoin="round"/>
+</svg>

assets/icons/generic_maximize.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.5 4.5H4.5V11.5H11.5V4.5Z" stroke="#FBF1C7"/>
+</svg>

assets/icons/generic_minimize.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4 8H12" stroke="black"/>
+</svg>

assets/icons/generic_restore.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.5 6.5H3.5V12.5H9.5V6.5Z" stroke="#FBF1C7"/>
+<path d="M10 8.5L12.5 8.5L12.5 3.5L7.5 3.5L7.5 6" stroke="#FBF1C7"/>
+</svg>

crates/title_bar/src/platforms/platform_generic.rs 🔗

@@ -0,0 +1,47 @@
+use gpui::{prelude::*, Action};
+
+use ui::prelude::*;
+
+use crate::window_controls::{WindowControl, WindowControlType};
+
+#[derive(IntoElement)]
+pub struct GenericWindowControls {
+    close_window_action: Box<dyn Action>,
+}
+
+impl GenericWindowControls {
+    pub fn new(close_action: Box<dyn Action>) -> Self {
+        Self {
+            close_window_action: close_action,
+        }
+    }
+}
+
+impl RenderOnce for GenericWindowControls {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        h_flex()
+            .id("generic-window-controls")
+            .px_3()
+            .gap_1p5()
+            .child(WindowControl::new(
+                "minimize",
+                WindowControlType::Minimize,
+                cx,
+            ))
+            .child(WindowControl::new(
+                "maximize-or-restore",
+                if cx.is_maximized() {
+                    WindowControlType::Restore
+                } else {
+                    WindowControlType::Maximize
+                },
+                cx,
+            ))
+            .child(WindowControl::new_close(
+                "close",
+                WindowControlType::Close,
+                self.close_window_action,
+                cx,
+            ))
+    }
+}

crates/title_bar/src/platforms/platform_linux.rs 🔗

@@ -1,145 +1,24 @@
-use gpui::{prelude::*, Action, Rgba, WindowAppearance};
+use gpui::{prelude::*, Action};
 
 use ui::prelude::*;
 
+use super::platform_generic::GenericWindowControls;
+
 #[derive(IntoElement)]
 pub struct LinuxWindowControls {
-    button_height: Pixels,
     close_window_action: Box<dyn Action>,
 }
 
 impl LinuxWindowControls {
-    pub fn new(button_height: Pixels, close_window_action: Box<dyn Action>) -> Self {
+    pub fn new(close_window_action: Box<dyn Action>) -> Self {
         Self {
-            button_height,
             close_window_action,
         }
     }
 }
 
 impl RenderOnce for LinuxWindowControls {
-    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        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,
-            },
-        };
-
-        div()
-            .id("linux-window-controls")
-            .flex()
-            .flex_row()
-            .justify_center()
-            .content_stretch()
-            .max_h(self.button_height)
-            .min_h(self.button_height)
-            .child(TitlebarButton::new(
-                "minimize",
-                TitlebarButtonType::Minimize,
-                button_hover_color,
-                self.close_window_action.boxed_clone(),
-            ))
-            .child(TitlebarButton::new(
-                "maximize-or-restore",
-                if cx.is_maximized() {
-                    TitlebarButtonType::Restore
-                } else {
-                    TitlebarButtonType::Maximize
-                },
-                button_hover_color,
-                self.close_window_action.boxed_clone(),
-            ))
-            .child(TitlebarButton::new(
-                "close",
-                TitlebarButtonType::Close,
-                close_button_hover_color,
-                self.close_window_action,
-            ))
-    }
-}
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-enum TitlebarButtonType {
-    Minimize,
-    Restore,
-    Maximize,
-    Close,
-}
-
-#[derive(IntoElement)]
-struct TitlebarButton {
-    id: ElementId,
-    icon: TitlebarButtonType,
-    hover_background_color: Rgba,
-    close_window_action: Box<dyn Action>,
-}
-
-impl TitlebarButton {
-    pub fn new(
-        id: impl Into<ElementId>,
-        icon: TitlebarButtonType,
-        hover_background_color: Rgba,
-        close_window_action: Box<dyn Action>,
-    ) -> Self {
-        Self {
-            id: id.into(),
-            icon,
-            hover_background_color,
-            close_window_action,
-        }
-    }
-}
-
-impl RenderOnce for TitlebarButton {
     fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
-        let width = px(36.);
-
-        h_flex()
-            .id(self.id)
-            .justify_center()
-            .content_center()
-            .w(width)
-            .h_full()
-            .hover(|style| style.bg(self.hover_background_color))
-            .active(|style| {
-                let mut active_color = self.hover_background_color;
-                active_color.a *= 0.2;
-
-                style.bg(active_color)
-            })
-            .child(Icon::new(match self.icon {
-                TitlebarButtonType::Minimize => IconName::Dash,
-                TitlebarButtonType::Restore => IconName::Minimize,
-                TitlebarButtonType::Maximize => IconName::Maximize,
-                TitlebarButtonType::Close => IconName::Close,
-            }))
-            .on_mouse_move(|_, cx| cx.stop_propagation())
-            .on_click(move |_, cx| {
-                cx.stop_propagation();
-                match self.icon {
-                    TitlebarButtonType::Minimize => cx.minimize_window(),
-                    TitlebarButtonType::Restore => cx.zoom_window(),
-                    TitlebarButtonType::Maximize => cx.zoom_window(),
-                    TitlebarButtonType::Close => {
-                        cx.dispatch_action(self.close_window_action.boxed_clone())
-                    }
-                }
-            })
+        GenericWindowControls::new(self.close_window_action.boxed_clone()).into_any_element()
     }
 }

crates/title_bar/src/title_bar.rs 🔗

@@ -1,6 +1,7 @@
 mod call_controls;
 mod collab;
 mod platforms;
+mod window_controls;
 
 use crate::platforms::{platform_linux, platform_mac, platform_windows};
 use auto_update::AutoUpdateStatus;
@@ -70,9 +71,10 @@ impl Render for TitleBar {
         let close_action = Box::new(workspace::CloseWindow);
 
         let platform_supported = cfg!(target_os = "macos");
+
         let height = Self::height(cx);
 
-        h_flex()
+        let mut title_bar = h_flex()
             .id("titlebar")
             .w_full()
             .pt(Self::top_padding(cx))
@@ -371,28 +373,34 @@ impl Render for TitleBar {
                                     }
                                 }),
                         )
-            )
-            .when(
-                self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(),
-                |title_bar| title_bar.child(platform_windows::WindowsWindowControls::new(height)),
-            )
-            .when(
-                self.platform_style == PlatformStyle::Linux
-                    && !cx.is_fullscreen()
-                    && cx.should_render_window_controls(),
-                |title_bar| {
-                    title_bar
-                        .child(platform_linux::LinuxWindowControls::new(height, close_action))
-                        .on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
-                            cx.show_window_menu(ev.position)
-                        })
-                        .on_mouse_move(move |ev, cx| {
-                            if ev.dragging() {
-                                cx.start_system_move();
-                            }
-                        })
-                },
-            )
+            );
+
+        // Windows Window Controls
+        title_bar = title_bar.when(
+            self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(),
+            |title_bar| title_bar.child(platform_windows::WindowsWindowControls::new(height)),
+        );
+
+        // Linux Window Controls
+        title_bar = title_bar.when(
+            self.platform_style == PlatformStyle::Linux
+                && !cx.is_fullscreen()
+                && cx.should_render_window_controls(),
+            |title_bar| {
+                title_bar
+                    .child(platform_linux::LinuxWindowControls::new(close_action))
+                    .on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
+                        cx.show_window_menu(ev.position)
+                    })
+                    .on_mouse_move(move |ev, cx| {
+                        if ev.dragging() {
+                            cx.start_system_move();
+                        }
+                    })
+            },
+        );
+
+        title_bar
     }
 }
 

crates/title_bar/src/window_controls.rs 🔗

@@ -0,0 +1,164 @@
+use gpui::{svg, Action, Hsla};
+use ui::prelude::*;
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum WindowControlType {
+    Minimize,
+    Restore,
+    Maximize,
+    Close,
+}
+
+impl WindowControlType {
+    /// Returns the icon name for the window control type.
+    ///
+    /// Will take a [PlatformStyle] in the future to return a different
+    /// icon name based on the platform.
+    pub fn icon(&self) -> IconName {
+        match self {
+            WindowControlType::Minimize => IconName::GenericMinimize,
+            WindowControlType::Restore => IconName::GenericRestore,
+            WindowControlType::Maximize => IconName::GenericMaximize,
+            WindowControlType::Close => IconName::GenericClose,
+        }
+    }
+}
+
+#[allow(unused)]
+pub struct WindowControlStyle {
+    background: Hsla,
+    background_hover: Hsla,
+    icon: Hsla,
+    icon_hover: Hsla,
+}
+
+impl WindowControlStyle {
+    pub fn default(cx: &WindowContext) -> Self {
+        let colors = cx.theme().colors();
+
+        Self {
+            background: colors.ghost_element_background,
+            background_hover: colors.ghost_element_background,
+            icon: colors.icon,
+            icon_hover: colors.icon_muted,
+        }
+    }
+
+    #[allow(unused)]
+    /// Sets the background color of the control.
+    pub fn background(mut self, color: impl Into<Hsla>) -> Self {
+        self.background = color.into();
+        self
+    }
+
+    #[allow(unused)]
+    /// Sets the background color of the control when hovered.
+    pub fn background_hover(mut self, color: impl Into<Hsla>) -> Self {
+        self.background_hover = color.into();
+        self
+    }
+
+    #[allow(unused)]
+    /// Sets the color of the icon.
+    pub fn icon(mut self, color: impl Into<Hsla>) -> Self {
+        self.icon = color.into();
+        self
+    }
+
+    #[allow(unused)]
+    /// Sets the color of the icon when hovered.
+    pub fn icon_hover(mut self, color: impl Into<Hsla>) -> Self {
+        self.icon_hover = color.into();
+        self
+    }
+}
+
+#[derive(IntoElement)]
+pub struct WindowControl {
+    id: ElementId,
+    icon: WindowControlType,
+    style: WindowControlStyle,
+    close_action: Option<Box<dyn Action>>,
+}
+
+impl WindowControl {
+    pub fn new(id: impl Into<ElementId>, icon: WindowControlType, cx: &WindowContext) -> Self {
+        let style = WindowControlStyle::default(cx);
+
+        Self {
+            id: id.into(),
+            icon,
+            style,
+            close_action: None,
+        }
+    }
+
+    pub fn new_close(
+        id: impl Into<ElementId>,
+        icon: WindowControlType,
+        close_action: Box<dyn Action>,
+        cx: &WindowContext,
+    ) -> Self {
+        let style = WindowControlStyle::default(cx);
+
+        Self {
+            id: id.into(),
+            icon,
+            style,
+            close_action: Some(close_action.boxed_clone()),
+        }
+    }
+
+    #[allow(unused)]
+    pub fn custom_style(
+        id: impl Into<ElementId>,
+        icon: WindowControlType,
+        style: WindowControlStyle,
+    ) -> Self {
+        Self {
+            id: id.into(),
+            icon,
+            style,
+            close_action: None,
+        }
+    }
+}
+
+impl RenderOnce for WindowControl {
+    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+        let icon = svg()
+            .size_5()
+            .flex_none()
+            .path(self.icon.icon().path())
+            .text_color(self.style.icon)
+            .group_hover("", |this| this.text_color(self.style.icon_hover));
+
+        h_flex()
+            .id(self.id)
+            .group("")
+            .cursor_pointer()
+            .justify_center()
+            .content_center()
+            .rounded_md()
+            .w_5()
+            .h_5()
+            .hover(|this| this.bg(self.style.background_hover))
+            .active(|this| this.bg(self.style.background_hover))
+            .child(icon)
+            .on_mouse_move(|_, cx| cx.stop_propagation())
+            .on_click(move |_, cx| {
+                cx.stop_propagation();
+                match self.icon {
+                    WindowControlType::Minimize => cx.minimize_window(),
+                    WindowControlType::Restore => cx.zoom_window(),
+                    WindowControlType::Maximize => cx.zoom_window(),
+                    WindowControlType::Close => cx.dispatch_action(
+                        self.close_action
+                            .as_ref()
+                            .expect("Use WindowControl::new_close() for close control.")
+                            .boxed_clone(),
+                    ),
+                }
+            })
+    }
+}

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

@@ -146,6 +146,10 @@ pub enum IconName {
     FontSize,
     FontWeight,
     Github,
+    GenericMinimize,
+    GenericMaximize,
+    GenericClose,
+    GenericRestore,
     Hash,
     HistoryRerun,
     Indicator,
@@ -290,6 +294,10 @@ impl IconName {
             IconName::FontSize => "icons/font_size.svg",
             IconName::FontWeight => "icons/font_weight.svg",
             IconName::Github => "icons/github.svg",
+            IconName::GenericMinimize => "icons/generic_minimize.svg",
+            IconName::GenericMaximize => "icons/generic_maximize.svg",
+            IconName::GenericClose => "icons/generic_close.svg",
+            IconName::GenericRestore => "icons/generic_restore.svg",
             IconName::Hash => "icons/hash.svg",
             IconName::HistoryRerun => "icons/history_rerun.svg",
             IconName::Indicator => "icons/indicator.svg",

crates/ui/src/styles/color.rs 🔗

@@ -23,6 +23,7 @@ pub enum Color {
     Selected,
     Success,
     Warning,
+    Custom(Hsla),
 }
 
 impl Color {
@@ -46,6 +47,7 @@ impl Color {
             Color::Selected => cx.theme().colors().text_accent,
             Color::Success => cx.theme().status().success,
             Color::Warning => cx.theme().status().warning,
+            Color::Custom(color) => *color,
         }
     }
 }