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

+180
View File
@@ -0,0 +1,180 @@
use std::rc::Rc;
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, ElementId, Entity, FocusHandle,
InteractiveElement, IntoElement, ParentElement, RenderOnce, StatefulInteractiveElement, Styled,
Window, div, prelude::*,
};
use super::WindowManager;
use crate::{
motion,
primitives::{Backdrop, FocusNext, FocusPrev, Icon, IconName, Place},
theme::{ActiveTheme, ControlSize, Elevation, IconSize, Radius, TextSize},
typography::Caption,
};
struct Switch {
selected: usize,
focus: FocusHandle,
opened: bool,
previous: Option<FocusHandle>,
}
type Close = Rc<dyn Fn(&mut Window, &mut App)>;
/// Lists managed windows over a scrim. Arrows or Tab move, Enter switches.
#[derive(IntoElement)]
pub struct WindowSwitcher {
id: ElementId,
on_close: Close,
}
impl WindowSwitcher {
pub fn new(
id: impl Into<ElementId>,
on_close: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
on_close: Rc::new(on_close),
}
}
}
fn step(state: &Entity<Switch>, by: isize, count: usize, cx: &mut App) {
if count == 0 {
return;
}
state.update(cx, |switch, cx| {
switch.selected = (switch.selected as isize + by).rem_euclid(count as isize) as usize;
cx.notify();
});
}
impl RenderOnce for WindowSwitcher {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let windows = WindowManager::windows(cx);
let count = windows.len();
let state = window.use_keyed_state(self.id.clone(), cx, |_, cx| Switch {
selected: 0,
focus: cx.focus_handle(),
opened: false,
previous: None,
});
if !state.read(cx).opened {
let previous = window.focused(cx);
window.focus(&state.read(cx).focus.clone());
state.update(cx, |switch, _| {
switch.opened = true;
switch.previous = previous;
});
}
if count > 0 && state.read(cx).selected >= count {
state.update(cx, |switch, _| switch.selected = count - 1);
}
let selected = state.read(cx).selected;
let close: Close = {
let (state, on_close) = (state.clone(), self.on_close);
Rc::new(move |window, cx| {
match state.read(cx).previous.clone() {
Some(previous) => {
window.focus(&previous);
log::info!("window switcher: closed, focus handed back");
}
None => log::info!("window switcher: closed, nothing was focused before"),
}
on_close(window, cx)
})
};
let theme = cx.theme();
let colors = &theme.colors;
let rows: Vec<_> = windows
.iter()
.enumerate()
.map(|(ix, (handle, title))| {
let (handle, close) = (*handle, close.clone());
div()
.id(("switch", ix))
.flex()
.items_center()
.gap_3()
.h(theme.control_height(ControlSize::Lg))
.px_3()
.rounded(theme.radius(Radius::Md))
.text_size(theme.text_size(TextSize::Base))
.when(ix == selected, |row| row.bg(colors.hover))
.cursor_pointer()
.on_click(move |_, window, cx| switch_to(handle, &close, window, cx))
.child(
Icon::new(IconName::AppWindow)
.size(IconSize::Sm)
.color(colors.fg_muted),
)
.child(title.clone())
})
.collect();
let handles: Vec<AnyWindowHandle> = windows.iter().map(|(handle, _)| *handle).collect();
let (next, prev, keys) = (state.clone(), state.clone(), state.clone());
let (close_keys, close_scrim) = (close.clone(), close);
let focus = state.read(cx).focus.clone();
let panel = div()
.id("window-switcher")
.track_focus(&focus)
.w(theme.sheet_size())
.p_2()
.flex()
.flex_col()
.gap_0p5()
.rounded(theme.radius(Radius::Xl))
.bg(colors.overlay)
.border_1()
.border_color(colors.border)
.shadow(theme.elevation(Elevation::Modal))
.on_action(move |_: &FocusNext, _, cx| step(&next, 1, count, cx))
.on_action(move |_: &FocusPrev, _, cx| step(&prev, -1, count, cx))
.on_key_down(move |event, window, cx| {
match event.keystroke.key.as_str() {
"down" => step(&keys, 1, count, cx),
"up" => step(&keys, -1, count, cx),
"enter" => {
if let Some(handle) = handles.get(keys.read(cx).selected) {
switch_to(*handle, &close_keys, window, cx);
}
}
"escape" => close_keys(window, cx),
_ => return,
}
cx.stop_propagation();
})
.children(rows)
.when(count == 0, |panel| {
panel.child(
div()
.p_3()
.child(Caption::new("No managed windows are open.")),
)
})
.with_animation(
"switcher-in",
Animation::new(motion::duration(motion::BASE, cx))
.with_easing(motion::ease_out_cubic),
|panel, t| panel.opacity(t).mt(motion::NUDGE * (1.0 - t)),
);
Backdrop::new(self.id)
.place(Place::Center)
.on_dismiss(move |window, cx| close_scrim(window, cx))
.child(panel)
}
}
fn switch_to(handle: AnyWindowHandle, close: &Close, window: &mut Window, cx: &mut App) {
close(window, cx);
if handle == window.window_handle() {
window.activate_window();
return;
}
if let Err(error) = WindowManager::focus(handle, cx) {
log::error!("window switcher: {error:#}");
}
}