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