Skip to repository content1479 lines · 55.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:35:45.491Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
data_table.rs
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, redistribute_hidden_widths,
11 render_column_resize_divider, 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 h_scroll_offset: Pixels,
69 window: &mut Window,
70 cx: &mut Context<Self>,
71 ) {
72 let col_idx = drag_event.drag(cx).col_idx;
73 // h_scroll_offset is negative when scrolled right; subtracting it maps drag_x to the
74 // column's natural (unscrolled) position. Only scrollable columns reach this path —
75 // pinned dividers are rendered non-interactive.
76 let drag_x = drag_event.event.position.x - drag_event.bounds.left() - h_scroll_offset;
77 self.drag_to(col_idx, drag_x, window.rem_size());
78 cx.notify();
79 }
80
81 /// Resizes `col_idx` such that its right edge sits at `drag_x`, where `drag_x` is in the
82 /// column-strip's natural (unscrolled) coordinate space, relative to its left edge.
83 pub(crate) fn drag_to(&mut self, col_idx: usize, drag_x: Pixels, rem_size: Pixels) {
84 let left_edge: Pixels = self.widths.as_slice()[..col_idx]
85 .iter()
86 .map(|width| width.to_pixels(rem_size))
87 .fold(px(0.), |acc, x| acc + x);
88
89 let new_width = drag_x - left_edge;
90 let new_width = self.apply_min_size(new_width, self.resize_behavior[col_idx], rem_size);
91
92 self.widths[col_idx] = AbsoluteLength::Pixels(new_width);
93 }
94
95 pub fn set_column_configuration(
96 &mut self,
97 col_idx: usize,
98 width: impl Into<AbsoluteLength>,
99 resize_behavior: TableResizeBehavior,
100 ) {
101 let width = width.into();
102 self.initial_widths[col_idx] = width;
103 self.widths[col_idx] = width;
104 self.resize_behavior[col_idx] = resize_behavior;
105 }
106
107 pub fn reset_column_to_initial_width(&mut self, col_idx: usize) {
108 self.widths[col_idx] = self.initial_widths[col_idx];
109 }
110
111 pub fn pinned_width(&self, pinned_cols: usize, rem_size: Pixels) -> Pixels {
112 self.widths[..pinned_cols]
113 .iter()
114 .map(|w| w.to_pixels(rem_size))
115 .fold(px(0.), |acc, x| acc + x)
116 }
117
118 pub fn scrollable_width(&self, pinned_cols: usize, rem_size: Pixels) -> Pixels {
119 self.widths[pinned_cols..]
120 .iter()
121 .map(|w| w.to_pixels(rem_size))
122 .fold(px(0.), |acc, x| acc + x)
123 }
124
125 fn apply_min_size(
126 &self,
127 width: Pixels,
128 behavior: TableResizeBehavior,
129 rem_size: Pixels,
130 ) -> Pixels {
131 match behavior.min_size() {
132 Some(min_rems) => {
133 let min_px = rem_size * min_rems;
134 width.max(min_px)
135 }
136 None => width,
137 }
138 }
139}
140
141struct UniformListData {
142 render_list_of_rows_fn:
143 Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<UncheckedTableRow<AnyElement>>>,
144 element_id: ElementId,
145 row_count: usize,
146}
147
148struct VariableRowHeightListData {
149 /// Unlike UniformList, this closure renders only single row, allowing each one to have its own height
150 render_row_fn: Box<dyn Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement>>,
151 list_state: ListState,
152 row_count: usize,
153}
154
155enum TableContents {
156 Vec(Vec<TableRow<AnyElement>>),
157 UniformList(UniformListData),
158 VariableRowHeightList(VariableRowHeightListData),
159}
160
161impl TableContents {
162 fn rows_mut(&mut self) -> Option<&mut Vec<TableRow<AnyElement>>> {
163 match self {
164 TableContents::Vec(rows) => Some(rows),
165 TableContents::UniformList(_) => None,
166 TableContents::VariableRowHeightList(_) => None,
167 }
168 }
169
170 fn len(&self) -> usize {
171 match self {
172 TableContents::Vec(rows) => rows.len(),
173 TableContents::UniformList(data) => data.row_count,
174 TableContents::VariableRowHeightList(data) => data.row_count,
175 }
176 }
177
178 fn is_empty(&self) -> bool {
179 self.len() == 0
180 }
181}
182
183pub struct TableInteractionState {
184 pub focus_handle: FocusHandle,
185 pub scroll_handle: UniformListScrollHandle,
186 pub horizontal_scroll_handle: ScrollHandle,
187 pub custom_scrollbar: Option<Scrollbars>,
188}
189
190impl TableInteractionState {
191 pub fn new(cx: &mut App) -> Self {
192 Self {
193 focus_handle: cx.focus_handle(),
194 scroll_handle: UniformListScrollHandle::new(),
195 horizontal_scroll_handle: ScrollHandle::new(),
196 custom_scrollbar: None,
197 }
198 }
199
200 pub fn with_custom_scrollbar(mut self, custom_scrollbar: Scrollbars) -> Self {
201 self.custom_scrollbar = Some(custom_scrollbar);
202 self
203 }
204
205 pub fn scroll_offset(&self) -> Point<Pixels> {
206 self.scroll_handle.offset()
207 }
208
209 pub fn set_scroll_offset(&self, offset: Point<Pixels>) {
210 self.scroll_handle.set_offset(offset);
211 }
212
213 pub fn listener<E: ?Sized>(
214 this: &Entity<Self>,
215 f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static,
216 ) -> impl Fn(&E, &mut Window, &mut App) + 'static {
217 let view = this.downgrade();
218 move |e: &E, window: &mut Window, cx: &mut App| {
219 view.update(cx, |view, cx| f(view, e, window, cx)).ok();
220 }
221 }
222}
223
224pub enum ColumnWidthConfig {
225 /// Static column widths (no resize handles).
226 Static {
227 widths: StaticColumnWidths,
228 /// Controls widths of the whole table.
229 table_width: Option<DefiniteLength>,
230 },
231 /// Redistributable columns — dragging redistributes the fixed available space
232 /// among columns without changing the overall table width.
233 Redistributable {
234 columns_state: Entity<RedistributableColumnsState>,
235 table_width: Option<DefiniteLength>,
236 },
237 /// Independently resizable columns — dragging changes absolute column widths
238 /// and thus the overall table width. Like spreadsheets.
239 Resizable(Entity<ResizableColumnsState>),
240}
241
242pub enum StaticColumnWidths {
243 /// All columns share space equally (flex-1 / Length::Auto).
244 Auto,
245 /// Each column has a specific width.
246 Explicit(TableRow<DefiniteLength>),
247}
248
249impl ColumnWidthConfig {
250 /// Auto-width columns, auto-size table.
251 pub fn auto() -> Self {
252 ColumnWidthConfig::Static {
253 widths: StaticColumnWidths::Auto,
254 table_width: None,
255 }
256 }
257
258 /// Redistributable columns with no fixed table width.
259 pub fn redistributable(columns_state: Entity<RedistributableColumnsState>) -> Self {
260 ColumnWidthConfig::Redistributable {
261 columns_state,
262 table_width: None,
263 }
264 }
265
266 /// Auto-width columns, fixed table width.
267 pub fn auto_with_table_width(width: impl Into<DefiniteLength>) -> Self {
268 ColumnWidthConfig::Static {
269 widths: StaticColumnWidths::Auto,
270 table_width: Some(width.into()),
271 }
272 }
273
274 /// Explicit column widths with no fixed table width.
275 pub fn explicit<T: Into<DefiniteLength>>(widths: Vec<T>) -> Self {
276 let cols = widths.len();
277 ColumnWidthConfig::Static {
278 widths: StaticColumnWidths::Explicit(
279 widths
280 .into_iter()
281 .map(Into::into)
282 .collect::<Vec<_>>()
283 .into_table_row(cols),
284 ),
285 table_width: None,
286 }
287 }
288
289 /// Column widths for rendering.
290 pub fn widths_to_render(&self, cx: &App) -> Option<TableRow<Length>> {
291 match self {
292 ColumnWidthConfig::Static {
293 widths: StaticColumnWidths::Auto,
294 ..
295 } => None,
296 ColumnWidthConfig::Static {
297 widths: StaticColumnWidths::Explicit(widths),
298 ..
299 } => Some(widths.map_cloned(Length::Definite)),
300 ColumnWidthConfig::Redistributable {
301 columns_state: entity,
302 ..
303 } => Some(entity.read(cx).widths_to_render()),
304 ColumnWidthConfig::Resizable(entity) => {
305 let state = entity.read(cx);
306 Some(
307 state
308 .widths
309 .map_cloned(|abs| Length::Definite(DefiniteLength::Absolute(abs))),
310 )
311 }
312 }
313 }
314
315 /// Table-level width.
316 pub fn table_width(&self, window: &Window, cx: &App) -> Option<Length> {
317 match self {
318 ColumnWidthConfig::Static { table_width, .. }
319 | ColumnWidthConfig::Redistributable { table_width, .. } => {
320 table_width.map(Length::Definite)
321 }
322 ColumnWidthConfig::Resizable(entity) => {
323 let state = entity.read(cx);
324 let rem_size = window.rem_size();
325 let total: Pixels = state
326 .widths
327 .as_slice()
328 .iter()
329 .map(|abs| abs.to_pixels(rem_size))
330 .fold(px(0.), |acc, x| acc + x);
331 Some(Length::Definite(DefiniteLength::Absolute(
332 AbsoluteLength::Pixels(total),
333 )))
334 }
335 }
336 }
337
338 /// ListHorizontalSizingBehavior for uniform_list.
339 pub fn list_horizontal_sizing(
340 &self,
341 window: &Window,
342 cx: &App,
343 ) -> ListHorizontalSizingBehavior {
344 match self {
345 ColumnWidthConfig::Resizable(_) => ListHorizontalSizingBehavior::FitList,
346 _ => match self.table_width(window, cx) {
347 Some(_) => ListHorizontalSizingBehavior::Unconstrained,
348 None => ListHorizontalSizingBehavior::FitList,
349 },
350 }
351 }
352}
353
354/// A table component
355#[derive(RegisterComponent, IntoElement)]
356pub struct Table {
357 striped: bool,
358 show_row_borders: bool,
359 show_row_hover: bool,
360 headers: Option<TableRow<AnyElement>>,
361 rows: TableContents,
362 interaction_state: Option<WeakEntity<TableInteractionState>>,
363 column_width_config: ColumnWidthConfig,
364 map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
365 use_ui_font: bool,
366 empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
367 /// The number of columns in the table. Used to assert column numbers in `TableRow` collections
368 cols: usize,
369 disable_base_cell_style: bool,
370 pinned_cols: usize,
371 /// Optional per-column visibility mask. When set, it overrides any filter derived from the
372 /// column width config. Columns whose entry is `true` are hidden.
373 column_filter: Option<TableRow<bool>>,
374}
375
376impl Table {
377 /// Creates a new table with the specified number of columns.
378 pub fn new(cols: usize) -> Self {
379 Self {
380 cols,
381 striped: false,
382 show_row_borders: true,
383 show_row_hover: true,
384 headers: None,
385 rows: TableContents::Vec(Vec::new()),
386 interaction_state: None,
387 map_row: None,
388 use_ui_font: true,
389 empty_table_callback: None,
390 disable_base_cell_style: false,
391 column_width_config: ColumnWidthConfig::auto(),
392 pinned_cols: 0,
393 column_filter: None,
394 }
395 }
396
397 /// Sets a per-column visibility mask. Columns whose entry is `true` are filtered out (hidden).
398 ///
399 /// This is useful when column visibility is driven by state that isn't part of the column
400 /// width config (for example a `Static`/`Explicit` width config). When set, this overrides
401 /// any filter derived from the column width config.
402 pub fn column_filter(mut self, filter: TableRow<bool>) -> Self {
403 self.column_filter = Some(filter);
404 self
405 }
406
407 /// Disables based styling of row cell (paddings, text ellipsis, nowrap, etc), keeping width settings
408 ///
409 /// Doesn't affect base style of header cell.
410 /// Doesn't remove overflow-hidden
411 pub fn disable_base_style(mut self) -> Self {
412 self.disable_base_cell_style = true;
413 self
414 }
415
416 /// Enables uniform list rendering.
417 /// The provided function will be passed directly to the `uniform_list` element.
418 /// Therefore, if this method is called, any calls to [`Table::row`] before or after
419 /// this method is called will be ignored.
420 pub fn uniform_list(
421 mut self,
422 id: impl Into<ElementId>,
423 row_count: usize,
424 render_item_fn: impl Fn(
425 Range<usize>,
426 &mut Window,
427 &mut App,
428 ) -> Vec<UncheckedTableRow<AnyElement>>
429 + 'static,
430 ) -> Self {
431 self.rows = TableContents::UniformList(UniformListData {
432 element_id: id.into(),
433 row_count,
434 render_list_of_rows_fn: Box::new(render_item_fn),
435 });
436 self
437 }
438
439 /// Enables rendering of tables with variable row heights, allowing each row to have its own height.
440 ///
441 /// This mode is useful for displaying content such as CSV data or multiline cells, where rows may not have uniform heights.
442 /// 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.
443 ///
444 /// # Parameters
445 /// - `row_count`: The total number of rows in the table.
446 /// - `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.
447 /// - `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.
448 pub fn variable_row_height_list(
449 mut self,
450 row_count: usize,
451 list_state: ListState,
452 render_row_fn: impl Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement> + 'static,
453 ) -> Self {
454 self.rows = TableContents::VariableRowHeightList(VariableRowHeightListData {
455 render_row_fn: Box::new(render_row_fn),
456 list_state,
457 row_count,
458 });
459 self
460 }
461
462 /// Enables row striping (alternating row colors)
463 pub fn striped(mut self) -> Self {
464 self.striped = true;
465 self
466 }
467
468 /// Hides the border lines between rows
469 pub fn hide_row_borders(mut self) -> Self {
470 self.show_row_borders = false;
471 self
472 }
473
474 /// Sets a fixed table width with auto column widths.
475 ///
476 /// This is a shorthand for `.width_config(ColumnWidthConfig::auto_with_table_width(width))`.
477 /// For resizable columns or explicit column widths, use [`Table::width_config`] directly.
478 pub fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
479 self.column_width_config = ColumnWidthConfig::auto_with_table_width(width);
480 self
481 }
482
483 /// Sets the column width configuration for the table.
484 pub fn width_config(mut self, config: ColumnWidthConfig) -> Self {
485 self.column_width_config = config;
486 self
487 }
488
489 /// Enables interaction (primarily scrolling) with the table.
490 ///
491 /// Vertical scrolling will be enabled by default if the table is taller than its container.
492 ///
493 /// Horizontal scrolling will only be enabled if a table width is set via [`ColumnWidthConfig`],
494 /// otherwise the list will always shrink the table columns to fit their contents.
495 pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
496 self.interaction_state = Some(interaction_state.downgrade());
497 self
498 }
499
500 pub fn header(mut self, headers: UncheckedTableRow<impl IntoElement>) -> Self {
501 self.headers = Some(
502 headers
503 .into_table_row(self.cols)
504 .map(IntoElement::into_any_element),
505 );
506 self
507 }
508
509 pub fn row(mut self, items: UncheckedTableRow<impl IntoElement>) -> Self {
510 if let Some(rows) = self.rows.rows_mut() {
511 rows.push(
512 items
513 .into_table_row(self.cols)
514 .map(IntoElement::into_any_element),
515 );
516 }
517 self
518 }
519
520 pub fn no_ui_font(mut self) -> Self {
521 self.use_ui_font = false;
522 self
523 }
524
525 /// Pins the first `n` columns so they remain visible during horizontal scrolling.
526 ///
527 /// Pinned columns are rendered in the same list item as scrollable columns, so row heights
528 /// remain consistent across all columns including variable-height rows.
529 /// Only supported when using `ColumnWidthConfig::Resizable`.
530 pub fn pin_cols(mut self, n: usize) -> Self {
531 self.pinned_cols = n;
532 self
533 }
534
535 pub fn map_row(
536 mut self,
537 callback: impl Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement + 'static,
538 ) -> Self {
539 self.map_row = Some(Rc::new(callback));
540 self
541 }
542
543 /// Hides the default hover background on table rows.
544 /// Use this when you want to handle row hover styling manually via `map_row`.
545 pub fn hide_row_hover(mut self) -> Self {
546 self.show_row_hover = false;
547 self
548 }
549
550 /// Provide a callback that is invoked when the table is rendered without any rows
551 pub fn empty_table_callback(
552 mut self,
553 callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
554 ) -> Self {
555 self.empty_table_callback = Some(Rc::new(callback));
556 self
557 }
558}
559
560/// True when the table should render a pinned section and a separate scrollable section.
561/// `pinned_cols == 0` and `pinned_cols >= cols` both fall back to a single-section layout.
562fn is_pinned_layout(pinned_cols: usize, cols: usize) -> bool {
563 pinned_cols > 0 && pinned_cols < cols
564}
565
566fn base_cell_style(width: Option<Length>) -> Div {
567 div()
568 .px_1p5()
569 .when_some(width, |this, width| this.w(width))
570 .when(width.is_none(), |this| this.flex_1())
571 .whitespace_nowrap()
572 .text_ellipsis()
573 .overflow_hidden()
574}
575
576fn base_cell_style_text(width: Option<Length>, use_ui_font: bool, cx: &App) -> Div {
577 base_cell_style(width).when(use_ui_font, |el| el.text_ui(cx))
578}
579
580fn render_cell(width: Option<Length>, cell: AnyElement, ctx: &TableRenderContext, cx: &App) -> Div {
581 if ctx.disable_base_cell_style {
582 div()
583 .when_some(width, |this, width| this.w(width))
584 .when(width.is_none(), |this| this.flex_1())
585 .overflow_hidden()
586 .child(cell)
587 } else {
588 base_cell_style_text(width, ctx.use_ui_font, cx)
589 .px_1()
590 .py_0p5()
591 .child(cell)
592 }
593}
594
595fn render_header_cell(
596 header: AnyElement,
597 width: Option<Length>,
598 header_idx: usize,
599 shared_element_id: &SharedString,
600 resize_info: Option<&HeaderResizeInfo>,
601 use_ui_font: bool,
602 cx: &App,
603) -> Stateful<Div> {
604 base_cell_style_text(width, use_ui_font, cx)
605 .px_1()
606 .py_0p5()
607 .child(header)
608 .id(ElementId::NamedInteger(
609 shared_element_id.clone(),
610 header_idx as u64,
611 ))
612 .when_some(resize_info.cloned(), |this, info| {
613 if info.resize_behavior[header_idx].is_resizable() {
614 this.on_click(move |event, window, cx| {
615 if event.click_count() > 1 {
616 info.reset_column(header_idx, window, cx);
617 }
618 })
619 } else {
620 this
621 }
622 })
623}
624
625pub fn render_table_row(
626 row_index: usize,
627 items: TableRow<impl IntoElement>,
628 table_context: TableRenderContext,
629 window: &mut Window,
630 cx: &mut App,
631) -> AnyElement {
632 let is_striped = table_context.striped;
633 let is_last = row_index == table_context.total_row_count - 1;
634 let bg = if row_index % 2 == 1 && is_striped {
635 Some(cx.theme().colors().text.opacity(0.05))
636 } else {
637 None
638 };
639 let cols = items.cols();
640 let column_widths = match &table_context.column_widths {
641 Some(widths) => widths.clone().map(Some),
642 None => vec![None; cols].into_table_row(cols),
643 };
644
645 let mut row = div()
646 // NOTE: `h_flex()` sneakily applies `items_center()` which is not default behavior for div element.
647 // Applying `.flex().flex_row()` manually to overcome that
648 .flex()
649 .flex_row()
650 .id(("table_row", row_index))
651 .size_full()
652 .when_some(bg, |row, bg| row.bg(bg))
653 .when(table_context.show_row_hover, |row| {
654 row.hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.6)))
655 })
656 .when(!is_striped && table_context.show_row_borders, |row| {
657 row.border_b_1()
658 .border_color(transparent_black())
659 .when(!is_last, |row| row.border_color(cx.theme().colors().border))
660 });
661
662 let pinned_cols = table_context.pinned_cols;
663 let column_filter = &table_context.column_filter;
664
665 if is_pinned_layout(pinned_cols, cols) {
666 let items_vec: Vec<AnyElement> = items.map(IntoElement::into_any_element).into_vec();
667 let widths_vec: Vec<Option<Length>> = column_widths.into_vec();
668
669 // Drop filtered columns before splitting into pinned/scrollable sections. The number of
670 // pinned columns that survive filtering determines where the kept cells are split.
671 let pinned_visible = (0..pinned_cols)
672 .filter(|&idx| column_is_visible(column_filter, idx))
673 .count();
674 let mut kept: Vec<(AnyElement, Option<Length>)> = items_vec
675 .into_iter()
676 .zip(widths_vec)
677 .enumerate()
678 .filter(|(idx, _)| column_is_visible(column_filter, *idx))
679 .map(|(_, pair)| pair)
680 .collect();
681 let scrollable: Vec<(AnyElement, Option<Length>)> = kept.drain(pinned_visible..).collect();
682
683 let pinned_section = div().flex().flex_row().flex_shrink_0().children(
684 kept.into_iter()
685 .map(|(cell, width)| render_cell(width, cell, &table_context, cx)),
686 );
687
688 // Scrollable section: overflow_x_scroll + track_scroll so GPUI handles the visual
689 // shift natively without requiring per-scroll re-renders of list items.
690 // restrict_scroll_to_axis lets vertical scroll events pass through to the list.
691 let mut scrollable_section = div()
692 .id(("table-row-scrollable", row_index as u64))
693 .flex_grow_1()
694 .overflow_x_scroll()
695 .flex()
696 .child(
697 div().flex().flex_row().children(
698 scrollable
699 .into_iter()
700 .map(|(cell, width)| render_cell(width, cell, &table_context, cx)),
701 ),
702 );
703
704 if let Some(ref handle) = table_context.h_scroll_handle {
705 scrollable_section = scrollable_section.track_scroll(handle);
706 }
707 scrollable_section.style().restrict_scroll_to_axis = Some(true);
708
709 row = row.child(pinned_section).child(scrollable_section);
710 } else {
711 row = row.children(
712 items
713 .map(IntoElement::into_any_element)
714 .into_vec()
715 .into_iter()
716 .zip(column_widths.into_vec())
717 .enumerate()
718 .filter(|(idx, _)| column_is_visible(column_filter, *idx))
719 .map(|(_, (cell, width))| render_cell(width, cell, &table_context, cx)),
720 );
721 }
722
723 let row = if let Some(map_row) = table_context.map_row {
724 map_row((row_index, row), window, cx)
725 } else {
726 row.into_any_element()
727 };
728
729 div().size_full().child(row).into_any_element()
730}
731
732pub fn render_table_header(
733 headers: TableRow<impl IntoElement>,
734 table_context: TableRenderContext,
735 resize_info: Option<HeaderResizeInfo>,
736 entity_id: Option<EntityId>,
737 cx: &mut App,
738) -> AnyElement {
739 let cols = headers.cols();
740 let column_widths = table_context
741 .column_widths
742 .map_or(vec![None; cols].into_table_row(cols), |widths| {
743 widths.map(Some)
744 });
745
746 let element_id = entity_id
747 .map(|entity| entity.to_string())
748 .unwrap_or_default();
749
750 let shared_element_id: SharedString = format!("table-{}", element_id).into();
751 let pinned_cols = table_context.pinned_cols;
752
753 let outer = h_flex()
754 .py_1()
755 .w_full()
756 .border_b_1()
757 .border_color(cx.theme().colors().border_variant);
758
759 let use_ui_font = table_context.use_ui_font;
760 let resize_info_ref = resize_info.as_ref();
761 let column_filter = &table_context.column_filter;
762
763 if is_pinned_layout(pinned_cols, cols) {
764 let headers_vec: Vec<AnyElement> = headers
765 .into_vec()
766 .into_iter()
767 .map(IntoElement::into_any_element)
768 .collect();
769 let widths_vec: Vec<Option<Length>> = column_widths.into_vec();
770
771 // Keep the original column index alongside each visible header so that resize info
772 // (which is indexed by original column position) stays correct after filtering.
773 let pinned_visible = (0..pinned_cols)
774 .filter(|&idx| column_is_visible(column_filter, idx))
775 .count();
776 let mut kept: Vec<(usize, AnyElement, Option<Length>)> = headers_vec
777 .into_iter()
778 .zip(widths_vec)
779 .enumerate()
780 .filter(|(idx, _)| column_is_visible(column_filter, *idx))
781 .map(|(idx, (h, width))| (idx, h, width))
782 .collect();
783 let scrollable: Vec<(usize, AnyElement, Option<Length>)> =
784 kept.drain(pinned_visible..).collect();
785
786 let pinned_section =
787 div()
788 .flex()
789 .flex_row()
790 .flex_shrink_0()
791 .children(kept.into_iter().map(|(header_idx, h, width)| {
792 render_header_cell(
793 h,
794 width,
795 header_idx,
796 &shared_element_id,
797 resize_info_ref,
798 use_ui_font,
799 cx,
800 )
801 }));
802
803 let inner = div().flex().flex_row().children(scrollable.into_iter().map(
804 |(header_idx, h, width)| {
805 render_header_cell(
806 h,
807 width,
808 header_idx,
809 &shared_element_id,
810 resize_info_ref,
811 use_ui_font,
812 cx,
813 )
814 },
815 ));
816 let mut scrollable_section = div()
817 .id("table-header-scrollable")
818 .flex_grow_1()
819 .overflow_x_scroll()
820 .flex()
821 .child(inner);
822
823 if let Some(ref handle) = table_context.h_scroll_handle {
824 scrollable_section = scrollable_section.track_scroll(handle);
825 }
826 scrollable_section.style().restrict_scroll_to_axis = Some(true);
827
828 outer
829 .child(pinned_section)
830 .child(scrollable_section)
831 .into_any_element()
832 } else {
833 outer
834 .children(
835 headers
836 .into_vec()
837 .into_iter()
838 .enumerate()
839 .zip(column_widths.into_vec())
840 .filter(|((idx, _), _)| column_is_visible(column_filter, *idx))
841 .map(|((header_idx, h), width)| {
842 render_header_cell(
843 h.into_any_element(),
844 width,
845 header_idx,
846 &shared_element_id,
847 resize_info_ref,
848 use_ui_font,
849 cx,
850 )
851 }),
852 )
853 .into_any_element()
854 }
855}
856
857#[derive(Clone)]
858pub struct TableRenderContext {
859 pub striped: bool,
860 pub show_row_borders: bool,
861 pub show_row_hover: bool,
862 pub total_row_count: usize,
863 pub column_widths: Option<TableRow<Length>>,
864 pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
865 pub use_ui_font: bool,
866 pub disable_base_cell_style: bool,
867 pub pinned_cols: usize,
868 /// Per-column visibility mask. When `Some`, columns whose entry is `true` are filtered
869 /// out (hidden) and not rendered. Indices map to the table's original column positions.
870 pub column_filter: Option<TableRow<bool>>,
871 /// Scroll handle shared by all scrollable sections in rows and headers.
872 /// When `pinned_cols > 0`, each row's scrollable section tracks this handle so all rows
873 /// scroll together without requiring per-scroll re-renders.
874 pub h_scroll_handle: Option<ScrollHandle>,
875}
876
877impl TableRenderContext {
878 fn new(table: &Table, h_scroll_handle: Option<ScrollHandle>, cx: &App) -> Self {
879 Self {
880 striped: table.striped,
881 show_row_borders: table.show_row_borders,
882 show_row_hover: table.show_row_hover,
883 total_row_count: table.rows.len(),
884 column_widths: table
885 .column_width_config
886 .widths_to_render(cx)
887 .map(|widths| redistribute_hidden_widths(&widths, table.column_filter.as_ref())),
888 map_row: table.map_row.clone(),
889 use_ui_font: table.use_ui_font,
890 disable_base_cell_style: table.disable_base_cell_style,
891 pinned_cols: table.pinned_cols,
892 column_filter: table.column_filter.clone(),
893 h_scroll_handle,
894 }
895 }
896
897 pub fn for_column_widths(column_widths: Option<TableRow<Length>>, use_ui_font: bool) -> Self {
898 Self {
899 striped: false,
900 show_row_borders: true,
901 show_row_hover: true,
902 total_row_count: 0,
903 column_widths,
904 map_row: None,
905 use_ui_font,
906 disable_base_cell_style: false,
907 pinned_cols: 0,
908 column_filter: None,
909 h_scroll_handle: None,
910 }
911 }
912
913 /// Sets the per-column visibility mask. Columns whose entry is `true` are hidden.
914 pub fn with_column_filter(mut self, column_filter: Option<TableRow<bool>>) -> Self {
915 self.column_filter = column_filter;
916 self
917 }
918}
919
920/// Returns `true` when the column at `col_idx` should be rendered (i.e. not filtered out).
921fn column_is_visible(filter: &Option<TableRow<bool>>, col_idx: usize) -> bool {
922 filter
923 .as_ref()
924 .map_or(true, |mask| !mask.get(col_idx).copied().unwrap_or(false))
925}
926
927/// Builds resize dividers for the given column range, positioned absolutely from `left: 0`.
928/// When `interactive` is false, dividers render as plain visual lines with no drag/click handlers.
929fn build_resize_dividers(
930 columns_state: &Entity<ResizableColumnsState>,
931 widths: &TableRow<AbsoluteLength>,
932 resize_behavior: &TableRow<TableResizeBehavior>,
933 range: Range<usize>,
934 interactive: bool,
935 rem_size: Pixels,
936 window: &mut Window,
937 cx: &mut App,
938) -> Vec<AnyElement> {
939 let entity_id = columns_state.entity_id();
940 let last = range.end.saturating_sub(1);
941 let mut dividers = Vec::with_capacity(range.end - range.start);
942 let mut accumulated = px(0.);
943
944 for col_idx in range {
945 accumulated = accumulated + widths[col_idx].to_pixels(rem_size);
946
947 // Add a resize divider after every column, including the last.
948 // For the last column the divider is pulled 1px inward so it isn't clipped
949 // by the overflow_hidden content container.
950 let divider_left = if col_idx == last {
951 accumulated - px(RESIZE_DIVIDER_WIDTH)
952 } else {
953 accumulated
954 };
955 let divider = div().id(col_idx).absolute().top_0().left(divider_left);
956 let on_reset: Rc<dyn Fn(&mut Window, &mut App)> = {
957 let columns_state = columns_state.clone();
958 Rc::new(move |_window, cx| {
959 columns_state.update(cx, |state, cx| {
960 state.reset_column_to_initial_width(col_idx);
961 cx.notify();
962 });
963 })
964 };
965 let is_resizable = interactive && resize_behavior[col_idx].is_resizable();
966 dividers.push(render_column_resize_divider(
967 divider,
968 col_idx,
969 is_resizable,
970 entity_id,
971 on_reset,
972 None,
973 window,
974 cx,
975 ));
976 }
977 dividers
978}
979
980fn render_resize_handles_resizable(
981 columns_state: &Entity<ResizableColumnsState>,
982 pinned_cols: usize,
983 h_scroll_handle: Option<&ScrollHandle>,
984 window: &mut Window,
985 cx: &mut App,
986) -> AnyElement {
987 let state = columns_state.read(cx);
988 let widths = state.widths.clone();
989 let resize_behavior = state.resize_behavior().clone();
990
991 let rem_size = window.rem_size();
992 let n_cols = widths.cols();
993 let pinned_cols = pinned_cols.min(n_cols);
994
995 if pinned_cols == 0 {
996 let dividers = build_resize_dividers(
997 columns_state,
998 &widths,
999 &resize_behavior,
1000 0..n_cols,
1001 true,
1002 rem_size,
1003 window,
1004 cx,
1005 );
1006 return div()
1007 .id("resize-handles")
1008 .absolute()
1009 .inset_0()
1010 .w_full()
1011 .children(dividers)
1012 .into_any_element();
1013 }
1014
1015 let pinned_width = state.pinned_width(pinned_cols, rem_size);
1016 let total_scrollable_width = state.scrollable_width(pinned_cols, rem_size);
1017
1018 // Non-interactive: pinned columns don't visually shift with scroll, so resizing them would
1019 // need separate drag-math from the scrollable columns. Header double-click reset still works.
1020 let pinned_dividers = build_resize_dividers(
1021 columns_state,
1022 &widths,
1023 &resize_behavior,
1024 0..pinned_cols,
1025 false,
1026 rem_size,
1027 window,
1028 cx,
1029 );
1030 let pinned_overlay = div()
1031 .id("resize-handles-pinned")
1032 .absolute()
1033 .top_0()
1034 .bottom_0()
1035 .left_0()
1036 .w(pinned_width)
1037 .children(pinned_dividers);
1038
1039 let scrollable_dividers = build_resize_dividers(
1040 columns_state,
1041 &widths,
1042 &resize_behavior,
1043 pinned_cols..n_cols,
1044 true,
1045 rem_size,
1046 window,
1047 cx,
1048 );
1049
1050 // Sized inner div gives the overflow container something to scroll against.
1051 let inner = div()
1052 .relative()
1053 .w(total_scrollable_width)
1054 .h_full()
1055 .children(scrollable_dividers);
1056
1057 let mut overlay = div()
1058 .id("resize-handles")
1059 .absolute()
1060 .top_0()
1061 .bottom_0()
1062 .left(pinned_width)
1063 .right_0()
1064 .overflow_x_scroll()
1065 .child(inner);
1066
1067 if let Some(handle) = h_scroll_handle {
1068 overlay = overlay.track_scroll(handle);
1069 }
1070 overlay.style().restrict_scroll_to_axis = Some(true);
1071
1072 div()
1073 .id("resize-handles-wrapper")
1074 .absolute()
1075 .inset_0()
1076 .child(pinned_overlay)
1077 .child(overlay)
1078 .into_any_element()
1079}
1080
1081impl RenderOnce for Table {
1082 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1083 let interaction_state = self
1084 .interaction_state
1085 .clone()
1086 .and_then(|state| state.upgrade());
1087 let uses_pinned_layout = is_pinned_layout(self.pinned_cols, self.cols);
1088 let pinned_cols = if uses_pinned_layout {
1089 self.pinned_cols
1090 } else {
1091 0
1092 };
1093
1094 // Shared by every row's scrollable section so they scroll in lockstep, and read by
1095 // on_drag_move to compensate drag_x for the horizontal scroll offset.
1096 let h_scroll_handle = if uses_pinned_layout {
1097 interaction_state
1098 .as_ref()
1099 .map(|s| s.read(cx).horizontal_scroll_handle.clone())
1100 } else {
1101 None
1102 };
1103
1104 let table_context = TableRenderContext::new(&self, h_scroll_handle.clone(), cx);
1105
1106 let header_resize_info =
1107 interaction_state
1108 .as_ref()
1109 .and_then(|_| match &self.column_width_config {
1110 ColumnWidthConfig::Redistributable { columns_state, .. } => {
1111 Some(HeaderResizeInfo::from_redistributable(columns_state, cx))
1112 }
1113 ColumnWidthConfig::Resizable(entity) => {
1114 Some(HeaderResizeInfo::from_resizable(entity, cx))
1115 }
1116 _ => None,
1117 });
1118
1119 // Pinned mode sizes each row internally, so no fixed table_width or h_scroll_container.
1120 let table_width = if uses_pinned_layout {
1121 None
1122 } else {
1123 self.column_width_config.table_width(window, cx)
1124 };
1125
1126 let horizontal_sizing = if uses_pinned_layout {
1127 ListHorizontalSizingBehavior::FitList
1128 } else {
1129 self.column_width_config.list_horizontal_sizing(window, cx)
1130 };
1131
1132 let no_rows_rendered = self.rows.is_empty();
1133 let variable_list_state = if let TableContents::VariableRowHeightList(data) = &self.rows {
1134 Some(data.list_state.clone())
1135 } else {
1136 None
1137 };
1138
1139 let (redistributable_entity, resizable_entity, resize_handles) =
1140 if let Some(_) = interaction_state.as_ref() {
1141 match &self.column_width_config {
1142 ColumnWidthConfig::Redistributable { columns_state, .. } => (
1143 Some(columns_state.clone()),
1144 None,
1145 Some(render_redistributable_columns_resize_handles(
1146 columns_state,
1147 self.column_filter.as_ref(),
1148 window,
1149 cx,
1150 )),
1151 ),
1152 ColumnWidthConfig::Resizable(entity) => (
1153 None,
1154 Some(entity.clone()),
1155 Some(render_resize_handles_resizable(
1156 entity,
1157 pinned_cols,
1158 h_scroll_handle.as_ref(),
1159 window,
1160 cx,
1161 )),
1162 ),
1163 _ => (None, None, None),
1164 }
1165 } else {
1166 (None, None, None)
1167 };
1168
1169 let is_resizable = resizable_entity.is_some();
1170
1171 let table = div()
1172 .when_some(table_width, |this, width| this.w(width))
1173 .h_full()
1174 .v_flex()
1175 .when_some(self.headers.take(), |this, headers| {
1176 this.child(render_table_header(
1177 headers,
1178 table_context.clone(),
1179 header_resize_info,
1180 interaction_state.as_ref().map(Entity::entity_id),
1181 cx,
1182 ))
1183 })
1184 .when_some(redistributable_entity, |this, widths| {
1185 bind_redistributable_columns(this, widths, self.column_filter.clone())
1186 })
1187 .when_some(resizable_entity, |this, entity| {
1188 let scroll_handle_for_drag = h_scroll_handle.clone();
1189 this.on_drag_move::<DraggedColumn>(move |event, window, cx| {
1190 if event.drag(cx).state_id != entity.entity_id() {
1191 return;
1192 }
1193 let h_scroll_offset = scroll_handle_for_drag
1194 .as_ref()
1195 .map(|h| h.offset().x)
1196 .unwrap_or(px(0.));
1197 entity.update(cx, |state, cx| {
1198 state.on_drag_move(event, h_scroll_offset, window, cx)
1199 });
1200 })
1201 })
1202 .child({
1203 let content = div()
1204 .flex_grow_1()
1205 .w_full()
1206 .relative()
1207 .overflow_hidden()
1208 .map(|parent| match self.rows {
1209 TableContents::Vec(items) => {
1210 parent.children(items.into_iter().enumerate().map(|(index, row)| {
1211 div().child(render_table_row(
1212 index,
1213 row,
1214 table_context.clone(),
1215 window,
1216 cx,
1217 ))
1218 }))
1219 }
1220 TableContents::UniformList(uniform_list_data) => parent.child(
1221 uniform_list(
1222 uniform_list_data.element_id,
1223 uniform_list_data.row_count,
1224 {
1225 let render_item_fn = uniform_list_data.render_list_of_rows_fn;
1226 move |range: Range<usize>, window, cx| {
1227 let elements = render_item_fn(range.clone(), window, cx)
1228 .into_iter()
1229 .map(|raw_row| raw_row.into_table_row(self.cols))
1230 .collect::<Vec<_>>();
1231 elements
1232 .into_iter()
1233 .zip(range)
1234 .map(|(row, row_index)| {
1235 render_table_row(
1236 row_index,
1237 row,
1238 table_context.clone(),
1239 window,
1240 cx,
1241 )
1242 })
1243 .collect()
1244 }
1245 },
1246 )
1247 .size_full()
1248 .flex_grow_1()
1249 .with_sizing_behavior(ListSizingBehavior::Auto)
1250 .with_horizontal_sizing_behavior(horizontal_sizing)
1251 .when_some(
1252 interaction_state.as_ref(),
1253 |this, state| {
1254 this.track_scroll(
1255 &state.read_with(cx, |s, _| s.scroll_handle.clone()),
1256 )
1257 },
1258 ),
1259 ),
1260 TableContents::VariableRowHeightList(variable_list_data) => parent.child(
1261 list(variable_list_data.list_state.clone(), {
1262 let render_item_fn = variable_list_data.render_row_fn;
1263 move |row_index: usize, window: &mut Window, cx: &mut App| {
1264 let row = render_item_fn(row_index, window, cx)
1265 .into_table_row(self.cols);
1266 render_table_row(
1267 row_index,
1268 row,
1269 table_context.clone(),
1270 window,
1271 cx,
1272 )
1273 }
1274 })
1275 .size_full()
1276 .flex_grow_1()
1277 .with_sizing_behavior(ListSizingBehavior::Auto),
1278 ),
1279 })
1280 .when_some(resize_handles, |parent, handles| parent.child(handles));
1281
1282 content.into_any_element()
1283 })
1284 .when_some(
1285 no_rows_rendered
1286 .then_some(self.empty_table_callback)
1287 .flatten(),
1288 |this, callback| {
1289 this.child(
1290 h_flex()
1291 .size_full()
1292 .p_3()
1293 .items_start()
1294 .justify_center()
1295 .child(callback(window, cx)),
1296 )
1297 },
1298 );
1299
1300 if let Some(state) = interaction_state.as_ref() {
1301 let content = if is_resizable && !uses_pinned_layout {
1302 let mut h_scroll_container = div()
1303 .id("table-h-scroll")
1304 .overflow_x_scroll()
1305 .flex_grow_1()
1306 .h_full()
1307 .track_scroll(&state.read(cx).horizontal_scroll_handle)
1308 .child(table);
1309 h_scroll_container.style().restrict_scroll_to_axis = Some(true);
1310 div().size_full().child(h_scroll_container)
1311 } else {
1312 table
1313 };
1314
1315 // Attach vertical scrollbars (converts Div → Stateful<Div>)
1316 let scrollbars = state
1317 .read(cx)
1318 .custom_scrollbar
1319 .clone()
1320 .unwrap_or_else(|| Scrollbars::new(ScrollAxes::Both));
1321 let mut content = if let Some(list_state) = variable_list_state {
1322 content.custom_scrollbars(scrollbars.tracked_scroll_handle(&list_state), window, cx)
1323 } else {
1324 content.custom_scrollbars(
1325 scrollbars.tracked_scroll_handle(&state.read(cx).scroll_handle),
1326 window,
1327 cx,
1328 )
1329 };
1330
1331 // Works for both modes since they share horizontal_scroll_handle.
1332 if is_resizable {
1333 if let ColumnWidthConfig::Resizable(rc_state) = self.column_width_config {
1334 let pinned_width = rc_state
1335 .read(cx)
1336 .pinned_width(pinned_cols, window.rem_size());
1337 // With pinned columns the scrollbar should only span the scrollable area.
1338 // An absolute overlay inset on all sides then overriding left to pinned_width
1339 // gives the correct bounds (full height via top+bottom, correct width via
1340 // right+left) without needing to hardcode the scrollbar thickness.
1341 let h_scrollbar = div().absolute().inset_0().left(pinned_width);
1342 let h_scrollbar = h_scrollbar.custom_scrollbars(
1343 Scrollbars::new(ScrollAxes::Horizontal)
1344 .tracked_scroll_handle(&state.read(cx).horizontal_scroll_handle),
1345 window,
1346 cx,
1347 );
1348 content = content.child(h_scrollbar);
1349 } else {
1350 content = content.custom_scrollbars(
1351 Scrollbars::new(ScrollAxes::Horizontal)
1352 .tracked_scroll_handle(&state.read(cx).horizontal_scroll_handle),
1353 window,
1354 cx,
1355 );
1356 }
1357 }
1358 content.style().restrict_scroll_to_axis = Some(true);
1359
1360 content
1361 .track_focus(&state.read(cx).focus_handle)
1362 .id(("table", state.entity_id()))
1363 .into_any_element()
1364 } else {
1365 table.into_any_element()
1366 }
1367 }
1368}
1369
1370impl Component for Table {
1371 fn scope() -> ComponentScope {
1372 ComponentScope::Layout
1373 }
1374
1375 fn description() -> &'static str {
1376 "A table component for displaying data in rows and columns with optional styling."
1377 }
1378
1379 fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
1380 v_flex()
1381 .gap_6()
1382 .children(vec![
1383 example_group_with_title(
1384 "Basic Tables",
1385 vec![
1386 single_example(
1387 "Simple Table",
1388 Table::new(3)
1389 .width(px(400.))
1390 .header(vec!["Name", "Age", "City"])
1391 .row(vec!["Alice", "28", "New York"])
1392 .row(vec!["Bob", "32", "San Francisco"])
1393 .row(vec!["Charlie", "25", "London"])
1394 .into_any_element(),
1395 ),
1396 single_example(
1397 "Two Column Table",
1398 Table::new(2)
1399 .header(vec!["Category", "Value"])
1400 .width(px(300.))
1401 .row(vec!["Revenue", "$100,000"])
1402 .row(vec!["Expenses", "$75,000"])
1403 .row(vec!["Profit", "$25,000"])
1404 .into_any_element(),
1405 ),
1406 ],
1407 ),
1408 example_group_with_title(
1409 "Styled Tables",
1410 vec![
1411 single_example(
1412 "Default",
1413 Table::new(3)
1414 .width(px(400.))
1415 .header(vec!["Product", "Price", "Stock"])
1416 .row(vec!["Laptop", "$999", "In Stock"])
1417 .row(vec!["Phone", "$599", "Low Stock"])
1418 .row(vec!["Tablet", "$399", "Out of Stock"])
1419 .into_any_element(),
1420 ),
1421 single_example(
1422 "Striped",
1423 Table::new(3)
1424 .width(px(400.))
1425 .striped()
1426 .header(vec!["Product", "Price", "Stock"])
1427 .row(vec!["Laptop", "$999", "In Stock"])
1428 .row(vec!["Phone", "$599", "Low Stock"])
1429 .row(vec!["Tablet", "$399", "Out of Stock"])
1430 .row(vec!["Headphones", "$199", "In Stock"])
1431 .into_any_element(),
1432 ),
1433 ],
1434 ),
1435 example_group_with_title(
1436 "Mixed Content Table",
1437 vec![single_example(
1438 "Table with Elements",
1439 Table::new(5)
1440 .width(px(840.))
1441 .header(vec!["Status", "Name", "Priority", "Deadline", "Action"])
1442 .row(vec![
1443 Indicator::dot().color(Color::Success).into_any_element(),
1444 "Project A".into_any_element(),
1445 "High".into_any_element(),
1446 "2023-12-31".into_any_element(),
1447 Button::new("view_a", "View")
1448 .style(ButtonStyle::Filled)
1449 .full_width()
1450 .into_any_element(),
1451 ])
1452 .row(vec![
1453 Indicator::dot().color(Color::Warning).into_any_element(),
1454 "Project B".into_any_element(),
1455 "Medium".into_any_element(),
1456 "2024-03-15".into_any_element(),
1457 Button::new("view_b", "View")
1458 .style(ButtonStyle::Filled)
1459 .full_width()
1460 .into_any_element(),
1461 ])
1462 .row(vec![
1463 Indicator::dot().color(Color::Error).into_any_element(),
1464 "Project C".into_any_element(),
1465 "Low".into_any_element(),
1466 "2024-06-30".into_any_element(),
1467 Button::new("view_c", "View")
1468 .style(ButtonStyle::Filled)
1469 .full_width()
1470 .into_any_element(),
1471 ])
1472 .into_any_element(),
1473 )],
1474 ),
1475 ])
1476 .into_any_element()
1477 }
1478}
1479