use ely_gpui_component::{ interaction::{Resizable, Rotatable, SelectionArea}, layout::{Transform, Viewport}, motion::Reorder, primitives::{FocusRing, Pressable}, theme::{ActiveTheme, Elevation, Radius, TextSize}, typography::format::plural, }; 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) -> 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::() .is_some_and(|chip| chip.from != lane) }) .drag_over::(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::(…) · can_drop(…) · on_drop(…)", cx)) } /// The pointer demos' state. struct Shapes { size: gpui::Size, angle: f32, picked: Vec, 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 {}", plural(doubles as u64, "time", "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)), ) }