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::layout::Align;
56use crate::reactive::Signal;
57use crate::style::FlexExt;
58use crate::theme::{theme, ColorName, Size};
59
60/// Events emitted by [`TableView`]. All row indices refer to the **source**
61/// rows, not the current display order.
62#[derive(Debug, Clone)]
63pub enum TableViewEvent {
64    /// The set of selected source rows changed (ascending indices).
65    SelectionChanged(Vec<usize>),
66    /// A row was activated by double-click or Enter.
67    Activated(usize),
68    /// The sort changed: `Some((column, dir))`, or `None` when cleared.
69    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
80/// One column of a [`TableView`]: header title, width policy, alignment,
81/// optional sort comparator, and a cell renderer.
82pub 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    /// Fixed pixel width. Without it the column flexes (see [`Column::flex`]).
106    pub fn width(mut self, width: f32) -> Self {
107        self.width = Some(width);
108        self
109    }
110
111    /// Grow factor for flexing columns (default `1.0`).
112    pub fn flex(mut self, flex: f32) -> Self {
113        self.flex = flex;
114        self
115    }
116
117    /// Lower width bound, honored by both flex sizing and drag-resizing
118    /// (default `60.0`).
119    pub fn min_width(mut self, min_width: f32) -> Self {
120        self.min_width = min_width;
121        self
122    }
123
124    /// Horizontal alignment of the header and cells (default `Align::Start`).
125    pub fn align(mut self, align: Align) -> Self {
126        self.align = align;
127        self
128    }
129
130    /// Make the column sortable. A header click cycles ascending →
131    /// descending → unsorted; the sort is a stable reorder of display
132    /// indices and never mutates the rows.
133    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    /// Custom cell renderer, re-invoked every frame so cells show live data.
139    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    /// Text-cell convenience: the string truncates with an ellipsis when the
150    /// column is too narrow.
151    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
157/// Row storage: an owned snapshot, or a live binding to a `Signal`.
158enum 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
172/// Drag payload for the header resize grips. `owner` scopes `on_drag_move` to
173/// the table that started the drag — the listener fires for every active drag
174/// of this type in the window, including other tables'.
175struct ResizeDrag {
176    owner: EntityId,
177    column: usize,
178}
179
180/// Resolved width policy for one column.
181#[derive(Clone, Copy)]
182enum ColWidth {
183    Fixed(f32),
184    Flex(f32, f32), // (grow factor, min width)
185}
186
187/// A rich data table. Create with
188/// `cx.new(|cx| TableView::new(cx).columns(...).rows(...))`.
189pub 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    /// Source index of each visible row, in display order. Recomputed at the
197    /// top of every render; listeners map display → source through it.
198    display_order: Vec<usize>,
199    /// Columns converted to fixed widths by drag-resizing.
200    resized: HashMap<usize, f32>,
201    /// Header-cell bounds captured after prepaint, for resize math.
202    header_bounds: Vec<Bounds<Pixels>>,
203    /// The `bind_rows` observer; dropped (cancelled) by `set_rows`/rebinding.
204    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    /// Provide the rows as an owned snapshot. Replace later with
243    /// [`TableView::set_rows`].
244    pub fn rows(mut self, rows: Vec<T>) -> Self {
245        self.rows = Rows::Owned(Rc::new(rows));
246        self
247    }
248
249    /// Bind the rows to a `Signal<Vec<T>>`: the table observes the signal
250    /// (signal writes repaint it) and reads the rows at render, so it always
251    /// shows the live value. Selection is pruned when rows disappear.
252    pub fn bind_rows(mut self, signal: &Signal<Vec<T>>, cx: &mut Context<Self>) -> Self {
253        self.rows = Rows::Bound(signal.clone());
254        // Held, not detached: `set_rows` (or a rebind) drops the subscription,
255        // so a stale observer never prunes against the old signal's length.
256        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    /// Fix the body height (px). The body becomes a virtualized
285    /// `uniform_list` scroll region — rows must share one height — and the
286    /// header stays outside it, so it is sticky for free.
287    pub fn height(mut self, height: f32) -> Self {
288        self.height = Some(height);
289        self
290    }
291
292    /// Rendered instead of the body when there are no rows.
293    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    // --- Entity methods ------------------------------------------------------
304
305    /// Replace the rows with a new owned snapshot (drops any signal binding).
306    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    /// The selected source-row indices, ascending.
315    pub fn selected(&self) -> Vec<usize> {
316        self.selection.selected()
317    }
318
319    /// The active sort, if any.
320    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    // --- Internals -----------------------------------------------------------
329
330    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    /// The display order for this frame: a stable index sort when a sorted
337    /// column is active, identity otherwise. Never touches the source rows.
338    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    /// Header-grip drags: the grip carries its column index; the mouse's
367    /// window x minus the header cell's left edge is the new fixed width.
368    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    /// Arrow keys: only consume the key when the cursor actually moves —
418    /// `SelectionMode::None` (the default) and empty tables are no-ops, and
419    /// the host should keep receiving those arrows.
420    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    /// Escape: only consume the key when it actually clears something, so
460    /// hosts (dialogs, ...) still see it otherwise.
461    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    // --- Rendering -----------------------------------------------------------
470
471    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            // The header cells' painted bounds, for resize math: children map
488            // 1:1 to columns (grips are nested inside the cells).
489            .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                // Don't let a stray click on the grip toggle the sort.
511                .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    /// Rows for the display range. For signal-bound rows the backing entity is
553    /// leased with `Entity::update`, which yields `&Vec<T>` *and* a usable
554    /// `&mut App` at once — cell closures need both.
555    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
727/// The display order given optional sorting: pure index math from `state`.
728fn 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
735/// Apply a column's width policy. Fixed columns never flex; flexing columns
736/// share leftover space by grow factor from a zero basis.
737fn 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
748/// Horizontal alignment of a cell's content.
749fn 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}