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