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