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
11pub 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 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 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}