widgetry/widgets/
table.rs

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    /// Get all entries, filtered and sorted according to the current settings.
81    pub fn get_filtered_data(&self, app: &A) -> Vec<&T> {
82        let mut data: Vec<&T> = Vec::new();
83
84        // Filter
85        for row in &self.data {
86            if (self.filter.apply)(&self.filter.state, row, app) {
87                data.push(row);
88            }
89        }
90
91        // Sort
92        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                // TODO Error handling
99            }
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        // Render the headers
113        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        // Render data
137        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        // Put together the UI
149        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        // return in separate container in case caller want to apply an outer-name
156        .container()
157    }
158
159    // Recalculate if true
160    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
201// Simpler wrappers than column(). The more generic case exists to allow for icons and non-text
202// things.
203impl<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
243/// Render a table with the specified headers and rows. Each row will be a clickable button with a
244/// string label.
245pub 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    // TODO Maybe can do this now simpler with to_geom
278    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}