use crate::{ h_flex, theme::{scale::ColorScaleStep, ActiveTheme}, Icon, IconName, InteractiveElementExt as _, Sizable as _, }; use gpui::{ black, div, prelude::FluentBuilder as _, px, relative, white, AnyElement, ClickEvent, Div, Element, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, Rgba, Stateful, StatefulInteractiveElement as _, Style, Styled, WindowContext, }; use std::rc::Rc; const HEIGHT: Pixels = px(34.); const TITLE_BAR_HEIGHT: Pixels = px(35.); #[cfg(target_os = "macos")] const TITLE_BAR_LEFT_PADDING: Pixels = px(80.); #[cfg(not(target_os = "macos"))] const TITLE_BAR_LEFT_PADDING: Pixels = px(12.); type OnCloseWindow = Option>>; /// TitleBar used to customize the appearance of the title bar. /// /// We can put some elements inside the title bar. #[derive(IntoElement)] pub struct TitleBar { base: Stateful
, children: Vec, on_close_window: OnCloseWindow, } impl TitleBar { pub fn new() -> Self { Self { base: div().id("title-bar").pl(TITLE_BAR_LEFT_PADDING), children: Vec::new(), on_close_window: None, } } /// Add custom for close window event, default is None, then click X button will call `cx.remove_window()`. /// Linux only, this will do nothing on other platforms. pub fn on_close_window( mut self, f: impl Fn(&ClickEvent, &mut WindowContext) + 'static, ) -> Self { if cfg!(target_os = "linux") { self.on_close_window = Some(Rc::new(Box::new(f))); } self } } impl Default for TitleBar { fn default() -> Self { Self::new() } } // The Windows control buttons have a fixed width of 35px. // // We don't need implementation the click event for the control buttons. // If user clicked in the bounds, the window event will be triggered. #[derive(IntoElement, Clone)] enum Control { Minimize, Restore, Maximize, Close { on_close_window: OnCloseWindow }, } impl Control { fn minimize() -> Self { Self::Minimize } fn restore() -> Self { Self::Restore } fn maximize() -> Self { Self::Maximize } fn close(on_close_window: OnCloseWindow) -> Self { Self::Close { on_close_window } } fn id(&self) -> &'static str { match self { Self::Minimize => "minimize", Self::Restore => "restore", Self::Maximize => "maximize", Self::Close { .. } => "close", } } fn icon(&self) -> IconName { match self { Self::Minimize => IconName::WindowMinimize, Self::Restore => IconName::WindowRestore, Self::Maximize => IconName::WindowMaximize, Self::Close { .. } => IconName::WindowClose, } } fn is_close(&self) -> bool { matches!(self, Self::Close { .. }) } fn fg(&self, cx: &WindowContext) -> Hsla { if cx.theme().appearance.is_dark() { white() } else { black() } } fn hover_fg(&self, cx: &WindowContext) -> Hsla { if self.is_close() || cx.theme().appearance.is_dark() { white() } else { black() } } fn hover_bg(&self, cx: &WindowContext) -> Rgba { if self.is_close() { Rgba { r: 232.0 / 255.0, g: 17.0 / 255.0, b: 32.0 / 255.0, a: 1.0, } } else if cx.theme().appearance.is_dark() { Rgba { r: 0.9, g: 0.9, b: 0.9, a: 0.1, } } else { Rgba { r: 0.1, g: 0.1, b: 0.1, a: 0.2, } } } } impl RenderOnce for Control { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let fg = self.fg(cx); let hover_fg = self.hover_fg(cx); let hover_bg = self.hover_bg(cx); let icon = self.clone(); let is_linux = cfg!(target_os = "linux"); let on_close_window = match &icon { Control::Close { on_close_window } => on_close_window.clone(), _ => None, }; div() .id(self.id()) .flex() .cursor_pointer() .w(TITLE_BAR_HEIGHT) .h_full() .justify_center() .content_center() .items_center() .text_color(fg) .when(is_linux, |this| { this.on_mouse_down(MouseButton::Left, move |_, cx| { cx.prevent_default(); cx.stop_propagation(); }) .on_click(move |_, cx| match icon { Self::Minimize => cx.minimize_window(), Self::Restore => cx.zoom_window(), Self::Maximize => cx.zoom_window(), Self::Close { .. } => { if let Some(f) = on_close_window.clone() { f(&ClickEvent::default(), cx); } else { cx.remove_window(); } } }) }) .hover(|style| style.bg(hover_bg).text_color(hover_fg)) .active(|style| style.bg(hover_bg)) .child(Icon::new(self.icon()).small()) } } #[derive(IntoElement)] struct WindowControls { on_close_window: OnCloseWindow, } impl RenderOnce for WindowControls { fn render(self, cx: &mut WindowContext) -> impl IntoElement { if cfg!(target_os = "macos") { return div().id("window-controls"); } h_flex() .id("window-controls") .items_center() .flex_shrink_0() .h_full() .child( h_flex() .justify_center() .content_stretch() .h_full() .child(Control::minimize()) .child(if cx.is_maximized() { Control::restore() } else { Control::maximize() }), ) .child(Control::close(self.on_close_window)) } } impl Styled for TitleBar { fn style(&mut self) -> &mut gpui::StyleRefinement { self.base.style() } } impl ParentElement for TitleBar { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements); } } impl RenderOnce for TitleBar { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let is_linux = cfg!(target_os = "linux"); div().flex_shrink_0().child( self.base .flex() .flex_row() .items_center() .justify_between() .h(HEIGHT) .border_b_1() .border_color(cx.theme().base.step(cx, ColorScaleStep::THREE)) .bg(cx.theme().base.step(cx, ColorScaleStep::ONE)) .when(cx.is_fullscreen(), |this| this.pl(px(12.))) .on_double_click(|_, cx| cx.zoom_window()) .child( h_flex() .h_full() .justify_between() .flex_shrink_0() .flex_1() .when(is_linux, |this| { this.child( div() .top_0() .left_0() .absolute() .size_full() .h_full() .child(TitleBarElement {}), ) }) .children(self.children), ) .child(WindowControls { on_close_window: self.on_close_window, }), ) } } /// A TitleBar Element that can be move the window. pub struct TitleBarElement {} impl IntoElement for TitleBarElement { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for TitleBarElement { type RequestLayoutState = (); type PrepaintState = (); fn id(&self) -> Option { None } fn request_layout( &mut self, _: Option<&gpui::GlobalElementId>, cx: &mut WindowContext, ) -> (gpui::LayoutId, Self::RequestLayoutState) { let style = Style { flex_grow: 1.0, flex_shrink: 1.0, size: gpui::Size { width: relative(1.).into(), height: relative(1.).into(), }, ..Default::default() }; let id = cx.request_layout(style, []); (id, ()) } fn prepaint( &mut self, _: Option<&gpui::GlobalElementId>, _: gpui::Bounds, _: &mut Self::RequestLayoutState, _: &mut WindowContext, ) -> Self::PrepaintState { } fn paint( &mut self, _: Option<&gpui::GlobalElementId>, bounds: gpui::Bounds, _: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, cx: &mut WindowContext, ) { use gpui::{MouseButton, MouseMoveEvent, MouseUpEvent}; cx.on_mouse_event(move |ev: &MouseMoveEvent, _, cx: &mut WindowContext| { if bounds.contains(&ev.position) && ev.pressed_button == Some(MouseButton::Left) { cx.start_window_move(); } }); cx.on_mouse_event(move |ev: &MouseUpEvent, _, cx: &mut WindowContext| { if ev.button == MouseButton::Left { cx.show_window_menu(ev.position); } }); } }