mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-11-25 22:13:27 +03:00
finally make autocomplete a real widget
This commit is contained in:
parent
6f56bf64bf
commit
0b6418bdb6
@ -1,8 +1,7 @@
|
||||
use crate::assets::Assets;
|
||||
use crate::backend::{GfxCtxInnards, PrerenderInnards};
|
||||
use crate::{
|
||||
Canvas, Color, Drawable, FancyColor, GeomBatch, HorizontalAlignment, ScreenDims, ScreenPt,
|
||||
ScreenRectangle, Text, VerticalAlignment,
|
||||
Canvas, Color, Drawable, FancyColor, GeomBatch, ScreenDims, ScreenPt, ScreenRectangle, Text,
|
||||
};
|
||||
use geom::{Bounds, Circle, Distance, Line, Polygon, Pt2D};
|
||||
use std::cell::Cell;
|
||||
@ -177,25 +176,6 @@ impl<'a> GfxCtx<'a> {
|
||||
|
||||
// Canvas stuff.
|
||||
|
||||
// The text box covers up what's beneath and eats the cursor (for get_cursor_in_map_space).
|
||||
// TODO Super close to deleting this.
|
||||
pub(crate) fn draw_blocking_text(
|
||||
&mut self,
|
||||
txt: Text,
|
||||
(horiz, vert): (HorizontalAlignment, VerticalAlignment),
|
||||
) {
|
||||
let batch = txt.render_g(self);
|
||||
let dims = batch.get_dims();
|
||||
let top_left = self
|
||||
.canvas
|
||||
.align_window(&self.prerender.assets, dims, horiz, vert);
|
||||
|
||||
self.canvas
|
||||
.mark_covered_area(ScreenRectangle::top_left(top_left, dims));
|
||||
let draw = self.upload(batch);
|
||||
self.redraw_at(top_left, &draw);
|
||||
}
|
||||
|
||||
pub fn draw_mouse_tooltip(&mut self, txt: Text) {
|
||||
// Add some padding
|
||||
let pad = 5.0;
|
||||
|
@ -165,15 +165,6 @@ impl UserInput {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO I'm not sure this is even useful anymore
|
||||
pub(crate) fn use_event_directly(&mut self) -> Option<Event> {
|
||||
if self.event_consumed {
|
||||
return None;
|
||||
}
|
||||
self.consume_event();
|
||||
Some(self.event)
|
||||
}
|
||||
|
||||
pub(crate) fn consume_event(&mut self) {
|
||||
assert!(!self.event_consumed);
|
||||
self.event_consumed = true;
|
||||
|
@ -68,7 +68,7 @@ pub use crate::widgets::spinner::Spinner;
|
||||
pub(crate) use crate::widgets::text_box::TextBox;
|
||||
pub use crate::widgets::WidgetImpl;
|
||||
|
||||
pub enum InputResult<T: Clone> {
|
||||
pub(crate) enum InputResult<T: Clone> {
|
||||
Canceled,
|
||||
StillActive,
|
||||
Done(String, T),
|
||||
|
@ -1,8 +1,8 @@
|
||||
use crate::widgets::containers::{Container, Nothing};
|
||||
use crate::{
|
||||
Btn, Button, Checkbox, Choice, Color, Drawable, Dropdown, EventCtx, Filler, GeomBatch, GfxCtx,
|
||||
HorizontalAlignment, JustDraw, Menu, MultiKey, RewriteColor, ScreenDims, ScreenPt,
|
||||
ScreenRectangle, Slider, Spinner, TextBox, VerticalAlignment, WidgetImpl,
|
||||
Autocomplete, Btn, Button, Checkbox, Choice, Color, Drawable, Dropdown, EventCtx, Filler,
|
||||
GeomBatch, GfxCtx, HorizontalAlignment, JustDraw, Menu, MultiKey, RewriteColor, ScreenDims,
|
||||
ScreenPt, ScreenRectangle, Slider, Spinner, TextBox, VerticalAlignment, WidgetImpl,
|
||||
};
|
||||
use geom::{Distance, Polygon};
|
||||
use std::collections::HashSet;
|
||||
@ -423,6 +423,10 @@ impl Widget {
|
||||
pub(crate) fn take_btn(self) -> Button {
|
||||
*self.widget.downcast::<Button>().ok().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn take_menu<T: 'static + Clone>(self) -> Menu<T> {
|
||||
*self.widget.downcast::<Menu<T>>().ok().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
enum Dims {
|
||||
@ -692,6 +696,10 @@ impl Composite {
|
||||
self.find::<Dropdown<T>>(name).current_value()
|
||||
}
|
||||
|
||||
pub fn autocomplete_done<T: 'static + Clone>(&self, name: &str) -> Option<Vec<T>> {
|
||||
self.find::<Autocomplete<T>>(name).final_value()
|
||||
}
|
||||
|
||||
pub fn filler_rect(&self, name: &str) -> ScreenRectangle {
|
||||
if let Some(w) = self.top_level.find(name) {
|
||||
if w.widget.is::<Filler>() {
|
||||
|
@ -120,6 +120,7 @@ impl Text {
|
||||
txt
|
||||
}
|
||||
|
||||
// TODO Remove this
|
||||
pub fn with_bg(mut self) -> Text {
|
||||
assert!(self.bg_color.is_none());
|
||||
self.bg_color = Some(BG_COLOR);
|
||||
|
@ -1,140 +1,120 @@
|
||||
use crate::{
|
||||
text, Event, EventCtx, GfxCtx, HorizontalAlignment, InputResult, Key, Line, Text,
|
||||
VerticalAlignment,
|
||||
Choice, EventCtx, GfxCtx, InputResult, Menu, Outcome, ScreenDims, ScreenPt, TextBox, Widget,
|
||||
WidgetImpl,
|
||||
};
|
||||
use simsearch::SimSearch;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::hash::Hash;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const NUM_SEARCH_RESULTS: usize = 5;
|
||||
const NUM_SEARCH_RESULTS: usize = 10;
|
||||
|
||||
pub struct Autocomplete<T: Clone + Hash + Eq> {
|
||||
prompt: String,
|
||||
choices: BTreeMap<String, HashSet<T>>,
|
||||
// TODO I don't even think we need to declare Clone...
|
||||
pub struct Autocomplete<T: Clone> {
|
||||
choices: HashMap<String, Vec<T>>,
|
||||
// Maps index to choice
|
||||
search_map: Vec<String>,
|
||||
search: SimSearch<usize>,
|
||||
|
||||
line: String,
|
||||
cursor_x: usize,
|
||||
shift_pressed: bool,
|
||||
current_results: Vec<usize>,
|
||||
cursor_y: usize,
|
||||
tb: TextBox,
|
||||
menu: Menu<()>,
|
||||
|
||||
current_line: String,
|
||||
chosen_values: Option<Vec<T>>,
|
||||
}
|
||||
|
||||
impl<T: Clone + Hash + Eq> Autocomplete<T> {
|
||||
pub fn new(prompt: &str, choices_list: Vec<(String, T)>) -> Autocomplete<T> {
|
||||
let mut choices = BTreeMap::new();
|
||||
for (name, data) in choices_list {
|
||||
if !choices.contains_key(&name) {
|
||||
choices.insert(name.clone(), HashSet::new());
|
||||
}
|
||||
choices.get_mut(&name).unwrap().insert(data);
|
||||
}
|
||||
let mut search_map = Vec::new();
|
||||
let mut search = SimSearch::new();
|
||||
let mut current_results = Vec::new();
|
||||
for (idx, name) in choices.keys().enumerate() {
|
||||
search_map.push(name.to_string());
|
||||
search.insert(idx, name);
|
||||
if idx < NUM_SEARCH_RESULTS {
|
||||
current_results.push(idx);
|
||||
}
|
||||
impl<T: 'static + Clone> Autocomplete<T> {
|
||||
// If multiple names map to the same data, all of the possible values will be returned
|
||||
pub fn new(ctx: &mut EventCtx, raw_choices: Vec<(String, T)>) -> Widget {
|
||||
let mut choices = HashMap::new();
|
||||
for (name, data) in raw_choices {
|
||||
choices.entry(name).or_insert_with(Vec::new).push(data);
|
||||
}
|
||||
|
||||
Autocomplete {
|
||||
prompt: prompt.to_string(),
|
||||
let mut search_map = Vec::new();
|
||||
let mut search = SimSearch::new();
|
||||
for name in choices.keys() {
|
||||
search.insert(search_map.len(), name);
|
||||
search_map.push(name.to_string());
|
||||
}
|
||||
|
||||
let mut a = Autocomplete {
|
||||
choices,
|
||||
search_map,
|
||||
search,
|
||||
|
||||
line: String::new(),
|
||||
cursor_x: 0,
|
||||
shift_pressed: false,
|
||||
current_results,
|
||||
cursor_y: 0,
|
||||
}
|
||||
}
|
||||
tb: TextBox::new(ctx, 50, String::new(), true),
|
||||
menu: Menu::<()>::new(ctx, Vec::new()).take_menu(),
|
||||
|
||||
pub fn draw(&self, g: &mut GfxCtx) {
|
||||
let mut txt = Text::from(Line(&self.prompt).small_heading()).with_bg();
|
||||
|
||||
txt.add(Line(&self.line[0..self.cursor_x]));
|
||||
if self.cursor_x < self.line.len() {
|
||||
// TODO This "cursor" looks awful!
|
||||
txt.append_all(vec![
|
||||
Line("|").fg(text::SELECTED_COLOR),
|
||||
Line(&self.line[self.cursor_x..=self.cursor_x]),
|
||||
Line(&self.line[self.cursor_x + 1..]),
|
||||
]);
|
||||
} else {
|
||||
txt.append(Line("|").fg(text::SELECTED_COLOR));
|
||||
}
|
||||
|
||||
for (idx, id) in self.current_results.iter().enumerate() {
|
||||
if idx == self.cursor_y {
|
||||
txt.add_highlighted(Line(&self.search_map[*id]), text::SELECTED_COLOR);
|
||||
} else {
|
||||
txt.add(Line(&self.search_map[*id]));
|
||||
}
|
||||
}
|
||||
|
||||
g.draw_blocking_text(
|
||||
txt,
|
||||
(HorizontalAlignment::Center, VerticalAlignment::Center),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn event(&mut self, ctx: &mut EventCtx) -> InputResult<HashSet<T>> {
|
||||
let maybe_ev = ctx.input.use_event_directly();
|
||||
if maybe_ev.is_none() {
|
||||
return InputResult::StillActive;
|
||||
}
|
||||
let ev = maybe_ev.unwrap();
|
||||
|
||||
if ev == Event::KeyPress(Key::Escape) {
|
||||
return InputResult::Canceled;
|
||||
} else if ev == Event::KeyPress(Key::Enter) {
|
||||
if self.current_results.is_empty() {
|
||||
return InputResult::Canceled;
|
||||
}
|
||||
let name = &self.search_map[self.current_results[self.cursor_y]];
|
||||
return InputResult::Done(name.to_string(), self.choices.remove(name).unwrap());
|
||||
} else if ev == Event::KeyPress(Key::LeftShift) {
|
||||
self.shift_pressed = true;
|
||||
} else if ev == Event::KeyRelease(Key::LeftShift) {
|
||||
self.shift_pressed = false;
|
||||
} else if ev == Event::KeyPress(Key::LeftArrow) {
|
||||
if self.cursor_x > 0 {
|
||||
self.cursor_x -= 1;
|
||||
}
|
||||
} else if ev == Event::KeyPress(Key::RightArrow) {
|
||||
self.cursor_x = (self.cursor_x + 1).min(self.line.len());
|
||||
} else if ev == Event::KeyPress(Key::UpArrow) {
|
||||
if self.cursor_y > 0 {
|
||||
self.cursor_y -= 1;
|
||||
}
|
||||
} else if ev == Event::KeyPress(Key::DownArrow) {
|
||||
self.cursor_y = (self.cursor_y + 1).min(self.current_results.len() - 1);
|
||||
} else if ev == Event::KeyPress(Key::Backspace) {
|
||||
if self.cursor_x > 0 {
|
||||
self.line.remove(self.cursor_x - 1);
|
||||
self.cursor_x -= 1;
|
||||
|
||||
self.current_results = self.search.search(&self.line);
|
||||
self.current_results.truncate(NUM_SEARCH_RESULTS);
|
||||
self.cursor_y = 0;
|
||||
}
|
||||
} else if let Event::KeyPress(key) = ev {
|
||||
if let Some(c) = key.to_char(self.shift_pressed) {
|
||||
self.line.insert(self.cursor_x, c);
|
||||
self.cursor_x += 1;
|
||||
|
||||
self.current_results = self.search.search(&self.line);
|
||||
self.current_results.truncate(NUM_SEARCH_RESULTS);
|
||||
self.cursor_y = 0;
|
||||
}
|
||||
current_line: String::new(),
|
||||
chosen_values: None,
|
||||
};
|
||||
InputResult::StillActive
|
||||
a.recalc_menu(ctx);
|
||||
Widget::new(Box::new(a))
|
||||
}
|
||||
|
||||
pub fn final_value(&self) -> Option<Vec<T>> {
|
||||
self.chosen_values.clone()
|
||||
}
|
||||
|
||||
fn recalc_menu(&mut self, ctx: &mut EventCtx) {
|
||||
let mut indices = self.search.search(&self.current_line);
|
||||
if indices.is_empty() {
|
||||
indices = (0..NUM_SEARCH_RESULTS.min(self.search_map.len())).collect();
|
||||
}
|
||||
|
||||
self.menu = Menu::new(
|
||||
ctx,
|
||||
indices
|
||||
.into_iter()
|
||||
.take(NUM_SEARCH_RESULTS)
|
||||
.map(|idx| Choice::new(&self.search_map[idx], ()))
|
||||
.collect(),
|
||||
)
|
||||
.take_menu();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static + Clone> WidgetImpl for Autocomplete<T> {
|
||||
fn get_dims(&self) -> ScreenDims {
|
||||
let d1 = self.tb.get_dims();
|
||||
let d2 = self.menu.get_dims();
|
||||
ScreenDims::new(d1.width.max(d2.width), d1.height + d2.height)
|
||||
}
|
||||
|
||||
fn set_pos(&mut self, top_left: ScreenPt) {
|
||||
self.tb.set_pos(top_left);
|
||||
self.menu.set_pos(ScreenPt::new(
|
||||
top_left.x,
|
||||
top_left.y + self.tb.get_dims().height,
|
||||
));
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, redo_layout: &mut bool) -> Option<Outcome> {
|
||||
assert!(self.chosen_values.is_none());
|
||||
|
||||
self.tb.event(ctx, redo_layout);
|
||||
if self.tb.get_line() != self.current_line {
|
||||
self.current_line = self.tb.get_line();
|
||||
self.recalc_menu(ctx);
|
||||
*redo_layout = true;
|
||||
} else {
|
||||
self.menu.event(ctx, redo_layout);
|
||||
match self.menu.state {
|
||||
InputResult::StillActive => {}
|
||||
// Ignore this and make sure the Composite has a quit control
|
||||
InputResult::Canceled => {}
|
||||
InputResult::Done(ref name, _) => {
|
||||
// Mutating choices is fine, because we're supposed to be consumed by the
|
||||
// caller immediately after this.
|
||||
self.chosen_values = Some(self.choices.remove(name).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn draw(&self, g: &mut GfxCtx) {
|
||||
self.tb.draw(g);
|
||||
self.menu.draw(g);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use geom::{Polygon, Pt2D};
|
||||
pub struct Dropdown<T: Clone> {
|
||||
current_idx: usize,
|
||||
btn: Button,
|
||||
// TODO Why not T?
|
||||
menu: Option<Menu<usize>>,
|
||||
label: String,
|
||||
|
||||
@ -70,7 +71,7 @@ impl<T: 'static + Clone> WidgetImpl for Dropdown<T> {
|
||||
} else {
|
||||
if self.btn.event(ctx, redo_layout).is_some() {
|
||||
// TODO set current idx in menu
|
||||
let mut menu = *Menu::new(
|
||||
let mut menu = Menu::new(
|
||||
ctx,
|
||||
self.choices
|
||||
.iter()
|
||||
@ -78,10 +79,7 @@ impl<T: 'static + Clone> WidgetImpl for Dropdown<T> {
|
||||
.map(|(idx, c)| c.with_value(idx))
|
||||
.collect(),
|
||||
)
|
||||
.widget
|
||||
.downcast::<Menu<usize>>()
|
||||
.ok()
|
||||
.unwrap();
|
||||
.take_menu();
|
||||
menu.set_pos(ScreenPt::new(
|
||||
self.btn.top_left.x,
|
||||
self.btn.top_left.y + self.btn.dims.height + 15.0,
|
||||
|
@ -80,6 +80,10 @@ impl<T: 'static + Clone> WidgetImpl for Menu<T> {
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, _redo_layout: &mut bool) -> Option<Outcome> {
|
||||
if self.choices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match self.state {
|
||||
InputResult::StillActive => {}
|
||||
_ => unreachable!(),
|
||||
@ -167,6 +171,10 @@ impl<T: 'static + Clone> WidgetImpl for Menu<T> {
|
||||
}
|
||||
|
||||
fn draw(&self, g: &mut GfxCtx) {
|
||||
if self.choices.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let draw = g.upload(self.calculate_txt().render_g(g));
|
||||
// In between tooltip and normal screenspace
|
||||
g.fork(Pt2D::new(0.0, 0.0), self.top_left, 1.0, Some(0.1));
|
||||
|
@ -94,7 +94,7 @@ impl Minimap {
|
||||
self.set_zoom(ctx, app, 3);
|
||||
}
|
||||
x if x == "search" => {
|
||||
return Some(Transition::Push(Box::new(navigate::Navigator::new(app))));
|
||||
return Some(Transition::Push(navigate::Navigator::new(ctx, app)));
|
||||
}
|
||||
x if x == "shortcuts" => {
|
||||
return Some(Transition::Push(shortcuts::ChoosingShortcut::new()));
|
||||
|
@ -1,117 +1,184 @@
|
||||
use crate::app::App;
|
||||
use crate::colors;
|
||||
use crate::common::Warping;
|
||||
use crate::game::{State, Transition};
|
||||
use crate::helpers::ID;
|
||||
use ezgui::{Autocomplete, EventCtx, GfxCtx, InputResult};
|
||||
use ezgui::{
|
||||
hotkey, Autocomplete, Btn, Composite, EventCtx, GfxCtx, Key, Line, Outcome, Text, Widget,
|
||||
};
|
||||
use map_model::RoadID;
|
||||
use std::collections::HashSet;
|
||||
|
||||
// TODO Canonicalize names, handling abbreviations like east/e and street/st
|
||||
pub struct Navigator {
|
||||
autocomplete: Autocomplete<RoadID>,
|
||||
composite: Composite,
|
||||
}
|
||||
|
||||
impl Navigator {
|
||||
pub fn new(app: &App) -> Navigator {
|
||||
// TODO Canonicalize names, handling abbreviations like east/e and street/st
|
||||
Navigator {
|
||||
autocomplete: Autocomplete::new(
|
||||
"Warp where?",
|
||||
app.primary
|
||||
.map
|
||||
.all_roads()
|
||||
.iter()
|
||||
.map(|r| (r.get_name(), r.id))
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
pub fn new(ctx: &mut EventCtx, app: &App) -> Box<dyn State> {
|
||||
Box::new(Navigator {
|
||||
composite: Composite::new(
|
||||
Widget::col(vec![
|
||||
Widget::row(vec![
|
||||
Line("Enter a street name").small_heading().draw(ctx),
|
||||
Btn::text_fg("X")
|
||||
.build_def(ctx, hotkey(Key::Escape))
|
||||
.align_right(),
|
||||
]),
|
||||
Autocomplete::new(
|
||||
ctx,
|
||||
app.primary
|
||||
.map
|
||||
.all_roads()
|
||||
.iter()
|
||||
.map(|r| (r.get_name(), r.id))
|
||||
.collect(),
|
||||
)
|
||||
.named("street"),
|
||||
])
|
||||
.bg(colors::PANEL_BG),
|
||||
)
|
||||
.build(ctx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl State for Navigator {
|
||||
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
|
||||
let map = &app.primary.map;
|
||||
match self.autocomplete.event(ctx) {
|
||||
InputResult::Canceled => Transition::Pop,
|
||||
InputResult::Done(name, ids) => {
|
||||
// Roads share intersections, so of course there'll be overlap here.
|
||||
let mut cross_streets = HashSet::new();
|
||||
for r in &ids {
|
||||
let road = map.get_r(*r);
|
||||
for i in &[road.src_i, road.dst_i] {
|
||||
for cross in &map.get_i(*i).roads {
|
||||
if !ids.contains(cross) {
|
||||
cross_streets.insert(*cross);
|
||||
}
|
||||
}
|
||||
}
|
||||
match self.composite.event(ctx) {
|
||||
Some(Outcome::Clicked(x)) => match x.as_ref() {
|
||||
"X" => {
|
||||
return Transition::Pop;
|
||||
}
|
||||
Transition::Replace(Box::new(CrossStreet {
|
||||
first: *ids.iter().next().unwrap(),
|
||||
autocomplete: Autocomplete::new(
|
||||
&format!("{} and what?", name),
|
||||
cross_streets
|
||||
.into_iter()
|
||||
.map(|r| (map.get_r(r).get_name(), r))
|
||||
.collect(),
|
||||
),
|
||||
}))
|
||||
}
|
||||
InputResult::StillActive => Transition::Keep,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
if let Some(roads) = self.composite.autocomplete_done("street") {
|
||||
return Transition::Replace(CrossStreet::new(ctx, app, roads));
|
||||
}
|
||||
|
||||
if self.composite.clicked_outside(ctx) {
|
||||
return Transition::Pop;
|
||||
}
|
||||
|
||||
Transition::Keep
|
||||
}
|
||||
|
||||
fn draw(&self, g: &mut GfxCtx, _: &App) {
|
||||
self.autocomplete.draw(g);
|
||||
State::grey_out_map(g);
|
||||
self.composite.draw(g);
|
||||
}
|
||||
}
|
||||
|
||||
struct CrossStreet {
|
||||
first: RoadID,
|
||||
autocomplete: Autocomplete<RoadID>,
|
||||
composite: Composite,
|
||||
}
|
||||
|
||||
impl CrossStreet {
|
||||
fn new(ctx: &mut EventCtx, app: &App, first: Vec<RoadID>) -> Box<dyn State> {
|
||||
let map = &app.primary.map;
|
||||
let mut cross_streets = HashSet::new();
|
||||
for r in &first {
|
||||
let road = map.get_r(*r);
|
||||
for i in &[road.src_i, road.dst_i] {
|
||||
for cross in &map.get_i(*i).roads {
|
||||
cross_streets.insert(*cross);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Roads share intersections, so of course there'll be overlap here.
|
||||
for r in &first {
|
||||
cross_streets.remove(r);
|
||||
}
|
||||
|
||||
Box::new(CrossStreet {
|
||||
first: first[0],
|
||||
composite: Composite::new(
|
||||
Widget::col(vec![
|
||||
Widget::row(vec![
|
||||
{
|
||||
let mut txt = Text::from(Line("What cross street?").small_heading());
|
||||
// TODO This isn't so clear...
|
||||
txt.add(Line(format!(
|
||||
"(Or just quit to go to {})",
|
||||
map.get_r(first[0]).get_name(),
|
||||
)));
|
||||
txt.draw(ctx)
|
||||
},
|
||||
Btn::text_fg("X")
|
||||
.build_def(ctx, hotkey(Key::Escape))
|
||||
.align_right(),
|
||||
]),
|
||||
Autocomplete::new(
|
||||
ctx,
|
||||
cross_streets
|
||||
.into_iter()
|
||||
.map(|r| (map.get_r(r).get_name(), r))
|
||||
.collect(),
|
||||
)
|
||||
.named("street"),
|
||||
])
|
||||
.bg(colors::PANEL_BG),
|
||||
)
|
||||
.build(ctx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl State for CrossStreet {
|
||||
// When None, this is done.
|
||||
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
|
||||
let map = &app.primary.map;
|
||||
match self.autocomplete.event(ctx) {
|
||||
InputResult::Canceled => {
|
||||
// Just warp to somewhere on the first road
|
||||
let road = map.get_r(self.first);
|
||||
println!("Warping to {}", road.get_name());
|
||||
Transition::Replace(Warping::new(
|
||||
ctx,
|
||||
road.center_pts.dist_along(road.center_pts.length() / 2.0).0,
|
||||
None,
|
||||
Some(ID::Lane(road.all_lanes()[0])),
|
||||
&mut app.primary,
|
||||
))
|
||||
}
|
||||
InputResult::Done(name, ids) => {
|
||||
println!(
|
||||
"Warping to {} and {}",
|
||||
map.get_r(self.first).get_name(),
|
||||
name
|
||||
);
|
||||
let road = map.get_r(*ids.iter().next().unwrap());
|
||||
let pt = if map.get_i(road.src_i).roads.contains(&self.first) {
|
||||
map.get_i(road.src_i).polygon.center()
|
||||
} else {
|
||||
map.get_i(road.dst_i).polygon.center()
|
||||
};
|
||||
Transition::Replace(Warping::new(
|
||||
ctx,
|
||||
pt,
|
||||
None,
|
||||
Some(ID::Lane(road.all_lanes()[0])),
|
||||
&mut app.primary,
|
||||
))
|
||||
}
|
||||
InputResult::StillActive => Transition::Keep,
|
||||
|
||||
match self.composite.event(ctx) {
|
||||
Some(Outcome::Clicked(x)) => match x.as_ref() {
|
||||
"X" => {
|
||||
// Just warp to somewhere on the first road
|
||||
let road = map.get_r(self.first);
|
||||
println!("Warping to {}", road.get_name());
|
||||
return Transition::Replace(Warping::new(
|
||||
ctx,
|
||||
road.center_pts.dist_along(road.center_pts.length() / 2.0).0,
|
||||
None,
|
||||
Some(ID::Lane(road.all_lanes()[0])),
|
||||
&mut app.primary,
|
||||
));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
if let Some(roads) = self.composite.autocomplete_done("street") {
|
||||
let road = map.get_r(roads[0]);
|
||||
println!(
|
||||
"Warping to {} and {}",
|
||||
map.get_r(self.first).get_name(),
|
||||
road.get_name()
|
||||
);
|
||||
let pt = if map.get_i(road.src_i).roads.contains(&self.first) {
|
||||
map.get_i(road.src_i).polygon.center()
|
||||
} else {
|
||||
map.get_i(road.dst_i).polygon.center()
|
||||
};
|
||||
return Transition::Replace(Warping::new(
|
||||
ctx,
|
||||
pt,
|
||||
None,
|
||||
Some(ID::Lane(road.all_lanes()[0])),
|
||||
&mut app.primary,
|
||||
));
|
||||
}
|
||||
|
||||
if self.composite.clicked_outside(ctx) {
|
||||
return Transition::Pop;
|
||||
}
|
||||
|
||||
Transition::Keep
|
||||
}
|
||||
|
||||
fn draw(&self, g: &mut GfxCtx, _: &App) {
|
||||
self.autocomplete.draw(g);
|
||||
State::grey_out_map(g);
|
||||
self.composite.draw(g);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user