Files
gpui-native-kit/crates/layout/tests/dock.rs
T

541 lines
18 KiB
Rust

use gpui::{
AppContext as _, Context, Entity, InteractiveElement as _, IntoElement, Modifiers, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Render, Styled as _,
TestAppContext, VisualTestContext, Window, div, point, px,
};
use gpui_native_layout::dock::{Dock, DockSidebar, DockState, PanelDescriptor, SplitDirection};
use gpui_native_theme::Theme;
use std::cell::Cell;
use std::rc::Rc;
struct Panel {
tag: &'static str,
}
impl Render for Panel {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.debug_selector(move || format!("panel-{}", self.tag))
}
}
/// The right sidebar view a panel declares.
struct SidePanel {
tag: &'static str,
}
impl Render for SidePanel {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.debug_selector(move || format!("side-{}", self.tag))
}
}
fn descriptor(tag: &'static str) -> PanelDescriptor {
PanelDescriptor::new(tag, tag, move |_, cx| cx.new(|_| Panel { tag }).into())
}
fn descriptor_with_right_sidebar(tag: &'static str) -> PanelDescriptor {
PanelDescriptor::new(tag, tag, move |_, cx| cx.new(|_| Panel { tag }).into())
.right_sidebar(move |_, cx| cx.new(|_| SidePanel { tag }).into())
}
struct Harness {
dock: Entity<DockState>,
}
impl Render for Harness {
fn render(
&mut self,
_window: &mut Window,
_cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
div()
.size_full()
.child(Dock::new("test-dock", self.dock.clone()))
}
}
fn dock_window(cx: &mut TestAppContext) -> (Entity<DockState>, VisualTestContext) {
cx.update(Theme::init);
let dock = cx.update(DockState::new);
let dock_for_window = dock.clone();
let window = cx.add_window(move |_, _| Harness {
dock: dock_for_window,
});
let mut visual = VisualTestContext::from_window(window.into(), cx);
redraw(&mut visual);
(dock, visual)
}
fn redraw(visual: &mut VisualTestContext) {
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
}
fn bounds(visual: &mut VisualTestContext, selector: &str) -> Option<gpui::Bounds<Pixels>> {
visual.debug_bounds(Box::leak(selector.to_string().into_boxed_str()))
}
fn center_of(bounds: gpui::Bounds<Pixels>) -> gpui::Point<Pixels> {
point(bounds.center().x, bounds.center().y)
}
fn press(visual: &mut VisualTestContext, at: gpui::Point<Pixels>, click_count: usize) {
visual.simulate_event(MouseDownEvent {
position: at,
click_count,
modifiers: Modifiers::default(),
button: MouseButton::Left,
first_mouse: false,
});
visual.simulate_event(MouseUpEvent {
position: at,
click_count,
modifiers: Modifiers::default(),
button: MouseButton::Left,
});
}
fn drag(visual: &mut VisualTestContext, from: gpui::Point<Pixels>, to: gpui::Point<Pixels>) {
visual.simulate_mouse_down(from, MouseButton::Left, Modifiers::default());
visual.simulate_event(MouseMoveEvent {
position: to,
pressed_button: Some(MouseButton::Left),
modifiers: Modifiers::default(),
});
visual.simulate_mouse_up(to, MouseButton::Left, Modifiers::default());
}
fn register(dock: &Entity<DockState>, tags: &[&'static str], cx: &mut TestAppContext) {
cx.update(|cx| {
dock.update(cx, |dock, cx| {
for tag in tags {
dock.register_panel(descriptor(tag), cx);
}
});
});
}
fn open(visual: &mut VisualTestContext, dock: &Entity<DockState>, tag: &'static str) {
visual.update(|window, cx| {
dock.update(cx, |dock, cx| dock.open_panel(tag, window, cx));
});
redraw(visual);
}
#[test]
fn opening_a_panel_adds_its_tab_and_activates_it() {
let mut cx = TestAppContext::single();
let (dock, mut visual) = dock_window(&mut cx);
register(&dock, &["inbox", "explore"], &mut cx);
open(&mut visual, &dock, "inbox");
assert!(
bounds(&mut visual, "panel-inbox").is_some(),
"opened panel missing"
);
assert!(
bounds(&mut visual, "test-dock-pane-0-tabs-tab-inbox").is_some(),
"tab missing"
);
open(&mut visual, &dock, "explore");
assert!(bounds(&mut visual, "panel-explore").is_some());
assert!(
bounds(&mut visual, "panel-inbox").is_none(),
"the inactive view must not render"
);
assert!(
bounds(&mut visual, "test-dock-pane-0-tabs-tab-inbox").is_some(),
"the strip keeps both tabs"
);
assert!(dock.read_with(&cx, |dock, _| dock.is_open("inbox")));
assert!(dock.read_with(&cx, |dock, _| dock.is_open("explore")));
}
#[test]
fn clicking_a_tab_activates_its_panel() {
let mut cx = TestAppContext::single();
let (dock, mut visual) = dock_window(&mut cx);
register(&dock, &["inbox", "explore"], &mut cx);
open(&mut visual, &dock, "inbox");
open(&mut visual, &dock, "explore");
let tab = bounds(&mut visual, "test-dock-pane-0-tabs-tab-inbox").expect("inbox tab missing");
press(&mut visual, center_of(tab), 1);
redraw(&mut visual);
assert!(bounds(&mut visual, "panel-inbox").is_some());
assert!(bounds(&mut visual, "panel-explore").is_none());
}
#[test]
fn the_tab_close_control_closes_its_panel() {
let mut cx = TestAppContext::single();
let (dock, mut visual) = dock_window(&mut cx);
register(&dock, &["inbox", "explore"], &mut cx);
open(&mut visual, &dock, "inbox");
open(&mut visual, &dock, "explore");
let close = bounds(&mut visual, "test-dock-pane-0-tabs-close-inbox").expect("close missing");
press(&mut visual, center_of(close), 1);
redraw(&mut visual);
assert!(bounds(&mut visual, "test-dock-pane-0-tabs-tab-inbox").is_none());
assert!(bounds(&mut visual, "panel-explore").is_some());
}
#[test]
fn closing_the_active_tab_activates_the_neighbor() {
let mut cx = TestAppContext::single();
let (dock, mut visual) = dock_window(&mut cx);
register(&dock, &["inbox", "explore"], &mut cx);
open(&mut visual, &dock, "inbox");
open(&mut visual, &dock, "explore");
cx.update(|cx| {
dock.update(cx, |dock, cx| dock.close_panel("explore", cx));
});
redraw(&mut visual);
assert!(
bounds(&mut visual, "panel-inbox").is_some(),
"the neighbor tab should activate"
);
assert!(bounds(&mut visual, "test-dock-pane-0-tabs-tab-explore").is_none());
assert!(bounds(&mut visual, "test-dock-pane-0-tabs-tab-inbox").is_some());
}
#[test]
fn splitting_moves_the_tab_and_closing_collapses() {
let mut cx = TestAppContext::single();
let (dock, mut visual) = dock_window(&mut cx);
register(&dock, &["inbox", "explore"], &mut cx);
open(&mut visual, &dock, "inbox");
open(&mut visual, &dock, "explore");
cx.update(|cx| {
dock.update(cx, |dock, cx| {
dock.split_panel("explore", SplitDirection::Right, cx)
});
});
redraw(&mut visual);
assert!(
bounds(&mut visual, "resize-handle").is_some(),
"the split renders a seam"
);
assert!(bounds(&mut visual, "test-dock-pane-0").is_some());
assert!(bounds(&mut visual, "test-dock-pane-1").is_some());
assert!(
bounds(&mut visual, "panel-explore").is_some(),
"the moved tab activates in its new pane"
);
// Closing the new pane's only tab collapses its split.
cx.update(|cx| {
dock.update(cx, |dock, cx| dock.close_panel("explore", cx));
});
redraw(&mut visual);
assert!(bounds(&mut visual, "resize-handle").is_none());
assert!(bounds(&mut visual, "test-dock-pane-1").is_none());
assert!(bounds(&mut visual, "test-dock-pane-0").is_some());
assert!(bounds(&mut visual, "panel-inbox").is_some());
}
#[test]
fn splitting_at_a_target_pane_places_the_new_pane_there() {
let mut cx = TestAppContext::single();
let (dock, mut visual) = dock_window(&mut cx);
register(&dock, &["inbox", "explore"], &mut cx);
open(&mut visual, &dock, "inbox");
open(&mut visual, &dock, "explore");
cx.update(|cx| {
dock.update(cx, |dock, cx| {
dock.split_panel("explore", SplitDirection::Right, cx)
});
});
redraw(&mut visual);
// Move inbox into a new pane left of explore's pane (pane 1), not beside
// inbox's own (pane 0, which empties and collapses).
cx.update(|cx| {
dock.update(cx, |dock, cx| {
dock.split_panel_at("inbox", "explore", SplitDirection::Left, cx)
});
});
redraw(&mut visual);
let moved = bounds(&mut visual, "test-dock-pane-3").expect("the new pane missing");
let target = bounds(&mut visual, "test-dock-pane-1").expect("target pane missing");
assert!(
moved.right() <= target.left(),
"the new pane must sit left of the anchored pane"
);
assert!(bounds(&mut visual, "test-dock-pane-0").is_none());
}
#[test]
fn dragging_the_seam_resizes_and_double_click_resets() {
let mut cx = TestAppContext::single();
let (dock, mut visual) = dock_window(&mut cx);
register(&dock, &["inbox", "explore"], &mut cx);
open(&mut visual, &dock, "inbox");
open(&mut visual, &dock, "explore");
cx.update(|cx| {
dock.update(cx, |dock, cx| {
dock.split_panel("explore", SplitDirection::Right, cx)
});
});
redraw(&mut visual);
let before_0 = bounds(&mut visual, "test-dock-pane-0").expect("pane 0 missing");
let before_1 = bounds(&mut visual, "test-dock-pane-1").expect("pane 1 missing");
let seam = bounds(&mut visual, "resize-handle").expect("seam missing");
drag(
&mut visual,
center_of(seam),
center_of(seam) + point(px(40.), px(0.)),
);
let grown = bounds(&mut visual, "test-dock-pane-0").expect("pane 0 missing");
let shrunk = bounds(&mut visual, "test-dock-pane-1").expect("pane 1 missing");
assert!(
grown.size.width > before_0.size.width,
"the seam leads pane 0"
);
assert!(shrunk.size.width < before_1.size.width, "pane 1 gives way");
// Double-clicking the seam restores the default ratio.
let seam = bounds(&mut visual, "resize-handle").expect("seam missing");
press(&mut visual, center_of(seam), 2);
redraw(&mut visual);
let reset_0 = bounds(&mut visual, "test-dock-pane-0").expect("pane 0 missing");
let reset_1 = bounds(&mut visual, "test-dock-pane-1").expect("pane 1 missing");
// The flex split may round an odd-pixel span by a hair.
assert!((reset_0.size.width - reset_1.size.width).abs() <= px(1.));
// A huge drag clamps instead of vanishing a side.
let seam = bounds(&mut visual, "resize-handle").expect("seam missing");
drag(
&mut visual,
center_of(seam),
center_of(seam) + point(px(2000.), px(0.)),
);
let clamped = bounds(&mut visual, "test-dock-pane-1").expect("clamped pane missing");
assert!(clamped.size.width > px(0.));
}
#[test]
fn dragging_a_tab_to_a_pane_edge_splits_it() {
let mut cx = TestAppContext::single();
let (dock, mut visual) = dock_window(&mut cx);
register(&dock, &["inbox", "explore"], &mut cx);
open(&mut visual, &dock, "inbox");
open(&mut visual, &dock, "explore");
// Drag explore's tab onto the right edge band of the pane's content.
let tab = bounds(&mut visual, "test-dock-pane-0-tabs-tab-explore").expect("tab missing");
let content = bounds(&mut visual, "test-dock-content-0").expect("content missing");
let drop_at = point(
content.right() - content.size.width * 0.125,
content.center().y,
);
visual.simulate_mouse_down(center_of(tab), MouseButton::Left, Modifiers::default());
// Past the drag threshold, the drag starts and the drop zones mount.
visual.simulate_event(MouseMoveEvent {
position: center_of(tab) + point(px(8.), px(2.)),
pressed_button: Some(MouseButton::Left),
modifiers: Modifiers::default(),
});
redraw(&mut visual);
visual.simulate_event(MouseMoveEvent {
position: drop_at,
pressed_button: Some(MouseButton::Left),
modifiers: Modifiers::default(),
});
visual.simulate_mouse_up(drop_at, MouseButton::Left, Modifiers::default());
redraw(&mut visual);
assert!(
bounds(&mut visual, "resize-handle").is_some(),
"the drop should have split the pane"
);
assert!(
bounds(&mut visual, "test-dock-pane-1-tabs-tab-explore").is_some(),
"the dragged tab lives in the new pane"
);
assert!(bounds(&mut visual, "panel-explore").is_some());
}
#[test]
fn moving_a_panel_merges_panes_and_collapses_the_source() {
let mut cx = TestAppContext::single();
let (dock, mut visual) = dock_window(&mut cx);
register(&dock, &["inbox", "explore"], &mut cx);
open(&mut visual, &dock, "inbox");
open(&mut visual, &dock, "explore");
cx.update(|cx| {
dock.update(cx, |dock, cx| {
dock.split_panel("explore", SplitDirection::Right, cx)
});
});
redraw(&mut visual);
cx.update(|cx| {
dock.update(cx, |dock, cx| dock.move_panel("explore", "inbox", cx));
});
redraw(&mut visual);
assert!(
bounds(&mut visual, "resize-handle").is_none(),
"the emptied source pane collapses"
);
assert!(bounds(&mut visual, "test-dock-pane-0-tabs-tab-inbox").is_some());
assert!(bounds(&mut visual, "test-dock-pane-0-tabs-tab-explore").is_some());
assert!(
bounds(&mut visual, "panel-explore").is_some(),
"the moved tab activates in its new pane"
);
}
#[test]
fn close_others_keeps_one_and_close_pane_empties() {
let mut cx = TestAppContext::single();
let (dock, mut visual) = dock_window(&mut cx);
register(&dock, &["inbox", "explore", "settings"], &mut cx);
open(&mut visual, &dock, "inbox");
open(&mut visual, &dock, "explore");
open(&mut visual, &dock, "settings");
cx.update(|cx| {
dock.update(cx, |dock, cx| dock.close_others("inbox", cx));
});
redraw(&mut visual);
assert!(bounds(&mut visual, "test-dock-pane-0-tabs-tab-inbox").is_some());
assert!(bounds(&mut visual, "test-dock-pane-0-tabs-tab-explore").is_none());
assert!(bounds(&mut visual, "test-dock-pane-0-tabs-tab-settings").is_none());
assert!(bounds(&mut visual, "panel-inbox").is_some());
cx.update(|cx| {
dock.update(cx, |dock, cx| dock.close_pane("inbox", cx));
});
redraw(&mut visual);
assert!(bounds(&mut visual, "test-dock-pane-0-tabs-tab-inbox").is_none());
assert!(
bounds(&mut visual, "panel-inbox").is_none(),
"the emptied pane renders nothing"
);
// The dock is reusable: opening again fills the empty root pane.
open(&mut visual, &dock, "inbox");
assert!(bounds(&mut visual, "test-dock-pane-0-tabs-tab-inbox").is_some());
}
struct SidebarHarness {
dock: Entity<DockState>,
}
impl Render for SidebarHarness {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.flex()
.flex_row()
.child(
div()
.flex_1()
.child(Dock::new("test-dock", self.dock.clone())),
)
.child(
div()
.w(px(160.))
.child(DockSidebar::new("test-dock-sidebar", self.dock.clone())),
)
}
}
fn sidebar_window(
cx: &mut TestAppContext,
) -> (Entity<DockState>, Rc<Cell<bool>>, VisualTestContext) {
cx.update(Theme::init);
let dock = cx.update(DockState::new);
let opened = Rc::new(Cell::new(false));
let opened_for_dock = opened.clone();
let dock_for_window = dock.clone();
let window = cx.add_window(move |_, _| SidebarHarness {
dock: dock_for_window,
});
dock.update(cx, |dock, _| {
dock.on_open_right_sidebar(move |cx| {
opened_for_dock.set(true);
let _ = cx;
});
});
let mut visual = VisualTestContext::from_window(window.into(), cx);
visual.update(|window, cx| window.draw(cx).clear(cx));
(dock, opened, visual)
}
#[test]
fn activating_a_panel_with_a_right_view_opens_the_sidebar() {
let mut cx = TestAppContext::single();
let (dock, opened, mut visual) = sidebar_window(&mut cx);
cx.update(|cx| {
dock.update(cx, |dock, cx| {
dock.register_panel(descriptor_with_right_sidebar("overview"), cx);
dock.register_panel(descriptor("inbox"), cx);
});
});
open(&mut visual, &dock, "overview");
assert!(
opened.get(),
"activation must ask the app to open the sidebar"
);
assert!(
bounds(&mut visual, "side-overview").is_some(),
"the sidebar shows the panel's own view"
);
// A panel without a right view leaves the sidebar showing the last one.
open(&mut visual, &dock, "inbox");
assert!(opened.get());
assert!(bounds(&mut visual, "side-overview").is_some());
}
#[test]
fn closing_the_displayed_tab_falls_back_to_another_right_view() {
let mut cx = TestAppContext::single();
let (dock, opened, mut visual) = sidebar_window(&mut cx);
cx.update(|cx| {
dock.update(cx, |dock, cx| {
dock.register_panel(descriptor_with_right_sidebar("overview"), cx);
dock.register_panel(descriptor_with_right_sidebar("explore"), cx);
});
});
open(&mut visual, &dock, "overview");
open(&mut visual, &dock, "explore");
assert!(bounds(&mut visual, "side-explore").is_some());
cx.update(|cx| {
dock.update(cx, |dock, cx| dock.close_panel("explore", cx));
});
redraw(&mut visual);
assert!(
bounds(&mut visual, "side-overview").is_some(),
"the sidebar falls back to a remaining panel's view"
);
let _ = opened;
}