ltn/components/
appwide_panel.rs

1use widgetry::tools::ChooseSomething;
2use widgetry::tools::PopupMsg;
3use widgetry::{
4    lctrl, Choice, CornerRounding, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome,
5    Panel, PanelDims, VerticalAlignment, Widget,
6};
7
8use crate::components::Mode;
9use crate::{pages, App, Transition};
10
11/// Both the top panel and the collapsible left sidebar.
12pub struct AppwidePanel {
13    pub top_panel: Panel,
14    pub left_panel: Panel,
15}
16
17impl AppwidePanel {
18    pub fn new(ctx: &mut EventCtx, app: &App, mode: Mode) -> Self {
19        let top_panel = make_top_panel(ctx, app, mode);
20        let left_panel = make_left_panel(ctx, app, &top_panel, mode);
21        Self {
22            top_panel,
23            left_panel,
24        }
25    }
26
27    pub fn event<F: Fn() -> Vec<&'static str>>(
28        &mut self,
29        ctx: &mut EventCtx,
30        app: &mut App,
31        preserve_state: &crate::save::PreserveState,
32        help: F,
33    ) -> Option<Transition> {
34        if let Outcome::Clicked(x) = self.top_panel.event(ctx) {
35            return match x.as_ref() {
36                "Home" => {
37                    if app.per_map.consultation.is_none() {
38                        Some(Transition::Clear(vec![
39                            map_gui::tools::TitleScreen::new_state(
40                                ctx,
41                                app,
42                                map_gui::tools::Executable::LTN,
43                                Box::new(|ctx, app, _| pages::PickArea::new_state(ctx, app)),
44                            ),
45                        ]))
46                    } else {
47                        Some(Transition::Push(pages::About::new_state(ctx)))
48                    }
49                }
50                "change map" => Some(Transition::Push(map_gui::tools::CityPicker::new_state(
51                    ctx,
52                    app,
53                    Box::new(|ctx, app| Transition::Replace(pages::PickArea::new_state(ctx, app))),
54                ))),
55                "search" => Some(Transition::Push(
56                    map_gui::tools::Navigator::new_state_with_target_zoom(ctx, app, 4.0),
57                )),
58                "help" => Some(Transition::Push(PopupMsg::new_state(ctx, "Help", help()))),
59                "about this tool" => Some(Transition::Push(pages::About::new_state(ctx))),
60                "Pick area" => Some(Transition::Replace(pages::PickArea::new_state(ctx, app))),
61                "Design LTN" => Some(Transition::Replace(pages::DesignLTN::new_state(
62                    ctx,
63                    app,
64                    app.per_map.current_neighbourhood.unwrap(),
65                ))),
66                "Plan route" => Some(Transition::Replace(pages::RoutePlanner::new_state(
67                    ctx, app,
68                ))),
69                "Crossings" => Some(Transition::Replace(pages::Crossings::new_state(ctx, app))),
70                "Predict impact" => Some(launch_impact(ctx, app)),
71                "Cycle network" => Some(Transition::Replace(pages::CycleNetwork::new_state(
72                    ctx, app,
73                ))),
74                "Census" => Some(Transition::Replace(pages::Census::new_state(ctx, app))),
75                _ => unreachable!(),
76            };
77        }
78
79        if let Outcome::Clicked(x) = self.left_panel.event(ctx) {
80            return if x == "show proposals" {
81                app.session.manage_proposals = true;
82                Some(Transition::Recreate)
83            } else if x == "hide proposals" {
84                app.session.manage_proposals = false;
85                Some(Transition::Recreate)
86            } else {
87                crate::save::Proposals::handle_action(ctx, app, preserve_state, &x)
88            };
89        }
90
91        None
92    }
93
94    pub fn draw(&self, g: &mut GfxCtx) {
95        self.top_panel.draw(g);
96        self.left_panel.draw(g);
97    }
98}
99
100fn launch_impact(ctx: &mut EventCtx, app: &mut App) -> Transition {
101    if &app.per_map.impact.map == app.per_map.map.get_name()
102        && app.per_map.impact.map_edit_key == app.per_map.map.get_edits_change_key()
103    {
104        return Transition::Replace(pages::ShowImpactResults::new_state(ctx, app));
105    }
106
107    Transition::Push(ChooseSomething::new_state(ctx,
108        "Impact prediction is experimental. You have to interpret the results carefully. The app may also freeze while calculating this.",
109        Choice::strings(vec!["Never mind", "I understand the warnings. Predict impact!"]),
110        Box::new(|choice, ctx, app| {
111            if choice == "Never mind" {
112                Transition::Pop
113            } else {
114                Transition::Multi(vec![
115                                  Transition::Pop,
116                                  Transition::Replace(pages::ShowImpactResults::new_state(ctx, app)),
117                ])
118            }
119        })))
120}
121
122fn make_top_panel(ctx: &mut EventCtx, app: &App, mode: Mode) -> Panel {
123    let consultation = app.per_map.consultation.is_some();
124
125    fn current_mode(ctx: &mut EventCtx, name: &str) -> Widget {
126        ctx.style()
127            .btn_solid_primary
128            .text(name)
129            .disabled(true)
130            .build_def(ctx)
131    }
132
133    // While we're adjusting a boundary, it's weird to navigate away without explicitly confirming
134    // or reverting the edits. Just remove the nav bar entirely.
135    let navbar = if mode != Mode::SelectBoundary {
136        Widget::row(vec![
137            if mode == Mode::PickArea {
138                current_mode(ctx, "Pick area")
139            } else {
140                ctx.style()
141                    .btn_outline
142                    .text("Pick area")
143                    .disabled(app.per_map.consultation.is_some())
144                    .disabled_tooltip("This consultation is only about the current area")
145                    .build_def(ctx)
146            },
147            if mode == Mode::ModifyNeighbourhood {
148                current_mode(ctx, "Design LTN")
149            } else {
150                ctx.style()
151                    .btn_outline
152                    .text("Design LTN")
153                    .disabled(app.per_map.current_neighbourhood.is_none())
154                    .disabled_tooltip("Pick an area first")
155                    .build_def(ctx)
156            },
157            if mode == Mode::RoutePlanner {
158                current_mode(ctx, "Plan route")
159            } else {
160                ctx.style()
161                    .btn_outline
162                    .text("Plan route")
163                    .hotkey(Key::R)
164                    .build_def(ctx)
165            },
166            if mode == Mode::Crossings {
167                current_mode(ctx, "Crossings")
168            } else {
169                ctx.style()
170                    .btn_outline
171                    .text("Crossings")
172                    .hotkey(Key::C)
173                    .disabled(app.per_map.consultation.is_some())
174                    .disabled_tooltip("Not supported here yet")
175                    .build_def(ctx)
176            },
177            if mode == Mode::Impact {
178                current_mode(ctx, "Predict impact")
179            } else {
180                ctx.style()
181                    .btn_outline
182                    .text("Predict impact")
183                    .disabled(app.per_map.consultation.is_some())
184                    .disabled_tooltip("Not supported here yet")
185                    .build_def(ctx)
186            },
187            if mode == Mode::CycleNetwork {
188                current_mode(ctx, "Cycle network")
189            } else {
190                ctx.style().btn_outline.text("Cycle network").build_def(ctx)
191            },
192            if mode == Mode::Census {
193                current_mode(ctx, "Census")
194            } else if app.per_map.map.all_census_zones().is_empty() {
195                Widget::nothing()
196            } else {
197                ctx.style().btn_outline.text("Census").build_def(ctx)
198            },
199        ])
200        .centered_vert()
201    } else {
202        Widget::nothing()
203    };
204    let col = vec![Widget::row(vec![
205        map_gui::tools::home_btn(ctx),
206        Line(if consultation {
207            "East Bristol Liveable Neighbourhood"
208        } else {
209            "Low traffic neighbourhoods"
210        })
211        .small_heading()
212        .into_widget(ctx)
213        .centered_vert(),
214        ctx.style()
215            .btn_plain
216            .icon("system/assets/tools/info.svg")
217            .build_widget(ctx, "about this tool")
218            .centered_vert()
219            .hide(consultation),
220        map_gui::tools::change_map_btn(ctx, app)
221            .centered_vert()
222            .hide(consultation),
223        navbar,
224        Widget::row(vec![
225            ctx.style()
226                .btn_plain
227                .icon("system/assets/tools/search.svg")
228                .hotkey(lctrl(Key::F))
229                .build_widget(ctx, "search")
230                .centered_vert(),
231            ctx.style()
232                .btn_plain
233                .icon("system/assets/tools/help.svg")
234                .build_widget(ctx, "help")
235                .centered_vert(),
236        ])
237        .align_right(),
238    ])];
239
240    Panel::new_builder(Widget::col(col).corner_rounding(CornerRounding::NoRounding))
241        .aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
242        .dims_width(PanelDims::ExactPercent(1.0))
243        .build(ctx)
244}
245
246fn make_left_panel(ctx: &mut EventCtx, app: &App, top_panel: &Panel, mode: Mode) -> Panel {
247    let mut col = Vec::new();
248
249    // Switching proposals in some modes is too complex to implement, so don't allow it
250    if app.session.manage_proposals && mode != Mode::Impact && mode != Mode::SelectBoundary {
251        col.push(
252            ctx.style()
253                .btn_plain
254                .icon("system/assets/tools/collapse_panel.svg")
255                .hotkey(Key::P)
256                .build_widget(ctx, "hide proposals")
257                .align_right(),
258        );
259        col.push(app.per_map.proposals.to_widget_expanded(ctx));
260    } else {
261        col.push(
262            ctx.style()
263                .btn_plain
264                .icon("system/assets/tools/expand_panel.svg")
265                .hotkey(Key::P)
266                .build_widget(ctx, "show proposals")
267                .align_right(),
268        );
269        if mode != Mode::Impact && mode != Mode::SelectBoundary {
270            col.push(app.per_map.proposals.to_widget_collapsed(ctx));
271        }
272    }
273
274    let top_height = top_panel.panel_dims().height;
275    Panel::new_builder(Widget::col(col).corner_rounding(CornerRounding::NoRounding))
276        .aligned(
277            HorizontalAlignment::Left,
278            VerticalAlignment::Below(top_height),
279        )
280        .dims_height(PanelDims::ExactPixels(
281            ctx.canvas.window_height - top_height,
282        ))
283        .build(ctx)
284}