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::click::Click;
20use super::row::LEAD;
21use super::row_menu::{self, RowAnchor, RowMenuItems};
22use super::row_pointer::{self, PickedRows, Picking, RowDrop, Spot};
23use super::rows::{self, RowScroll, Step};
24use super::select_box;
25use super::{ContextItem, IndexMessage};
26use layout::Placed;
27pub use model::{Column, ColumnWidth, SortDirection, TableCell, TableRow};
28use paint::RowPaint;
29
30const COLUMN_GAP: u16 = 2;
32
33const MARK: u16 = 2;
35
36type SortMessage<Msg> = Box<dyn Fn(usize, SortDirection) -> Msg>;
38
39pub struct Table<Msg> {
77 columns: Vec<Column>,
78 rows: Arc<[TableRow]>,
79 selected: Option<usize>,
80 checked: Option<Vec<bool>>,
81 sort: Option<(usize, SortDirection)>,
82 empty: String,
83 on_select: Option<IndexMessage<Msg>>,
84 on_activate: Option<IndexMessage<Msg>>,
85 on_toggle: Option<IndexMessage<Msg>>,
86 on_sort: Option<SortMessage<Msg>>,
87 menu: Option<RowMenuItems<Msg>>,
88 menu_on_activate: bool,
89 picking: Picking<Msg>,
90}
91
92#[derive(Debug, Default)]
93struct TableMemory {
94 fit: Option<(Arc<[TableRow]>, Vec<u16>)>,
96 column_offset: usize,
98 max_column_offset: usize,
100 more: bool,
102 placed: Vec<Placed>,
103}
104
105impl<Msg: 'static> Table<Msg> {
106 #[must_use]
108 pub fn new(columns: impl IntoIterator<Item = Column>, rows: impl Into<Arc<[TableRow]>>) -> Self {
109 Self {
110 columns: columns.into_iter().collect(),
111 rows: rows.into(),
112 selected: None,
113 checked: None,
114 sort: None,
115 empty: String::new(),
116 on_select: None,
117 on_activate: None,
118 on_toggle: None,
119 on_sort: None,
120 menu: None,
121 menu_on_activate: false,
122 picking: Picking::default(),
123 }
124 }
125
126 #[must_use]
128 pub fn selected(mut self, index: Option<usize>) -> Self {
129 self.selected = index;
130 self
131 }
132
133 #[must_use]
135 pub fn checked(mut self, checked: Vec<bool>) -> Self {
136 self.checked = Some(checked);
137 self
138 }
139
140 #[must_use]
143 pub fn sort(mut self, column: usize, direction: SortDirection) -> Self {
144 self.sort = Some((column, direction));
145 self
146 }
147
148 #[must_use]
150 pub fn empty_text(mut self, text: impl Into<String>) -> Self {
151 self.empty = text.into();
152 self
153 }
154
155 #[must_use]
157 pub fn on_select(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
158 self.on_select = Some(Box::new(message));
159 self
160 }
161
162 #[must_use]
164 pub fn on_activate(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
165 self.on_activate = Some(Box::new(message));
166 self
167 }
168
169 #[must_use]
171 pub fn on_toggle(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
172 self.on_toggle = Some(Box::new(message));
173 self
174 }
175
176 #[must_use]
179 pub fn on_sort(mut self, message: impl Fn(usize, SortDirection) -> Msg + 'static) -> Self {
180 self.on_sort = Some(Box::new(message));
181 self
182 }
183
184 #[must_use]
193 pub fn context_menu(mut self, items: impl Fn(usize) -> Vec<ContextItem<Msg>> + 'static) -> Self {
194 self.menu = Some(Box::new(items));
195 self
196 }
197
198 #[must_use]
207 pub fn menu_on_activate(mut self, on: bool) -> Self {
208 self.menu_on_activate = on;
209 self
210 }
211
212 #[must_use]
216 pub fn activate_on(mut self, click: Click) -> Self {
217 self.picking.activate_on = click;
218 self
219 }
220
221 #[must_use]
232 pub fn multi_select(mut self, selected: &[usize], message: impl Fn(Vec<usize>) -> Msg + 'static) -> Self {
233 self.picking.chosen = selected.to_vec();
234 self.picking.on_choose = Some(Box::new(message));
235 self
236 }
237
238 #[must_use]
243 pub fn box_select(mut self, on: bool) -> Self {
244 self.picking.box_select = on;
245 self
246 }
247
248 #[must_use]
258 pub fn droppable(
259 mut self,
260 message: impl Fn(RowDrop) -> Msg + 'static,
261 accepts: impl Fn(usize) -> bool + 'static,
262 ) -> Self {
263 self.picking.dropping = Some((Box::new(message), Box::new(accepts)));
264 self
265 }
266
267 #[must_use]
271 pub fn on_copy_drop(mut self, message: impl Fn(RowDrop) -> Msg + 'static) -> Self {
272 self.picking.copy_drop = Some(Box::new(message));
273 self
274 }
275
276 fn rows_width(area: Rect, overflows: bool) -> u16 {
278 area.width.saturating_sub(u16::from(overflows))
279 }
280
281 fn menu_event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
285 let area = cx.area();
286 let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
287 let total = self.rows.len();
288 let visible = usize::from(body.height);
289 let overflows = total > visible;
290 row_menu::event(
291 cx,
292 event,
293 self.menu.as_ref(),
294 total,
295 |cx, x, y| {
296 if y < body.y || x >= area.x + i32::from(Self::rows_width(area, overflows)) {
297 return None;
298 }
299 let offset = cx.memory::<RowScroll>().offset;
300 let row = usize::try_from(y - body.y).ok().map(|row| offset + row).filter(|row| *row < total)?;
301 let checked = self.checked.as_ref().is_some_and(|checked| checked.get(row).copied().unwrap_or(false));
302 if self.picking.is_multi() && !self.picking.is_chosen(row) {
303 self.picking.select_one(cx, self, row);
304 } else if !checked && !self.picking.is_multi() {
305 self.select(cx, row);
306 }
307 Some(RowAnchor { row, at: Rect::new(x, y, 1, 1), keyboard: false })
308 },
309 |cx| self.selected_anchor(cx),
310 )
311 }
312
313 fn selected_anchor(&self, cx: &mut EventCx<'_, Msg>) -> Option<RowAnchor> {
316 let area = cx.area();
317 let body_y = area.y + 1;
318 let total = self.rows.len();
319 let visible = usize::from(area.height.saturating_sub(1));
320 let overflows = total > visible;
321 let row = self.selected.filter(|row| *row < total)?;
322 let memory = cx.memory::<RowScroll>();
323 if row < memory.offset {
324 memory.offset = row;
325 } else if visible > 0 && row >= memory.offset + visible {
326 memory.offset = row + 1 - visible;
327 }
328 let y = body_y + i32::try_from(row - memory.offset).unwrap_or(0);
329 let at = Rect::new(area.x, y, Self::rows_width(area, overflows), 1);
330 Some(RowAnchor { row, at, keyboard: true })
331 }
332
333 fn activation_is_menu(&self) -> bool {
335 self.menu_on_activate && self.menu.is_some()
336 }
337
338 fn lead(&self) -> u16 {
339 LEAD + if self.checked.is_some() { MARK } else { 0 }
340 }
341
342 fn select(&self, cx: &mut EventCx<'_, Msg>, index: usize) {
343 if Some(index) != self.selected
344 && let Some(message) = &self.on_select
345 {
346 cx.emit(message(index));
347 }
348 }
349
350 fn activate(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
351 let Some(message) = &self.on_activate else {
352 return false;
353 };
354 cx.memory::<RowScroll>().flashed = Some(index);
355 cx.flash();
356 cx.emit(message(index));
357 true
358 }
359
360 fn toggle(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
361 match (&self.checked, &self.on_toggle) {
362 (Some(_), Some(message)) => {
363 cx.emit(message(index));
364 true
365 }
366 _ => false,
367 }
368 }
369
370 fn request_sort(&self, cx: &mut EventCx<'_, Msg>, column: usize, direction: SortDirection) -> bool {
371 match &self.on_sort {
372 Some(message) if self.columns.get(column).is_some_and(|c| c.sortable) => {
373 cx.emit(message(column, direction));
374 true
375 }
376 _ => false,
377 }
378 }
379
380 fn click_sort(&self, column: usize) -> SortDirection {
382 match self.sort {
383 Some((sorted, direction)) if sorted == column => direction.reversed(),
384 _ => SortDirection::Ascending,
385 }
386 }
387
388 fn scroll_columns(cx: &mut EventCx<'_, Msg>, forward: bool) -> bool {
391 let memory = cx.memory::<TableMemory>();
392 if memory.max_column_offset == 0 {
393 return false;
394 }
395 memory.column_offset = if forward {
396 (memory.column_offset + 1).min(memory.max_column_offset)
397 } else {
398 memory.column_offset.saturating_sub(1)
399 };
400 true
401 }
402
403 fn scroll_arrow_at(cx: &mut EventCx<'_, Msg>, area: Rect, x: i32) -> Option<bool> {
407 let memory = cx.memory::<TableMemory>();
408 if x == area.x && memory.column_offset > 0 {
409 Some(false)
410 } else if x == area.right() - 1 && memory.more {
411 Some(true)
412 } else {
413 None
414 }
415 }
416}
417
418impl<Msg: 'static> Widget<Msg> for Table<Msg> {
419 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
420 let rows = self.rows.len().max(1) + 1;
421 let widths =
422 self.columns.iter().fold(0u16, |sum, c| sum.saturating_add(c.title_width()).saturating_add(COLUMN_GAP));
423 Size::new(widths.saturating_add(self.lead() + 1), clamp_u16(i32::try_from(rows).unwrap_or(i32::MAX)))
424 .min(available)
425 }
426
427 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
428 if area.is_empty() {
429 return;
430 }
431 cx.register_hit(area);
432 let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
433 let total = self.rows.len();
434 let visible = usize::from(body.height);
435 let overflows = total > visible;
436 let lead = self.lead();
437 let room = area.width.saturating_sub(lead + u16::from(overflows));
438
439 let (placed, column_offset, more) = {
440 let memory = cx.memory::<TableMemory>();
441 let widest = if self.columns.iter().any(|c| c.width == ColumnWidth::Fit) {
442 self.widest_cells(memory)
443 } else {
444 vec![0; self.columns.len()]
445 };
446 let (widths, overflow) = self.widths(&widest, room);
447 let arrow = if overflow { 2 - u16::from(overflows) } else { 0 };
451 let room = room.saturating_sub(arrow);
452 let max_offset = if overflow { Self::max_offset(&widths, room) } else { 0 };
453 memory.max_column_offset = max_offset;
454 memory.column_offset = memory.column_offset.min(max_offset);
455 let placed = Self::place(&widths, memory.column_offset, area.x + i32::from(lead), room);
456 let more =
457 placed.last().is_some_and(|last| last.column + 1 < widths.len() || last.width < widths[last.column]);
458 memory.placed.clone_from(&placed);
459 memory.more = more;
460 (placed, memory.column_offset, more)
461 };
462 self.paint_header(cx, area, &placed, column_offset, more);
463
464 if total == 0 {
465 let faint = cx.style("list-header", None, &[]).text();
466 let budget = area.width.saturating_sub(LEAD);
467 cx.text(area.x + i32::from(LEAD), body.y, &self.empty, faint, budget);
468 return;
469 }
470 let focused = cx.is_focused();
471 let pressed = cx.is_pressed();
472 let menu_row = row_menu::open_row(cx, self.menu.as_ref());
475 if menu_row.is_some() {
476 cx.request_overlay(area);
477 }
478 let offset = cx.memory::<RowScroll>().follow(self.selected, total, visible);
479 let row_width = Self::rows_width(area, overflows);
480 let rows_rect = Rect::new(area.x, body.y, row_width, body.height);
481 let target = row_pointer::dragged(cx).and_then(|((x, y), carried)| {
483 let index = offset + usize::try_from(y - body.y).ok()?;
484 (rows_rect.contains(x, y) && index < total && self.picking.takes_drop(&carried, index)).then_some(index)
485 });
486 for (row, index) in (offset..total).take(visible).enumerate() {
487 let rect = Rect::new(area.x, body.y + i32::try_from(row).unwrap_or(0), row_width, 1);
488 self.paint_row(cx, rect, index, &placed, RowPaint { focused, pressed, menu_row, target });
489 }
490 if let Some(drawn) = row_pointer::drawn_box(cx) {
491 select_box::paint(cx, drawn, rows_rect);
492 }
493 rows::paint_scrollbar(cx, body, total, offset, None);
494 }
495
496 fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
497 row_menu::paint(cx, self.menu.as_ref(), anchor);
498 }
499
500 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
501 if self.menu_event(cx, event) {
502 return true;
503 }
504 let area = cx.area();
505 let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
506 let total = self.rows.len();
507 match event {
508 Event::Key(key) => {
509 let page = usize::from(body.height);
510 let extend = |_: &mut EventCx<'_, Msg>, plain: &crate::event::KeyEvent| {
511 rows::SHIFT_STEPS
512 .contains(&plain.chord.key)
513 .then(|| Step::from_key(plain).and_then(|step| step.apply(self.selected, total, page)))
514 };
515 if self.picking.selection_key(cx, key, self, total, extend) {
516 return true;
517 }
518 if let Some(step) = Step::from_key(key) {
519 let Some(target) = step.apply(self.selected, total, page) else {
520 return false;
521 };
522 if self.picking.is_multi() {
523 self.picking.select_one(cx, self, target);
524 } else {
525 self.select(cx, target);
526 }
527 return true;
528 }
529 if key.is_plain(Key::Left) || key.is_plain(Key::Right) {
530 return Self::scroll_columns(cx, key.is_plain(Key::Right));
531 }
532 if key.is_plain(Key::Enter) {
533 if self.activation_is_menu() {
534 return self
535 .selected_anchor(cx)
536 .is_some_and(|anchor| row_menu::open_as_action(cx, self.menu.as_ref(), &anchor));
537 }
538 return self.selected.is_some_and(|index| self.activate(cx, index));
539 }
540 if key.is_plain(Key::Space) {
541 let Some(index) = self.selected else { return false };
542 return self.picking.toggle(cx, self, index) || self.toggle(cx, index) || self.activate(cx, index);
543 }
544 let shift_s = KeyChord { key: Key::Char('s'), mods: Modifiers { shift: true, ..Modifiers::default() } };
545 if self.on_sort.is_some() && (key.is_plain(Key::Char('s')) || key.chord == shift_s) {
546 return match (key.chord == shift_s, self.sort) {
547 (true, Some((column, direction))) => self.request_sort(cx, column, direction.reversed()),
548 (true, None) => false,
549 (false, current) => {
550 let start = current.map_or(0, |(column, _)| column + 1);
551 let count = self.columns.len();
552 let next = (0..count)
553 .map(|step| (start + step) % count.max(1))
554 .find(|i| self.columns[*i].sortable);
555 next.is_some_and(|column| self.request_sort(cx, column, SortDirection::Ascending))
556 }
557 };
558 }
559 false
560 }
561 Event::Mouse(mouse) => {
562 if rows::scroll_mouse(cx, mouse, body, total) {
563 return true;
564 }
565 if mouse.kind == MouseKind::Down(MouseButton::Left) {
566 if mouse.y == area.y {
567 if let Some(forward) = Self::scroll_arrow_at(cx, area, mouse.x) {
568 return Self::scroll_columns(cx, forward);
569 }
570 let placed = cx.memory::<TableMemory>().placed.clone();
571 let Some(place) = placed.iter().find(|place| Self::spans(place, mouse.x)) else {
572 return false;
573 };
574 return self.request_sort(cx, place.column, self.click_sort(place.column));
575 }
576 if self.checked.is_some()
577 && mouse.x < area.x + i32::from(LEAD + MARK)
578 && let Spot::Row(index) = self.spot(cx, mouse.x, mouse.y)
579 && self.toggle(cx, index)
580 {
581 return true;
582 }
583 }
584 self.picking.mouse(cx, mouse, self).unwrap_or(false)
585 }
586 _ => false,
587 }
588 }
589
590 fn focusable(&self) -> bool {
591 !self.rows.is_empty()
592 }
593}
594
595impl<Msg: 'static> PickedRows<Msg> for Table<Msg> {
596 fn spot(&self, cx: &mut EventCx<'_, Msg>, x: i32, y: i32) -> Spot {
597 let area = cx.area();
598 let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
599 let total = self.rows.len();
600 let overflows = total > usize::from(body.height);
601 let rows = Rect::new(area.x, body.y, Self::rows_width(area, overflows), body.height);
602 if !rows.contains(x, y) {
603 return Spot::Outside;
604 }
605 let index = cx.memory::<RowScroll>().offset + usize::try_from(y - body.y).unwrap_or(0);
606 if index < total { Spot::Row(index) } else { Spot::Free }
607 }
608
609 fn covered(&self, cx: &mut EventCx<'_, Msg>, rect: Rect) -> Vec<usize> {
610 let area = cx.area();
611 let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
612 let offset = cx.memory::<RowScroll>().offset;
613 let (top, bottom) = (rect.y.max(body.y), rect.bottom().min(body.bottom()));
614 (top..bottom)
615 .filter_map(|y| usize::try_from(y - body.y).ok())
616 .map(|row| offset + row)
617 .filter(|index| *index < self.rows.len())
618 .collect()
619 }
620
621 fn cursor(&self) -> Option<usize> {
622 self.selected
623 }
624
625 fn select(&self, cx: &mut EventCx<'_, Msg>, index: usize) {
626 Table::select(self, cx, index);
627 }
628
629 fn open(&self, cx: &mut EventCx<'_, Msg>, index: usize, (x, y): (i32, i32)) {
630 if self.activation_is_menu() {
631 let anchor = RowAnchor { row: index, at: Rect::new(x, y, 1, 1), keyboard: false };
632 row_menu::open_as_action(cx, self.menu.as_ref(), &anchor);
633 } else {
634 self.activate(cx, index);
635 }
636 }
637}