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