Files
Ely-GPUI-Components/src/shell/switcher.rs
T

167 lines
5.9 KiB
Rust

use std::rc::Rc;
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, ElementId, Entity, InteractiveElement,
IntoElement, ParentElement, RenderOnce, StatefulInteractiveElement, Styled, Window, div,
prelude::*,
};
use super::WindowManager;
use crate::{
motion,
primitives::{Backdrop, FocusNext, FocusPrev, Icon, IconName, Place, give_back, take_focus},
theme::{ActiveTheme, ControlSize, Elevation, IconSize, Radius, TextSize},
typography::Caption,
};
#[derive(Default)]
struct Switch {
selected: usize,
}
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, |_, _| Switch::default());
let takeover = take_focus(self.id.clone(), window, cx);
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 (takeover, on_close) = (takeover.clone(), self.on_close);
Rc::new(move |window, cx| {
give_back(&takeover, window, cx);
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, chosen) =
(state.clone(), state.clone(), state.clone(), state.clone());
let (close_keys, close_enter, close_scrim) = (close.clone(), close.clone(), close);
let focus = takeover.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),
"escape" => close_keys(window, cx),
"enter" if !event.keystroke.modifiers.modified() => {}
_ => return,
}
cx.stop_propagation();
})
.on_key_up(move |event, window, cx| {
let stroke = &event.keystroke;
if stroke.key == "enter" && !stroke.modifiers.modified() {
cx.stop_propagation();
if let Some(handle) = handles.get(chosen.read(cx).selected) {
switch_to(*handle, &close_enter, window, cx);
}
}
})
.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:#}");
}
}