Interaction · Pointer: resize, turn, band selection, and gpui's own drag
Resizable sizes a box from its right and bottom edges and a grip at its corner, within its least and greatest size; the grip is a Tab stop whose arrows resize a step. Rotatable turns an svg to its angle inside a ring whose knob a drag moves, Shift snapping to 15 degrees, with the arrows turning it a notch. SelectionArea selects items with a band drawn on empty space, a press, Command or Shift to add, and a cursor the arrows walk with Space to add or drop; Escape clears and Command-A takes all. The gallery shows gpui's own drag and drop, drag preview, double press and wheel zoom, with pointers for Sortable, LongPress and PanZoom; pinch stays blocked, gpui 0.2.2 forwards no magnify event. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9bde90a492
commit
f513df6aaa
14 files changed
+1413
-16
No files matched your search
@@ -0,0 +1,238 @@
|
||||
use gpui::{
|
||||
AnyElement, Context, Entity, IntoElement, KeyBinding, Modifiers, MouseButton, ParentElement,
|
||||
Pixels, Point, Render, SharedString, Size, Styled, TestAppContext, VisualTestContext, Window,
|
||||
div, point, px, size,
|
||||
};
|
||||
|
||||
use super::{Resizable, Rotatable, SelectionArea};
|
||||
use crate::{forms, primitives::FocusNext, theme::Theme};
|
||||
|
||||
type Part = fn(&Bench, Entity<Bench>) -> AnyElement;
|
||||
|
||||
/// A view that shows one interaction part and holds what the owner decides: a size, an angle and a selection.
|
||||
struct Bench {
|
||||
part: Part,
|
||||
size: Size<Pixels>,
|
||||
angle: f32,
|
||||
selected: Vec<SharedString>,
|
||||
}
|
||||
|
||||
impl Render for Bench {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().p(px(20.0)).child((self.part)(self, cx.entity()))
|
||||
}
|
||||
}
|
||||
|
||||
fn bench(part: Part, cx: &mut TestAppContext) -> (Entity<Bench>, &mut VisualTestContext) {
|
||||
cx.update(|cx| {
|
||||
Theme::init(cx);
|
||||
Theme::update(cx, |theme| theme.reduced_motion = true);
|
||||
forms::bind_keys(cx);
|
||||
cx.bind_keys([KeyBinding::new("tab", FocusNext, None)]);
|
||||
});
|
||||
let (host, cx) = cx.add_window_view(|_, _| Bench {
|
||||
part,
|
||||
size: size(px(200.0), px(120.0)),
|
||||
angle: 0.0,
|
||||
selected: Vec::new(),
|
||||
});
|
||||
settle(cx);
|
||||
(host, cx)
|
||||
}
|
||||
|
||||
fn settle(cx: &mut VisualTestContext) {
|
||||
for _ in 0..3 {
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, _| window.refresh());
|
||||
}
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
fn tab(stops: usize, cx: &mut VisualTestContext) {
|
||||
cx.update(|window, _| {
|
||||
window.blur();
|
||||
(0..stops).for_each(|_| window.focus_next());
|
||||
});
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
fn tap(key: &str, cx: &mut VisualTestContext) {
|
||||
cx.simulate_keystrokes(key);
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
fn at(x: f32, y: f32) -> Point<Pixels> {
|
||||
point(px(x), px(y))
|
||||
}
|
||||
|
||||
/// Presses at `from`, moves through `path` with the button held, and lets go at its end.
|
||||
fn drag(
|
||||
from: Point<Pixels>,
|
||||
path: &[Point<Pixels>],
|
||||
modifiers: Modifiers,
|
||||
cx: &mut VisualTestContext,
|
||||
) {
|
||||
cx.simulate_mouse_down(from, MouseButton::Left, modifiers);
|
||||
for step in path {
|
||||
cx.simulate_mouse_move(*step, Some(MouseButton::Left), modifiers);
|
||||
settle(cx);
|
||||
}
|
||||
let end = *path.last().expect("a path");
|
||||
cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
fn resizable(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||
Resizable::new(
|
||||
"box",
|
||||
bench.size,
|
||||
size(px(100.0), px(80.0)),
|
||||
size(px(300.0), px(200.0)),
|
||||
)
|
||||
.on_resize(move |next, _, cx| {
|
||||
owner.update(cx, |bench, cx| {
|
||||
bench.size = next;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.child(div().size_full())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// The grip is the one stop; the arrows resize a step, and a drag on the right edge only widens.
|
||||
#[gpui::test]
|
||||
fn a_box_resizes_from_its_grip_and_its_edge_within_its_limits(cx: &mut TestAppContext) {
|
||||
let (host, cx) = bench(resizable, cx);
|
||||
tab(1, cx);
|
||||
tap("right", cx);
|
||||
tap("down", cx);
|
||||
let stepped = host.read_with(cx, |bench, _| bench.size);
|
||||
assert_eq!(stepped, size(px(208.0), px(128.0)));
|
||||
drag(
|
||||
at(228.0, 60.0),
|
||||
&[at(240.0, 60.0), at(260.0, 60.0), at(400.0, 60.0)],
|
||||
Modifiers::none(),
|
||||
cx,
|
||||
);
|
||||
let dragged = host.read_with(cx, |bench, _| bench.size);
|
||||
assert_eq!(
|
||||
dragged,
|
||||
size(px(300.0), px(128.0)),
|
||||
"the edge stops at the widest"
|
||||
);
|
||||
}
|
||||
|
||||
fn rotatable(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||
Rotatable::new("dial", "icons/arrow-up.svg", px(100.0), bench.angle)
|
||||
.on_turn(move |next, _, cx| {
|
||||
owner.update(cx, |bench, cx| {
|
||||
bench.angle = next;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// The ring is the one stop; Left and Right turn it a notch, round the circle.
|
||||
#[gpui::test]
|
||||
fn a_dial_turns_by_notches_from_the_keyboard(cx: &mut TestAppContext) {
|
||||
let (host, cx) = bench(rotatable, cx);
|
||||
tab(1, cx);
|
||||
tap("left", cx);
|
||||
assert_eq!(host.read_with(cx, |bench, _| bench.angle), 345.0);
|
||||
tap("right", cx);
|
||||
tap("right", cx);
|
||||
assert_eq!(host.read_with(cx, |bench, _| bench.angle), 15.0);
|
||||
}
|
||||
|
||||
/// The knob sits on top of a ring centered at 70, 70; a drag to 86 degrees with Shift snaps to 90.
|
||||
#[gpui::test]
|
||||
fn a_dial_turns_toward_its_dragged_knob(cx: &mut TestAppContext) {
|
||||
let (host, cx) = bench(rotatable, cx);
|
||||
drag(
|
||||
at(70.0, 26.0),
|
||||
&[at(90.0, 40.0), at(110.0, 55.0), at(130.0, 66.0)],
|
||||
Modifiers::shift(),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(host.read_with(cx, |bench, _| bench.angle), 90.0);
|
||||
}
|
||||
|
||||
fn area(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||
["a", "b", "c"]
|
||||
.into_iter()
|
||||
.fold(
|
||||
SelectionArea::new("tiles")
|
||||
.flex()
|
||||
.gap(px(10.0))
|
||||
.p(px(20.0))
|
||||
.w(px(300.0)),
|
||||
|area, key| area.item(key, div().size(px(40.0))),
|
||||
)
|
||||
.selected(bench.selected.clone())
|
||||
.on_change(move |keys, _, cx| {
|
||||
let keys = keys.to_vec();
|
||||
owner.update(cx, |bench, cx| {
|
||||
bench.selected = keys;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn picked(host: &Entity<Bench>, cx: &mut VisualTestContext) -> Vec<String> {
|
||||
host.read_with(cx, |bench, _| {
|
||||
bench.selected.iter().map(ToString::to_string).collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Tiles sit at x 41, 93 and 145 inside the box, which starts at 20; a band from empty space below takes the two it crosses.
|
||||
#[gpui::test]
|
||||
fn a_band_selects_what_it_crosses(cx: &mut TestAppContext) {
|
||||
let (host, cx) = bench(area, cx);
|
||||
drag(
|
||||
at(100.0, 100.0),
|
||||
&[at(110.0, 95.0), at(150.0, 70.0), at(160.0, 60.0)],
|
||||
Modifiers::none(),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(picked(&host, cx), ["b", "c"]);
|
||||
assert!(cx.debug_bounds("selection-band").is_some());
|
||||
}
|
||||
|
||||
/// A press selects a tile alone, Command adds another, a press on empty space clears, and with focus Command-A takes all and Escape clears.
|
||||
#[gpui::test]
|
||||
fn presses_and_keys_pick_tiles(cx: &mut TestAppContext) {
|
||||
let (host, cx) = bench(area, cx);
|
||||
cx.simulate_click(at(60.0, 60.0), Modifiers::none());
|
||||
settle(cx);
|
||||
assert_eq!(picked(&host, cx), ["a"]);
|
||||
cx.simulate_click(at(112.0, 60.0), Modifiers::command());
|
||||
settle(cx);
|
||||
assert_eq!(picked(&host, cx), ["a", "b"]);
|
||||
cx.simulate_click(at(250.0, 100.0), Modifiers::none());
|
||||
settle(cx);
|
||||
assert!(
|
||||
picked(&host, cx).is_empty(),
|
||||
"a press on empty space clears"
|
||||
);
|
||||
tap("cmd-a", cx);
|
||||
assert_eq!(picked(&host, cx), ["a", "b", "c"]);
|
||||
tap("escape", cx);
|
||||
assert!(picked(&host, cx).is_empty());
|
||||
}
|
||||
|
||||
/// The box is one stop: the arrows walk the cursor and Space adds or drops the tile it is on.
|
||||
#[gpui::test]
|
||||
fn the_keyboard_walks_and_picks_tiles(cx: &mut TestAppContext) {
|
||||
let (host, cx) = bench(area, cx);
|
||||
tab(1, cx);
|
||||
tap("right", cx);
|
||||
tap("space", cx);
|
||||
tap("right", cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(picked(&host, cx), ["b", "c"]);
|
||||
tap("left", cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(picked(&host, cx), ["c"]);
|
||||
}
|
||||
Reference in new issue
Block a user