1use std::{ops::Range, rc::Rc};
2
3use gpui::{
4 AbsoluteLength, AppContext as _, DefiniteLength, DragMoveEvent, Entity, EntityId, FocusHandle,
5 Length, ListHorizontalSizingBehavior, ListSizingBehavior, ListState, Point, Stateful,
6 UniformListScrollHandle, WeakEntity, list, transparent_black, uniform_list,
7};
8use itertools::intersperse_with;
9
10use crate::{
11 ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
12 ComponentScope, Context, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator,
13 InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
14 ScrollAxes, ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled,
15 StyledExt as _, StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex,
16 px, single_example,
17 table_row::{IntoTableRow as _, TableRow},
18 v_flex,
19};
20
21pub mod table_row;
22#[cfg(test)]
23mod tests;
24
25const RESIZE_COLUMN_WIDTH: f32 = 8.0;
26const RESIZE_DIVIDER_WIDTH: f32 = 1.0;
27
28/// Represents an unchecked table row, which is a vector of elements.
29/// Will be converted into `TableRow<T>` internally
30pub type UncheckedTableRow<T> = Vec<T>;
31
32#[derive(Debug)]
33pub(crate) struct DraggedColumn(pub(crate) usize);
34
35struct UniformListData {
36 render_list_of_rows_fn:
37 Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<UncheckedTableRow<AnyElement>>>,
38 element_id: ElementId,
39 row_count: usize,
40}
41
42struct VariableRowHeightListData {
43 /// Unlike UniformList, this closure renders only single row, allowing each one to have its own height
44 render_row_fn: Box<dyn Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement>>,
45 list_state: ListState,
46 row_count: usize,
47}
48
49enum TableContents {
50 Vec(Vec<TableRow<AnyElement>>),
51 UniformList(UniformListData),
52 VariableRowHeightList(VariableRowHeightListData),
53}
54
55impl TableContents {
56 fn rows_mut(&mut self) -> Option<&mut Vec<TableRow<AnyElement>>> {
57 match self {
58 TableContents::Vec(rows) => Some(rows),
59 TableContents::UniformList(_) => None,
60 TableContents::VariableRowHeightList(_) => None,
61 }
62 }
63
64 fn len(&self) -> usize {
65 match self {
66 TableContents::Vec(rows) => rows.len(),
67 TableContents::UniformList(data) => data.row_count,
68 TableContents::VariableRowHeightList(data) => data.row_count,
69 }
70 }
71
72 fn is_empty(&self) -> bool {
73 self.len() == 0
74 }
75}
76
77pub struct TableInteractionState {
78 pub focus_handle: FocusHandle,
79 pub scroll_handle: UniformListScrollHandle,
80 pub custom_scrollbar: Option<Scrollbars>,
81}
82
83impl TableInteractionState {
84 pub fn new(cx: &mut App) -> Self {
85 Self {
86 focus_handle: cx.focus_handle(),
87 scroll_handle: UniformListScrollHandle::new(),
88 custom_scrollbar: None,
89 }
90 }
91
92 pub fn with_custom_scrollbar(mut self, custom_scrollbar: Scrollbars) -> Self {
93 self.custom_scrollbar = Some(custom_scrollbar);
94 self
95 }
96
97 pub fn scroll_offset(&self) -> Point<Pixels> {
98 self.scroll_handle.offset()
99 }
100
101 pub fn set_scroll_offset(&self, offset: Point<Pixels>) {
102 self.scroll_handle.set_offset(offset);
103 }
104
105 pub fn listener<E: ?Sized>(
106 this: &Entity<Self>,
107 f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static,
108 ) -> impl Fn(&E, &mut Window, &mut App) + 'static {
109 let view = this.downgrade();
110 move |e: &E, window: &mut Window, cx: &mut App| {
111 view.update(cx, |view, cx| f(view, e, window, cx)).ok();
112 }
113 }
114}
115
116/// Renders invisible resize handles overlaid on top of table content.
117///
118/// - Spacer: invisible element that matches the width of table column content
119/// - Divider: contains the actual resize handle that users can drag to resize columns
120///
121/// Structure: [spacer] [divider] [spacer] [divider] [spacer]
122///
123/// Business logic:
124/// 1. Creates spacers matching each column width
125/// 2. Intersperses (inserts) resize handles between spacers (interactive only for resizable columns)
126/// 3. Each handle supports hover highlighting, double-click to reset, and drag to resize
127/// 4. Returns an absolute-positioned overlay that sits on top of table content
128fn render_resize_handles(
129 column_widths: &TableRow<Length>,
130 resizable_columns: &TableRow<TableResizeBehavior>,
131 initial_sizes: &TableRow<DefiniteLength>,
132 columns: Option<Entity<RedistributableColumnsState>>,
133 window: &mut Window,
134 cx: &mut App,
135) -> AnyElement {
136 let spacers = column_widths
137 .as_slice()
138 .iter()
139 .map(|width| base_cell_style(Some(*width)).into_any_element());
140
141 let mut column_ix = 0;
142 let resizable_columns_shared = Rc::new(resizable_columns.clone());
143 let initial_sizes_shared = Rc::new(initial_sizes.clone());
144 let mut resizable_columns_iter = resizable_columns.as_slice().iter();
145
146 let dividers = intersperse_with(spacers, || {
147 let resizable_columns = Rc::clone(&resizable_columns_shared);
148 let initial_sizes = Rc::clone(&initial_sizes_shared);
149 window.with_id(column_ix, |window| {
150 let mut resize_divider = div()
151 .id(column_ix)
152 .relative()
153 .top_0()
154 .w(px(RESIZE_DIVIDER_WIDTH))
155 .h_full()
156 .bg(cx.theme().colors().border.opacity(0.8));
157
158 let mut resize_handle = div()
159 .id("column-resize-handle")
160 .absolute()
161 .left_neg_0p5()
162 .w(px(RESIZE_COLUMN_WIDTH))
163 .h_full();
164
165 if resizable_columns_iter
166 .next()
167 .is_some_and(TableResizeBehavior::is_resizable)
168 {
169 let hovered = window.use_state(cx, |_window, _cx| false);
170
171 resize_divider = resize_divider.when(*hovered.read(cx), |div| {
172 div.bg(cx.theme().colors().border_focused)
173 });
174
175 resize_handle = resize_handle
176 .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered))
177 .cursor_col_resize()
178 .when_some(columns.clone(), |this, columns| {
179 this.on_click(move |event, window, cx| {
180 if event.click_count() >= 2 {
181 columns.update(cx, |columns, _| {
182 columns.on_double_click(
183 column_ix,
184 &initial_sizes,
185 &resizable_columns,
186 window,
187 );
188 })
189 }
190
191 cx.stop_propagation();
192 })
193 })
194 .on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| {
195 cx.new(|_cx| gpui::Empty)
196 })
197 }
198
199 column_ix += 1;
200 resize_divider.child(resize_handle).into_any_element()
201 })
202 });
203
204 h_flex()
205 .id("resize-handles")
206 .absolute()
207 .inset_0()
208 .w_full()
209 .children(dividers)
210 .into_any_element()
211}
212
213#[derive(Debug, Copy, Clone, PartialEq)]
214pub enum TableResizeBehavior {
215 None,
216 Resizable,
217 MinSize(f32),
218}
219
220impl TableResizeBehavior {
221 pub fn is_resizable(&self) -> bool {
222 *self != TableResizeBehavior::None
223 }
224
225 pub fn min_size(&self) -> Option<f32> {
226 match self {
227 TableResizeBehavior::None => None,
228 TableResizeBehavior::Resizable => Some(0.05),
229 TableResizeBehavior::MinSize(min_size) => Some(*min_size),
230 }
231 }
232}
233
234pub enum ColumnWidthConfig {
235 /// Static column widths (no resize handles).
236 Static {
237 widths: StaticColumnWidths,
238 /// Controls widths of the whole table.
239 table_width: Option<DefiniteLength>,
240 },
241 /// Redistributable columns — dragging redistributes the fixed available space
242 /// among columns without changing the overall table width.
243 Redistributable {
244 columns_state: Entity<RedistributableColumnsState>,
245 table_width: Option<DefiniteLength>,
246 },
247}
248
249pub enum StaticColumnWidths {
250 /// All columns share space equally (flex-1 / Length::Auto).
251 Auto,
252 /// Each column has a specific width.
253 Explicit(TableRow<DefiniteLength>),
254}
255
256impl ColumnWidthConfig {
257 /// Auto-width columns, auto-size table.
258 pub fn auto() -> Self {
259 ColumnWidthConfig::Static {
260 widths: StaticColumnWidths::Auto,
261 table_width: None,
262 }
263 }
264
265 /// Redistributable columns with no fixed table width.
266 pub fn redistributable(columns_state: Entity<RedistributableColumnsState>) -> Self {
267 ColumnWidthConfig::Redistributable {
268 columns_state,
269 table_width: None,
270 }
271 }
272
273 /// Auto-width columns, fixed table width.
274 pub fn auto_with_table_width(width: impl Into<DefiniteLength>) -> Self {
275 ColumnWidthConfig::Static {
276 widths: StaticColumnWidths::Auto,
277 table_width: Some(width.into()),
278 }
279 }
280
281 /// Column widths for rendering.
282 pub fn widths_to_render(&self, cx: &App) -> Option<TableRow<Length>> {
283 match self {
284 ColumnWidthConfig::Static {
285 widths: StaticColumnWidths::Auto,
286 ..
287 } => None,
288 ColumnWidthConfig::Static {
289 widths: StaticColumnWidths::Explicit(widths),
290 ..
291 } => Some(widths.map_cloned(Length::Definite)),
292 ColumnWidthConfig::Redistributable {
293 columns_state: entity,
294 ..
295 } => {
296 let state = entity.read(cx);
297 Some(state.preview_widths.map_cloned(Length::Definite))
298 }
299 }
300 }
301
302 /// Table-level width.
303 pub fn table_width(&self) -> Option<Length> {
304 match self {
305 ColumnWidthConfig::Static { table_width, .. }
306 | ColumnWidthConfig::Redistributable { table_width, .. } => {
307 table_width.map(Length::Definite)
308 }
309 }
310 }
311
312 /// ListHorizontalSizingBehavior for uniform_list.
313 pub fn list_horizontal_sizing(&self) -> ListHorizontalSizingBehavior {
314 match self.table_width() {
315 Some(_) => ListHorizontalSizingBehavior::Unconstrained,
316 None => ListHorizontalSizingBehavior::FitList,
317 }
318 }
319
320 /// Render resize handles overlay if applicable.
321 pub fn render_resize_handles(&self, window: &mut Window, cx: &mut App) -> Option<AnyElement> {
322 match self {
323 ColumnWidthConfig::Redistributable {
324 columns_state: entity,
325 ..
326 } => {
327 let (column_widths, resize_behavior, initial_widths) = {
328 let state = entity.read(cx);
329 (
330 state.preview_widths.map_cloned(Length::Definite),
331 state.resize_behavior.clone(),
332 state.initial_widths.clone(),
333 )
334 };
335 Some(render_resize_handles(
336 &column_widths,
337 &resize_behavior,
338 &initial_widths,
339 Some(entity.clone()),
340 window,
341 cx,
342 ))
343 }
344 _ => None,
345 }
346 }
347
348 /// Returns info needed for header double-click-to-reset, if applicable.
349 pub fn header_resize_info(&self, cx: &App) -> Option<HeaderResizeInfo> {
350 match self {
351 ColumnWidthConfig::Redistributable { columns_state, .. } => {
352 let state = columns_state.read(cx);
353 Some(HeaderResizeInfo {
354 columns_state: columns_state.downgrade(),
355 resize_behavior: state.resize_behavior.clone(),
356 initial_widths: state.initial_widths.clone(),
357 })
358 }
359 _ => None,
360 }
361 }
362}
363
364#[derive(Clone)]
365pub struct HeaderResizeInfo {
366 pub columns_state: WeakEntity<RedistributableColumnsState>,
367 pub resize_behavior: TableRow<TableResizeBehavior>,
368 pub initial_widths: TableRow<DefiniteLength>,
369}
370
371pub struct RedistributableColumnsState {
372 pub(crate) initial_widths: TableRow<DefiniteLength>,
373 pub(crate) committed_widths: TableRow<DefiniteLength>,
374 pub(crate) preview_widths: TableRow<DefiniteLength>,
375 pub(crate) resize_behavior: TableRow<TableResizeBehavior>,
376 pub(crate) cached_table_width: Pixels,
377}
378
379impl RedistributableColumnsState {
380 pub fn new(
381 cols: usize,
382 initial_widths: UncheckedTableRow<impl Into<DefiniteLength>>,
383 resize_behavior: UncheckedTableRow<TableResizeBehavior>,
384 ) -> Self {
385 let widths: TableRow<DefiniteLength> = initial_widths
386 .into_iter()
387 .map(Into::into)
388 .collect::<Vec<_>>()
389 .into_table_row(cols);
390 Self {
391 initial_widths: widths.clone(),
392 committed_widths: widths.clone(),
393 preview_widths: widths,
394 resize_behavior: resize_behavior.into_table_row(cols),
395 cached_table_width: Default::default(),
396 }
397 }
398
399 pub fn cols(&self) -> usize {
400 self.committed_widths.cols()
401 }
402
403 pub fn initial_widths(&self) -> &TableRow<DefiniteLength> {
404 &self.initial_widths
405 }
406
407 pub fn resize_behavior(&self) -> &TableRow<TableResizeBehavior> {
408 &self.resize_behavior
409 }
410
411 fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 {
412 match length {
413 DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width,
414 DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => {
415 rems_width.to_pixels(rem_size) / bounds_width
416 }
417 DefiniteLength::Fraction(fraction) => *fraction,
418 }
419 }
420
421 pub(crate) fn on_double_click(
422 &mut self,
423 double_click_position: usize,
424 initial_sizes: &TableRow<DefiniteLength>,
425 resize_behavior: &TableRow<TableResizeBehavior>,
426 window: &mut Window,
427 ) {
428 let bounds_width = self.cached_table_width;
429 let rem_size = window.rem_size();
430 let initial_sizes =
431 initial_sizes.map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
432 let widths = self
433 .committed_widths
434 .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
435
436 let updated_widths = Self::reset_to_initial_size(
437 double_click_position,
438 widths,
439 initial_sizes,
440 resize_behavior,
441 );
442 self.committed_widths = updated_widths.map(DefiniteLength::Fraction);
443 self.preview_widths = self.committed_widths.clone();
444 }
445
446 pub(crate) fn reset_to_initial_size(
447 col_idx: usize,
448 mut widths: TableRow<f32>,
449 initial_sizes: TableRow<f32>,
450 resize_behavior: &TableRow<TableResizeBehavior>,
451 ) -> TableRow<f32> {
452 let diff = initial_sizes[col_idx] - widths[col_idx];
453
454 let left_diff =
455 initial_sizes[..col_idx].iter().sum::<f32>() - widths[..col_idx].iter().sum::<f32>();
456 let right_diff = initial_sizes[col_idx + 1..].iter().sum::<f32>()
457 - widths[col_idx + 1..].iter().sum::<f32>();
458
459 let go_left_first = if diff < 0.0 {
460 left_diff > right_diff
461 } else {
462 left_diff < right_diff
463 };
464
465 if !go_left_first {
466 let diff_remaining =
467 Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, 1);
468
469 if diff_remaining != 0.0 && col_idx > 0 {
470 Self::propagate_resize_diff(
471 diff_remaining,
472 col_idx,
473 &mut widths,
474 resize_behavior,
475 -1,
476 );
477 }
478 } else {
479 let diff_remaining =
480 Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, -1);
481
482 if diff_remaining != 0.0 {
483 Self::propagate_resize_diff(
484 diff_remaining,
485 col_idx,
486 &mut widths,
487 resize_behavior,
488 1,
489 );
490 }
491 }
492
493 widths
494 }
495
496 pub(crate) fn on_drag_move(
497 &mut self,
498 drag_event: &DragMoveEvent<DraggedColumn>,
499 window: &mut Window,
500 cx: &mut Context<Self>,
501 ) {
502 let drag_position = drag_event.event.position;
503 let bounds = drag_event.bounds;
504
505 let mut col_position = 0.0;
506 let rem_size = window.rem_size();
507 let bounds_width = bounds.right() - bounds.left();
508 let col_idx = drag_event.drag(cx).0;
509
510 let divider_width = Self::get_fraction(
511 &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_DIVIDER_WIDTH))),
512 bounds_width,
513 rem_size,
514 );
515
516 let mut widths = self
517 .committed_widths
518 .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
519
520 for length in widths[0..=col_idx].iter() {
521 col_position += length + divider_width;
522 }
523
524 let mut total_length_ratio = col_position;
525 for length in widths[col_idx + 1..].iter() {
526 total_length_ratio += length;
527 }
528 let cols = self.resize_behavior.cols();
529 total_length_ratio += (cols - 1 - col_idx) as f32 * divider_width;
530
531 let drag_fraction = (drag_position.x - bounds.left()) / bounds_width;
532 let drag_fraction = drag_fraction * total_length_ratio;
533 let diff = drag_fraction - col_position - divider_width / 2.0;
534
535 Self::drag_column_handle(diff, col_idx, &mut widths, &self.resize_behavior);
536
537 self.preview_widths = widths.map(DefiniteLength::Fraction);
538 }
539
540 pub(crate) fn drag_column_handle(
541 diff: f32,
542 col_idx: usize,
543 widths: &mut TableRow<f32>,
544 resize_behavior: &TableRow<TableResizeBehavior>,
545 ) {
546 if diff > 0.0 {
547 Self::propagate_resize_diff(diff, col_idx, widths, resize_behavior, 1);
548 } else {
549 Self::propagate_resize_diff(-diff, col_idx + 1, widths, resize_behavior, -1);
550 }
551 }
552
553 pub(crate) fn propagate_resize_diff(
554 diff: f32,
555 col_idx: usize,
556 widths: &mut TableRow<f32>,
557 resize_behavior: &TableRow<TableResizeBehavior>,
558 direction: i8,
559 ) -> f32 {
560 let mut diff_remaining = diff;
561 if resize_behavior[col_idx].min_size().is_none() {
562 return diff;
563 }
564
565 let step_right;
566 let step_left;
567 if direction < 0 {
568 step_right = 0;
569 step_left = 1;
570 } else {
571 step_right = 1;
572 step_left = 0;
573 }
574 if col_idx == 0 && direction < 0 {
575 return diff;
576 }
577 let mut curr_column = col_idx + step_right - step_left;
578
579 while diff_remaining != 0.0 && curr_column < widths.cols() {
580 let Some(min_size) = resize_behavior[curr_column].min_size() else {
581 if curr_column == 0 {
582 break;
583 }
584 curr_column -= step_left;
585 curr_column += step_right;
586 continue;
587 };
588
589 let curr_width = widths[curr_column] - diff_remaining;
590 widths[curr_column] = curr_width;
591
592 if min_size > curr_width {
593 diff_remaining = min_size - curr_width;
594 widths[curr_column] = min_size;
595 } else {
596 diff_remaining = 0.0;
597 break;
598 }
599 if curr_column == 0 {
600 break;
601 }
602 curr_column -= step_left;
603 curr_column += step_right;
604 }
605 widths[col_idx] = widths[col_idx] + (diff - diff_remaining);
606
607 diff_remaining
608 }
609}
610
611/// A table component
612#[derive(RegisterComponent, IntoElement)]
613pub struct Table {
614 striped: bool,
615 show_row_borders: bool,
616 show_row_hover: bool,
617 headers: Option<TableRow<AnyElement>>,
618 rows: TableContents,
619 interaction_state: Option<WeakEntity<TableInteractionState>>,
620 column_width_config: ColumnWidthConfig,
621 map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
622 use_ui_font: bool,
623 empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
624 /// The number of columns in the table. Used to assert column numbers in `TableRow` collections
625 cols: usize,
626 disable_base_cell_style: bool,
627}
628
629impl Table {
630 /// Creates a new table with the specified number of columns.
631 pub fn new(cols: usize) -> Self {
632 Self {
633 cols,
634 striped: false,
635 show_row_borders: true,
636 show_row_hover: true,
637 headers: None,
638 rows: TableContents::Vec(Vec::new()),
639 interaction_state: None,
640 map_row: None,
641 use_ui_font: true,
642 empty_table_callback: None,
643 disable_base_cell_style: false,
644 column_width_config: ColumnWidthConfig::auto(),
645 }
646 }
647
648 /// Disables based styling of row cell (paddings, text ellipsis, nowrap, etc), keeping width settings
649 ///
650 /// Doesn't affect base style of header cell.
651 /// Doesn't remove overflow-hidden
652 pub fn disable_base_style(mut self) -> Self {
653 self.disable_base_cell_style = true;
654 self
655 }
656
657 /// Enables uniform list rendering.
658 /// The provided function will be passed directly to the `uniform_list` element.
659 /// Therefore, if this method is called, any calls to [`Table::row`] before or after
660 /// this method is called will be ignored.
661 pub fn uniform_list(
662 mut self,
663 id: impl Into<ElementId>,
664 row_count: usize,
665 render_item_fn: impl Fn(
666 Range<usize>,
667 &mut Window,
668 &mut App,
669 ) -> Vec<UncheckedTableRow<AnyElement>>
670 + 'static,
671 ) -> Self {
672 self.rows = TableContents::UniformList(UniformListData {
673 element_id: id.into(),
674 row_count,
675 render_list_of_rows_fn: Box::new(render_item_fn),
676 });
677 self
678 }
679
680 /// Enables rendering of tables with variable row heights, allowing each row to have its own height.
681 ///
682 /// This mode is useful for displaying content such as CSV data or multiline cells, where rows may not have uniform heights.
683 /// It is generally slower than [`Table::uniform_list`] due to the need to measure each row individually, but it provides correct layout for non-uniform or multiline content.
684 ///
685 /// # Parameters
686 /// - `row_count`: The total number of rows in the table.
687 /// - `list_state`: The [`ListState`] used for managing scroll position and virtualization. This must be initialized and managed by the caller, and should be kept in sync with the number of rows.
688 /// - `render_row_fn`: A closure that renders a single row, given the row index, a mutable reference to [`Window`], and a mutable reference to [`App`]. It should return an array of [`AnyElement`]s, one for each column.
689 pub fn variable_row_height_list(
690 mut self,
691 row_count: usize,
692 list_state: ListState,
693 render_row_fn: impl Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement> + 'static,
694 ) -> Self {
695 self.rows = TableContents::VariableRowHeightList(VariableRowHeightListData {
696 render_row_fn: Box::new(render_row_fn),
697 list_state,
698 row_count,
699 });
700 self
701 }
702
703 /// Enables row striping (alternating row colors)
704 pub fn striped(mut self) -> Self {
705 self.striped = true;
706 self
707 }
708
709 /// Hides the border lines between rows
710 pub fn hide_row_borders(mut self) -> Self {
711 self.show_row_borders = false;
712 self
713 }
714
715 /// Sets a fixed table width with auto column widths.
716 ///
717 /// This is a shorthand for `.width_config(ColumnWidthConfig::auto_with_table_width(width))`.
718 /// For resizable columns or explicit column widths, use [`Table::width_config`] directly.
719 pub fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
720 self.column_width_config = ColumnWidthConfig::auto_with_table_width(width);
721 self
722 }
723
724 /// Sets the column width configuration for the table.
725 pub fn width_config(mut self, config: ColumnWidthConfig) -> Self {
726 self.column_width_config = config;
727 self
728 }
729
730 /// Enables interaction (primarily scrolling) with the table.
731 ///
732 /// Vertical scrolling will be enabled by default if the table is taller than its container.
733 ///
734 /// Horizontal scrolling will only be enabled if a table width is set via [`ColumnWidthConfig`],
735 /// otherwise the list will always shrink the table columns to fit their contents.
736 pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
737 self.interaction_state = Some(interaction_state.downgrade());
738 self
739 }
740
741 pub fn header(mut self, headers: UncheckedTableRow<impl IntoElement>) -> Self {
742 self.headers = Some(
743 headers
744 .into_table_row(self.cols)
745 .map(IntoElement::into_any_element),
746 );
747 self
748 }
749
750 pub fn row(mut self, items: UncheckedTableRow<impl IntoElement>) -> Self {
751 if let Some(rows) = self.rows.rows_mut() {
752 rows.push(
753 items
754 .into_table_row(self.cols)
755 .map(IntoElement::into_any_element),
756 );
757 }
758 self
759 }
760
761 pub fn no_ui_font(mut self) -> Self {
762 self.use_ui_font = false;
763 self
764 }
765
766 pub fn map_row(
767 mut self,
768 callback: impl Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement + 'static,
769 ) -> Self {
770 self.map_row = Some(Rc::new(callback));
771 self
772 }
773
774 /// Hides the default hover background on table rows.
775 /// Use this when you want to handle row hover styling manually via `map_row`.
776 pub fn hide_row_hover(mut self) -> Self {
777 self.show_row_hover = false;
778 self
779 }
780
781 /// Provide a callback that is invoked when the table is rendered without any rows
782 pub fn empty_table_callback(
783 mut self,
784 callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
785 ) -> Self {
786 self.empty_table_callback = Some(Rc::new(callback));
787 self
788 }
789}
790
791fn base_cell_style(width: Option<Length>) -> Div {
792 div()
793 .px_1p5()
794 .when_some(width, |this, width| this.w(width))
795 .when(width.is_none(), |this| this.flex_1())
796 .whitespace_nowrap()
797 .text_ellipsis()
798 .overflow_hidden()
799}
800
801fn base_cell_style_text(width: Option<Length>, use_ui_font: bool, cx: &App) -> Div {
802 base_cell_style(width).when(use_ui_font, |el| el.text_ui(cx))
803}
804
805pub fn render_table_row(
806 row_index: usize,
807 items: TableRow<impl IntoElement>,
808 table_context: TableRenderContext,
809 window: &mut Window,
810 cx: &mut App,
811) -> AnyElement {
812 let is_striped = table_context.striped;
813 let is_last = row_index == table_context.total_row_count - 1;
814 let bg = if row_index % 2 == 1 && is_striped {
815 Some(cx.theme().colors().text.opacity(0.05))
816 } else {
817 None
818 };
819 let cols = items.cols();
820 let column_widths = table_context
821 .column_widths
822 .map_or(vec![None; cols].into_table_row(cols), |widths| {
823 widths.map(Some)
824 });
825
826 let mut row = div()
827 // NOTE: `h_flex()` sneakily applies `items_center()` which is not default behavior for div element.
828 // Applying `.flex().flex_row()` manually to overcome that
829 .flex()
830 .flex_row()
831 .id(("table_row", row_index))
832 .size_full()
833 .when_some(bg, |row, bg| row.bg(bg))
834 .when(table_context.show_row_hover, |row| {
835 row.hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.6)))
836 })
837 .when(!is_striped && table_context.show_row_borders, |row| {
838 row.border_b_1()
839 .border_color(transparent_black())
840 .when(!is_last, |row| row.border_color(cx.theme().colors().border))
841 });
842
843 row = row.children(
844 items
845 .map(IntoElement::into_any_element)
846 .into_vec()
847 .into_iter()
848 .zip(column_widths.into_vec())
849 .map(|(cell, width)| {
850 if table_context.disable_base_cell_style {
851 div()
852 .when_some(width, |this, width| this.w(width))
853 .when(width.is_none(), |this| this.flex_1())
854 .overflow_hidden()
855 .child(cell)
856 } else {
857 base_cell_style_text(width, table_context.use_ui_font, cx)
858 .px_1()
859 .py_0p5()
860 .child(cell)
861 }
862 }),
863 );
864
865 let row = if let Some(map_row) = table_context.map_row {
866 map_row((row_index, row), window, cx)
867 } else {
868 row.into_any_element()
869 };
870
871 div().size_full().child(row).into_any_element()
872}
873
874pub fn render_table_header(
875 headers: TableRow<impl IntoElement>,
876 table_context: TableRenderContext,
877 resize_info: Option<HeaderResizeInfo>,
878 entity_id: Option<EntityId>,
879 cx: &mut App,
880) -> impl IntoElement {
881 let cols = headers.cols();
882 let column_widths = table_context
883 .column_widths
884 .map_or(vec![None; cols].into_table_row(cols), |widths| {
885 widths.map(Some)
886 });
887
888 let element_id = entity_id
889 .map(|entity| entity.to_string())
890 .unwrap_or_default();
891
892 let shared_element_id: SharedString = format!("table-{}", element_id).into();
893
894 div()
895 .flex()
896 .flex_row()
897 .items_center()
898 .w_full()
899 .border_b_1()
900 .border_color(cx.theme().colors().border)
901 .children(
902 headers
903 .into_vec()
904 .into_iter()
905 .enumerate()
906 .zip(column_widths.into_vec())
907 .map(|((header_idx, h), width)| {
908 base_cell_style_text(width, table_context.use_ui_font, cx)
909 .px_1()
910 .py_0p5()
911 .child(h)
912 .id(ElementId::NamedInteger(
913 shared_element_id.clone(),
914 header_idx as u64,
915 ))
916 .when_some(resize_info.as_ref().cloned(), |this, info| {
917 if info.resize_behavior[header_idx].is_resizable() {
918 this.on_click(move |event, window, cx| {
919 if event.click_count() > 1 {
920 info.columns_state
921 .update(cx, |column, _| {
922 column.on_double_click(
923 header_idx,
924 &info.initial_widths,
925 &info.resize_behavior,
926 window,
927 );
928 })
929 .ok();
930 }
931 })
932 } else {
933 this
934 }
935 })
936 }),
937 )
938}
939
940#[derive(Clone)]
941pub struct TableRenderContext {
942 pub striped: bool,
943 pub show_row_borders: bool,
944 pub show_row_hover: bool,
945 pub total_row_count: usize,
946 pub column_widths: Option<TableRow<Length>>,
947 pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
948 pub use_ui_font: bool,
949 pub disable_base_cell_style: bool,
950}
951
952impl TableRenderContext {
953 fn new(table: &Table, cx: &App) -> Self {
954 Self {
955 striped: table.striped,
956 show_row_borders: table.show_row_borders,
957 show_row_hover: table.show_row_hover,
958 total_row_count: table.rows.len(),
959 column_widths: table.column_width_config.widths_to_render(cx),
960 map_row: table.map_row.clone(),
961 use_ui_font: table.use_ui_font,
962 disable_base_cell_style: table.disable_base_cell_style,
963 }
964 }
965}
966
967impl RenderOnce for Table {
968 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
969 let table_context = TableRenderContext::new(&self, cx);
970 let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
971
972 let header_resize_info = interaction_state
973 .as_ref()
974 .and_then(|_| self.column_width_config.header_resize_info(cx));
975
976 let table_width = self.column_width_config.table_width();
977 let horizontal_sizing = self.column_width_config.list_horizontal_sizing();
978 let no_rows_rendered = self.rows.is_empty();
979
980 // Extract redistributable entity for drag/drop/prepaint handlers
981 let redistributable_entity =
982 interaction_state
983 .as_ref()
984 .and_then(|_| match &self.column_width_config {
985 ColumnWidthConfig::Redistributable {
986 columns_state: entity,
987 ..
988 } => Some(entity.downgrade()),
989 _ => None,
990 });
991
992 let resize_handles = interaction_state
993 .as_ref()
994 .and_then(|_| self.column_width_config.render_resize_handles(window, cx));
995
996 let table = div()
997 .when_some(table_width, |this, width| this.w(width))
998 .h_full()
999 .v_flex()
1000 .when_some(self.headers.take(), |this, headers| {
1001 this.child(render_table_header(
1002 headers,
1003 table_context.clone(),
1004 header_resize_info,
1005 interaction_state.as_ref().map(Entity::entity_id),
1006 cx,
1007 ))
1008 })
1009 .when_some(redistributable_entity, {
1010 |this, widths| {
1011 this.on_drag_move::<DraggedColumn>({
1012 let widths = widths.clone();
1013 move |e, window, cx| {
1014 widths
1015 .update(cx, |widths, cx| {
1016 widths.on_drag_move(e, window, cx);
1017 })
1018 .ok();
1019 }
1020 })
1021 .on_children_prepainted({
1022 let widths = widths.clone();
1023 move |bounds, _, cx| {
1024 widths
1025 .update(cx, |widths, _| {
1026 // This works because all children x axis bounds are the same
1027 widths.cached_table_width =
1028 bounds[0].right() - bounds[0].left();
1029 })
1030 .ok();
1031 }
1032 })
1033 .on_drop::<DraggedColumn>(move |_, _, cx| {
1034 widths
1035 .update(cx, |widths, _| {
1036 widths.committed_widths = widths.preview_widths.clone();
1037 })
1038 .ok();
1039 })
1040 }
1041 })
1042 .child({
1043 let content = div()
1044 .flex_grow()
1045 .w_full()
1046 .relative()
1047 .overflow_hidden()
1048 .map(|parent| match self.rows {
1049 TableContents::Vec(items) => {
1050 parent.children(items.into_iter().enumerate().map(|(index, row)| {
1051 div().child(render_table_row(
1052 index,
1053 row,
1054 table_context.clone(),
1055 window,
1056 cx,
1057 ))
1058 }))
1059 }
1060 TableContents::UniformList(uniform_list_data) => parent.child(
1061 uniform_list(
1062 uniform_list_data.element_id,
1063 uniform_list_data.row_count,
1064 {
1065 let render_item_fn = uniform_list_data.render_list_of_rows_fn;
1066 move |range: Range<usize>, window, cx| {
1067 let elements = render_item_fn(range.clone(), window, cx)
1068 .into_iter()
1069 .map(|raw_row| raw_row.into_table_row(self.cols))
1070 .collect::<Vec<_>>();
1071 elements
1072 .into_iter()
1073 .zip(range)
1074 .map(|(row, row_index)| {
1075 render_table_row(
1076 row_index,
1077 row,
1078 table_context.clone(),
1079 window,
1080 cx,
1081 )
1082 })
1083 .collect()
1084 }
1085 },
1086 )
1087 .size_full()
1088 .flex_grow()
1089 .with_sizing_behavior(ListSizingBehavior::Auto)
1090 .with_horizontal_sizing_behavior(horizontal_sizing)
1091 .when_some(
1092 interaction_state.as_ref(),
1093 |this, state| {
1094 this.track_scroll(
1095 &state.read_with(cx, |s, _| s.scroll_handle.clone()),
1096 )
1097 },
1098 ),
1099 ),
1100 TableContents::VariableRowHeightList(variable_list_data) => parent.child(
1101 list(variable_list_data.list_state.clone(), {
1102 let render_item_fn = variable_list_data.render_row_fn;
1103 move |row_index: usize, window: &mut Window, cx: &mut App| {
1104 let row = render_item_fn(row_index, window, cx)
1105 .into_table_row(self.cols);
1106 render_table_row(
1107 row_index,
1108 row,
1109 table_context.clone(),
1110 window,
1111 cx,
1112 )
1113 }
1114 })
1115 .size_full()
1116 .flex_grow()
1117 .with_sizing_behavior(ListSizingBehavior::Auto),
1118 ),
1119 })
1120 .when_some(resize_handles, |parent, handles| parent.child(handles));
1121
1122 if let Some(state) = interaction_state.as_ref() {
1123 let scrollbars = state
1124 .read(cx)
1125 .custom_scrollbar
1126 .clone()
1127 .unwrap_or_else(|| Scrollbars::new(ScrollAxes::Both));
1128 content
1129 .custom_scrollbars(
1130 scrollbars.tracked_scroll_handle(&state.read(cx).scroll_handle),
1131 window,
1132 cx,
1133 )
1134 .into_any_element()
1135 } else {
1136 content.into_any_element()
1137 }
1138 })
1139 .when_some(
1140 no_rows_rendered
1141 .then_some(self.empty_table_callback)
1142 .flatten(),
1143 |this, callback| {
1144 this.child(
1145 h_flex()
1146 .size_full()
1147 .p_3()
1148 .items_start()
1149 .justify_center()
1150 .child(callback(window, cx)),
1151 )
1152 },
1153 );
1154
1155 if let Some(interaction_state) = interaction_state.as_ref() {
1156 table
1157 .track_focus(&interaction_state.read(cx).focus_handle)
1158 .id(("table", interaction_state.entity_id()))
1159 .into_any_element()
1160 } else {
1161 table.into_any_element()
1162 }
1163 }
1164}
1165
1166impl Component for Table {
1167 fn scope() -> ComponentScope {
1168 ComponentScope::Layout
1169 }
1170
1171 fn description() -> Option<&'static str> {
1172 Some("A table component for displaying data in rows and columns with optional styling.")
1173 }
1174
1175 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
1176 Some(
1177 v_flex()
1178 .gap_6()
1179 .children(vec![
1180 example_group_with_title(
1181 "Basic Tables",
1182 vec![
1183 single_example(
1184 "Simple Table",
1185 Table::new(3)
1186 .width(px(400.))
1187 .header(vec!["Name", "Age", "City"])
1188 .row(vec!["Alice", "28", "New York"])
1189 .row(vec!["Bob", "32", "San Francisco"])
1190 .row(vec!["Charlie", "25", "London"])
1191 .into_any_element(),
1192 ),
1193 single_example(
1194 "Two Column Table",
1195 Table::new(2)
1196 .header(vec!["Category", "Value"])
1197 .width(px(300.))
1198 .row(vec!["Revenue", "$100,000"])
1199 .row(vec!["Expenses", "$75,000"])
1200 .row(vec!["Profit", "$25,000"])
1201 .into_any_element(),
1202 ),
1203 ],
1204 ),
1205 example_group_with_title(
1206 "Styled Tables",
1207 vec![
1208 single_example(
1209 "Default",
1210 Table::new(3)
1211 .width(px(400.))
1212 .header(vec!["Product", "Price", "Stock"])
1213 .row(vec!["Laptop", "$999", "In Stock"])
1214 .row(vec!["Phone", "$599", "Low Stock"])
1215 .row(vec!["Tablet", "$399", "Out of Stock"])
1216 .into_any_element(),
1217 ),
1218 single_example(
1219 "Striped",
1220 Table::new(3)
1221 .width(px(400.))
1222 .striped()
1223 .header(vec!["Product", "Price", "Stock"])
1224 .row(vec!["Laptop", "$999", "In Stock"])
1225 .row(vec!["Phone", "$599", "Low Stock"])
1226 .row(vec!["Tablet", "$399", "Out of Stock"])
1227 .row(vec!["Headphones", "$199", "In Stock"])
1228 .into_any_element(),
1229 ),
1230 ],
1231 ),
1232 example_group_with_title(
1233 "Mixed Content Table",
1234 vec![single_example(
1235 "Table with Elements",
1236 Table::new(5)
1237 .width(px(840.))
1238 .header(vec!["Status", "Name", "Priority", "Deadline", "Action"])
1239 .row(vec![
1240 Indicator::dot().color(Color::Success).into_any_element(),
1241 "Project A".into_any_element(),
1242 "High".into_any_element(),
1243 "2023-12-31".into_any_element(),
1244 Button::new("view_a", "View")
1245 .style(ButtonStyle::Filled)
1246 .full_width()
1247 .into_any_element(),
1248 ])
1249 .row(vec![
1250 Indicator::dot().color(Color::Warning).into_any_element(),
1251 "Project B".into_any_element(),
1252 "Medium".into_any_element(),
1253 "2024-03-15".into_any_element(),
1254 Button::new("view_b", "View")
1255 .style(ButtonStyle::Filled)
1256 .full_width()
1257 .into_any_element(),
1258 ])
1259 .row(vec![
1260 Indicator::dot().color(Color::Error).into_any_element(),
1261 "Project C".into_any_element(),
1262 "Low".into_any_element(),
1263 "2024-06-30".into_any_element(),
1264 Button::new("view_c", "View")
1265 .style(ButtonStyle::Filled)
1266 .full_width()
1267 .into_any_element(),
1268 ])
1269 .into_any_element(),
1270 )],
1271 ),
1272 ])
1273 .into_any_element(),
1274 )
1275 }
1276}