Skip to content

Commit

Permalink
Drag-n-drop for waypoints! #743
Browse files Browse the repository at this point in the history
Also some drag-n-drop API tweaks:
- Don't require the caller to set `named()`
- Vertical card layout!

Some UX issues:
- deletion button alignment
- horizontal card alignment off
- sync up highlighting between map and cards
- selection state for a waypoint is meaningless
  • Loading branch information
dabreegster committed Sep 3, 2021
1 parent 2234c52 commit b231ba6
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 60 deletions.
100 changes: 70 additions & 30 deletions game/src/common/waypoints.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use geom::{Circle, Distance, FindClosest, Polygon};
use sim::TripEndpoint;
use widgetry::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, Image, Line, Outcome, Text, TextExt, Widget,
Color, DragDrop, Drawable, EventCtx, GeomBatch, GfxCtx, Image, Line, Outcome, RewriteColor,
StackAxis, Text, Widget,
};

use crate::app::App;
Expand Down Expand Up @@ -64,32 +65,61 @@ impl InputWaypoints {
}

pub fn get_panel_widget(&self, ctx: &mut EventCtx) -> Widget {
let mut col = Vec::new();
let mut drag_drop = DragDrop::new(ctx, "waypoint cards", StackAxis::Vertical);
let mut delete_buttons = Vec::new();

for (idx, waypt) in self.waypoints.iter().enumerate() {
col.push(Widget::row(vec![
format!("{}) {}", waypt.order, waypt.label)
.text_widget(ctx)
.centered_vert(),
let batch = Text::from(Line(format!("{}) {}", waypt.order, waypt.label))).render(ctx);
let bounds = batch.get_bounds();
let image = Image::from_batch(batch, bounds)
.color(RewriteColor::NoOp)
.padding(16);

let (default_batch, bounds) = image.clone().build_batch(ctx).unwrap();
let (hovering_batch, _) = image
.clone()
.bg_color(ctx.style().btn_tab.bg_disabled.dull(0.3))
.build_batch(ctx)
.unwrap();
let (selected_batch, _) = image
.bg_color(ctx.style().btn_solid_primary.bg)
.build_batch(ctx)
.unwrap();

drag_drop.push_card(
idx,
bounds.into(),
default_batch,
hovering_batch,
selected_batch,
);

delete_buttons.push(
ctx.style()
.btn_plain_destructive
.text("X")
.build_widget(ctx, &format!("delete waypoint {}", idx)),
]));
);
}
drag_drop.set_initial_state(None, None);

col.push(Widget::row(vec![
Image::from_path("system/assets/tools/mouse.svg").into_widget(ctx),
Text::from_all(vec![
Line("Click").fg(ctx.style().text_hotkey_color),
Line(" to add a waypoint, "),
Line("drag").fg(ctx.style().text_hotkey_color),
Line(" a waypoint to move it"),
])
.into_widget(ctx),
]));

Widget::col(col)
Widget::col(vec![
Widget::row(vec![
drag_drop.into_widget(ctx),
// TODO The alignment doesn't match the cards, but it's... usable
Widget::col(delete_buttons).evenly_spaced(),
]),
Widget::row(vec![
Image::from_path("system/assets/tools/mouse.svg").into_widget(ctx),
Text::from_all(vec![
Line("Click").fg(ctx.style().text_hotkey_color),
Line(" to add a waypoint, "),
Line("drag").fg(ctx.style().text_hotkey_color),
Line(" a waypoint to move it"),
])
.into_widget(ctx),
]),
])
}

pub fn get_waypoints(&self) -> Vec<TripEndpoint> {
Expand Down Expand Up @@ -142,20 +172,30 @@ impl InputWaypoints {
}
}

if let Outcome::Clicked(x) = outcome {
if let Some(x) = x.strip_prefix("delete waypoint ") {
let idx = x.parse::<usize>().unwrap();
self.waypoints.remove(idx);
// Recalculate labels, in case we deleted in the middle
for (idx, waypt) in self.waypoints.iter_mut().enumerate() {
*waypt = Waypoint::new(ctx, app, waypt.at, idx);
}
match outcome {
Outcome::Clicked(x) => {
if let Some(x) = x.strip_prefix("delete waypoint ") {
let idx = x.parse::<usize>().unwrap();
self.waypoints.remove(idx);
// Recalculate labels, in case we deleted in the middle
for (idx, waypt) in self.waypoints.iter_mut().enumerate() {
*waypt = Waypoint::new(ctx, app, waypt.at, idx);
}

self.update_waypoints_drawable(ctx);
self.update_waypoints_drawable(ctx);
return true;
} else {
panic!("Unknown InputWaypoints click {}", x);
}
}
Outcome::DragDropReleased(_, old_idx, new_idx) => {
self.waypoints.swap(old_idx, new_idx);
// The order field is baked in, so calculate everything again from scratch
let waypoints = self.get_waypoints();
self.overwrite(ctx, app, waypoints);
return true;
} else {
panic!("Unknown InputWaypoints click {}", x);
}
_ => {}
}

false
Expand Down
5 changes: 2 additions & 3 deletions game/src/edit/roads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use map_model::{
use widgetry::{
lctrl, Choice, Color, ControlState, DragDrop, Drawable, EdgeInsets, EventCtx, GeomBatch,
GeomBatchStack, GfxCtx, HorizontalAlignment, Image, Key, Line, Outcome, Panel, PersistentSplit,
Spinner, State, Text, TextExt, VerticalAlignment, Widget, DEFAULT_CORNER_RADIUS,
Spinner, StackAxis, State, Text, TextExt, VerticalAlignment, Widget, DEFAULT_CORNER_RADIUS,
};

use crate::app::{App, Transition};
Expand Down Expand Up @@ -625,7 +625,7 @@ fn make_main_panel(
row
}),
]);
let mut drag_drop = DragDrop::new(ctx, "lane cards");
let mut drag_drop = DragDrop::new(ctx, "lane cards", StackAxis::Horizontal);

let road_width = road.get_width(map);
let lanes_ltr = road.lanes_ltr();
Expand Down Expand Up @@ -892,7 +892,6 @@ fn make_main_panel(
.margin_below(16),
drag_drop
.into_widget(ctx)
.named("lane cards")
.bg(ctx.style().text_primary_color.tint(0.3))
.margin_left(16),
// We use a sort of "tab" metaphor for the selected lane above and this "edit" section
Expand Down
6 changes: 3 additions & 3 deletions game/src/edit/traffic_signals/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use map_model::{
use widgetry::{
include_labeled_bytes, lctrl, Color, ControlState, DragDrop, DrawBaselayer, Drawable, EventCtx,
GeomBatch, GeomBatchStack, GfxCtx, HorizontalAlignment, Image, Key, Line, Outcome, Panel,
RewriteColor, State, Text, TextExt, VerticalAlignment, Widget,
RewriteColor, StackAxis, State, Text, TextExt, VerticalAlignment, Widget,
};

use crate::app::{App, ShowEverything, Transition};
Expand Down Expand Up @@ -694,7 +694,7 @@ fn make_side_panel(
.collect(),
);

let mut drag_drop = DragDrop::new(ctx, "stage cards");
let mut drag_drop = DragDrop::new(ctx, "stage cards", StackAxis::Horizontal);
for idx in 0..canonical_signal.stages.len() {
let mut stack = GeomBatchStack::vertical(vec![
Text::from(Line(format!(
Expand Down Expand Up @@ -736,7 +736,7 @@ fn make_side_panel(
}
drag_drop.set_initial_state(Some(selected), None);

col.push(drag_drop.into_widget(ctx).named("stage cards"));
col.push(drag_drop.into_widget(ctx));

col.push(Widget::row(vec![
// TODO Say "normally" to account for variable stages?
Expand Down
2 changes: 2 additions & 0 deletions game/src/ungap/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ impl RoutePlanner {
self.waypoints.get_panel_widget(ctx),
]))
.aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
// Hovering on a card
.ignore_initial_events()
.build(ctx);
}

Expand Down
12 changes: 6 additions & 6 deletions widgetry/src/geom/geom_batch_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,17 @@ impl Default for GeomBatchStack {

impl GeomBatchStack {
pub fn horizontal(batches: Vec<GeomBatch>) -> Self {
GeomBatchStack {
batches,
axis: Axis::Horizontal,
..Default::default()
}
Self::from_axis(batches, Axis::Horizontal)
}

pub fn vertical(batches: Vec<GeomBatch>) -> Self {
Self::from_axis(batches, Axis::Vertical)
}

pub fn from_axis(batches: Vec<GeomBatch>, axis: Axis) -> Self {
GeomBatchStack {
batches,
axis: Axis::Vertical,
axis,
..Default::default()
}
}
Expand Down
56 changes: 42 additions & 14 deletions widgetry/src/widgets/drag_drop.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
Drawable, EventCtx, GeomBatch, GeomBatchStack, GfxCtx, Outcome, ScreenDims, ScreenPt,
ScreenRectangle, Widget, WidgetImpl, WidgetOutput,
ScreenRectangle, StackAxis, Widget, WidgetImpl, WidgetOutput,
};

const SPACE_BETWEEN_CARDS: f64 = 2.0;
Expand All @@ -10,6 +10,7 @@ pub struct DragDrop<T: Copy + PartialEq> {
cards: Vec<Card<T>>,
draw: Drawable,
state: State,
axis: StackAxis,
dims: ScreenDims,
top_left: ScreenPt,
}
Expand Down Expand Up @@ -47,7 +48,11 @@ impl<T: 'static + Copy + PartialEq> DragDrop<T> {
/// - `Outcome::Changed("dragging " + label)` while dragging, when the drop position of the
/// card changes. Call `get_dragging_state` to learn the indices.
/// - `Outcome::DragDropReleased` when a card is dropped
pub fn new(ctx: &EventCtx, label: &str) -> Self {
///
/// When you build a `Panel` containing one of these, you may need to call
/// `ignore_initial_events()`. If the cursor is hovering over a card when the panel is first
/// created, `Outcome::Changed` is immediately fired from this widget.
pub fn new(ctx: &EventCtx, label: &str, axis: StackAxis) -> Self {
DragDrop {
label: label.to_string(),
cards: vec![],
Expand All @@ -56,14 +61,16 @@ impl<T: 'static + Copy + PartialEq> DragDrop<T> {
hovering: None,
selected: None,
},
axis,
dims: ScreenDims::zero(),
top_left: ScreenPt::zero(),
}
}

pub fn into_widget(mut self, ctx: &EventCtx) -> Widget {
self.recalc_draw(ctx);
Widget::new(Box::new(self))
let label = self.label.clone();
Widget::new(Box::new(self)).named(label)
}

pub fn selected_value(&self) -> Option<T> {
Expand Down Expand Up @@ -129,7 +136,7 @@ impl<T: 'static + Copy + PartialEq> DragDrop<T> {

impl<T: 'static + Copy + PartialEq> DragDrop<T> {
fn recalc_draw(&mut self, ctx: &EventCtx) {
let mut stack = GeomBatchStack::horizontal(Vec::new());
let mut stack = GeomBatchStack::from_axis(Vec::new(), self.axis);
stack.set_spacing(SPACE_BETWEEN_CARDS);

let (dims, batch) = match self.state {
Expand All @@ -152,22 +159,36 @@ impl<T: 'static + Copy + PartialEq> DragDrop<T> {
cursor_at,
new_idx,
} => {
let width = self.cards[orig_idx].dims.width;
let orig_dims = self.cards[orig_idx].dims;

for (idx, card) in self.cards.iter().enumerate() {
// the target we're dragging
let batch = if idx == orig_idx {
card.selected_batch.clone()
} else if idx <= new_idx && idx > orig_idx {
// move batch to the left if target is newly greater than us
card.default_batch
.clone()
.translate(-(width + SPACE_BETWEEN_CARDS), 0.0)
// move batch to the left or top if target is newly greater than us
match self.axis {
StackAxis::Horizontal => card
.default_batch
.clone()
.translate(-(orig_dims.width + SPACE_BETWEEN_CARDS), 0.0),
StackAxis::Vertical => card
.default_batch
.clone()
.translate(0.0, -(orig_dims.height + SPACE_BETWEEN_CARDS)),
}
} else if idx >= new_idx && idx < orig_idx {
// move batch to the right if target is newly less than us
card.default_batch
.clone()
.translate(width + SPACE_BETWEEN_CARDS, 0.0)
// move batch to the right or bottom if target is newly less than us
match self.axis {
StackAxis::Horizontal => card
.default_batch
.clone()
.translate(orig_dims.width + SPACE_BETWEEN_CARDS, 0.0),
StackAxis::Vertical => card
.default_batch
.clone()
.translate(0.0, orig_dims.height + SPACE_BETWEEN_CARDS),
}
} else {
card.default_batch.clone()
};
Expand Down Expand Up @@ -209,7 +230,14 @@ impl<T: 'static + Copy + PartialEq> DragDrop<T> {
if ScreenRectangle::top_left(top_left, *dims).contains(pt) {
return Some(idx);
}
top_left.x += dims.width + SPACE_BETWEEN_CARDS;
match self.axis {
StackAxis::Horizontal => {
top_left.x += dims.width + SPACE_BETWEEN_CARDS;
}
StackAxis::Vertical => {
top_left.y += dims.height + SPACE_BETWEEN_CARDS;
}
}
}
None
}
Expand Down
5 changes: 4 additions & 1 deletion widgetry/src/widgets/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,12 @@ impl<'a, 'c> Image<'a, 'c> {
}

/// Create a new `Image` from a [`GeomBatch`].
///
/// By default, the given `bounds` will be used for padding, background, etc.
pub fn from_batch(batch: GeomBatch, bounds: Bounds) -> Self {
Self {
source: Some(Cow::Owned(ImageSource::GeomBatch(batch, bounds))),
dims: Some(bounds.into()),
..Default::default()
}
}
Expand Down Expand Up @@ -182,7 +185,7 @@ impl<'a, 'c> Image<'a, 'c> {
}

/// Scale the bounds containing the image. If `dims` are not specified, the image's intrinsic
/// size will be used.
/// size will be used, but padding and background settings will be ignored.
///
/// See [`Self::content_mode`] to control how the image scales to fit its custom bounds.
pub fn dims<D: Into<ScreenDims>>(mut self, dims: D) -> Self {
Expand Down
6 changes: 3 additions & 3 deletions widgetry_demo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use geom::{Angle, Duration, Percent, Polygon, Pt2D, Time};
use widgetry::{
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,
ScreenDims, Series, Settings, SharedAppState, StackAxis, State, TabController, Text, TextExt,
Texture, Toggle, Transition, UpdateType, VerticalAlignment, Widget,
};

pub fn main() {
Expand Down Expand Up @@ -626,7 +626,7 @@ fn build_drag_drop(ctx: &EventCtx, num_cards: usize) -> DragDrop<usize> {
(dims, default_batch, hovering_batch, selected_batch)
}

let mut drag_drop = DragDrop::new(ctx, "drag and drop cards");
let mut drag_drop = DragDrop::new(ctx, "drag and drop cards", StackAxis::Horizontal);
for i in 0..num_cards {
let (dims, default_batch, hovering_batch, selected_batch) = build_card(ctx, i);
drag_drop.push_card(i, dims, default_batch, hovering_batch, selected_batch);
Expand Down

0 comments on commit b231ba6

Please sign in to comment.