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;
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.selection
410 .click(self.mode, &self.display_order, display, toggle, range);
411 let after = self.selection.selected();
412 if before != after {
413 cx.emit(TableViewEvent::SelectionChanged(after));
414 }
415 cx.notify();
416 }
417
418 fn step(&mut self, delta: isize, extend: bool, cx: &mut Context<Self>) {
422 let before = self.selection.selected();
423 let Some(display) = self
424 .selection
425 .step(self.mode, &self.display_order, delta, extend)
426 else {
427 return;
428 };
429 if self.height.is_some() {
430 self.scroll.scroll_to_item(display, ScrollStrategy::Center);
431 }
432 let after = self.selection.selected();
433 if before != after {
434 cx.emit(TableViewEvent::SelectionChanged(after));
435 }
436 cx.notify();
437 cx.stop_propagation();
438 }
439
440 fn on_key(&mut self, ev: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
441 let shift = ev.keystroke.modifiers.shift;
442 match ev.keystroke.key.as_str() {
443 "up" => self.step(-1, shift, cx),
444 "down" => self.step(1, shift, cx),
445 "enter" => {
446 let target = self.selection.cursor().or_else(|| {
447 let selected = self.selection.selected();
448 (selected.len() == 1).then(|| selected[0])
449 });
450 if let Some(source) = target {
451 cx.emit(TableViewEvent::Activated(source));
452 cx.stop_propagation();
453 }
454 }
455 "escape" => self.clear_selection(cx),
456 _ => {}
457 }
458 }
459
460 fn clear_selection(&mut self, cx: &mut Context<Self>) {
463 if self.selection.clear() {
464 cx.emit(TableViewEvent::SelectionChanged(Vec::new()));
465 cx.notify();
466 cx.stop_propagation();
467 }
468 }
469
470 fn render_header(&self, cx: &mut Context<Self>) -> Div {
473 let t = theme(cx);
474 let font = t.font_size(Size::Sm);
475 let dimmed = t.dimmed().hsla();
476 let text = t.text().hsla();
477 let accent = t.primary().hsla();
478 let grip_hover = t.primary().alpha(0.6);
479 let line = t.border().hsla();
480
481 let owner = cx.entity_id();
482 let view = cx.weak_entity();
483 let mut row = div()
484 .flex()
485 .w_full()
486 .border_b_1()
487 .border_color(line)
488 .on_children_prepainted(move |bounds, _window, app| {
491 view.update(app, |this, _| this.header_bounds = bounds).ok();
492 });
493
494 for ix in 0..self.columns.len() {
495 let col = &self.columns[ix];
496 let sortable = col.sort.is_some();
497 let sort_dir = self.sort.filter(|&(c, _)| c == ix).map(|(_, d)| d);
498
499 let grip = div()
500 .id(("guise-tableview-grip", ix))
501 .absolute()
502 .top(px(0.0))
503 .bottom(px(0.0))
504 .right(px(-3.0))
505 .w(px(6.0))
506 .cursor_col_resize()
507 .hover(move |s| s.bg(grip_hover))
508 .on_drag(ResizeDrag { owner, column: ix }, |_, _, _, cx| {
509 cx.new(|_| Empty)
510 })
511 .on_click(|_ev, _window, cx| cx.stop_propagation());
513
514 let mut cell = div()
515 .relative()
516 .flex()
517 .items_center()
518 .gap(px(6.0))
519 .px(px(12.0))
520 .py(px(8.0))
521 .text_size(px(font))
522 .text_color(dimmed)
523 .font_weight(FontWeight::SEMIBOLD);
524 cell = sized(cell, self.col_width(ix));
525 cell = aligned(cell, col.align);
526 cell = cell.child(div().min_w(px(0.0)).truncate().child(col.title.clone()));
527 if let Some(dir) = sort_dir {
528 cell = cell.child(div().text_size(px(font * 0.65)).text_color(accent).child(
529 SharedString::new_static(match dir {
530 SortDir::Asc => "\u{25b2}",
531 SortDir::Desc => "\u{25bc}",
532 }),
533 ));
534 }
535 cell = cell.child(grip);
536
537 let cell: AnyElement = if sortable {
538 cell.id(("guise-tableview-head", ix))
539 .cursor_pointer()
540 .hover(move |s| s.text_color(text))
541 .on_click(cx.listener(move |this, _ev, _window, cx| {
542 this.toggle_sort(ix, cx);
543 }))
544 .into_any_element()
545 } else {
546 cell.into_any_element()
547 };
548 row = row.child(cell);
549 }
550 row
551 }
552
553 fn render_rows(
557 &self,
558 range: Range<usize>,
559 window: &mut Window,
560 cx: &mut Context<Self>,
561 ) -> Vec<AnyElement> {
562 let view = cx.weak_entity();
563 match self.rows.clone() {
564 Rows::Owned(rows) => range
565 .filter_map(|display| {
566 let source = *self.display_order.get(display)?;
567 let row = rows.get(source)?;
568 Some(self.render_row(&view, display, source, row, window, cx))
569 })
570 .collect(),
571 Rows::Bound(signal) => signal.entity().update(cx, |rows, cx| {
572 range
573 .filter_map(|display| {
574 let source = *self.display_order.get(display)?;
575 let row = rows.get(source)?;
576 Some(self.render_row(&view, display, source, row, window, cx))
577 })
578 .collect()
579 }),
580 }
581 }
582
583 fn render_row(
584 &self,
585 view: &WeakEntity<Self>,
586 display: usize,
587 source: usize,
588 row: &T,
589 window: &mut Window,
590 cx: &mut App,
591 ) -> AnyElement {
592 let t = theme(cx);
593 let font = t.font_size(Size::Sm);
594 let text = t.text().hsla();
595 let line = t.border().hsla();
596 let stripe = t.surface_hover().hsla();
597 let hover = t
598 .color(ColorName::Gray, if t.scheme.is_dark() { 6 } else { 1 })
599 .hsla();
600 let selected_bg = t.primary().alpha(0.12);
601
602 let is_selected = self.selection.is_selected(source);
603
604 let mut tr = div()
605 .id(("guise-tableview-row", display))
606 .flex()
607 .w_full()
608 .border_b_1()
609 .border_color(line)
610 .text_size(px(font))
611 .text_color(text);
612
613 if is_selected {
614 tr = tr.bg(selected_bg);
615 } else if self.striped && display % 2 == 1 {
616 tr = tr.bg(stripe);
617 }
618 if self.highlight_on_hover && !is_selected {
619 tr = tr.hover(move |s| s.bg(hover));
620 }
621
622 for (ix, col) in self.columns.iter().enumerate() {
623 let mut cell = div()
624 .flex()
625 .items_center()
626 .px(px(12.0))
627 .py(px(8.0))
628 .overflow_hidden();
629 cell = sized(cell, self.col_width(ix));
630 cell = aligned(cell, col.align);
631 cell = match &col.content {
632 Some(CellContent::Text(to_text)) => {
633 cell.child(div().min_w(px(0.0)).truncate().child(to_text(row)))
634 }
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.update(app, |this, cx| {
649 window.focus(&this.focus);
650 this.row_mouse_down(display, toggle, range, count, cx);
651 })
652 .ok();
653 },
654 );
655
656 tr.into_any_element()
657 }
658}
659
660impl<T: 'static> Render for TableView<T> {
661 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
662 self.display_order = self.compute_order(cx);
663 let count = self.display_order.len();
664
665 let t = theme(cx);
666 let line = t.border().hsla();
667 let dimmed = t.dimmed().hsla();
668 let font = t.font_size(Size::Sm);
669 let radius = t.radius(t.default_radius);
670
671 let header = self.render_header(cx);
672
673 let body: AnyElement = if count == 0 {
674 match &self.empty {
675 Some(builder) => builder(window, cx),
676 None => div()
677 .flex()
678 .items_center()
679 .justify_center()
680 .py(px(24.0))
681 .text_size(px(font))
682 .text_color(dimmed)
683 .child(SharedString::new_static("No data"))
684 .into_any_element(),
685 }
686 } else if let Some(height) = self.height {
687 uniform_list(
688 "guise-tableview-body",
689 count,
690 cx.processor(|this, range: Range<usize>, window, cx| {
691 this.render_rows(range, window, cx)
692 }),
693 )
694 .h(px(height))
695 .w_full()
696 .track_scroll(self.scroll.clone())
697 .into_any_element()
698 } else {
699 div()
700 .flex()
701 .flex_col()
702 .w_full()
703 .children(self.render_rows(0..count, window, cx))
704 .into_any_element()
705 };
706
707 let mut table = div()
708 .id("guise-tableview")
709 .track_focus(&self.focus)
710 .on_key_down(cx.listener(Self::on_key))
711 .on_drag_move(cx.listener(Self::on_resize_drag))
712 .flex()
713 .flex_col()
714 .w_full()
715 .child(header)
716 .child(body);
717 if self.with_border {
718 table = table
719 .border_1()
720 .border_color(line)
721 .rounded(px(radius))
722 .overflow_hidden();
723 }
724 table.probe("TableView")
725 }
726}
727
728fn order_of<T>(rows: &[T], sort: Option<(SortDir, Comparator<T>)>) -> Vec<usize> {
730 match sort {
731 Some((dir, cmp)) => sorted_order(rows, dir, &*cmp),
732 None => identity_order(rows.len()),
733 }
734}
735
736fn sized(cell: Div, width: ColWidth) -> Div {
739 match width {
740 ColWidth::Fixed(w) => cell.w(px(w)).flex_none(),
741 ColWidth::Flex(factor, min) => cell
742 .grow(factor)
743 .shrink(1.0)
744 .flex_basis(px(0.0))
745 .min_w(px(min)),
746 }
747}
748
749fn aligned(cell: Div, align: Align) -> Div {
751 match align {
752 Align::Start | Align::Stretch => cell.justify_start(),
753 Align::Center => cell.justify_center(),
754 Align::End => cell.justify_end(),
755 }
756}