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 const 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 const fn len(&self) -> usize {
44 match self {
45 TableContents::Vec(rows) => rows.len(),
46 TableContents::UniformList(data) => data.row_count,
47 }
48 }
49
50 const 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 const 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 const 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 .h_full()
645 .id(("table_row", row_index))
646 .w_full()
647 .justify_between()
648 .when_some(bg, |row, bg| row.bg(bg))
649 .when(!is_striped, |row| {
650 row.border_b_1()
651 .border_color(transparent_black())
652 .when(!is_last, |row| row.border_color(cx.theme().colors().border))
653 });
654
655 row = row.children(
656 items
657 .map(IntoElement::into_any_element)
658 .into_iter()
659 .zip(column_widths)
660 .map(|(cell, width)| base_cell_style_text(width, cx).px_1().py_0p5().child(cell)),
661 );
662
663 let row = if let Some(map_row) = table_context.map_row {
664 map_row((row_index, row), window, cx)
665 } else {
666 row.into_any_element()
667 };
668
669 div().size_full().child(row).into_any_element()
670}
671
672pub fn render_table_header<const COLS: usize>(
673 headers: [impl IntoElement; COLS],
674 table_context: TableRenderContext<COLS>,
675 columns_widths: Option<(
676 WeakEntity<TableColumnWidths<COLS>>,
677 [TableResizeBehavior; COLS],
678 [DefiniteLength; COLS],
679 )>,
680 entity_id: Option<EntityId>,
681 cx: &mut App,
682) -> impl IntoElement {
683 let column_widths = table_context
684 .column_widths
685 .map_or([None; COLS], |widths| widths.map(Some));
686
687 let element_id = entity_id
688 .map(|entity| entity.to_string())
689 .unwrap_or_default();
690
691 let shared_element_id: SharedString = format!("table-{}", element_id).into();
692
693 div()
694 .flex()
695 .flex_row()
696 .items_center()
697 .justify_between()
698 .w_full()
699 .p_2()
700 .border_b_1()
701 .border_color(cx.theme().colors().border)
702 .children(headers.into_iter().enumerate().zip(column_widths).map(
703 |((header_idx, h), width)| {
704 base_cell_style_text(width, cx)
705 .child(h)
706 .id(ElementId::NamedInteger(
707 shared_element_id.clone(),
708 header_idx as u64,
709 ))
710 .when_some(
711 columns_widths.as_ref().cloned(),
712 |this, (column_widths, resizables, initial_sizes)| {
713 if resizables[header_idx].is_resizable() {
714 this.on_click(move |event, window, cx| {
715 if event.click_count() > 1 {
716 column_widths
717 .update(cx, |column, _| {
718 column.on_double_click(
719 header_idx,
720 &initial_sizes,
721 &resizables,
722 window,
723 );
724 })
725 .ok();
726 }
727 })
728 } else {
729 this
730 }
731 },
732 )
733 },
734 ))
735}
736
737#[derive(Clone)]
738pub struct TableRenderContext<const COLS: usize> {
739 pub striped: bool,
740 pub total_row_count: usize,
741 pub column_widths: Option<[Length; COLS]>,
742 pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
743}
744
745impl<const COLS: usize> TableRenderContext<COLS> {
746 fn new(table: &Table<COLS>, cx: &App) -> Self {
747 Self {
748 striped: table.striped,
749 total_row_count: table.rows.len(),
750 column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)),
751 map_row: table.map_row.clone(),
752 }
753 }
754}
755
756impl<const COLS: usize> RenderOnce for Table<COLS> {
757 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
758 let table_context = TableRenderContext::new(&self, cx);
759 let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
760 let current_widths = self
761 .col_widths
762 .as_ref()
763 .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable)))
764 .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior));
765
766 let current_widths_with_initial_sizes = self
767 .col_widths
768 .as_ref()
769 .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable, widths.initial)))
770 .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial));
771
772 let width = self.width;
773 let no_rows_rendered = self.rows.is_empty();
774
775 let table = div()
776 .when_some(width, |this, width| this.w(width))
777 .h_full()
778 .v_flex()
779 .when_some(self.headers.take(), |this, headers| {
780 this.child(render_table_header(
781 headers,
782 table_context.clone(),
783 current_widths_with_initial_sizes,
784 interaction_state.as_ref().map(Entity::entity_id),
785 cx,
786 ))
787 })
788 .when_some(current_widths, {
789 |this, (widths, resize_behavior)| {
790 this.on_drag_move::<DraggedColumn>({
791 let widths = widths.clone();
792 move |e, window, cx| {
793 widths
794 .update(cx, |widths, cx| {
795 widths.on_drag_move(e, &resize_behavior, window, cx);
796 })
797 .ok();
798 }
799 })
800 .on_children_prepainted({
801 let widths = widths.clone();
802 move |bounds, _, cx| {
803 widths
804 .update(cx, |widths, _| {
805 // This works because all children x axis bounds are the same
806 widths.cached_bounds_width =
807 bounds[0].right() - bounds[0].left();
808 })
809 .ok();
810 }
811 })
812 .on_drop::<DraggedColumn>(move |_, _, cx| {
813 widths
814 .update(cx, |widths, _| {
815 widths.widths = widths.visible_widths;
816 })
817 .ok();
818 // Finish the resize operation
819 })
820 }
821 })
822 .child({
823 let content = div()
824 .flex_grow()
825 .w_full()
826 .relative()
827 .overflow_hidden()
828 .map(|parent| match self.rows {
829 TableContents::Vec(items) => {
830 parent.children(items.into_iter().enumerate().map(|(index, row)| {
831 div().child(render_table_row(
832 index,
833 row,
834 table_context.clone(),
835 window,
836 cx,
837 ))
838 }))
839 }
840 TableContents::UniformList(uniform_list_data) => parent.child(
841 uniform_list(
842 uniform_list_data.element_id,
843 uniform_list_data.row_count,
844 {
845 let render_item_fn = uniform_list_data.render_item_fn;
846 move |range: Range<usize>, window, cx| {
847 let elements = render_item_fn(range.clone(), window, cx);
848 elements
849 .into_iter()
850 .zip(range)
851 .map(|(row, row_index)| {
852 render_table_row(
853 row_index,
854 row,
855 table_context.clone(),
856 window,
857 cx,
858 )
859 })
860 .collect()
861 }
862 },
863 )
864 .size_full()
865 .flex_grow()
866 .with_sizing_behavior(ListSizingBehavior::Auto)
867 .with_horizontal_sizing_behavior(if width.is_some() {
868 ListHorizontalSizingBehavior::Unconstrained
869 } else {
870 ListHorizontalSizingBehavior::FitList
871 })
872 .when_some(
873 interaction_state.as_ref(),
874 |this, state| {
875 this.track_scroll(
876 state.read_with(cx, |s, _| s.scroll_handle.clone()),
877 )
878 },
879 ),
880 ),
881 })
882 .when_some(
883 self.col_widths.as_ref().zip(interaction_state.as_ref()),
884 |parent, (table_widths, state)| {
885 parent.child(state.update(cx, |state, cx| {
886 let resizable_columns = table_widths.resizable;
887 let column_widths = table_widths.lengths(cx);
888 let columns = table_widths.current.clone();
889 let initial_sizes = table_widths.initial;
890 state.render_resize_handles(
891 &column_widths,
892 &resizable_columns,
893 initial_sizes,
894 columns,
895 window,
896 cx,
897 )
898 }))
899 },
900 );
901
902 if let Some(state) = interaction_state.as_ref() {
903 let scrollbars = state
904 .read(cx)
905 .custom_scrollbar
906 .clone()
907 .unwrap_or_else(|| Scrollbars::new(super::ScrollAxes::Both));
908 content
909 .custom_scrollbars(
910 scrollbars.tracked_scroll_handle(state.read(cx).scroll_handle.clone()),
911 window,
912 cx,
913 )
914 .into_any_element()
915 } else {
916 content.into_any_element()
917 }
918 })
919 .when_some(
920 no_rows_rendered
921 .then_some(self.empty_table_callback)
922 .flatten(),
923 |this, callback| {
924 this.child(
925 h_flex()
926 .size_full()
927 .p_3()
928 .items_start()
929 .justify_center()
930 .child(callback(window, cx)),
931 )
932 },
933 );
934
935 if let Some(interaction_state) = interaction_state.as_ref() {
936 table
937 .track_focus(&interaction_state.read(cx).focus_handle)
938 .id(("table", interaction_state.entity_id()))
939 .into_any_element()
940 } else {
941 table.into_any_element()
942 }
943 }
944}
945
946impl Component for Table<3> {
947 fn scope() -> ComponentScope {
948 ComponentScope::Layout
949 }
950
951 fn description() -> Option<&'static str> {
952 Some("A table component for displaying data in rows and columns with optional styling.")
953 }
954
955 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
956 Some(
957 v_flex()
958 .gap_6()
959 .children(vec![
960 example_group_with_title(
961 "Basic Tables",
962 vec![
963 single_example(
964 "Simple Table",
965 Table::new()
966 .width(px(400.))
967 .header(["Name", "Age", "City"])
968 .row(["Alice", "28", "New York"])
969 .row(["Bob", "32", "San Francisco"])
970 .row(["Charlie", "25", "London"])
971 .into_any_element(),
972 ),
973 single_example(
974 "Two Column Table",
975 Table::new()
976 .header(["Category", "Value"])
977 .width(px(300.))
978 .row(["Revenue", "$100,000"])
979 .row(["Expenses", "$75,000"])
980 .row(["Profit", "$25,000"])
981 .into_any_element(),
982 ),
983 ],
984 ),
985 example_group_with_title(
986 "Styled Tables",
987 vec![
988 single_example(
989 "Default",
990 Table::new()
991 .width(px(400.))
992 .header(["Product", "Price", "Stock"])
993 .row(["Laptop", "$999", "In Stock"])
994 .row(["Phone", "$599", "Low Stock"])
995 .row(["Tablet", "$399", "Out of Stock"])
996 .into_any_element(),
997 ),
998 single_example(
999 "Striped",
1000 Table::new()
1001 .width(px(400.))
1002 .striped()
1003 .header(["Product", "Price", "Stock"])
1004 .row(["Laptop", "$999", "In Stock"])
1005 .row(["Phone", "$599", "Low Stock"])
1006 .row(["Tablet", "$399", "Out of Stock"])
1007 .row(["Headphones", "$199", "In Stock"])
1008 .into_any_element(),
1009 ),
1010 ],
1011 ),
1012 example_group_with_title(
1013 "Mixed Content Table",
1014 vec![single_example(
1015 "Table with Elements",
1016 Table::new()
1017 .width(px(840.))
1018 .header(["Status", "Name", "Priority", "Deadline", "Action"])
1019 .row([
1020 Indicator::dot().color(Color::Success).into_any_element(),
1021 "Project A".into_any_element(),
1022 "High".into_any_element(),
1023 "2023-12-31".into_any_element(),
1024 Button::new("view_a", "View")
1025 .style(ButtonStyle::Filled)
1026 .full_width()
1027 .into_any_element(),
1028 ])
1029 .row([
1030 Indicator::dot().color(Color::Warning).into_any_element(),
1031 "Project B".into_any_element(),
1032 "Medium".into_any_element(),
1033 "2024-03-15".into_any_element(),
1034 Button::new("view_b", "View")
1035 .style(ButtonStyle::Filled)
1036 .full_width()
1037 .into_any_element(),
1038 ])
1039 .row([
1040 Indicator::dot().color(Color::Error).into_any_element(),
1041 "Project C".into_any_element(),
1042 "Low".into_any_element(),
1043 "2024-06-30".into_any_element(),
1044 Button::new("view_c", "View")
1045 .style(ButtonStyle::Filled)
1046 .full_width()
1047 .into_any_element(),
1048 ])
1049 .into_any_element(),
1050 )],
1051 ),
1052 ])
1053 .into_any_element(),
1054 )
1055 }
1056}
1057
1058#[cfg(test)]
1059mod test {
1060 use super::*;
1061
1062 fn is_almost_eq(a: &[f32], b: &[f32]) -> bool {
1063 a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6)
1064 }
1065
1066 fn cols_to_str<const COLS: usize>(cols: &[f32; COLS], total_size: f32) -> String {
1067 cols.map(|f| "*".repeat(f32::round(f * total_size) as usize))
1068 .join("|")
1069 }
1070
1071 fn parse_resize_behavior<const COLS: usize>(
1072 input: &str,
1073 total_size: f32,
1074 ) -> [TableResizeBehavior; COLS] {
1075 let mut resize_behavior = [TableResizeBehavior::None; COLS];
1076 let mut max_index = 0;
1077 for (index, col) in input.split('|').enumerate() {
1078 if col.starts_with('X') || col.is_empty() {
1079 resize_behavior[index] = TableResizeBehavior::None;
1080 } else if col.starts_with('*') {
1081 resize_behavior[index] =
1082 TableResizeBehavior::MinSize(col.len() as f32 / total_size);
1083 } else {
1084 panic!("invalid test input: unrecognized resize behavior: {}", col);
1085 }
1086 max_index = index;
1087 }
1088
1089 if max_index + 1 != COLS {
1090 panic!("invalid test input: too many columns");
1091 }
1092 resize_behavior
1093 }
1094
1095 mod reset_column_size {
1096 use super::*;
1097
1098 fn parse<const COLS: usize>(input: &str) -> ([f32; COLS], f32, Option<usize>) {
1099 let mut widths = [f32::NAN; COLS];
1100 let mut column_index = None;
1101 for (index, col) in input.split('|').enumerate() {
1102 widths[index] = col.len() as f32;
1103 if col.starts_with('X') {
1104 column_index = Some(index);
1105 }
1106 }
1107
1108 for w in widths {
1109 assert!(w.is_finite(), "incorrect number of columns");
1110 }
1111 let total = widths.iter().sum::<f32>();
1112 for width in &mut widths {
1113 *width /= total;
1114 }
1115 (widths, total, column_index)
1116 }
1117
1118 #[track_caller]
1119 fn check_reset_size<const COLS: usize>(
1120 initial_sizes: &str,
1121 widths: &str,
1122 expected: &str,
1123 resize_behavior: &str,
1124 ) {
1125 let (initial_sizes, total_1, None) = parse::<COLS>(initial_sizes) else {
1126 panic!("invalid test input: initial sizes should not be marked");
1127 };
1128 let (widths, total_2, Some(column_index)) = parse::<COLS>(widths) else {
1129 panic!("invalid test input: widths should be marked");
1130 };
1131 assert_eq!(
1132 total_1, total_2,
1133 "invalid test input: total width not the same {total_1}, {total_2}"
1134 );
1135 let (expected, total_3, None) = parse::<COLS>(expected) else {
1136 panic!("invalid test input: expected should not be marked: {expected:?}");
1137 };
1138 assert_eq!(
1139 total_2, total_3,
1140 "invalid test input: total width not the same"
1141 );
1142 let resize_behavior = parse_resize_behavior::<COLS>(resize_behavior, total_1);
1143 let result = TableColumnWidths::reset_to_initial_size(
1144 column_index,
1145 widths,
1146 initial_sizes,
1147 &resize_behavior,
1148 );
1149 let is_eq = is_almost_eq(&result, &expected);
1150 if !is_eq {
1151 let result_str = cols_to_str(&result, total_1);
1152 let expected_str = cols_to_str(&expected, total_1);
1153 panic!(
1154 "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}"
1155 );
1156 }
1157 }
1158
1159 macro_rules! check_reset_size {
1160 (columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => {
1161 check_reset_size::<$cols>($initial, $current, $expected, $resizing);
1162 };
1163 ($name:ident, columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => {
1164 #[test]
1165 fn $name() {
1166 check_reset_size::<$cols>($initial, $current, $expected, $resizing);
1167 }
1168 };
1169 }
1170
1171 check_reset_size!(
1172 basic_right,
1173 columns: 5,
1174 starting: "**|**|**|**|**",
1175 snapshot: "**|**|X|***|**",
1176 expected: "**|**|**|**|**",
1177 minimums: "X|*|*|*|*",
1178 );
1179
1180 check_reset_size!(
1181 basic_left,
1182 columns: 5,
1183 starting: "**|**|**|**|**",
1184 snapshot: "**|**|***|X|**",
1185 expected: "**|**|**|**|**",
1186 minimums: "X|*|*|*|**",
1187 );
1188
1189 check_reset_size!(
1190 squashed_left_reset_col2,
1191 columns: 6,
1192 starting: "*|***|**|**|****|*",
1193 snapshot: "*|*|X|*|*|********",
1194 expected: "*|*|**|*|*|*******",
1195 minimums: "X|*|*|*|*|*",
1196 );
1197
1198 check_reset_size!(
1199 grow_cascading_right,
1200 columns: 6,
1201 starting: "*|***|****|**|***|*",
1202 snapshot: "*|***|X|**|**|*****",
1203 expected: "*|***|****|*|*|****",
1204 minimums: "X|*|*|*|*|*",
1205 );
1206
1207 check_reset_size!(
1208 squashed_right_reset_col4,
1209 columns: 6,
1210 starting: "*|***|**|**|****|*",
1211 snapshot: "*|********|*|*|X|*",
1212 expected: "*|*****|*|*|****|*",
1213 minimums: "X|*|*|*|*|*",
1214 );
1215
1216 check_reset_size!(
1217 reset_col6_right,
1218 columns: 6,
1219 starting: "*|***|**|***|***|**",
1220 snapshot: "*|***|**|***|**|XXX",
1221 expected: "*|***|**|***|***|**",
1222 minimums: "X|*|*|*|*|*",
1223 );
1224
1225 check_reset_size!(
1226 reset_col6_left,
1227 columns: 6,
1228 starting: "*|***|**|***|***|**",
1229 snapshot: "*|***|**|***|****|X",
1230 expected: "*|***|**|***|***|**",
1231 minimums: "X|*|*|*|*|*",
1232 );
1233
1234 check_reset_size!(
1235 last_column_grow_cascading,
1236 columns: 6,
1237 starting: "*|***|**|**|**|***",
1238 snapshot: "*|*******|*|**|*|X",
1239 expected: "*|******|*|*|*|***",
1240 minimums: "X|*|*|*|*|*",
1241 );
1242
1243 check_reset_size!(
1244 goes_left_when_left_has_extreme_diff,
1245 columns: 6,
1246 starting: "*|***|****|**|**|***",
1247 snapshot: "*|********|X|*|**|**",
1248 expected: "*|*****|****|*|**|**",
1249 minimums: "X|*|*|*|*|*",
1250 );
1251
1252 check_reset_size!(
1253 basic_shrink_right,
1254 columns: 6,
1255 starting: "**|**|**|**|**|**",
1256 snapshot: "**|**|XXX|*|**|**",
1257 expected: "**|**|**|**|**|**",
1258 minimums: "X|*|*|*|*|*",
1259 );
1260
1261 check_reset_size!(
1262 shrink_should_go_left,
1263 columns: 6,
1264 starting: "*|***|**|*|*|*",
1265 snapshot: "*|*|XXX|**|*|*",
1266 expected: "*|**|**|**|*|*",
1267 minimums: "X|*|*|*|*|*",
1268 );
1269
1270 check_reset_size!(
1271 shrink_should_go_right,
1272 columns: 6,
1273 starting: "*|***|**|**|**|*",
1274 snapshot: "*|****|XXX|*|*|*",
1275 expected: "*|****|**|**|*|*",
1276 minimums: "X|*|*|*|*|*",
1277 );
1278 }
1279
1280 mod drag_handle {
1281 use super::*;
1282
1283 fn parse<const COLS: usize>(input: &str) -> ([f32; COLS], f32, Option<usize>) {
1284 let mut widths = [f32::NAN; COLS];
1285 let column_index = input.replace("*", "").find("I");
1286 for (index, col) in input.replace("I", "|").split('|').enumerate() {
1287 widths[index] = col.len() as f32;
1288 }
1289
1290 for w in widths {
1291 assert!(w.is_finite(), "incorrect number of columns");
1292 }
1293 let total = widths.iter().sum::<f32>();
1294 for width in &mut widths {
1295 *width /= total;
1296 }
1297 (widths, total, column_index)
1298 }
1299
1300 #[track_caller]
1301 fn check<const COLS: usize>(
1302 distance: i32,
1303 widths: &str,
1304 expected: &str,
1305 resize_behavior: &str,
1306 ) {
1307 let (mut widths, total_1, Some(column_index)) = parse::<COLS>(widths) else {
1308 panic!("invalid test input: widths should be marked");
1309 };
1310 let (expected, total_2, None) = parse::<COLS>(expected) else {
1311 panic!("invalid test input: expected should not be marked: {expected:?}");
1312 };
1313 assert_eq!(
1314 total_1, total_2,
1315 "invalid test input: total width not the same"
1316 );
1317 let resize_behavior = parse_resize_behavior::<COLS>(resize_behavior, total_1);
1318
1319 let distance = distance as f32 / total_1;
1320
1321 let result = TableColumnWidths::drag_column_handle(
1322 distance,
1323 column_index,
1324 &mut widths,
1325 &resize_behavior,
1326 );
1327
1328 let is_eq = is_almost_eq(&widths, &expected);
1329 if !is_eq {
1330 let result_str = cols_to_str(&widths, total_1);
1331 let expected_str = cols_to_str(&expected, total_1);
1332 panic!(
1333 "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}"
1334 );
1335 }
1336 }
1337
1338 macro_rules! check {
1339 (columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => {
1340 check!($cols, $dist, $snapshot, $expected, $resizing);
1341 };
1342 ($name:ident, columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => {
1343 #[test]
1344 fn $name() {
1345 check::<$cols>($dist, $current, $expected, $resizing);
1346 }
1347 };
1348 }
1349
1350 check!(
1351 basic_right_drag,
1352 columns: 3,
1353 distance: 1,
1354 snapshot: "**|**I**",
1355 expected: "**|***|*",
1356 minimums: "X|*|*",
1357 );
1358
1359 check!(
1360 drag_left_against_mins,
1361 columns: 5,
1362 distance: -1,
1363 snapshot: "*|*|*|*I*******",
1364 expected: "*|*|*|*|*******",
1365 minimums: "X|*|*|*|*",
1366 );
1367
1368 check!(
1369 drag_left,
1370 columns: 5,
1371 distance: -2,
1372 snapshot: "*|*|*|*****I***",
1373 expected: "*|*|*|***|*****",
1374 minimums: "X|*|*|*|*",
1375 );
1376 }
1377}