T04 window and shell: chrome, bars, rail, tabs, windows, dialogs, tray, badge, zoom

This commit is contained in:
ZacharyZhang-NY committed 2026-09-25 03:21:03 -04:00
1 parent 2bdf902bf4
commit 77bcd5be07
37 files changed
+4516 -111

No files matched your search

+422
View File
@@ -0,0 +1,422 @@
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)
}
}