T04 window and shell: chrome, bars, rail, tabs, windows, dialogs, tray, badge, zoom
This commit is contained in:
37 files changed
+4516
-111
No files matched your search
@@ -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:#}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user