use std::{cell::RefCell, rc::Rc}; use gpui::{ AnyWindowHandle, AppContext as _, Context, InputEvent as _, IntoElement, KeyDownEvent, Keystroke, Modifiers, MouseButton, ParentElement as _, Render, Styled as _, TestAppContext, VisualTestContext, Window, div, point, px, }; use gpui_native_core::slider::Slider; use gpui_native_theme::Theme; struct Harness { changes: Rc>>, value: f32, disabled: bool, } impl Render for Harness { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let changes = self.changes.clone(); let view = cx.entity(); div().size_full().flex().flex_col().child( div().w(px(200.)).child( Slider::new("test-slider", self.value) .min(0.0) .max(100.0) .step(10.0) .disabled(self.disabled) .on_change(move |next, _, cx| { changes.borrow_mut().push(next); view.update(cx, |harness, cx| { harness.value = next; cx.notify(); }); }), ), ) } } /// A maximized window holding a 200px slider over 0..=100 stepped by 10 in /// the top-left corner: `x` maps to `round(x / 200 * 10) * 10`. fn slider_window(disabled: bool) -> (VisualTestContext, AnyWindowHandle, Rc>>) { let mut cx = TestAppContext::single(); cx.update(Theme::init); let changes = Rc::new(RefCell::new(Vec::new())); let harness_changes = changes.clone(); let window = cx.add_window(move |_, _| Harness { changes: harness_changes, value: 0.0, disabled, }); let window = window.into(); cx.update_window(window, |_, window, cx| { window.draw(cx).clear(cx); }) .unwrap(); (VisualTestContext::from_window(window, &cx), window, changes) } fn draw(cx: &mut VisualTestContext, window: AnyWindowHandle) { cx.update_window(window, |_, window, cx| { window.draw(cx).clear(cx); }) .unwrap(); } #[test] fn clicking_the_track_sets_the_snapped_value() { let (mut cx, _window, changes) = slider_window(false); cx.simulate_click(point(px(100.), px(10.)), Modifiers::default()); assert_eq!(*changes.borrow(), vec![50.0]); cx.simulate_click(point(px(40.), px(10.)), Modifiers::default()); assert_eq!(*changes.borrow(), vec![50.0, 20.0]); } #[test] fn dragging_reports_values_until_release() { let (mut cx, _window, changes) = slider_window(false); cx.simulate_mouse_down( point(px(40.), px(10.)), MouseButton::Left, Modifiers::default(), ); cx.simulate_mouse_move( point(px(120.), px(10.)), MouseButton::Left, Modifiers::default(), ); cx.simulate_mouse_up( point(px(120.), px(10.)), MouseButton::Left, Modifiers::default(), ); assert_eq!(*changes.borrow(), vec![20.0, 60.0]); // Moving after release must not change the value. cx.simulate_mouse_move( point(px(180.), px(10.)), MouseButton::Left, Modifiers::default(), ); assert_eq!(*changes.borrow(), vec![20.0, 60.0]); } #[test] fn drag_clamps_to_the_range_ends() { let (mut cx, _window, changes) = slider_window(false); cx.simulate_mouse_down( point(px(100.), px(10.)), MouseButton::Left, Modifiers::default(), ); // The drag captures the pointer: it keeps reporting past the track's // right edge, clamped to the top of the range. cx.simulate_mouse_move( point(px(400.), px(10.)), MouseButton::Left, Modifiers::default(), ); assert_eq!(*changes.borrow(), vec![50.0, 100.0]); } #[test] fn disabled_slider_does_not_report_changes() { let (mut cx, _window, changes) = slider_window(true); cx.simulate_click(point(px(100.), px(10.)), Modifiers::default()); assert!(changes.borrow().is_empty()); } #[test] fn arrow_keys_step_the_value_while_focused() { let (mut cx, window, changes) = slider_window(false); // The click focuses the slider and sets the snapped value at that point. cx.simulate_click(point(px(2.), px(10.)), Modifiers::default()); assert_eq!(*changes.borrow(), vec![0.0]); draw(&mut cx, window); for key in ["right", "right", "left"] { cx.update_window(window, |_, window, cx| { window.dispatch_event( KeyDownEvent { keystroke: Keystroke::parse(key).unwrap(), is_held: false, prefer_character_input: false, } .to_platform_input(), cx, ); }) .unwrap(); } assert_eq!(*changes.borrow(), vec![0.0, 10.0, 20.0, 10.0]); } struct TwoSliders { changes_a: Rc>>, changes_b: Rc>>, value_a: f32, value_b: f32, } impl Render for TwoSliders { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let (changes_a, changes_b) = (self.changes_a.clone(), self.changes_b.clone()); div() .size_full() .child( div().absolute().top_0().left_0().w(px(200.)).child( Slider::new("slider-a", self.value_a) .min(0.0) .max(100.0) .step(10.0) .on_change({ let view = cx.entity(); let changes_a = changes_a.clone(); move |next, _, cx| { changes_a.borrow_mut().push(next); view.update(cx, |harness, cx| { harness.value_a = next; cx.notify(); }); } }), ), ) .child( div().absolute().top(px(40.)).left_0().w(px(200.)).child( Slider::new("slider-b", self.value_b) .min(0.0) .max(100.0) .step(10.0) .on_change({ let view = cx.entity(); move |next, _, cx| { changes_b.borrow_mut().push(next); view.update(cx, |harness, cx| { harness.value_b = next; cx.notify(); }); } }), ), ) } } #[allow(clippy::type_complexity)] fn two_slider_window() -> ( VisualTestContext, Rc>>, Rc>>, ) { let mut cx = TestAppContext::single(); cx.update(Theme::init); let changes_a = Rc::new(RefCell::new(Vec::new())); let changes_b = Rc::new(RefCell::new(Vec::new())); let (changes_a_for_window, changes_b_for_window) = (changes_a.clone(), changes_b.clone()); let window = cx.add_window(move |_, _| TwoSliders { changes_a: changes_a_for_window, changes_b: changes_b_for_window, value_a: 0.0, value_b: 0.0, }); let window = window.into(); cx.update_window(window, |_, window, cx| { window.draw(cx).clear(cx); }) .unwrap(); ( VisualTestContext::from_window(window, &cx), changes_a, changes_b, ) } /// Dragging one slider must never drive a sibling slider: the drag flag is /// per instance, not shared keyed state. #[test] fn two_sliders_drag_independently() { let (mut cx, changes_a, changes_b) = two_slider_window(); // Slider A occupies y 0..16, slider B starts at y 40. cx.simulate_mouse_down( point(px(100.), px(10.)), MouseButton::Left, Modifiers::default(), ); cx.simulate_mouse_move( point(px(120.), px(10.)), MouseButton::Left, Modifiers::default(), ); cx.simulate_mouse_up( point(px(120.), px(10.)), MouseButton::Left, Modifiers::default(), ); assert_eq!(*changes_a.borrow(), vec![50.0, 60.0]); assert!(changes_b.borrow().is_empty(), "A's drag leaked into B"); cx.simulate_mouse_down( point(px(100.), px(50.)), MouseButton::Left, Modifiers::default(), ); cx.simulate_mouse_move( point(px(40.), px(50.)), MouseButton::Left, Modifiers::default(), ); cx.simulate_mouse_up( point(px(40.), px(50.)), MouseButton::Left, Modifiers::default(), ); assert_eq!(*changes_b.borrow(), vec![50.0, 20.0]); assert_eq!( *changes_a.borrow(), vec![50.0, 60.0], "B's drag leaked into A" ); }