Files
Ely-GPUI-Components/examples/gallery/pages/interaction/pointer.rs
T
ZacharyZhang-NYandClaude Opus 5.5 f513df6aaa 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>
2026-09-27 15:42:03 -04:00

326 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use ely_gpui_component::{
interaction::{Resizable, Rotatable, SelectionArea},
layout::{Transform, Viewport},
motion::Reorder,
primitives::{FocusRing, Pressable},
theme::{ActiveTheme, Elevation, Radius, TextSize},
};
use gpui::{
App, Context, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, Window, div, point, prelude::*, px, size,
};
use crate::{
probe::probe,
ui::{blocked, change, code, keep, section, specimen, specimens},
};
/// A chip on its way between lanes: its name and the lane it left.
#[derive(Clone)]
struct Chip {
name: SharedString,
from: usize,
}
/// What follows the pointer while a chip is dragged.
struct ChipPreview(SharedString);
impl Render for ChipPreview {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme();
div()
.px_3()
.py_1p5()
.rounded(theme.radius(Radius::Md))
.border_1()
.border_color(theme.colors.border_strong)
.bg(theme.colors.surface)
.shadow(theme.elevation(Elevation::Floating))
.text_size(theme.text_size(TextSize::Sm))
.child(self.0.clone())
}
}
const LANES: [&str; 2] = ["Today", "Later"];
/// Which lane each chip is in.
struct Board(Vec<(SharedString, usize)>);
pub fn drag_and_drop(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let state = keep(
"interaction-board",
|| {
Board(
["Design", "Build", "Ship"]
.map(|name| (name.into(), 0))
.to_vec(),
)
},
window,
cx,
);
let chips = state.read(cx).0.clone();
let theme = cx.theme();
let lanes = LANES.iter().enumerate().map(|(lane, title)| {
let dropped = state.clone();
let held = chips.iter().filter(|(_, at)| *at == lane).map(|(name, _)| {
let chip = Chip {
name: name.clone(),
from: lane,
};
div()
.id(SharedString::from(format!("interaction-chip-{name}")))
.px_3()
.py_1p5()
.rounded(theme.radius(Radius::Md))
.border_1()
.border_color(theme.colors.border)
.bg(theme.colors.surface)
.cursor(gpui::CursorStyle::OpenHand)
.text_size(theme.text_size(TextSize::Sm))
.on_drag(chip, |chip, _, _, cx| {
cx.new(|_| ChipPreview(chip.name.clone()))
})
.child(name.clone())
});
let lit = theme.colors.hover;
div()
.id(SharedString::from(format!("interaction-lane-{lane}")))
.w(px(180.))
.min_h(px(140.))
.flex()
.flex_col()
.gap_2()
.p_3()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(theme.colors.border)
.can_drop(move |value, _, _| {
value
.downcast_ref::<Chip>()
.is_some_and(|chip| chip.from != lane)
})
.drag_over::<Chip>(move |style, _, _, _| style.bg(lit))
.on_drop(move |chip: &Chip, _, cx| {
let name = chip.name.clone();
change(&dropped, cx, |board| {
for (each, at) in board.0.iter_mut() {
if *each == name {
*at = lane;
}
}
})
})
.child(
div()
.text_size(theme.text_size(TextSize::Xs))
.text_color(theme.colors.fg_subtle)
.child(*title),
)
.children(held)
});
section(
"Draggable · Droppable / DropTarget · DragOverlay / DragPreview · DragGhost",
"gpui's own drag and drop: on_drag carries a value and builds the view that follows the pointer, drag_over lights a lane, can_drop refuses the lane a chip left, and on_drop takes it. Ely adds nothing.",
cx,
)
.child(div().flex().flex_wrap().gap_4().children(lanes))
.child(code("on_drag(value, |value, offset, window, cx| cx.new(…)) · drag_over::<T>(…) · can_drop(…) · on_drop(…)", cx))
}
/// The pointer demos' state.
struct Shapes {
size: gpui::Size<gpui::Pixels>,
angle: f32,
picked: Vec<SharedString>,
order: Vec<&'static str>,
presses: usize,
doubles: usize,
held: usize,
}
pub fn shapes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let state = keep(
"interaction-shapes",
|| Shapes {
size: size(px(260.), px(140.)),
angle: 45.0,
picked: vec!["tile-2".into()],
order: vec!["Inbox", "Today", "Upcoming", "Someday"],
presses: 0,
doubles: 0,
held: 0,
},
window,
cx,
);
let now = state.read(cx);
let (size_now, angle, picked, order) =
(now.size, now.angle, now.picked.clone(), now.order.clone());
let (presses, doubles, held) = (now.presses, now.doubles, now.held);
let theme = cx.theme();
let [resize, turn, pick, sort, double, press, long] = [(); 7].map(|_| state.clone());
let tiles = (0..12).fold(
SelectionArea::new("interaction-tiles")
.w(px(300.))
.flex()
.flex_wrap()
.gap(px(8.))
.p_3()
.rounded_lg()
.border_1()
.border_color(theme.colors.border),
|area, ix| {
area.item(
SharedString::from(format!("tile-{ix}")),
div().size(px(56.)).rounded_md().bg(theme.colors.sunken),
)
},
);
let list = order
.iter()
.fold(Reorder::new("interaction-sortable"), |list, name| {
list.row(
*name,
div()
.px_3()
.py_2()
.rounded_md()
.border_1()
.border_color(theme.colors.border)
.bg(theme.colors.surface)
.child(*name),
)
});
section(
"Resizable · Rotatable · Selectable / SelectionArea · RubberBandSelection · Sortable",
"A box resized from its edges or its grip; a picture turned by its knob, as gpui turns svgs alone; tiles taken by a band drawn on empty space; rows sorted by drag through motion::Reorder.",
cx,
)
.child(
specimens()
.child(specimen(
"Resizable",
probe(
"interaction-resizable",
Resizable::new("interaction-resizable", size_now, size(px(160.), px(96.)), size(px(360.), px(220.)))
.on_resize(move |next, _, cx| change(&resize, cx, |shapes| shapes.size = next))
.child(
div()
.size_full()
.p_4()
.rounded_lg()
.border_1()
.border_color(theme.colors.border)
.bg(theme.colors.surface)
.text_size(theme.text_size(TextSize::Sm))
.text_color(theme.colors.fg_muted)
.child(format!("{:.0} × {:.0}", f32::from(size_now.width), f32::from(size_now.height))),
),
),
cx,
))
.child(specimen(
"Rotatable",
probe(
"interaction-rotatable",
Rotatable::new("interaction-rotatable", "icons/arrow-up.svg", px(120.), angle)
.on_turn(move |next, _, cx| change(&turn, cx, |shapes| shapes.angle = next)),
),
cx,
)),
)
.child(
specimens()
.child(specimen(
"SelectionArea",
probe(
"interaction-tiles",
tiles.selected(picked).on_change(move |keys, _, cx| {
let keys = keys.to_vec();
change(&pick, cx, |shapes| shapes.picked = keys)
}),
),
cx,
))
.child(specimen(
"Sortable → motion::Reorder",
div().w(px(220.)).child(list.on_reorder(move |from, to, _, cx| {
change(&sort, cx, |shapes| {
let row = shapes.order.remove(from);
shapes.order.insert(to, row);
})
})),
cx,
)),
)
.child(
section(
"LongPress · DoubleClick · Gesture · PanZoom",
"A long press is primitives::Pressable; a double press is gpui's click_count. The wheel pans and, with Command, zooms layout::Viewport; two fingers swipe lists::SwipeableListItem.",
cx,
)
.child(
specimens()
.child(specimen(
"LongPress → primitives::Pressable",
Pressable::new("interaction-press", move |down, _, cx| {
let theme = cx.theme();
div()
.px_4()
.py_3()
.rounded_lg()
.border_1()
.border_color(theme.colors.border)
.bg(if down { theme.colors.active } else { theme.colors.surface })
.text_size(theme.text_size(TextSize::Sm))
.child(format!("Pressed {presses} · held {held}"))
.into_any_element()
})
.on_press(move |_, cx| change(&press, cx, |shapes| shapes.presses += 1))
.on_long_press(move |_, cx| change(&long, cx, |shapes| shapes.held += 1)),
cx,
))
.child(specimen(
"DoubleClick",
div()
.id("interaction-double")
.px_4()
.py_3()
.rounded_lg()
.border_1()
.border_color(theme.colors.border)
.tab_index(0)
.focus_ring(cx)
.text_size(theme.text_size(TextSize::Sm))
.on_click(move |event, _, cx| {
if event.click_count() == 2 {
change(&double, cx, |shapes| shapes.doubles += 1)
}
})
.child(format!("Opened {doubles} times")),
cx,
))
.child(specimen(
"PanZoom → layout::Viewport",
div().w(px(240.)).h(px(140.)).rounded_lg().border_1().border_color(theme.colors.border).overflow_hidden().child(
Viewport::new("interaction-viewport", |view: Transform, _, cx| {
let theme = cx.theme();
let corner = view.apply(point(px(80.), px(30.)));
div()
.absolute()
.left(corner.x)
.top(corner.y)
.size(px(80.) * view.scale)
.rounded_lg()
.bg(theme.colors.sunken)
.into_any_element()
}),
),
cx,
)),
)
.child(blocked("Pinch: gpui 0.2.2 forwards no magnify event, so a trackpad pinch never reaches a view.", cx)),
)
}