diff --git a/editor/src/common/mod.rs b/editor/src/common/mod.rs index 7c8bbe4148..d1a3d6a37f 100644 --- a/editor/src/common/mod.rs +++ b/editor/src/common/mod.rs @@ -1,4 +1,5 @@ mod associated; +mod navigate; mod turn_cycler; mod warp; @@ -11,6 +12,7 @@ pub struct CommonState { associated: associated::ShowAssociatedState, turn_cycler: turn_cycler::TurnCyclerState, warp: Option, + navigate: Option, } impl CommonState { @@ -19,6 +21,7 @@ impl CommonState { associated: associated::ShowAssociatedState::Inactive, turn_cycler: turn_cycler::TurnCyclerState::new(), warp: None, + navigate: None, } } @@ -34,6 +37,16 @@ impl CommonState { if ctx.input.unimportant_key_pressed(Key::J, "warp") { self.warp = Some(warp::WarpState::new()); } + if let Some(ref mut navigate) = self.navigate { + if let Some(evmode) = navigate.event(ctx, ui) { + return Some(evmode); + } + self.navigate = None; + } + // TODO This definitely conflicts with some modes. + if ctx.input.unimportant_key_pressed(Key::K, "navigate") { + self.navigate = Some(navigate::Navigator::new(ui)); + } self.associated.event(ui); self.turn_cycler.event(ctx, ui); @@ -51,6 +64,9 @@ impl CommonState { if let Some(ref warp) = self.warp { warp.draw(g); } + if let Some(ref navigate) = self.navigate { + navigate.draw(g); + } self.turn_cycler.draw(g, ui); } diff --git a/editor/src/common/navigate.rs b/editor/src/common/navigate.rs new file mode 100644 index 0000000000..9139097766 --- /dev/null +++ b/editor/src/common/navigate.rs @@ -0,0 +1,45 @@ +use crate::ui::UI; +use ezgui::{Autocomplete, EventCtx, EventLoopMode, GfxCtx, InputResult}; +use map_model::RoadID; + +pub enum Navigator { + // TODO Ask for a cross-street after the first one + Searching(Autocomplete), +} + +impl Navigator { + pub fn new(ui: &UI) -> Navigator { + // TODO Canonicalize names, handling abbreviations like east/e and street/st + Navigator::Searching(Autocomplete::new( + "Warp to what?", + ui.primary + .map + .all_roads() + .iter() + .map(|r| (r.get_name(), r.id)) + .collect(), + )) + } + + // When None, this is done. + pub fn event(&mut self, ctx: &mut EventCtx, ui: &UI) -> Option { + match self { + Navigator::Searching(autocomplete) => match autocomplete.event(ctx.input) { + InputResult::Canceled => None, + InputResult::Done(name, ids) => { + println!("Search for '{}' yielded {:?}", name, ids); + None + } + InputResult::StillActive => Some(EventLoopMode::InputOnly), + }, + } + } + + pub fn draw(&self, g: &mut GfxCtx) { + match self { + Navigator::Searching(ref autocomplete) => { + autocomplete.draw(g); + } + } + } +} diff --git a/editor/src/helpers.rs b/editor/src/helpers.rs index 63cdaddd94..daa4f9c85c 100644 --- a/editor/src/helpers.rs +++ b/editor/src/helpers.rs @@ -101,13 +101,7 @@ impl ID { ID::Road(id) => { let r = map.get_r(id); txt.add_line(format!("{} (originally {}) is ", r.id, r.stable_id)); - txt.append( - r.osm_tags - .get("name") - .unwrap_or(&"???".to_string()) - .to_string(), - Some(Color::CYAN), - ); + txt.append(r.get_name(), Some(Color::CYAN)); txt.add_line(format!("From OSM way {}", r.osm_way_id)); } ID::Lane(id) => { @@ -117,13 +111,7 @@ impl ID { let i2 = map.get_destination_intersection(id); txt.add_line(format!("{} is ", l.id)); - txt.append( - r.osm_tags - .get("name") - .unwrap_or(&"???".to_string()) - .to_string(), - Some(Color::CYAN), - ); + txt.append(r.get_name(), Some(Color::CYAN)); txt.add_line(format!("From OSM way {}", r.osm_way_id)); txt.add_line(format!( "Parent {} (originally {}) points to {}", diff --git a/editor/src/sandbox/mod.rs b/editor/src/sandbox/mod.rs index f3468b10f5..2014534c7a 100644 --- a/editor/src/sandbox/mod.rs +++ b/editor/src/sandbox/mod.rs @@ -61,6 +61,9 @@ impl SandboxMode { &ShowEverything::new(), false, ); + if let Some(evmode) = mode.common.event(ctx, &state.ui) { + return evmode; + } if let State::Spawning(ref mut spawner) = mode.state { if spawner.event(ctx, &mut state.ui) { @@ -111,10 +114,6 @@ impl SandboxMode { ctx.input .set_mode_with_new_prompt("Sandbox Mode", txt, ctx.canvas); - if let Some(evmode) = mode.common.event(ctx, &state.ui) { - return evmode; - } - if let Some(spawner) = spawner::AgentSpawner::new(ctx, &mut state.ui) { mode.state = State::Spawning(spawner); return EventLoopMode::InputOnly; diff --git a/ezgui/Cargo.toml b/ezgui/Cargo.toml index a06085a458..289ef0810a 100644 --- a/ezgui/Cargo.toml +++ b/ezgui/Cargo.toml @@ -13,6 +13,7 @@ glutin = "0.20.0" palette = "0.4" serde = "1.0.89" serde_derive = "1.0.89" +simsearch = "0.1.4" textwrap = "0.11" [target.'cfg(target_os = "linux")'.dependencies] diff --git a/ezgui/src/lib.rs b/ezgui/src/lib.rs index 78abcbfb82..4eff85e096 100644 --- a/ezgui/src/lib.rs +++ b/ezgui/src/lib.rs @@ -19,7 +19,7 @@ pub use crate::runner::{run, EventLoopMode, GUI}; pub use crate::screen_geom::ScreenPt; pub use crate::text::Text; pub use crate::widgets::{ - Folder, LogScroller, ScrollingMenu, TextBox, TopMenu, Wizard, WrappedWizard, + Autocomplete, Folder, LogScroller, ScrollingMenu, TextBox, TopMenu, Wizard, WrappedWizard, }; pub enum InputResult { diff --git a/ezgui/src/widgets/autocomplete.rs b/ezgui/src/widgets/autocomplete.rs new file mode 100644 index 0000000000..ebad583f12 --- /dev/null +++ b/ezgui/src/widgets/autocomplete.rs @@ -0,0 +1,138 @@ +use crate::{text, Event, GfxCtx, InputResult, Key, Text, UserInput, CENTERED}; +use simsearch::SimSearch; +use std::collections::{BTreeMap, HashSet}; +use std::hash::Hash; + +const NUM_SEARCH_RESULTS: usize = 5; + +pub struct Autocomplete { + prompt: String, + choices: BTreeMap>, + // Maps index to choice + search_map: Vec, + search: SimSearch, + + line: String, + cursor_x: usize, + shift_pressed: bool, + current_results: Vec, + cursor_y: usize, +} + +impl Autocomplete { + pub fn new(prompt: &str, choices_list: Vec<(String, T)>) -> Autocomplete { + 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); + } + } + + Autocomplete { + prompt: prompt.to_string(), + choices, + search_map, + search, + + line: String::new(), + cursor_x: 0, + shift_pressed: false, + current_results, + cursor_y: 0, + } + } + + pub fn draw(&self, g: &mut GfxCtx) { + let mut txt = Text::new(); + txt.add_styled_line(self.prompt.clone(), None, Some(text::PROMPT_COLOR), None); + + txt.add_line(self.line[0..self.cursor_x].to_string()); + if self.cursor_x < self.line.len() { + // TODO This "cursor" looks awful! + txt.append("|".to_string(), Some(text::SELECTED_COLOR)); + txt.append(self.line[self.cursor_x..=self.cursor_x].to_string(), None); + txt.append(self.line[self.cursor_x + 1..].to_string(), None); + } else { + txt.append("|".to_string(), Some(text::SELECTED_COLOR)); + } + + for (idx, id) in self.current_results.iter().enumerate() { + if idx == self.cursor_y { + txt.add_styled_line( + self.search_map[*id].clone(), + None, + Some(text::SELECTED_COLOR), + None, + ); + } else { + txt.add_line(self.search_map[*id].clone()); + } + } + + g.draw_blocking_text(&txt, CENTERED); + } + + pub fn event(&mut self, input: &mut UserInput) -> InputResult> { + let maybe_ev = 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; + } + }; + InputResult::StillActive + } +} diff --git a/ezgui/src/widgets/mod.rs b/ezgui/src/widgets/mod.rs index 9ea3c22971..fe70cbc95b 100644 --- a/ezgui/src/widgets/mod.rs +++ b/ezgui/src/widgets/mod.rs @@ -1,3 +1,4 @@ +mod autocomplete; mod log_scroller; mod menu; mod screenshot; @@ -6,6 +7,7 @@ mod text_box; mod top_menu; mod wizard; +pub use self::autocomplete::Autocomplete; pub use self::log_scroller::LogScroller; pub use self::menu::{Menu, Position}; pub(crate) use self::screenshot::{screenshot_current, screenshot_everything}; diff --git a/ezgui/src/widgets/text_box.rs b/ezgui/src/widgets/text_box.rs index 5ed782507e..536cc8ada8 100644 --- a/ezgui/src/widgets/text_box.rs +++ b/ezgui/src/widgets/text_box.rs @@ -5,8 +5,7 @@ use crate::{text, Event, GfxCtx, InputResult, Key, Text, UserInput, CENTERED}; pub struct TextBox { prompt: String, // TODO A rope would be cool. - // TODO dont be pub - pub line: String, + line: String, cursor_x: usize, shift_pressed: bool, } diff --git a/map_model/src/road.rs b/map_model/src/road.rs index 57e98706c8..11c68fca65 100644 --- a/map_model/src/road.rs +++ b/map_model/src/road.rs @@ -331,4 +331,11 @@ impl Road { ) } } + + pub fn get_name(&self) -> String { + self.osm_tags + .get("name") + .unwrap_or(&"???".to_string()) + .to_string() + } }