prototyping a different way of specifying contextual actions

This commit is contained in:
Dustin Carlino 2020-03-28 14:55:54 -07:00
parent dd9b43c990
commit 6172fa5215
7 changed files with 237 additions and 79 deletions

View File

@ -1,7 +1,8 @@
pub mod setup;
use crate::app::{App, PerMap};
use crate::common::{tool_panel, CommonState};
use crate::common::{tool_panel, CommonState, ContextualActions};
use crate::helpers::ID;
use crate::debug::DebugMode;
use crate::game::{State, Transition};
use crate::managed::{WrappedComposite, WrappedOutcome};
@ -138,7 +139,7 @@ impl State for ABTestMode {
self.recalculate_stuff(app, ctx);
}*/
if let Some(t) = self.common.event(ctx, app, None) {
if let Some(t) = self.common.event(ctx, app, None, &mut Actions {}) {
return t;
}
match self.tool_panel.event(ctx, app) {
@ -364,3 +365,13 @@ pub struct ABTestSavestate {
secondary_map: Map,
secondary_sim: Sim,
}
struct Actions;
impl ContextualActions for Actions {
fn actions(&self, app: &App, id: ID) -> Vec<(Key, String)> {
Vec::new()
}
fn execute(&mut self, ctx: &mut EventCtx, app: &mut App, id: ID, action: String) -> Transition {
Transition::Keep
}
}

View File

@ -20,22 +20,26 @@ pub use self::warp::Warping;
use crate::app::App;
use crate::game::Transition;
use crate::helpers::{list_names, ID};
pub use crate::info::ContextualActions;
use crate::info::InfoPanel;
use crate::sandbox::SpeedControls;
use ezgui::{
lctrl, Color, EventCtx, GeomBatch, GfxCtx, Key, Line, ScreenDims, ScreenPt, ScreenRectangle,
hotkey, lctrl, Color, EventCtx, GeomBatch, GfxCtx, Key, Line, ScreenDims, ScreenPt, ScreenRectangle,
Text,
};
use geom::Polygon;
use std::collections::BTreeSet;
pub struct CommonState {
// TODO Better to express these as mutex
info_panel: Option<InfoPanel>,
// Just for drawing the OSD
cached_actions: Vec<(Key, String)>,
}
impl CommonState {
pub fn new() -> CommonState {
CommonState { info_panel: None }
CommonState { info_panel: None, cached_actions: Vec::new() }
}
// This has to be called after anything that calls app.per_obj.action(). Oof.
@ -45,6 +49,7 @@ impl CommonState {
ctx: &mut EventCtx,
app: &mut App,
maybe_speed: Option<&mut SpeedControls>,
ctx_actions: &mut dyn ContextualActions,
) -> Option<Transition> {
if ctx.input.new_was_pressed(&lctrl(Key::S).unwrap()) {
app.opts.dev = !app.opts.dev;
@ -65,13 +70,14 @@ impl CommonState {
id.clone(),
actions,
maybe_speed,
ctx_actions,
));
return None;
}
}
if let Some(ref mut info) = self.info_panel {
let (closed, maybe_t) = info.event(ctx, app, maybe_speed);
let (closed, maybe_t) = info.event(ctx, app, maybe_speed, ctx_actions);
if closed {
self.info_panel = None;
assert!(app.per_obj.info_panel_open);
@ -82,6 +88,19 @@ impl CommonState {
}
}
if self.info_panel.is_none() {
if let Some(id) = app.primary.current_selection.clone() {
self.cached_actions = ctx_actions.actions(app, id.clone());
// Allow hotkeys to work without opening the panel.
for (k, action) in &self.cached_actions {
if ctx.input.new_was_pressed(&hotkey(*k).unwrap()) {
return Some(ctx_actions.execute(ctx, app, id, action.clone()));
}
}
}
}
None
}
@ -267,8 +286,21 @@ impl CommonState {
}
// Meant to be used for launching from other states
pub fn launch_info_panel(&mut self, id: ID, ctx: &mut EventCtx, app: &mut App) {
self.info_panel = Some(InfoPanel::launch(ctx, app, id, Vec::new(), None));
pub fn launch_info_panel(
&mut self,
id: ID,
ctx: &mut EventCtx,
app: &mut App,
ctx_actions: &mut dyn ContextualActions,
) {
self.info_panel = Some(InfoPanel::launch(
ctx,
app,
id,
Vec::new(),
None,
ctx_actions,
));
app.per_obj.info_panel_open = true;
}

View File

@ -1,8 +1,9 @@
use crate::app::{App, PerMap};
use crate::common::ContextualActions;
use crate::game::{State, Transition, WizardState};
use crate::helpers::ID;
use crate::sandbox::SandboxMode;
use ezgui::{EventCtx, GfxCtx, Warper, Wizard};
use ezgui::{EventCtx, GfxCtx, Key, Warper, Wizard};
use geom::Pt2D;
use map_model::{AreaID, BuildingID, IntersectionID, LaneID, RoadID};
use sim::{PedestrianID, TripID};
@ -62,11 +63,12 @@ impl State for Warping {
if let Some(id) = self.id.clone() {
Transition::PopWithData(Box::new(move |state, app, ctx| {
if let Some(ref mut s) = state.downcast_mut::<SandboxMode>() {
s.controls
.common
.as_mut()
.unwrap()
.launch_info_panel(id, ctx, app);
s.controls.common.as_mut().unwrap().launch_info_panel(
id,
ctx,
app,
&mut Actions {},
);
}
}))
} else {
@ -129,3 +131,14 @@ fn warp_point(line: &str, primary: &PerMap) -> Option<(Option<ID>, Pt2D, f64)> {
None
}
}
// TODO Nooo! Need to be passed one of these from the creator
struct Actions;
impl ContextualActions for Actions {
fn actions(&self, app: &App, id: ID) -> Vec<(Key, String)> {
Vec::new()
}
fn execute(&mut self, ctx: &mut EventCtx, app: &mut App, id: ID, action: String) -> Transition {
Transition::Keep
}
}

View File

@ -4,7 +4,7 @@ mod polygons;
use crate::app::{App, ShowLayers, ShowObject};
use crate::colors;
use crate::common::{tool_panel, CommonState};
use crate::common::{tool_panel, CommonState, ContextualActions};
use crate::game::{msg, DrawBaselayer, State, Transition, WizardState};
use crate::helpers::ID;
use crate::managed::{WrappedComposite, WrappedOutcome};
@ -295,7 +295,7 @@ impl State for DebugMode {
return Transition::Push(floodfiller);
}
if let Some(t) = self.common.event(ctx, app, None) {
if let Some(t) = self.common.event(ctx, app, None, &mut Actions {}) {
return t;
}
match self.tool_panel.event(ctx, app) {
@ -460,3 +460,13 @@ fn calc_all_routes(ctx: &EventCtx, app: &mut App) -> (usize, Drawable) {
}
(cnt, ctx.upload(batch))
}
struct Actions;
impl ContextualActions for Actions {
fn actions(&self, app: &App, id: ID) -> Vec<(Key, String)> {
Vec::new()
}
fn execute(&mut self, ctx: &mut EventCtx, app: &mut App, id: ID, action: String) -> Transition {
Transition::Keep
}
}

View File

@ -1,6 +1,6 @@
use crate::app::App;
use crate::colors;
use crate::common::{tool_panel, Colorer, CommonState, Warping};
use crate::common::{tool_panel, Colorer, CommonState, ContextualActions, Warping};
use crate::game::{State, Transition, WizardState};
use crate::helpers::ID;
use crate::managed::{WrappedComposite, WrappedOutcome};
@ -171,59 +171,19 @@ impl State for ScenarioManager {
app.recalculate_current_selection(ctx);
}
if let Some(ID::Building(b)) = app.primary.current_selection {
let from = self.trips_from_bldg.get(b);
let to = self.trips_to_bldg.get(b);
if !from.is_empty() || !to.is_empty() {
if app.per_obj.action(ctx, Key::T, "browse trips") {
// TODO Avoid the clone? Just happens once though.
let mut all_trips = from.clone();
all_trips.extend(to);
return Transition::Push(make_trip_picker(
self.scenario.clone(),
all_trips,
"building",
OD::Bldg(b),
));
} else if self.demand.is_none()
&& app.per_obj.action(ctx, Key::P, "show trips to and from")
{
self.demand =
Some(show_demand(&self.scenario, from, to, OD::Bldg(b), app, ctx));
}
}
} else if let Some(ID::Intersection(i)) = app.primary.current_selection {
let from = self.trips_from_border.get(i);
let to = self.trips_to_border.get(i);
if !from.is_empty() || !to.is_empty() {
if app.per_obj.action(ctx, Key::T, "browse trips") {
// TODO Avoid the clone? Just happens once though.
let mut all_trips = from.clone();
all_trips.extend(to);
return Transition::Push(make_trip_picker(
self.scenario.clone(),
all_trips,
"border",
OD::Border(i),
));
} else if self.demand.is_none()
&& app.per_obj.action(ctx, Key::P, "show trips to and from")
{
self.demand = Some(show_demand(
&self.scenario,
from,
to,
OD::Border(i),
app,
ctx,
));
}
}
}
if let Some(t) = self.common.event(ctx, app, None) {
if let Some(t) = self.common.event(
ctx,
app,
None,
&mut Actions {
demand: &mut self.demand,
scenario: &self.scenario,
trips_from_bldg: &self.trips_from_bldg,
trips_to_bldg: &self.trips_to_bldg,
trips_from_border: &self.trips_from_border,
trips_to_border: &self.trips_to_border,
},
) {
return t;
}
match self.tool_panel.event(ctx, app) {
@ -612,3 +572,91 @@ impl State for DotMap {
self.composite.draw(g);
}
}
struct Actions<'a> {
demand: &'a mut Option<Drawable>,
scenario: &'a Scenario,
trips_from_bldg: &'a MultiMap<BuildingID, (usize, usize)>,
trips_to_bldg: &'a MultiMap<BuildingID, (usize, usize)>,
trips_from_border: &'a MultiMap<IntersectionID, (usize, usize)>,
trips_to_border: &'a MultiMap<IntersectionID, (usize, usize)>,
}
impl<'a> ContextualActions for Actions<'a> {
fn actions(&self, app: &App, id: ID) -> Vec<(Key, String)> {
let mut actions = Vec::new();
// TODO Actually no, tell them the ID.
if let ID::Building(b) = id {
let from = self.trips_from_bldg.get(b);
let to = self.trips_to_bldg.get(b);
if !from.is_empty() || !to.is_empty() {
actions.push((Key::T, "browse trips".to_string()));
if self.demand.is_none() {
actions.push((Key::P, "show trips to and from".to_string()));
}
}
} else if let ID::Intersection(i) = id {
let from = self.trips_from_border.get(i);
let to = self.trips_to_border.get(i);
if !from.is_empty() || !to.is_empty() {
actions.push((Key::T, "browse trips".to_string()));
if self.demand.is_none() {
actions.push((Key::P, "show trips to and from".to_string()));
}
}
}
actions
}
fn execute(&mut self, ctx: &mut EventCtx, app: &mut App, id: ID, action: String) -> Transition {
match (id, action.as_ref()) {
(ID::Building(b), "browse trips") => {
// TODO Avoid the clone? Just happens once though.
let mut all_trips = self.trips_from_bldg.get(b).clone();
all_trips.extend(self.trips_to_bldg.get(b).clone());
Transition::Push(make_trip_picker(
self.scenario.clone(),
all_trips,
"building",
OD::Bldg(b),
))
}
(ID::Building(b), "show trips to and from") => {
*self.demand = Some(show_demand(
self.scenario,
self.trips_from_bldg.get(b),
self.trips_to_bldg.get(b),
OD::Bldg(b),
app,
ctx,
));
Transition::Keep
}
_ => unreachable!(),
}
/*if app.per_obj.action(ctx, Key::T, "browse trips") {
// TODO Avoid the clone? Just happens once though.
let mut all_trips = from.clone();
all_trips.extend(to);
return Transition::Push(make_trip_picker(
self.scenario.clone(),
all_trips,
"border",
OD::Border(i),
));
} else if self.demand.is_none()
&& app.per_obj.action(ctx, Key::P, "show trips to and from")
{
self.demand = Some(show_demand(
&self.scenario,
from,
to,
OD::Border(i),
app,
ctx,
));
}*/
}
}

View File

@ -139,8 +139,16 @@ impl InfoPanel {
id: ID,
actions: Vec<(Key, String)>,
maybe_speed: Option<&mut SpeedControls>,
ctx_actions: &mut dyn ContextualActions,
) -> InfoPanel {
InfoPanel::new(ctx, app, Tab::from_id(app, id), actions, maybe_speed)
InfoPanel::new(
ctx,
app,
Tab::from_id(app, id),
actions,
maybe_speed,
ctx_actions,
)
}
fn new(
@ -149,6 +157,7 @@ impl InfoPanel {
tab: Tab,
actions: Vec<(Key, String)>,
maybe_speed: Option<&mut SpeedControls>,
ctx_actions: &mut dyn ContextualActions,
) -> InfoPanel {
/*if maybe_speed.map(|s| s.is_paused()).unwrap_or(false)
&& id.agent_id().is_some()
@ -198,9 +207,22 @@ impl InfoPanel {
.margin(5),
);
}
let maybe_id = tab.clone().to_id(app);
if let Some(id) = maybe_id.clone() {
for (key, label) in ctx_actions.actions(app, id) {
let mut txt = Text::new();
txt.append(Line(key.describe()).fg(ezgui::HOTKEY_COLOR));
txt.append(Line(format!(" - {}", label)));
col.push(
Btn::text_bg(label, txt, colors::SECTION_BG, colors::HOVERING)
.build_def(ctx, hotkey(key))
.margin(5),
);
}
}
// Highlight something?
if let Some((id, outline)) = tab.clone().to_id(app).and_then(|id| {
if let Some((id, outline)) = maybe_id.clone().and_then(|id| {
app.primary
.draw_map
.get_obj(
@ -264,9 +286,7 @@ impl InfoPanel {
// Follow the agent. When the sim is paused, this lets the player naturally pan away,
// because the InfoPanel isn't being updated.
if let Some(pt) = tab
.clone()
.to_id(app)
if let Some(pt) = maybe_id
.and_then(|id| id.agent_id())
.and_then(|a| app.primary.sim.canonical_pt_for_agent(a, &app.primary.map))
{
@ -299,6 +319,7 @@ impl InfoPanel {
ctx: &mut EventCtx,
app: &mut App,
maybe_speed: Option<&mut SpeedControls>,
ctx_actions: &mut dyn ContextualActions,
) -> (bool, Option<Transition>) {
// Can click on the map to cancel
if ctx.canvas.get_cursor_in_map_space().is_some()
@ -317,11 +338,13 @@ impl InfoPanel {
self.tab.clone(),
self.actions.clone(),
maybe_speed,
ctx_actions,
);
self.composite.restore_scroll(ctx, preserve_scroll);
return (false, None);
}
let maybe_id = self.tab.clone().to_id(app);
match self.composite.event(ctx) {
Some(Outcome::Clicked(action)) => {
if let Some(new_tab) = self.hyperlinks.get(&action).cloned() {
@ -329,12 +352,13 @@ impl InfoPanel {
ctx,
app,
new_tab.clone(),
if self.tab.clone().to_id(app) == new_tab.to_id(app) {
if maybe_id == new_tab.to_id(app) {
self.actions.clone()
} else {
Vec::new()
},
maybe_speed,
ctx_actions,
);
return (false, None);
} else if action == "close info" {
@ -373,8 +397,10 @@ impl InfoPanel {
))),
)
} else {
app.primary.current_selection = Some(self.tab.clone().to_id(app).unwrap());
(true, Some(Transition::ApplyObjectAction(action)))
/*app.primary.current_selection = Some(self.tab.clone().to_id(app).unwrap());
(true, Some(Transition::ApplyObjectAction(action)))*/
(true, Some(ctx_actions.execute(ctx, app, maybe_id.unwrap(), action)))
}
}
None => (false, None),
@ -480,3 +506,9 @@ fn header_btns(ctx: &EventCtx) -> Widget {
])
.align_right()
}
pub trait ContextualActions {
// TODO &str?
fn actions(&self, app: &App, id: ID) -> Vec<(Key, String)>;
fn execute(&mut self, ctx: &mut EventCtx, app: &mut App, id: ID, action: String) -> Transition;
}

View File

@ -4,7 +4,9 @@ mod speed;
use crate::app::App;
use crate::colors;
use crate::common::{tool_panel, CommonState, Minimap, Overlays, RoutePreview, ShowBusRoute};
use crate::common::{
tool_panel, CommonState, ContextualActions, Minimap, Overlays, RoutePreview, ShowBusRoute,
};
use crate::debug::DebugMode;
use crate::edit::{
apply_map_edits, can_edit_lane, save_edits_as, EditMode, LaneEditor, StopSignEditor,
@ -236,7 +238,7 @@ impl State for SandboxMode {
// also let this work before tool_panel, so Key::Escape from the info panel beats the one
// to quit. And let speed update the sim before we update the info panel.
if let Some(ref mut c) = self.controls.common {
if let Some(t) = c.event(ctx, app, self.controls.speed.as_mut()) {
if let Some(t) = c.event(ctx, app, self.controls.speed.as_mut(), &mut Actions {}) {
return t;
}
}
@ -510,3 +512,13 @@ impl AgentMeter {
self.composite.draw(g);
}
}
struct Actions;
impl ContextualActions for Actions {
fn actions(&self, app: &App, id: ID) -> Vec<(Key, String)> {
Vec::new()
}
fn execute(&mut self, ctx: &mut EventCtx, app: &mut App, id: ID, action: String) -> Transition {
Transition::Keep
}
}