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,
}
}
}