use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ AnyElement, App, ClickEvent, Context, Decorations, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, Render, RenderOnce, StatefulInteractiveElement as _, StyleRefinement, Styled, TitlebarOptions, Window, WindowControlArea, div, px, }; use smallvec::SmallVec; use theme::ActiveTheme; use crate::{Icon, IconName, InteractiveElementExt as _, Sizable as _, StyledExt, h_flex}; pub const TITLE_BAR_HEIGHT: Pixels = px(34.); #[cfg(target_os = "macos")] pub const TRAFFIC_LIGHT_PADDING: f32 = 80.; /// TitleBar used to customize the appearance of the title bar. /// /// We can put some elements inside the title bar. #[derive(IntoElement)] #[allow(clippy::type_complexity)] pub struct TitleBar { style: StyleRefinement, children: SmallVec<[AnyElement; 1]>, on_close_window: Option>>, } impl TitleBar { /// Create a new TitleBar. pub fn new() -> Self { Self { style: StyleRefinement::default(), children: SmallVec::new(), on_close_window: None, } } /// Returns the default title bar options for compatible with the [`crate::TitleBar`]. pub fn title_bar_options() -> TitlebarOptions { TitlebarOptions { title: None, appears_transparent: true, traffic_light_position: Some(gpui::point(px(9.0), px(9.0))), } } /// Add custom for close window event, default is None, then click X button will call `window.remove_window()`. /// Linux only, this will do nothing on other platforms. pub fn on_close_window( mut self, f: impl Fn(&ClickEvent, &mut Window, &mut App) + '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)] #[allow(clippy::type_complexity)] enum ControlIcon { Minimize, Restore, Maximize, Close { on_close_window: Option>>, }, } impl ControlIcon { fn minimize() -> Self { Self::Minimize } fn restore() -> Self { Self::Restore } fn maximize() -> Self { Self::Maximize } #[allow(clippy::type_complexity)] fn close(on_close_window: Option>>) -> 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 window_control_area(&self) -> WindowControlArea { match self { Self::Minimize => WindowControlArea::Min, Self::Restore | Self::Maximize => WindowControlArea::Max, Self::Close { .. } => WindowControlArea::Close, } } fn is_close(&self) -> bool { matches!(self, Self::Close { .. }) } #[inline] fn hover_fg(&self, cx: &App) -> Hsla { if self.is_close() { cx.theme().danger_foreground } else { cx.theme().text } } #[inline] fn hover_bg(&self, cx: &App) -> Hsla { if self.is_close() { cx.theme().danger_background } else { cx.theme().ghost_element_hover } } #[inline] fn active_bg(&self, cx: &mut App) -> Hsla { if self.is_close() { cx.theme().danger_active } else { cx.theme().ghost_element_active } } } impl RenderOnce for ControlIcon { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let is_linux = cfg!(target_os = "linux"); let is_windows = cfg!(target_os = "windows"); let icon = self.clone(); let hover_fg = self.hover_fg(cx); let hover_bg = self.hover_bg(cx); let active_bg = self.active_bg(cx); let on_close_window = match &self { ControlIcon::Close { on_close_window } => on_close_window.clone(), _ => None, }; div() .id(self.id()) .flex() .w(TITLE_BAR_HEIGHT) .h_full() .flex_shrink_0() .justify_center() .content_center() .items_center() .text_color(cx.theme().text) .hover(|style| style.bg(hover_bg).text_color(hover_fg)) .active(|style| style.bg(active_bg).text_color(hover_fg)) .when(is_windows, |this| { this.window_control_area(self.window_control_area()) }) .when(is_linux, |this| { this.on_mouse_down(MouseButton::Left, move |_, window, cx| { window.prevent_default(); cx.stop_propagation(); }) .on_click(move |_, window, cx| { cx.stop_propagation(); match icon { Self::Minimize => window.minimize_window(), Self::Restore | Self::Maximize => window.zoom_window(), Self::Close { .. } => { if let Some(f) = on_close_window.clone() { f(&ClickEvent::default(), window, cx); } else { window.remove_window(); } } } }) }) .child(Icon::new(self.icon()).small()) } } #[derive(IntoElement)] #[allow(clippy::type_complexity)] struct WindowControls { on_close_window: Option>>, } impl RenderOnce for WindowControls { fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement { if cfg!(target_os = "macos") || cfg!(target_family = "wasm") { return div().id("window-controls"); } h_flex() .id("window-controls") .items_center() .flex_shrink_0() .h_full() .child(ControlIcon::minimize()) .child(if window.is_maximized() { ControlIcon::restore() } else { ControlIcon::maximize() }) .child(ControlIcon::close(self.on_close_window)) } } impl Styled for TitleBar { fn style(&mut self) -> &mut gpui::StyleRefinement { &mut self.style } } impl ParentElement for TitleBar { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements); } } struct TitleBarState { should_move: bool, } impl Render for TitleBarState { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() } } impl RenderOnce for TitleBar { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let is_client_decorated = matches!(window.window_decorations(), Decorations::Client { .. }); let is_web = cfg!(target_family = "wasm"); let is_linux = cfg!(target_os = "linux"); let is_macos = cfg!(target_os = "macos"); let state = window.use_state(cx, |_, _| TitleBarState { should_move: false }); div().flex_shrink_0().child( div() .id("title-bar") .flex() .flex_row() .items_center() .justify_between() .h(TITLE_BAR_HEIGHT) .map(|this| { if window.is_fullscreen() { this.px_2() } else if cx.theme().platform.is_mac() { this.pr_2().pl(px(TRAFFIC_LIGHT_PADDING)) } else { this.px_2() } }) .border_b_1() .border_color(cx.theme().border) .bg(cx.theme().title_bar) .refine_style(&self.style) .when(is_linux, |this| { this.on_double_click(|_, window, _| window.zoom_window()) }) .when(is_macos, |this| { this.on_double_click(|_, window, _| window.titlebar_double_click()) }) .on_mouse_down_out(window.listener_for(&state, |state, _, _, _| { state.should_move = false; })) .on_mouse_down( MouseButton::Left, window.listener_for(&state, |state, _, _, _| { state.should_move = true; }), ) .on_mouse_up( MouseButton::Left, window.listener_for(&state, |state, _, _, _| { state.should_move = false; }), ) .on_mouse_move(window.listener_for(&state, |state, _, window, _| { if state.should_move { state.should_move = false; window.start_window_move(); } })) .child( h_flex() .id("bar") .h_full() .justify_between() .flex_shrink_0() .flex_1() .when(!is_web, |this| { this.window_control_area(WindowControlArea::Drag) .when(window.is_fullscreen(), |this| this.pl_3()) .when(is_linux && is_client_decorated, |this| { this.child( div() .top_0() .left_0() .absolute() .size_full() .h_full() .on_mouse_down( MouseButton::Right, move |ev, window, _| { window.show_window_menu(ev.position) }, ), ) }) }) .children(self.children), ) .child(WindowControls { on_close_window: self.on_close_window, }), ) } }