1use std::{ops::Range, rc::Rc, time::Duration};
2
3use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide};
4use gpui::{
5 AbsoluteLength, AppContext, Axis, Context, DefiniteLength, DragMoveEvent, Entity, FocusHandle,
6 Length, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Point, Stateful, Task,
7 UniformListScrollHandle, WeakEntity, transparent_black, uniform_list,
8};
9
10use itertools::intersperse_with;
11use settings::Settings as _;
12use ui::{
13 ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
14 ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator,
15 InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
16 Scrollbar, ScrollbarState, StatefulInteractiveElement as _, Styled, StyledExt as _,
17 StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex,
18};
19
20#[derive(Debug)]
21struct DraggedColumn(usize);
22
23struct UniformListData<const COLS: usize> {
24 render_item_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>,
25 element_id: ElementId,
26 row_count: usize,
27}
28
29enum TableContents<const COLS: usize> {
30 Vec(Vec<[AnyElement; COLS]>),
31 UniformList(UniformListData<COLS>),
32}
33
34impl<const COLS: usize> TableContents<COLS> {
35 fn rows_mut(&mut self) -> Option<&mut Vec<[AnyElement; COLS]>> {
36 match self {
37 TableContents::Vec(rows) => Some(rows),
38 TableContents::UniformList(_) => None,
39 }
40 }
41
42 fn len(&self) -> usize {
43 match self {
44 TableContents::Vec(rows) => rows.len(),
45 TableContents::UniformList(data) => data.row_count,
46 }
47 }
48
49 fn is_empty(&self) -> bool {
50 self.len() == 0
51 }
52}
53
54pub struct TableInteractionState {
55 pub focus_handle: FocusHandle,
56 pub scroll_handle: UniformListScrollHandle,
57 pub horizontal_scrollbar: ScrollbarProperties,
58 pub vertical_scrollbar: ScrollbarProperties,
59}
60
61impl TableInteractionState {
62 pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
63 cx.new(|cx| {
64 let focus_handle = cx.focus_handle();
65
66 cx.on_focus_out(&focus_handle, window, |this: &mut Self, _, window, cx| {
67 this.hide_scrollbars(window, cx);
68 })
69 .detach();
70
71 let scroll_handle = UniformListScrollHandle::new();
72 let vertical_scrollbar = ScrollbarProperties {
73 axis: Axis::Vertical,
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 horizontal_scrollbar = ScrollbarProperties {
82 axis: Axis::Horizontal,
83 state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
84 show_scrollbar: false,
85 show_track: false,
86 auto_hide: false,
87 hide_task: None,
88 };
89
90 let mut this = Self {
91 focus_handle,
92 scroll_handle,
93 horizontal_scrollbar,
94 vertical_scrollbar,
95 };
96
97 this.update_scrollbar_visibility(cx);
98 this
99 })
100 }
101
102 pub fn get_scrollbar_offset(&self, axis: Axis) -> Point<Pixels> {
103 match axis {
104 Axis::Vertical => self.vertical_scrollbar.state.scroll_handle().offset(),
105 Axis::Horizontal => self.horizontal_scrollbar.state.scroll_handle().offset(),
106 }
107 }
108
109 pub fn set_scrollbar_offset(&self, axis: Axis, offset: Point<Pixels>) {
110 match axis {
111 Axis::Vertical => self
112 .vertical_scrollbar
113 .state
114 .scroll_handle()
115 .set_offset(offset),
116 Axis::Horizontal => self
117 .horizontal_scrollbar
118 .state
119 .scroll_handle()
120 .set_offset(offset),
121 }
122 }
123
124 fn update_scrollbar_visibility(&mut self, cx: &mut Context<Self>) {
125 let show_setting = EditorSettings::get_global(cx).scrollbar.show;
126
127 let scroll_handle = self.scroll_handle.0.borrow();
128
129 let autohide = |show: ShowScrollbar, cx: &mut Context<Self>| match show {
130 ShowScrollbar::Auto => true,
131 ShowScrollbar::System => cx
132 .try_global::<ScrollbarAutoHide>()
133 .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
134 ShowScrollbar::Always => false,
135 ShowScrollbar::Never => false,
136 };
137
138 let longest_item_width = scroll_handle.last_item_size.and_then(|size| {
139 (size.contents.width > size.item.width).then_some(size.contents.width)
140 });
141
142 // is there an item long enough that we should show a horizontal scrollbar?
143 let item_wider_than_container = if let Some(longest_item_width) = longest_item_width {
144 longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0)
145 } else {
146 true
147 };
148
149 let show_scrollbar = match show_setting {
150 ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
151 ShowScrollbar::Never => false,
152 };
153 let show_vertical = show_scrollbar;
154
155 let show_horizontal = item_wider_than_container && show_scrollbar;
156
157 let show_horizontal_track =
158 show_horizontal && matches!(show_setting, ShowScrollbar::Always);
159
160 // TODO: we probably should hide the scroll track when the list doesn't need to scroll
161 let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always);
162
163 self.vertical_scrollbar = ScrollbarProperties {
164 axis: self.vertical_scrollbar.axis,
165 state: self.vertical_scrollbar.state.clone(),
166 show_scrollbar: show_vertical,
167 show_track: show_vertical_track,
168 auto_hide: autohide(show_setting, cx),
169 hide_task: None,
170 };
171
172 self.horizontal_scrollbar = ScrollbarProperties {
173 axis: self.horizontal_scrollbar.axis,
174 state: self.horizontal_scrollbar.state.clone(),
175 show_scrollbar: show_horizontal,
176 show_track: show_horizontal_track,
177 auto_hide: autohide(show_setting, cx),
178 hide_task: None,
179 };
180
181 cx.notify();
182 }
183
184 fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
185 self.horizontal_scrollbar.hide(window, cx);
186 self.vertical_scrollbar.hide(window, cx);
187 }
188
189 pub fn listener<E: ?Sized>(
190 this: &Entity<Self>,
191 f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static,
192 ) -> impl Fn(&E, &mut Window, &mut App) + 'static {
193 let view = this.downgrade();
194 move |e: &E, window: &mut Window, cx: &mut App| {
195 view.update(cx, |view, cx| f(view, e, window, cx)).ok();
196 }
197 }
198
199 fn render_resize_handles<const COLS: usize>(
200 &self,
201 column_widths: &[Length; COLS],
202 resizable_columns: &[bool; COLS],
203 window: &mut Window,
204 cx: &mut App,
205 ) -> AnyElement {
206 let spacers = column_widths
207 .iter()
208 .map(|width| base_cell_style(Some(*width)).into_any_element());
209
210 let mut column_ix = 0;
211 let mut resizable_columns = resizable_columns.into_iter();
212 let dividers = intersperse_with(spacers, || {
213 window.with_id(column_ix, |window| {
214 let mut resize_divider = div()
215 // This is required because this is evaluated at a different time than the use_state call above
216 .id(column_ix)
217 .relative()
218 .top_0()
219 .w_0p5()
220 .h_full()
221 .bg(cx.theme().colors().border.opacity(0.5));
222
223 let mut resize_handle = div()
224 .id("column-resize-handle")
225 .absolute()
226 .left_neg_0p5()
227 .w(px(5.0))
228 .h_full();
229
230 if Some(&true) == resizable_columns.next() {
231 let hovered = window.use_state(cx, |_window, _cx| false);
232 resize_divider = resize_divider.when(*hovered.read(cx), |div| {
233 div.bg(cx.theme().colors().border_focused)
234 });
235 resize_handle = resize_handle
236 .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered))
237 .cursor_col_resize()
238 .on_drag(DraggedColumn(column_ix), |ix, _offset, _window, cx| {
239 eprintln!("Start resizing column {:?}", ix);
240 cx.new(|_cx| gpui::Empty)
241 })
242 }
243
244 column_ix += 1;
245 resize_divider.child(resize_handle).into_any_element()
246 })
247 });
248
249 div()
250 .id("resize-handles")
251 .h_flex()
252 .absolute()
253 .w_full()
254 .inset_0()
255 .children(dividers)
256 .into_any_element()
257 }
258
259 fn render_vertical_scrollbar_track(
260 this: &Entity<Self>,
261 parent: Div,
262 scroll_track_size: Pixels,
263 cx: &mut App,
264 ) -> Div {
265 if !this.read(cx).vertical_scrollbar.show_track {
266 return parent;
267 }
268 let child = v_flex()
269 .h_full()
270 .flex_none()
271 .w(scroll_track_size)
272 .bg(cx.theme().colors().background)
273 .child(
274 div()
275 .size_full()
276 .flex_1()
277 .border_l_1()
278 .border_color(cx.theme().colors().border),
279 );
280 parent.child(child)
281 }
282
283 fn render_vertical_scrollbar(this: &Entity<Self>, parent: Div, cx: &mut App) -> Div {
284 if !this.read(cx).vertical_scrollbar.show_scrollbar {
285 return parent;
286 }
287 let child = div()
288 .id(("table-vertical-scrollbar", this.entity_id()))
289 .occlude()
290 .flex_none()
291 .h_full()
292 .cursor_default()
293 .absolute()
294 .right_0()
295 .top_0()
296 .bottom_0()
297 .w(px(12.))
298 .on_mouse_move(Self::listener(this, |_, _, _, cx| {
299 cx.notify();
300 cx.stop_propagation()
301 }))
302 .on_hover(|_, _, cx| {
303 cx.stop_propagation();
304 })
305 .on_mouse_up(
306 MouseButton::Left,
307 Self::listener(this, |this, _, window, cx| {
308 if !this.vertical_scrollbar.state.is_dragging()
309 && !this.focus_handle.contains_focused(window, cx)
310 {
311 this.vertical_scrollbar.hide(window, cx);
312 cx.notify();
313 }
314
315 cx.stop_propagation();
316 }),
317 )
318 .on_any_mouse_down(|_, _, cx| {
319 cx.stop_propagation();
320 })
321 .on_scroll_wheel(Self::listener(&this, |_, _, _, cx| {
322 cx.notify();
323 }))
324 .children(Scrollbar::vertical(
325 this.read(cx).vertical_scrollbar.state.clone(),
326 ));
327 parent.child(child)
328 }
329
330 /// Renders the horizontal scrollbar.
331 ///
332 /// The right offset is used to determine how far to the right the
333 /// scrollbar should extend to, useful for ensuring it doesn't collide
334 /// with the vertical scrollbar when visible.
335 fn render_horizontal_scrollbar(
336 this: &Entity<Self>,
337 parent: Div,
338 right_offset: Pixels,
339 cx: &mut App,
340 ) -> Div {
341 if !this.read(cx).horizontal_scrollbar.show_scrollbar {
342 return parent;
343 }
344 let child = div()
345 .id(("table-horizontal-scrollbar", this.entity_id()))
346 .occlude()
347 .flex_none()
348 .w_full()
349 .cursor_default()
350 .absolute()
351 .bottom_neg_px()
352 .left_0()
353 .right_0()
354 .pr(right_offset)
355 .on_mouse_move(Self::listener(this, |_, _, _, cx| {
356 cx.notify();
357 cx.stop_propagation()
358 }))
359 .on_hover(|_, _, cx| {
360 cx.stop_propagation();
361 })
362 .on_any_mouse_down(|_, _, cx| {
363 cx.stop_propagation();
364 })
365 .on_mouse_up(
366 MouseButton::Left,
367 Self::listener(this, |this, _, window, cx| {
368 if !this.horizontal_scrollbar.state.is_dragging()
369 && !this.focus_handle.contains_focused(window, cx)
370 {
371 this.horizontal_scrollbar.hide(window, cx);
372 cx.notify();
373 }
374
375 cx.stop_propagation();
376 }),
377 )
378 .on_scroll_wheel(Self::listener(this, |_, _, _, cx| {
379 cx.notify();
380 }))
381 .children(Scrollbar::horizontal(
382 // percentage as f32..end_offset as f32,
383 this.read(cx).horizontal_scrollbar.state.clone(),
384 ));
385 parent.child(child)
386 }
387
388 fn render_horizontal_scrollbar_track(
389 this: &Entity<Self>,
390 parent: Div,
391 scroll_track_size: Pixels,
392 cx: &mut App,
393 ) -> Div {
394 if !this.read(cx).horizontal_scrollbar.show_track {
395 return parent;
396 }
397 let child = h_flex()
398 .w_full()
399 .h(scroll_track_size)
400 .flex_none()
401 .relative()
402 .child(
403 div()
404 .w_full()
405 .flex_1()
406 // for some reason the horizontal scrollbar is 1px
407 // taller than the vertical scrollbar??
408 .h(scroll_track_size - px(1.))
409 .bg(cx.theme().colors().background)
410 .border_t_1()
411 .border_color(cx.theme().colors().border),
412 )
413 .when(this.read(cx).vertical_scrollbar.show_track, |parent| {
414 parent
415 .child(
416 div()
417 .flex_none()
418 // -1px prevents a missing pixel between the two container borders
419 .w(scroll_track_size - px(1.))
420 .h_full(),
421 )
422 .child(
423 // HACK: Fill the missing 1px 🥲
424 div()
425 .absolute()
426 .right(scroll_track_size - px(1.))
427 .bottom(scroll_track_size - px(1.))
428 .size_px()
429 .bg(cx.theme().colors().border),
430 )
431 });
432
433 parent.child(child)
434 }
435}
436
437pub struct ColumnWidths<const COLS: usize> {
438 widths: [DefiniteLength; COLS],
439 initialized: bool,
440}
441
442impl<const COLS: usize> ColumnWidths<COLS> {
443 pub fn new(_: &mut App) -> Self {
444 Self {
445 widths: [DefiniteLength::default(); COLS],
446 initialized: false,
447 }
448 }
449
450 fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 {
451 match length {
452 DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width,
453 DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => {
454 rems_width.to_pixels(rem_size) / bounds_width
455 }
456 DefiniteLength::Fraction(fraction) => *fraction,
457 }
458 }
459
460 fn on_drag_move(
461 &mut self,
462 drag_event: &DragMoveEvent<DraggedColumn>,
463 window: &mut Window,
464 cx: &mut Context<Self>,
465 ) {
466 // - [ ] Fix bugs in resize
467 // - [ ] Create and respect a minimum size
468 // - [ ] Cascade resize columns to next column if at minimum width
469 // - [ ] Double click to reset column widths
470 let drag_position = drag_event.event.position;
471 let bounds = drag_event.bounds;
472
473 let mut col_position = 0.0;
474 let rem_size = window.rem_size();
475 let bounds_width = bounds.right() - bounds.left();
476 let col_idx = drag_event.drag(cx).0;
477
478 for length in self.widths[0..=col_idx].iter() {
479 col_position += Self::get_fraction(length, bounds_width, rem_size);
480 }
481
482 let mut total_length_ratio = col_position;
483 for length in self.widths[col_idx + 1..].iter() {
484 total_length_ratio += Self::get_fraction(length, bounds_width, rem_size);
485 }
486
487 let drag_fraction = (drag_position.x - bounds.left()) / bounds_width;
488 let drag_fraction = drag_fraction * total_length_ratio;
489 let diff = drag_fraction - col_position;
490
491 let is_dragging_right = diff > 0.0;
492
493 // TODO: broken when dragging left
494 // split into an if dragging_right { this } else { new loop}
495 // then combine if same logic
496 let mut diff_left = diff;
497 let mut curr_column = col_idx + 1;
498 let min_size = 0.05; // todo!
499
500 if is_dragging_right {
501 while diff_left > 0.0 && curr_column < COLS {
502 let mut curr_width =
503 Self::get_fraction(&self.widths[curr_column], bounds_width, rem_size)
504 - diff_left;
505
506 diff_left = 0.0;
507 if min_size > curr_width {
508 diff_left += min_size - curr_width;
509 curr_width = min_size;
510 }
511 self.widths[curr_column] = DefiniteLength::Fraction(curr_width);
512 curr_column += 1;
513 }
514
515 self.widths[col_idx] = DefiniteLength::Fraction(
516 Self::get_fraction(&self.widths[col_idx], bounds_width, rem_size)
517 + (diff - diff_left),
518 );
519 } else {
520 curr_column = col_idx;
521 while diff_left < 0.0 && curr_column > 0 {
522 let mut curr_width =
523 Self::get_fraction(&self.widths[curr_column], bounds_width, rem_size)
524 + diff_left;
525
526 diff_left = 0.0;
527 if curr_width < min_size {
528 diff_left = curr_width - min_size;
529 curr_width = min_size
530 }
531
532 self.widths[curr_column] = DefiniteLength::Fraction(curr_width);
533 curr_column -= 1;
534 }
535
536 self.widths[col_idx + 1] = DefiniteLength::Fraction(
537 Self::get_fraction(&self.widths[col_idx + 1], bounds_width, rem_size)
538 - (diff - diff_left),
539 );
540 }
541 }
542}
543
544pub struct TableWidths<const COLS: usize> {
545 initial: [DefiniteLength; COLS],
546 current: Option<Entity<ColumnWidths<COLS>>>,
547 resizable: [bool; COLS],
548}
549
550impl<const COLS: usize> TableWidths<COLS> {
551 pub fn new(widths: [impl Into<DefiniteLength>; COLS]) -> Self {
552 let widths = widths.map(Into::into);
553
554 TableWidths {
555 initial: widths.clone(),
556 current: None,
557 resizable: [false; COLS],
558 }
559 }
560
561 fn lengths(&self, cx: &App) -> [Length; COLS] {
562 self.current
563 .as_ref()
564 .map(|entity| entity.read(cx).widths.map(|w| Length::Definite(w)))
565 .unwrap_or(self.initial.map(|w| Length::Definite(w)))
566 }
567}
568
569/// A table component
570#[derive(RegisterComponent, IntoElement)]
571pub struct Table<const COLS: usize = 3> {
572 striped: bool,
573 width: Option<Length>,
574 headers: Option<[AnyElement; COLS]>,
575 rows: TableContents<COLS>,
576 interaction_state: Option<WeakEntity<TableInteractionState>>,
577 col_widths: Option<TableWidths<COLS>>,
578 map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
579 empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
580}
581
582impl<const COLS: usize> Table<COLS> {
583 /// number of headers provided.
584 pub fn new() -> Self {
585 Self {
586 striped: false,
587 width: None,
588 headers: None,
589 rows: TableContents::Vec(Vec::new()),
590 interaction_state: None,
591 map_row: None,
592 empty_table_callback: None,
593 col_widths: None,
594 }
595 }
596
597 /// Enables uniform list rendering.
598 /// The provided function will be passed directly to the `uniform_list` element.
599 /// Therefore, if this method is called, any calls to [`Table::row`] before or after
600 /// this method is called will be ignored.
601 pub fn uniform_list(
602 mut self,
603 id: impl Into<ElementId>,
604 row_count: usize,
605 render_item_fn: impl Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>
606 + 'static,
607 ) -> Self {
608 self.rows = TableContents::UniformList(UniformListData {
609 element_id: id.into(),
610 row_count: row_count,
611 render_item_fn: Box::new(render_item_fn),
612 });
613 self
614 }
615
616 /// Enables row striping.
617 pub fn striped(mut self) -> Self {
618 self.striped = true;
619 self
620 }
621
622 /// Sets the width of the table.
623 /// Will enable horizontal scrolling if [`Self::interactable`] is also called.
624 pub fn width(mut self, width: impl Into<Length>) -> Self {
625 self.width = Some(width.into());
626 self
627 }
628
629 /// Enables interaction (primarily scrolling) with the table.
630 ///
631 /// Vertical scrolling will be enabled by default if the table is taller than its container.
632 ///
633 /// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise
634 /// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`]
635 /// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will
636 /// be set to [`ListHorizontalSizingBehavior::FitList`].
637 pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
638 self.interaction_state = Some(interaction_state.downgrade());
639 self
640 }
641
642 pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self {
643 self.headers = Some(headers.map(IntoElement::into_any_element));
644 self
645 }
646
647 pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self {
648 if let Some(rows) = self.rows.rows_mut() {
649 rows.push(items.map(IntoElement::into_any_element));
650 }
651 self
652 }
653
654 pub fn column_widths(mut self, widths: [impl Into<DefiniteLength>; COLS]) -> Self {
655 if self.col_widths.is_none() {
656 self.col_widths = Some(TableWidths::new(widths));
657 }
658 self
659 }
660
661 pub fn resizable_columns(
662 mut self,
663 resizable: [impl Into<bool>; COLS],
664 column_widths: &Entity<ColumnWidths<COLS>>,
665 cx: &mut App,
666 ) -> Self {
667 if let Some(table_widths) = self.col_widths.as_mut() {
668 table_widths.resizable = resizable.map(Into::into);
669 let column_widths = table_widths
670 .current
671 .get_or_insert_with(|| column_widths.clone());
672
673 column_widths.update(cx, |widths, _| {
674 if !widths.initialized {
675 widths.initialized = true;
676 widths.widths = table_widths.initial.clone();
677 }
678 })
679 }
680 self
681 }
682
683 pub fn map_row(
684 mut self,
685 callback: impl Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement + 'static,
686 ) -> Self {
687 self.map_row = Some(Rc::new(callback));
688 self
689 }
690
691 /// Provide a callback that is invoked when the table is rendered without any rows
692 pub fn empty_table_callback(
693 mut self,
694 callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
695 ) -> Self {
696 self.empty_table_callback = Some(Rc::new(callback));
697 self
698 }
699}
700
701fn base_cell_style(width: Option<Length>) -> Div {
702 div()
703 .px_1p5()
704 .when_some(width, |this, width| this.w(width))
705 .when(width.is_none(), |this| this.flex_1())
706 .justify_start()
707 .whitespace_nowrap()
708 .text_ellipsis()
709 .overflow_hidden()
710}
711
712fn base_cell_style_text(width: Option<Length>, cx: &App) -> Div {
713 base_cell_style(width).text_ui(cx)
714}
715
716pub fn render_row<const COLS: usize>(
717 row_index: usize,
718 items: [impl IntoElement; COLS],
719 table_context: TableRenderContext<COLS>,
720 window: &mut Window,
721 cx: &mut App,
722) -> AnyElement {
723 let is_striped = table_context.striped;
724 let is_last = row_index == table_context.total_row_count - 1;
725 let bg = if row_index % 2 == 1 && is_striped {
726 Some(cx.theme().colors().text.opacity(0.05))
727 } else {
728 None
729 };
730 let column_widths = table_context
731 .column_widths
732 .map_or([None; COLS], |widths| widths.map(Some));
733
734 let mut row = h_flex()
735 .h_full()
736 .id(("table_row", row_index))
737 .w_full()
738 .justify_between()
739 .when_some(bg, |row, bg| row.bg(bg))
740 .when(!is_striped, |row| {
741 row.border_b_1()
742 .border_color(transparent_black())
743 .when(!is_last, |row| row.border_color(cx.theme().colors().border))
744 });
745
746 row = row.children(
747 items
748 .map(IntoElement::into_any_element)
749 .into_iter()
750 .zip(column_widths)
751 .map(|(cell, width)| base_cell_style_text(width, cx).px_1p5().py_1().child(cell)),
752 );
753
754 let row = if let Some(map_row) = table_context.map_row {
755 map_row((row_index, row), window, cx)
756 } else {
757 row.into_any_element()
758 };
759
760 div().h_full().w_full().child(row).into_any_element()
761}
762
763pub fn render_header<const COLS: usize>(
764 headers: [impl IntoElement; COLS],
765 table_context: TableRenderContext<COLS>,
766 cx: &mut App,
767) -> impl IntoElement {
768 let column_widths = table_context
769 .column_widths
770 .map_or([None; COLS], |widths| widths.map(Some));
771 div()
772 .flex()
773 .flex_row()
774 .items_center()
775 .justify_between()
776 .w_full()
777 .p_2()
778 .border_b_1()
779 .border_color(cx.theme().colors().border)
780 .children(
781 headers
782 .into_iter()
783 .zip(column_widths)
784 .map(|(h, width)| base_cell_style_text(width, cx).child(h)),
785 )
786}
787
788#[derive(Clone)]
789pub struct TableRenderContext<const COLS: usize> {
790 pub striped: bool,
791 pub total_row_count: usize,
792 pub column_widths: Option<[Length; COLS]>,
793 pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
794}
795
796impl<const COLS: usize> TableRenderContext<COLS> {
797 fn new(table: &Table<COLS>, cx: &App) -> Self {
798 Self {
799 striped: table.striped,
800 total_row_count: table.rows.len(),
801 column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)),
802 map_row: table.map_row.clone(),
803 }
804 }
805}
806
807impl<const COLS: usize> RenderOnce for Table<COLS> {
808 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
809 let table_context = TableRenderContext::new(&self, cx);
810 let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
811 let current_widths = self
812 .col_widths
813 .as_ref()
814 .and_then(|widths| widths.current.as_ref())
815 .map(|curr| curr.downgrade());
816
817 let scroll_track_size = px(16.);
818 let h_scroll_offset = if interaction_state
819 .as_ref()
820 .is_some_and(|state| state.read(cx).vertical_scrollbar.show_scrollbar)
821 {
822 // magic number
823 px(3.)
824 } else {
825 px(0.)
826 };
827
828 let width = self.width;
829 let no_rows_rendered = self.rows.is_empty();
830
831 let table = div()
832 .when_some(width, |this, width| this.w(width))
833 .h_full()
834 .v_flex()
835 .when_some(self.headers.take(), |this, headers| {
836 this.child(render_header(headers, table_context.clone(), cx))
837 })
838 .when_some(current_widths, |this, widths| {
839 this.on_drag_move::<DraggedColumn>(move |e, window, cx| {
840 widths
841 .update(cx, |widths, cx| {
842 widths.on_drag_move(e, window, cx);
843 })
844 .ok();
845 })
846 })
847 .on_drop::<DraggedColumn>(|_, _, _| {
848 // Finish the resize operation
849 })
850 .child(
851 div()
852 .flex_grow()
853 .w_full()
854 .relative()
855 .overflow_hidden()
856 .map(|parent| match self.rows {
857 TableContents::Vec(items) => {
858 parent.children(items.into_iter().enumerate().map(|(index, row)| {
859 render_row(index, row, table_context.clone(), window, cx)
860 }))
861 }
862 TableContents::UniformList(uniform_list_data) => parent.child(
863 uniform_list(
864 uniform_list_data.element_id,
865 uniform_list_data.row_count,
866 {
867 let render_item_fn = uniform_list_data.render_item_fn;
868 move |range: Range<usize>, window, cx| {
869 let elements = render_item_fn(range.clone(), window, cx);
870 elements
871 .into_iter()
872 .zip(range)
873 .map(|(row, row_index)| {
874 render_row(
875 row_index,
876 row,
877 table_context.clone(),
878 window,
879 cx,
880 )
881 })
882 .collect()
883 }
884 },
885 )
886 .size_full()
887 .flex_grow()
888 .with_sizing_behavior(ListSizingBehavior::Auto)
889 .with_horizontal_sizing_behavior(if width.is_some() {
890 ListHorizontalSizingBehavior::Unconstrained
891 } else {
892 ListHorizontalSizingBehavior::FitList
893 })
894 .when_some(
895 interaction_state.as_ref(),
896 |this, state| {
897 this.track_scroll(
898 state.read_with(cx, |s, _| s.scroll_handle.clone()),
899 )
900 },
901 ),
902 ),
903 })
904 .when_some(
905 self.col_widths.as_ref().zip(interaction_state.as_ref()),
906 |parent, (table_widths, state)| {
907 parent.child(state.update(cx, |state, cx| {
908 let resizable_columns = table_widths.resizable;
909 let column_widths = table_widths.lengths(cx);
910 state.render_resize_handles(
911 &column_widths,
912 &resizable_columns,
913 window,
914 cx,
915 )
916 }))
917 },
918 )
919 .when_some(interaction_state.as_ref(), |this, interaction_state| {
920 this.map(|this| {
921 TableInteractionState::render_vertical_scrollbar_track(
922 interaction_state,
923 this,
924 scroll_track_size,
925 cx,
926 )
927 })
928 .map(|this| {
929 TableInteractionState::render_vertical_scrollbar(
930 interaction_state,
931 this,
932 cx,
933 )
934 })
935 }),
936 )
937 .when_some(
938 no_rows_rendered
939 .then_some(self.empty_table_callback)
940 .flatten(),
941 |this, callback| {
942 this.child(
943 h_flex()
944 .size_full()
945 .p_3()
946 .items_start()
947 .justify_center()
948 .child(callback(window, cx)),
949 )
950 },
951 )
952 .when_some(
953 width.and(interaction_state.as_ref()),
954 |this, interaction_state| {
955 this.map(|this| {
956 TableInteractionState::render_horizontal_scrollbar_track(
957 interaction_state,
958 this,
959 scroll_track_size,
960 cx,
961 )
962 })
963 .map(|this| {
964 TableInteractionState::render_horizontal_scrollbar(
965 interaction_state,
966 this,
967 h_scroll_offset,
968 cx,
969 )
970 })
971 },
972 );
973
974 if let Some(interaction_state) = interaction_state.as_ref() {
975 table
976 .track_focus(&interaction_state.read(cx).focus_handle)
977 .id(("table", interaction_state.entity_id()))
978 .on_hover({
979 let interaction_state = interaction_state.downgrade();
980 move |hovered, window, cx| {
981 interaction_state
982 .update(cx, |interaction_state, cx| {
983 if *hovered {
984 interaction_state.horizontal_scrollbar.show(cx);
985 interaction_state.vertical_scrollbar.show(cx);
986 cx.notify();
987 } else if !interaction_state
988 .focus_handle
989 .contains_focused(window, cx)
990 {
991 interaction_state.hide_scrollbars(window, cx);
992 }
993 })
994 .ok();
995 }
996 })
997 .into_any_element()
998 } else {
999 table.into_any_element()
1000 }
1001 }
1002}
1003
1004// computed state related to how to render scrollbars
1005// one per axis
1006// on render we just read this off the keymap editor
1007// we update it when
1008// - settings change
1009// - on focus in, on focus out, on hover, etc.
1010#[derive(Debug)]
1011pub struct ScrollbarProperties {
1012 axis: Axis,
1013 show_scrollbar: bool,
1014 show_track: bool,
1015 auto_hide: bool,
1016 hide_task: Option<Task<()>>,
1017 state: ScrollbarState,
1018}
1019
1020impl ScrollbarProperties {
1021 // Shows the scrollbar and cancels any pending hide task
1022 fn show(&mut self, cx: &mut Context<TableInteractionState>) {
1023 if !self.auto_hide {
1024 return;
1025 }
1026 self.show_scrollbar = true;
1027 self.hide_task.take();
1028 cx.notify();
1029 }
1030
1031 fn hide(&mut self, window: &mut Window, cx: &mut Context<TableInteractionState>) {
1032 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
1033
1034 if !self.auto_hide {
1035 return;
1036 }
1037
1038 let axis = self.axis;
1039 self.hide_task = Some(cx.spawn_in(window, async move |keymap_editor, cx| {
1040 cx.background_executor()
1041 .timer(SCROLLBAR_SHOW_INTERVAL)
1042 .await;
1043
1044 if let Some(keymap_editor) = keymap_editor.upgrade() {
1045 keymap_editor
1046 .update(cx, |keymap_editor, cx| {
1047 match axis {
1048 Axis::Vertical => {
1049 keymap_editor.vertical_scrollbar.show_scrollbar = false
1050 }
1051 Axis::Horizontal => {
1052 keymap_editor.horizontal_scrollbar.show_scrollbar = false
1053 }
1054 }
1055 cx.notify();
1056 })
1057 .ok();
1058 }
1059 }));
1060 }
1061}
1062
1063impl Component for Table<3> {
1064 fn scope() -> ComponentScope {
1065 ComponentScope::Layout
1066 }
1067
1068 fn description() -> Option<&'static str> {
1069 Some("A table component for displaying data in rows and columns with optional styling.")
1070 }
1071
1072 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
1073 Some(
1074 v_flex()
1075 .gap_6()
1076 .children(vec![
1077 example_group_with_title(
1078 "Basic Tables",
1079 vec![
1080 single_example(
1081 "Simple Table",
1082 Table::new()
1083 .width(px(400.))
1084 .header(["Name", "Age", "City"])
1085 .row(["Alice", "28", "New York"])
1086 .row(["Bob", "32", "San Francisco"])
1087 .row(["Charlie", "25", "London"])
1088 .into_any_element(),
1089 ),
1090 single_example(
1091 "Two Column Table",
1092 Table::new()
1093 .header(["Category", "Value"])
1094 .width(px(300.))
1095 .row(["Revenue", "$100,000"])
1096 .row(["Expenses", "$75,000"])
1097 .row(["Profit", "$25,000"])
1098 .into_any_element(),
1099 ),
1100 ],
1101 ),
1102 example_group_with_title(
1103 "Styled Tables",
1104 vec![
1105 single_example(
1106 "Default",
1107 Table::new()
1108 .width(px(400.))
1109 .header(["Product", "Price", "Stock"])
1110 .row(["Laptop", "$999", "In Stock"])
1111 .row(["Phone", "$599", "Low Stock"])
1112 .row(["Tablet", "$399", "Out of Stock"])
1113 .into_any_element(),
1114 ),
1115 single_example(
1116 "Striped",
1117 Table::new()
1118 .width(px(400.))
1119 .striped()
1120 .header(["Product", "Price", "Stock"])
1121 .row(["Laptop", "$999", "In Stock"])
1122 .row(["Phone", "$599", "Low Stock"])
1123 .row(["Tablet", "$399", "Out of Stock"])
1124 .row(["Headphones", "$199", "In Stock"])
1125 .into_any_element(),
1126 ),
1127 ],
1128 ),
1129 example_group_with_title(
1130 "Mixed Content Table",
1131 vec![single_example(
1132 "Table with Elements",
1133 Table::new()
1134 .width(px(840.))
1135 .header(["Status", "Name", "Priority", "Deadline", "Action"])
1136 .row([
1137 Indicator::dot().color(Color::Success).into_any_element(),
1138 "Project A".into_any_element(),
1139 "High".into_any_element(),
1140 "2023-12-31".into_any_element(),
1141 Button::new("view_a", "View")
1142 .style(ButtonStyle::Filled)
1143 .full_width()
1144 .into_any_element(),
1145 ])
1146 .row([
1147 Indicator::dot().color(Color::Warning).into_any_element(),
1148 "Project B".into_any_element(),
1149 "Medium".into_any_element(),
1150 "2024-03-15".into_any_element(),
1151 Button::new("view_b", "View")
1152 .style(ButtonStyle::Filled)
1153 .full_width()
1154 .into_any_element(),
1155 ])
1156 .row([
1157 Indicator::dot().color(Color::Error).into_any_element(),
1158 "Project C".into_any_element(),
1159 "Low".into_any_element(),
1160 "2024-06-30".into_any_element(),
1161 Button::new("view_c", "View")
1162 .style(ButtonStyle::Filled)
1163 .full_width()
1164 .into_any_element(),
1165 ])
1166 .into_any_element(),
1167 )],
1168 ),
1169 ])
1170 .into_any_element(),
1171 )
1172 }
1173}