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,249 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, ElementId, EntityId, FontWeight, InteractiveElement, IntoElement,
|
||||
MouseButton, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
|
||||
Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
buttons::IconButton,
|
||||
motion,
|
||||
primitives::{DragGhost, Icon, IconName},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
|
||||
};
|
||||
|
||||
/// One window-level tab.
|
||||
#[derive(Clone)]
|
||||
pub struct WindowTab {
|
||||
id: SharedString,
|
||||
title: SharedString,
|
||||
icon: Option<IconName>,
|
||||
}
|
||||
|
||||
impl WindowTab {
|
||||
pub fn new(id: impl Into<SharedString>, title: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
title: title.into(),
|
||||
icon: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct TabDrag {
|
||||
owner: EntityId,
|
||||
from: usize,
|
||||
}
|
||||
|
||||
type OnTab = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
|
||||
type OnAdd = Rc<dyn Fn(&mut Window, &mut App)>;
|
||||
type OnReorder = Rc<dyn Fn(usize, usize, &mut Window, &mut App)>;
|
||||
|
||||
/// Browser-style tabs across the top of a window.
|
||||
#[derive(IntoElement)]
|
||||
pub struct TabBar {
|
||||
id: ElementId,
|
||||
tabs: Vec<WindowTab>,
|
||||
selected: Option<SharedString>,
|
||||
on_select: Option<OnTab>,
|
||||
on_close: Option<OnTab>,
|
||||
on_add: Option<OnAdd>,
|
||||
on_reorder: Option<OnReorder>,
|
||||
}
|
||||
|
||||
impl TabBar {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
tabs: Vec::new(),
|
||||
selected: None,
|
||||
on_select: None,
|
||||
on_close: None,
|
||||
on_add: None,
|
||||
on_reorder: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tab(mut self, tab: WindowTab) -> Self {
|
||||
self.tabs.push(tab);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, id: impl Into<SharedString>) -> Self {
|
||||
self.selected = Some(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_select(
|
||||
mut self,
|
||||
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_select = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Shows a close button on the selected and hovered tab.
|
||||
pub fn on_close(
|
||||
mut self,
|
||||
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_close = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Shows a new-tab button after the tabs.
|
||||
pub fn on_add(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_add = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Lets tabs drag; reports `(from, to)` indexes on drop.
|
||||
pub fn on_reorder(
|
||||
mut self,
|
||||
handler: impl Fn(usize, usize, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_reorder = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TabBar {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let owner = window
|
||||
.use_keyed_state(self.id.clone(), cx, |_, _| ())
|
||||
.entity_id();
|
||||
let theme = cx.theme();
|
||||
let (narrow, wide) = theme.tab_width();
|
||||
let enter = motion::duration(motion::BASE, cx);
|
||||
let nudge = motion::NUDGE;
|
||||
let tabs = self.tabs.into_iter().enumerate().map(|(ix, tab)| {
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let selected = self.selected.as_ref() == Some(&tab.id);
|
||||
let group = SharedString::from(format!("window-tab-{}", tab.id));
|
||||
let (select, close, reorder) = (
|
||||
self.on_select.clone(),
|
||||
self.on_close.clone(),
|
||||
self.on_reorder.clone(),
|
||||
);
|
||||
let (id, close_id) = (tab.id.clone(), tab.id.clone());
|
||||
let (title, icon) = (tab.title.clone(), tab.icon);
|
||||
let (lit, edge) = (colors.hover, colors.focus);
|
||||
div()
|
||||
.id(tab.id.clone())
|
||||
.group(group.clone())
|
||||
.flex()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.min_w(narrow)
|
||||
.max_w(wide)
|
||||
.h(theme.control_height(ControlSize::Md))
|
||||
.pl_3()
|
||||
.pr_1()
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.border_1()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.map(|el| {
|
||||
if selected {
|
||||
el.bg(colors.surface)
|
||||
.border_color(colors.border)
|
||||
.text_color(colors.fg)
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
} else {
|
||||
el.border_color(gpui::transparent_black())
|
||||
.text_color(colors.fg_muted)
|
||||
.hover(|style| style.bg(lit))
|
||||
}
|
||||
})
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.on_click(move |_, window, cx| {
|
||||
log::info!("tab bar: select {id}");
|
||||
if let Some(select) = &select {
|
||||
select(&id, window, cx);
|
||||
}
|
||||
})
|
||||
.when_some(reorder, |el, reorder| {
|
||||
el.on_drag(TabDrag { owner, from: ix }, move |_, _, _, cx| {
|
||||
DragGhost::new(title.clone(), icon, cx)
|
||||
})
|
||||
.drag_over::<TabDrag>(move |style, drag, _, _| {
|
||||
if drag.owner == owner {
|
||||
style.border_color(edge)
|
||||
} else {
|
||||
style
|
||||
}
|
||||
})
|
||||
.on_drop(move |drag: &TabDrag, window, cx| {
|
||||
if drag.owner == owner && drag.from != ix {
|
||||
log::info!("tab bar: move {} -> {ix}", drag.from);
|
||||
reorder(drag.from, ix, window, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.when_some(tab.icon, |el, icon| {
|
||||
el.child(Icon::new(icon).size(IconSize::Sm).color(if selected {
|
||||
colors.fg
|
||||
} else {
|
||||
colors.fg_subtle
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.overflow_hidden()
|
||||
.whitespace_nowrap()
|
||||
.text_ellipsis()
|
||||
.child(tab.title.clone()),
|
||||
)
|
||||
.when_some(close, |el, close| {
|
||||
el.child(
|
||||
div()
|
||||
.when(!selected, |slot| {
|
||||
slot.invisible().group_hover(group, |style| style.visible())
|
||||
})
|
||||
.child(
|
||||
IconButton::new(("close", ix), IconName::X)
|
||||
.size(ControlSize::Sm)
|
||||
.on_click(move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
log::info!("tab bar: close {close_id}");
|
||||
close(&close_id, window, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
.with_animation(
|
||||
SharedString::from(format!("tab-in-{}", tab.id)),
|
||||
Animation::new(enter).with_easing(motion::ease_out_cubic),
|
||||
move |el, t| el.opacity(t).mt(nudge * (1.0 - t)),
|
||||
)
|
||||
});
|
||||
div()
|
||||
.id(self.id)
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.min_w_0()
|
||||
.children(tabs)
|
||||
.when_some(self.on_add, |bar, add| {
|
||||
bar.child(
|
||||
IconButton::new("new-tab", IconName::Plus)
|
||||
.size(ControlSize::Sm)
|
||||
.on_click(move |_, window, cx| {
|
||||
log::info!("tab bar: new tab");
|
||||
add(window, cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user