mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-25 07:25:47 +03:00
prototyping a different way of specifying contextual actions
This commit is contained in:
parent
dd9b43c990
commit
6172fa5215
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
));
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user