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, TextOverflowExt};
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
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  /// Arrow keys: only consume the key when the cursor actually moves —
420  /// `SelectionMode::None` (the default) and empty tables are no-ops, and
421  /// the host should keep receiving those arrows.
422  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  /// Escape: only consume the key when it actually clears something, so
462  /// hosts (dialogs, ...) still see it otherwise.
463  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  // --- Rendering -----------------------------------------------------------
472
473  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      // The header cells' painted bounds, for resize math: children map
490      // 1:1 to columns (grips are nested inside the cells).
491      .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        // Don't let a stray click on the grip toggle the sort.
513        .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  /// Rows for the display range. For signal-bound rows the backing entity is
556  /// leased with `Entity::update`, which yields `&Vec<T>` *and* a usable
557  /// `&mut App` at once — cell closures need both.
558  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
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}