diff --git a/assets/icons/generic_close.svg b/assets/icons/generic_close.svg new file mode 100644 index 0000000000..0fd213daf9 --- /dev/null +++ b/assets/icons/generic_close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/generic_maximize.svg b/assets/icons/generic_maximize.svg new file mode 100644 index 0000000000..e44abd8f06 --- /dev/null +++ b/assets/icons/generic_maximize.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/generic_minimize.svg b/assets/icons/generic_minimize.svg new file mode 100644 index 0000000000..4b43cde274 --- /dev/null +++ b/assets/icons/generic_minimize.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/generic_restore.svg b/assets/icons/generic_restore.svg new file mode 100644 index 0000000000..3bf581f2cd --- /dev/null +++ b/assets/icons/generic_restore.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/title_bar/src/platforms.rs b/crates/title_bar/src/platforms.rs index 67e87d45ea..2f0f9a5392 100644 --- a/crates/title_bar/src/platforms.rs +++ b/crates/title_bar/src/platforms.rs @@ -1,3 +1,4 @@ +pub mod platform_generic; pub mod platform_linux; pub mod platform_mac; pub mod platform_windows; diff --git a/crates/title_bar/src/platforms/platform_generic.rs b/crates/title_bar/src/platforms/platform_generic.rs new file mode 100644 index 0000000000..42e32de4e9 --- /dev/null +++ b/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, +} + +impl GenericWindowControls { + pub fn new(close_action: Box) -> 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, + )) + } +} diff --git a/crates/title_bar/src/platforms/platform_linux.rs b/crates/title_bar/src/platforms/platform_linux.rs index 35908b0b7d..c2142fc8d5 100644 --- a/crates/title_bar/src/platforms/platform_linux.rs +++ b/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, } impl LinuxWindowControls { - pub fn new(button_height: Pixels, close_window_action: Box) -> Self { + pub fn new(close_window_action: Box) -> 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, -} - -impl TitlebarButton { - pub fn new( - id: impl Into, - icon: TitlebarButtonType, - hover_background_color: Rgba, - close_window_action: Box, - ) -> 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() } } diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 72ea788f9d..e218305905 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/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 } } diff --git a/crates/title_bar/src/window_controls.rs b/crates/title_bar/src/window_controls.rs new file mode 100644 index 0000000000..5b44f0c446 --- /dev/null +++ b/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) -> 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) -> Self { + self.background_hover = color.into(); + self + } + + #[allow(unused)] + /// Sets the color of the icon. + pub fn icon(mut self, color: impl Into) -> 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) -> Self { + self.icon_hover = color.into(); + self + } +} + +#[derive(IntoElement)] +pub struct WindowControl { + id: ElementId, + icon: WindowControlType, + style: WindowControlStyle, + close_action: Option>, +} + +impl WindowControl { + pub fn new(id: impl Into, 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, + icon: WindowControlType, + close_action: Box, + 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, + 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(), + ), + } + }) + } +} diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index d1fe7e8979..c9135c2883 100644 --- a/crates/ui/src/components/icon.rs +++ b/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", diff --git a/crates/ui/src/styles/color.rs b/crates/ui/src/styles/color.rs index 0ccafdc1c6..b35728e478 100644 --- a/crates/ui/src/styles/color.rs +++ b/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, } } }