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.
This commit is contained in:
1 parent
8ade21b7ba
commit
040fc94f49
14 files changed
+1413
-16
No files matched your search
@@ -0,0 +1,180 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, AppContext, Bounds, DragMoveEvent, ElementId, EmptyView, EntityId, InteractiveElement,
|
||||
IntoElement, ParentElement, Pixels, Point, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement, Styled, Transformation, Window, div, radians, relative, svg,
|
||||
};
|
||||
|
||||
use crate::{layout::seeded::use_seeded, primitives::FocusRing, theme::ActiveTheme};
|
||||
|
||||
/// What the arrows and Shift snap to, in degrees.
|
||||
const NOTCH: f32 = 15.0;
|
||||
|
||||
struct Turn {
|
||||
owner: EntityId,
|
||||
}
|
||||
|
||||
/// The angle, clockwise from straight up and in 0..360, at which `at` lies from the middle of `bounds`; `snap` rounds it to a notch.
|
||||
fn angle_at(bounds: Bounds<Pixels>, at: Point<Pixels>, snap: bool) -> f32 {
|
||||
let middle = bounds.center();
|
||||
let (dx, dy) = (f32::from(at.x - middle.x), f32::from(at.y - middle.y));
|
||||
let angle = dx.atan2(-dy).to_degrees().rem_euclid(360.0);
|
||||
match snap {
|
||||
true => ((angle / NOTCH).round() * NOTCH).rem_euclid(360.0),
|
||||
false => angle,
|
||||
}
|
||||
}
|
||||
|
||||
type OnTurn = Rc<dyn Fn(f32, &mut Window, &mut App)>;
|
||||
|
||||
/// An svg turned to its angle, in degrees clockwise from straight up, inside a ring with a knob at that angle. A drag on the knob turns it, Shift snapping to 15°; with focus, Left and Right turn it by 15°. gpui 0.2.2 turns svgs alone, so the picture is an svg path.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Rotatable {
|
||||
id: ElementId,
|
||||
path: SharedString,
|
||||
side: Pixels,
|
||||
angle: f32,
|
||||
on_turn: Option<OnTurn>,
|
||||
}
|
||||
|
||||
impl Rotatable {
|
||||
/// `path` names the svg asset; `side` is the ring's width.
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
path: impl Into<SharedString>,
|
||||
side: Pixels,
|
||||
angle: f32,
|
||||
) -> Self {
|
||||
assert!(
|
||||
angle.is_finite(),
|
||||
"rotatable: angle {angle} is not a number"
|
||||
);
|
||||
Self {
|
||||
id: id.into(),
|
||||
path: path.into(),
|
||||
side,
|
||||
angle: angle.rem_euclid(360.0),
|
||||
on_turn: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs with each new angle.
|
||||
pub fn on_turn(mut self, handler: impl Fn(f32, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_turn = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Rotatable {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let id = self.id;
|
||||
let on_turn = self
|
||||
.on_turn
|
||||
.unwrap_or_else(|| panic!("rotatable {id:?} has no on_turn"));
|
||||
let held = use_seeded((id.clone(), "angle"), self.angle, window, cx);
|
||||
let (owner, angle) = (held.entity_id(), held.read(cx).value);
|
||||
let turn: OnTurn = Rc::new(move |next, window, cx| {
|
||||
log::info!("rotatable: {next:.0}°");
|
||||
held.update(cx, |held, cx| {
|
||||
held.value = next;
|
||||
cx.notify();
|
||||
});
|
||||
on_turn(next, window, cx);
|
||||
});
|
||||
let theme = cx.theme();
|
||||
let (side, knob) = (
|
||||
self.side,
|
||||
theme.interaction().grip.to_pixels(window.rem_size()),
|
||||
);
|
||||
let reach = (side - knob) / 2.0;
|
||||
let (sin, cos) = angle.to_radians().sin_cos();
|
||||
let (dragged, keyed) = (turn.clone(), turn);
|
||||
div()
|
||||
.id(id.clone())
|
||||
.relative()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(side)
|
||||
.rounded_full()
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.tab_index(0)
|
||||
.focus_ring(cx)
|
||||
.on_drag_move(move |event: &DragMoveEvent<Turn>, window, cx| {
|
||||
if event.drag(cx).owner != owner {
|
||||
return;
|
||||
}
|
||||
let next = angle_at(
|
||||
event.bounds,
|
||||
event.event.position,
|
||||
event.event.modifiers.shift,
|
||||
);
|
||||
if next != angle {
|
||||
dragged(next, window, cx);
|
||||
}
|
||||
})
|
||||
.on_key_down(move |event, window, cx| {
|
||||
let by = match event.keystroke.key.as_str() {
|
||||
"left" => -NOTCH,
|
||||
"right" => NOTCH,
|
||||
_ => return,
|
||||
};
|
||||
cx.stop_propagation();
|
||||
keyed(
|
||||
((angle / NOTCH).round() * NOTCH + by).rem_euclid(360.0),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.child(
|
||||
svg()
|
||||
.path(self.path)
|
||||
.size(side * 0.5)
|
||||
.text_color(theme.colors.fg)
|
||||
.with_transformation(Transformation::rotate(radians(angle.to_radians()))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.left(relative(0.5))
|
||||
.top(relative(0.5))
|
||||
.ml(reach * sin)
|
||||
.mt(-(reach * cos))
|
||||
.size_0()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.id((id, "knob"))
|
||||
.flex_none()
|
||||
.size(knob)
|
||||
.rounded_full()
|
||||
.bg(theme.colors.accent)
|
||||
.cursor(gpui::CursorStyle::OpenHand)
|
||||
.on_drag(Turn { owner }, |_, _, _, cx| cx.new(|_| EmptyView)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{Bounds, point, px, size};
|
||||
|
||||
use super::angle_at;
|
||||
|
||||
#[test]
|
||||
fn an_angle_runs_clockwise_from_straight_up_and_snaps_to_a_notch() {
|
||||
let ring = Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
|
||||
assert_eq!(angle_at(ring, point(px(50.0), px(0.0)), false), 0.0);
|
||||
assert_eq!(angle_at(ring, point(px(100.0), px(50.0)), false), 90.0);
|
||||
assert_eq!(angle_at(ring, point(px(50.0), px(100.0)), false), 180.0);
|
||||
assert_eq!(angle_at(ring, point(px(0.0), px(50.0)), false), 270.0);
|
||||
let near = angle_at(ring, point(px(60.0), px(0.0)), true);
|
||||
assert_eq!(near, 15.0, "11° snaps to the nearest notch");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user