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