heavily revamping wizard callers... wow, what a huge difference!

This commit is contained in:
Dustin Carlino 2019-08-07 15:03:45 -07:00
parent 3b1671fdcb
commit 8c7116af97
17 changed files with 185 additions and 326 deletions

View File

@ -121,9 +121,9 @@ pub fn deserialize_multimap<
}
// Just list all things from a directory, return sorted by name, with file extension removed.
// Pretty hacky that we return a (String, String). Also hacky that map_name can be blank. ;)
pub fn list_all_objects(dir: &str, map_name: &str) -> Vec<(String, String)> {
let mut results: BTreeSet<(String, String)> = BTreeSet::new();
// Hacky that map_name can be blank. ;)
pub fn list_all_objects(dir: &str, map_name: &str) -> Vec<String> {
let mut results: BTreeSet<String> = BTreeSet::new();
match std::fs::read_dir(format!("../data/{}/{}", dir, map_name)) {
Ok(iter) => {
for entry in iter {
@ -138,7 +138,7 @@ pub fn list_all_objects(dir: &str, map_name: &str) -> Vec<(String, String)> {
.to_os_string()
.into_string()
.unwrap();
results.insert((name.clone(), name));
results.insert(name);
}
}
Err(ref e) if e.kind() == ErrorKind::NotFound => {}

View File

@ -105,38 +105,32 @@ impl State for Scoreboard {
fn browse_trips(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option<Transition> {
let mut wizard = wiz.wrap(ctx);
let mode = wizard
.choose_something_no_keys::<TripMode>(
"Browse which trips?",
Box::new(|| {
vec![
("walk".to_string(), TripMode::Walk),
("bike".to_string(), TripMode::Bike),
("transit".to_string(), TripMode::Transit),
("drive".to_string(), TripMode::Drive),
]
}),
)?
.choose_something_no_keys("Browse which trips?", || {
vec![
("walk".to_string(), TripMode::Walk),
("bike".to_string(), TripMode::Bike),
("transit".to_string(), TripMode::Transit),
("drive".to_string(), TripMode::Drive),
]
})?
.1;
// TODO Ewwww. Can't do this inside choices_generator because trips isn't &'a static.
let trips = CompareTrips::new(
ui.primary.sim.get_finished_trips(),
ui.secondary.as_ref().unwrap().sim.get_finished_trips(),
);
let mut filtered: Vec<&(TripID, TripMode, Duration, Duration)> = trips
.finished_trips
.iter()
.filter(|(_, m, t1, t2)| *m == mode && *t1 != *t2)
.collect();
filtered.sort_by_key(|(_, _, t1, t2)| *t1 - *t2);
filtered.reverse();
let choices: Vec<(String, TripID)> = filtered
.into_iter()
.map(|(id, _, t1, t2)| (format!("{} taking {} vs {}", id, t1, t2), *id))
.collect();
wizard.choose_something_no_keys::<TripID>(
"Examine which trip?",
Box::new(move || choices.clone()),
)?;
wizard.choose_something_no_keys("Examine which trip?", || {
let trips = CompareTrips::new(
ui.primary.sim.get_finished_trips(),
ui.secondary.as_ref().unwrap().sim.get_finished_trips(),
);
let mut filtered: Vec<&(TripID, TripMode, Duration, Duration)> = trips
.finished_trips
.iter()
.filter(|(_, m, t1, t2)| *m == mode && *t1 != *t2)
.collect();
filtered.sort_by_key(|(_, _, t1, t2)| *t1 - *t2);
filtered.reverse();
filtered
.into_iter()
.map(|(id, _, t1, t2)| (format!("{} taking {} vs {}", id, t1, t2), *id))
.collect()
})?;
// TODO show more details...
Some(Transition::Pop)
}

View File

@ -20,30 +20,29 @@ fn pick_ab_test(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option<Tra
let mut wizard = wiz.wrap(ctx);
let load_existing = "Load existing A/B test";
let create_new = "Create new A/B test";
let map_name = ui.primary.map.get_name().to_string();
let ab_test = if wizard
.choose_string("What A/B test to manage?", vec![load_existing, create_new])?
== load_existing
{
wizard
.choose_something_no_keys::<ABTest>(
"Load which A/B test?",
Box::new(move || abstutil::load_all_objects(abstutil::AB_TESTS, &map_name)),
)?
.choose_something_no_keys("Load which A/B test?", || {
abstutil::load_all_objects(abstutil::AB_TESTS, ui.primary.map.get_name())
})?
.1
} else {
let test_name = wizard.input_string("Name the A/B test")?;
let map_name = ui.primary.map.get_name();
let t = ABTest {
test_name,
map_name: map_name.clone(),
scenario_name: choose_scenario(map_name.clone(), &mut wizard, "What scenario to run?")?,
map_name: map_name.to_string(),
scenario_name: choose_scenario(map_name, &mut wizard, "What scenario to run?")?,
edits1_name: choose_edits(
map_name.clone(),
map_name,
&mut wizard,
"For the 1st run, what map edits to use?",
)?,
edits2_name: choose_edits(
map_name.clone(),
map_name,
&mut wizard,
"For the 2nd run, what map edits to use?",
)?,
@ -232,39 +231,23 @@ fn launch_savestate(test: &ABTest, ss_path: String, ui: &mut UI, ctx: &mut Event
)
}
fn choose_scenario(map_name: String, wizard: &mut WrappedWizard, query: &str) -> Option<String> {
wizard
.choose_something_no_keys::<String>(
query,
Box::new(move || abstutil::list_all_objects(abstutil::SCENARIOS, &map_name)),
)
.map(|(n, _)| n)
fn choose_scenario(map_name: &str, wizard: &mut WrappedWizard, query: &str) -> Option<String> {
wizard.choose_actual_string(query, || {
abstutil::list_all_objects(abstutil::SCENARIOS, map_name)
})
}
fn choose_edits(map_name: String, wizard: &mut WrappedWizard, query: &str) -> Option<String> {
wizard
.choose_something_no_keys::<String>(
query,
Box::new(move || {
let mut list = abstutil::list_all_objects("edits", &map_name);
list.push(("no_edits".to_string(), "no_edits".to_string()));
list
}),
)
.map(|(n, _)| n)
fn choose_edits(map_name: &str, wizard: &mut WrappedWizard, query: &str) -> Option<String> {
wizard.choose_actual_string(query, || {
let mut list = abstutil::list_all_objects("edits", map_name);
list.push("no_edits".to_string());
list
})
}
fn pick_savestate(test: &ABTest, wizard: &mut WrappedWizard) -> Option<String> {
let path = abstutil::path1(&test.map_name, abstutil::AB_TEST_SAVES, &test.test_name);
wizard
.choose_something_no_keys::<()>(
"Load which savestate?",
Box::new(move || {
abstutil::list_dir(std::path::Path::new(&path))
.into_iter()
.map(|f| (f, ()))
.collect()
}),
)
.map(|(f, _)| f)
wizard.choose_actual_string("Load which savestate?", || {
abstutil::list_dir(std::path::Path::new(&path))
})
}

View File

@ -2,9 +2,7 @@ use crate::common::warp::Warping;
use crate::game::{State, Transition};
use crate::ui::UI;
use abstutil::Cloneable;
use ezgui::{
hotkey, EventCtx, EventLoopMode, GfxCtx, Key, MultiKey, Warper, Wizard, WrappedWizard,
};
use ezgui::{hotkey, EventCtx, EventLoopMode, GfxCtx, Key, Warper, Wizard, WrappedWizard};
use geom::Pt2D;
use serde_derive::{Deserialize, Serialize};
@ -62,34 +60,33 @@ fn choose_shortcut(
shortcuts: Vec<Shortcut>,
ui: &UI,
) -> Option<Shortcut> {
// TODO Handle >9
// TODO Allow deleting
let keys = vec![
Key::Num1,
Key::Num2,
Key::Num3,
Key::Num4,
Key::Num5,
Key::Num6,
Key::Num7,
Key::Num8,
Key::Num9,
];
let (_, mut s) = wizard.new_choose_something("Jump to which shortcut?", || {
// TODO Handle >9
// TODO Allow deleting
let keys = vec![
Key::Num1,
Key::Num2,
Key::Num3,
Key::Num4,
Key::Num5,
Key::Num6,
Key::Num7,
Key::Num8,
Key::Num9,
];
let choices: Vec<(Option<MultiKey>, String, Shortcut)> = shortcuts
.into_iter()
.enumerate()
.map(|(idx, s)| {
if idx == 0 {
(None, s.name.clone(), s)
} else {
(hotkey(keys[idx - 1]), s.name.clone(), s)
}
})
.collect();
let (_, mut s) =
wizard.choose_something("Jump to which shortcut?", Box::new(move || choices.clone()))?;
shortcuts
.into_iter()
.enumerate()
.map(|(idx, s)| {
if idx == 0 {
(None, s.name.clone(), s)
} else {
(hotkey(keys[idx - 1]), s.name.clone(), s)
}
})
.collect()
})?;
if s.name == "Create a new shortcut here" {
// TODO Enforce non-empty, unique names
let name = wizard.input_string("Name this shortcut")?;

View File

@ -108,20 +108,16 @@ impl BusRoutePicker {
impl State for BusRoutePicker {
fn event(&mut self, ctx: &mut EventCtx, ui: &mut UI) -> Transition {
// TODO Argh, constantly doing this.
let choices: Vec<(String, BusRouteID)> = self
.choices
.iter()
.map(|id| (ui.primary.map.get_br(*id).name.clone(), *id))
.collect();
if let Some((_, id)) = self
.wizard
.wrap(ctx)
.choose_something_no_keys::<BusRouteID>(
"Explore which bus route?",
Box::new(move || choices.clone()),
)
let choices = self.choices.clone();
if let Some((_, id)) =
self.wizard
.wrap(ctx)
.choose_something_no_keys("Explore which bus route?", || {
choices
.into_iter()
.map(|id| (ui.primary.map.get_br(id).name.clone(), id))
.collect()
})
{
return Transition::Replace(Box::new(BusRouteExplorer::for_route(
ui.primary.map.get_br(id),

View File

@ -16,10 +16,9 @@ impl ColorChooser {
}
fn pick_color(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option<Transition> {
let choices = ui.cs.color_names();
let (name, _) = wiz
let name = wiz
.wrap(ctx)
.choose_something_no_keys::<()>("Change which color?", Box::new(move || choices.clone()))?;
.choose_actual_string("Change which color?", || ui.cs.color_names())?;
Some(Transition::Replace(Box::new(ColorChanger {
name: name.clone(),
original: ui.cs.get_modified(&name),

View File

@ -322,14 +322,11 @@ fn load_edits(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option<Trans
// TODO Exclude current
let map_name = map.get_name().to_string();
let (_, new_edits) = wizard.choose_something_no_keys::<MapEdits>(
"Load which map edits?",
Box::new(move || {
let mut list = abstutil::load_all_objects("edits", &map_name);
list.push(("no_edits".to_string(), MapEdits::new(map_name.clone())));
list
}),
)?;
let (_, new_edits) = wizard.choose_something_no_keys("Load which map edits?", || {
let mut list = abstutil::load_all_objects("edits", &map_name);
list.push(("no_edits".to_string(), MapEdits::new(map_name.clone())));
list
})?;
apply_map_edits(&mut ui.primary, &ui.cs, ctx, new_edits);
Some(Transition::Pop)
}
@ -502,35 +499,25 @@ impl State for BulkEditLanes {
}
fn bulk_edit(r: RoadID, wizard: &mut WrappedWizard, map: &Map) -> Option<MapEdits> {
let from = wizard
.choose_something(
"Change all lanes of type...",
Box::new(|| {
vec![
(None, "driving".to_string(), LaneType::Driving),
(None, "parking".to_string(), LaneType::Parking),
(None, "biking".to_string(), LaneType::Biking),
(None, "bus".to_string(), LaneType::Bus),
]
}),
)?
.1;
let to = wizard
.choose_something(
"Change to all lanes of type...",
Box::new(move || {
vec![
(None, "driving".to_string(), LaneType::Driving),
(None, "parking".to_string(), LaneType::Parking),
(None, "biking".to_string(), LaneType::Biking),
(None, "bus".to_string(), LaneType::Bus),
]
.into_iter()
.filter(|(_, _, lt)| *lt != from)
.collect()
}),
)?
.1;
let (_, from) = wizard.choose_something_no_keys("Change all lanes of type...", || {
vec![
("driving".to_string(), LaneType::Driving),
("parking".to_string(), LaneType::Parking),
("biking".to_string(), LaneType::Biking),
("bus".to_string(), LaneType::Bus),
]
})?;
let (_, to) = wizard.choose_something_no_keys("Change to all lanes of type...", || {
vec![
("driving".to_string(), LaneType::Driving),
("parking".to_string(), LaneType::Parking),
("biking".to_string(), LaneType::Biking),
("bus".to_string(), LaneType::Bus),
]
.into_iter()
.filter(|(_, lt)| *lt != from)
.collect()
})?;
// Do the dirty deed. Match by road name; OSM way ID changes a fair bit.
let road_name = map.get_r(r).get_name();

View File

@ -5,9 +5,7 @@ use crate::helpers::ID;
use crate::render::{draw_signal_cycle, DrawCtx, DrawOptions, DrawTurn, TrafficSignalDiagram};
use crate::ui::{ShowEverything, UI};
use abstutil::Timer;
use ezgui::{
hotkey, Color, EventCtx, GeomBatch, GfxCtx, Key, ModalMenu, MultiKey, Wizard, WrappedWizard,
};
use ezgui::{hotkey, Color, EventCtx, GeomBatch, GfxCtx, Key, ModalMenu, Wizard, WrappedWizard};
use geom::Duration;
use map_model::{ControlTrafficSignal, Cycle, IntersectionID, Map, TurnID, TurnPriority, TurnType};
@ -268,19 +266,10 @@ fn choose_preset(
id: IntersectionID,
mut wizard: WrappedWizard,
) -> Option<ControlTrafficSignal> {
// TODO I wanted to do all of this work just once per wizard, but we can't touch map inside a
// closure. Grr.
let choices: Vec<(Option<MultiKey>, String, ControlTrafficSignal)> =
ControlTrafficSignal::get_possible_policies(map, id)
.into_iter()
.map(|(name, ts)| (None, name, ts))
.collect();
wizard
.choose_something::<ControlTrafficSignal>(
"Use which preset for this intersection?",
Box::new(move || choices.clone()),
)
.choose_something_no_keys("Use which preset for this intersection?", || {
ControlTrafficSignal::get_possible_policies(map, id)
})
.map(|(_, ts)| ts)
}

View File

@ -105,9 +105,8 @@ impl ColorScheme {
self.map[name]
}
// Just for the color picker plugin, that's why the funky return value
pub fn color_names(&self) -> Vec<(String, ())> {
let mut names: Vec<(String, ())> = self.map.keys().map(|n| (n.clone(), ())).collect();
pub fn color_names(&self) -> Vec<String> {
let mut names: Vec<String> = self.map.keys().map(|n| n.to_string()).collect();
names.sort();
names
}

View File

@ -113,12 +113,13 @@ fn convert_trips_to_scenario(
fn load_scenario(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option<Transition> {
let map_name = ui.primary.map.get_name().to_string();
let (_, s) = wiz.wrap(ctx).choose_something_no_keys::<String>(
"Load which scenario?",
Box::new(move || abstutil::list_all_objects(abstutil::SCENARIOS, &map_name)),
)?;
let s = wiz
.wrap(ctx)
.choose_actual_string("Load which scenario?", || {
abstutil::list_all_objects(abstutil::SCENARIOS, &map_name)
})?;
let scenario = abstutil::read_binary(
&abstutil::path1_bin(ui.primary.map.get_name(), abstutil::SCENARIOS, &s),
&abstutil::path1_bin(&map_name, abstutil::SCENARIOS, &s),
&mut Timer::throwaway(),
)
.unwrap();

View File

@ -191,11 +191,9 @@ fn load_neighborhood_builder(
wizard: &mut WrappedWizard,
query: &str,
) -> Option<NeighborhoodBuilder> {
let map_name = map.get_name().to_string();
wizard
.choose_something_no_keys::<NeighborhoodBuilder>(
query,
Box::new(move || abstutil::load_all_objects(abstutil::NEIGHBORHOODS, &map_name)),
)
.choose_something_no_keys(query, || {
abstutil::load_all_objects(abstutil::NEIGHBORHOODS, &map.get_name())
})
.map(|(_, n)| n)
}

View File

@ -186,14 +186,11 @@ fn edit_scenario(map: &Map, scenario: &mut Scenario, mut wizard: WrappedWizard)
}
fn choose_neighborhood(map: &Map, wizard: &mut WrappedWizard, query: &str) -> Option<String> {
let map_name = map.get_name().to_string();
let gps_bounds = map.get_gps_bounds().clone();
// Load the full object, since we usually visualize the neighborhood when menuing over it
wizard
.choose_something_no_keys::<Neighborhood>(
query,
Box::new(move || Neighborhood::load_all(&map_name, &gps_bounds)),
)
.choose_something_no_keys(query, || {
Neighborhood::load_all(map.get_name(), map.get_gps_bounds())
})
.map(|(n, _)| n)
}

View File

@ -211,15 +211,11 @@ impl State for SandboxMode {
fn load_savestate(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option<Transition> {
let path = ui.primary.sim.save_dir();
let (ss, _) = wiz.wrap(ctx).choose_something_no_keys::<()>(
"Load which savestate?",
Box::new(move || {
let ss = wiz
.wrap(ctx)
.choose_actual_string("Load which savestate?", || {
abstutil::list_dir(std::path::Path::new(&path))
.into_iter()
.map(|f| (f, ()))
.collect()
}),
)?;
})?;
ctx.loading_screen("load savestate", |ctx, mut timer| {
ui.primary.sim = Sim::load_savestate(ss, &mut timer).expect("Can't load savestate");

View File

@ -68,17 +68,14 @@ impl State for Scoreboard {
fn browse_trips(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option<Transition> {
let mut wizard = wiz.wrap(ctx);
let (_, mode) = wizard.choose_something_no_keys::<TripMode>(
"Browse which trips?",
Box::new(|| {
vec![
("walk".to_string(), TripMode::Walk),
("bike".to_string(), TripMode::Bike),
("transit".to_string(), TripMode::Transit),
("drive".to_string(), TripMode::Drive),
]
}),
)?;
let (_, mode) = wizard.choose_something_no_keys("Browse which trips?", || {
vec![
("walk".to_string(), TripMode::Walk),
("bike".to_string(), TripMode::Bike),
("transit".to_string(), TripMode::Transit),
("drive".to_string(), TripMode::Drive),
]
})?;
wizard.new_choose_something("Examine which trip?", || {
let trips = ui.primary.sim.get_finished_trips();
let mut filtered: Vec<&(TripID, TripMode, Duration)> = trips

View File

@ -363,21 +363,19 @@ fn instantiate_scenario(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Op
"random scenario with some agents".to_string()
};
let map = &ui.primary.map;
let map_name = map.get_name().to_string();
let (_, scenario_name) = wiz.wrap(ctx).choose_something_no_keys::<String>(
"Instantiate which scenario?",
Box::new(move || {
let mut list = vec![
(builtin.clone(), "builtin".to_string()),
("just buses".to_string(), "just buses".to_string()),
];
list.extend(abstutil::list_all_objects(abstutil::SCENARIOS, &map_name));
list
}),
)?;
let scenario_name =
wiz.wrap(ctx)
.choose_actual_string("Instantiate which scenario?", || {
let mut list = vec![builtin.clone(), "just buses".to_string()];
list.extend(abstutil::list_all_objects(
abstutil::SCENARIOS,
map.get_name(),
));
list
})?;
let scenario = if scenario_name == "builtin" {
let scenario = if scenario_name == builtin {
if let Some(n) = num_agents {
Scenario::scaled_run(map, n)
} else {

View File

@ -159,16 +159,13 @@ fn splash_screen(
{
x if x == sandbox => Some(Transition::Push(Box::new(SandboxMode::new(ctx)))),
x if x == load_map => {
let current_map = ui.primary.map.get_name().to_string();
if let Some((name, _)) = wizard.choose_something_no_keys::<String>(
"Load which map?",
Box::new(move || {
abstutil::list_all_objects("maps", "")
.into_iter()
.filter(|(n, _)| n != &current_map)
.collect()
}),
) {
if let Some(name) = wizard.choose_actual_string("Load which map?", || {
let current_map = ui.primary.map.get_name();
abstutil::list_all_objects("maps", "")
.into_iter()
.filter(|n| n != current_map)
.collect()
}) {
ui.save_editor_state(ctx.canvas);
// This retains no state, but that's probably fine.
let mut flags = ui.primary.current_flags.clone();

View File

@ -324,107 +324,24 @@ impl<'a, 'b> WrappedWizard<'a, 'b> {
}
}
pub fn choose_something<R: 'static + Clone + Cloneable>(
// TODO rename choose_something and choose_something_hotkeys
pub fn choose_something_no_keys<
R: 'static + Clone + Cloneable,
F: FnOnce() -> Vec<(String, R)>,
>(
&mut self,
query: &str,
choices_generator: Box<Fn() -> Vec<(Option<MultiKey>, String, R)>>,
choices_generator: F,
) -> Option<(String, R)> {
if !self.ready_results.is_empty() {
let first = self.ready_results.pop_front().unwrap();
// We have to downcast twice! \o/
let pair: &(String, Box<Cloneable>) = first
.as_any()
.downcast_ref::<(String, Box<Cloneable>)>()
.unwrap();
let item: &R = pair.1.as_any().downcast_ref::<R>().unwrap();
return Some((pair.0.to_string(), item.clone()));
}
// If the menu was empty, wait for the user to acknowledge the text-box before aborting the
// wizard.
if self.wizard.log_scroller.is_some() {
if self
.wizard
.log_scroller
.as_mut()
.unwrap()
.event(self.ctx.input)
{
self.wizard.log_scroller = None;
self.wizard.alive = false;
}
return None;
}
if self.wizard.menu.is_none() {
let choices: Vec<(Option<MultiKey>, String, R)> = choices_generator();
if choices.is_empty() {
self.wizard.log_scroller = Some(LogScroller::new(
"Wizard".to_string(),
vec![format!("No choices for \"{}\", canceling wizard", query)],
));
return None;
}
let boxed_choices: Vec<(Option<MultiKey>, String, Box<Cloneable>)> = choices
.into_iter()
.map(|(multikey, s, item)| (multikey, s, item.clone_box()))
.collect();
self.wizard.menu = Some(Menu::new(
Text::prompt(query),
vec![boxed_choices],
true,
false,
Position::ScreenCenter,
self.ctx.canvas,
));
}
assert!(self.wizard.alive);
// Otherwise, we try to use one event for two inputs potentially
if self.ctx.input.has_been_consumed() {
return None;
}
let ev = self.ctx.input.use_event_directly().unwrap();
match self
.wizard
.menu
.as_mut()
.unwrap()
.event(ev, self.ctx.canvas)
{
InputResult::Canceled => {
self.wizard.menu = None;
self.wizard.alive = false;
None
}
InputResult::StillActive => None,
InputResult::Done(choice, item) => {
self.wizard.menu = None;
self.wizard
.confirmed_state
.push(Box::new((choice.to_string(), item.clone())));
let downcasted_item: &R = item.as_any().downcast_ref::<R>().unwrap();
Some((choice, downcasted_item.clone()))
}
}
}
pub fn choose_something_no_keys<R: 'static + Clone + Cloneable>(
&mut self,
query: &str,
choices_generator: Box<Fn() -> Vec<(String, R)>>,
) -> Option<(String, R)> {
let wrapped_generator = Box::new(move || {
self.new_choose_something(query, || {
choices_generator()
.into_iter()
.map(|(name, data)| (None, name, data))
.map(|(s, data)| (None, s, data))
.collect()
});
self.choose_something(query, wrapped_generator)
})
}
// TODO rename choose_str
pub fn choose_string(&mut self, query: &str, choices: Vec<&str>) -> Option<String> {
self.new_choose_something(query, || {
choices
@ -435,6 +352,20 @@ impl<'a, 'b> WrappedWizard<'a, 'b> {
.map(|(s, _)| s)
}
pub fn choose_actual_string<F: Fn() -> Vec<String>>(
&mut self,
query: &str,
choices_generator: F,
) -> Option<String> {
self.new_choose_something(query, || {
choices_generator()
.into_iter()
.map(|s| (None, s, ()))
.collect()
})
.map(|(s, _)| s)
}
pub fn choose_string_hotkeys(
&mut self,
query: &str,