Skip to main content

guise/data/tableview/
mod.rs

1//! `TableView` — a rich, generic data table (gpui entity).
2//!
3//! Renders typed rows through per-column cell closures, with sortable
4//! headers, click/cmd/shift row selection, a sticky header, drag-resizable
5//! columns and an optionally virtualized body. The simple string [`Table`]
6//! (`data/table.rs`) remains for simple cases.
7//!
8//! ```ignore
9//! struct User { name: String, age: u32 }
10//!
11//! let table = cx.new(|cx| {
12//!     TableView::new(cx)
13//!         .columns(vec![
14//!             Column::new("Name")
15//!                 .text(|u: &User| u.name.clone().into())
16//!                 .sortable_by(|a, b| a.name.cmp(&b.name)),
17//!             Column::new("Age")
18//!                 .width(80.0)
19//!                 .align(Align::End)
20//!                 .text(|u: &User| u.age.to_string().into())
21//!                 .sortable_by(|a, b| a.age.cmp(&b.age)),
22//!         ])
23//!         .rows(users)
24//!         .selection_mode(SelectionMode::Multi)
25//!         .striped(true)
26//!         .with_border(true)
27//!         .height(320.0) // fixed height => virtualized, scrollable body
28//! });
29//! cx.subscribe(&table, |_, _, event: &TableViewEvent, _| match event {
30//!     TableViewEvent::SelectionChanged(rows) => println!("selected {rows:?}"),
31//!     TableViewEvent::Activated(row) => println!("open {row}"),
32//!     TableViewEvent::Sorted(sort) => println!("sort {sort:?}"),
33//! })
34//! .detach();
35//! ```
36
37mod 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/// Events emitted by [`TableView`]. All row indices refer to the **source**
62/// rows, not the current display order.
63#[derive(Debug, Clone)]
64pub enum TableViewEvent {
65    /// The set of selected source rows changed (ascending indices).
66    SelectionChanged(Vec<usize>),
67    /// A row was activated by double-click or Enter.
68    Activated(usize),
69    /// The sort changed: `Some((column, dir))`, or `None` when cleared.
70    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
81/// One column of a [`TableView`]: header title, width policy, alignment,
82/// optional sort comparator, and a cell renderer.
83pub 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    /// Fixed pixel width. Without it the column flexes (see [`Column::flex`]).
107    pub fn width(mut self, width: f32) -> Self {
108        self.width = Some(width);
109        self
110    }
111
112    /// Grow factor for flexing columns (default `1.0`).
113    pub fn flex(mut self, flex: f32) -> Self {
114        self.flex = flex;
115        self
116    }
117
118    /// Lower width bound, honored by both flex sizing and drag-resizing
119    /// (default `60.0`).
120    pub fn min_width(mut self, min_width: f32) -> Self {
121        self.min_width = min_width;
122        self
123    }
124
125    /// Horizontal alignment of the header and cells (default `Align::Start`).
126    pub fn align(mut self, align: Align) -> Self {
127        self.align = align;
128        self
129    }
130
131    /// Make the column sortable. A header click cycles ascending →
132    /// descending → unsorted; the sort is a stable reorder of display
133    /// indices and never mutates the rows.
134    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    /// Custom cell renderer, re-invoked every frame so cells show live data.
140    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    /// Text-cell convenience: the string truncates with an ellipsis when the
151    /// column is too narrow.
152    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
158/// Row storage: an owned snapshot, or a live binding to a `Signal`.
159enum 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
173/// Drag payload for the header resize grips. `owner` scopes `on_drag_move` to
174/// the table that started the drag — the listener fires for every active drag
175/// of this type in the window, including other tables'.
176struct ResizeDrag {
177    owner: EntityId,
178    column: usize,
179}
180
181/// Resolved width policy for one column.
182#[derive(Clone, Copy)]
183enum ColWidth {
184    Fixed(f32),
185    Flex(f32, f32), // (grow factor, min width)
186}
187
188/// A rich data table. Create with
189/// `cx.new(|cx| TableView::new(cx).columns(...).rows(...))`.
190pub 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    /// Source index of each visible row, in display order. Recomputed at the
198    /// top of every render; listeners map display → source through it.
199    display_order: Vec<usize>,
200    /// Columns converted to fixed widths by drag-resizing.
201    resized: HashMap<usize, f32>,
202    /// Header-cell bounds captured after prepaint, for resize math.
203    header_bounds: Vec<Bounds<Pixels>>,
204    /// The `bind_rows` observer; dropped (cancelled) by `set_rows`/rebinding.
205    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    /// Provide the rows as an owned snapshot. Replace later with
244    /// [`TableView::set_rows`].
245    pub fn rows(mut self, rows: Vec<T>) -> Self {
246        self.rows = Rows::Owned(Rc::new(rows));
247        self
248    }
249
250    /// Bind the rows to a `Signal<Vec<T>>`: the table observes the signal
251    /// (signal writes repaint it) and reads the rows at render, so it always
252    /// shows the live value. Selection is pruned when rows disappear.
253    pub fn bind_rows(mut self, signal: &Signal<Vec<T>>, cx: &mut Context<Self>) -> Self {
254        self.rows = Rows::Bound(signal.clone());
255        // Held, not detached: `set_rows` (or a rebind) drops the subscription,
256        // so a stale observer never prunes against the old signal's length.
257        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    /// Fix the body height (px). The body becomes a virtualized
286    /// `uniform_list` scroll region — rows must share one height — and the
287    /// header stays outside it, so it is sticky for free.
288    pub fn height(mut self, height: f32) -> Self {
289        self.height = Some(height);
290        self
291    }
292
293    /// Rendered instead of the body when there are no rows.
294    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    // --- Entity methods ------------------------------------------------------
305
306    /// Replace the rows with a new owned snapshot (drops any signal binding).
307    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    /// The selected source-row indices, ascending.
316    pub fn selected(&self) -> Vec<usize> {
317        self.selection.selected()
318    }
319
320    /// The active sort, if any.
321    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    // --- Internals -----------------------------------------------------------
330
331    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    /// The display order for this frame: a stable index sort when a sorted
338    /// column is active, identity otherwise. Never touches the source rows.
339    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    /// Header-grip drags: the grip carries its column index; the mouse's
368    /// window x minus the header cell's left edge is the new fixed width.
369    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    /// Arrow keys: only consume the key when the cursor actually moves —
419    /// `SelectionMode::None` (the default) and empty tables are no-ops, and
420    /// the host should keep receiving those arrows.
421    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    /// Escape: only consume the key when it actually clears something, so
461    /// hosts (dialogs, ...) still see it otherwise.
462    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    // --- Rendering -----------------------------------------------------------
471
472    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            // The header cells' painted bounds, for resize math: children map
489            // 1:1 to columns (grips are nested inside the cells).
490            .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                // Don't let a stray click on the grip toggle the sort.
512                .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    /// Rows for the display range. For signal-bound rows the backing entity is
554    /// leased with `Entity::update`, which yields `&Vec<T>` *and* a usable
555    /// `&mut App` at once — cell closures need both.
556    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
728/// The display order given optional sorting: pure index math from `state`.
729fn 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
736/// Apply a column's width policy. Fixed columns never flex; flexing columns
737/// share leftover space by grow factor from a zero basis.
738fn 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
749/// Horizontal alignment of a cell's content.
750fn 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}