use anyhow::Context as _; use gpui::{ AnyView, AnyWindowHandle, App, AppContext, Bounds, Context, Entity, FocusHandle, Global, InteractiveElement, IntoElement, ParentElement, Pixels, Point, Render, SharedString, Size, Styled, TitlebarOptions, Window, WindowBounds, WindowHandle, WindowKind, WindowOptions, div, point, prelude::*, }; use super::drag_region; use crate::{ buttons::IconButton, primitives::{FocusScope, IconName}, theme::{ActiveTheme, ControlSize, TextSize}, }; fn titlebar(title: SharedString, cx: &App) -> TitlebarOptions { TitlebarOptions { title: Some(title), appears_transparent: true, traffic_light_position: Some(cx.theme().traffic_light_origin()), } } /// A window whose content is one component. Escape closes it. pub struct Hosted { pub content: T, focus: FocusHandle, } impl Hosted { pub(crate) fn new(content: T, focus: FocusHandle) -> Self { Self { content, focus } } } impl Render for Hosted { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let bar = drag_region("hosted-bar", window, cx); let theme = cx.theme(); div() .id("hosted") .size_full() .on_key_down(|event, window, _| { if event.keystroke.key == "escape" { window.remove_window(); } }) .child( FocusScope::new(&self.focus) .root() .size_full() .flex() .flex_col() .bg(theme.colors.bg) .text_color(theme.colors.fg) .font_family(theme.font_family.clone()) .child(bar.flex_none().h(theme.titlebar_height())) .child(self.content.clone()), ) } } /// Opens `content` alone in a fixed-size window, centered. pub fn open_hosted( title: impl Into, size: Size, content: T, cx: &mut App, ) -> anyhow::Result>> { let title = title.into(); let options = WindowOptions { window_bounds: Some(WindowBounds::Windowed(Bounds::centered(None, size, cx))), titlebar: Some(titlebar(title.clone(), cx)), is_resizable: false, is_minimizable: false, ..Default::default() }; let handle = cx.open_window(options, |window, cx| { let focus = cx.focus_handle(); window.focus(&focus, cx); cx.new(|_| Hosted::new(content, focus)) })?; log::info!("window: opened {title}"); Ok(handle) } /// Windows opened through it, oldest first, with where each opened. #[derive(Default)] pub struct WindowManager { windows: Vec<(AnyWindowHandle, SharedString, Point)>, } impl Global for WindowManager {} impl WindowManager { /// Opens a window one title bar below and right of the newest. pub fn open( title: impl Into, size: Size, cx: &mut App, build: impl FnOnce(&mut Window, &mut App) -> Entity, ) -> anyhow::Result> { let title = title.into(); Self::windows(cx); let theme = cx.theme(); let step = theme.titlebar_height().to_pixels(theme.base_rem()); let newest = cx .default_global::() .windows .last() .map(|(_, _, at)| *at); let bounds = match newest { Some(at) => Bounds::new(at + point(step, step), size), None => Bounds::centered(None, size, cx), }; let options = WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), titlebar: Some(titlebar(title.clone(), cx)), ..Default::default() }; let handle = cx.open_window(options, build)?; cx.default_global::().windows.push(( handle.into(), title.clone(), bounds.origin, )); log::info!("window manager: opened {title}"); Ok(handle) } /// Managed windows still open, oldest first. pub fn windows(cx: &mut App) -> Vec<(AnyWindowHandle, SharedString)> { let open = cx.windows(); let manager = cx.default_global::(); manager.windows.retain(|(handle, ..)| open.contains(handle)); manager .windows .iter() .map(|(handle, title, _)| (*handle, title.clone())) .collect() } pub fn focus(handle: AnyWindowHandle, cx: &mut App) -> anyhow::Result<()> { log::info!("window manager: focus {:?}", handle.window_id()); handle.update(cx, |_, window, _| window.activate_window()) } } /// Screen corner for a mini window. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Corner { TopLeft, TopRight, BottomLeft, BottomRight, } /// A small window above all others, around a host view. pub struct MiniWindow { title: SharedString, view: AnyView, back: Option, } impl MiniWindow { /// Pinned to a display corner or centered; expanding returns to the opener. pub fn open( title: impl Into, size: Size, corner: Option, view: AnyView, cx: &mut App, ) -> anyhow::Result> { let title = title.into(); let back = cx.active_window(); let area = cx .primary_display() .context("a mini window needs a display")? .bounds(); let margin = cx.theme().window_margin(); let (left, top) = (area.left() + margin, area.top() + margin * 2.0); let (right, bottom) = ( area.right() - margin - size.width, area.bottom() - margin - size.height, ); let bounds = match corner { Some(Corner::TopLeft) => Bounds::new(point(left, top), size), Some(Corner::TopRight) => Bounds::new(point(right, top), size), Some(Corner::BottomLeft) => Bounds::new(point(left, bottom), size), Some(Corner::BottomRight) => Bounds::new(point(right, bottom), size), None => Bounds::centered(None, size, cx), }; let options = WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), titlebar: None, kind: WindowKind::PopUp, is_resizable: false, is_minimizable: false, ..Default::default() }; let handle = cx.open_window(options, |_, cx| { cx.new(|_| MiniWindow { title: title.clone(), view, back, }) })?; log::info!("mini window: opened {title} at {corner:?}"); Ok(handle) } } impl Render for MiniWindow { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let drag = drag_region("mini-bar", window, cx); let theme = cx.theme(); let back = self.back; div() .id("mini-window") .group("mini-window") .relative() .size_full() .bg(theme.colors.bg) .text_color(theme.colors.fg) .font_family(theme.font_family.clone()) .child(self.view.clone()) .child( div() .absolute() .top_0() .left_0() .right_0() .flex() .items_center() .gap_1() .h(theme.control_height(ControlSize::Lg)) .px_2() .bg(theme.colors.overlay.opacity(0.92)) .border_b_1() .border_color(theme.colors.border) .text_size(theme.text_size(TextSize::Xs)) .text_color(theme.colors.fg_muted) .invisible() .group_hover("mini-window", |style| style.visible()) // Only the title drags: Windows takes the region as caption. .child( drag.flex_1() .h_full() .flex() .items_center() .child(self.title.clone()), ) .when_some(back, |bar, back| { bar.child( IconButton::new("mini-expand", IconName::Maximize2) .size(ControlSize::Sm) .on_click(move |_, window, cx| { window.remove_window(); let returned = back.update(cx, |_, window, _| window.activate_window()); if let Err(error) = returned { log::warn!( "mini window: no window to return to: {error:#}" ); } }), ) }) .child( IconButton::new("mini-close", IconName::X) .size(ControlSize::Sm) .on_click(|_, window, _| window.remove_window()), ), ) } }