use std::rc::Rc; use gpui::{ AnyElement, App, Div, ElementId, FontWeight, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, Stateful, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, prelude::*, }; use smallvec::SmallVec; use crate::{ primitives::{Icon, IconName}, theme::{ActiveTheme, ControlSize, IconSize, TextSize}, }; /// Which system's window buttons to draw. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ControlsStyle { Mac, Windows, Linux, } impl ControlsStyle { /// The style of the system this build targets. pub fn current() -> Self { if cfg!(target_os = "macos") { Self::Mac } else if cfg!(target_os = "windows") { Self::Windows } else { Self::Linux } } } /// Drags the window, and zooms it on double-click. pub fn drag_region(id: impl Into, window: &mut Window, cx: &mut App) -> Stateful
{ let id = id.into(); let pressed = window.use_keyed_state(id.clone(), cx, |_, _| false); let (down, moved, up, out) = (pressed.clone(), pressed.clone(), pressed.clone(), pressed); div() .id(id) .window_control_area(WindowControlArea::Drag) .on_mouse_down(MouseButton::Left, move |_, _, cx| { down.update(cx, |pressed, _| *pressed = true) }) .on_mouse_move(move |_, window, cx| { if *moved.read(cx) { moved.update(cx, |pressed, _| *pressed = false); window.start_window_move(); } }) .on_mouse_up(MouseButton::Left, move |_, _, cx| { up.update(cx, |pressed, _| *pressed = false) }) .on_mouse_up_out(MouseButton::Left, move |_, _, cx| { out.update(cx, |pressed, _| *pressed = false) }) .on_click(|event, window, _| { if !event.standard_click() || event.click_count() != 2 { return; } if cfg!(target_os = "macos") { window.titlebar_double_click(); } else if cfg!(target_os = "linux") { window.zoom_window(); } }) } #[derive(Clone, Copy, Debug)] enum Control { Minimize, Maximize, Close, } type OnClose = Rc; fn run(control: Control, on_close: Option<&OnClose>, window: &mut Window, cx: &mut App) { log::info!("window controls: {control:?}"); match (control, on_close) { (Control::Minimize, _) => window.minimize_window(), (Control::Maximize, _) => window.zoom_window(), (Control::Close, Some(close)) => close(window, cx), (Control::Close, None) => window.remove_window(), } } /// Minimize, maximize and close, drawn in one system's manner. #[derive(IntoElement)] pub struct WindowControls { id: ElementId, style: ControlsStyle, on_close: Option, } impl WindowControls { pub fn new(id: impl Into) -> Self { Self { id: id.into(), style: ControlsStyle::current(), on_close: None, } } pub fn style(mut self, style: ControlsStyle) -> Self { self.style = style; self } /// Replaces closing the window, e.g. to confirm first. pub fn on_close(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { self.on_close = Some(Rc::new(handler)); self } } impl RenderOnce for WindowControls { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { match self.style { ControlsStyle::Mac => traffic_lights(self.id, self.on_close, window, cx), ControlsStyle::Windows => caption_buttons(self.id, self.on_close, window, cx), ControlsStyle::Linux => round_buttons(self.id, self.on_close, window, cx), } } } fn traffic_lights( id: ElementId, on_close: Option, window: &mut Window, cx: &mut App, ) -> AnyElement { let theme = cx.theme(); let colors = &theme.colors; let active = window.is_window_active(); let lights = [ (Control::Close, colors.danger, "×"), (Control::Minimize, colors.warning, "−"), (Control::Maximize, colors.success, "+"), ]; let group = SharedString::from(format!("traffic-{id}")); div() .id(id) .group(group.clone()) .flex() .items_center() .gap_2() .children(lights.map(|(control, tint, glyph)| { let on_close = on_close.clone(); div() .id(SharedString::from(format!("{control:?}"))) .size(theme.traffic_light()) .flex() .items_center() .justify_center() .rounded_full() .bg(if active { tint } else { colors.border_strong }) .border_1() .border_color(colors.shadow.opacity(0.12)) .text_size(theme.text_size(TextSize::Xs)) .line_height(theme.traffic_light()) .font_weight(FontWeight::BOLD) .text_color(gpui::transparent_black()) .group_hover(group.clone(), |style| { style.text_color(colors.shadow.opacity(0.6)) }) .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) .on_click(move |_, window, cx| run(control, on_close.as_ref(), window, cx)) .child(glyph) })) .into_any_element() } fn caption_buttons( id: ElementId, on_close: Option, window: &mut Window, cx: &mut App, ) -> AnyElement { let theme = cx.theme(); let colors = &theme.colors; let maximize = if window.is_maximized() { IconName::Copy } else { IconName::Square }; let buttons = [ (Control::Minimize, IconName::Minus, WindowControlArea::Min), (Control::Maximize, maximize, WindowControlArea::Max), (Control::Close, IconName::X, WindowControlArea::Close), ]; div() .id(id) .flex() .h_full() .children(buttons.map(|(control, icon, area)| { let on_close = on_close.clone(); let group = SharedString::from(format!("caption-{control:?}")); let (hover, glyph) = match control { Control::Close => (colors.danger, colors.on_accent), _ => (colors.hover, colors.fg), }; div() .id(group.clone()) .group(group.clone()) .flex() .items_center() .justify_center() .w(theme.caption_button_width()) .h_full() .window_control_area(area) .hover(|style| style.bg(hover)) .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) .on_click(move |_, window, cx| run(control, on_close.as_ref(), window, cx)) .child( Icon::new(icon) .size(IconSize::Sm) .color(colors.fg_muted) .group_hover_color(group, glyph), ) })) .into_any_element() } fn round_buttons( id: ElementId, on_close: Option, window: &mut Window, cx: &mut App, ) -> AnyElement { let theme = cx.theme(); let colors = &theme.colors; let offered = window.window_controls(); let buttons = [ (Control::Minimize, IconName::Minus, offered.minimize), (Control::Maximize, IconName::Square, offered.maximize), (Control::Close, IconName::X, true), ]; div() .id(id) .flex() .items_center() .gap_2() .children( buttons .into_iter() .filter(|(_, _, shown)| *shown) .map(|(control, icon, _)| { let on_close = on_close.clone(); div() .id(SharedString::from(format!("{control:?}"))) .flex() .items_center() .justify_center() .size(theme.control_height(ControlSize::Sm)) .rounded_full() .bg(colors.hover) .hover(|style| style.bg(colors.active)) .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) .on_click(move |_, window, cx| run(control, on_close.as_ref(), window, cx)) .child(Icon::new(icon).size(IconSize::Xs).color(colors.fg)) }), ) .into_any_element() } /// The live close handler, read by the window's close request. struct CloseRoute { handler: Option, registered: bool, } /// Sends system closes to `handler` while the bar lives and holds one. fn route_close(id: &ElementId, handler: Option, window: &mut Window, cx: &mut App) { let route = window.use_keyed_state(id.clone(), cx, |_, _| CloseRoute { handler: None, registered: false, }); let register = handler.is_some() && !route.read(cx).registered; route.update(cx, |route, _| { route.handler = handler; route.registered |= register; }); if register { let live = route.downgrade(); window.on_window_should_close(cx, move |window, cx| { let handler = live .upgrade() .and_then(|route| route.read(cx).handler.clone()); let Some(close) = handler else { return true; }; log::info!("title bar: system close routed to on_close"); close(window, cx); false }); } } /// Window title bar. Its empty space drags the window. #[derive(IntoElement)] pub struct TitleBar { id: ElementId, title: Option, style: Option, on_close: Option, leading: SmallVec<[AnyElement; 2]>, actions: SmallVec<[AnyElement; 2]>, } impl TitleBar { /// On macOS the system draws the traffic lights; the bar leaves them room. pub fn new(id: impl Into) -> Self { Self { id: id.into(), title: None, style: None, on_close: None, leading: SmallVec::new(), actions: SmallVec::new(), } } pub fn title(mut self, title: impl Into) -> Self { self.title = Some(title.into()); self } /// Draws this style's controls instead of the system's. pub fn style(mut self, style: ControlsStyle) -> Self { self.style = Some(style); self } /// Runs instead of closing; without a forced style, system closes run it too. pub fn on_close(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { self.on_close = Some(Rc::new(handler)); self } /// Right-aligned content before the window controls. pub fn action(mut self, action: impl IntoElement) -> Self { self.actions.push(action.into_any_element()); self } } impl ParentElement for TitleBar { fn extend(&mut self, elements: impl IntoIterator) { self.leading.extend(elements); } } impl RenderOnce for TitleBar { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { if self.style.is_none() { route_close(&self.id, self.on_close.clone(), window, cx); } let bar = drag_region(self.id, window, cx); let theme = cx.theme(); let system = self.style.is_none() && cfg!(target_os = "macos"); let style = self.style.unwrap_or_else(ControlsStyle::current); let mut trailing = (!system).then_some(WindowControls { id: "window-controls".into(), style, on_close: self.on_close, }); let leading = if style == ControlsStyle::Mac { trailing.take() } else { None }; let title = self.title.map(|title| { div() .text_size(theme.text_size(TextSize::Sm)) .font_weight(FontWeight::MEDIUM) .text_color(theme.colors.fg_muted) .child(title) }); let centered = style != ControlsStyle::Windows; let inset = theme.traffic_light_inset(); let fullscreen = window.is_fullscreen(); let bar = bar .relative() .flex() .flex_none() .items_center() .gap_2() .w_full() .h(theme.titlebar_height()) .bg(theme.colors.bg) .border_b_1() .border_color(theme.colors.border); let bar = match (system, style) { (true, _) if fullscreen => bar.pl_3().pr_2(), (true, _) => bar.pl(inset).pr_2(), (false, ControlsStyle::Mac) => bar.pl_3().pr_2().children(leading), (false, ControlsStyle::Windows) => bar.pl_3(), (false, ControlsStyle::Linux) => bar.pl_3().pr_2(), }; let bar = bar.children(self.leading); let bar = match (centered, title) { (false, Some(title)) => bar.child(title), (_, title) => bar.when_some(title, |bar, title| { bar.child( div() .absolute() .inset_0() .flex() .items_center() .justify_center() .child(title), ) }), }; bar.child(div().flex_1()) .children(self.actions) .children(trailing) } }