platform_titlebar.rs

  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}