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}