Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:32:29.824Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

data_table.rs

1479 lines · 55.9 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.