layout: make strips the titlebar with window controls
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
//! The window controls: minimize, maximize/restore, close — in the platform's
|
||||
//! own dress and behavior. The titlebar strips drag the window themselves via
|
||||
//! `WindowControlArea::Drag`; macOS draws its traffic lights in the shell's
|
||||
//! reserved pad and needs no controls of ours.
|
||||
|
||||
use gpui::{App, ClickEvent, ElementId, IntoElement, RenderOnce, Window, div};
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use gpui::{
|
||||
AnyElement, InteractiveElement as _, ParentElement as _, StatefulInteractiveElement as _,
|
||||
Styled as _,
|
||||
};
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use gpui_native_icons::{IconRole, icon};
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use gpui_native_theme::ActiveTheme as _;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use gpui::Decorations;
|
||||
#[cfg(target_os = "windows")]
|
||||
use gpui::{WindowControlArea, px, rgba};
|
||||
|
||||
/// macOS traffic-lights zone: the pad a leading titlebar strip reserves.
|
||||
pub const TRAFFIC_LIGHT_PAD: f32 = 78.0;
|
||||
/// The width of a Windows caption button.
|
||||
#[cfg(target_os = "windows")]
|
||||
const CAPTION_WIDTH: f32 = 36.0;
|
||||
/// Windows paints its caption close hover red in every theme.
|
||||
#[cfg(target_os = "windows")]
|
||||
const WINDOWS_CLOSE_HOVER: (f32, f32, f32) = (232.0 / 255.0, 17.0 / 255.0, 32.0 / 255.0);
|
||||
|
||||
/// The window controls for the trailing end of a titlebar strip.
|
||||
#[derive(IntoElement)]
|
||||
pub struct WindowControls {
|
||||
id: ElementId,
|
||||
}
|
||||
|
||||
impl WindowControls {
|
||||
/// Creates the controls row.
|
||||
pub fn new<I>(id: I) -> Self
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
{
|
||||
Self { id: id.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for WindowControls {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let id = self.id.clone();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = (id, window, cx);
|
||||
div()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let controls = window.window_controls();
|
||||
div()
|
||||
.flex_none()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.h_full()
|
||||
.when(controls.minimize, |row| {
|
||||
row.child(caption_button(
|
||||
cx,
|
||||
SharedString::from(format!("{id}-minimize")),
|
||||
IconRole::WindowMinimize,
|
||||
WindowControlArea::Min,
|
||||
window.is_minimizable(),
|
||||
false,
|
||||
))
|
||||
})
|
||||
.when(controls.maximize, |row| {
|
||||
row.child(caption_button(
|
||||
cx,
|
||||
SharedString::from(format!("{id}-maximize")),
|
||||
if window.is_maximized() {
|
||||
IconRole::WindowRestore
|
||||
} else {
|
||||
IconRole::WindowMaximize
|
||||
},
|
||||
WindowControlArea::Max,
|
||||
window.is_resizable(),
|
||||
false,
|
||||
))
|
||||
})
|
||||
.child(caption_button(
|
||||
cx,
|
||||
SharedString::from(format!("{id}-close")),
|
||||
IconRole::Close,
|
||||
WindowControlArea::Close,
|
||||
true,
|
||||
true,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = id;
|
||||
let controls = window.window_controls();
|
||||
let client_decorated =
|
||||
matches!(window.window_decorations(), Decorations::Client { .. });
|
||||
|
||||
div()
|
||||
.flex_none()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.when(client_decorated && controls.minimize, |row| {
|
||||
row.child(circle_button(
|
||||
cx,
|
||||
"window-minimize",
|
||||
IconRole::WindowMinimize,
|
||||
window.is_minimizable(),
|
||||
|window, _cx| window.minimize_window(),
|
||||
))
|
||||
})
|
||||
.when(client_decorated && controls.maximize, |row| {
|
||||
row.child(circle_button(
|
||||
cx,
|
||||
"window-maximize",
|
||||
if window.is_maximized() {
|
||||
IconRole::WindowRestore
|
||||
} else {
|
||||
IconRole::WindowMaximize
|
||||
},
|
||||
window.is_resizable(),
|
||||
|window, _cx| window.zoom_window(),
|
||||
))
|
||||
})
|
||||
.when(client_decorated, |row| {
|
||||
row.child(circle_button(
|
||||
cx,
|
||||
"window-close",
|
||||
IconRole::Close,
|
||||
true,
|
||||
|window, _cx| window.remove_window(),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Double-click on a titlebar strip maximizes where the platform does not do it.
|
||||
pub fn maximize_on_double_click() -> impl Fn(&ClickEvent, &mut Window, &mut App) + 'static {
|
||||
move |event, window, _cx| {
|
||||
if event.click_count() != 2 {
|
||||
return;
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
window.titlebar_double_click();
|
||||
#[cfg(target_os = "linux")]
|
||||
window.zoom_window();
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let _ = (event, window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One Windows caption button.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn caption_button(
|
||||
cx: &App,
|
||||
id: ElementId,
|
||||
role: IconRole,
|
||||
area: WindowControlArea,
|
||||
enabled: bool,
|
||||
close: bool,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let glyph_size = theme.sizes.small.caption_size;
|
||||
let (close_r, close_g, close_b) = WINDOWS_CLOSE_HOVER;
|
||||
|
||||
let close_hover = rgba((
|
||||
(close_r * 255.0) as u8,
|
||||
(close_g * 255.0) as u8,
|
||||
(close_b * 255.0) as u8,
|
||||
0xff,
|
||||
));
|
||||
let hover = if close {
|
||||
close_hover
|
||||
} else {
|
||||
theme.colors.muted_hover
|
||||
};
|
||||
|
||||
div()
|
||||
.id(id)
|
||||
.debug_selector({
|
||||
let id = SharedString::from(format!("window-control-{}", role.slug()));
|
||||
move || id.to_string()
|
||||
})
|
||||
.occlude()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.w(px(CAPTION_WIDTH))
|
||||
.h_full()
|
||||
.window_control_area(area)
|
||||
.text_color(if enabled {
|
||||
theme.colors.foreground
|
||||
} else {
|
||||
theme.colors.muted_foreground
|
||||
})
|
||||
.when(enabled, |button| {
|
||||
button
|
||||
.cursor_pointer()
|
||||
.hover(|hovered| {
|
||||
hovered.bg(hover).text_color(if close {
|
||||
gpui::white()
|
||||
} else {
|
||||
theme.colors.foreground
|
||||
})
|
||||
})
|
||||
.active(|pressed| {
|
||||
pressed
|
||||
.bg(if close {
|
||||
hover.opacity(0.8)
|
||||
} else {
|
||||
theme.ramps.secondary.pressed
|
||||
})
|
||||
.text_color(theme.colors.foreground)
|
||||
})
|
||||
})
|
||||
.child(icon(role).size(glyph_size))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Adwaita's headerbar control: a small circle around a symbolic glyph.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn circle_button<F>(
|
||||
cx: &App,
|
||||
id: &'static str,
|
||||
role: IconRole,
|
||||
enabled: bool,
|
||||
action: F,
|
||||
) -> AnyElement
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) + 'static,
|
||||
{
|
||||
let theme = cx.theme();
|
||||
let small = theme.sizes.small;
|
||||
|
||||
div()
|
||||
.id(id)
|
||||
.debug_selector(move || format!("window-control-{}", role.slug()))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(small.control_height)
|
||||
.rounded(theme.radius.full)
|
||||
.text_color(if enabled {
|
||||
theme.colors.foreground
|
||||
} else {
|
||||
theme.colors.muted_foreground
|
||||
})
|
||||
.when(enabled, |button| {
|
||||
button
|
||||
.cursor_pointer()
|
||||
.hover(|hovered| hovered.bg(theme.colors.muted_hover))
|
||||
.on_click(move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
action(window, cx);
|
||||
})
|
||||
})
|
||||
.child(icon(role).size(small.dense_icon_size))
|
||||
.into_any_element()
|
||||
}
|
||||
Reference in New Issue
Block a user