adding categories to unimportant actions, arranging them in a tree

This commit is contained in:
Dustin Carlino 2018-09-21 10:47:34 -07:00
parent 0a50fbd8bd
commit f6ddd8aeaa
17 changed files with 239 additions and 42 deletions

View File

@ -198,3 +198,62 @@ as the calls to the wizard are deterministic.
Menus are super awkward -- drawing extra effects, mainly.
cursive crate is good inspiration for the API
## Menus
Dynamically populating the TreeMenu every frame while possible input keys are collected has problems.
- How do we remember the permanent state between frames?
- What if the possible actions change between frames, screwing up that state anyway?
- stop handing events to the game entirely?
Rethink assumptions. Is it nice to dynamically populate the menu in a bunch of different places?
- Won't have any control whatsoever for order of entries, and I'll definitely want that.
- Hard to understand all the things that could happen; it'd be nice to see them in one place
- Lots of plugins have boilerplate code for state management. Even if they keep
it, might be cool to at least peel out the code to activate the plugin.
- all plugins become Optional; dont have to represent the nil state
- or more extreme: enum of active plugin
- Similarly, might be nice to have kind of a static list of context-sensitive, right click menu actions for each type of object?
current TreeMenu:
- Debug ()
- Show extra ()
- hide Intersection(IntersectionID(59)) (H)
- start searching (/)
- to show OSM classifications (6)
- unhide everything (K)
- visualize steepness (5)
- Show layers ()
- toggle buildings (1)
- toggle debug mode (G)
- toggle extra KML shapes (7)
- toggle intersections (2)
- toggle lanes (3)
- toggle parcels (4)
- toggle turn icons (9)
- Validate map geometry (I)
- start searching for something to warp to (J)
- Edit map ()
- start drawing a polygon (N)
- Settings ()
- configure colors (8)
- Sim ()
- Seed the map with agents (S)
- Setup ()
- spawn some agents for a scenario (W)
- load sim state (P)
- run one step (M)
- run sim (Space)
- save sim state (O)
- slow down sim ([)
- speed up sim (])
- quit (Escape)
Back up and think about ideal for these background controls...

View File

@ -70,3 +70,14 @@ pub struct Ctx<'a> {
pub canvas: &'a Canvas,
pub sim: &'a Sim,
}
// TODO not the right module for this, totally temp
pub const ROOT_MENU: &str = "";
pub const DEBUG: &str = "Debug";
pub const DEBUG_EXTRA: &str = "Debug/Show extra";
pub const DEBUG_LAYERS: &str = "Debug/Show layers";
pub const EDIT_MAP: &str = "Edit map";
pub const SETTINGS: &str = "Settings";
pub const SIM: &str = "Sim";
pub const SIM_SETUP: &str = "Sim/Setup";

View File

@ -3,7 +3,7 @@
use colors::Colors;
use ezgui::UserInput;
use graphics::types::Color;
use objects::{Ctx, ID};
use objects::{Ctx, DEBUG_EXTRA, ID};
use piston::input::Key;
use plugins::Colorizer;
@ -23,7 +23,7 @@ impl OsmClassifier {
} else {
"to show OSM classifications"
};
if input.unimportant_key_pressed(Key::D6, msg) {
if input.unimportant_key_pressed(Key::D6, DEBUG_EXTRA, msg) {
self.active = !self.active;
}
self.active

View File

@ -3,6 +3,7 @@
use colors::{ColorScheme, Colors};
use ezgui::{Canvas, GfxCtx, InputResult, Menu, UserInput};
use graphics;
use objects::SETTINGS;
use piston::input::{Key, MouseCursorEvent};
use plugins::Colorizer;
use std::str::FromStr;
@ -31,7 +32,7 @@ impl ColorPicker {
let mut new_state: Option<ColorPicker> = None;
match self {
ColorPicker::Inactive => {
if input.unimportant_key_pressed(Key::D8, "configure colors") {
if input.unimportant_key_pressed(Key::D8, SETTINGS, "configure colors") {
new_state = Some(ColorPicker::Choosing(Menu::new(
"Pick a color to change",
Colors::iter().map(|c| c.to_string()).collect(),

View File

@ -2,6 +2,7 @@ use abstutil;
use ezgui::{Canvas, GfxCtx, InputResult, Menu, TextBox, TextOSD, UserInput};
use geom::{Circle, Line, Polygon, Pt2D};
use map_model::Map;
use objects::EDIT_MAP;
use piston::input::{Button, Key, ReleaseEvent};
use plugins::Colorizer;
use polygons;
@ -35,7 +36,7 @@ impl DrawPolygonState {
let mut new_state: Option<DrawPolygonState> = None;
match self {
DrawPolygonState::Empty => {
if input.unimportant_key_pressed(Key::N, "start drawing a polygon") {
if input.unimportant_key_pressed(Key::N, EDIT_MAP, "start drawing a polygon") {
new_state = Some(DrawPolygonState::DrawingPoints(
Vec::new(),
None,

View File

@ -4,6 +4,7 @@ use geo;
use geo::prelude::Intersects;
use geom::{Polygon, Pt2D};
use map_model::{BuildingID, IntersectionID, LaneID, Map, ParcelID};
use objects::DEBUG;
use piston::input::Key;
use plugins::Colorizer;
use render::DrawMap;
@ -99,7 +100,7 @@ impl Validator {
let mut new_state: Option<Validator> = None;
match self {
Validator::Inactive => {
if input.unimportant_key_pressed(Key::I, "Validate map geometry") {
if input.unimportant_key_pressed(Key::I, DEBUG, "Validate map geometry") {
new_state = Some(Validator::start(draw_map));
}
}

View File

@ -1,7 +1,7 @@
// Copyright 2018 Google LLC, licensed under http://www.apache.org/licenses/LICENSE-2.0
use ezgui::UserInput;
use objects::ID;
use objects::{DEBUG_EXTRA, ID};
use piston::input::Key;
use plugins::Colorizer;
use std::collections::HashSet;
@ -18,7 +18,7 @@ impl Hider {
}
pub fn event(&mut self, input: &mut UserInput, selected: &mut Option<ID>) -> bool {
if input.unimportant_key_pressed(Key::K, "unhide everything") {
if input.unimportant_key_pressed(Key::K, DEBUG_EXTRA, "unhide everything") {
println!("Unhiding {} things", self.items.len());
self.items.clear();
return true;
@ -37,7 +37,7 @@ impl Hider {
return false;
}
};
if input.unimportant_key_pressed(Key::H, &format!("hide {:?}", item)) {
if input.unimportant_key_pressed(Key::H, DEBUG_EXTRA, &format!("hide {:?}", item)) {
self.items.insert(item);
println!("Hiding {:?}", item);
*selected = None;

View File

@ -1,7 +1,7 @@
use control::ControlMap;
use ezgui::UserInput;
use map_model::{EditReason, Edits, LaneID, LaneType, Map};
use objects::ID;
use objects::{EDIT_MAP, ID};
use piston::input::Key;
use plugins::Colorizer;
use render::DrawMap;
@ -33,7 +33,7 @@ impl RoadEditor {
match self {
RoadEditor::Inactive(edits) => match selected {
None => {
if input.unimportant_key_pressed(Key::E, "Start editing roads") {
if input.unimportant_key_pressed(Key::E, EDIT_MAP, "Start editing roads") {
// TODO cloning edits sucks! want to consume self
new_state = Some(RoadEditor::Active(edits.clone()));
}

View File

@ -3,7 +3,7 @@
use colors::{ColorScheme, Colors};
use ezgui::{Canvas, GfxCtx, InputResult, TextBox, UserInput};
use graphics::types::Color;
use objects::{Ctx, ID};
use objects::{Ctx, DEBUG_EXTRA, ID};
use piston::input::Key;
use plugins::Colorizer;
use std::collections::BTreeMap;
@ -30,7 +30,7 @@ impl SearchState {
let mut new_state: Option<SearchState> = None;
match self {
SearchState::Empty => {
if input.unimportant_key_pressed(Key::Slash, "start searching") {
if input.unimportant_key_pressed(Key::Slash, DEBUG_EXTRA, "start searching") {
new_state = Some(SearchState::EnteringSearch(TextBox::new(
"Search for what?",
)));

View File

@ -3,7 +3,7 @@
use control::ControlMap;
use ezgui::{EventLoopMode, TextOSD, UserInput};
use map_model::Map;
use objects::ID;
use objects::{ID, SIM};
use piston::input::{Key, UpdateEvent};
use sim::{Benchmark, Sim, TIMESTEP};
use std::time::{Duration, Instant};
@ -37,20 +37,20 @@ impl SimController {
selected: Option<ID>,
osd: &mut TextOSD,
) -> EventLoopMode {
if input.unimportant_key_pressed(Key::S, "Seed the map with agents") {
if input.unimportant_key_pressed(Key::S, SIM, "Seed the map with agents") {
sim.small_spawn(map);
}
if input.unimportant_key_pressed(Key::LeftBracket, "slow down sim") {
if input.unimportant_key_pressed(Key::LeftBracket, SIM, "slow down sim") {
self.desired_speed -= ADJUST_SPEED;
self.desired_speed = self.desired_speed.max(0.0);
}
if input.unimportant_key_pressed(Key::RightBracket, "speed up sim") {
if input.unimportant_key_pressed(Key::RightBracket, SIM, "speed up sim") {
self.desired_speed += ADJUST_SPEED;
}
if input.unimportant_key_pressed(Key::O, "save sim state") {
if input.unimportant_key_pressed(Key::O, SIM, "save sim state") {
sim.save();
}
if input.unimportant_key_pressed(Key::P, "load sim state") {
if input.unimportant_key_pressed(Key::P, SIM, "load sim state") {
match sim.load_most_recent() {
Ok(new_sim) => {
*sim = new_sim;
@ -60,16 +60,16 @@ impl SimController {
};
}
if self.last_step.is_some() {
if input.unimportant_key_pressed(Key::Space, "pause sim") {
if input.unimportant_key_pressed(Key::Space, SIM, "pause sim") {
self.last_step = None;
self.benchmark = None;
self.sim_speed = String::from("paused");
}
} else {
if input.unimportant_key_pressed(Key::Space, "run sim") {
if input.unimportant_key_pressed(Key::Space, SIM, "run sim") {
self.last_step = Some(Instant::now());
self.benchmark = Some(sim.start_benchmark());
} else if input.unimportant_key_pressed(Key::M, "run one step") {
} else if input.unimportant_key_pressed(Key::M, SIM, "run one step") {
sim.step(map, control_map);
}
}

View File

@ -5,7 +5,7 @@
use ezgui::UserInput;
use graphics::types::Color;
use map_model::{Lane, Map};
use objects::{Ctx, ID};
use objects::{Ctx, DEBUG_EXTRA, ID};
use piston::input::Key;
use plugins::Colorizer;
use std::f64;
@ -41,7 +41,7 @@ impl SteepnessVisualizer {
} else {
"visualize steepness"
};
if input.unimportant_key_pressed(Key::D5, msg) {
if input.unimportant_key_pressed(Key::D5, DEBUG_EXTRA, msg) {
self.active = !self.active;
}
self.active

View File

@ -1,7 +1,7 @@
use ezgui::{Canvas, GfxCtx, InputResult, TextBox, UserInput};
use geom::Pt2D;
use map_model::{AreaID, BuildingID, IntersectionID, LaneID, Map, ParcelID, RoadID};
use objects::ID;
use objects::{DEBUG, ID};
use piston::input::Key;
use plugins::Colorizer;
use sim::{CarID, PedestrianID, Sim};
@ -24,8 +24,11 @@ impl WarpState {
let mut new_state: Option<WarpState> = None;
match self {
WarpState::Empty => {
if input.unimportant_key_pressed(Key::J, "start searching for something to warp to")
{
if input.unimportant_key_pressed(
Key::J,
DEBUG,
"start searching for something to warp to",
) {
new_state = Some(WarpState::EnteringSearch(TextBox::new("Warp to what?")));
}
}

View File

@ -1,6 +1,7 @@
use ezgui::{Canvas, GfxCtx, InputResult, Menu, TextBox, UserInput};
use geom::Polygon;
use map_model::Map;
use objects::SIM_SETUP;
use piston::input::Key;
use plugins::Colorizer;
use polygons;
@ -36,7 +37,11 @@ impl WizardSample {
let mut new_state: Option<WizardSample> = None;
match self {
WizardSample::Inactive => {
if input.unimportant_key_pressed(Key::W, "spawn some agents for a scenario") {
if input.unimportant_key_pressed(
Key::W,
SIM_SETUP,
"spawn some agents for a scenario",
) {
new_state = Some(WizardSample::Active(Wizard::new()));
}
}

View File

@ -12,7 +12,7 @@ use graphics::types::Color;
use kml;
use map_model;
use map_model::IntersectionID;
use objects::{Ctx, ID};
use objects::{Ctx, DEBUG_LAYERS, ID, ROOT_MENU};
use piston::input::{Key, MouseCursorEvent};
use piston::window::Size;
use plugins::classification::OsmClassifier;
@ -369,7 +369,7 @@ impl UI {
}
}
if input.unimportant_key_pressed(Key::Escape, "quit") {
if input.unimportant_key_pressed(Key::Escape, ROOT_MENU, "quit") {
let state = EditorState {
map_name: self.map.get_name().clone(),
cam_x: self.canvas.cam_x,
@ -544,21 +544,33 @@ pub struct ToggleableLayers {
impl ToggleableLayers {
fn new() -> ToggleableLayers {
ToggleableLayers {
show_lanes: ToggleableLayer::new("lanes", Key::D3, Some(MIN_ZOOM_FOR_LANES)),
show_buildings: ToggleableLayer::new("buildings", Key::D1, Some(0.0)),
show_lanes: ToggleableLayer::new(
DEBUG_LAYERS,
"lanes",
Key::D3,
Some(MIN_ZOOM_FOR_LANES),
),
show_buildings: ToggleableLayer::new(DEBUG_LAYERS, "buildings", Key::D1, Some(0.0)),
show_intersections: ToggleableLayer::new(
DEBUG_LAYERS,
"intersections",
Key::D2,
Some(MIN_ZOOM_FOR_LANES),
),
show_parcels: ToggleableLayer::new("parcels", Key::D4, Some(MIN_ZOOM_FOR_PARCELS)),
show_parcels: ToggleableLayer::new(
DEBUG_LAYERS,
"parcels",
Key::D4,
Some(MIN_ZOOM_FOR_PARCELS),
),
show_extra_shapes: ToggleableLayer::new(
DEBUG_LAYERS,
"extra KML shapes",
Key::D7,
Some(MIN_ZOOM_FOR_LANES),
),
show_all_turn_icons: ToggleableLayer::new("turn icons", Key::D9, None),
debug_mode: ToggleableLayer::new("debug mode", Key::G, None),
show_all_turn_icons: ToggleableLayer::new(DEBUG_LAYERS, "turn icons", Key::D9, None),
debug_mode: ToggleableLayer::new(DEBUG_LAYERS, "debug mode", Key::G, None),
}
}

View File

@ -3,6 +3,7 @@
use keys::describe_key;
use piston::input::{Button, Event, IdleArgs, Key, PressEvent};
use std::collections::HashMap;
use tree_menu::TreeMenu;
use TextOSD;
// As we check for user input, record the input and the thing that would happen. This will let us
@ -18,6 +19,8 @@ pub struct UserInput {
// TODO hack :(
empty_event: Event,
unimportant_actions_tree: TreeMenu,
}
// TODO it'd be nice to automatically detect cases where two callers are trying to check for the
@ -32,6 +35,7 @@ impl UserInput {
important_actions: Vec::new(),
reserved_keys: HashMap::new(),
empty_event: Event::from(IdleArgs { dt: 0.0 }),
unimportant_actions_tree: TreeMenu::new(),
}
}
@ -117,7 +121,7 @@ impl UserInput {
false
}
pub fn unimportant_key_pressed(&mut self, key: Key, action: &str) -> bool {
pub fn unimportant_key_pressed(&mut self, key: Key, category: &str, action: &str) -> bool {
self.reserve_key(key, action);
if self.event_consumed {
@ -132,6 +136,8 @@ impl UserInput {
}
self.unimportant_actions
.push(format!("Press {} to {}", describe_key(key), action));
self.unimportant_actions_tree
.add_action(Some(key), category, action);
false
}
@ -166,6 +172,8 @@ impl UserInput {
for a in self.important_actions.into_iter() {
osd.add_line(a);
}
println!("{}", self.unimportant_actions_tree);
}
fn reserve_key(&mut self, key: Key, action: &str) {

View File

@ -15,6 +15,7 @@ mod menu;
mod runner;
mod text;
mod text_box;
mod tree_menu;
pub use canvas::Canvas;
use graphics::character::CharacterCache;
@ -136,6 +137,7 @@ impl<'a> GfxCtx<'a> {
}
pub struct ToggleableLayer {
category: String,
layer_name: String,
key: Key,
// If None, never automatically enable at a certain zoom level.
@ -145,11 +147,17 @@ pub struct ToggleableLayer {
}
impl ToggleableLayer {
pub fn new(layer_name: &str, key: Key, min_zoom: Option<f64>) -> ToggleableLayer {
pub fn new(
category: &str,
layer_name: &str,
key: Key,
min_zoom: Option<f64>,
) -> ToggleableLayer {
ToggleableLayer {
key,
min_zoom,
layer_name: String::from(layer_name),
category: category.to_string(),
layer_name: layer_name.to_string(),
enabled: false,
}
}
@ -172,11 +180,8 @@ impl ToggleableLayer {
pub fn event(&mut self, input: &mut input::UserInput) -> bool {
if input.unimportant_key_pressed(
self.key,
&format!(
"Press {} to toggle {}",
keys::describe_key(self.key),
self.layer_name
),
&self.category,
&format!("toggle {}", self.layer_name),
) {
self.enabled = !self.enabled;
return true;

91
ezgui/src/tree_menu.rs Normal file
View File

@ -0,0 +1,91 @@
use keys::describe_key;
use piston::input::Key;
use std::collections::{BTreeMap, VecDeque};
use std::fmt;
pub struct TreeMenu {
root: BTreeMap<String, Item>,
}
impl TreeMenu {
pub fn new() -> TreeMenu {
TreeMenu {
root: BTreeMap::new(),
}
}
pub fn add_action(&mut self, hotkey: Option<Key>, path: &str, action: &str) {
// Split returns something for an empty string
if path == "" {
populate_tree(VecDeque::new(), &mut self.root, hotkey, action);
return;
}
let parts: Vec<&str> = path.split("/").collect();
populate_tree(VecDeque::from(parts), &mut self.root, hotkey, action);
}
}
enum Item {
Action(Option<Key>),
Tree(Option<Key>, BTreeMap<String, Item>),
}
impl fmt::Display for TreeMenu {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "TreeMenu:\n")?;
print(0, &self.root, f)
}
}
fn print(depth: usize, tree: &BTreeMap<String, Item>, f: &mut fmt::Formatter) -> fmt::Result {
let pad = String::from_utf8(vec![b' '; 4 * depth]).unwrap();
for (name, item) in tree {
match item {
Item::Action(key) => {
write!(f, "{}- {} ({})\n", pad, name, describe_maybe_key(key))?;
}
Item::Tree(key, subtree) => {
write!(f, "{}- {} ({})\n", pad, name, describe_maybe_key(key))?;
print(depth + 1, subtree, f)?;
}
}
}
Ok(())
}
fn describe_maybe_key(key: &Option<Key>) -> String {
match key {
Some(k) => describe_key(*k),
None => "".to_string(),
}
}
fn populate_tree(
mut path_parts: VecDeque<&str>,
tree: &mut BTreeMap<String, Item>,
hotkey: Option<Key>,
action: &str,
) {
let part = match path_parts.pop_front() {
Some(p) => p,
None => {
assert!(!tree.contains_key(action));
tree.insert(action.to_string(), Item::Action(hotkey));
return;
}
};
if !tree.contains_key(part) {
tree.insert(part.to_string(), Item::Tree(None, BTreeMap::new()));
}
match tree.get_mut(part).unwrap() {
Item::Action(_) => {
panic!("add_action specifies a path that's an action, not a subtree");
}
Item::Tree(_, subtree) => {
populate_tree(path_parts, subtree, hotkey, action);
}
}
}