1mod state;
38
39pub use state::{SelectionMode, SortDir};
40
41use std::cmp::Ordering;
42use std::collections::HashMap;
43use std::ops::Range;
44use std::rc::Rc;
45
46use gpui::prelude::*;
47use gpui::{
48 div, px, uniform_list, AnyElement, App, Bounds, Context, Div, DragMoveEvent, Empty, EntityId,
49 EventEmitter, FocusHandle, FontWeight, KeyDownEvent, MouseButton, MouseDownEvent, Pixels,
50 ScrollStrategy, SharedString, Subscription, UniformListScrollHandle, WeakEntity, Window,
51};
52
53use self::state::{cycle_sort, identity_order, sorted_order, SelectionState};
54use super::Content;
55use crate::devtools::Probed;
56use crate::layout::Align;
57use crate::reactive::Signal;
58use crate::style::{FlexExt, TextOverflowExt};
59use crate::theme::{theme, ColorName, Size};
60
61#[derive(Debug, Clone)]
64pub enum TableViewEvent {
65 SelectionChanged(Vec<usize>),
67 Activated(usize),
69 Sorted(Option<(usize, SortDir)>),
71}
72
73type Comparator<T> = Rc<dyn Fn(&T, &T) -> Ordering>;
74type CellBuilder<T> = Rc<dyn Fn(&T, &mut Window, &mut App) -> AnyElement>;
75
76enum CellContent<T> {
77 Text(Rc<dyn Fn(&T) -> SharedString>),
78 Element(CellBuilder<T>),
79}
80
81pub struct Column<T> {
84 title: SharedString,
85 width: Option<f32>,
86 flex: f32,
87 min_width: f32,
88 align: Align,
89 sort: Option<Comparator<T>>,
90 content: Option<CellContent<T>>,
91}
92
93impl<T> Column<T> {
94 pub fn new(title: impl Into<SharedString>) -> Self {
95 Column {
96 title: title.into(),
97 width: None,
98 flex: 1.0,
99 min_width: 60.0,
100 align: Align::Start,
101 sort: None,
102 content: None,
103 }
104 }
105
106 pub fn width(mut self, width: f32) -> Self {
108 self.width = Some(width);
109 self
110 }
111
112 pub fn flex(mut self, flex: f32) -> Self {
114 self.flex = flex;
115 self
116 }
117
118 pub fn min_width(mut self, min_width: f32) -> Self {
121 self.min_width = min_width;
122 self
123 }
124
125 pub fn align(mut self, align: Align) -> Self {
127 self.align = align;
128 self
129 }
130
131 pub fn sortable_by(mut self, cmp: impl Fn(&T, &T) -> Ordering + 'static) -> Self {
135 self.sort = Some(Rc::new(cmp));
136 self
137 }
138
139 pub fn cell<E>(mut self, cell: impl Fn(&T, &mut Window, &mut App) -> E + 'static) -> Self
141 where
142 E: IntoElement,
143 {
144 self.content = Some(CellContent::Element(Rc::new(move |row, window, cx| {
145 cell(row, window, cx).into_any_element()
146 })));
147 self
148 }
149
150 pub fn text(mut self, text: impl Fn(&T) -> SharedString + 'static) -> Self {
153 self.content = Some(CellContent::Text(Rc::new(text)));
154 self
155 }
156}
157
158enum Rows<T> {
160 Owned(Rc<Vec<T>>),
161 Bound(Signal<Vec<T>>),
162}
163
164impl<T> Clone for Rows<T> {
165 fn clone(&self) -> Self {
166 match self {
167 Rows::Owned(rows) => Rows::Owned(rows.clone()),
168 Rows::Bound(signal) => Rows::Bound(signal.clone()),
169 }
170 }
171}
172
173struct ResizeDrag {
177 owner: EntityId,
178 column: usize,
179}
180
181#[derive(Clone, Copy)]
183enum ColWidth {
184 Fixed(f32),
185 Flex(f32, f32), }
187
188pub struct TableView<T: 'static> {
191 columns: Vec<Column<T>>,
192 rows: Rows<T>,
193 focus: FocusHandle,
194 mode: SelectionMode,
195 selection: SelectionState,
196 sort: Option<(usize, SortDir)>,
197 display_order: Vec<usize>,
200 resized: HashMap<usize, f32>,
202 header_bounds: Vec<Bounds<Pixels>>,
204 rows_sub: Option<Subscription>,
206 striped: bool,
207 highlight_on_hover: bool,
208 with_border: bool,
209 height: Option<f32>,
210 empty: Option<Content>,
211 scroll: UniformListScrollHandle,
212}
213
214impl<T: 'static> EventEmitter<TableViewEvent> for TableView<T> {}
215
216impl<T: 'static> TableView<T> {
217 pub fn new(cx: &mut Context<Self>) -> Self {
218 TableView {
219 columns: Vec::new(),
220 rows: Rows::Owned(Rc::new(Vec::new())),
221 focus: cx.focus_handle(),
222 mode: SelectionMode::None,
223 selection: SelectionState::default(),
224 sort: None,
225 display_order: Vec::new(),
226 resized: HashMap::new(),
227 header_bounds: Vec::new(),
228 rows_sub: None,
229 striped: false,
230 highlight_on_hover: false,
231 with_border: false,
232 height: None,
233 empty: None,
234 scroll: UniformListScrollHandle::new(),
235 }
236 }
237
238 pub fn columns(mut self, columns: Vec<Column<T>>) -> Self {
239 self.columns = columns;
240 self
241 }
242
243 pub fn rows(mut self, rows: Vec<T>) -> Self {
246 self.rows = Rows::Owned(Rc::new(rows));
247 self
248 }
249
250 pub fn bind_rows(mut self, signal: &Signal<Vec<T>>, cx: &mut Context<Self>) -> Self {
254 self.rows = Rows::Bound(signal.clone());
255 self.rows_sub = Some(cx.observe(signal.entity(), |this, rows, cx| {
258 let len = rows.read(cx).len();
259 this.prune_selection(len, cx);
260 cx.notify();
261 }));
262 self
263 }
264
265 pub fn selection_mode(mut self, mode: SelectionMode) -> Self {
266 self.mode = mode;
267 self
268 }
269
270 pub fn striped(mut self, striped: bool) -> Self {
271 self.striped = striped;
272 self
273 }
274
275 pub fn highlight_on_hover(mut self, highlight: bool) -> Self {
276 self.highlight_on_hover = highlight;
277 self
278 }
279
280 pub fn with_border(mut self, with_border: bool) -> Self {
281 self.with_border = with_border;
282 self
283 }
284
285 pub fn height(mut self, height: f32) -> Self {
289 self.height = Some(height);
290 self
291 }
292
293 pub fn empty<E>(mut self, builder: impl Fn(&mut Window, &mut App) -> E + 'static) -> Self
295 where
296 E: IntoElement,
297 {
298 self.empty = Some(Box::new(move |window, cx| {
299 builder(window, cx).into_any_element()
300 }));
301 self
302 }
303
304 pub fn set_rows(&mut self, rows: Vec<T>, cx: &mut Context<Self>) {
308 let len = rows.len();
309 self.rows = Rows::Owned(Rc::new(rows));
310 self.rows_sub = None;
311 self.prune_selection(len, cx);
312 cx.notify();
313 }
314
315 pub fn selected(&self) -> Vec<usize> {
317 self.selection.selected()
318 }
319
320 pub fn sort_state(&self) -> Option<(usize, SortDir)> {
322 self.sort
323 }
324
325 pub fn focus_handle(&self) -> FocusHandle {
326 self.focus.clone()
327 }
328
329 fn prune_selection(&mut self, len: usize, cx: &mut Context<Self>) {
332 if self.selection.retain_below(len) {
333 cx.emit(TableViewEvent::SelectionChanged(self.selection.selected()));
334 }
335 }
336
337 fn compute_order(&self, cx: &App) -> Vec<usize> {
340 let sort = self.sort.and_then(|(col, dir)| {
341 let cmp = self.columns.get(col)?.sort.clone()?;
342 Some((dir, cmp))
343 });
344 match &self.rows {
345 Rows::Owned(rows) => order_of(rows, sort),
346 Rows::Bound(signal) => order_of(signal.read(cx), sort),
347 }
348 }
349
350 fn col_width(&self, ix: usize) -> ColWidth {
351 let col = &self.columns[ix];
352 if let Some(&w) = self.resized.get(&ix) {
353 ColWidth::Fixed(w.max(col.min_width))
354 } else if let Some(w) = col.width {
355 ColWidth::Fixed(w.max(col.min_width))
356 } else {
357 ColWidth::Flex(col.flex, col.min_width)
358 }
359 }
360
361 fn toggle_sort(&mut self, column: usize, cx: &mut Context<Self>) {
362 self.sort = cycle_sort(self.sort, column);
363 cx.emit(TableViewEvent::Sorted(self.sort));
364 cx.notify();
365 }
366
367 fn on_resize_drag(
370 &mut self,
371 ev: &DragMoveEvent<ResizeDrag>,
372 _window: &mut Window,
373 cx: &mut Context<Self>,
374 ) {
375 let (owner, column) = {
376 let drag = ev.drag(cx);
377 (drag.owner, drag.column)
378 };
379 if owner != cx.entity_id() {
380 return;
381 }
382 let Some(bounds) = self.header_bounds.get(column) else {
383 return;
384 };
385 let min = self.columns.get(column).map(|c| c.min_width).unwrap_or(0.0);
386 let width = f32::from(ev.event.position.x - bounds.left()).max(min);
387 self.resized.insert(column, width);
388 cx.notify();
389 }
390
391 fn row_mouse_down(
392 &mut self,
393 display: usize,
394 toggle: bool,
395 range: bool,
396 click_count: usize,
397 cx: &mut Context<Self>,
398 ) {
399 if click_count == 2 {
400 if let Some(&source) = self.display_order.get(display) {
401 cx.emit(TableViewEvent::Activated(source));
402 }
403 return;
404 }
405 if matches!(self.mode, SelectionMode::None) {
406 return;
407 }
408 let before = self.selection.selected();
409 self
410 .selection
411 .click(self.mode, &self.display_order, display, toggle, range);
412 let after = self.selection.selected();
413 if before != after {
414 cx.emit(TableViewEvent::SelectionChanged(after));
415 }
416 cx.notify();
417 }
418
419 fn step(&mut self, delta: isize, extend: bool, cx: &mut Context<Self>) {
423 let before = self.selection.selected();
424 let Some(display) = self
425 .selection
426 .step(self.mode, &self.display_order, delta, extend)
427 else {
428 return;
429 };
430 if self.height.is_some() {
431 self.scroll.scroll_to_item(display, ScrollStrategy::Center);
432 }
433 let after = self.selection.selected();
434 if before != after {
435 cx.emit(TableViewEvent::SelectionChanged(after));
436 }
437 cx.notify();
438 cx.stop_propagation();
439 }
440
441 fn on_key(&mut self, ev: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
442 let shift = ev.keystroke.modifiers.shift;
443 match ev.keystroke.key.as_str() {
444 "up" => self.step(-1, shift, cx),
445 "down" => self.step(1, shift, cx),
446 "enter" => {
447 let target = self.selection.cursor().or_else(|| {
448 let selected = self.selection.selected();
449 (selected.len() == 1).then(|| selected[0])
450 });
451 if let Some(source) = target {
452 cx.emit(TableViewEvent::Activated(source));
453 cx.stop_propagation();
454 }
455 }
456 "escape" => self.clear_selection(cx),
457 _ => {}
458 }
459 }
460
461 fn clear_selection(&mut self, cx: &mut Context<Self>) {
464 if self.selection.clear() {
465 cx.emit(TableViewEvent::SelectionChanged(Vec::new()));
466 cx.notify();
467 cx.stop_propagation();
468 }
469 }
470
471 fn render_header(&self, cx: &mut Context<Self>) -> Div {
474 let t = theme(cx);
475 let font = t.font_size(Size::Sm);
476 let dimmed = t.dimmed().hsla();
477 let text = t.text().hsla();
478 let accent = t.primary().hsla();
479 let grip_hover = t.primary().alpha(0.6);
480 let line = t.border().hsla();
481
482 let owner = cx.entity_id();
483 let view = cx.weak_entity();
484 let mut row = div()
485 .flex()
486 .w_full()
487 .border_b_1()
488 .border_color(line)
489 .on_children_prepainted(move |bounds, _window, app| {
492 view.update(app, |this, _| this.header_bounds = bounds).ok();
493 });
494
495 for ix in 0..self.columns.len() {
496 let col = &self.columns[ix];
497 let sortable = col.sort.is_some();
498 let sort_dir = self.sort.filter(|&(c, _)| c == ix).map(|(_, d)| d);
499
500 let grip = div()
501 .id(("guise-tableview-grip", ix))
502 .absolute()
503 .top(px(0.0))
504 .bottom(px(0.0))
505 .right(px(-3.0))
506 .w(px(6.0))
507 .cursor_col_resize()
508 .hover(move |s| s.bg(grip_hover))
509 .on_drag(ResizeDrag { owner, column: ix }, |_, _, _, cx| {
510 cx.new(|_| Empty)
511 })
512 .on_click(|_ev, _window, cx| cx.stop_propagation());
514
515 let mut cell = div()
516 .relative()
517 .flex()
518 .items_center()
519 .gap(px(6.0))
520 .px(px(12.0))
521 .py(px(8.0))
522 .text_size(px(font))
523 .text_color(dimmed)
524 .font_weight(FontWeight::SEMIBOLD);
525 cell = sized(cell, self.col_width(ix));
526 cell = aligned(cell, col.align);
527 cell = cell.child(div().truncate_text().child(col.title.clone()));
528 if let Some(dir) = sort_dir {
529 cell = cell.child(div().text_size(px(font * 0.65)).text_color(accent).child(
530 SharedString::new_static(match dir {
531 SortDir::Asc => "\u{25b2}",
532 SortDir::Desc => "\u{25bc}",
533 }),
534 ));
535 }
536 cell = cell.child(grip);
537
538 let cell: AnyElement = if sortable {
539 cell
540 .id(("guise-tableview-head", ix))
541 .cursor_pointer()
542 .hover(move |s| s.text_color(text))
543 .on_click(cx.listener(move |this, _ev, _window, cx| {
544 this.toggle_sort(ix, cx);
545 }))
546 .into_any_element()
547 } else {
548 cell.into_any_element()
549 };
550 row = row.child(cell);
551 }
552 row
553 }
554
555 fn render_rows(
559 &self,
560 range: Range<usize>,
561 window: &mut Window,
562 cx: &mut Context<Self>,
563 ) -> Vec<AnyElement> {
564 let view = cx.weak_entity();
565 match self.rows.clone() {
566 Rows::Owned(rows) => range
567 .filter_map(|display| {
568 let source = *self.display_order.get(display)?;
569 let row = rows.get(source)?;
570 Some(self.render_row(&view, display, source, row, window, cx))
571 })
572 .collect(),
573 Rows::Bound(signal) => signal.entity().update(cx, |rows, cx| {
574 range
575 .filter_map(|display| {
576 let source = *self.display_order.get(display)?;
577 let row = rows.get(source)?;
578 Some(self.render_row(&view, display, source, row, window, cx))
579 })
580 .collect()
581 }),
582 }
583 }
584
585 fn render_row(
586 &self,
587 view: &WeakEntity<Self>,
588 display: usize,
589 source: usize,
590 row: &T,
591 window: &mut Window,
592 cx: &mut App,
593 ) -> AnyElement {
594 let t = theme(cx);
595 let font = t.font_size(Size::Sm);
596 let text = t.text().hsla();
597 let line = t.border().hsla();
598 let stripe = t.surface_hover().hsla();
599 let hover = t
600 .color(ColorName::Gray, if t.scheme.is_dark() { 6 } else { 1 })
601 .hsla();
602 let selected_bg = t.primary().alpha(0.12);
603
604 let is_selected = self.selection.is_selected(source);
605
606 let mut tr = div()
607 .id(("guise-tableview-row", display))
608 .flex()
609 .w_full()
610 .border_b_1()
611 .border_color(line)
612 .text_size(px(font))
613 .text_color(text);
614
615 if is_selected {
616 tr = tr.bg(selected_bg);
617 } else if self.striped && display % 2 == 1 {
618 tr = tr.bg(stripe);
619 }
620 if self.highlight_on_hover && !is_selected {
621 tr = tr.hover(move |s| s.bg(hover));
622 }
623
624 for (ix, col) in self.columns.iter().enumerate() {
625 let mut cell = div()
626 .flex()
627 .items_center()
628 .px(px(12.0))
629 .py(px(8.0))
630 .overflow_hidden();
631 cell = sized(cell, self.col_width(ix));
632 cell = aligned(cell, col.align);
633 cell = match &col.content {
634 Some(CellContent::Text(to_text)) => cell.child(div().truncate_text().child(to_text(row))),
635 Some(CellContent::Element(build)) => cell.child(build(row, window, cx)),
636 None => cell,
637 };
638 tr = tr.child(cell);
639 }
640
641 let view = view.clone();
642 tr = tr.on_mouse_down(
643 MouseButton::Left,
644 move |ev: &MouseDownEvent, window, app| {
645 let toggle = ev.modifiers.platform;
646 let range = ev.modifiers.shift;
647 let count = ev.click_count;
648 view
649 .update(app, |this, cx| {
650 window.focus(&this.focus);
651 this.row_mouse_down(display, toggle, range, count, cx);
652 })
653 .ok();
654 },
655 );
656
657 tr.into_any_element()
658 }
659}
660
661impl<T: 'static> Render for TableView<T> {
662 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
663 self.display_order = self.compute_order(cx);
664 let count = self.display_order.len();
665
666 let t = theme(cx);
667 let line = t.border().hsla();
668 let dimmed = t.dimmed().hsla();
669 let font = t.font_size(Size::Sm);
670 let radius = t.radius(t.default_radius);
671
672 let header = self.render_header(cx);
673
674 let body: AnyElement = if count == 0 {
675 match &self.empty {
676 Some(builder) => builder(window, cx),
677 None => div()
678 .flex()
679 .items_center()
680 .justify_center()
681 .py(px(24.0))
682 .text_size(px(font))
683 .text_color(dimmed)
684 .child(SharedString::new_static("No data"))
685 .into_any_element(),
686 }
687 } else if let Some(height) = self.height {
688 uniform_list(
689 "guise-tableview-body",
690 count,
691 cx.processor(|this, range: Range<usize>, window, cx| this.render_rows(range, window, cx)),
692 )
693 .h(px(height))
694 .w_full()
695 .track_scroll(self.scroll.clone())
696 .into_any_element()
697 } else {
698 div()
699 .flex()
700 .flex_col()
701 .w_full()
702 .children(self.render_rows(0..count, window, cx))
703 .into_any_element()
704 };
705
706 let mut table = div()
707 .id("guise-tableview")
708 .track_focus(&self.focus)
709 .on_key_down(cx.listener(Self::on_key))
710 .on_drag_move(cx.listener(Self::on_resize_drag))
711 .flex()
712 .flex_col()
713 .w_full()
714 .child(header)
715 .child(body);
716 if self.with_border {
717 table = table
718 .border_1()
719 .border_color(line)
720 .rounded(px(radius))
721 .overflow_hidden();
722 }
723 table.probe("TableView")
724 }
725}
726
727fn order_of<T>(rows: &[T], sort: Option<(SortDir, Comparator<T>)>) -> Vec<usize> {
729 match sort {
730 Some((dir, cmp)) => sorted_order(rows, dir, &*cmp),
731 None => identity_order(rows.len()),
732 }
733}
734
735fn sized(cell: Div, width: ColWidth) -> Div {
738 match width {
739 ColWidth::Fixed(w) => cell.w(px(w)).flex_none(),
740 ColWidth::Flex(factor, min) => cell
741 .grow(factor)
742 .shrink(1.0)
743 .flex_basis(px(0.0))
744 .min_w(px(min)),
745 }
746}
747
748fn aligned(cell: Div, align: Align) -> Div {
750 match align {
751 Align::Start | Align::Stretch => cell.justify_start(),
752 Align::Center => cell.justify_center(),
753 Align::End => cell.justify_end(),
754 }
755}