mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-25 23:43:25 +03:00
Cut over the story map tool to the brave new World. #763
Figure out how to make objects both clickable and draggable!
This commit is contained in:
parent
8524cfce12
commit
5a37e223c0
@ -148,7 +148,7 @@ impl State<App> for DevToolsMode {
|
||||
return Transition::Push(kml::ViewKML::new_state(ctx, app, None));
|
||||
}
|
||||
"story maps" => {
|
||||
return Transition::Push(story::StoryMapEditor::new_state(ctx));
|
||||
return Transition::Push(story::StoryMapEditor::new_state(ctx, app));
|
||||
}
|
||||
"collisions" => {
|
||||
return Transition::Push(collisions::CollisionsViewer::new_state(ctx, app));
|
||||
|
@ -1,177 +1,177 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use geom::{Distance, LonLat, PolyLine, Polygon, Pt2D, Ring};
|
||||
use geom::{Distance, LonLat, PolyLine, Pt2D, Ring};
|
||||
use map_gui::render::DrawOptions;
|
||||
use map_gui::tools::{ChooseSomething, PromptInput};
|
||||
use widgetry::mapspace::{ObjectID, World, WorldOutcome};
|
||||
use widgetry::{
|
||||
lctrl, Choice, Color, DrawBaselayer, Drawable, EventCtx, GeomBatch, GfxCtx,
|
||||
HorizontalAlignment, Key, Line, Outcome, Panel, RewriteColor, State, Text, TextBox,
|
||||
VerticalAlignment, Widget,
|
||||
lctrl, Choice, Color, DrawBaselayer, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key,
|
||||
Line, Outcome, Panel, SimpleState, State, Text, TextBox, VerticalAlignment, Widget,
|
||||
};
|
||||
|
||||
use crate::app::{App, ShowEverything, Transition};
|
||||
use crate::common::CommonState;
|
||||
|
||||
// TODO This is a really great example of things that widgetry ought to make easier. Maybe a radio
|
||||
// button-ish thing to start?
|
||||
|
||||
// Good inspiration: http://sfo-assess.dha.io/, https://github.com/mapbox/storytelling,
|
||||
// https://storymap.knightlab.com/
|
||||
|
||||
/// A simple tool to place markers and free-hand shapes over a map, then label them.
|
||||
pub struct StoryMapEditor {
|
||||
panel: Panel,
|
||||
story: StoryMap,
|
||||
mode: Mode,
|
||||
world: World<MarkerID>,
|
||||
|
||||
dirty: bool,
|
||||
|
||||
// Index into story.markers
|
||||
// TODO Stick in Mode::View?
|
||||
hovering: Option<usize>,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum Mode {
|
||||
View,
|
||||
PlacingMarker,
|
||||
Dragging(Pt2D, usize),
|
||||
Editing(usize, Panel),
|
||||
Freehand(Option<Lasso>),
|
||||
}
|
||||
// TODO We'll constantly rebuild the world, so these are indices into a list of markers. Maybe we
|
||||
// should just assign opaque IDs and hash into them. (Deleting a marker in the middle of the list
|
||||
// would mean changing IDs of everything after it.)
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
struct MarkerID(usize);
|
||||
impl ObjectID for MarkerID {}
|
||||
|
||||
impl StoryMapEditor {
|
||||
pub fn new_state(ctx: &mut EventCtx) -> Box<dyn State<App>> {
|
||||
let story = StoryMap::new();
|
||||
let mode = Mode::View;
|
||||
let dirty = false;
|
||||
Box::new(StoryMapEditor {
|
||||
panel: make_panel(ctx, &story, &mode, dirty),
|
||||
story,
|
||||
mode,
|
||||
dirty,
|
||||
hovering: None,
|
||||
})
|
||||
pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
|
||||
Self::from_story(ctx, app, StoryMap::new())
|
||||
}
|
||||
|
||||
fn redo_panel(&mut self, ctx: &mut EventCtx) {
|
||||
self.panel = make_panel(ctx, &self.story, &self.mode, self.dirty);
|
||||
fn from_story(ctx: &mut EventCtx, app: &App, story: StoryMap) -> Box<dyn State<App>> {
|
||||
let mut state = StoryMapEditor {
|
||||
panel: Panel::empty(ctx),
|
||||
story,
|
||||
world: World::unbounded(),
|
||||
|
||||
dirty: false,
|
||||
};
|
||||
state.rebuild_panel(ctx);
|
||||
state.rebuild_world(ctx, app);
|
||||
Box::new(state)
|
||||
}
|
||||
|
||||
fn rebuild_panel(&mut self, ctx: &mut EventCtx) {
|
||||
self.panel = Panel::new_builder(Widget::col(vec![
|
||||
Widget::row(vec![
|
||||
Line("Story map editor").small_heading().into_widget(ctx),
|
||||
Widget::vert_separator(ctx, 30.0),
|
||||
ctx.style()
|
||||
.btn_outline
|
||||
.popup(&self.story.name)
|
||||
.hotkey(lctrl(Key::L))
|
||||
.build_widget(ctx, "load"),
|
||||
ctx.style()
|
||||
.btn_plain
|
||||
.icon("system/assets/tools/save.svg")
|
||||
.hotkey(lctrl(Key::S))
|
||||
.disabled(!self.dirty)
|
||||
.build_widget(ctx, "save"),
|
||||
ctx.style().btn_close_widget(ctx),
|
||||
]),
|
||||
ctx.style()
|
||||
.btn_plain
|
||||
.icon_text("system/assets/tools/select.svg", "Draw freehand")
|
||||
.hotkey(Key::F)
|
||||
.build_def(ctx),
|
||||
]))
|
||||
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
|
||||
.build(ctx);
|
||||
}
|
||||
|
||||
fn rebuild_world(&mut self, ctx: &mut EventCtx, app: &App) {
|
||||
let mut world = World::bounded(app.primary.map.get_bounds());
|
||||
|
||||
for (idx, marker) in self.story.markers.iter().enumerate() {
|
||||
let mut draw_normal = GeomBatch::new();
|
||||
let label_center = if marker.pts.len() == 1 {
|
||||
// TODO Erase the "B" from it though...
|
||||
draw_normal = map_gui::tools::goal_marker(ctx, marker.pts[0], 2.0);
|
||||
marker.pts[0]
|
||||
} else {
|
||||
let poly = Ring::must_new(marker.pts.clone()).into_polygon();
|
||||
draw_normal.push(Color::RED.alpha(0.8), poly.clone());
|
||||
if let Ok(o) = poly.to_outline(Distance::meters(1.0)) {
|
||||
draw_normal.push(Color::RED, o);
|
||||
}
|
||||
poly.polylabel()
|
||||
};
|
||||
|
||||
let mut draw_hovered = draw_normal.clone();
|
||||
|
||||
draw_normal.append(
|
||||
Text::from(&marker.label)
|
||||
.bg(Color::CYAN)
|
||||
.render_autocropped(ctx)
|
||||
.scale(0.5)
|
||||
.centered_on(label_center),
|
||||
);
|
||||
let hitbox = draw_normal.unioned_polygon();
|
||||
draw_hovered.append(
|
||||
Text::from(&marker.label)
|
||||
.bg(Color::CYAN)
|
||||
.render_autocropped(ctx)
|
||||
.scale(0.75)
|
||||
.centered_on(label_center),
|
||||
);
|
||||
|
||||
world
|
||||
.add(MarkerID(idx))
|
||||
.hitbox(hitbox)
|
||||
.draw(draw_normal)
|
||||
.draw_hovered(draw_hovered)
|
||||
.hotkey(Key::Backspace, "delete")
|
||||
.clickable()
|
||||
.draggable()
|
||||
.build(ctx);
|
||||
}
|
||||
|
||||
world.initialize_hover(ctx);
|
||||
world.rebuilt_during_drag(&self.world);
|
||||
self.world = world;
|
||||
}
|
||||
}
|
||||
|
||||
impl State<App> for StoryMapEditor {
|
||||
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
|
||||
match self.mode {
|
||||
Mode::View => {
|
||||
ctx.canvas_movement();
|
||||
|
||||
if ctx.redo_mouseover() {
|
||||
self.hovering = None;
|
||||
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
|
||||
self.hovering = self
|
||||
.story
|
||||
.markers
|
||||
.iter()
|
||||
.position(|m| m.hitbox.contains_pt(pt));
|
||||
}
|
||||
}
|
||||
if let Some(idx) = self.hovering {
|
||||
if ctx.input.pressed(Key::LeftControl) {
|
||||
self.mode =
|
||||
Mode::Dragging(ctx.canvas.get_cursor_in_map_space().unwrap(), idx);
|
||||
} else if app.per_obj.left_click(ctx, "edit marker") {
|
||||
self.mode = Mode::Editing(idx, self.story.markers[idx].make_editor(ctx));
|
||||
}
|
||||
}
|
||||
match self.world.event(ctx) {
|
||||
WorldOutcome::ClickedFreeSpace(pt) => {
|
||||
self.story.markers.push(Marker {
|
||||
pts: vec![pt],
|
||||
label: String::new(),
|
||||
});
|
||||
self.dirty = true;
|
||||
self.rebuild_panel(ctx);
|
||||
self.rebuild_world(ctx, app);
|
||||
return Transition::Push(EditingMarker::new_state(
|
||||
ctx,
|
||||
self.story.markers.len() - 1,
|
||||
"new marker",
|
||||
));
|
||||
}
|
||||
Mode::PlacingMarker => {
|
||||
ctx.canvas_movement();
|
||||
|
||||
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
|
||||
if app.primary.map.get_boundary_polygon().contains_pt(pt)
|
||||
&& app.per_obj.left_click(ctx, "place a marker here")
|
||||
{
|
||||
let idx = self.story.markers.len();
|
||||
self.story
|
||||
.markers
|
||||
.push(Marker::new(ctx, vec![pt], String::new()));
|
||||
self.dirty = true;
|
||||
self.redo_panel(ctx);
|
||||
self.mode = Mode::Editing(idx, self.story.markers[idx].make_editor(ctx));
|
||||
}
|
||||
WorldOutcome::Dragging {
|
||||
obj: MarkerID(idx),
|
||||
dx,
|
||||
dy,
|
||||
} => {
|
||||
for pt in &mut self.story.markers[idx].pts {
|
||||
*pt = pt.offset(dx, dy);
|
||||
}
|
||||
self.dirty = true;
|
||||
self.rebuild_panel(ctx);
|
||||
self.rebuild_world(ctx, app);
|
||||
}
|
||||
Mode::Dragging(ref mut last_pt, idx) => {
|
||||
if ctx.redo_mouseover() {
|
||||
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
|
||||
if app.primary.map.get_boundary_polygon().contains_pt(pt) {
|
||||
let dx = pt.x() - last_pt.x();
|
||||
let dy = pt.y() - last_pt.y();
|
||||
*last_pt = pt;
|
||||
self.story.markers[idx] = Marker::new(
|
||||
ctx,
|
||||
self.story.markers[idx]
|
||||
.pts
|
||||
.iter()
|
||||
.map(|pt| pt.offset(dx, dy))
|
||||
.collect(),
|
||||
self.story.markers[idx].event.clone(),
|
||||
);
|
||||
self.dirty = true;
|
||||
self.redo_panel(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.input.key_released(Key::LeftControl) {
|
||||
self.mode = Mode::View;
|
||||
}
|
||||
WorldOutcome::Keypress("delete", MarkerID(idx)) => {
|
||||
self.story.markers.remove(idx);
|
||||
self.dirty = true;
|
||||
self.rebuild_panel(ctx);
|
||||
self.rebuild_world(ctx, app);
|
||||
}
|
||||
Mode::Editing(idx, ref mut panel) => {
|
||||
ctx.canvas_movement();
|
||||
if let Outcome::Clicked(x) = panel.event(ctx) {
|
||||
match x.as_ref() {
|
||||
"close" => {
|
||||
self.mode = Mode::View;
|
||||
self.redo_panel(ctx);
|
||||
}
|
||||
"confirm" => {
|
||||
self.story.markers[idx] = Marker::new(
|
||||
ctx,
|
||||
self.story.markers[idx].pts.clone(),
|
||||
panel.text_box("event"),
|
||||
);
|
||||
self.dirty = true;
|
||||
self.mode = Mode::View;
|
||||
self.redo_panel(ctx);
|
||||
}
|
||||
"delete" => {
|
||||
self.mode = Mode::View;
|
||||
self.hovering = None;
|
||||
self.story.markers.remove(idx);
|
||||
self.dirty = true;
|
||||
self.redo_panel(ctx);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::Freehand(None) => {
|
||||
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
|
||||
if ctx.input.left_mouse_button_pressed() {
|
||||
self.mode = Mode::Freehand(Some(Lasso::new(pt)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::Freehand(Some(ref mut lasso)) => {
|
||||
if let Some(result) = lasso.event(ctx) {
|
||||
let idx = self.story.markers.len();
|
||||
self.story
|
||||
.markers
|
||||
.push(Marker::new(ctx, result.into_points(), String::new()));
|
||||
self.dirty = true;
|
||||
self.redo_panel(ctx);
|
||||
self.mode = Mode::Editing(idx, self.story.markers[idx].make_editor(ctx));
|
||||
}
|
||||
WorldOutcome::ClickedObject(MarkerID(idx)) => {
|
||||
return Transition::Push(EditingMarker::new_state(
|
||||
ctx,
|
||||
idx,
|
||||
&self.story.markers[idx].label,
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Outcome::Clicked(x) = self.panel.event(ctx) {
|
||||
@ -195,7 +195,7 @@ impl State<App> for StoryMapEditor {
|
||||
editor.story.name = name;
|
||||
editor.story.save(app);
|
||||
editor.dirty = false;
|
||||
editor.redo_panel(ctx);
|
||||
editor.rebuild_panel(ctx);
|
||||
})),
|
||||
])
|
||||
}),
|
||||
@ -203,7 +203,7 @@ impl State<App> for StoryMapEditor {
|
||||
} else {
|
||||
self.story.save(app);
|
||||
self.dirty = false;
|
||||
self.redo_panel(ctx);
|
||||
self.rebuild_panel(ctx);
|
||||
}
|
||||
}
|
||||
"load" => {
|
||||
@ -215,7 +215,7 @@ impl State<App> for StoryMapEditor {
|
||||
if story.name == self.story.name {
|
||||
continue;
|
||||
}
|
||||
if let Some(s) = StoryMap::load(ctx, app, story) {
|
||||
if let Some(s) = StoryMap::load(app, story) {
|
||||
choices.push(Choice::new(name, s));
|
||||
}
|
||||
}
|
||||
@ -231,32 +231,19 @@ impl State<App> for StoryMapEditor {
|
||||
ctx,
|
||||
"Load story",
|
||||
choices,
|
||||
Box::new(|story, _, _| {
|
||||
Box::new(|story, ctx, app| {
|
||||
Transition::Multi(vec![
|
||||
Transition::Pop,
|
||||
Transition::ModifyState(Box::new(move |state, ctx, _| {
|
||||
let editor = state.downcast_mut::<StoryMapEditor>().unwrap();
|
||||
editor.story = story;
|
||||
editor.dirty = false;
|
||||
editor.redo_panel(ctx);
|
||||
})),
|
||||
Transition::Replace(StoryMapEditor::from_story(ctx, app, story)),
|
||||
])
|
||||
}),
|
||||
));
|
||||
}
|
||||
"new marker" => {
|
||||
self.hovering = None;
|
||||
self.mode = Mode::PlacingMarker;
|
||||
self.redo_panel(ctx);
|
||||
}
|
||||
"draw freehand" => {
|
||||
self.hovering = None;
|
||||
self.mode = Mode::Freehand(None);
|
||||
self.redo_panel(ctx);
|
||||
}
|
||||
"pan" => {
|
||||
self.mode = Mode::View;
|
||||
self.redo_panel(ctx);
|
||||
"Draw freehand" => {
|
||||
return Transition::Push(Box::new(DrawFreehand {
|
||||
lasso: Lasso::new(),
|
||||
new_idx: self.story.markers.len(),
|
||||
}));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
@ -274,82 +261,11 @@ impl State<App> for StoryMapEditor {
|
||||
opts.label_buildings = true;
|
||||
app.draw(g, opts, &ShowEverything::new());
|
||||
|
||||
match self.mode {
|
||||
Mode::PlacingMarker => {
|
||||
if g.canvas.get_cursor_in_map_space().is_some() {
|
||||
g.fork_screenspace();
|
||||
map_gui::tools::goal_marker(g, g.canvas.get_cursor().to_pt(), 1.0)
|
||||
.color(RewriteColor::Change(Color::hex("#CC4121"), Color::GREEN))
|
||||
.draw(g);
|
||||
g.unfork();
|
||||
}
|
||||
}
|
||||
Mode::Editing(_, ref panel) => {
|
||||
panel.draw(g);
|
||||
}
|
||||
Mode::Freehand(Some(ref lasso)) => {
|
||||
lasso.draw(g);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
for (idx, m) in self.story.markers.iter().enumerate() {
|
||||
if self.hovering == Some(idx) {
|
||||
m.draw_hovered(g);
|
||||
} else {
|
||||
g.redraw(&m.draw);
|
||||
}
|
||||
}
|
||||
|
||||
self.panel.draw(g);
|
||||
CommonState::draw_osd(g, app);
|
||||
self.world.draw(g);
|
||||
}
|
||||
}
|
||||
|
||||
fn make_panel(ctx: &mut EventCtx, story: &StoryMap, mode: &Mode, dirty: bool) -> Panel {
|
||||
Panel::new_builder(Widget::col(vec![
|
||||
Widget::row(vec![
|
||||
Line("Story map editor").small_heading().into_widget(ctx),
|
||||
Widget::vert_separator(ctx, 30.0),
|
||||
ctx.style()
|
||||
.btn_outline
|
||||
.popup(&story.name)
|
||||
.hotkey(lctrl(Key::L))
|
||||
.build_widget(ctx, "load"),
|
||||
ctx.style()
|
||||
.btn_plain
|
||||
.icon("system/assets/tools/save.svg")
|
||||
.hotkey(lctrl(Key::S))
|
||||
.disabled(!dirty)
|
||||
.build_widget(ctx, "save"),
|
||||
ctx.style().btn_close_widget(ctx),
|
||||
]),
|
||||
Widget::row(vec![
|
||||
ctx.style()
|
||||
.btn_plain
|
||||
.icon("system/assets/tools/pin.svg")
|
||||
.disabled(matches!(mode, Mode::PlacingMarker))
|
||||
.hotkey(Key::M)
|
||||
.build_widget(ctx, "new marker"),
|
||||
ctx.style()
|
||||
.btn_plain
|
||||
.icon("system/assets/tools/pan.svg")
|
||||
.disabled(matches!(mode, Mode::View))
|
||||
.hotkey(Key::Escape)
|
||||
.build_widget(ctx, "pan"),
|
||||
ctx.style()
|
||||
.btn_plain
|
||||
.icon("system/assets/tools/select.svg")
|
||||
.disabled(matches!(mode, Mode::Freehand(_)))
|
||||
.hotkey(Key::P)
|
||||
.build_widget(ctx, "draw freehand"),
|
||||
])
|
||||
.evenly_spaced(),
|
||||
]))
|
||||
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
|
||||
.build(ctx)
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
struct RecordedStoryMap {
|
||||
name: String,
|
||||
@ -363,9 +279,7 @@ struct StoryMap {
|
||||
|
||||
struct Marker {
|
||||
pts: Vec<Pt2D>,
|
||||
event: String,
|
||||
hitbox: Polygon,
|
||||
draw: Drawable,
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl StoryMap {
|
||||
@ -376,11 +290,13 @@ impl StoryMap {
|
||||
}
|
||||
}
|
||||
|
||||
fn load(ctx: &mut EventCtx, app: &App, story: RecordedStoryMap) -> Option<StoryMap> {
|
||||
fn load(app: &App, story: RecordedStoryMap) -> Option<StoryMap> {
|
||||
let mut markers = Vec::new();
|
||||
for (gps_pts, event) in story.markers {
|
||||
let pts = app.primary.map.get_gps_bounds().try_convert(&gps_pts)?;
|
||||
markers.push(Marker::new(ctx, pts, event));
|
||||
for (gps_pts, label) in story.markers {
|
||||
markers.push(Marker {
|
||||
pts: app.primary.map.get_gps_bounds().try_convert(&gps_pts)?,
|
||||
label,
|
||||
});
|
||||
}
|
||||
Some(StoryMap {
|
||||
name: story.name,
|
||||
@ -397,7 +313,7 @@ impl StoryMap {
|
||||
.map(|m| {
|
||||
(
|
||||
app.primary.map.get_gps_bounds().convert_back(&m.pts),
|
||||
m.event.clone(),
|
||||
m.label.clone(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
@ -409,121 +325,151 @@ impl StoryMap {
|
||||
}
|
||||
}
|
||||
|
||||
impl Marker {
|
||||
fn new(ctx: &mut EventCtx, pts: Vec<Pt2D>, event: String) -> Marker {
|
||||
let mut batch = GeomBatch::new();
|
||||
struct EditingMarker {
|
||||
idx: usize,
|
||||
}
|
||||
|
||||
let hitbox = if pts.len() == 1 {
|
||||
batch.append(map_gui::tools::goal_marker(ctx, pts[0], 2.0));
|
||||
batch.append(
|
||||
Text::from(&event)
|
||||
.bg(Color::CYAN)
|
||||
.render_autocropped(ctx)
|
||||
.scale(0.5)
|
||||
.centered_on(pts[0]),
|
||||
);
|
||||
batch.unioned_polygon()
|
||||
} else {
|
||||
let poly = Ring::must_new(pts.clone()).into_polygon();
|
||||
batch.push(Color::RED.alpha(0.8), poly.clone());
|
||||
if let Ok(o) = poly.to_outline(Distance::meters(1.0)) {
|
||||
batch.push(Color::RED, o);
|
||||
}
|
||||
// TODO Refactor
|
||||
batch.append(
|
||||
Text::from(&event)
|
||||
.bg(Color::CYAN)
|
||||
.render_autocropped(ctx)
|
||||
.scale(0.5)
|
||||
.centered_on(poly.polylabel()),
|
||||
);
|
||||
poly
|
||||
};
|
||||
Marker {
|
||||
pts,
|
||||
event,
|
||||
hitbox,
|
||||
draw: ctx.upload(batch),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_hovered(&self, g: &mut GfxCtx) {
|
||||
let mut batch = GeomBatch::new();
|
||||
if self.pts.len() == 1 {
|
||||
batch.append(
|
||||
map_gui::tools::goal_marker(g, self.pts[0], 2.0)
|
||||
.color(RewriteColor::Change(Color::hex("#CC4121"), Color::RED)),
|
||||
);
|
||||
batch.append(
|
||||
Text::from(&self.event)
|
||||
.bg(Color::CYAN)
|
||||
.render_autocropped(g)
|
||||
.scale(0.75)
|
||||
.centered_on(self.pts[0]),
|
||||
);
|
||||
} else {
|
||||
batch.push(Color::RED, Ring::must_new(self.pts.clone()).into_polygon());
|
||||
// TODO Refactor plz
|
||||
batch.append(
|
||||
Text::from(&self.event)
|
||||
.bg(Color::CYAN)
|
||||
.render_autocropped(g)
|
||||
.scale(0.75)
|
||||
.centered_on(self.hitbox.polylabel()),
|
||||
);
|
||||
}
|
||||
batch.draw(g);
|
||||
}
|
||||
|
||||
fn make_editor(&self, ctx: &mut EventCtx) -> Panel {
|
||||
Panel::new_builder(Widget::col(vec![
|
||||
impl EditingMarker {
|
||||
fn new_state(ctx: &mut EventCtx, idx: usize, label: &str) -> Box<dyn State<App>> {
|
||||
let panel = Panel::new_builder(Widget::col(vec![
|
||||
Widget::row(vec![
|
||||
Line("Editing marker").small_heading().into_widget(ctx),
|
||||
ctx.style().btn_close_widget(ctx),
|
||||
]),
|
||||
ctx.style().btn_outline.text("delete").build_def(ctx),
|
||||
TextBox::default_widget(ctx, "event", self.event.clone()),
|
||||
TextBox::default_widget(ctx, "label", label.to_string()),
|
||||
ctx.style()
|
||||
.btn_outline
|
||||
.text("confirm")
|
||||
.hotkey(Key::Enter)
|
||||
.build_def(ctx),
|
||||
]))
|
||||
.build(ctx)
|
||||
.build(ctx);
|
||||
<dyn SimpleState<_>>::new_state(panel, Box::new(EditingMarker { idx }))
|
||||
}
|
||||
}
|
||||
|
||||
impl SimpleState<App> for EditingMarker {
|
||||
fn on_click(&mut self, _: &mut EventCtx, _: &mut App, x: &str, panel: &Panel) -> Transition {
|
||||
match x {
|
||||
"close" => {
|
||||
return Transition::Pop;
|
||||
}
|
||||
"confirm" => {
|
||||
let idx = self.idx;
|
||||
let label = panel.text_box("label");
|
||||
return Transition::Multi(vec![
|
||||
Transition::Pop,
|
||||
Transition::ModifyState(Box::new(move |state, ctx, app| {
|
||||
let editor = state.downcast_mut::<StoryMapEditor>().unwrap();
|
||||
editor.story.markers[idx].label = label;
|
||||
|
||||
editor.dirty = true;
|
||||
editor.rebuild_panel(ctx);
|
||||
editor.rebuild_world(ctx, app);
|
||||
})),
|
||||
]);
|
||||
}
|
||||
"delete" => {
|
||||
let idx = self.idx;
|
||||
return Transition::Multi(vec![
|
||||
Transition::Pop,
|
||||
Transition::ModifyState(Box::new(move |state, ctx, app| {
|
||||
let editor = state.downcast_mut::<StoryMapEditor>().unwrap();
|
||||
editor.story.markers.remove(idx);
|
||||
|
||||
editor.dirty = true;
|
||||
editor.rebuild_panel(ctx);
|
||||
editor.rebuild_world(ctx, app);
|
||||
})),
|
||||
]);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_baselayer(&self) -> DrawBaselayer {
|
||||
DrawBaselayer::PreviousState
|
||||
}
|
||||
}
|
||||
|
||||
struct DrawFreehand {
|
||||
lasso: Lasso,
|
||||
new_idx: usize,
|
||||
}
|
||||
|
||||
impl State<App> for DrawFreehand {
|
||||
fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition {
|
||||
if let Some(result) = self.lasso.event(ctx) {
|
||||
let idx = self.new_idx;
|
||||
return Transition::Multi(vec![
|
||||
Transition::Pop,
|
||||
Transition::ModifyState(Box::new(move |state, ctx, app| {
|
||||
let editor = state.downcast_mut::<StoryMapEditor>().unwrap();
|
||||
editor.story.markers.push(Marker {
|
||||
pts: result.into_points(),
|
||||
label: String::new(),
|
||||
});
|
||||
|
||||
editor.dirty = true;
|
||||
editor.rebuild_panel(ctx);
|
||||
editor.rebuild_world(ctx, app);
|
||||
})),
|
||||
Transition::Push(EditingMarker::new_state(ctx, idx, "new marker")),
|
||||
]);
|
||||
}
|
||||
|
||||
Transition::Keep
|
||||
}
|
||||
|
||||
fn draw_baselayer(&self) -> DrawBaselayer {
|
||||
DrawBaselayer::PreviousState
|
||||
}
|
||||
|
||||
fn draw(&self, g: &mut GfxCtx, _: &App) {
|
||||
self.lasso.draw(g);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO This should totally be an widgetry tool
|
||||
// TODO Simplify points
|
||||
struct Lasso {
|
||||
pl: PolyLine,
|
||||
pl: Option<PolyLine>,
|
||||
}
|
||||
|
||||
impl Lasso {
|
||||
fn new(pt: Pt2D) -> Lasso {
|
||||
Lasso {
|
||||
pl: PolyLine::must_new(vec![pt, pt.offset(0.1, 0.0)]),
|
||||
}
|
||||
fn new() -> Lasso {
|
||||
Lasso { pl: None }
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx) -> Option<Ring> {
|
||||
if ctx.input.left_mouse_button_released() {
|
||||
return Some(simplify(self.pl.points().clone()));
|
||||
if self.pl.is_none() {
|
||||
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
|
||||
if ctx.input.left_mouse_button_pressed() {
|
||||
self.pl = Some(PolyLine::must_new(vec![pt, pt.offset(0.1, 0.0)]));
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if ctx.input.left_mouse_button_released() {
|
||||
return Some(simplify(self.pl.take().unwrap().into_points()));
|
||||
}
|
||||
|
||||
let current_pl = self.pl.as_ref().unwrap();
|
||||
if ctx.redo_mouseover() {
|
||||
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
|
||||
if let Ok(pl) = PolyLine::new(vec![self.pl.last_pt(), pt]) {
|
||||
if let Ok(pl) = PolyLine::new(vec![current_pl.last_pt(), pt]) {
|
||||
// Did we make a crossing?
|
||||
if let Some((hit, _)) = self.pl.intersection(&pl) {
|
||||
if let Some(slice) = self.pl.get_slice_starting_at(hit) {
|
||||
if let Some((hit, _)) = current_pl.intersection(&pl) {
|
||||
if let Some(slice) = current_pl.get_slice_starting_at(hit) {
|
||||
return Some(simplify(slice.into_points()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut pts = self.pl.points().clone();
|
||||
let mut pts = current_pl.points().clone();
|
||||
pts.push(pt);
|
||||
if let Ok(new) = PolyLine::new(pts) {
|
||||
self.pl = new;
|
||||
self.pl = Some(new);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -532,11 +478,12 @@ impl Lasso {
|
||||
}
|
||||
|
||||
fn draw(&self, g: &mut GfxCtx) {
|
||||
g.draw_polygon(
|
||||
Color::RED.alpha(0.8),
|
||||
self.pl
|
||||
.make_polygons(Distance::meters(5.0) / g.canvas.cam_zoom),
|
||||
);
|
||||
if let Some(ref pl) = self.pl {
|
||||
g.draw_polygon(
|
||||
Color::RED.alpha(0.8),
|
||||
pl.make_polygons(Distance::meters(5.0) / g.canvas.cam_zoom),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,8 +21,9 @@ pub struct World<ID: ObjectID> {
|
||||
quadtree: QuadTree<ID>,
|
||||
|
||||
hovering: Option<ID>,
|
||||
// If we're currently dragging, where was the cursor during the last movement?
|
||||
dragging_from: Option<Pt2D>,
|
||||
// If we're currently dragging, where was the cursor during the last movement, and has the
|
||||
// cursor moved since starting the drag?
|
||||
dragging_from: Option<(Pt2D, bool)>,
|
||||
}
|
||||
|
||||
/// The result of a `World` handling an event
|
||||
@ -33,6 +34,8 @@ pub enum WorldOutcome<ID: ObjectID> {
|
||||
Dragging { obj: ID, dx: f64, dy: f64 },
|
||||
/// While hovering on an object with a defined hotkey, that key was pressed.
|
||||
Keypress(&'static str, ID),
|
||||
/// A hoverable object was clicked
|
||||
ClickedObject(ID),
|
||||
/// Nothing interesting happened
|
||||
Nothing,
|
||||
}
|
||||
@ -49,6 +52,7 @@ pub struct ObjectBuilder<'a, ID: ObjectID> {
|
||||
zorder: usize,
|
||||
draw_normal: Option<GeomBatch>,
|
||||
draw_hover: Option<GeomBatch>,
|
||||
clickable: bool,
|
||||
draggable: bool,
|
||||
keybindings: Vec<(MultiKey, &'static str)>,
|
||||
}
|
||||
@ -105,6 +109,13 @@ impl<'a, ID: ObjectID> ObjectBuilder<'a, ID> {
|
||||
self.draw_hovered(batch)
|
||||
}
|
||||
|
||||
/// Mark the object as clickable. `WorldOutcome::ClickedObject` will be fired.
|
||||
pub fn clickable(mut self) -> Self {
|
||||
assert!(!self.clickable, "called clickable twice");
|
||||
self.clickable = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark the object as draggable. The user can hover on this object, then click and drag it.
|
||||
/// `WorldOutcome::Dragging` events will be fired.
|
||||
///
|
||||
@ -145,6 +156,7 @@ impl<'a, ID: ObjectID> ObjectBuilder<'a, ID> {
|
||||
.expect("didn't specify how to draw normally"),
|
||||
),
|
||||
draw_hover: self.draw_hover.take().map(|batch| ctx.upload(batch)),
|
||||
clickable: self.clickable,
|
||||
draggable: self.draggable,
|
||||
keybindings: self.keybindings,
|
||||
},
|
||||
@ -159,6 +171,7 @@ struct Object<ID: ObjectID> {
|
||||
zorder: usize,
|
||||
draw_normal: Drawable,
|
||||
draw_hover: Option<Drawable>,
|
||||
clickable: bool,
|
||||
draggable: bool,
|
||||
// TODO How should we communicate these keypresses are possible? Something standard, like
|
||||
// button tooltips?
|
||||
@ -203,6 +216,7 @@ impl<ID: ObjectID> World<ID> {
|
||||
zorder: 0,
|
||||
draw_normal: None,
|
||||
draw_hover: None,
|
||||
clickable: false,
|
||||
draggable: false,
|
||||
keybindings: Vec::new(),
|
||||
}
|
||||
@ -225,9 +239,15 @@ impl<ID: ObjectID> World<ID> {
|
||||
|
||||
/// Let objects in the world respond to something happening.
|
||||
pub fn event(&mut self, ctx: &mut EventCtx) -> WorldOutcome<ID> {
|
||||
if let Some(drag_from) = self.dragging_from {
|
||||
if let Some((drag_from, moved)) = self.dragging_from {
|
||||
if ctx.input.left_mouse_button_released() {
|
||||
self.dragging_from = None;
|
||||
// For objects that're both clickable and draggable, we don't know what the user is
|
||||
// doing until they release the mouse!
|
||||
if !moved && self.objects[&self.hovering.unwrap()].clickable {
|
||||
return WorldOutcome::ClickedObject(self.hovering.unwrap());
|
||||
}
|
||||
|
||||
self.hovering = ctx
|
||||
.canvas
|
||||
.get_cursor_in_map_space()
|
||||
@ -243,7 +263,7 @@ impl<ID: ObjectID> World<ID> {
|
||||
if let Some(cursor) = ctx.canvas.get_cursor_in_map_space() {
|
||||
let dx = cursor.x() - drag_from.x();
|
||||
let dy = cursor.y() - drag_from.y();
|
||||
self.dragging_from = Some(cursor);
|
||||
self.dragging_from = Some((cursor, true));
|
||||
return WorldOutcome::Dragging {
|
||||
obj: self.hovering.unwrap(),
|
||||
dx,
|
||||
@ -272,10 +292,16 @@ impl<ID: ObjectID> World<ID> {
|
||||
if let Some(id) = self.hovering {
|
||||
let obj = &self.objects[&id];
|
||||
|
||||
// For objects both clickable and draggable, the branch below will win, and we'll
|
||||
// detect a normal click elsewhere.
|
||||
if obj.clickable && ctx.normal_left_click() {
|
||||
return WorldOutcome::ClickedObject(id);
|
||||
}
|
||||
|
||||
if obj.draggable {
|
||||
allow_panning = false;
|
||||
if ctx.input.left_mouse_button_pressed() {
|
||||
self.dragging_from = Some(cursor);
|
||||
self.dragging_from = Some((cursor, false));
|
||||
return WorldOutcome::Nothing;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user