1use std::cmp::Ordering;
4use std::collections::BTreeSet;
5
6use anyhow::Result;
7
8use abstutil::prettyprint_usize;
9use geom::{Distance, Duration, Polygon};
10use synthpop::TripMode;
11use widgetry::tools::{FutureLoader, PopupMsg};
12use widgetry::{Color, EventCtx, GeomBatch, Line, State, Text, Toggle, Transition, Widget};
13
14use crate::AppLike;
15
16pub struct FilePicker;
17type PickerOutput = (String, Vec<u8>);
18
19impl FilePicker {
20 pub fn new_state<A: 'static + AppLike>(
22 ctx: &mut EventCtx,
23 start_dir: Option<String>,
24 on_load: Box<
25 dyn FnOnce(&mut EventCtx, &mut A, Result<Option<PickerOutput>>) -> Transition<A>,
26 >,
27 ) -> Box<dyn State<A>> {
28 let (_, outer_progress_rx) = futures_channel::mpsc::channel(1);
29 let (_, inner_progress_rx) = futures_channel::mpsc::channel(1);
30 FutureLoader::<A, Option<PickerOutput>>::new_state(
31 ctx,
32 Box::pin(async move {
33 let mut builder = rfd::AsyncFileDialog::new();
34 if let Some(dir) = start_dir {
35 builder = builder.set_directory(&dir);
36 }
37 let result = if let Some(handle) = builder.pick_file().await {
39 Some((handle.file_name(), handle.read().await))
40 } else {
41 None
42 };
43 let wrap: Box<dyn Send + FnOnce(&A) -> Option<PickerOutput>> =
44 Box::new(move |_: &A| result);
45 Ok(wrap)
46 }),
47 outer_progress_rx,
48 inner_progress_rx,
49 "Waiting for a file to be chosen",
50 on_load,
51 )
52 }
53}
54
55pub struct FileSaver;
56
57pub enum FileSaverContents {
59 String(String),
60 Bytes(Vec<u8>),
61}
62
63impl FileSaver {
64 pub fn new_state<A: 'static + AppLike>(
66 ctx: &mut EventCtx,
67 filename: String,
68 start_dir: Option<String>,
69 write: FileSaverContents,
70 on_load: Box<dyn FnOnce(&mut EventCtx, &mut A, Result<Result<String>>) -> Transition<A>>,
72 ) -> Box<dyn State<A>> {
73 let (_, outer_progress_rx) = futures_channel::mpsc::channel(1);
74 let (_, inner_progress_rx) = futures_channel::mpsc::channel(1);
75 FutureLoader::<A, Result<String>>::new_state(
76 ctx,
77 Box::pin(async move {
78 let mut builder = rfd::AsyncFileDialog::new().set_file_name(&filename);
79 if let Some(dir) = start_dir {
80 builder = builder.set_directory(&dir);
81 }
82
83 #[cfg(not(target_arch = "wasm32"))]
84 let result = if let Some(handle) = builder.save_file().await {
85 let path = handle.path().display().to_string();
86 match write {
88 FileSaverContents::String(string) => fs_err::write(&path, string),
89 FileSaverContents::Bytes(bytes) => fs_err::write(&path, bytes),
90 }
91 .map(|_| path)
92 .map_err(|err| err.into())
93 } else {
94 Err(anyhow!("no file chosen to save"))
95 };
96
97 #[cfg(target_arch = "wasm32")]
98 let result = {
99 let _ = builder;
101
102 match write {
105 FileSaverContents::String(string) => {
106 abstio::write_file(filename.clone(), string)
107 }
108 FileSaverContents::Bytes(_bytes) => {
109 Err(anyhow!("writing binary files on web unsupported"))
112 }
113 }
114 };
115
116 let wrap: Box<dyn Send + FnOnce(&A) -> Result<String>> =
117 Box::new(move |_: &A| result);
118 Ok(wrap)
119 }),
120 outer_progress_rx,
121 inner_progress_rx,
122 "Waiting for a file to be chosen",
123 on_load,
124 )
125 }
126
127 pub fn with_default_messages<A: 'static + AppLike>(
129 ctx: &mut EventCtx,
130 filename: String,
131 start_dir: Option<String>,
132 write: FileSaverContents,
133 ) -> Box<dyn State<A>> {
134 Self::new_state(
135 ctx,
136 filename,
137 start_dir,
138 write,
139 Box::new(|ctx, _, result| {
140 Transition::Replace(match result {
141 Ok(Ok(path)) => PopupMsg::new_state(
142 ctx,
143 "File saved",
144 vec![format!("File saved to {path}")],
145 ),
146 Err(err) | Ok(Err(err)) => {
147 PopupMsg::new_state(ctx, "Save failed", vec![err.to_string()])
148 }
149 })
150 }),
151 )
152 }
153}
154
155pub fn percentage_bar(ctx: &EventCtx, txt: Text, pct_green: f64) -> Widget {
156 let bad_color = Color::RED;
157 let good_color = Color::GREEN;
158
159 let total_width = 450.0;
160 let height = 32.0;
161 let radius = 4.0;
162
163 let mut batch = GeomBatch::new();
164 batch.push(
166 bad_color,
167 Polygon::rounded_rectangle(total_width, height, radius),
168 );
169 if let Some(poly) = Polygon::maybe_rounded_rectangle(pct_green * total_width, height, radius) {
171 batch.push(good_color, poly);
172 }
173 let label = txt.render_autocropped(ctx);
175 let dims = label.get_dims();
176 batch.append(label.translate(10.0, height / 2.0 - dims.height / 2.0));
177 batch.into_widget(ctx)
178}
179
180pub fn cmp_dist(txt: &mut Text, app: &dyn AppLike, dist: Distance, shorter: &str, longer: &str) {
182 match dist.cmp(&Distance::ZERO) {
183 Ordering::Less => {
184 txt.add_line(
185 Line(format!(
186 "{} {}",
187 (-dist).to_string(&app.opts().units),
188 shorter
189 ))
190 .fg(Color::GREEN),
191 );
192 }
193 Ordering::Greater => {
194 txt.add_line(
195 Line(format!("{} {}", dist.to_string(&app.opts().units), longer)).fg(Color::RED),
196 );
197 }
198 Ordering::Equal => {}
199 }
200}
201
202pub fn cmp_duration(
204 txt: &mut Text,
205 app: &dyn AppLike,
206 duration: Duration,
207 shorter: &str,
208 longer: &str,
209) {
210 match duration.cmp(&Duration::ZERO) {
211 Ordering::Less => {
212 txt.add_line(
213 Line(format!(
214 "{} {}",
215 (-duration).to_string(&app.opts().units),
216 shorter
217 ))
218 .fg(Color::GREEN),
219 );
220 }
221 Ordering::Greater => {
222 txt.add_line(
223 Line(format!(
224 "{} {}",
225 duration.to_string(&app.opts().units),
226 longer
227 ))
228 .fg(Color::RED),
229 );
230 }
231 Ordering::Equal => {}
232 }
233}
234
235pub fn cmp_count(txt: &mut Text, before: usize, after: usize) {
237 match after.cmp(&before) {
238 std::cmp::Ordering::Equal => {
239 txt.add_line(Line("same"));
240 }
241 std::cmp::Ordering::Less => {
242 txt.add_appended(vec![
243 Line(prettyprint_usize(before - after)).fg(Color::GREEN),
244 Line(" less"),
245 ]);
246 }
247 std::cmp::Ordering::Greater => {
248 txt.add_appended(vec![
249 Line(prettyprint_usize(after - before)).fg(Color::RED),
250 Line(" more"),
251 ]);
252 }
253 }
254}
255
256pub fn color_for_mode(app: &dyn AppLike, m: TripMode) -> Color {
257 match m {
258 TripMode::Walk => app.cs().unzoomed_pedestrian,
259 TripMode::Bike => app.cs().unzoomed_bike,
260 TripMode::Transit => app.cs().unzoomed_bus,
261 TripMode::Drive => app.cs().unzoomed_car,
262 }
263}
264
265pub fn checkbox_per_mode(
266 ctx: &mut EventCtx,
267 app: &dyn AppLike,
268 current_state: &BTreeSet<TripMode>,
269) -> Widget {
270 let mut filters = Vec::new();
271 for m in TripMode::all() {
272 filters.push(
273 Toggle::colored_checkbox(
274 ctx,
275 m.ongoing_verb(),
276 color_for_mode(app, m),
277 current_state.contains(&m),
278 )
279 .margin_right(24),
280 );
281 }
282 Widget::custom_row(filters)
283}