diff --git a/widgetry/src/lib.rs b/widgetry/src/lib.rs index 8c137f2c9c..e3b2008579 100644 --- a/widgetry/src/lib.rs +++ b/widgetry/src/lib.rs @@ -8,6 +8,7 @@ //! * [`Button`] - clickable buttons with keybindings and tooltips //! * [`Toggle`] - checkboxes, switches, and other toggles //! * [`CompareTimes`] - a scatter plot specialized for comparing times +//! * [`DragDrop`] - a reorderable row of draggable cards //! * [`DrawWithTooltips`] - draw static geometry, with mouse tooltips in certain regions //! * [`Dropdown`] - a button that expands into a menu //! * [`FanChart`] - visualize a range of values over time @@ -54,6 +55,7 @@ pub use crate::widgets::autocomplete::Autocomplete; pub(crate) use crate::widgets::button::Button; pub use crate::widgets::button::{ButtonBuilder, MultiButton}; pub use crate::widgets::compare_times::CompareTimes; +pub use crate::widgets::drag_drop::DragDrop; pub(crate) use crate::widgets::dropdown::Dropdown; pub use crate::widgets::fan_chart::FanChart; pub use crate::widgets::filler::Filler; diff --git a/widgetry/src/widgets/drag_drop.rs b/widgetry/src/widgets/drag_drop.rs new file mode 100644 index 0000000000..98a4093bea --- /dev/null +++ b/widgetry/src/widgets/drag_drop.rs @@ -0,0 +1,119 @@ +use crate::{ + Drawable, EventCtx, GeomBatch, GeomBatchStack, GfxCtx, RewriteColor, ScreenDims, ScreenPt, + ScreenRectangle, Widget, WidgetImpl, WidgetOutput, +}; + +pub struct DragDrop { + members: Vec<(K, GeomBatch, ScreenDims)>, + draw: Drawable, + hovering: Option, + dragging: Option, + + dims: ScreenDims, + top_left: ScreenPt, +} + +impl DragDrop { + pub fn new_widget(ctx: &EventCtx, members: Vec<(K, GeomBatch)>) -> Widget { + let mut dd = DragDrop { + members: members + .into_iter() + .map(|(key, batch)| { + let dims = batch.get_dims(); + (key, batch, dims) + }) + .collect(), + draw: Drawable::empty(ctx), + hovering: None, + dragging: None, + + dims: ScreenDims::square(0.0), + top_left: ScreenPt::new(0.0, 0.0), + }; + dd.recalc_draw(ctx); + Widget::new(Box::new(dd)) + } +} + +impl DragDrop { + fn recalc_draw(&mut self, ctx: &EventCtx) { + let mut stack = GeomBatchStack::horizontal(Vec::new()); + for (idx, (_, batch, _)) in self.members.iter().enumerate() { + let mut batch = batch.clone(); + if let Some(drag_idx) = self.dragging { + // If we're dragging, fade everything out except what we're dragging and where + // we're maybe going to drop + if idx == drag_idx { + // Leave it + } else if self.hovering == Some(idx) { + // Possible drop + batch = batch.color(RewriteColor::ChangeAlpha(0.8)); + } else { + // Fade it out + batch = batch.color(RewriteColor::ChangeAlpha(0.5)); + } + } else if self.hovering == Some(idx) { + // If we're not dragging, show what we're hovering on + batch = batch.color(RewriteColor::ChangeAlpha(0.5)); + } + stack.push(batch); + } + let batch = stack.batch(); + self.dims = batch.get_dims(); + self.draw = batch.upload(ctx); + } + + fn mouseover_card(&self, ctx: &EventCtx) -> Option { + let pt = ctx.canvas.get_cursor_in_screen_space()?; + let mut top_left = self.top_left; + for (idx, (_, _, dims)) in self.members.iter().enumerate() { + if ScreenRectangle::top_left(top_left, *dims).contains(pt) { + return Some(idx); + } + top_left.x += dims.width; + } + None + } +} + +impl WidgetImpl for DragDrop { + fn get_dims(&self) -> ScreenDims { + self.dims + } + + fn set_pos(&mut self, top_left: ScreenPt) { + self.top_left = top_left; + } + + fn event(&mut self, ctx: &mut EventCtx, _: &mut WidgetOutput) { + if let Some(old_idx) = self.dragging { + if ctx.input.left_mouse_button_released() { + self.dragging = None; + if let Some(new_idx) = self.hovering { + if old_idx != new_idx { + // TODO Emit a Changed event, then the caller can go fetch the new ordering + self.members.swap(old_idx, new_idx); + self.recalc_draw(ctx); + } + } + } + } + if ctx.redo_mouseover() { + let old = self.hovering.take(); + self.hovering = self.mouseover_card(ctx); + if old != self.hovering { + self.recalc_draw(ctx); + } + } + if let Some(idx) = self.hovering { + if ctx.input.left_mouse_button_pressed() { + self.dragging = Some(idx); + self.recalc_draw(ctx); + } + } + } + + fn draw(&self, g: &mut GfxCtx) { + g.redraw_at(self.top_left, &self.draw); + } +} diff --git a/widgetry/src/widgets/mod.rs b/widgetry/src/widgets/mod.rs index a9dd48dd08..436b37804e 100644 --- a/widgetry/src/widgets/mod.rs +++ b/widgetry/src/widgets/mod.rs @@ -21,6 +21,7 @@ pub mod autocomplete; pub mod button; pub mod compare_times; pub mod containers; +pub mod drag_drop; pub mod dropdown; pub mod fan_chart; pub mod filler; diff --git a/widgetry_demo/src/lib.rs b/widgetry_demo/src/lib.rs index 2a81c1a5f6..1792477d7c 100644 --- a/widgetry_demo/src/lib.rs +++ b/widgetry_demo/src/lib.rs @@ -5,7 +5,7 @@ use rand_xorshift::XorShiftRng; use geom::{Angle, Duration, Percent, Polygon, Pt2D, Time}; use widgetry::{ - lctrl, Choice, Color, ContentMode, Drawable, EventCtx, Fill, GeomBatch, GfxCtx, + lctrl, Choice, Color, ContentMode, DragDrop, Drawable, EventCtx, Fill, GeomBatch, GfxCtx, HorizontalAlignment, Image, Key, Line, LinePlot, Outcome, Panel, PersistentSplit, PlotOptions, ScreenDims, Series, Settings, SharedAppState, State, TabController, Text, TextExt, Texture, Toggle, Transition, UpdateType, VerticalAlignment, Widget, @@ -315,12 +315,17 @@ fn setup_scrollable_canvas(ctx: &mut EventCtx) -> Drawable { } fn make_tabs(ctx: &mut EventCtx) -> TabController { + let draggable_cards = (0..5) + .map(|i| (i, make_draggable_card(ctx, i))) + .collect::>(); let style = ctx.style(); let mut tabs = TabController::new("demo_tabs"); let gallery_bar_item = style.btn_tab.text("Component Gallery"); let gallery_content = Widget::col(vec![ + "Reorder the cards below".text_widget(ctx), + DragDrop::new_widget(ctx, draggable_cards), Text::from(Line("Text").big_heading_styled().size(18)).into_widget(ctx), Text::from_all(vec![ Line("You can "), @@ -604,6 +609,14 @@ fn make_controls(ctx: &mut EventCtx, tabs: &mut TabController) -> Panel { .build(ctx) } +fn make_draggable_card(ctx: &mut EventCtx, num: usize) -> GeomBatch { + // TODO Kind of hardcoded. At least center the text or draw nice outlines? + let mut batch = GeomBatch::new(); + batch.push(Color::RED, Polygon::rectangle(100.0, 150.0)); + batch.append(Text::from(format!("Card {}", num)).render(ctx)); + batch +} + // Boilerplate for web support #[cfg(target_arch = "wasm32")]