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}