use gpui::{ Entity, InteractiveElement as _, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Render, Styled as _, TestAppContext, VisualTestContext, Window, div, point, px, }; use gpui_native_data::sidebar::Sidebar; use gpui_native_layout::shell::{LayoutState, Shell}; use gpui_native_theme::{ActiveTheme as _, Size, Theme}; struct Harness { state: Entity, } impl Render for Harness { fn render( &mut self, _window: &mut Window, _cx: &mut gpui::Context, ) -> impl gpui::IntoElement { div().size_full().child( Shell::new("test-layout", self.state.clone()) .left_sidebar(Sidebar::new("test-left")) .center( div() .size_full() .debug_selector(|| "test-center".to_string()), ) .right_sidebar( div() .size_full() .debug_selector(|| "test-right".to_string()), ), ) } } fn layout_window(cx: &mut TestAppContext) -> (Entity, VisualTestContext) { cx.update(Theme::init); let state = cx.update(LayoutState::new); let state_for_window = state.clone(); let window = cx.add_window(move |_, _| Harness { state: state_for_window, }); let mut visual = VisualTestContext::from_window(window.into(), cx); redraw(&mut visual); (state, visual) } fn redraw(visual: &mut VisualTestContext) { visual.update(|window, cx| { window.draw(cx).clear(cx); }); } fn bounds(visual: &mut VisualTestContext, selector: &'static str) -> Option> { visual.debug_bounds(selector) } fn center_of(bounds: gpui::Bounds) -> gpui::Point { point(bounds.center().x, bounds.center().y) } fn press(visual: &mut VisualTestContext, at: gpui::Point, 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, to: gpui::Point) { 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()); } #[test] fn left_sidebar_starts_visible_and_right_hidden() { let mut cx = TestAppContext::single(); let (state, mut visual) = layout_window(&mut cx); assert!(state.read_with(&cx, |state, _| state.is_left_sidebar_visible())); assert!(!state.read_with(&cx, |state, _| state.is_right_sidebar_visible())); assert!( bounds(&mut visual, "sidebar-test-left").is_some(), "left region missing" ); assert!( bounds(&mut visual, "test-center").is_some(), "center missing" ); assert!( bounds(&mut visual, "test-right").is_none(), "right region must be hidden by default" ); } #[test] fn toggling_a_sidebar_shows_and_hides_its_region() { let mut cx = TestAppContext::single(); let (state, mut visual) = layout_window(&mut cx); cx.update(|cx| { state.update(cx, |state, cx| state.set_right_sidebar_visible(true, cx)); }); redraw(&mut visual); assert!( bounds(&mut visual, "test-right").is_some(), "right region missing" ); cx.update(|cx| { state.update(cx, |state, cx| state.set_left_sidebar_visible(false, cx)); }); redraw(&mut visual); assert!( bounds(&mut visual, "sidebar-test-left").is_none(), "left region lingering" ); // The center keeps filling the shell. assert!( bounds(&mut visual, "test-center").is_some(), "center missing" ); } #[test] fn the_header_toggle_hides_its_sidebar() { let mut cx = TestAppContext::single(); let (state, mut visual) = layout_window(&mut cx); // The toggle sits at the trailing end of the sidebar's header strip, an // icon-only small button: derive its center from the theme metrics. let metrics = cx.update(|cx| cx.theme().sizes.resolve(Size::default())); let small = cx.update(|cx| cx.theme().sizes.resolve(Size::Small)); let region = bounds(&mut visual, "sidebar-test-left").expect("left sidebar missing"); // The Sidebar fills the content area below the strip; the toggle sits at // the strip's trailing end, one strip height above the sidebar's top. let toggle = point( region.right() - metrics.dense_padding_x - small.control_height / 2.0, region.top() - metrics.strip_height / 2.0, ); press(&mut visual, toggle, 1); redraw(&mut visual); assert!( !state.read_with(&cx, |state, _| state.is_left_sidebar_visible()), "the toggle should hide the left sidebar" ); assert!(bounds(&mut visual, "sidebar-test-left").is_none()); } #[test] fn dragging_the_seam_resizes_and_clamps() { let mut cx = TestAppContext::single(); let (state, mut visual) = layout_window(&mut cx); let before = state.read_with(&cx, |state, _| state.left_sidebar_width()); let seam = bounds(&mut visual, "resize-handle").expect("left seam missing"); drag( &mut visual, center_of(seam), center_of(seam) + point(px(50.), px(0.)), ); assert_eq!( state.read_with(&cx, |state, _| state.left_sidebar_width()), before + px(50.) ); // A huge drag clamps instead of collapsing the sidebar. let seam = bounds(&mut visual, "resize-handle").expect("seam missing"); drag( &mut visual, center_of(seam), center_of(seam) + point(px(2000.), px(0.)), ); assert_eq!( state.read_with(&cx, |state, _| state.left_sidebar_width()), px(480.) ); // Dragging back left shrinks it to the low clamp, no drift from the first. let seam = bounds(&mut visual, "resize-handle").expect("seam missing"); drag( &mut visual, center_of(seam), center_of(seam) - point(px(2000.), px(0.)), ); assert_eq!( state.read_with(&cx, |state, _| state.left_sidebar_width()), px(160.) ); } #[test] fn the_right_seam_grows_the_right_sidebar_leftwards() { let mut cx = TestAppContext::single(); let (state, mut visual) = layout_window(&mut cx); // Hide the left sidebar so the shell has exactly one seam. cx.update(|cx| { state.update(cx, |state, cx| state.set_left_sidebar_visible(false, cx)); state.update(cx, |state, cx| state.set_right_sidebar_visible(true, cx)); }); redraw(&mut visual); let before = state.read_with(&cx, |state, _| state.right_sidebar_width()); let seam = bounds(&mut visual, "resize-handle").expect("right seam missing"); // Dragging left (negative) grows the right sidebar. drag( &mut visual, center_of(seam), center_of(seam) - point(px(30.), px(0.)), ); let grown = state.read_with(&cx, |state, _| state.right_sidebar_width()); assert_eq!(grown, before + px(30.)); // The right region's width follows the state. let region = bounds(&mut visual, "test-right").expect("right region missing"); assert_eq!(region.size.width, grown); }