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