1// allowing due to multiple platform conditional code
2#![allow(unused_imports)]
3
4use gpui::{
5 div,
6 prelude::FluentBuilder,
7 px, AnyElement, Div, Element, ElementId, Fill, InteractiveElement, Interactivity, IntoElement,
8 ParentElement, Pixels, RenderOnce, Rgba, Stateful, StatefulInteractiveElement, StyleRefinement,
9 Styled,
10 WindowAppearance::{Dark, Light, VibrantDark, VibrantLight},
11 WindowContext,
12};
13use smallvec::SmallVec;
14
15use crate::h_flex;
16
17pub enum PlatformStyle {
18 Linux,
19 Windows,
20 MacOs,
21}
22
23impl PlatformStyle {
24 pub fn platform() -> Self {
25 if cfg!(windows) {
26 Self::Windows
27 } else if cfg!(macos) {
28 Self::MacOs
29 } else {
30 Self::Linux
31 }
32 }
33
34 pub fn windows(&self) -> bool {
35 matches!(self, Self::Windows)
36 }
37
38 pub fn macos(&self) -> bool {
39 matches!(self, Self::MacOs)
40 }
41}
42
43#[derive(IntoElement)]
44pub struct PlatformTitlebar {
45 platform: PlatformStyle,
46 titlebar_bg: Option<Fill>,
47 content: Stateful<Div>,
48 children: SmallVec<[AnyElement; 2]>,
49}
50
51impl Styled for PlatformTitlebar {
52 fn style(&mut self) -> &mut StyleRefinement {
53 self.content.style()
54 }
55}
56
57impl PlatformTitlebar {
58 /// Change the platform style used
59 pub fn with_platform_style(self, style: PlatformStyle) -> Self {
60 Self {
61 platform: style,
62 ..self
63 }
64 }
65
66 fn titlebar_top_padding(&self, cx: &WindowContext) -> Pixels {
67 if self.platform.windows() && cx.is_maximized() {
68 // todo(windows): get padding from win32 api, need HWND from window context somehow
69 // should be GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) * 2
70 px(8.0)
71 } else {
72 px(0.0)
73 }
74 }
75
76 fn windows_caption_button_width(_cx: &WindowContext) -> Pixels {
77 // todo(windows): get padding from win32 api, need HWND from window context somehow
78 // should be GetSystemMetricsForDpi(SM_CXSIZE, dpi)
79 px(36.0)
80 }
81
82 fn render_window_controls_right(&self, cx: &mut WindowContext) -> impl Element {
83 if self.platform.windows() {
84 let btn_height = cx.titlebar_height() - self.titlebar_top_padding(cx);
85 let close_btn_hover_color = Rgba {
86 r: 232.0 / 255.0,
87 g: 17.0 / 255.0,
88 b: 32.0 / 255.0,
89 a: 1.0,
90 };
91
92 let btn_hover_color = match cx.appearance() {
93 Light | VibrantLight => Rgba {
94 r: 0.1,
95 g: 0.1,
96 b: 0.1,
97 a: 0.2,
98 },
99 Dark | VibrantDark => Rgba {
100 r: 0.9,
101 g: 0.9,
102 b: 0.9,
103 a: 0.1,
104 },
105 };
106
107 fn windows_caption_btn(
108 id: &'static str,
109 icon_text: &'static str,
110 hover_color: Rgba,
111 cx: &WindowContext,
112 ) -> Stateful<Div> {
113 let mut active_color = hover_color;
114 active_color.a *= 0.2;
115 h_flex()
116 .id(id)
117 .h_full()
118 .justify_center()
119 .content_center()
120 .items_center()
121 .w(PlatformTitlebar::windows_caption_button_width(cx))
122 .hover(|style| style.bg(hover_color))
123 .active(|style| style.bg(active_color))
124 .child(icon_text)
125 }
126
127 div()
128 .id("caption-buttons-windows")
129 .flex()
130 .flex_row()
131 .justify_center()
132 .content_stretch()
133 .max_h(btn_height)
134 .min_h(btn_height)
135 .font("Segoe Fluent Icons")
136 .text_size(gpui::Pixels(10.0))
137 .children(vec![
138 windows_caption_btn("minimize", "\u{e921}", btn_hover_color, cx), // minimize icon
139 windows_caption_btn(
140 "maximize",
141 if cx.is_maximized() {
142 "\u{e923}" // restore icon
143 } else {
144 "\u{e922}" // maximize icon
145 },
146 btn_hover_color,
147 cx,
148 ),
149 windows_caption_btn("close", "\u{e8bb}", close_btn_hover_color, cx), // close icon
150 ])
151 } else {
152 div().id("caption-buttons-windows")
153 }
154 }
155
156 /// Sets the background color of titlebar.
157 pub fn titlebar_bg<F>(mut self, fill: F) -> Self
158 where
159 F: Into<Fill>,
160 Self: Sized,
161 {
162 self.titlebar_bg = Some(fill.into());
163 self
164 }
165}
166
167pub fn platform_titlebar(id: impl Into<ElementId>) -> PlatformTitlebar {
168 let id = id.into();
169 PlatformTitlebar {
170 platform: PlatformStyle::platform(),
171 titlebar_bg: None,
172 content: div().id(id.clone()),
173 children: SmallVec::new(),
174 }
175}
176
177impl RenderOnce for PlatformTitlebar {
178 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
179 let titlebar_height = cx.titlebar_height();
180 let titlebar_top_padding = self.titlebar_top_padding(cx);
181 let window_controls_right = self.render_window_controls_right(cx);
182 let macos = self.platform.macos();
183 h_flex()
184 .id("titlebar")
185 .w_full()
186 .pt(titlebar_top_padding)
187 .max_h(titlebar_height)
188 .min_h(titlebar_height)
189 .map(|mut this| {
190 this.style().background = self.titlebar_bg;
191
192 if macos {
193 if !cx.is_fullscreen() {
194 // Use pixels here instead of a rem-based size because the macOS traffic
195 // lights are a static size, and don't scale with the rest of the UI.
196 return this.pl(px(80.));
197 }
198 }
199
200 this
201 })
202 .content_stretch()
203 .child(
204 self.content
205 .flex()
206 .flex_row()
207 .w_full()
208 .id("titlebar-content")
209 .children(self.children),
210 )
211 .child(window_controls_right)
212 }
213}
214
215impl InteractiveElement for PlatformTitlebar {
216 fn interactivity(&mut self) -> &mut Interactivity {
217 self.content.interactivity()
218 }
219}
220impl StatefulInteractiveElement for PlatformTitlebar {}
221
222impl ParentElement for PlatformTitlebar {
223 fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
224 self.children.extend(elements)
225 }
226}