423 lines
14 KiB
Rust
423 lines
14 KiB
Rust
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<ElementId>, window: &mut Window, cx: &mut App) -> Stateful<Div> {
|
||
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<dyn Fn(&mut Window, &mut App)>;
|
||
|
||
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<OnClose>,
|
||
}
|
||
|
||
impl WindowControls {
|
||
pub fn new(id: impl Into<ElementId>) -> 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<OnClose>,
|
||
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<OnClose>,
|
||
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<OnClose>,
|
||
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<OnClose>,
|
||
registered: bool,
|
||
}
|
||
|
||
/// Sends system closes to `handler` while the bar lives and holds one.
|
||
fn route_close(id: &ElementId, handler: Option<OnClose>, 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<SharedString>,
|
||
style: Option<ControlsStyle>,
|
||
on_close: Option<OnClose>,
|
||
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<ElementId>) -> 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<SharedString>) -> 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<Item = AnyElement>) {
|
||
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)
|
||
}
|
||
}
|