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