game/pregame/
proposals.rs1use std::collections::HashMap;
2
3use geom::Percent;
4use map_gui::load::MapLoader;
5use map_model::PermanentMapEdits;
6use synthpop::Scenario;
7use widgetry::tools::{open_browser, PopupMsg};
8use widgetry::{EventCtx, Key, Line, Panel, SimpleState, State, Text, Widget};
9
10use crate::app::{App, Transition};
11use crate::edit::apply_map_edits;
12use crate::sandbox::{GameplayMode, SandboxMode};
13
14pub struct Proposals {
15 proposals: HashMap<String, PermanentMapEdits>,
16 current: Option<String>,
17}
18
19impl Proposals {
20 pub fn new_state(ctx: &mut EventCtx, current: Option<String>) -> Box<dyn State<App>> {
21 let mut proposals = HashMap::new();
22 let mut tab_buttons = Vec::new();
23 let mut current_tab_rows = Vec::new();
24 for (name, edits) in
28 abstio::load_all_objects::<PermanentMapEdits>(abstio::path("system/proposals"))
29 {
30 if current == Some(name.clone()) {
31 let mut txt = Text::new();
32 txt.add_line(Line(edits.get_title()).small_heading());
33 for l in edits.proposal_description.iter().skip(1) {
34 txt.add_line(l);
35 }
36 current_tab_rows.push(
37 txt.wrap_to_pct(ctx, 70)
38 .into_widget(ctx)
39 .margin_below(15)
40 .margin_above(15),
41 );
42
43 if edits.proposal_link.is_some() {
44 current_tab_rows.push(
45 ctx.style()
46 .btn_plain
47 .btn()
48 .label_underlined_text("Read detailed write-up")
49 .build_def(ctx)
50 .margin_below(10),
51 );
52 }
53 current_tab_rows.push(
54 ctx.style()
55 .btn_solid_primary
56 .text("Try out this proposal")
57 .hotkey(Key::Enter)
58 .build_def(ctx),
59 );
60
61 tab_buttons.push(
62 ctx.style()
63 .btn_tab
64 .text(edits.get_title())
65 .disabled(true)
66 .build_def(ctx)
67 .margin_below(10),
68 );
69 } else {
70 let hotkey = Key::NUM_KEYS
71 .get(proposals.len())
72 .map(|key| widgetry::MultiKey::from(*key));
73 tab_buttons.push(
74 ctx.style()
75 .btn_outline
76 .text(edits.get_title())
77 .no_tooltip()
78 .hotkey(hotkey)
79 .build_widget(ctx, &name)
80 .margin_below(10),
81 );
82 }
83
84 proposals.insert(name, edits);
85 }
86
87 let panel = Panel::new_builder(Widget::col(vec![
88 Widget::row(vec![
89 Line("Community proposals").small_heading().into_widget(ctx),
90 ctx.style().btn_close_widget(ctx),
91 ]),
92 {
93 let mut txt =
94 Text::from("These are proposed changes to Seattle made by community members.");
95 txt.add_line("Contact dabreegster@gmail.com to add your idea here!");
96 txt.into_widget(ctx).centered_horiz()
97 },
98 Widget::custom_row(tab_buttons)
99 .flex_wrap(ctx, Percent::int(80))
100 .margin_above(60),
101 Widget::col(current_tab_rows),
102 ]))
103 .build(ctx);
104 <dyn SimpleState<_>>::new_state(panel, Box::new(Proposals { proposals, current }))
105 }
106}
107
108impl SimpleState<App> for Proposals {
109 fn on_click(
110 &mut self,
111 ctx: &mut EventCtx,
112 app: &mut App,
113 x: &str,
114 _: &mut Panel,
115 ) -> Transition {
116 match x {
117 "close" => Transition::Pop,
118 "Try out this proposal" => launch(
119 ctx,
120 app,
121 self.proposals[self.current.as_ref().unwrap()].clone(),
122 ),
123 "Read detailed write-up" => {
124 open_browser(
125 self.proposals[self.current.as_ref().unwrap()]
126 .proposal_link
127 .clone()
128 .unwrap(),
129 );
130 Transition::Keep
131 }
132 x => Transition::Replace(Proposals::new_state(ctx, Some(x.to_string()))),
133 }
134 }
135}
136
137fn launch(ctx: &mut EventCtx, app: &App, edits: PermanentMapEdits) -> Transition {
138 #[cfg(not(target_arch = "wasm32"))]
139 {
140 if !abstio::file_exists(edits.map_name.path()) {
141 return map_gui::tools::prompt_to_download_missing_data(
142 ctx,
143 edits.map_name.clone(),
144 Box::new(move |ctx, app| launch(ctx, app, edits)),
145 );
146 }
147 }
148
149 Transition::Push(MapLoader::new_state(
150 ctx,
151 app,
152 edits.map_name.clone(),
153 Box::new(move |ctx, app| {
154 let maybe_err = ctx.loading_screen("apply edits", |ctx, timer| {
156 match edits.into_edits(&app.primary.map) {
157 Ok(edits) => {
158 apply_map_edits(ctx, app, edits);
159 app.primary.map.recalculate_pathfinding_after_edits(timer);
160 None
161 }
162 Err(err) => Some(err),
163 }
164 });
165 if let Some(err) = maybe_err {
166 Transition::Replace(PopupMsg::new_state(
167 ctx,
168 "Can't load proposal",
169 vec![err.to_string()],
170 ))
171 } else {
172 app.primary.layer = Some(Box::new(crate::layer::map::Static::edits(ctx, app)));
173 Transition::Replace(SandboxMode::simple_new(
174 app,
175 GameplayMode::PlayScenario(
176 app.primary.map.get_name().clone(),
177 Scenario::default_scenario_for_map(app.primary.map.get_name()),
178 Vec::new(),
179 ),
180 ))
181 }
182 }),
183 ))
184}