Skip to main content

table_demo/
table_demo.rs

1//! Table Demo
2//!
3//! Demonstrates the basic (non-interactive) `Table` widget:
4//! - Fixed column widths + alignment
5//! - Optional borders + header separator
6//! - Horizontal + vertical scrolling via arrow keys (and Vim-ish hjkl)
7//! - Uses `WindowView` to keep the demo contained, and `end_frame()` for stable rendering
8//!
9//! Controls:
10//! - Up/Down or k/j: scroll rows
11//! - Left/Right or h/l: scroll horizontally (cells)
12//! - Esc: reset scroll
13//! - q: quit
14
15use minui::prelude::*;
16
17#[derive(Debug)]
18struct State {
19    scroll_x: u16,
20    scroll_y: u16,
21}
22
23fn main() -> minui::Result<()> {
24    let initial = State {
25        scroll_x: 0,
26        scroll_y: 0,
27    };
28
29    let mut app = App::new(initial)?;
30
31    app.run(
32        // ============================
33        // Update
34        // ============================
35        |state, event| {
36            match event {
37                // Prefer modifier-aware keys first (the current KeyboardHandler emits these for most keys).
38                Event::KeyWithModifiers(k) => {
39                    // Allow Shift+arrows to scroll faster as a tiny UX boost.
40                    let fast = if k.mods.shift { 5 } else { 1 };
41
42                    match k.key {
43                        // Arrow keys (with modifiers)
44                        KeyKind::Up => {
45                            state.scroll_y = state.scroll_y.saturating_sub(fast);
46                            return true;
47                        }
48                        KeyKind::Down => {
49                            state.scroll_y = state.scroll_y.saturating_add(fast);
50                            return true;
51                        }
52                        KeyKind::Left => {
53                            state.scroll_x = state.scroll_x.saturating_sub(fast);
54                            return true;
55                        }
56                        KeyKind::Right => {
57                            state.scroll_x = state.scroll_x.saturating_add(fast);
58                            return true;
59                        }
60
61                        // Vim-ish hjkl (delivered as modifier-aware Char events)
62                        KeyKind::Char('k') => {
63                            state.scroll_y = state.scroll_y.saturating_sub(1);
64                            return true;
65                        }
66                        KeyKind::Char('j') => {
67                            state.scroll_y = state.scroll_y.saturating_add(1);
68                            return true;
69                        }
70                        KeyKind::Char('h') => {
71                            state.scroll_x = state.scroll_x.saturating_sub(1);
72                            return true;
73                        }
74                        KeyKind::Char('l') => {
75                            state.scroll_x = state.scroll_x.saturating_add(1);
76                            return true;
77                        }
78
79                        // Reset scroll on Escape for convenience.
80                        KeyKind::Escape => {
81                            state.scroll_x = 0;
82                            state.scroll_y = 0;
83                            return true;
84                        }
85
86                        // Quit
87                        KeyKind::Char('q') => return false,
88
89                        _ => false,
90                    }
91                }
92
93                // Legacy fallback (some backends/apps may still emit these)
94                Event::Character('q') => return false,
95                Event::Escape => {
96                    // Reset scroll on Esc for convenience (legacy path).
97                    state.scroll_x = 0;
98                    state.scroll_y = 0;
99                    return true;
100                }
101
102                // Vertical scroll (legacy non-modifier events)
103                Event::KeyUp => {
104                    state.scroll_y = state.scroll_y.saturating_sub(1);
105                    return true;
106                }
107                Event::KeyDown => {
108                    state.scroll_y = state.scroll_y.saturating_add(1);
109                    return true;
110                }
111
112                // Horizontal scroll (legacy non-modifier events)
113                Event::KeyLeft => {
114                    state.scroll_x = state.scroll_x.saturating_sub(1);
115                    return true;
116                }
117                Event::KeyRight => {
118                    state.scroll_x = state.scroll_x.saturating_add(1);
119                    return true;
120                }
121
122
123
124                // Fallback for character keys if a backend still emits legacy Character events.
125                Event::Character('k') => {
126                    state.scroll_y = state.scroll_y.saturating_sub(1);
127                    true
128                }
129                Event::Character('j') => {
130                    state.scroll_y = state.scroll_y.saturating_add(1);
131                    true
132                }
133                Event::Character('h') => {
134                    state.scroll_x = state.scroll_x.saturating_sub(1);
135                    true
136                }
137                Event::Character('l') => {
138                    state.scroll_x = state.scroll_x.saturating_add(1);
139                    true
140                }
141
142                // Some terminals map Home/End; MinUI currently doesn't expose those as dedicated events.
143                // If you add them later, you can wire them here.
144                // NOTE: Escape is handled in the modifier-aware path (`KeyKind::Escape`) above.
145
146                _ => true,
147            }
148        },
149        // ============================
150        // Draw
151        // ============================
152        |state, window| {
153            window.clear_cursor_request();
154
155            let (w, h) = window.get_size();
156
157            // Header / instructions
158            window.write_str(
159                0,
160                0,
161                "Table demo — arrows/hjkl to scroll, Shift+arrows scroll faster (if supported), Esc resets, q quits",
162            )?;
163
164            // Keep the demo within a bordered area to show it plays well with layout.
165            let margin: u16 = 2;
166            let top: u16 = 2;
167
168            let demo_w = w.saturating_sub(margin * 2);
169            let demo_h = h.saturating_sub(top + margin);
170
171            // Build columns
172            let columns = vec![
173                TableColumn::new("ID")
174                    .with_width(6)
175                    .with_alignment(Alignment::Right),
176                TableColumn::new("Name").with_width(18),
177                TableColumn::new("Status").with_width(10),
178                TableColumn::new("Email").with_width(28),
179                TableColumn::new("Notes").with_width(40),
180            ];
181
182            // Build rows (a bit wide + many rows to demonstrate scrolling)
183            let mut rows: Vec<Vec<String>> = Vec::new();
184            for i in 0..250u16 {
185                let status = if i % 3 == 0 {
186                    "active"
187                } else if i % 3 == 1 {
188                    "paused"
189                } else {
190                    "disabled"
191                };
192
193                rows.push(vec![
194                    format!("{i}"),
195                    format!("User {i:03}"),
196                    status.to_string(),
197                    format!("user{i:03}@example.com"),
198                    format!(
199                        "This is a longer notes field for row {i:03}. Try horizontal scrolling → to see more text."
200                    ),
201                ]);
202            }
203
204            // Clamp scroll offsets so there is no overscroll past content.
205            //
206            // Note: the Table widget is renderer-only right now, so the demo clamps.
207            //
208            // Clamp vertical scroll to *visible body height* so you can't scroll into empty space
209            // below the last row.
210            let body_h: u16 = demo_h.saturating_sub(2) // table border (top+bottom)
211                .saturating_sub(1) // header row
212                .saturating_sub(1); // header separator row
213            let visible_rows = body_h as usize;
214
215            let max_scroll_y = rows
216                .len()
217                .saturating_sub(visible_rows)
218                .min(rows.len().saturating_sub(1)) as u16;
219
220            state.scroll_y = state.scroll_y.min(max_scroll_y);
221
222            // Compute a conservative maximum horizontal scroll based on total configured table width.
223            // This ensures you can't scroll into empty space past the last column.
224            let total_content_w: u16 = columns
225                .iter()
226                .map(|c| c.width())
227                .sum::<u16>()
228                .saturating_add((columns.len().saturating_sub(1)) as u16); // separators
229            let max_scroll_x = total_content_w.saturating_sub(1);
230            state.scroll_x = state.scroll_x.min(max_scroll_x);
231
232            // Draw the table
233            let table = Table::new(margin, top, demo_w, demo_h)
234                .with_columns(columns)
235                .with_rows(rows)
236                .with_border(true)
237                .with_border_chars(BorderChars::single_line())
238                .with_header(true)
239                .with_header_separator(true)
240                .with_scroll(state.scroll_x, state.scroll_y);
241
242            table.draw(window)?;
243
244            // Small status line
245            let status = format!(
246                "scroll_x={} cells, scroll_y={} rows",
247                state.scroll_x, state.scroll_y
248            );
249            window.write_str(h.saturating_sub(1), 0, &status)?;
250
251            window.end_frame()?;
252            Ok(())
253        },
254    )?;
255
256    Ok(())
257}