diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs
index 9ab4c94ff1..9c210871da 100644
--- a/crates/collab_ui/src/collab_titlebar_item.rs
+++ b/crates/collab_ui/src/collab_titlebar_item.rs
@@ -14,7 +14,7 @@ use std::sync::Arc;
use theme::ActiveTheme;
use ui::{
h_flex, popover_menu, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike,
- ButtonStyle, ContextMenu, Icon, IconButton, IconName, PlatformTitlebar, TintColor, Tooltip,
+ ButtonStyle, ContextMenu, Icon, IconButton, IconName, TintColor, TitleBar, Tooltip,
};
use util::ResultExt;
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
@@ -58,8 +58,7 @@ impl Render for CollabTitlebarItem {
let project_id = self.project.read(cx).remote_id();
let workspace = self.workspace.upgrade();
- PlatformTitlebar::new("collab-titlebar")
- .background(cx.theme().colors().title_bar_background)
+ TitleBar::new("collab-titlebar")
// note: on windows titlebar behaviour is handled by the platform implementation
.when(cfg!(not(windows)), |this| {
this.on_click(|event, cx| {
diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs
index bbda1119ec..c238542478 100644
--- a/crates/storybook/src/story_selector.rs
+++ b/crates/storybook/src/story_selector.rs
@@ -29,10 +29,10 @@ pub enum ComponentStory {
ListHeader,
ListItem,
OverflowScroll,
- PlatformTitlebar,
Scroll,
Tab,
TabBar,
+ TitleBar,
ToggleButton,
Text,
ViewportUnits,
@@ -61,11 +61,11 @@ 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(),
Self::TabBar => cx.new_view(|_| ui::TabBarStory).into(),
+ Self::TitleBar => cx.new_view(|_| ui::TitleBarStory).into(),
Self::ToggleButton => cx.new_view(|_| ui::ToggleButtonStory).into(),
Self::ViewportUnits => cx.new_view(|_| crate::stories::ViewportUnitsStory).into(),
Self::Picker => PickerStory::new(cx).into(),
diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs
index c78774f2b8..b3fc5dd2ee 100644
--- a/crates/ui/src/components.rs
+++ b/crates/ui/src/components.rs
@@ -9,13 +9,13 @@ mod indicator;
mod keybinding;
mod label;
mod list;
-mod platform_titlebar;
mod popover;
mod popover_menu;
mod right_click_menu;
mod stack;
mod tab;
mod tab_bar;
+mod title_bar;
mod tooltip;
#[cfg(feature = "stories")]
@@ -32,13 +32,13 @@ pub use indicator::*;
pub use keybinding::*;
pub use label::*;
pub use list::*;
-pub use platform_titlebar::*;
pub use popover::*;
pub use popover_menu::*;
pub use right_click_menu::*;
pub use stack::*;
pub use tab::*;
pub use tab_bar::*;
+pub use title_bar::*;
pub use tooltip::*;
#[cfg(feature = "stories")]
diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs
index e5ca6f62f7..d595c6c4d0 100644
--- a/crates/ui/src/components/keybinding.rs
+++ b/crates/ui/src/components/keybinding.rs
@@ -1,30 +1,6 @@
use crate::{h_flex, prelude::*, Icon, IconName, IconSize};
use gpui::{relative, Action, FocusHandle, IntoElement, Keystroke};
-/// The way a [`KeyBinding`] should be displayed.
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub enum KeyBindingDisplay {
- /// Display in macOS style.
- Mac,
- /// Display in Linux style.
- Linux,
- /// Display in Windows style.
- Windows,
-}
-
-impl KeyBindingDisplay {
- /// Returns the [`KeyBindingDisplay`] for the current platform.
- pub const fn platform() -> Self {
- if cfg!(target_os = "linux") {
- KeyBindingDisplay::Linux
- } else if cfg!(target_os = "windows") {
- KeyBindingDisplay::Windows
- } else {
- KeyBindingDisplay::Mac
- }
- }
-}
-
#[derive(IntoElement, Clone)]
pub struct KeyBinding {
/// A keybinding consists of a key and a set of modifier keys.
@@ -33,8 +9,8 @@ pub struct KeyBinding {
/// This should always contain at least one element.
key_binding: gpui::KeyBinding,
- /// How keybindings should be displayed.
- display: KeyBindingDisplay,
+ /// The [`PlatformStyle`] to use when displaying this keybinding.
+ platform_style: PlatformStyle,
}
impl KeyBinding {
@@ -76,13 +52,13 @@ impl KeyBinding {
pub fn new(key_binding: gpui::KeyBinding) -> Self {
Self {
key_binding,
- display: KeyBindingDisplay::platform(),
+ platform_style: PlatformStyle::platform(),
}
}
- /// Sets how this [`KeyBinding`] should be displayed.
- pub fn display(mut self, display: KeyBindingDisplay) -> Self {
- self.display = display;
+ /// Sets the [`PlatformStyle`] for this [`KeyBinding`].
+ pub fn platform_style(mut self, platform_style: PlatformStyle) -> Self {
+ self.platform_style = platform_style;
self
}
}
@@ -97,43 +73,49 @@ impl RenderOnce for KeyBinding {
h_flex()
.flex_none()
- .map(|el| match self.display {
- KeyBindingDisplay::Mac => el.gap_0p5(),
- KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => el,
+ .map(|el| match self.platform_style {
+ PlatformStyle::Mac => el.gap_0p5(),
+ PlatformStyle::Linux | PlatformStyle::Windows => el,
})
.p_0p5()
.rounded_sm()
.text_color(cx.theme().colors().text_muted)
- .when(keystroke.modifiers.function, |el| match self.display {
- KeyBindingDisplay::Mac => el.child(Key::new("fn")),
- KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
- el.child(Key::new("Fn")).child(Key::new("+"))
+ .when(keystroke.modifiers.function, |el| {
+ match self.platform_style {
+ PlatformStyle::Mac => el.child(Key::new("fn")),
+ PlatformStyle::Linux | PlatformStyle::Windows => {
+ el.child(Key::new("Fn")).child(Key::new("+"))
+ }
}
})
- .when(keystroke.modifiers.control, |el| match self.display {
- KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Control)),
- KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
- el.child(Key::new("Ctrl")).child(Key::new("+"))
+ .when(keystroke.modifiers.control, |el| {
+ match self.platform_style {
+ PlatformStyle::Mac => el.child(KeyIcon::new(IconName::Control)),
+ PlatformStyle::Linux | PlatformStyle::Windows => {
+ el.child(Key::new("Ctrl")).child(Key::new("+"))
+ }
}
})
- .when(keystroke.modifiers.alt, |el| match self.display {
- KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Option)),
- KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
+ .when(keystroke.modifiers.alt, |el| match self.platform_style {
+ PlatformStyle::Mac => el.child(KeyIcon::new(IconName::Option)),
+ PlatformStyle::Linux | PlatformStyle::Windows => {
el.child(Key::new("Alt")).child(Key::new("+"))
}
})
- .when(keystroke.modifiers.command, |el| match self.display {
- KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Command)),
- KeyBindingDisplay::Linux => {
- el.child(Key::new("Super")).child(Key::new("+"))
- }
- KeyBindingDisplay::Windows => {
- el.child(Key::new("Win")).child(Key::new("+"))
+ .when(keystroke.modifiers.command, |el| {
+ match self.platform_style {
+ PlatformStyle::Mac => el.child(KeyIcon::new(IconName::Command)),
+ PlatformStyle::Linux => {
+ el.child(Key::new("Super")).child(Key::new("+"))
+ }
+ PlatformStyle::Windows => {
+ el.child(Key::new("Win")).child(Key::new("+"))
+ }
}
})
- .when(keystroke.modifiers.shift, |el| match self.display {
- KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Shift)),
- KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
+ .when(keystroke.modifiers.shift, |el| match self.platform_style {
+ PlatformStyle::Mac => el.child(KeyIcon::new(IconName::Shift)),
+ PlatformStyle::Linux | PlatformStyle::Windows => {
el.child(Key::new("Shift")).child(Key::new("+"))
}
})
diff --git a/crates/ui/src/components/platform_titlebar.rs b/crates/ui/src/components/platform_titlebar.rs
deleted file mode 100644
index 9d889d01df..0000000000
--- a/crates/ui/src/components/platform_titlebar.rs
+++ /dev/null
@@ -1,215 +0,0 @@
-use gpui::{transparent_black, AnyElement, Fill, Interactivity, Rgba, Stateful, WindowAppearance};
-use smallvec::SmallVec;
-
-use crate::prelude::*;
-
-pub enum PlatformStyle {
- Linux,
- Windows,
- MacOs,
-}
-
-pub fn titlebar_height(cx: &mut WindowContext) -> Pixels {
- (1.75 * cx.rem_size()).max(px(32.))
-}
-
-impl PlatformStyle {
- pub fn platform() -> Self {
- if cfg!(target_os = "windows") {
- Self::Windows
- } else if cfg!(target_os = "macos") {
- Self::MacOs
- } else {
- Self::Linux
- }
- }
-
- pub fn windows(&self) -> bool {
- matches!(self, Self::Windows)
- }
-
- pub fn macos(&self) -> bool {
- matches!(self, Self::MacOs)
- }
-}
-
-#[derive(IntoElement)]
-pub struct PlatformTitlebar {
- platform: PlatformStyle,
- background: Fill,
- content: Stateful
,
- children: SmallVec<[AnyElement; 2]>,
-}
-
-impl PlatformTitlebar {
- pub fn new(id: impl Into
) -> Self {
- Self {
- platform: PlatformStyle::platform(),
- background: transparent_black().into(),
- content: div().id(id.into()),
- children: SmallVec::new(),
- }
- }
-
- /// 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(mut self, fill: F) -> Self
- where
- F: Into,
- 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
- px(8.0)
- } else {
- px(0.0)
- }
- }
-
- fn windows_caption_button_width(_cx: &WindowContext) -> Pixels {
- // todo(windows): get padding from win32 api, need HWND from window context somehow
- // should be GetSystemMetricsForDpi(SM_CXSIZE, dpi)
- px(36.0)
- }
-
- fn render_window_controls_right(&self, cx: &mut WindowContext) -> impl Element {
- 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 {
- 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)
- }
-
- const MINIMIZE_ICON: &str = "\u{e921}";
- const RESTORE_ICON: &str = "\u{e923}";
- const MAXIMIZE_ICON: &str = "\u{e922}";
- const CLOSE_ICON: &str = "\u{e8bb}";
-
- 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.top_padding(cx);
- let window_controls_right = self.render_window_controls_right(cx);
-
- h_flex()
- .id("titlebar")
- .w_full()
- .pt(titlebar_top_padding)
- .h(titlebar_height)
- .map(|this| {
- if cx.is_fullscreen() {
- this.pl_2()
- } 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.))
- } else {
- this.pl_2()
- }
- })
- .bg(self.background)
- .content_stretch()
- .child(
- self.content
- .id("titlebar-content")
- .flex()
- .flex_row()
- .justify_between()
- .w_full()
- .children(self.children),
- )
- .child(window_controls_right)
- }
-}
-
-impl InteractiveElement for PlatformTitlebar {
- fn interactivity(&mut self) -> &mut Interactivity {
- self.content.interactivity()
- }
-}
-
-impl StatefulInteractiveElement for PlatformTitlebar {}
-
-impl ParentElement for PlatformTitlebar {
- fn extend(&mut self, elements: impl Iterator
- ) {
- self.children.extend(elements)
- }
-}
diff --git a/crates/ui/src/components/stories.rs b/crates/ui/src/components/stories.rs
index 8764b39698..eb84ed3ccb 100644
--- a/crates/ui/src/components/stories.rs
+++ b/crates/ui/src/components/stories.rs
@@ -10,9 +10,9 @@ mod label;
mod list;
mod list_header;
mod list_item;
-mod platform_titlebar;
mod tab;
mod tab_bar;
+mod title_bar;
mod toggle_button;
pub use avatar::*;
@@ -27,7 +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 title_bar::*;
pub use toggle_button::*;
diff --git a/crates/ui/src/components/stories/keybinding.rs b/crates/ui/src/components/stories/keybinding.rs
index 48be6ad82e..e6901887e4 100644
--- a/crates/ui/src/components/stories/keybinding.rs
+++ b/crates/ui/src/components/stories/keybinding.rs
@@ -3,7 +3,7 @@ use gpui::Render;
use itertools::Itertools;
use story::{Story, StoryContainer};
-use crate::{prelude::*, KeyBinding, KeyBindingDisplay};
+use crate::{prelude::*, KeyBinding};
pub struct KeybindingStory;
@@ -59,18 +59,22 @@ impl Render for KeybindingStory {
.child(KeyBinding::new(binding("ctrl-a shift-z")))
.child(KeyBinding::new(binding("fn-s")))
.child(Story::label("Single Key with All Modifiers (Linux)"))
- .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")).display(KeyBindingDisplay::Linux))
+ .child(
+ KeyBinding::new(binding("ctrl-alt-cmd-shift-z")).platform_style(PlatformStyle::Linux),
+ )
.child(Story::label("Chord (Linux)"))
- .child(KeyBinding::new(binding("a z")).display(KeyBindingDisplay::Linux))
+ .child(KeyBinding::new(binding("a z")).platform_style(PlatformStyle::Linux))
.child(Story::label("Chord with Modifier (Linux)"))
- .child(KeyBinding::new(binding("ctrl-a shift-z")).display(KeyBindingDisplay::Linux))
- .child(KeyBinding::new(binding("fn-s")).display(KeyBindingDisplay::Linux))
+ .child(KeyBinding::new(binding("ctrl-a shift-z")).platform_style(PlatformStyle::Linux))
+ .child(KeyBinding::new(binding("fn-s")).platform_style(PlatformStyle::Linux))
.child(Story::label("Single Key with All Modifiers (Windows)"))
- .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")).display(KeyBindingDisplay::Windows))
+ .child(
+ KeyBinding::new(binding("ctrl-alt-cmd-shift-z")).platform_style(PlatformStyle::Windows),
+ )
.child(Story::label("Chord (Windows)"))
- .child(KeyBinding::new(binding("a z")).display(KeyBindingDisplay::Windows))
+ .child(KeyBinding::new(binding("a z")).platform_style(PlatformStyle::Windows))
.child(Story::label("Chord with Modifier (Windows)"))
- .child(KeyBinding::new(binding("ctrl-a shift-z")).display(KeyBindingDisplay::Windows))
- .child(KeyBinding::new(binding("fn-s")).display(KeyBindingDisplay::Windows))
+ .child(KeyBinding::new(binding("ctrl-a shift-z")).platform_style(PlatformStyle::Windows))
+ .child(KeyBinding::new(binding("fn-s")).platform_style(PlatformStyle::Windows))
}
}
diff --git a/crates/ui/src/components/stories/platform_titlebar.rs b/crates/ui/src/components/stories/platform_titlebar.rs
deleted file mode 100644
index d2eec79b80..0000000000
--- a/crates/ui/src/components/stories/platform_titlebar.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-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) -> 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()
- }
-}
diff --git a/crates/ui/src/components/stories/title_bar.rs b/crates/ui/src/components/stories/title_bar.rs
new file mode 100644
index 0000000000..78e0d85e30
--- /dev/null
+++ b/crates/ui/src/components/stories/title_bar.rs
@@ -0,0 +1,56 @@
+use gpui::Render;
+use story::{StoryContainer, StoryItem, StorySection};
+
+use crate::{prelude::*, PlatformStyle, TitleBar};
+
+pub struct TitleBarStory;
+
+impl Render for TitleBarStory {
+ fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement {
+ fn add_sample_children(titlebar: TitleBar) -> TitleBar {
+ titlebar
+ .child(div().size_2().bg(gpui::red()))
+ .child(div().size_2().bg(gpui::blue()))
+ .child(div().size_2().bg(gpui::green()))
+ }
+
+ StoryContainer::new("TitleBar", "crates/ui/src/components/stories/title_bar.rs")
+ .child(
+ StorySection::new().child(
+ StoryItem::new(
+ "Default (macOS)",
+ TitleBar::new("macos")
+ .platform_style(PlatformStyle::Mac)
+ .map(add_sample_children),
+ )
+ .description("")
+ .usage(""),
+ ),
+ )
+ .child(
+ StorySection::new().child(
+ StoryItem::new(
+ "Default (Linux)",
+ TitleBar::new("linux")
+ .platform_style(PlatformStyle::Linux)
+ .map(add_sample_children),
+ )
+ .description("")
+ .usage(""),
+ ),
+ )
+ .child(
+ StorySection::new().child(
+ StoryItem::new(
+ "Default (Windows)",
+ TitleBar::new("windows")
+ .platform_style(PlatformStyle::Windows)
+ .map(add_sample_children),
+ )
+ .description("")
+ .usage(""),
+ ),
+ )
+ .into_element()
+ }
+}
diff --git a/crates/ui/src/components/title_bar.rs b/crates/ui/src/components/title_bar.rs
new file mode 100644
index 0000000000..2eb181f9b2
--- /dev/null
+++ b/crates/ui/src/components/title_bar.rs
@@ -0,0 +1,4 @@
+mod title_bar;
+mod windows_window_controls;
+
+pub use title_bar::*;
diff --git a/crates/ui/src/components/title_bar/title_bar.rs b/crates/ui/src/components/title_bar/title_bar.rs
new file mode 100644
index 0000000000..d6b3463783
--- /dev/null
+++ b/crates/ui/src/components/title_bar/title_bar.rs
@@ -0,0 +1,96 @@
+use gpui::{AnyElement, Interactivity, Stateful};
+use smallvec::SmallVec;
+
+use crate::components::title_bar::windows_window_controls::WindowsWindowControls;
+use crate::prelude::*;
+
+#[derive(IntoElement)]
+pub struct TitleBar {
+ platform_style: PlatformStyle,
+ content: Stateful
,
+ children: SmallVec<[AnyElement; 2]>,
+}
+
+impl TitleBar {
+ pub fn height(cx: &mut WindowContext) -> Pixels {
+ (1.75 * cx.rem_size()).max(px(32.))
+ }
+
+ pub fn new(id: impl Into) -> Self {
+ Self {
+ platform_style: PlatformStyle::platform(),
+ content: div().id(id.into()),
+ children: SmallVec::new(),
+ }
+ }
+
+ /// Sets the platform style.
+ pub fn platform_style(mut self, style: PlatformStyle) -> Self {
+ self.platform_style = style;
+ self
+ }
+
+ fn top_padding(&self, cx: &WindowContext) -> Pixels {
+ if self.platform_style == PlatformStyle::Windows && cx.is_maximized() {
+ // todo(windows): get padding from win32 api, need HWND from window context somehow
+ // should be GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) * 2
+ px(8.)
+ } else {
+ px(0.)
+ }
+ }
+}
+
+impl InteractiveElement for TitleBar {
+ fn interactivity(&mut self) -> &mut Interactivity {
+ self.content.interactivity()
+ }
+}
+
+impl StatefulInteractiveElement for TitleBar {}
+
+impl ParentElement for TitleBar {
+ fn extend(&mut self, elements: impl Iterator- ) {
+ self.children.extend(elements)
+ }
+}
+
+impl RenderOnce for TitleBar {
+ fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+ let height = Self::height(cx);
+ let top_padding = self.top_padding(cx);
+
+ h_flex()
+ .id("titlebar")
+ .w_full()
+ .pt(top_padding)
+ .h(height)
+ .map(|this| {
+ if cx.is_fullscreen() {
+ this.pl_2()
+ } else if self.platform_style == PlatformStyle::Mac {
+ // 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.))
+ } else {
+ this.pl_2()
+ }
+ })
+ .bg(cx.theme().colors().title_bar_background)
+ .content_stretch()
+ .child(
+ self.content
+ .id("titlebar-content")
+ .flex()
+ .flex_row()
+ .justify_between()
+ .w_full()
+ .children(self.children),
+ )
+ .when(self.platform_style == PlatformStyle::Windows, |title_bar| {
+ let button_height = Self::height(cx) - top_padding;
+
+ title_bar.child(WindowsWindowControls::new(button_height))
+ })
+ }
+}
diff --git a/crates/ui/src/components/title_bar/windows_window_controls.rs b/crates/ui/src/components/title_bar/windows_window_controls.rs
new file mode 100644
index 0000000000..504a849d20
--- /dev/null
+++ b/crates/ui/src/components/title_bar/windows_window_controls.rs
@@ -0,0 +1,127 @@
+use gpui::{prelude::*, Rgba, WindowAppearance};
+
+use crate::prelude::*;
+
+#[derive(IntoElement)]
+pub struct WindowsWindowControls {
+ button_height: Pixels,
+}
+
+impl WindowsWindowControls {
+ pub fn new(button_height: Pixels) -> Self {
+ Self { button_height }
+ }
+}
+
+impl RenderOnce for WindowsWindowControls {
+ 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("windows-window-controls")
+ .flex()
+ .flex_row()
+ .justify_center()
+ .content_stretch()
+ .max_h(self.button_height)
+ .min_h(self.button_height)
+ .font("Segoe Fluent Icons")
+ .text_size(px(10.0))
+ .child(WindowsCaptionButton::new(
+ "minimize",
+ WindowsCaptionButtonIcon::Minimize,
+ button_hover_color,
+ ))
+ .child(WindowsCaptionButton::new(
+ "maximize-or-restore",
+ if cx.is_maximized() {
+ WindowsCaptionButtonIcon::Restore
+ } else {
+ WindowsCaptionButtonIcon::Maximize
+ },
+ button_hover_color,
+ ))
+ .child(WindowsCaptionButton::new(
+ "close",
+ WindowsCaptionButtonIcon::Close,
+ close_button_hover_color,
+ ))
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+enum WindowsCaptionButtonIcon {
+ Minimize,
+ Restore,
+ Maximize,
+ Close,
+}
+
+#[derive(IntoElement)]
+struct WindowsCaptionButton {
+ id: ElementId,
+ icon: WindowsCaptionButtonIcon,
+ hover_background_color: Rgba,
+}
+
+impl WindowsCaptionButton {
+ pub fn new(
+ id: impl Into,
+ icon: WindowsCaptionButtonIcon,
+ hover_background_color: Rgba,
+ ) -> Self {
+ Self {
+ id: id.into(),
+ icon,
+ hover_background_color,
+ }
+ }
+}
+
+impl RenderOnce for WindowsCaptionButton {
+ fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+ // todo(windows): get padding from win32 api, need HWND from window context somehow
+ // should be GetSystemMetricsForDpi(SM_CXSIZE, dpi)
+ let width = px(36.0);
+
+ 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(match self.icon {
+ WindowsCaptionButtonIcon::Minimize => "\u{e921}",
+ WindowsCaptionButtonIcon::Restore => "\u{e923}",
+ WindowsCaptionButtonIcon::Maximize => "\u{e922}",
+ WindowsCaptionButtonIcon::Close => "\u{e8bb}",
+ })
+ }
+}
diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs
index dd69e914cf..5cf56bc58d 100644
--- a/crates/ui/src/prelude.rs
+++ b/crates/ui/src/prelude.rs
@@ -11,7 +11,7 @@ pub use crate::clickable::*;
pub use crate::disableable::*;
pub use crate::fixed::*;
pub use crate::selectable::*;
-pub use crate::styles::{rems_from_px, vh, vw};
+pub use crate::styles::{rems_from_px, vh, vw, PlatformStyle};
pub use crate::visible_on_hover::*;
pub use crate::{h_flex, v_flex};
pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton};
diff --git a/crates/ui/src/styles.rs b/crates/ui/src/styles.rs
index 0b16cb95f3..ad02ae9756 100644
--- a/crates/ui/src/styles.rs
+++ b/crates/ui/src/styles.rs
@@ -1,9 +1,11 @@
mod color;
mod elevation;
+mod platform;
mod typography;
mod units;
pub use color::*;
pub use elevation::*;
+pub use platform::*;
pub use typography::*;
pub use units::*;
diff --git a/crates/ui/src/styles/platform.rs b/crates/ui/src/styles/platform.rs
new file mode 100644
index 0000000000..bb2c3ea47b
--- /dev/null
+++ b/crates/ui/src/styles/platform.rs
@@ -0,0 +1,25 @@
+/// The platform style to use when rendering UI.
+///
+/// This can be used to abstract over platform differences.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum PlatformStyle {
+ /// Display in macOS style.
+ Mac,
+ /// Display in Linux style.
+ Linux,
+ /// Display in Windows style.
+ Windows,
+}
+
+impl PlatformStyle {
+ /// Returns the [`PlatformStyle`] for the current platform.
+ pub const fn platform() -> Self {
+ if cfg!(target_os = "linux") {
+ Self::Linux
+ } else if cfg!(target_os = "windows") {
+ Self::Windows
+ } else {
+ Self::Mac
+ }
+ }
+}
diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs
index 16dc059435..6fdbe936ea 100644
--- a/crates/workspace/src/workspace.rs
+++ b/crates/workspace/src/workspace.rs
@@ -4809,7 +4809,7 @@ impl Element for DisconnectedOverlay {
.bg(background)
.absolute()
.left_0()
- .top(ui::titlebar_height(cx))
+ .top(ui::TitleBar::height(cx))
.size_full()
.flex()
.items_center()