use std::{cell::Cell, rc::Rc};
use gpui::{
App, Div, ElementId, MAX_BUTTONS_PER_SIDE, MouseButton, Stateful, Window, WindowButton,
WindowButtonLayout, WindowControlArea, WindowControls, div, prelude::*, px, rgb,
};
use theme::Theme;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CaptionStyle {
#[default]
Rectangular,
Lights,
}
impl gpui::Global for CaptionStyle {}
impl CaptionStyle {
pub fn button_layout(
self,
desktop: Option<WindowButtonLayout>,
allowed: WindowControls,
) -> WindowButtonLayout {
let mut layout = match self {
Self::Rectangular => desktop.unwrap_or(TRAILING),
Self::Lights => WindowButtonLayout {
left: [
Some(WindowButton::Close),
Some(WindowButton::Minimize),
Some(WindowButton::Maximize),
],
right: [None; MAX_BUTTONS_PER_SIDE],
},
};
for button in layout.left.iter_mut().chain(layout.right.iter_mut()) {
if matches!(button, Some(WindowButton::Minimize) if !allowed.minimize)
|| matches!(button, Some(WindowButton::Maximize) if !allowed.maximize)
{
*button = None;
}
}
layout
}
}
pub const LIGHTS_WIDTH: f32 = 3.0 * Theme::CAPTION_BUTTON_WIDTH;
pub fn lights_width(window: &Window) -> f32 {
if window.is_fullscreen() {
return 0.0;
}
CaptionStyle::Lights
.button_layout(None, window.window_controls())
.left
.into_iter()
.flatten()
.count() as f32
* Theme::CAPTION_BUTTON_WIDTH
}
pub(crate) fn caption_style(cx: &App) -> CaptionStyle {
cx.try_global::<CaptionStyle>().copied().unwrap_or_default()
}
const CAPTION_GROUP: &str = "caption-controls";
#[derive(Clone, Default)]
pub struct DragState(Rc<Cell<bool>>);
pub fn titlebar(id: impl Into<ElementId>, traffic_lights: bool, window: &Window) -> Stateful<Div> {
div()
.id(id)
.w_full()
.h(px(Theme::TITLEBAR_HEIGHT))
.flex()
.flex_row()
.items_center()
.when(traffic_lights && !window.is_fullscreen(), |bar| {
bar.pl(px(Theme::TRAFFIC_LIGHT_INSET))
})
}
pub fn grip(id: impl Into<ElementId>, drag: &DragState, window: &Window) -> Stateful<Div> {
let (armed, disarm, release) = (drag.0.clone(), drag.0.clone(), drag.0.clone());
let moving = drag.0.clone();
let zoomable = window.window_controls().maximize && window.is_resizable();
div()
.id(id)
.flex_1()
.self_stretch()
.on_mouse_down(MouseButton::Left, move |_, _, _| armed.set(true))
.on_mouse_up(MouseButton::Left, move |_, _, _| release.set(false))
.on_mouse_down_out(move |_, _, _| disarm.set(false))
.on_mouse_move(move |_, window, _| {
if moving.replace(false) {
window.start_window_move();
}
})
.window_control_area(WindowControlArea::Drag)
.when(window.window_controls().window_menu, |grip| {
grip.on_mouse_down(MouseButton::Right, |event, window, _| {
window.show_window_menu(event.position);
})
})
.on_click(move |click, window, _| {
if click.click_count() == 2 {
match cfg!(any(target_os = "linux", target_os = "freebsd")) {
true if zoomable => window.zoom_window(),
true => {}
false => window.titlebar_double_click(),
}
}
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CaptionSide {
Left,
Right,
}
pub fn controls(side: CaptionSide, window: &Window, cx: &App) -> Div {
let row = div()
.group(CAPTION_GROUP)
.flex()
.flex_row()
.flex_none()
.items_center()
.h_full();
if window.is_fullscreen() {
return row;
}
let layout = caption_style(cx).button_layout(cx.button_layout(), window.window_controls());
let buttons = match side {
CaptionSide::Left => layout.left,
CaptionSide::Right => layout.right,
};
buttons.into_iter().flatten().fold(row, |row, button| {
row.child(caption_button(button, window, cx))
})
}
const TRAILING: WindowButtonLayout = WindowButtonLayout {
left: [None; MAX_BUTTONS_PER_SIDE],
right: [
Some(WindowButton::Minimize),
Some(WindowButton::Maximize),
Some(WindowButton::Close),
],
};
const CAPTION_GLYPH: f32 = 10.0;
fn caption_button(button: WindowButton, window: &Window, cx: &App) -> Stateful<Div> {
let theme = Theme::of(cx);
let (area, glyph, hover) = match button {
WindowButton::Close => (WindowControlArea::Close, icons::glyph::X, theme.danger),
WindowButton::Minimize => (
WindowControlArea::Min,
icons::glyph::Minus,
theme.element_hover,
),
WindowButton::Maximize => (
WindowControlArea::Max,
match window.is_maximized() {
true => icons::glyph::Copy,
false => icons::glyph::Square,
},
theme.element_hover,
),
};
div()
.id(button.id())
.debug_selector(move || format!("caption-{}", button.id()))
.flex()
.flex_none()
.items_center()
.justify_center()
.w(px(Theme::CAPTION_BUTTON_WIDTH))
.h_full()
.map(|control| match caption_style(cx) {
CaptionStyle::Rectangular => control.hover(|button| button.bg(hover)).child(
icons::icon(glyph)
.size(px(CAPTION_GLYPH))
.text_color(theme.text),
),
CaptionStyle::Lights => {
let (color, mark) = match button {
WindowButton::Close => (0xff5f57, icons::glyph::X),
WindowButton::Minimize => (0xfebc2e, icons::glyph::Minus),
WindowButton::Maximize => (0x28c840, icons::glyph::Plus),
};
control.child(
div()
.size(px(12.0))
.rounded_full()
.bg(rgb(if window.is_window_active() {
color
} else {
0x999999
}))
.flex()
.items_center()
.justify_center()
.child(
div()
.invisible()
.group_hover(CAPTION_GROUP, |mark| mark.visible())
.child(icons::icon(mark).size(px(8.0)).text_color(rgb(0x333333))),
),
)
}
})
.window_control_area(area)
.when(!cfg!(target_os = "windows"), |control| {
control.on_click(move |_, window, _| match button {
WindowButton::Close => window.remove_window(),
WindowButton::Minimize => window.minimize_window(),
WindowButton::Maximize => window.zoom_window(),
})
})
}