1mod layout;
7mod model;
8mod paint;
9#[cfg(test)]
10mod tests;
11
12use std::sync::Arc;
13
14use crate::event::{Event, MouseButton, MouseKind};
15use crate::geometry::{Rect, Size, clamp_u16};
16use crate::keymap::{Key, KeyChord, Modifiers};
17use crate::widget::{EventCx, MeasureCx, PaintCx, Widget};
18
19use super::IndexMessage;
20use super::row::LEAD;
21use super::rows::{self, RowScroll, Step};
22use layout::Placed;
23pub use model::{Column, ColumnWidth, SortDirection, TableCell, TableRow};
24
25const COLUMN_GAP: u16 = 2;
27
28const MARK: u16 = 2;
30
31type SortMessage<Msg> = Box<dyn Fn(usize, SortDirection) -> Msg>;
33
34pub struct Table<Msg> {
59 columns: Vec<Column>,
60 rows: Arc<[TableRow]>,
61 selected: Option<usize>,
62 checked: Option<Vec<bool>>,
63 sort: Option<(usize, SortDirection)>,
64 empty: String,
65 on_select: Option<IndexMessage<Msg>>,
66 on_activate: Option<IndexMessage<Msg>>,
67 on_toggle: Option<IndexMessage<Msg>>,
68 on_sort: Option<SortMessage<Msg>>,
69}
70
71#[derive(Debug, Default)]
72struct TableMemory {
73 fit: Option<(Arc<[TableRow]>, Vec<u16>)>,
75 column_offset: usize,
77 max_column_offset: usize,
79 more: bool,
81 placed: Vec<Placed>,
82}
83
84impl<Msg: 'static> Table<Msg> {
85 #[must_use]
87 pub fn new(columns: impl IntoIterator<Item = Column>, rows: impl Into<Arc<[TableRow]>>) -> Self {
88 Self {
89 columns: columns.into_iter().collect(),
90 rows: rows.into(),
91 selected: None,
92 checked: None,
93 sort: None,
94 empty: String::new(),
95 on_select: None,
96 on_activate: None,
97 on_toggle: None,
98 on_sort: None,
99 }
100 }
101
102 #[must_use]
104 pub fn selected(mut self, index: Option<usize>) -> Self {
105 self.selected = index;
106 self
107 }
108
109 #[must_use]
111 pub fn checked(mut self, checked: Vec<bool>) -> Self {
112 self.checked = Some(checked);
113 self
114 }
115
116 #[must_use]
119 pub fn sort(mut self, column: usize, direction: SortDirection) -> Self {
120 self.sort = Some((column, direction));
121 self
122 }
123
124 #[must_use]
126 pub fn empty_text(mut self, text: impl Into<String>) -> Self {
127 self.empty = text.into();
128 self
129 }
130
131 #[must_use]
133 pub fn on_select(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
134 self.on_select = Some(Box::new(message));
135 self
136 }
137
138 #[must_use]
140 pub fn on_activate(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
141 self.on_activate = Some(Box::new(message));
142 self
143 }
144
145 #[must_use]
147 pub fn on_toggle(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
148 self.on_toggle = Some(Box::new(message));
149 self
150 }
151
152 #[must_use]
155 pub fn on_sort(mut self, message: impl Fn(usize, SortDirection) -> Msg + 'static) -> Self {
156 self.on_sort = Some(Box::new(message));
157 self
158 }
159
160 fn lead(&self) -> u16 {
161 LEAD + if self.checked.is_some() { MARK } else { 0 }
162 }
163
164 fn select(&self, cx: &mut EventCx<'_, Msg>, index: usize) {
165 if Some(index) != self.selected
166 && let Some(message) = &self.on_select
167 {
168 cx.emit(message(index));
169 }
170 }
171
172 fn activate(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
173 let Some(message) = &self.on_activate else {
174 return false;
175 };
176 cx.memory::<RowScroll>().flashed = Some(index);
177 cx.flash();
178 cx.emit(message(index));
179 true
180 }
181
182 fn toggle(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
183 match (&self.checked, &self.on_toggle) {
184 (Some(_), Some(message)) => {
185 cx.emit(message(index));
186 true
187 }
188 _ => false,
189 }
190 }
191
192 fn request_sort(&self, cx: &mut EventCx<'_, Msg>, column: usize, direction: SortDirection) -> bool {
193 match &self.on_sort {
194 Some(message) if self.columns.get(column).is_some_and(|c| c.sortable) => {
195 cx.emit(message(column, direction));
196 true
197 }
198 _ => false,
199 }
200 }
201
202 fn click_sort(&self, column: usize) -> SortDirection {
204 match self.sort {
205 Some((sorted, direction)) if sorted == column => direction.reversed(),
206 _ => SortDirection::Ascending,
207 }
208 }
209
210 fn scroll_columns(cx: &mut EventCx<'_, Msg>, forward: bool) -> bool {
213 let memory = cx.memory::<TableMemory>();
214 if memory.max_column_offset == 0 {
215 return false;
216 }
217 memory.column_offset = if forward {
218 (memory.column_offset + 1).min(memory.max_column_offset)
219 } else {
220 memory.column_offset.saturating_sub(1)
221 };
222 true
223 }
224
225 fn scroll_arrow_at(cx: &mut EventCx<'_, Msg>, area: Rect, x: i32) -> Option<bool> {
229 let memory = cx.memory::<TableMemory>();
230 if x == area.x && memory.column_offset > 0 {
231 Some(false)
232 } else if x == area.right() - 1 && memory.more {
233 Some(true)
234 } else {
235 None
236 }
237 }
238}
239
240impl<Msg: 'static> Widget<Msg> for Table<Msg> {
241 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
242 let rows = self.rows.len().max(1) + 1;
243 let widths =
244 self.columns.iter().fold(0u16, |sum, c| sum.saturating_add(c.title_width()).saturating_add(COLUMN_GAP));
245 Size::new(widths.saturating_add(self.lead() + 1), clamp_u16(i32::try_from(rows).unwrap_or(i32::MAX)))
246 .min(available)
247 }
248
249 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
250 if area.is_empty() {
251 return;
252 }
253 cx.register_hit(area);
254 let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
255 let total = self.rows.len();
256 let visible = usize::from(body.height);
257 let overflows = total > visible;
258 let lead = self.lead();
259 let room = area.width.saturating_sub(lead + u16::from(overflows));
260
261 let (placed, column_offset, more) = {
262 let memory = cx.memory::<TableMemory>();
263 let widest = if self.columns.iter().any(|c| c.width == ColumnWidth::Fit) {
264 self.widest_cells(memory)
265 } else {
266 vec![0; self.columns.len()]
267 };
268 let (widths, overflow) = self.widths(&widest, room);
269 let arrow = if overflow { 2 - u16::from(overflows) } else { 0 };
273 let room = room.saturating_sub(arrow);
274 let max_offset = if overflow { Self::max_offset(&widths, room) } else { 0 };
275 memory.max_column_offset = max_offset;
276 memory.column_offset = memory.column_offset.min(max_offset);
277 let placed = Self::place(&widths, memory.column_offset, area.x + i32::from(lead), room);
278 let more =
279 placed.last().is_some_and(|last| last.column + 1 < widths.len() || last.width < widths[last.column]);
280 memory.placed.clone_from(&placed);
281 memory.more = more;
282 (placed, memory.column_offset, more)
283 };
284 self.paint_header(cx, area, &placed, column_offset, more);
285
286 if total == 0 {
287 let faint = cx.style("list-header", None, &[]).text();
288 let budget = area.width.saturating_sub(LEAD);
289 cx.text(area.x + i32::from(LEAD), body.y, &self.empty, faint, budget);
290 return;
291 }
292 let focused = cx.is_focused();
293 let pressed = cx.is_pressed();
294 let offset = cx.memory::<RowScroll>().follow(self.selected, total, visible);
295 let row_width = area.width.saturating_sub(u16::from(overflows));
296 for (row, index) in (offset..total).take(visible).enumerate() {
297 let rect = Rect::new(area.x, body.y + i32::try_from(row).unwrap_or(0), row_width, 1);
298 self.paint_row(cx, rect, index, &placed, focused, pressed);
299 }
300 rows::paint_scrollbar(cx, body, total, offset, None);
301 }
302
303 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
304 let area = cx.area();
305 let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
306 let total = self.rows.len();
307 match event {
308 Event::Key(key) => {
309 if let Some(step) = Step::from_key(key) {
310 let Some(target) = step.apply(self.selected, total, usize::from(body.height)) else {
311 return false;
312 };
313 self.select(cx, target);
314 return true;
315 }
316 if key.is_plain(Key::Left) || key.is_plain(Key::Right) {
317 return Self::scroll_columns(cx, key.is_plain(Key::Right));
318 }
319 if key.is_plain(Key::Enter) {
320 return self.selected.is_some_and(|index| self.activate(cx, index));
321 }
322 if key.is_plain(Key::Space) {
323 let Some(index) = self.selected else { return false };
324 return self.toggle(cx, index) || self.activate(cx, index);
325 }
326 let shift_s = KeyChord { key: Key::Char('s'), mods: Modifiers { shift: true, ..Modifiers::default() } };
327 if self.on_sort.is_some() && (key.is_plain(Key::Char('s')) || key.chord == shift_s) {
328 return match (key.chord == shift_s, self.sort) {
329 (true, Some((column, direction))) => self.request_sort(cx, column, direction.reversed()),
330 (true, None) => false,
331 (false, current) => {
332 let start = current.map_or(0, |(column, _)| column + 1);
333 let count = self.columns.len();
334 let next = (0..count)
335 .map(|step| (start + step) % count.max(1))
336 .find(|i| self.columns[*i].sortable);
337 next.is_some_and(|column| self.request_sort(cx, column, SortDirection::Ascending))
338 }
339 };
340 }
341 false
342 }
343 Event::Mouse(mouse) => {
344 if rows::scroll_mouse(cx, mouse, body, total) {
345 return true;
346 }
347 if mouse.kind != MouseKind::Down(MouseButton::Left) {
348 return false;
349 }
350 if mouse.y == area.y {
351 if let Some(forward) = Self::scroll_arrow_at(cx, area, mouse.x) {
352 return Self::scroll_columns(cx, forward);
353 }
354 let placed = cx.memory::<TableMemory>().placed.clone();
355 let Some(place) = placed.iter().find(|place| Self::spans(place, mouse.x)) else {
356 return false;
357 };
358 return self.request_sort(cx, place.column, self.click_sort(place.column));
359 }
360 let offset = cx.memory::<RowScroll>().offset;
361 let Some(index) = usize::try_from(mouse.y - body.y).ok().map(|row| offset + row).filter(|i| *i < total)
362 else {
363 return false;
364 };
365 if self.checked.is_some() && mouse.x < area.x + i32::from(LEAD + MARK) && self.toggle(cx, index) {
366 return true;
367 }
368 self.select(cx, index);
369 self.activate(cx, index);
370 true
371 }
372 _ => false,
373 }
374 }
375
376 fn focusable(&self) -> bool {
377 !self.rows.is_empty()
378 }
379}