1use crate::component::{Component, EventCx, LayoutCx, MeasureCx};
2use crate::event::Event;
3use crate::geom::{Rect, Size};
4use crate::layout::Constraint;
5use crate::render::RenderCx;
6use crate::style::Style;
7
8#[derive(Default)]
10pub struct TableColumn {
11 pub title: String,
13 pub width: u16,
15 pub align: crate::style::TextAlign,
17}
18
19pub struct Table {
29 columns: Vec<TableColumn>,
30 rows: Vec<Vec<String>>,
31 selected: Option<usize>,
32 scroll_offset: usize,
33 rect: Rect,
35 style: Style,
36 header_style: Style,
37 select_style: Style,
38}
39
40impl Table {
41 pub fn new() -> Self {
43 Self {
44 columns: Vec::new(),
45 rows: Vec::new(),
46 selected: None,
47 scroll_offset: 0,
48 rect: Rect::default(),
49 style: Style::default(),
50 header_style: Style::default().bold(),
51 select_style: Style::default(),
52 }
53 }
54
55 pub fn columns(mut self, columns: Vec<TableColumn>) -> Self {
57 self.columns = columns;
58 self
59 }
60
61 pub fn rows(mut self, rows: Vec<Vec<String>>) -> Self {
63 self.rows = rows;
64 self
65 }
66
67 pub fn style(mut self, style: Style) -> Self {
69 self.style = style;
70 self
71 }
72
73 pub fn header_style(mut self, style: Style) -> Self {
75 self.header_style = style;
76 self
77 }
78
79 pub fn select_style(mut self, style: Style) -> Self {
81 self.select_style = style;
82 self
83 }
84
85 pub fn selected(&self) -> Option<usize> {
87 self.selected
88 }
89
90 pub fn set_selected(&mut self, index: Option<usize>, cx: &mut EventCx) {
92 self.selected = index;
93 cx.invalidate_paint();
94 }
95
96 pub fn row_count(&self) -> usize {
98 self.rows.len()
99 }
100
101 pub fn sort_by_column(&mut self, col: usize, cx: &mut EventCx) {
104 if col < self.columns.len() {
105 self.rows.sort_by(|a, b| {
106 let ca = a.get(col).map(|s| s.as_str()).unwrap_or("");
107 let cb = b.get(col).map(|s| s.as_str()).unwrap_or("");
108 ca.cmp(cb)
109 });
110 cx.invalidate_paint();
111 }
112 }
113
114 pub fn set_column_width(&mut self, col: usize, width: u16, cx: &mut EventCx) {
117 if col < self.columns.len() {
118 self.columns[col].width = width.max(3);
119 cx.invalidate_layout();
120 }
121 }
122
123 pub fn adjust_column_width(&mut self, col: usize, delta: i16, cx: &mut EventCx) {
126 if col < self.columns.len() {
127 let new = (self.columns[col].width as i16 + delta).max(3) as u16;
128 self.columns[col].width = new;
129 cx.invalidate_layout();
130 }
131 }
132
133 fn visible_rows(&self, height: u16) -> usize {
135 let usable = height.saturating_sub(2); usable as usize
137 }
138}
139
140impl Component for Table {
141 fn render(&self, cx: &mut RenderCx) {
142 let columns = &self.columns;
143 if columns.is_empty() {
144 return;
145 }
146
147 let col_count = columns.len();
148 let visible = self.visible_rows(cx.rect.height);
149 let start_row = self.scroll_offset;
150 let end_row = (start_row + visible).min(self.rows.len());
151
152 cx.set_style(self.header_style.clone());
154 for (i, col) in columns.iter().enumerate() {
155 let text = truncate_to_width(&col.title, col.width, col.align);
156 cx.text(&text);
157 if i < col_count - 1 {
158 cx.text("│");
159 }
160 }
161 cx.line("");
162
163 cx.set_style(self.style.clone());
165 for (i, col) in columns.iter().enumerate() {
166 let sep = "─".repeat(col.width as usize);
167 cx.text(&sep);
168 if i < col_count - 1 {
169 cx.text("┼");
170 }
171 }
172 cx.line("");
173
174 for row_idx in start_row..end_row {
176 let is_selected = self.selected == Some(row_idx);
177 if is_selected {
178 cx.set_style(self.select_style.clone());
179 } else {
180 cx.set_style(self.style.clone());
181 }
182
183 let row = &self.rows[row_idx];
184 for (i, col) in columns.iter().enumerate() {
185 let cell_text = row.get(i).map(|s| s.as_str()).unwrap_or("");
186 let text = truncate_to_width(cell_text, col.width, col.align);
187 cx.text(&text);
188 if i < col_count - 1 {
189 cx.text("│");
190 }
191 }
192 cx.line("");
193 }
194 }
195
196 fn measure(&self, _constraint: Constraint, _cx: &mut MeasureCx) -> Size {
197 let col_count = self.columns.len() as u16;
198 if col_count == 0 {
199 return Size { width: 0, height: 0 };
200 }
201
202 let width: u16 = self.columns.iter().map(|c| c.width).sum::<u16>()
203 + col_count.saturating_sub(1); let visible = self.rows.len().min(u16::MAX as usize) as u16;
206 let height = 2u16.saturating_add(visible); Size { width, height }
209 }
210
211 fn event(&mut self, event: &Event, cx: &mut EventCx) {
212 if matches!(event, Event::Focus | Event::Blur | Event::Tick) {
213 return;
214 }
215
216 if self.rows.is_empty() {
217 return;
218 }
219
220 if let Event::Key(key_event) = event {
221 match &key_event.key {
222 crate::event::Key::Up => {
223 let new_idx = match self.selected {
224 Some(i) if i > 0 => i - 1,
225 _ => 0,
226 };
227 self.selected = Some(new_idx);
228 self.scroll_to_visible(new_idx);
229 cx.invalidate_paint();
230 }
231 crate::event::Key::Down => {
232 let max = self.rows.len() - 1;
233 let new_idx = match self.selected {
234 Some(i) if i < max => i + 1,
235 Some(i) => i,
236 None => 0,
237 };
238 self.selected = Some(new_idx);
239 self.scroll_to_visible(new_idx);
240 cx.invalidate_paint();
241 }
242 _ => {}
243 }
244 }
245 }
246
247 fn layout(&mut self, rect: Rect, _cx: &mut LayoutCx) {
248 self.rect = rect;
249 }
250
251 fn focusable(&self) -> bool {
252 false
253 }
254
255 fn style(&self) -> Style {
256 self.style.clone()
257 }
258}
259
260impl Table {
261 fn scroll_to_visible(&mut self, idx: usize) {
262 let visible = self.visible_rows(self.rect.height);
263 if visible == 0 {
264 return;
265 }
266 if idx < self.scroll_offset {
267 self.scroll_offset = idx;
268 } else if idx >= self.scroll_offset + visible {
269 self.scroll_offset = idx.saturating_sub(visible.saturating_sub(1));
270 }
271 }
272}
273
274fn truncate_to_width(text: &str, max_width: u16, align: crate::style::TextAlign) -> String {
278 let mut result = String::new();
279 let mut used: u16 = 0;
280 for ch in text.chars() {
281 let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
282 if used + w > max_width {
283 break;
284 }
285 used += w;
286 result.push(ch);
287 }
288 let padding = max_width.saturating_sub(used);
289 match align {
290 crate::style::TextAlign::Left => {
291 while used < max_width { result.push(' '); used += 1; }
292 }
293 crate::style::TextAlign::Center => {
294 let left = padding / 2;
295 let right = padding - left;
296 let mut s = String::new();
297 for _ in 0..left { s.push(' '); }
298 s.push_str(&result);
299 for _ in 0..right { s.push(' '); }
300 result = s;
301 }
302 crate::style::TextAlign::Right => {
303 let mut s = String::new();
304 for _ in 0..padding { s.push(' '); }
305 s.push_str(&result);
306 result = s;
307 }
308 }
309 result
310}