423 lines
14 KiB
Rust
423 lines
14 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, DockState, PanelDescriptor, SplitDirection};
|
|
use gpui_native_theme::Theme;
|
|
|
|
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))
|
|
}
|
|
}
|
|
|
|
fn descriptor(tag: &'static str) -> PanelDescriptor {
|
|
PanelDescriptor::new(tag, tag, move |_, cx| cx.new(|_| Panel { 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());
|
|
}
|