Skip to main content

rusty_bubbles/
table.rs

1//! Cleanroom Rust port of upstream Go source file: `table/table.go`
2//! Upstream Target Tag / Version: `v2.1.0`
3//!
4//! <public-docs>
5//! # Table
6//!
7//! A simple table component for Bubble Tea applications.
8//! </public-docs>
9
10use crate::help;
11use crate::key::{self, Binding};
12use crate::viewport;
13use rusty_bubbletea::key::KeyPressMsg;
14use rusty_bubbletea::model::{Cmd, Msg};
15use rusty_lipgloss::{self, Style};
16use rusty_x_ansi;
17
18/// Model defines a state for the table widget.
19#[derive(Debug)]
20pub struct Model {
21    /// The key bindings for the table.
22    pub key_map: KeyMap,
23    /// The help view for the table.
24    pub help: help::Model,
25
26    cols: Vec<Column>,
27    rows: Vec<Row>,
28    cursor: usize,
29    focus: bool,
30    styles: Styles,
31
32    viewport: viewport::Model,
33    start: usize,
34    end: usize,
35}
36
37/// Row represents one line in the table.
38pub type Row = Vec<String>;
39
40/// Column defines the table structure.
41#[derive(Debug, Clone)]
42pub struct Column {
43    /// The title of the column.
44    pub title: String,
45    /// The width of the column.
46    pub width: usize,
47}
48
49/// KeyMap defines keybindings. It satisfies to the help.KeyMap interface,
50/// which is used to render the help menu.
51#[derive(Debug, Clone)]
52pub struct KeyMap {
53    /// LineUp binding.
54    pub line_up: Binding,
55    /// LineDown binding.
56    pub line_down: Binding,
57    /// PageUp binding.
58    pub page_up: Binding,
59    /// PageDown binding.
60    pub page_down: Binding,
61    /// HalfPageUp binding.
62    pub half_page_up: Binding,
63    /// HalfPageDown binding.
64    pub half_page_down: Binding,
65    /// GotoTop binding.
66    pub goto_top: Binding,
67    /// GotoBottom binding.
68    pub goto_bottom: Binding,
69}
70
71/// ShortHelp implements the KeyMap interface.
72impl KeyMap {
73    /// ShortHelp returns the bindings shown in the abbreviated help view.
74    pub fn short_help(&self) -> Vec<Binding> {
75        vec![self.line_up.clone(), self.line_down.clone()]
76    }
77
78    /// FullHelp returns the bindings shown in the full help view.
79    pub fn full_help(&self) -> Vec<Vec<Binding>> {
80        vec![
81            vec![
82                self.line_up.clone(),
83                self.line_down.clone(),
84                self.goto_top.clone(),
85                self.goto_bottom.clone(),
86            ],
87            vec![
88                self.page_up.clone(),
89                self.page_down.clone(),
90                self.half_page_up.clone(),
91                self.half_page_down.clone(),
92            ],
93        ]
94    }
95}
96
97impl help::KeyMap for KeyMap {
98    fn short_help(&self) -> Vec<Binding> {
99        KeyMap::short_help(self)
100    }
101
102    fn full_help(&self) -> Vec<Vec<Binding>> {
103        KeyMap::full_help(self)
104    }
105}
106
107/// DefaultKeyMap returns a default set of keybindings.
108pub fn default_key_map() -> KeyMap {
109    KeyMap {
110        line_up: key::new_binding(vec![
111            key::with_keys(&["up", "k"]),
112            key::with_help("↑/k", "up"),
113        ]),
114        line_down: key::new_binding(vec![
115            key::with_keys(&["down", "j"]),
116            key::with_help("↓/j", "down"),
117        ]),
118        page_up: key::new_binding(vec![
119            key::with_keys(&["b", "pgup"]),
120            key::with_help("b/pgup", "page up"),
121        ]),
122        page_down: key::new_binding(vec![
123            key::with_keys(&["f", "pgdown", "space"]),
124            key::with_help("f/pgdn", "page down"),
125        ]),
126        half_page_up: key::new_binding(vec![
127            key::with_keys(&["u", "ctrl+u"]),
128            key::with_help("u", "½ page up"),
129        ]),
130        half_page_down: key::new_binding(vec![
131            key::with_keys(&["d", "ctrl+d"]),
132            key::with_help("d", "½ page down"),
133        ]),
134        goto_top: key::new_binding(vec![
135            key::with_keys(&["home", "g"]),
136            key::with_help("g/home", "go to start"),
137        ]),
138        goto_bottom: key::new_binding(vec![
139            key::with_keys(&["end", "G"]),
140            key::with_help("G/end", "go to end"),
141        ]),
142    }
143}
144
145/// Styles contains style definitions for this list component. By default,
146/// these values are generated by [`default_styles`].
147#[derive(Debug, Clone)]
148pub struct Styles {
149    /// The style for the header row.
150    pub header: Style,
151    /// The style for cells.
152    pub cell: Style,
153    /// The style for the selected row.
154    pub selected: Style,
155}
156
157/// DefaultStyles returns a set of default style definitions for this table.
158pub fn default_styles() -> Styles {
159    Styles {
160        selected: rusty_lipgloss::new_style().bold(true).foreground("212"),
161        header: rusty_lipgloss::new_style().bold(true).padding(&[0, 1]),
162        cell: rusty_lipgloss::new_style().padding(&[0, 1]),
163    }
164}
165
166/// Option is used to set options in [`new`]. For example:
167///
168/// ```rust
169/// # use rusty_bubbles::table;
170/// let table = table::new(vec![table::with_columns(&[table::Column {
171///     title: "ID".to_string(),
172///     width: 10,
173/// }])]);
174/// ```
175pub type Option = Box<dyn FnOnce(&mut Model)>; // (std::option::Option is used for optionals)
176
177/// New creates a new model for the table widget.
178pub fn new(opts: Vec<Option>) -> Model {
179    let mut m = Model {
180        cursor: 0,
181        viewport: viewport::new(vec![viewport::with_height(20)]),
182
183        key_map: default_key_map(),
184        help: help::new(),
185        styles: default_styles(),
186
187        cols: vec![],
188        rows: vec![],
189        focus: false,
190        start: 0,
191        end: 0,
192    };
193
194    for opt in opts {
195        opt(&mut m);
196    }
197
198    m.update_viewport();
199
200    m
201}
202
203/// WithColumns sets the table columns (headers).
204pub fn with_columns(cols: &[Column]) -> Option {
205    let cols = cols.to_vec();
206    Box::new(move |m: &mut Model| {
207        m.cols = cols;
208    })
209}
210
211/// WithRows sets the table rows (data).
212pub fn with_rows(rows: &[Row]) -> Option {
213    let rows = rows.to_vec();
214    Box::new(move |m: &mut Model| {
215        m.rows = rows;
216    })
217}
218
219/// WithHeight sets the height of the table.
220pub fn with_height(h: usize) -> Option {
221    Box::new(move |m: &mut Model| {
222        let hh = rusty_lipgloss::size::height(&m.headers_view());
223        m.viewport.set_height(h - hh);
224    })
225}
226
227/// WithWidth sets the width of the table.
228pub fn with_width(w: usize) -> Option {
229    Box::new(move |m: &mut Model| {
230        m.viewport.set_width(w);
231    })
232}
233
234/// WithFocused sets the focus state of the table.
235pub fn with_focused(f: bool) -> Option {
236    Box::new(move |m: &mut Model| {
237        m.focus = f;
238    })
239}
240
241/// WithStyles sets the table styles.
242pub fn with_styles(s: Styles) -> Option {
243    Box::new(move |m: &mut Model| {
244        m.styles = s;
245    })
246}
247
248/// WithKeyMap sets the key map.
249pub fn with_key_map(km: KeyMap) -> Option {
250    Box::new(move |m: &mut Model| {
251        m.key_map = km;
252    })
253}
254
255impl Model {
256    /// SetStyles sets the table styles.
257    pub fn set_styles(&mut self, s: Styles) {
258        self.styles = s;
259        self.update_viewport();
260    }
261
262    /// Update is the Bubble Tea update loop.
263    pub fn update(&mut self, msg: &dyn Msg) -> Cmd {
264        if !self.focus {
265            return None;
266        }
267
268        if let Some(m) = msg.as_any().downcast_ref::<KeyPressMsg>() {
269            let k = &m.0;
270            if key::matches(k, std::slice::from_ref(&self.key_map.line_up)) {
271                self.move_up(1);
272            } else if key::matches(k, std::slice::from_ref(&self.key_map.line_down)) {
273                self.move_down(1);
274            } else if key::matches(k, std::slice::from_ref(&self.key_map.page_up)) {
275                self.move_up(self.viewport.height());
276            } else if key::matches(k, std::slice::from_ref(&self.key_map.page_down)) {
277                self.move_down(self.viewport.height());
278            } else if key::matches(k, std::slice::from_ref(&self.key_map.half_page_up)) {
279                self.move_up(self.viewport.height() / 2);
280            } else if key::matches(k, std::slice::from_ref(&self.key_map.half_page_down)) {
281                self.move_down(self.viewport.height() / 2);
282            } else if key::matches(k, std::slice::from_ref(&self.key_map.goto_top)) {
283                self.goto_top();
284            } else if key::matches(k, std::slice::from_ref(&self.key_map.goto_bottom)) {
285                self.goto_bottom();
286            }
287        }
288
289        None
290    }
291
292    /// Focused returns the focus state of the table.
293    pub fn focused(&self) -> bool {
294        self.focus
295    }
296
297    /// Focus focuses the table, allowing the user to move around the rows
298    /// and interact.
299    pub fn focus(&mut self) {
300        self.focus = true;
301        self.update_viewport();
302    }
303
304    /// Blur blurs the table, preventing selection or movement.
305    pub fn blur(&mut self) {
306        self.focus = false;
307        self.update_viewport();
308    }
309
310    /// View renders the component.
311    pub fn view(&self) -> String {
312        self.headers_view() + "\n" + &self.viewport.view()
313    }
314
315    /// HelpView is a helper method for rendering the help menu from the
316    /// keymap. Note that this view is not rendered by default and you must
317    /// call it manually in your application, where applicable.
318    pub fn help_view(&self) -> String {
319        self.help.view(&self.key_map)
320    }
321
322    /// UpdateViewport updates the list content based on the previously
323    /// defined columns and rows.
324    pub fn update_viewport(&mut self) {
325        let mut rendered_rows: Vec<String> = Vec::with_capacity(self.rows.len());
326
327        // Render only rows from: m.cursor-m.viewport.Height to:
328        // m.cursor+m.viewport.Height. Constant runtime, independent of
329        // number of rows in a table. Limits the number of renderedRows to a
330        // maximum of 2*m.viewport.Height.
331        self.start = clamp(
332            self.cursor.saturating_sub(self.viewport.height()),
333            0,
334            self.cursor,
335        );
336        self.end = clamp(
337            self.cursor + self.viewport.height(),
338            self.cursor,
339            self.rows.len(),
340        );
341        for i in self.start..self.end {
342            rendered_rows.push(self.render_row(i));
343        }
344
345        let refs: Vec<&str> = rendered_rows.iter().map(|s| s.as_str()).collect();
346        self.viewport
347            .set_content(&rusty_lipgloss::join::join_vertical(
348                rusty_lipgloss::LEFT,
349                &refs,
350            ));
351    }
352
353    /// SelectedRow returns the selected row.
354    pub fn selected_row(&self) -> std::option::Option<Row> {
355        if self.cursor >= self.rows.len() {
356            return None;
357        }
358
359        Some(self.rows[self.cursor].clone())
360    }
361
362    /// Rows returns the current rows.
363    pub fn rows(&self) -> &[Row] {
364        &self.rows
365    }
366
367    /// Columns returns the current columns.
368    pub fn columns(&self) -> &[Column] {
369        &self.cols
370    }
371
372    /// SetRows sets a new rows state.
373    pub fn set_rows(&mut self, r: &[Row]) {
374        self.rows = r.to_vec();
375
376        if self.cursor > self.rows.len().saturating_sub(1) {
377            self.cursor = self.rows.len().saturating_sub(1);
378        }
379
380        self.update_viewport();
381    }
382
383    /// SetColumns sets a new columns state.
384    pub fn set_columns(&mut self, c: &[Column]) {
385        self.cols = c.to_vec();
386        self.update_viewport();
387    }
388
389    /// SetWidth sets the width of the viewport of the table.
390    pub fn set_width(&mut self, w: usize) {
391        self.viewport.set_width(w);
392        self.update_viewport();
393    }
394
395    /// SetHeight sets the height of the viewport of the table.
396    pub fn set_height(&mut self, h: usize) {
397        let hh = rusty_lipgloss::size::height(&self.headers_view());
398        self.viewport.set_height(h - hh);
399        self.update_viewport();
400    }
401
402    /// Height returns the viewport height of the table.
403    pub fn height(&self) -> usize {
404        self.viewport.height()
405    }
406
407    /// Width returns the viewport width of the table.
408    pub fn width(&self) -> usize {
409        self.viewport.width()
410    }
411
412    /// Cursor returns the index of the selected row.
413    pub fn cursor(&self) -> usize {
414        self.cursor
415    }
416
417    /// SetCursor sets the cursor position in the table.
418    pub fn set_cursor(&mut self, n: usize) {
419        self.cursor = clamp(n, 0, self.rows.len().saturating_sub(1));
420        self.update_viewport();
421    }
422
423    /// MoveUp moves the selection up by any number of rows.
424    /// It can not go above the first row.
425    pub fn move_up(&mut self, n: usize) {
426        // Upstream uses signed ints and clamps to 0; saturating subtraction
427        // mirrors that without overflowing.
428        self.cursor = clamp(
429            self.cursor.saturating_sub(n),
430            0,
431            self.rows.len().saturating_sub(1),
432        );
433
434        let mut offset = self.viewport.y_offset();
435        if self.start == 0 {
436            offset = clamp(offset, 0, self.cursor);
437        } else if self.start < self.viewport.height() {
438            offset = clamp(clamp(offset + n, 0, self.cursor), 0, self.viewport.height());
439        } else if offset >= 1 {
440            offset = clamp(offset + n, 1, self.viewport.height());
441        }
442        self.viewport.set_y_offset(offset);
443        self.update_viewport();
444    }
445
446    /// MoveDown moves the selection down by any number of rows.
447    /// It can not go below the last row.
448    pub fn move_down(&mut self, n: usize) {
449        self.cursor = clamp(self.cursor + n, 0, self.rows.len().saturating_sub(1));
450        self.update_viewport();
451
452        let mut offset = self.viewport.y_offset();
453        if self.end == self.rows.len() && offset > 0 {
454            offset = clamp(offset - n, 1, self.viewport.height());
455        } else if self.cursor > (self.end - self.start) / 2 && offset > 0 {
456            offset = clamp(offset - n, 1, self.cursor);
457        } else if offset > 1 {
458            // no-op
459        } else if self.cursor > offset + self.viewport.height() - 1 {
460            offset = clamp(offset + 1, 0, 1);
461        }
462        self.viewport.set_y_offset(offset);
463    }
464
465    /// GotoTop moves the selection to the first row.
466    pub fn goto_top(&mut self) {
467        let n = self.cursor;
468        self.move_up(n);
469    }
470
471    /// GotoBottom moves the selection to the last row.
472    pub fn goto_bottom(&mut self) {
473        let n = self.rows.len();
474        self.move_down(n);
475    }
476
477    /// FromValues create the table rows from a simple string. It uses `\n`
478    /// by default for getting all the rows and the given separator for the
479    /// fields on each row.
480    pub fn from_values(&mut self, value: &str, separator: &str) {
481        let mut rows: Vec<Row> = vec![];
482        for line in value.split('\n') {
483            let mut r: Row = vec![];
484            for field in line.split(separator) {
485                r.push(field.to_string());
486            }
487            rows.push(r);
488        }
489
490        self.set_rows(&rows);
491    }
492
493    fn headers_view(&self) -> String {
494        let mut s: Vec<String> = Vec::with_capacity(self.cols.len());
495        for col in &self.cols {
496            if col.width == 0 {
497                continue;
498            }
499            let style = rusty_lipgloss::new_style()
500                .width(col.width)
501                .max_width(col.width)
502                .inline(true);
503            let rendered_cell = style.render(&rusty_x_ansi::truncate(&col.title, col.width, "…"));
504            s.push(self.styles.header.clone().render(&rendered_cell));
505        }
506        let refs: Vec<&str> = s.iter().map(|x| x.as_str()).collect();
507        rusty_lipgloss::join::join_horizontal(rusty_lipgloss::TOP, &refs)
508    }
509
510    fn render_row(&self, r: usize) -> String {
511        let mut s: Vec<String> = Vec::with_capacity(self.cols.len());
512        for (i, value) in self.rows[r].iter().enumerate() {
513            if self.cols[i].width == 0 {
514                continue;
515            }
516            let style = rusty_lipgloss::new_style()
517                .width(self.cols[i].width)
518                .max_width(self.cols[i].width)
519                .inline(true);
520            let rendered_cell =
521                style.render(&rusty_x_ansi::truncate(value, self.cols[i].width, "…"));
522            s.push(self.styles.cell.clone().render(&rendered_cell));
523        }
524
525        let refs: Vec<&str> = s.iter().map(|x| x.as_str()).collect();
526        let row = rusty_lipgloss::join::join_horizontal(rusty_lipgloss::TOP, &refs);
527
528        if r == self.cursor {
529            return self.styles.selected.clone().render(&row);
530        }
531
532        row
533    }
534}
535
536fn clamp(v: usize, low: usize, high: usize) -> usize {
537    v.max(low).min(high)
538}