1use abstutil::prettyprint_usize;
2use geom::Polygon;
3
4use crate::{
5 include_labeled_bytes, Color, ControlState, EventCtx, GeomBatch, Key, Line, Panel, Text,
6 TextExt, Widget,
7};
8
9const ROWS: usize = 8;
10
11pub struct Table<A, T, F> {
12 id: String,
13 data: Vec<T>,
14 label_per_row: Box<dyn Fn(&T) -> String>,
15 columns: Vec<Column<A, T>>,
16 filter: Filter<A, T, F>,
17
18 sort_by: String,
19 descending: bool,
20 skip: usize,
21}
22
23pub enum Col<T> {
24 Static,
25 Sortable(Box<dyn Fn(&mut Vec<&T>)>),
26}
27
28struct Column<A, T> {
29 name: String,
30 render: Box<dyn Fn(&EventCtx, &A, &T) -> GeomBatch>,
31 col: Col<T>,
32}
33
34pub struct Filter<A, T, F> {
35 pub state: F,
36 pub to_controls: Box<dyn Fn(&mut EventCtx, &A, &F) -> Widget>,
37 pub from_controls: Box<dyn Fn(&Panel) -> F>,
38 pub apply: Box<dyn Fn(&F, &T, &A) -> bool>,
39}
40
41impl<A, T, F> Table<A, T, F> {
42 pub fn new(
43 id: impl Into<String>,
44 data: Vec<T>,
45 label_per_row: Box<dyn Fn(&T) -> String>,
46 default_sort_by: &str,
47 filter: Filter<A, T, F>,
48 ) -> Table<A, T, F> {
49 Table {
50 id: id.into(),
51 data,
52 label_per_row,
53 columns: Vec::new(),
54 filter,
55
56 sort_by: default_sort_by.to_string(),
57 descending: true,
58 skip: 0,
59 }
60 }
61
62 pub fn column(
63 &mut self,
64 name: &str,
65 render: Box<dyn Fn(&EventCtx, &A, &T) -> GeomBatch>,
66 col: Col<T>,
67 ) {
68 self.columns.push(Column {
69 name: name.to_string(),
70 render,
71 col,
72 });
73 }
74
75 pub fn replace_render(&self, ctx: &mut EventCtx, app: &A, panel: &mut Panel) {
76 let new_widget = self.render(ctx, app);
77 panel.replace(ctx, &self.id, new_widget);
78 }
79
80 pub fn get_filtered_data(&self, app: &A) -> Vec<&T> {
82 let mut data: Vec<&T> = Vec::new();
83
84 for row in &self.data {
86 if (self.filter.apply)(&self.filter.state, row, app) {
87 data.push(row);
88 }
89 }
90
91 for col in &self.columns {
93 if col.name == self.sort_by {
94 if let Col::Sortable(ref sorter) = col.col {
95 (sorter)(&mut data);
96 break;
97 }
98 }
100 }
101 if self.descending {
102 data.reverse();
103 }
104
105 data
106 }
107
108 pub fn render(&self, ctx: &mut EventCtx, app: &A) -> Widget {
109 let data = self.get_filtered_data(app);
110 let num_filtered = data.len();
111
112 let headers = self
114 .columns
115 .iter()
116 .map(|col| {
117 if self.sort_by == col.name {
118 ctx.style()
119 .btn_outline
120 .icon_text("tmp", &col.name)
121 .image_bytes(if self.descending {
122 include_labeled_bytes!("../../icons/arrow_down.svg")
123 } else {
124 include_labeled_bytes!("../../icons/arrow_up.svg")
125 })
126 .label_first()
127 .build_widget(ctx, &col.name)
128 } else if let Col::Sortable(_) = col.col {
129 ctx.style().btn_outline.text(&col.name).build_def(ctx)
130 } else {
131 Line(&col.name).into_widget(ctx).centered_vert()
132 }
133 })
134 .collect();
135
136 let mut rows = Vec::new();
138 for row in data.into_iter().skip(self.skip).take(ROWS) {
139 rows.push((
140 (self.label_per_row)(row),
141 self.columns
142 .iter()
143 .map(|col| (col.render)(ctx, app, row))
144 .collect(),
145 ));
146 }
147
148 Widget::col(vec![
150 (self.filter.to_controls)(ctx, app, &self.filter.state),
151 render_table(ctx, headers, rows, 0.88 * ctx.canvas.window_width),
152 make_pagination(ctx, num_filtered, self.skip),
153 ])
154 .named(&self.id)
155 .container()
157 }
158
159 pub fn clicked(&mut self, action: &str) -> bool {
161 if action == "previous" {
162 self.skip -= ROWS;
163 return true;
164 }
165 if action == "next" {
166 self.skip += ROWS;
167 return true;
168 }
169 for col in &self.columns {
170 if col.name == action {
171 self.skip = 0;
172 if self.sort_by == action {
173 self.descending = !self.descending;
174 } else {
175 self.sort_by = action.to_string();
176 self.descending = true;
177 }
178 return true;
179 }
180 }
181 false
182 }
183
184 pub fn panel_changed(&mut self, panel: &Panel) {
185 self.filter.state = (self.filter.from_controls)(panel);
186 self.skip = 0;
187 }
188}
189
190impl<A, T> Filter<A, T, ()> {
191 pub fn empty() -> Filter<A, T, ()> {
192 Filter {
193 state: (),
194 to_controls: Box::new(|_, _, _| Widget::nothing()),
195 from_controls: Box::new(|_| ()),
196 apply: Box::new(|_, _, _| true),
197 }
198 }
199}
200
201impl<A, T: 'static, F> Table<A, T, F> {
204 pub fn static_col(&mut self, name: &str, to_str: Box<dyn Fn(&T) -> String>) {
205 self.column(
206 name,
207 Box::new(move |ctx, _, x| Text::from((to_str)(x)).render(ctx)),
208 Col::Static,
209 );
210 }
211}
212
213fn make_pagination(ctx: &mut EventCtx, total: usize, skip: usize) -> Widget {
214 let next = ctx
215 .style()
216 .btn_next()
217 .disabled(skip + 1 + ROWS >= total)
218 .hotkey(Key::RightArrow);
219 let prev = ctx
220 .style()
221 .btn_prev()
222 .disabled(skip == 0)
223 .hotkey(Key::LeftArrow);
224
225 Widget::row(vec![
226 prev.build_widget(ctx, "previous"),
227 format!(
228 "{}-{} of {}",
229 if total > 0 {
230 prettyprint_usize(skip + 1)
231 } else {
232 "0".to_string()
233 },
234 prettyprint_usize((skip + 1 + ROWS).min(total)),
235 prettyprint_usize(total)
236 )
237 .text_widget(ctx)
238 .centered_vert(),
239 next.build_widget(ctx, "next"),
240 ])
241}
242
243pub fn render_table(
246 ctx: &mut EventCtx,
247 headers: Vec<Widget>,
248 rows: Vec<(String, Vec<GeomBatch>)>,
249 total_width: f64,
250) -> Widget {
251 let total_width = total_width;
252 let mut width_per_col: Vec<f64> = headers.iter().map(|w| w.get_width_for_forcing()).collect();
253 for (_, row) in &rows {
254 for (col, width) in row.iter().zip(width_per_col.iter_mut()) {
255 *width = width.max(col.get_dims().width);
256 }
257 }
258 let extra_margin = ((total_width - width_per_col.clone().into_iter().sum::<f64>())
259 / (width_per_col.len() - 1) as f64)
260 .max(0.0);
261
262 let mut col = vec![Widget::custom_row(
263 headers
264 .into_iter()
265 .enumerate()
266 .map(|(idx, w)| {
267 let margin = extra_margin + width_per_col[idx] - w.get_width_for_forcing();
268 if idx == width_per_col.len() - 1 {
269 w.margin_right((margin - extra_margin) as usize)
270 } else {
271 w.margin_right(margin as usize)
272 }
273 })
274 .collect(),
275 )];
276
277 for (label, row) in rows {
279 let mut batch = GeomBatch::new();
280 batch.autocrop_dims = false;
281 let mut x1 = 0.0;
282 for (col, width) in row.into_iter().zip(width_per_col.iter()) {
283 batch.append(col.translate(x1, 0.0));
284 x1 += *width + extra_margin;
285 }
286
287 let rect = Polygon::rectangle(total_width, batch.get_dims().height);
288 let mut hovered = GeomBatch::new();
289 hovered.push(Color::hex("#7C7C7C"), rect.clone());
290 hovered.append(batch.clone());
291
292 col.push(
293 ctx.style()
294 .btn_plain
295 .btn()
296 .custom_batch(batch, ControlState::Default)
297 .custom_batch(hovered, ControlState::Hovered)
298 .no_tooltip()
299 .build_widget(ctx, &label),
300 );
301 }
302
303 Widget::custom_col(col)
304}