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