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