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