table.rs

  1use std::{ops::Range, time::Duration};
  2
  3use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide};
  4use gpui::{
  5    AppContext, Axis, Context, Entity, FocusHandle, FontWeight, Length,
  6    ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Task, UniformListScrollHandle,
  7    WeakEntity, uniform_list,
  8};
  9use settings::Settings as _;
 10use ui::{
 11    ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
 12    ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator,
 13    InteractiveElement as _, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
 14    Scrollbar, ScrollbarState, StatefulInteractiveElement as _, Styled, StyledExt as _,
 15    StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex,
 16};
 17
 18struct UniformListData<const COLS: usize> {
 19    render_item_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>,
 20    element_id: ElementId,
 21    row_count: usize,
 22}
 23
 24enum TableContents<const COLS: usize> {
 25    Vec(Vec<[AnyElement; COLS]>),
 26    UniformList(UniformListData<COLS>),
 27}
 28
 29impl<const COLS: usize> TableContents<COLS> {
 30    fn rows_mut(&mut self) -> Option<&mut Vec<[AnyElement; COLS]>> {
 31        match self {
 32            TableContents::Vec(rows) => Some(rows),
 33            TableContents::UniformList(_) => None,
 34        }
 35    }
 36
 37    fn len(&self) -> usize {
 38        match self {
 39            TableContents::Vec(rows) => rows.len(),
 40            TableContents::UniformList(data) => data.row_count,
 41        }
 42    }
 43}
 44
 45pub struct TableInteractionState {
 46    pub focus_handle: FocusHandle,
 47    pub scroll_handle: UniformListScrollHandle,
 48    pub horizontal_scrollbar: ScrollbarProperties,
 49    pub vertical_scrollbar: ScrollbarProperties,
 50}
 51
 52impl TableInteractionState {
 53    pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
 54        cx.new(|cx| {
 55            let focus_handle = cx.focus_handle();
 56
 57            cx.on_focus_out(&focus_handle, window, |this: &mut Self, _, window, cx| {
 58                this.hide_scrollbars(window, cx);
 59            })
 60            .detach();
 61
 62            let scroll_handle = UniformListScrollHandle::new();
 63            let vertical_scrollbar = ScrollbarProperties {
 64                axis: Axis::Vertical,
 65                state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
 66                show_scrollbar: false,
 67                show_track: false,
 68                auto_hide: false,
 69                hide_task: None,
 70            };
 71
 72            let horizontal_scrollbar = ScrollbarProperties {
 73                axis: Axis::Horizontal,
 74                state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
 75                show_scrollbar: false,
 76                show_track: false,
 77                auto_hide: false,
 78                hide_task: None,
 79            };
 80
 81            let mut this = Self {
 82                focus_handle,
 83                scroll_handle,
 84                horizontal_scrollbar,
 85                vertical_scrollbar,
 86            };
 87
 88            this.update_scrollbar_visibility(cx);
 89            this
 90        })
 91    }
 92
 93    fn update_scrollbar_visibility(&mut self, cx: &mut Context<Self>) {
 94        let show_setting = EditorSettings::get_global(cx).scrollbar.show;
 95
 96        let scroll_handle = self.scroll_handle.0.borrow();
 97
 98        let autohide = |show: ShowScrollbar, cx: &mut Context<Self>| match show {
 99            ShowScrollbar::Auto => true,
100            ShowScrollbar::System => cx
101                .try_global::<ScrollbarAutoHide>()
102                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
103            ShowScrollbar::Always => false,
104            ShowScrollbar::Never => false,
105        };
106
107        let longest_item_width = scroll_handle.last_item_size.and_then(|size| {
108            (size.contents.width > size.item.width).then_some(size.contents.width)
109        });
110
111        // is there an item long enough that we should show a horizontal scrollbar?
112        let item_wider_than_container = if let Some(longest_item_width) = longest_item_width {
113            longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0)
114        } else {
115            true
116        };
117
118        let show_scrollbar = match show_setting {
119            ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
120            ShowScrollbar::Never => false,
121        };
122        let show_vertical = show_scrollbar;
123
124        let show_horizontal = item_wider_than_container && show_scrollbar;
125
126        let show_horizontal_track =
127            show_horizontal && matches!(show_setting, ShowScrollbar::Always);
128
129        // TODO: we probably should hide the scroll track when the list doesn't need to scroll
130        let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always);
131
132        self.vertical_scrollbar = ScrollbarProperties {
133            axis: self.vertical_scrollbar.axis,
134            state: self.vertical_scrollbar.state.clone(),
135            show_scrollbar: show_vertical,
136            show_track: show_vertical_track,
137            auto_hide: autohide(show_setting, cx),
138            hide_task: None,
139        };
140
141        self.horizontal_scrollbar = ScrollbarProperties {
142            axis: self.horizontal_scrollbar.axis,
143            state: self.horizontal_scrollbar.state.clone(),
144            show_scrollbar: show_horizontal,
145            show_track: show_horizontal_track,
146            auto_hide: autohide(show_setting, cx),
147            hide_task: None,
148        };
149
150        cx.notify();
151    }
152
153    fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
154        self.horizontal_scrollbar.hide(window, cx);
155        self.vertical_scrollbar.hide(window, cx);
156    }
157
158    // fn listener(this: Entity<Self>, fn: F) ->
159
160    pub fn listener<E: ?Sized>(
161        this: &Entity<Self>,
162        f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static,
163    ) -> impl Fn(&E, &mut Window, &mut App) + 'static {
164        let view = this.downgrade();
165        move |e: &E, window: &mut Window, cx: &mut App| {
166            view.update(cx, |view, cx| f(view, e, window, cx)).ok();
167        }
168    }
169
170    fn render_vertical_scrollbar(this: &Entity<Self>, cx: &mut App) -> impl IntoElement {
171        div()
172            .id("keymap-editor-vertical-scroll")
173            .occlude()
174            .flex_none()
175            .h_full()
176            .cursor_default()
177            .absolute()
178            .right_0()
179            .top_0()
180            .bottom_0()
181            .w(px(12.))
182            .on_mouse_move(Self::listener(this, |_, _, _, cx| {
183                cx.notify();
184                cx.stop_propagation()
185            }))
186            .on_hover(|_, _, cx| {
187                cx.stop_propagation();
188            })
189            .on_mouse_up(
190                MouseButton::Left,
191                Self::listener(this, |this, _, window, cx| {
192                    if !this.vertical_scrollbar.state.is_dragging()
193                        && !this.focus_handle.contains_focused(window, cx)
194                    {
195                        this.vertical_scrollbar.hide(window, cx);
196                        cx.notify();
197                    }
198
199                    cx.stop_propagation();
200                }),
201            )
202            .on_any_mouse_down(|_, _, cx| {
203                cx.stop_propagation();
204            })
205            .on_scroll_wheel(Self::listener(&this, |_, _, _, cx| {
206                cx.notify();
207            }))
208            .children(Scrollbar::vertical(
209                this.read(cx).vertical_scrollbar.state.clone(),
210            ))
211    }
212
213    /// Renders the horizontal scrollbar.
214    ///
215    /// The right offset is used to determine how far to the right the
216    /// scrollbar should extend to, useful for ensuring it doesn't collide
217    /// with the vertical scrollbar when visible.
218    fn render_horizontal_scrollbar(
219        this: &Entity<Self>,
220        right_offset: Pixels,
221        cx: &mut App,
222    ) -> impl IntoElement {
223        div()
224            .id("keymap-editor-horizontal-scroll")
225            .occlude()
226            .flex_none()
227            .w_full()
228            .cursor_default()
229            .absolute()
230            .bottom_neg_px()
231            .left_0()
232            .right_0()
233            .pr(right_offset)
234            .on_mouse_move(Self::listener(this, |_, _, _, cx| {
235                cx.notify();
236                cx.stop_propagation()
237            }))
238            .on_hover(|_, _, cx| {
239                cx.stop_propagation();
240            })
241            .on_any_mouse_down(|_, _, cx| {
242                cx.stop_propagation();
243            })
244            .on_mouse_up(
245                MouseButton::Left,
246                Self::listener(this, |this, _, window, cx| {
247                    if !this.horizontal_scrollbar.state.is_dragging()
248                        && !this.focus_handle.contains_focused(window, cx)
249                    {
250                        this.horizontal_scrollbar.hide(window, cx);
251                        cx.notify();
252                    }
253
254                    cx.stop_propagation();
255                }),
256            )
257            .on_scroll_wheel(Self::listener(this, |_, _, _, cx| {
258                cx.notify();
259            }))
260            .children(Scrollbar::horizontal(
261                // percentage as f32..end_offset as f32,
262                this.read(cx).horizontal_scrollbar.state.clone(),
263            ))
264    }
265}
266
267/// A table component
268#[derive(RegisterComponent, IntoElement)]
269pub struct Table<const COLS: usize = 3> {
270    striped: bool,
271    width: Length,
272    headers: Option<[AnyElement; COLS]>,
273    rows: TableContents<COLS>,
274    interaction_state: Option<WeakEntity<TableInteractionState>>,
275    column_widths: Option<[Length; COLS]>,
276}
277
278impl<const COLS: usize> Table<COLS> {
279    /// number of headers provided.
280    pub fn new() -> Self {
281        Table {
282            striped: false,
283            width: Length::Auto,
284            headers: None,
285            rows: TableContents::Vec(Vec::new()),
286            interaction_state: None,
287            column_widths: None,
288        }
289    }
290
291    /// Enables uniform list rendering.
292    /// The provided function will be passed directly to the `uniform_list` element.
293    /// Therefore, if this method is called, any calls to [`Table::row`] before or after
294    /// this method is called will be ignored.
295    pub fn uniform_list(
296        mut self,
297        id: impl Into<ElementId>,
298        row_count: usize,
299        render_item_fn: impl Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>
300        + 'static,
301    ) -> Self {
302        self.rows = TableContents::UniformList(UniformListData {
303            element_id: id.into(),
304            row_count: row_count,
305            render_item_fn: Box::new(render_item_fn),
306        });
307        self
308    }
309
310    /// Enables row striping.
311    pub fn striped(mut self) -> Self {
312        self.striped = true;
313        self
314    }
315
316    /// Sets the width of the table.
317    pub fn width(mut self, width: impl Into<Length>) -> Self {
318        self.width = width.into();
319        self
320    }
321
322    pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
323        self.interaction_state = Some(interaction_state.downgrade());
324        self
325    }
326
327    pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self {
328        self.headers = Some(headers.map(IntoElement::into_any_element));
329        self
330    }
331
332    pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self {
333        if let Some(rows) = self.rows.rows_mut() {
334            rows.push(items.map(IntoElement::into_any_element));
335        }
336        self
337    }
338
339    pub fn column_widths(mut self, widths: [impl Into<Length>; COLS]) -> Self {
340        self.column_widths = Some(widths.map(Into::into));
341        self
342    }
343}
344
345fn base_cell_style(width: Option<Length>, cx: &App) -> Div {
346    div()
347        .px_1p5()
348        .when_some(width, |this, width| this.w(width))
349        .when(width.is_none(), |this| this.flex_1())
350        .justify_start()
351        .text_ui(cx)
352        .whitespace_nowrap()
353        .text_ellipsis()
354        .overflow_hidden()
355}
356
357pub fn render_row<const COLS: usize>(
358    row_index: usize,
359    items: [impl IntoElement; COLS],
360    table_context: TableRenderContext<COLS>,
361    cx: &App,
362) -> AnyElement {
363    let is_last = row_index == table_context.total_row_count - 1;
364    let bg = if row_index % 2 == 1 && table_context.striped {
365        Some(cx.theme().colors().text.opacity(0.05))
366    } else {
367        None
368    };
369    let column_widths = table_context
370        .column_widths
371        .map_or([None; COLS], |widths| widths.map(|width| Some(width)));
372    div()
373        .w_full()
374        .flex()
375        .flex_row()
376        .items_center()
377        .justify_between()
378        .px_1p5()
379        .py_1()
380        .when_some(bg, |row, bg| row.bg(bg))
381        .when(!is_last, |row| {
382            row.border_b_1().border_color(cx.theme().colors().border)
383        })
384        .children(
385            items
386                .map(IntoElement::into_any_element)
387                .into_iter()
388                .zip(column_widths)
389                .map(|(cell, width)| base_cell_style(width, cx).child(cell)),
390        )
391        .into_any_element()
392}
393
394pub fn render_header<const COLS: usize>(
395    headers: [impl IntoElement; COLS],
396    table_context: TableRenderContext<COLS>,
397    cx: &mut App,
398) -> impl IntoElement {
399    let column_widths = table_context
400        .column_widths
401        .map_or([None; COLS], |widths| widths.map(|width| Some(width)));
402    div()
403        .flex()
404        .flex_row()
405        .items_center()
406        .justify_between()
407        .w_full()
408        .p_2()
409        .border_b_1()
410        .border_color(cx.theme().colors().border)
411        .children(headers.into_iter().zip(column_widths).map(|(h, width)| {
412            base_cell_style(width, cx)
413                .font_weight(FontWeight::SEMIBOLD)
414                .child(h)
415        }))
416}
417
418#[derive(Clone, Copy)]
419pub struct TableRenderContext<const COLS: usize> {
420    pub striped: bool,
421    pub total_row_count: usize,
422    pub column_widths: Option<[Length; COLS]>,
423}
424
425impl<const COLS: usize> TableRenderContext<COLS> {
426    fn new(table: &Table<COLS>) -> Self {
427        Self {
428            striped: table.striped,
429            total_row_count: table.rows.len(),
430            column_widths: table.column_widths,
431        }
432    }
433}
434
435impl<const COLS: usize> RenderOnce for Table<COLS> {
436    fn render(mut self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
437        // match self.ro
438        let table_context = TableRenderContext::new(&self);
439        let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
440
441        let scroll_track_size = px(16.);
442        let h_scroll_offset = if interaction_state
443            .as_ref()
444            .is_some_and(|state| state.read(cx).vertical_scrollbar.show_scrollbar)
445        {
446            // magic number
447            px(3.)
448        } else {
449            px(0.)
450        };
451
452        div()
453            .id("todo! how to have id")
454            .w(self.width)
455            .overflow_hidden()
456            .when_some(interaction_state.as_ref(), |this, interaction_state| {
457                this.track_focus(&interaction_state.read(cx).focus_handle)
458                    .on_hover({
459                        let interaction_state = interaction_state.downgrade();
460                        move |hovered, window, cx| {
461                            interaction_state
462                                .update(cx, |interaction_state, cx| {
463                                    if *hovered {
464                                        interaction_state.horizontal_scrollbar.show(cx);
465                                        interaction_state.vertical_scrollbar.show(cx);
466                                        cx.notify();
467                                    } else if !interaction_state
468                                        .focus_handle
469                                        .contains_focused(window, cx)
470                                    {
471                                        interaction_state.hide_scrollbars(window, cx);
472                                    }
473                                })
474                                .ok(); // todo! handle error?
475                        }
476                    })
477            })
478            .when_some(self.headers.take(), |this, headers| {
479                this.child(render_header(headers, table_context, cx))
480            })
481            .map(|parent| match self.rows {
482                TableContents::Vec(items) => parent.children(
483                    items
484                        .into_iter()
485                        .enumerate()
486                        .map(|(index, row)| render_row(index, row, table_context, cx)),
487                ),
488                TableContents::UniformList(uniform_list_data) => parent
489                    .h_full()
490                    .v_flex()
491                    .child(
492                        div()
493                            .flex_grow()
494                            .w_full()
495                            .relative()
496                            .overflow_hidden()
497                            .child(
498                                uniform_list(
499                                    uniform_list_data.element_id,
500                                    uniform_list_data.row_count,
501                                    {
502                                        let render_item_fn = uniform_list_data.render_item_fn;
503                                        move |range: Range<usize>, window, cx| {
504                                            let elements =
505                                                render_item_fn(range.clone(), window, cx);
506                                            elements
507                                                .into_iter()
508                                                .zip(range)
509                                                .map(|(row, row_index)| {
510                                                    render_row(row_index, row, table_context, cx)
511                                                })
512                                                .collect()
513                                        }
514                                    },
515                                )
516                                .size_full()
517                                .flex_grow()
518                                .with_sizing_behavior(ListSizingBehavior::Auto)
519                                .with_horizontal_sizing_behavior(
520                                    ListHorizontalSizingBehavior::Unconstrained,
521                                )
522                                .when_some(
523                                    interaction_state.as_ref(),
524                                    |this, state| {
525                                        this.track_scroll(
526                                            state.read_with(cx, |s, _| s.scroll_handle.clone()),
527                                        )
528                                    },
529                                ),
530                            )
531                            .when(
532                                interaction_state.as_ref().is_some_and(|state| {
533                                    state.read(cx).vertical_scrollbar.show_track
534                                }),
535                                |this| {
536                                    this.child(
537                                        v_flex()
538                                            .h_full()
539                                            .flex_none()
540                                            .w(scroll_track_size)
541                                            .bg(cx.theme().colors().background)
542                                            .child(
543                                                div()
544                                                    .size_full()
545                                                    .flex_1()
546                                                    .border_l_1()
547                                                    .border_color(cx.theme().colors().border),
548                                            ),
549                                    )
550                                },
551                            )
552                            .when_some(
553                                interaction_state.as_ref().filter(|state| {
554                                    state.read(cx).vertical_scrollbar.show_scrollbar
555                                }),
556                                |this, interaction_state| {
557                                    this.child(TableInteractionState::render_vertical_scrollbar(
558                                        interaction_state,
559                                        cx,
560                                    ))
561                                },
562                            ),
563                    )
564                    .when(
565                        interaction_state
566                            .as_ref()
567                            .is_some_and(|state| state.read(cx).horizontal_scrollbar.show_track),
568                        |this| {
569                            this.child(
570                                h_flex()
571                                    .w_full()
572                                    .h(scroll_track_size)
573                                    .flex_none()
574                                    .relative()
575                                    .child(
576                                        div()
577                                            .w_full()
578                                            .flex_1()
579                                            // for some reason the horizontal scrollbar is 1px
580                                            // taller than the vertical scrollbar??
581                                            .h(scroll_track_size - px(1.))
582                                            .bg(cx.theme().colors().background)
583                                            .border_t_1()
584                                            .border_color(cx.theme().colors().border),
585                                    )
586                                    .when(
587                                        interaction_state.as_ref().is_some_and(|state| {
588                                            state.read(cx).vertical_scrollbar.show_track
589                                        }),
590                                        |this| {
591                                            this.child(
592                                                div()
593                                                    .flex_none()
594                                                    // -1px prevents a missing pixel between the two container borders
595                                                    .w(scroll_track_size - px(1.))
596                                                    .h_full(),
597                                            )
598                                            .child(
599                                                // HACK: Fill the missing 1px 🥲
600                                                div()
601                                                    .absolute()
602                                                    .right(scroll_track_size - px(1.))
603                                                    .bottom(scroll_track_size - px(1.))
604                                                    .size_px()
605                                                    .bg(cx.theme().colors().border),
606                                            )
607                                        },
608                                    ),
609                            )
610                        },
611                    )
612                    .when_some(
613                        interaction_state
614                            .as_ref()
615                            .filter(|state| state.read(cx).horizontal_scrollbar.show_scrollbar),
616                        |this, interaction_state| {
617                            this.child(TableInteractionState::render_horizontal_scrollbar(
618                                interaction_state,
619                                h_scroll_offset,
620                                cx,
621                            ))
622                        },
623                    ),
624            })
625    }
626}
627
628// computed state related to how to render scrollbars
629// one per axis
630// on render we just read this off the keymap editor
631// we update it when
632// - settings change
633// - on focus in, on focus out, on hover, etc.
634#[derive(Debug)]
635pub struct ScrollbarProperties {
636    axis: Axis,
637    show_scrollbar: bool,
638    show_track: bool,
639    auto_hide: bool,
640    hide_task: Option<Task<()>>,
641    state: ScrollbarState,
642}
643
644impl ScrollbarProperties {
645    // Shows the scrollbar and cancels any pending hide task
646    fn show(&mut self, cx: &mut Context<TableInteractionState>) {
647        if !self.auto_hide {
648            return;
649        }
650        self.show_scrollbar = true;
651        self.hide_task.take();
652        cx.notify();
653    }
654
655    fn hide(&mut self, window: &mut Window, cx: &mut Context<TableInteractionState>) {
656        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
657
658        if !self.auto_hide {
659            return;
660        }
661
662        let axis = self.axis;
663        self.hide_task = Some(cx.spawn_in(window, async move |keymap_editor, cx| {
664            cx.background_executor()
665                .timer(SCROLLBAR_SHOW_INTERVAL)
666                .await;
667
668            if let Some(keymap_editor) = keymap_editor.upgrade() {
669                keymap_editor
670                    .update(cx, |keymap_editor, cx| {
671                        match axis {
672                            Axis::Vertical => {
673                                keymap_editor.vertical_scrollbar.show_scrollbar = false
674                            }
675                            Axis::Horizontal => {
676                                keymap_editor.horizontal_scrollbar.show_scrollbar = false
677                            }
678                        }
679                        cx.notify();
680                    })
681                    .ok();
682            }
683        }));
684    }
685}
686
687impl Component for Table<3> {
688    fn scope() -> ComponentScope {
689        ComponentScope::Layout
690    }
691
692    fn description() -> Option<&'static str> {
693        Some("A table component for displaying data in rows and columns with optional styling.")
694    }
695
696    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
697        Some(
698            v_flex()
699                .gap_6()
700                .children(vec![
701                    example_group_with_title(
702                        "Basic Tables",
703                        vec![
704                            single_example(
705                                "Simple Table",
706                                Table::new()
707                                    .width(px(400.))
708                                    .header(["Name", "Age", "City"])
709                                    .row(["Alice", "28", "New York"])
710                                    .row(["Bob", "32", "San Francisco"])
711                                    .row(["Charlie", "25", "London"])
712                                    .into_any_element(),
713                            ),
714                            single_example(
715                                "Two Column Table",
716                                Table::new()
717                                    .header(["Category", "Value"])
718                                    .width(px(300.))
719                                    .row(["Revenue", "$100,000"])
720                                    .row(["Expenses", "$75,000"])
721                                    .row(["Profit", "$25,000"])
722                                    .into_any_element(),
723                            ),
724                        ],
725                    ),
726                    example_group_with_title(
727                        "Styled Tables",
728                        vec![
729                            single_example(
730                                "Default",
731                                Table::new()
732                                    .width(px(400.))
733                                    .header(["Product", "Price", "Stock"])
734                                    .row(["Laptop", "$999", "In Stock"])
735                                    .row(["Phone", "$599", "Low Stock"])
736                                    .row(["Tablet", "$399", "Out of Stock"])
737                                    .into_any_element(),
738                            ),
739                            single_example(
740                                "Striped",
741                                Table::new()
742                                    .width(px(400.))
743                                    .striped()
744                                    .header(["Product", "Price", "Stock"])
745                                    .row(["Laptop", "$999", "In Stock"])
746                                    .row(["Phone", "$599", "Low Stock"])
747                                    .row(["Tablet", "$399", "Out of Stock"])
748                                    .row(["Headphones", "$199", "In Stock"])
749                                    .into_any_element(),
750                            ),
751                        ],
752                    ),
753                    example_group_with_title(
754                        "Mixed Content Table",
755                        vec![single_example(
756                            "Table with Elements",
757                            Table::new()
758                                .width(px(840.))
759                                .header(["Status", "Name", "Priority", "Deadline", "Action"])
760                                .row([
761                                    Indicator::dot().color(Color::Success).into_any_element(),
762                                    "Project A".into_any_element(),
763                                    "High".into_any_element(),
764                                    "2023-12-31".into_any_element(),
765                                    Button::new("view_a", "View")
766                                        .style(ButtonStyle::Filled)
767                                        .full_width()
768                                        .into_any_element(),
769                                ])
770                                .row([
771                                    Indicator::dot().color(Color::Warning).into_any_element(),
772                                    "Project B".into_any_element(),
773                                    "Medium".into_any_element(),
774                                    "2024-03-15".into_any_element(),
775                                    Button::new("view_b", "View")
776                                        .style(ButtonStyle::Filled)
777                                        .full_width()
778                                        .into_any_element(),
779                                ])
780                                .row([
781                                    Indicator::dot().color(Color::Error).into_any_element(),
782                                    "Project C".into_any_element(),
783                                    "Low".into_any_element(),
784                                    "2024-06-30".into_any_element(),
785                                    Button::new("view_c", "View")
786                                        .style(ButtonStyle::Filled)
787                                        .full_width()
788                                        .into_any_element(),
789                                ])
790                                .into_any_element(),
791                        )],
792                    ),
793                ])
794                .into_any_element(),
795        )
796    }
797}