game/ungap/
mod.rs

1mod bike_network;
2mod explore;
3mod layers;
4mod predict;
5mod quick_sketch;
6mod trip;
7
8use geom::CornerRadii;
9use map_gui::tools::CityPicker;
10use widgetry::{
11    EventCtx, HorizontalAlignment, Key, Line, Panel, PanelDims, State, VerticalAlignment, Widget,
12    DEFAULT_CORNER_RADIUS,
13};
14
15pub use self::explore::ExploreMap;
16pub use self::layers::Layers;
17use crate::app::{App, Transition};
18pub use predict::ModeShiftData;
19pub use trip::RoutingPreferences;
20
21// The 3 modes are very different States, so TabController doesn't seem like the best fit
22#[derive(PartialEq)]
23pub enum Tab {
24    Explore,
25    Trip,
26    AddLanes,
27    PredictImpact,
28}
29
30pub trait TakeLayers {
31    fn take_layers(self) -> Layers;
32}
33
34impl Tab {
35    pub fn make_left_panel(self, ctx: &mut EventCtx, app: &App, contents: Widget) -> Panel {
36        // Ideally TabController could manage this, but the contents of each section are
37        // substantial, controlled by entirely different States.
38
39        let mut contents = Some(contents.section(ctx));
40
41        // map_gui::tools::app_header uses 2 rows, but we've tuned the horizontal space here. It's
42        // nicer to fit on one row.
43        let header = Widget::row(vec![
44            map_gui::tools::home_btn(ctx),
45            Line("Ungap the Map")
46                .small_heading()
47                .into_widget(ctx)
48                .centered_vert(),
49            map_gui::tools::change_map_btn(ctx, app)
50                .centered_vert()
51                .align_right(),
52        ]);
53
54        let mut build_tab = |(tab, image_path, tab_title, hotkey): (Tab, &str, &str, Key)| {
55            let mut btn = ctx
56                .style()
57                .btn_tab
58                .icon_text(image_path, tab_title)
59                .hotkey(hotkey);
60
61            if self == tab {
62                btn = btn
63                    .corner_rounding(CornerRadii {
64                        top_left: DEFAULT_CORNER_RADIUS,
65                        top_right: DEFAULT_CORNER_RADIUS,
66                        bottom_left: 0.0,
67                        bottom_right: 0.0,
68                    })
69                    .disabled(true);
70            }
71
72            // Add a little margin to compensate for the border on the tab content
73            // otherwise things look ever-so-slightly out of alignment.
74            let btn_widget = btn.build_def(ctx).margin_left(1);
75            let mut tab_elements = vec![btn_widget];
76
77            if self == tab {
78                let mut contents = contents.take().unwrap();
79                contents = contents.corner_rounding(CornerRadii {
80                    top_left: 0.0,
81                    top_right: DEFAULT_CORNER_RADIUS,
82                    bottom_left: DEFAULT_CORNER_RADIUS,
83                    bottom_right: DEFAULT_CORNER_RADIUS,
84                });
85                tab_elements.push(contents);
86            }
87
88            Widget::custom_col(tab_elements)
89        };
90
91        let tabs = Widget::col(vec![
92            build_tab((
93                Tab::Explore,
94                "system/assets/tools/pan.svg",
95                "Explore",
96                Key::Num1,
97            )),
98            build_tab((
99                Tab::Trip,
100                "system/assets/tools/pin.svg",
101                "Your trip",
102                Key::Num2,
103            )),
104            build_tab((
105                Tab::AddLanes,
106                "system/assets/tools/pencil.svg",
107                "Propose new bike lanes",
108                Key::Num3,
109            )),
110            build_tab((
111                Tab::PredictImpact,
112                "system/assets/meters/trip_histogram.svg",
113                "Predict impact",
114                Key::Num4,
115            )),
116        ]);
117
118        let mut panel = Panel::new_builder(Widget::col(vec![header, tabs]))
119            // The different tabs have different widths. To avoid the UI bouncing around as the user
120            // navigates, this is hardcoded to be a bit wider than the widest tab.
121            .dims_width(PanelDims::ExactPixels(620.0))
122            .dims_height(PanelDims::ExactPercent(1.0))
123            .aligned(HorizontalAlignment::Left, VerticalAlignment::Top);
124        if self == Tab::Trip {
125            // Hovering on a card
126            panel = panel.ignore_initial_events();
127        }
128        panel.build(ctx)
129    }
130
131    pub fn handle_action<T: TakeLayers + State<App>>(
132        self,
133        ctx: &mut EventCtx,
134        app: &mut App,
135        action: &str,
136    ) -> Option<Transition> {
137        match action {
138            "Home" => Some(Transition::Pop),
139            "change map" => {
140                Some(Transition::Push(CityPicker::new_state(
141                    ctx,
142                    app,
143                    Box::new(move |ctx, app| {
144                        // Since we're totally changing maps, don't reuse the Layers
145                        let layers = Layers::new(ctx, app);
146                        Transition::Multi(vec![
147                            Transition::Pop,
148                            Transition::Replace(match self {
149                                Tab::Explore => ExploreMap::new_state(ctx, app, layers),
150                                Tab::Trip => trip::TripPlanner::new_state(ctx, app, layers),
151                                Tab::AddLanes => {
152                                    quick_sketch::QuickSketch::new_state(ctx, app, layers)
153                                }
154                                Tab::PredictImpact => {
155                                    predict::ShowGaps::new_state(ctx, app, layers)
156                                }
157                            }),
158                        ])
159                    }),
160                )))
161            }
162            "Explore" => Some(Transition::ConsumeState(Box::new(|state, ctx, app| {
163                let state = state.downcast::<T>().ok().unwrap();
164                vec![ExploreMap::new_state(ctx, app, state.take_layers())]
165            }))),
166            "Your trip" => Some(Transition::ConsumeState(Box::new(|state, ctx, app| {
167                let state = state.downcast::<T>().ok().unwrap();
168                vec![trip::TripPlanner::new_state(ctx, app, state.take_layers())]
169            }))),
170            "Propose new bike lanes" => {
171                // This is only necessary to do coming from ExploreMap, but eh
172                app.primary.current_selection = None;
173                Some(Transition::ConsumeState(Box::new(|state, ctx, app| {
174                    let state = state.downcast::<T>().ok().unwrap();
175                    vec![quick_sketch::QuickSketch::new_state(
176                        ctx,
177                        app,
178                        state.take_layers(),
179                    )]
180                })))
181            }
182            "Predict impact" => Some(Transition::ConsumeState(Box::new(|state, ctx, app| {
183                let state = state.downcast::<T>().ok().unwrap();
184                vec![predict::ShowGaps::new_state(ctx, app, state.take_layers())]
185            }))),
186            _ => None,
187        }
188    }
189}