feat: add resizable component
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
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<ResizableState>,
|
||||
_observation: Subscription,
|
||||
}
|
||||
|
||||
impl Render for Harness {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> 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<ResizableState>, 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<Harness>| {
|
||||
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<gpui::Pixels> {
|
||||
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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user