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]
231 pub fn multi_select(mut self, selected: &[usize], message: impl Fn(Vec<usize>) -> Msg + 'static) -> Self {
232 self.picking.chosen = selected.to_vec();
233 self.picking.on_choose = Some(Box::new(message));
234 self
235 }
236
237 #[must_use]
242 pub fn box_select(mut self, on: bool) -> Self {
243 self.picking.box_select = on;
244 self
245 }
246
247 #[must_use]
257 pub fn droppable(
258 mut self,
259 message: impl Fn(RowDrop) -> Msg + 'static,
260 accepts: impl Fn(usize) -> bool + 'static,
261 ) -> Self {
262 self.picking.dropping = Some((Box::new(message), Box::new(accepts)));
263 self
264 }
265
266 #[must_use]
270 pub fn on_copy_drop(mut self, message: impl Fn(RowDrop) -> Msg + 'static) -> Self {
271 self.picking.copy_drop = Some(Box::new(message));
272 self
273 }
274
275 fn rows_width(area: Rect, overflows: bool) -> u16 {
277 area.width.saturating_sub(u16::from(overflows))
278 }
279
280 fn menu_event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
284 let area = cx.area();
285 let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
286 let total = self.rows.len();
287 let visible = usize::from(body.height);
288 let overflows = total > visible;
289 row_menu::event(
290 cx,
291 event,
292 self.menu.as_ref(),
293 total,
294 |cx, x, y| {
295 if y < body.y || x >= area.x + i32::from(Self::rows_width(area, overflows)) {
296 return None;
297 }
298 let offset = cx.memory::<RowScroll>().offset;
299 let row = usize::try_from(y - body.y).ok().map(|row| offset + row).filter(|row| *row < total)?;
300 let checked = self.checked.as_ref().is_some_and(|checked| checked.get(row).copied().unwrap_or(false));
301 if self.picking.is_multi() && !self.picking.is_chosen(row) {
302 self.picking.select_one(cx, self, row);
303 } else if !checked && !self.picking.is_multi() {
304 self.select(cx, row);
305 }
306 Some(RowAnchor { row, at: Rect::new(x, y, 1, 1), keyboard: false })
307 },
308 |cx| self.selected_anchor(cx),
309 )
310 }
311
312 fn selected_anchor(&self, cx: &mut EventCx<'_, Msg>) -> Option<RowAnchor> {
315 let area = cx.area();
316 let body_y = area.y + 1;
317 let total = self.rows.len();
318 let visible = usize::from(area.height.saturating_sub(1));
319 let overflows = total > visible;
320 let row = self.selected.filter(|row| *row < total)?;
321 let memory = cx.memory::<RowScroll>();
322 if row < memory.offset {
323 memory.offset = row;
324 } else if visible > 0 && row >= memory.offset + visible {
325 memory.offset = row + 1 - visible;
326 }
327 let y = body_y + i32::try_from(row - memory.offset).unwrap_or(0);
328 let at = Rect::new(area.x, y, Self::rows_width(area, overflows), 1);
329 Some(RowAnchor { row, at, keyboard: true })
330 }
331
332 fn activation_is_menu(&self) -> bool {
334 self.menu_on_activate && self.menu.is_some()
335 }
336
337 fn lead(&self) -> u16 {
338 LEAD + if self.checked.is_some() { MARK } else { 0 }
339 }
340
341 fn select(&self, cx: &mut EventCx<'_, Msg>, index: usize) {
342 if Some(index) != self.selected
343 && let Some(message) = &self.on_select
344 {
345 cx.emit(message(index));
346 }
347 }
348
349 fn activate(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
350 let Some(message) = &self.on_activate else {
351 return false;
352 };
353 cx.memory::<RowScroll>().flashed = Some(index);
354 cx.flash();
355 cx.emit(message(index));
356 true
357 }
358
359 fn toggle(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
360 match (&self.checked, &self.on_toggle) {
361 (Some(_), Some(message)) => {
362 cx.emit(message(index));
363 true
364 }
365 _ => false,
366 }
367 }
368
369 fn request_sort(&self, cx: &mut EventCx<'_, Msg>, column: usize, direction: SortDirection) -> bool {
370 match &self.on_sort {
371 Some(message) if self.columns.get(column).is_some_and(|c| c.sortable) => {
372 cx.emit(message(column, direction));
373 true
374 }
375 _ => false,
376 }
377 }
378
379 fn click_sort(&self, column: usize) -> SortDirection {
381 match self.sort {
382 Some((sorted, direction)) if sorted == column => direction.reversed(),
383 _ => SortDirection::Ascending,
384 }
385 }
386
387 fn scroll_columns(cx: &mut EventCx<'_, Msg>, forward: bool) -> bool {
390 let memory = cx.memory::<TableMemory>();
391 if memory.max_column_offset == 0 {
392 return false;
393 }
394 memory.column_offset = if forward {
395 (memory.column_offset + 1).min(memory.max_column_offset)
396 } else {
397 memory.column_offset.saturating_sub(1)
398 };
399 true
400 }
401
402 fn scroll_arrow_at(cx: &mut EventCx<'_, Msg>, area: Rect, x: i32) -> Option<bool> {
406 let memory = cx.memory::<TableMemory>();
407 if x == area.x && memory.column_offset > 0 {
408 Some(false)
409 } else if x == area.right() - 1 && memory.more {
410 Some(true)
411 } else {
412 None
413 }
414 }
415}
416
417impl<Msg: 'static> Widget<Msg> for Table<Msg> {
418 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
419 let rows = self.rows.len().max(1) + 1;
420 let widths =
421 self.columns.iter().fold(0u16, |sum, c| sum.saturating_add(c.title_width()).saturating_add(COLUMN_GAP));
422 Size::new(widths.saturating_add(self.lead() + 1), clamp_u16(i32::try_from(rows).unwrap_or(i32::MAX)))
423 .min(available)
424 }
425
426 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
427 if area.is_empty() {
428 return;
429 }
430 cx.register_hit(area);
431 let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
432 let total = self.rows.len();
433 let visible = usize::from(body.height);
434 let overflows = total > visible;
435 let lead = self.lead();
436 let room = area.width.saturating_sub(lead + u16::from(overflows));
437
438 let (placed, column_offset, more) = {
439 let memory = cx.memory::<TableMemory>();
440 let widest = if self.columns.iter().any(|c| c.width == ColumnWidth::Fit) {
441 self.widest_cells(memory)
442 } else {
443 vec![0; self.columns.len()]
444 };
445 let (widths, overflow) = self.widths(&widest, room);
446 let arrow = if overflow { 2 - u16::from(overflows) } else { 0 };
450 let room = room.saturating_sub(arrow);
451 let max_offset = if overflow { Self::max_offset(&widths, room) } else { 0 };
452 memory.max_column_offset = max_offset;
453 memory.column_offset = memory.column_offset.min(max_offset);
454 let placed = Self::place(&widths, memory.column_offset, area.x + i32::from(lead), room);
455 let more =
456 placed.last().is_some_and(|last| last.column + 1 < widths.len() || last.width < widths[last.column]);
457 memory.placed.clone_from(&placed);
458 memory.more = more;
459 (placed, memory.column_offset, more)
460 };
461 self.paint_header(cx, area, &placed, column_offset, more);
462
463 if total == 0 {
464 let faint = cx.style("list-header", None, &[]).text();
465 let budget = area.width.saturating_sub(LEAD);
466 cx.text(area.x + i32::from(LEAD), body.y, &self.empty, faint, budget);
467 return;
468 }
469 let focused = cx.is_focused();
470 let pressed = cx.is_pressed();
471 let menu_row = row_menu::open_row(cx, self.menu.as_ref());
474 if menu_row.is_some() {
475 cx.request_overlay(area);
476 }
477 let offset = cx.memory::<RowScroll>().follow(self.selected, total, visible);
478 let row_width = Self::rows_width(area, overflows);
479 let rows_rect = Rect::new(area.x, body.y, row_width, body.height);
480 let target = row_pointer::dragged(cx).and_then(|((x, y), carried)| {
482 let index = offset + usize::try_from(y - body.y).ok()?;
483 (rows_rect.contains(x, y) && index < total && self.picking.takes_drop(&carried, index)).then_some(index)
484 });
485 for (row, index) in (offset..total).take(visible).enumerate() {
486 let rect = Rect::new(area.x, body.y + i32::try_from(row).unwrap_or(0), row_width, 1);
487 self.paint_row(cx, rect, index, &placed, RowPaint { focused, pressed, menu_row, target });
488 }
489 if let Some(drawn) = row_pointer::drawn_box(cx) {
490 select_box::paint(cx, drawn, rows_rect);
491 }
492 rows::paint_scrollbar(cx, body, total, offset, None);
493 }
494
495 fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
496 row_menu::paint(cx, self.menu.as_ref(), anchor);
497 }
498
499 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
500 if self.menu_event(cx, event) {
501 return true;
502 }
503 let area = cx.area();
504 let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
505 let total = self.rows.len();
506 match event {
507 Event::Key(key) => {
508 if let Some(step) = Step::from_key(key) {
509 let Some(target) = step.apply(self.selected, total, usize::from(body.height)) else {
510 return false;
511 };
512 if self.picking.is_multi() {
513 self.picking.select_one(cx, self, target);
514 } else {
515 self.select(cx, target);
516 }
517 return true;
518 }
519 if key.is_plain(Key::Left) || key.is_plain(Key::Right) {
520 return Self::scroll_columns(cx, key.is_plain(Key::Right));
521 }
522 if key.is_plain(Key::Enter) {
523 if self.activation_is_menu() {
524 return self
525 .selected_anchor(cx)
526 .is_some_and(|anchor| row_menu::open_as_action(cx, self.menu.as_ref(), &anchor));
527 }
528 return self.selected.is_some_and(|index| self.activate(cx, index));
529 }
530 if key.is_plain(Key::Space) {
531 let Some(index) = self.selected else { return false };
532 return self.picking.toggle(cx, self, index) || self.toggle(cx, index) || self.activate(cx, index);
533 }
534 let shift_s = KeyChord { key: Key::Char('s'), mods: Modifiers { shift: true, ..Modifiers::default() } };
535 if self.on_sort.is_some() && (key.is_plain(Key::Char('s')) || key.chord == shift_s) {
536 return match (key.chord == shift_s, self.sort) {
537 (true, Some((column, direction))) => self.request_sort(cx, column, direction.reversed()),
538 (true, None) => false,
539 (false, current) => {
540 let start = current.map_or(0, |(column, _)| column + 1);
541 let count = self.columns.len();
542 let next = (0..count)
543 .map(|step| (start + step) % count.max(1))
544 .find(|i| self.columns[*i].sortable);
545 next.is_some_and(|column| self.request_sort(cx, column, SortDirection::Ascending))
546 }
547 };
548 }
549 false
550 }
551 Event::Mouse(mouse) => {
552 if rows::scroll_mouse(cx, mouse, body, total) {
553 return true;
554 }
555 if mouse.kind == MouseKind::Down(MouseButton::Left) {
556 if mouse.y == area.y {
557 if let Some(forward) = Self::scroll_arrow_at(cx, area, mouse.x) {
558 return Self::scroll_columns(cx, forward);
559 }
560 let placed = cx.memory::<TableMemory>().placed.clone();
561 let Some(place) = placed.iter().find(|place| Self::spans(place, mouse.x)) else {
562 return false;
563 };
564 return self.request_sort(cx, place.column, self.click_sort(place.column));
565 }
566 if self.checked.is_some()
567 && mouse.x < area.x + i32::from(LEAD + MARK)
568 && let Spot::Row(index) = self.spot(cx, mouse.x, mouse.y)
569 && self.toggle(cx, index)
570 {
571 return true;
572 }
573 }
574 self.picking.mouse(cx, mouse, self).unwrap_or(false)
575 }
576 _ => false,
577 }
578 }
579
580 fn focusable(&self) -> bool {
581 !self.rows.is_empty()
582 }
583}
584
585impl<Msg: 'static> PickedRows<Msg> for Table<Msg> {
586 fn spot(&self, cx: &mut EventCx<'_, Msg>, x: i32, y: i32) -> Spot {
587 let area = cx.area();
588 let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
589 let total = self.rows.len();
590 let overflows = total > usize::from(body.height);
591 let rows = Rect::new(area.x, body.y, Self::rows_width(area, overflows), body.height);
592 if !rows.contains(x, y) {
593 return Spot::Outside;
594 }
595 let index = cx.memory::<RowScroll>().offset + usize::try_from(y - body.y).unwrap_or(0);
596 if index < total { Spot::Row(index) } else { Spot::Free }
597 }
598
599 fn covered(&self, cx: &mut EventCx<'_, Msg>, rect: Rect) -> Vec<usize> {
600 let area = cx.area();
601 let body = Rect::new(area.x, area.y + 1, area.width, area.height.saturating_sub(1));
602 let offset = cx.memory::<RowScroll>().offset;
603 let (top, bottom) = (rect.y.max(body.y), rect.bottom().min(body.bottom()));
604 (top..bottom)
605 .filter_map(|y| usize::try_from(y - body.y).ok())
606 .map(|row| offset + row)
607 .filter(|index| *index < self.rows.len())
608 .collect()
609 }
610
611 fn cursor(&self) -> Option<usize> {
612 self.selected
613 }
614
615 fn select(&self, cx: &mut EventCx<'_, Msg>, index: usize) {
616 Table::select(self, cx, index);
617 }
618
619 fn open(&self, cx: &mut EventCx<'_, Msg>, index: usize, (x, y): (i32, i32)) {
620 if self.activation_is_menu() {
621 let anchor = RowAnchor { row: index, at: Rect::new(x, y, 1, 1), keyboard: false };
622 row_menu::open_as_action(cx, self.menu.as_ref(), &anchor);
623 } else {
624 self.activate(cx, index);
625 }
626 }
627}