use gpui::{ AppContext as _, Context, Entity, IntoElement, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement as _, Pixels, Render, Styled as _, Subscription, TestAppContext, VisualTestContext, Window, div, point, px, }; use gpui_native_layout::resizable::{Resizable, ResizablePane, ResizableState}; use gpui_native_theme::{ActiveTheme as _, Size, Theme}; const GROUP_WIDTH: f32 = 200.0; const GROUP_HEIGHT: f32 = 300.0; struct Harness { state: Entity, _observation: Subscription, } impl Render for Harness { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div().w(px(GROUP_WIDTH)).h(px(GROUP_HEIGHT)).child( Resizable::new("test-split", self.state.clone()) .debug_selector(|| "test-pane".to_string()) .size_full() .pane(div()) .pane(div()), ) } } fn split_window(sizes: [Pixels; 1]) -> (TestAppContext, Entity, VisualTestContext) { let mut cx = TestAppContext::single(); cx.update(Theme::init); let state = cx.update(ResizableState::new); state.update(&mut cx, |state, cx| state.set_sizes(sizes, cx)); let window_state = state.clone(); let window = cx.add_window(move |_, cx: &mut Context| { let observation = cx.observe(&window_state, |_, _, cx| cx.notify()); Harness { state: window_state, _observation: observation, } }); let window = window.into(); cx.update_window(window, |_, window, cx| { window.draw(cx).clear(cx); }) .unwrap(); let visual = VisualTestContext::from_window(window, &cx); (cx, state, visual) } fn divider_center(visual: &mut VisualTestContext) -> gpui::Point { let bounds = visual .debug_bounds("resize-handle") .expect("divider rendered"); point(bounds.center().x, bounds.center().y) } fn drag_divider(visual: &mut VisualTestContext, delta: gpui::Pixels) { let start = divider_center(visual); visual.simulate_mouse_down(start, MouseButton::Left, Modifiers::default()); visual.simulate_event(MouseMoveEvent { position: point(start.x, start.y + delta), pressed_button: Some(MouseButton::Left), modifiers: Modifiers::default(), }); visual.simulate_mouse_up(start, MouseButton::Left, Modifiers::default()); } #[test] fn a_drag_moves_the_pane_and_its_rendered_height() { let (mut cx, state, mut visual) = split_window([px(100.)]); drag_divider(&mut visual, px(60.)); state.update(&mut cx, |state, _| assert_eq!(state.sizes(), &[px(160.)])); // The pane re-renders at the new size after the state notifies the owner. visual.update(|window, cx| window.draw(cx).clear(cx)); let bounds = visual .debug_bounds("test-pane-0") .expect("first pane rendered"); assert_eq!(bounds.size.height, px(160.)); } #[test] fn a_drag_stops_where_the_remainder_keeps_its_floor() { let (mut cx, state, mut visual) = split_window([px(100.)]); let thickness = cx.update(|cx| cx.theme().sizes.resolve(Size::default()).dense_padding_x); let floor = ResizablePane::DEFAULT_MIN; drag_divider(&mut visual, px(1000.)); state.update(&mut cx, |state, _| { assert_eq!(state.sizes(), &[px(GROUP_HEIGHT) - thickness - floor]); }); } #[test] fn a_double_click_resets_the_pane_to_the_equal_split() { let (mut cx, state, mut visual) = split_window([px(240.)]); let thickness = cx.update(|cx| cx.theme().sizes.resolve(Size::default()).dense_padding_x); let start = divider_center(&mut visual); visual.simulate_event(MouseDownEvent { position: start, modifiers: Modifiers::default(), button: MouseButton::Left, click_count: 2, first_mouse: false, }); state.update(&mut cx, |state, _| { assert_eq!( state.sizes(), &[px((GROUP_HEIGHT - thickness.as_f32()) / 2.)] ); }); } #[test] fn arrow_keys_resize_the_focused_pane() { let (mut cx, state, mut visual) = split_window([px(100.)]); let start = divider_center(&mut visual); // A single click focuses the handle without resetting anything. visual.simulate_mouse_down(start, MouseButton::Left, Modifiers::default()); visual.simulate_mouse_up(start, MouseButton::Left, Modifiers::default()); visual.simulate_keystrokes("down"); visual.simulate_keystrokes("up"); state.update(&mut cx, |state, _| assert_eq!(state.sizes(), &[px(100.)])); } #[test] fn an_unfocused_handle_ignores_arrow_keys() { let (mut cx, state, mut visual) = split_window([px(100.)]); let start = divider_center(&mut visual); let _ = start; visual.simulate_keystrokes("down"); state.update(&mut cx, |state, _| assert_eq!(state.sizes(), &[px(100.)])); } #[test] fn the_panes_and_the_divider_fill_the_group_exactly() { let (_cx, _state, mut visual) = split_window([px(100.)]); visual.update(|window, cx| window.draw(cx).clear(cx)); let first = visual .debug_bounds("test-pane-0") .expect("first pane rendered"); let second = visual .debug_bounds("test-pane-1") .expect("remainder pane rendered"); let divider = visual .debug_bounds("resize-handle") .expect("divider rendered"); assert_eq!( first.size.height + divider.size.height + second.size.height, px(GROUP_HEIGHT) ); assert_eq!( second.size.height, px(GROUP_HEIGHT) - first.size.height - divider.size.height ); }