Skip to main content

herogpui_components/
table.rs

1//! Table — port of `@heroui/table` (v3).
2//!
3//! `variant` selects between the surface container (`primary`) and the flat,
4//! transparent-row layout (`secondary`).
5//!
6//! Selection and sorting are controlled, as they are in v3: the table reports
7//! what the user asked for and the caller owns the data. In particular the
8//! table never reorders its own rows — it cannot, since a row is a list of
9//! rendered cells, not a record.
10
11use gpui::{
12    prelude::*, px, AnyElement, App, ClickEvent, InteractiveElement, IntoElement, ParentElement,
13    Pixels, RenderOnce, SharedString, Styled, Window,
14};
15use herogpui_core::{element_id, SelectionBehavior, SelectionMode};
16use herogpui_theme::ActiveTheme;
17
18use crate::{
19    a11y::{self, A11y as _},
20    checkbox::Checkbox,
21    icons,
22};
23
24type OnRowClick = std::sync::Arc<dyn Fn(&usize, &ClickEvent, &mut Window, &mut App) + 'static>;
25type OnSelectionChange = std::sync::Arc<dyn Fn(&[SharedString], &mut Window, &mut App) + 'static>;
26type OnSortChange = std::sync::Arc<dyn Fn(&SortDescriptor, &mut Window, &mut App) + 'static>;
27type OnLoadMore = std::sync::Arc<dyn Fn(&mut Window, &mut App) + 'static>;
28type OnResize = std::sync::Arc<dyn Fn(&[(SharedString, Pixels)], &mut Window, &mut App) + 'static>;
29type VirtualRowKey = std::sync::Arc<dyn Fn(usize) -> SharedString + 'static>;
30type VirtualRow = std::sync::Arc<dyn Fn(usize) -> TableRow + 'static>;
31type VirtualRowText = std::sync::Arc<dyn Fn(usize) -> SharedString + 'static>;
32type VirtualTree = std::sync::Arc<dyn Fn(usize) -> VirtualTreeMetadata + 'static>;
33
34const DEFAULT_COLUMN_MIN_WIDTH: f32 = 75.;
35
36/// Visual variant of a table (`variant`).
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub enum TableVariant {
39    /// A surface container behind the rows.
40    #[default]
41    Primary,
42    /// Flat, with transparent rows and no container.
43    Secondary,
44}
45
46impl TableVariant {
47    /// Every table variant, in display order.
48    pub const ALL: [TableVariant; 2] = [TableVariant::Primary, TableVariant::Secondary];
49
50    /// The variant's display name.
51    pub fn label(self) -> &'static str {
52        match self {
53            TableVariant::Primary => "Primary",
54            TableVariant::Secondary => "Secondary",
55        }
56    }
57}
58
59/// `sortDirection` on `Table.SortableColumnHeader`.
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum SortDirection {
62    /// Sort from lowest to highest.
63    Ascending,
64    /// Sort from highest to lowest.
65    Descending,
66}
67
68impl SortDirection {
69    /// The direction a second click on the same column asks for.
70    pub fn flipped(self) -> Self {
71        match self {
72            SortDirection::Ascending => SortDirection::Descending,
73            SortDirection::Descending => SortDirection::Ascending,
74        }
75    }
76}
77
78type Indicator = std::sync::Arc<dyn Fn(SortDirection) -> AnyElement + 'static>;
79
80/// `sortDescriptor` — which column is sorted, and which way.
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct SortDescriptor {
83    /// Key of the sorted column.
84    pub column: SharedString,
85    /// Direction of the sort.
86    pub direction: SortDirection,
87}
88
89impl SortDescriptor {
90    /// Creates a descriptor for `column` sorted in `direction`.
91    pub fn new(column: impl Into<SharedString>, direction: SortDirection) -> Self {
92        Self {
93            column: column.into(),
94            direction,
95        }
96    }
97
98    /// The descriptor a click on `column` should produce.
99    ///
100    /// Clicking the sorted column flips it; clicking any other column starts
101    /// ascending, which is what React Aria does.
102    pub fn next(current: Option<&SortDescriptor>, column: impl Into<SharedString>) -> Self {
103        let column = column.into();
104        match current {
105            Some(d) if d.column == column => Self {
106                column,
107                direction: d.direction.flipped(),
108            },
109            _ => Self {
110                column,
111                direction: SortDirection::Ascending,
112            },
113        }
114    }
115}
116
117/// One column (`Table.Column`).
118#[must_use = "builder methods return a new value; pass the column to its table"]
119#[derive(Clone, Debug)]
120pub struct TableColumn {
121    label: SharedString,
122    allows_sorting: bool,
123    is_row_header: bool,
124    /// `allowsResizing` — whether a handle on this column's trailing edge
125    /// resizes it.
126    allows_resizing: bool,
127    /// `width` — a caller-owned controlled column width.
128    width: Option<Pixels>,
129    /// `defaultWidth` — the uncontrolled starting width. Without one the
130    /// column shares the row evenly, which is what `flex-1` does.
131    default_width: Option<Pixels>,
132    /// `minWidth` — the column's floor.
133    min_width: Option<Pixels>,
134    /// `maxWidth` — the column's ceiling.
135    max_width: Option<Pixels>,
136    /// Frozen at the leading edge (HeroGPUI extension).
137    frozen: bool,
138}
139
140impl TableColumn {
141    /// Creates a column with the given header label.
142    pub fn new(label: impl Into<SharedString>) -> Self {
143        Self {
144            label: label.into(),
145            allows_sorting: false,
146            is_row_header: false,
147            allows_resizing: false,
148            width: None,
149            default_width: None,
150            min_width: None,
151            max_width: None,
152            frozen: false,
153        }
154    }
155
156    /// `allowsResizing` — put a drag handle on this column's trailing edge.
157    pub fn allows_resizing(mut self, v: bool) -> Self {
158        self.allows_resizing = v;
159        self
160    }
161
162    /// `defaultWidth` — the column's starting width, which a resize handle then
163    /// moves.
164    pub fn default_width(mut self, width: impl Into<Pixels>) -> Self {
165        self.default_width = Some(width.into());
166        self
167    }
168
169    /// `width` — the caller-owned controlled width of this column.
170    pub fn width(mut self, width: impl Into<Pixels>) -> Self {
171        self.width = Some(width.into());
172        self
173    }
174
175    /// `minWidth` — the width this column will not go below.
176    pub fn min_width(mut self, width: impl Into<Pixels>) -> Self {
177        self.min_width = Some(width.into());
178        self
179    }
180
181    /// `maxWidth` — the width this column will not exceed.
182    pub fn max_width(mut self, width: impl Into<Pixels>) -> Self {
183        self.max_width = Some(width.into());
184        self
185    }
186
187    /// `allowsSorting` — makes the header a sort control.
188    pub fn allows_sorting(mut self, v: bool) -> Self {
189        self.allows_sorting = v;
190        self
191    }
192
193    /// `isRowHeader` — this column names the row.
194    ///
195    /// With no accessibility layer to expose it to, the effect here is visual:
196    /// the column's cells carry medium weight so the identifying value reads
197    /// first.
198    pub fn is_row_header(mut self, v: bool) -> Self {
199        self.is_row_header = v;
200        self
201    }
202
203    /// Freezes this column at the table's leading edge (HeroGPUI extension,
204    /// after gpui-kit's `fixed_left` columns; not a HeroUI v3 prop): when the
205    /// table is wider than its box, the frozen columns (and the selection
206    /// column) stay put while the other columns scroll horizontally beneath
207    /// the header, which scrolls with them.
208    ///
209    /// Frozen columns are always displayed first, in their relative order, so
210    /// a `column_order` that interleaves them is shown with the frozen ones
211    /// moved to the front, and a column move never crosses from the frozen
212    /// region into the scrolling one or back: unfreeze the column to move it
213    /// there. A frozen column is sized at its explicit, resized or measured
214    /// width rather than sharing out the free space. When every column is
215    /// frozen nothing scrolls and the table renders as an ordinary one.
216    pub fn frozen(mut self, frozen: bool) -> Self {
217        self.frozen = frozen;
218        self
219    }
220}
221
222impl From<SharedString> for TableColumn {
223    fn from(label: SharedString) -> Self {
224        TableColumn::new(label)
225    }
226}
227
228impl From<&str> for TableColumn {
229    fn from(label: &str) -> Self {
230        TableColumn::new(label.to_owned())
231    }
232}
233
234/// One row (`Table.Row`).
235#[must_use = "builder methods return a new value; pass it on to its component"]
236pub struct TableRow {
237    key: Option<SharedString>,
238    text_value: Option<SharedString>,
239    cells: Vec<AnyElement>,
240    /// The rows nested under this one. v3 calls them the row's `children`, and
241    /// `expandedKeys` decides which parents show theirs.
242    children: Vec<TableRow>,
243}
244
245/// Cheap tree structure for one preorder item in a virtual table.
246///
247/// The row factory still runs only for items the viewport builds. This metadata
248/// is projected for the whole collection so expansion can derive the visible
249/// indices without eagerly constructing any cells.
250#[derive(Clone, Debug, Default, PartialEq, Eq)]
251pub struct VirtualTreeMetadata {
252    /// Nesting depth of the item; zero for a root.
253    pub depth: usize,
254    /// Key of the item's parent, or `None` for a root.
255    pub parent_key: Option<SharedString>,
256    /// Whether the item has children.
257    pub has_children: bool,
258}
259
260impl TableRow {
261    /// Creates a row from its cells, with no key, text value or children.
262    pub fn new(cells: Vec<AnyElement>) -> Self {
263        Self {
264            key: None,
265            text_value: None,
266            cells,
267            children: Vec::new(),
268        }
269    }
270
271    /// The rows nested under this one.
272    pub fn children(mut self, rows: Vec<TableRow>) -> Self {
273        self.children = rows;
274        self
275    }
276
277    /// The selection key. Defaults to the row's index.
278    pub fn key(mut self, key: impl Into<SharedString>) -> Self {
279        self.key = Some(key.into());
280        self
281    }
282
283    /// The plain-text value used by the table's typeahead.
284    ///
285    /// Cells are opaque `AnyElement`s, so rows without this value do not
286    /// participate in typeahead even when a cell visibly draws text.
287    pub fn text_value(mut self, value: impl Into<SharedString>) -> Self {
288        self.text_value = Some(value.into());
289        self
290    }
291}
292
293type OnExpandedChange = std::sync::Arc<dyn Fn(&[SharedString], &mut Window, &mut App) + 'static>;
294
295#[derive(Clone, Copy, PartialEq, Eq)]
296enum RowActivation {
297    Pointer,
298    Enter,
299    Space,
300}
301
302#[derive(Clone, Copy, PartialEq, Eq)]
303enum RowIntent {
304    Action,
305    Selection,
306    None,
307}
308
309#[derive(Clone, Debug, Default)]
310struct TableTypeahead {
311    query: String,
312    last: Option<web_time::Instant>,
313}
314
315#[derive(Clone, Debug, Default)]
316struct TableSelectionRange {
317    anchor: Option<SharedString>,
318    current: Option<SharedString>,
319    is_all: bool,
320}
321
322impl TableTypeahead {
323    fn is_active(&self, now: web_time::Instant) -> bool {
324        !self.query.is_empty()
325            && self
326                .last
327                .is_some_and(|last| now.duration_since(last) <= crate::list_nav::TYPEAHEAD_TIMEOUT)
328    }
329
330    fn push(&mut self, key: &str, now: web_time::Instant) -> String {
331        if self
332            .last
333            .is_none_or(|last| now.duration_since(last) > crate::list_nav::TYPEAHEAD_TIMEOUT)
334        {
335            self.query.clear();
336        }
337        self.last = Some(now);
338        self.query.push_str(key);
339        self.query.clone()
340    }
341
342    fn clear(&mut self) {
343        self.query.clear();
344        self.last = None;
345    }
346}
347
348#[derive(Clone)]
349struct TableTypeaheadNavigation {
350    state: gpui::Entity<TableTypeahead>,
351    labels: Vec<String>,
352    virtual_indices: Option<Vec<usize>>,
353    virtual_text: Option<VirtualRowText>,
354    stops: Vec<usize>,
355    keys: Vec<SharedString>,
356    cursor: gpui::Entity<Option<SharedString>>,
357    fixed_virtual: bool,
358    fixed_scroll: crate::VirtualListHandle,
359    variable_scroll: Option<gpui::ListState>,
360}
361
362impl TableTypeaheadNavigation {
363    fn push(
364        &self,
365        character: &str,
366        now: web_time::Instant,
367        clear_on_failure: bool,
368        cx: &mut App,
369    ) -> bool {
370        let from = self
371            .cursor
372            .read(cx)
373            .as_ref()
374            .and_then(|key| self.keys.iter().position(|row| row == key))
375            .filter(|index| self.stops.contains(index));
376        let mut query = String::new();
377        self.state.update(cx, |state, _| {
378            query = state.push(character, now);
379        });
380        let projected_labels = self
381            .virtual_text
382            .as_ref()
383            .zip(self.virtual_indices.as_ref())
384            .map(|(text_value, indices)| {
385                indices
386                    .iter()
387                    .map(|index| text_value(*index).to_string())
388                    .collect::<Vec<_>>()
389            });
390        let searchable_labels = projected_labels.as_ref().unwrap_or(&self.labels);
391        let Some(next) =
392            crate::list_nav::typeahead(searchable_labels, &self.stops, from, &query, false)
393        else {
394            if clear_on_failure {
395                self.state.update(cx, |state, _| state.clear());
396            }
397            return false;
398        };
399        self.cursor.update(cx, |value, cx| {
400            *value = self.keys.get(next).cloned();
401            cx.notify();
402        });
403        if self.fixed_virtual {
404            self.fixed_scroll
405                .scroll_to_item(next, crate::VirtualListScroll::Center);
406        } else if let Some(state) = &self.variable_scroll {
407            state.scroll_to(gpui::ListOffset {
408                item_ix: next,
409                offset_in_item: px(0.),
410            });
411        }
412        true
413    }
414}
415
416#[derive(Clone, PartialEq, Eq)]
417enum LoadMoreCollection {
418    Rows(Vec<SharedString>),
419    Virtual {
420        count: usize,
421        identity: SharedString,
422    },
423}
424
425fn row_intent(
426    activation: RowActivation,
427    mode: SelectionMode,
428    behavior: SelectionBehavior,
429    selection_is_empty: bool,
430    has_action: bool,
431) -> RowIntent {
432    // React Aria's replace behavior reserves a row's plain pointer activation
433    // for selection whenever the row is selectable; its action becomes a
434    // secondary (double-click/Enter) action. Toggle keeps the existing
435    // single-click action while the collection is empty.
436    let primary_action = has_action
437        && (mode == SelectionMode::None
438            || (behavior == SelectionBehavior::Toggle && selection_is_empty));
439    match activation {
440        RowActivation::Pointer if primary_action => RowIntent::Action,
441        RowActivation::Pointer if mode != SelectionMode::None => RowIntent::Selection,
442        RowActivation::Enter if primary_action => RowIntent::Action,
443        RowActivation::Enter if !has_action && mode != SelectionMode::None => RowIntent::Selection,
444        RowActivation::Space if mode != SelectionMode::None => RowIntent::Selection,
445        _ => RowIntent::None,
446    }
447}
448
449fn select_all_flags(selectable: &[SharedString], selected: &[SharedString]) -> (bool, bool) {
450    let selected_count = selectable
451        .iter()
452        .filter(|key| selected.contains(key))
453        .count();
454    let all_selected = !selectable.is_empty() && selected_count == selectable.len();
455    let indeterminate = !selected.is_empty() && !all_selected;
456    (all_selected, indeterminate)
457}
458
459fn same_selection(left: &[SharedString], right: &[SharedString]) -> bool {
460    left.len() == right.len()
461        && left.iter().collect::<std::collections::HashSet<_>>()
462            == right.iter().collect::<std::collections::HashSet<_>>()
463}
464
465fn extend_selection_range(
466    current: &[SharedString],
467    collection: &[SharedString],
468    selectable: &[SharedString],
469    range: &TableSelectionRange,
470    target: &SharedString,
471) -> Vec<SharedString> {
472    if range.is_all {
473        return vec![target.clone()];
474    }
475    let anchor = range.anchor.as_ref().unwrap_or(target);
476    let previous = range.current.as_ref().unwrap_or(target);
477    let mut anchor_at = None;
478    let mut previous_at = None;
479    let mut target_at = None;
480    for (index, key) in collection.iter().enumerate() {
481        anchor_at = anchor_at.or_else(|| (key == anchor).then_some(index));
482        previous_at = previous_at.or_else(|| (key == previous).then_some(index));
483        target_at = target_at.or_else(|| (key == target).then_some(index));
484    }
485    let between = |from: Option<usize>, to: Option<usize>| {
486        from.zip(to)
487            .map(|(from, to)| if from <= to { from..=to } else { to..=from })
488    };
489    let mut next = current.to_vec();
490    if let Some(previous_range) = between(anchor_at, previous_at) {
491        let removed: std::collections::HashSet<&SharedString> =
492            previous_range.map(|index| &collection[index]).collect();
493        next.retain(|key| !removed.contains(key));
494    }
495    if let Some(target_range) = between(anchor_at, target_at) {
496        let selectable: std::collections::HashSet<&SharedString> = selectable.iter().collect();
497        let mut selected: std::collections::HashSet<SharedString> = next.iter().cloned().collect();
498        for index in target_range {
499            let key = &collection[index];
500            if selectable.contains(key) && selected.insert(key.clone()) {
501                next.push(key.clone());
502            }
503        }
504    }
505    next
506}
507
508/// Pinned React Aria 3.51.0 `useSelectableCollection` registers Home and End
509/// only for the chords each platform's handler admits: none, Shift, Alt, and
510/// Alt+Shift on macOS -- no Meta or Control handler exists -- and none,
511/// Shift, Control, and Control+Shift on Windows and Linux. The upstream
512/// matcher reads exactly the browser's canonical modifier flags -- Alt,
513/// Control, Meta, Shift -- so GPUI's `function` flag is ignored here: a
514/// browser exposes no Fn state for it to read, so vetoing on the flag would
515/// claim a pinned guard that does not exist, and the framework delivers an
516/// Fn-bearing press with every matched modifier flag still false. A chord
517/// outside the registration is entirely inert: no focus move, no selection,
518/// no preventDefault. `macos` is simulated explicitly so every platform's
519/// unit tests can prove both maps.
520fn home_end_registered(modifiers: gpui::Modifiers, macos: bool) -> bool {
521    if macos {
522        !modifiers.control && !modifiers.platform
523    } else {
524        !modifiers.alt && !modifiers.platform
525    }
526}
527
528/// Pinned `useSelectableCollection` (`isCtrlKeyPressed`): a Shift move
529/// extends the range on the collection's navigation keys, while Home and
530/// End extend only from Control+Shift on Windows and Linux. macOS registers
531/// no Home/End extension at all -- its Shift and Alt+Shift chords move the
532/// focus alone -- so the platform is an explicit bool rather than a `cfg!`.
533fn shift_home_end_extends(key_name: &str, control: bool, macos: bool) -> bool {
534    !matches!(key_name, "home" | "end") || (!macos && control)
535}
536
537/// A column with a `defaultWidth` takes it; the rest split what is left.
538///
539/// `flex_basis(0)` is the part that matters: a bare `flex_1` sizes a cell by its
540/// content, so the tree column's indent shifted every column after it, and a
541/// fraction of the *whole* row cannot account for the fixed columns.
542fn flex_cell(el: gpui::Div) -> gpui::Div {
543    el.flex_basis(px(0.)).flex_1()
544}
545
546/// Frozen leading columns (HeroGPUI extension, see [`TableColumn::frozen`]):
547/// how many display positions are frozen, and the one horizontal scroll
548/// state every row's scrolling part and the header's share.
549///
550/// GPUI has no sticky positioning, so each row (and the header) is split in
551/// two: a frozen part that never moves and a clipped part that scrolls. The
552/// row element stays the parent of both, so its hover group, press,
553/// selection and keyboard cursor span the whole row, and a cell scrolled out
554/// of view is clipped by its part's content mask rather than hidden under a
555/// frozen cell, so a press on a frozen cell cannot reach it.
556#[derive(Clone)]
557struct FrozenColumns {
558    /// The frozen display positions, `0..count`; always fewer than the
559    /// columns, since a table with every column frozen does not split.
560    count: usize,
561    scroll: gpui::ScrollHandle,
562    /// The view to notify when a wheel over a frozen part scrolls.
563    view: gpui::EntityId,
564}
565
566impl FrozenColumns {
567    /// The frozen part of a row or of the header. A horizontal wheel over it
568    /// scrolls the rest of the table, the way it does over the scrolling
569    /// part.
570    fn frozen_part(&self) -> gpui::Div {
571        let scroll = self.scroll.clone();
572        let view = self.view;
573        gpui::div()
574            .flex()
575            .flex_shrink_0()
576            .on_scroll_wheel(move |event, window, cx| {
577                let at = scroll.offset();
578                crate::util::shift_wheel_scroll_x(&scroll, event);
579                let delta = event.delta.pixel_delta(window.line_height());
580                let x_delta = if delta.x != px(0.) {
581                    delta.x
582                } else {
583                    scroll.offset().x - at.x
584                };
585                if x_delta == px(0.) {
586                    return;
587                }
588                let x = (at.x + x_delta).clamp(-scroll.max_offset().x, px(0.));
589                if x != at.x {
590                    scroll.set_offset(gpui::point(x, at.y));
591                    cx.notify(view);
592                }
593            })
594    }
595
596    /// The scrolling part: its cells are direct children, so the shared
597    /// handle's child bounds are the cells' and `scroll_to_item` can reveal
598    /// one. `restrict_scroll_to_axis` leaves a vertical wheel to the body.
599    fn scrolling_part(&self, part: gpui::Div, id: gpui::ElementId) -> gpui::Stateful<gpui::Div> {
600        part.flex()
601            .flex_1()
602            .min_w_0()
603            .id(id)
604            .overflow_x_scroll()
605            .track_scroll(&self.scroll)
606            .restrict_scroll_to_axis()
607    }
608
609    /// Reveals the cell at display position `column` when it scrolls.
610    fn reveal(&self, column: usize) {
611        if column >= self.count {
612            self.scroll.scroll_to_item(column - self.count);
613        }
614    }
615}
616
617/// `to` held inside the region `from` is in, so a column move never crosses
618/// between the frozen and the scrolling columns.
619fn clamp_to_region(from: usize, to: usize, frozen: usize) -> usize {
620    if frozen == 0 {
621        to
622    } else if from < frozen {
623        to.min(frozen - 1)
624    } else {
625        to.max(frozen)
626    }
627}
628
629/// `order` with the frozen columns first, each region keeping its order.
630fn frozen_first(order: &[usize], columns: &[TableColumn]) -> Vec<usize> {
631    let frozen = |ix: &usize| columns.get(*ix).is_some_and(|c| c.frozen);
632    order
633        .iter()
634        .copied()
635        .filter(frozen)
636        .chain(order.iter().copied().filter(|ix| !frozen(ix)))
637        .collect()
638}
639
640/// The pinned table stylesheet replaces the tree column's start padding with
641/// one `1rem` step for every 1-based row level. `flatten_tree` stores the root
642/// at depth zero, so a root keeps the regular 16px cell padding and a child at
643/// depth `d` resolves to `16px * (d + 1)`. Keeping this in one helper avoids
644/// the intrinsic-width probe and the painted cell drifting apart at deeper
645/// levels.
646fn tree_column_padding(depth: usize) -> Option<Pixels> {
647    (depth > 0).then(|| px(16. * (depth as f32 + 1.)))
648}
649
650/// The horizontal extent of the direct children that a cell lays out. GPUI
651/// reports absolute child bounds, so taking their union keeps a label and a
652/// sort indicator together while ignoring an empty cell. One-pixel separator
653/// overlays are deliberately excluded; they are decorative and must not turn
654/// into a column's intrinsic width.
655#[allow(clippy::float_cmp)] // f32::MAX is the unset sentinel, checked exactly
656fn intrinsic_children_width(bounds: &[gpui::Bounds<Pixels>]) -> f32 {
657    let mut left = f32::MAX;
658    let mut right = f32::MIN;
659    for bound in bounds.iter().filter(|bound| bound.size.width > px(2.)) {
660        left = left.min(f32::from(bound.origin.x));
661        right = right.max(f32::from(bound.origin.x + bound.size.width));
662    }
663    if left == f32::MAX {
664        0.0
665    } else {
666        (right - left).max(0.0)
667    }
668}
669
670/// Paints the split inset ring HeroUI uses for a focused row cell.
671///
672/// A single rounded overlay on the row makes the focus geometry look plausible
673/// until a cell has its own clipping/background: the outer corners and the
674/// shared vertical edges then bleed into neighboring cells. Keeping the four
675/// strips on each cell reproduces the stylesheet's first/middle/last cases
676/// without changing cell layout or intercepting the row's click handler.
677fn table_cell_focus_ring(cx: &App, first: bool, last: bool) -> gpui::Div {
678    let color = cx.colors().focus;
679    let radius = crate::util::key_radius(cx);
680    let mut top = gpui::div()
681        .absolute()
682        .top_0()
683        .left_0()
684        .right_0()
685        .h(px(2.))
686        .bg(color);
687    let mut bottom = gpui::div()
688        .absolute()
689        .bottom_0()
690        .left_0()
691        .right_0()
692        .h(px(2.))
693        .bg(color);
694    if first {
695        top = top.rounded_tl(radius);
696        bottom = bottom.rounded_bl(radius);
697    }
698    if last {
699        top = top.rounded_tr(radius);
700        bottom = bottom.rounded_br(radius);
701    }
702    let mut ring = gpui::div().absolute().inset_0().child(top).child(bottom);
703    if first {
704        ring = ring.child(
705            gpui::div()
706                .absolute()
707                .top_0()
708                .bottom_0()
709                .left_0()
710                .w(px(2.))
711                .rounded_tl(radius)
712                .rounded_bl(radius)
713                .bg(color),
714        );
715    }
716    if last {
717        ring = ring.child(
718            gpui::div()
719                .absolute()
720                .top_0()
721                .bottom_0()
722                .right_0()
723                .w(px(2.))
724                .rounded_tr(radius)
725                .rounded_br(radius)
726                .bg(color),
727        );
728    }
729    ring
730}
731
732/// HeroUI Table.
733#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
734#[derive(IntoElement)]
735pub struct Table {
736    /// Distinguishes one table's keyed state from another's: the resized column
737    /// widths, the drag in progress, the focus handle and the row cursor all
738    /// hang off it. v3 needs no such prop -- React keys by position in the tree
739    /// -- so the default is a name shared by every table, which only goes wrong
740    /// when a page shows two of them.
741    id: SharedString,
742    /// `indicator` — v3's render prop for the sort chevron, handed the
743    /// `sortDirection` the column is sorted in.
744    indicator: Option<Indicator>,
745    columns: Vec<TableColumn>,
746    rows: Vec<TableRow>,
747    /// `treeColumn` — which column carries the expand chevron.
748    tree_column: usize,
749    /// `expandedKeys` — the parent rows showing their children.
750    expanded_keys: Vec<SharedString>,
751    on_expanded_change: Option<OnExpandedChange>,
752    /// `TableLayout`'s `rowHeight`. Together with [`Table::virtual_rows`] it
753    /// virtualizes the body: a fixed row height is what lets the scroll geometry
754    /// be computed rather than laid out.
755    row_height: Option<Pixels>,
756    /// `TableLayout`'s `estimatedRowHeight` — virtualizes rows whose heights
757    /// differ, where `rowHeight` needs them all the same.
758    estimated_row_height: Option<Pixels>,
759    /// `TableLayout`'s `loaderHeight` — the fixed height of the load-more row.
760    loader_height: Option<Pixels>,
761    /// `scrollOffset` on `Table.LoadMore`, in viewport heights.
762    load_more_offset: f32,
763    /// How tall the virtual body is. v3 sets it with a `className`.
764    max_h: Option<Pixels>,
765    /// `TableLayout`'s `gap` and `padding`, both 0 in v3: rows meet, separated
766    /// by their border rather than by space.
767    gap: Option<Pixels>,
768    padding: Option<Pixels>,
769    /// The row factory a virtual table needs. Cells are `AnyElement`, which
770    /// cannot be built up front and then handed out again on the next scroll,
771    /// so a virtual table asks for its rows one at a time.
772    virtual_rows: Option<(usize, SharedString, VirtualRowKey, VirtualRow)>,
773    virtual_text_value: Option<VirtualRowText>,
774    virtual_tree_metadata: Option<VirtualTree>,
775    variant: TableVariant,
776    selection_mode: SelectionMode,
777    selection_behavior: SelectionBehavior,
778    selected_keys: Vec<SharedString>,
779    default_selected_keys: Vec<SharedString>,
780    disabled_keys: Vec<SharedString>,
781    disallow_empty_selection: bool,
782    is_selection_controlled: bool,
783    sort_descriptor: Option<SortDescriptor>,
784    show_indicator: bool,
785    is_pending: bool,
786    empty_state: Option<AnyElement>,
787    footer: Option<AnyElement>,
788    on_row_click: Option<OnRowClick>,
789    on_selection_change: Option<OnSelectionChange>,
790    on_sort_change: Option<OnSortChange>,
791    on_load_more: Option<OnLoadMore>,
792    on_resize_start: Option<OnResize>,
793    on_resize: Option<OnResize>,
794    on_resize_end: Option<OnResize>,
795    /// The fill a hovered *unselected* row takes, in place of the variant's
796    /// hover wash. A selected row keeps its selection fill.
797    row_hover_bg: Option<gpui::Hsla>,
798    /// The wrapper shell's corner radius, in place of the owning
799    /// `container_radius` helper. `Primary` paints its own tray radius over
800    /// this one, exactly as it covers the helper's value.
801    radius: Option<Pixels>,
802    /// The `sx` slot, refined over the root style at the end of render.
803    sx: Option<Box<gpui::StyleRefinement>>,
804    /// Column reordering (HeroGPUI extension; see [`Table::allows_column_reorder`]).
805    column_reorder: ColumnReorder,
806    /// Cell selection (HeroGPUI extension; see [`Table::cell_selectable`]).
807    cell_selection: CellSelection,
808}
809
810impl Table {
811    /// Creates a table with the given column labels.
812    pub fn new(columns: Vec<SharedString>) -> Self {
813        Self {
814            id: SharedString::from("table"),
815            indicator: None,
816            columns: columns.into_iter().map(TableColumn::new).collect(),
817            rows: Vec::new(),
818            tree_column: 0,
819            expanded_keys: Vec::new(),
820            on_expanded_change: None,
821            row_height: None,
822            estimated_row_height: None,
823            loader_height: None,
824            load_more_offset: 1.0,
825            max_h: None,
826            gap: None,
827            padding: None,
828            virtual_rows: None,
829            virtual_text_value: None,
830            virtual_tree_metadata: None,
831            variant: TableVariant::Primary,
832            selection_mode: SelectionMode::None,
833            selection_behavior: SelectionBehavior::Toggle,
834            selected_keys: Vec::new(),
835            default_selected_keys: Vec::new(),
836            disabled_keys: Vec::new(),
837            disallow_empty_selection: false,
838            is_selection_controlled: false,
839            sort_descriptor: None,
840            show_indicator: true,
841            is_pending: false,
842            empty_state: None,
843            footer: None,
844            on_row_click: None,
845            on_selection_change: None,
846            on_sort_change: None,
847            on_load_more: None,
848            on_resize_start: None,
849            on_resize: None,
850            on_resize_end: None,
851            row_hover_bg: None,
852            radius: None,
853            sx: None,
854            column_reorder: ColumnReorder::default(),
855            cell_selection: CellSelection::default(),
856        }
857    }
858
859    /// The id this table's own state is keyed by.
860    ///
861    /// Two tables on one page share their resized widths, their row cursor and
862    /// their focus without it, because `use_keyed_state` keys by the id it is
863    /// given and nothing else.
864    pub fn id(mut self, id: impl Into<SharedString>) -> Self {
865        self.id = id.into();
866        self
867    }
868
869    /// `indicator` — replaces the sort chevron.
870    ///
871    /// The closure receives `sortDirection`, the value v3 passes into the same
872    /// render prop, so a caller can draw an arrow, a caret or a label without
873    /// re-deriving which way the column is sorted.
874    pub fn indicator(mut self, render: impl Fn(SortDirection) -> AnyElement + 'static) -> Self {
875        self.indicator = Some(std::sync::Arc::new(render));
876        self
877    }
878
879    /// Replaces the columns with fully configured ones.
880    pub fn columns(mut self, columns: Vec<TableColumn>) -> Self {
881        self.columns = columns;
882        self
883    }
884
885    /// Adds one configured column.
886    pub fn column(mut self, column: impl Into<TableColumn>) -> Self {
887        self.columns.push(column.into());
888        self
889    }
890
891    /// Sets the table's visual variant.
892    pub fn variant(mut self, variant: TableVariant) -> Self {
893        self.variant = variant;
894        self
895    }
896
897    /// `TableLayout`'s `rowHeight` — and, with [`Table::virtual_rows`], what
898    /// virtualizes the body.
899    ///
900    /// v3 wraps the table in `<Virtualizer layout={TableLayout}
901    /// layoutOptions={{rowHeight: 40}}>`; the wrapper has no separate identity
902    /// here, so the option that defines the layout carries it. a uniform
903    /// [`VirtualList`](crate::VirtualList) builds only the rows the viewport shows, and it can do
904    /// that because every row is this tall.
905    pub fn row_height(mut self, height: impl Into<Pixels>) -> Self {
906        self.row_height = Some(height.into());
907        self
908    }
909
910    /// How tall the scrolling body is. v3's example sets it with `h-[400px]` on
911    /// the table itself.
912    pub fn max_h(mut self, height: impl Into<Pixels>) -> Self {
913        self.max_h = Some(height.into());
914        self
915    }
916
917    /// `TableLayout`'s `gap` -- space between rows, which v3 leaves at 0.
918    pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
919        self.gap = Some(gap.into());
920        self
921    }
922
923    /// `TableLayout`'s `padding` -- space around the rows, 0 in v3.
924    pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
925        self.padding = Some(padding.into());
926        self
927    }
928
929    /// The rows of a virtualized table, built on demand.
930    ///
931    /// v3 writes `<Table items={users}>{(user) => <Table.Row>…}</Table>` and the
932    /// Virtualizer calls that function for the rows in view. Cells here are
933    /// `AnyElement`, which is built once and consumed once, so a virtual table
934    /// cannot be handed its rows up front -- it has to be able to ask again
935    /// every time the viewport moves.
936    /// `key` projects the collection key without building the row. Keyboard
937    /// navigation and select-all need the full collection, but calling `row`
938    /// for every item would defeat virtualization.
939    /// `identity` stays stable for one collection and changes when the caller
940    /// replaces it, so a visible load-more sentinel can re-arm without building
941    /// every row just to compare their keys.
942    /// `TableLayout`'s `estimatedRowHeight` — virtualize rows that are not all
943    /// one height.
944    ///
945    /// `rowHeight` maps to a uniform `VirtualList`, which measures one row and multiplies;
946    /// this maps to gpui's `list`, which measures every row it builds. The
947    /// estimate is what it renders beyond the viewport while it learns the real
948    /// heights.
949    pub fn estimated_row_height(mut self, height: impl Into<Pixels>) -> Self {
950        self.estimated_row_height = Some(height.into());
951        self
952    }
953
954    /// `TableLayout`'s `loaderHeight` — the height of the load-more row.
955    pub fn loader_height(mut self, height: impl Into<Pixels>) -> Self {
956        self.loader_height = Some(height.into());
957        self
958    }
959
960    /// Virtual rows for a collection of `count` items.
961    ///
962    /// `identity` names the collection: it must stay stable for one collection
963    /// and change when the caller replaces it. The key and row closures are
964    /// treated as stable for that identity — their output is cached between
965    /// frames and replayed whenever count, identity, tree mode and the
966    /// expanded set are unchanged. If `key` (or [`Table::virtual_tree_metadata`]'s
967    /// `metadata`) would return different output for the same indices without
968    /// the collection being replaced, the caller must change `identity` so the
969    /// cached projection is rebuilt.
970    pub fn virtual_rows(
971        mut self,
972        count: usize,
973        identity: impl Into<SharedString>,
974        key: impl Fn(usize) -> SharedString + 'static,
975        row: impl Fn(usize) -> TableRow + 'static,
976    ) -> Self {
977        self.virtual_rows = Some((
978            count,
979            identity.into(),
980            std::sync::Arc::new(key),
981            std::sync::Arc::new(row),
982        ));
983        self
984    }
985
986    /// Projects each virtual row's `textValue` without building its cells.
987    /// Virtual rows do not participate in typeahead unless this is supplied.
988    pub fn virtual_text_value(
989        mut self,
990        text_value: impl Fn(usize) -> SharedString + 'static,
991    ) -> Self {
992        self.virtual_text_value = Some(std::sync::Arc::new(text_value));
993        self
994    }
995
996    /// Supplies preorder tree structure for [`Table::virtual_rows`].
997    ///
998    /// `count` remains the size of the underlying collection. The projection
999    /// identifies each item's parent, depth and expandability; controlled
1000    /// `expanded_keys` then decides which source indices are visible. The
1001    /// metadata closure is treated as stable for the [`Table::virtual_rows`]
1002    /// collection identity: if its output changes for the same indices, the
1003    /// caller must change that identity so the cached projection is rebuilt.
1004    pub fn virtual_tree_metadata(
1005        mut self,
1006        metadata: impl Fn(usize) -> VirtualTreeMetadata + 'static,
1007    ) -> Self {
1008        self.virtual_tree_metadata = Some(std::sync::Arc::new(metadata));
1009        self
1010    }
1011
1012    /// Adds one row of cells (`Table.Row` cells).
1013    /// `treeColumn` — the column whose cells carry the expand chevron.
1014    pub fn tree_column(mut self, index: usize) -> Self {
1015        self.tree_column = index;
1016        self
1017    }
1018
1019    /// `expandedKeys` — the parent rows showing their children.
1020    pub fn expanded_keys(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
1021        self.expanded_keys = keys.into_iter().collect();
1022        self
1023    }
1024
1025    /// Reports the expanded set after a chevron is pressed.
1026    pub fn on_expanded_change(
1027        mut self,
1028        f: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
1029    ) -> Self {
1030        self.on_expanded_change = Some(std::sync::Arc::new(f));
1031        self
1032    }
1033
1034    /// Adds a row, with any nested rows it carries.
1035    pub fn tree_row(mut self, row: TableRow) -> Self {
1036        self.rows.push(row);
1037        self
1038    }
1039
1040    /// Appends a row built from the given cells.
1041    pub fn row(mut self, cells: Vec<AnyElement>) -> Self {
1042        self.rows.push(TableRow::new(cells));
1043        self
1044    }
1045
1046    /// Adds one row under an explicit selection key.
1047    pub fn keyed_row(mut self, key: impl Into<SharedString>, cells: Vec<AnyElement>) -> Self {
1048        self.rows.push(TableRow::new(cells).key(key));
1049        self
1050    }
1051
1052    /// `selectionMode` — adds the selection column when not `None`.
1053    pub fn selection_mode(mut self, mode: SelectionMode) -> Self {
1054        self.selection_mode = mode;
1055        self
1056    }
1057
1058    /// `selectionBehavior` — how a multi-selection changes when a row is
1059    /// activated. `Toggle` preserves the current set; `Replace` makes a plain
1060    /// activation the sole selected row. Single selection keeps its own
1061    /// React Stately toggle/replace rule for both values.
1062    pub fn selection_behavior(mut self, behavior: SelectionBehavior) -> Self {
1063        self.selection_behavior = behavior;
1064        self
1065    }
1066
1067    /// The fill a hovered *unselected* interactive row takes, in place of the
1068    /// variant's hover wash (`bg-surface/40` primary, `bg-default/50`
1069    /// secondary). A selected row keeps its `bg-surface/10` fill; a disabled
1070    /// or inert row never hovers.
1071    pub fn row_hover_bg(mut self, color: impl Into<gpui::Hsla>) -> Self {
1072        self.row_hover_bg = Some(color.into());
1073        self
1074    }
1075
1076    /// The wrapper shell's corner radius, in place of the owning
1077    /// `container_radius` helper. `Primary` paints its own tray radius over
1078    /// this one, exactly as it covers the helper's value, so the override
1079    /// shows wherever the helper's value does. Not a v3 prop; the removed v2
1080    /// `radius` prop is prohibited and this is a per-component repository
1081    /// extension.
1082    pub fn radius(mut self, radius: impl Into<Pixels>) -> Self {
1083        self.radius = Some(radius.into());
1084        self
1085    }
1086
1087    /// `selectedKeys` — the controlled selection.
1088    pub fn selected_keys(
1089        mut self,
1090        keys: impl IntoIterator<Item = impl Into<SharedString>>,
1091    ) -> Self {
1092        self.selected_keys = keys.into_iter().map(Into::into).collect();
1093        self.is_selection_controlled = true;
1094        self
1095    }
1096
1097    /// `defaultSelectedKeys` — the initial uncontrolled selection. Later
1098    /// changes are owned by the table until `selected_keys` is supplied.
1099    pub fn default_selected_keys(
1100        mut self,
1101        keys: impl IntoIterator<Item = impl Into<SharedString>>,
1102    ) -> Self {
1103        self.default_selected_keys = keys.into_iter().map(Into::into).collect();
1104        self
1105    }
1106
1107    /// `disallowEmptySelection` — keep the last selected row when a toggle or
1108    /// Escape would otherwise clear the collection.
1109    pub fn disallow_empty_selection(mut self, disallow: bool) -> Self {
1110        self.disallow_empty_selection = disallow;
1111        self
1112    }
1113
1114    /// React Aria's inherited `disabledKeys` — rows excluded from selection,
1115    /// actions and the roving keyboard stops.
1116    pub fn disabled_keys(
1117        mut self,
1118        keys: impl IntoIterator<Item = impl Into<SharedString>>,
1119    ) -> Self {
1120        self.disabled_keys = keys.into_iter().map(Into::into).collect();
1121        self
1122    }
1123
1124    /// `onSelectionChange` — the whole selection after a row is toggled.
1125    pub fn on_selection_change(
1126        mut self,
1127        f: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
1128    ) -> Self {
1129        self.on_selection_change = Some(std::sync::Arc::new(f));
1130        self
1131    }
1132
1133    /// `sortDescriptor` — the current sort, shown in the header.
1134    pub fn sort_descriptor(mut self, descriptor: SortDescriptor) -> Self {
1135        self.sort_descriptor = Some(descriptor);
1136        self
1137    }
1138
1139    /// `onSortChange` — a sortable header was clicked.
1140    ///
1141    /// The table does not reorder its rows; a row is already-rendered cells, so
1142    /// the caller sorts its data and rebuilds.
1143    pub fn on_sort_change(
1144        mut self,
1145        f: impl Fn(&SortDescriptor, &mut Window, &mut App) + 'static,
1146    ) -> Self {
1147        self.on_sort_change = Some(std::sync::Arc::new(f));
1148        self
1149    }
1150
1151    /// `showIndicator` — whether a sorted column draws its direction chevron.
1152    pub fn show_indicator(mut self, v: bool) -> Self {
1153        self.show_indicator = v;
1154        self
1155    }
1156
1157    /// `isLoading` on `Table.LoadMore` — shows the loading sentinel row.
1158    pub fn is_pending(mut self, v: bool) -> Self {
1159        self.is_pending = v;
1160        self
1161    }
1162
1163    /// `scrollOffset` on `Table.LoadMore` — how many viewport heights before
1164    /// the sentinel enters view the load should start. React Aria defaults to
1165    /// one; v3's async example sets zero for an exact intersection.
1166    pub fn scroll_offset(mut self, offset: f32) -> Self {
1167        self.load_more_offset = offset;
1168        self
1169    }
1170
1171    /// `onLoadMore` — the sentinel row entered the scroll viewport.
1172    pub fn on_load_more(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
1173        self.on_load_more = Some(std::sync::Arc::new(f));
1174        self
1175    }
1176
1177    /// `onResizeStart` on `Table.ResizableContainer` — reports the current
1178    /// pixel widths when pointer or keyboard resizing begins.
1179    pub fn on_resize_start(
1180        mut self,
1181        f: impl Fn(&[(SharedString, Pixels)], &mut Window, &mut App) + 'static,
1182    ) -> Self {
1183        self.on_resize_start = Some(std::sync::Arc::new(f));
1184        self
1185    }
1186
1187    /// `onResize` on `Table.ResizableContainer` — reports each proposed width
1188    /// map. Feed the values back through [`TableColumn::width`] for controlled
1189    /// resizing.
1190    pub fn on_resize(
1191        mut self,
1192        f: impl Fn(&[(SharedString, Pixels)], &mut Window, &mut App) + 'static,
1193    ) -> Self {
1194        self.on_resize = Some(std::sync::Arc::new(f));
1195        self
1196    }
1197
1198    /// `onResizeEnd` on `Table.ResizableContainer` — reports the final pixel
1199    /// widths after pointer or keyboard resizing ends.
1200    pub fn on_resize_end(
1201        mut self,
1202        f: impl Fn(&[(SharedString, Pixels)], &mut Window, &mut App) + 'static,
1203    ) -> Self {
1204        self.on_resize_end = Some(std::sync::Arc::new(f));
1205        self
1206    }
1207
1208    /// `Table.Footer` — a row under the body, which is where v3 puts a table's
1209    /// pagination.
1210    pub fn footer(mut self, content: impl IntoElement) -> Self {
1211        self.footer = Some(content.into_any_element());
1212        self
1213    }
1214
1215    /// `renderEmptyState` — shown in place of rows when there are none.
1216    pub fn empty_state(mut self, content: impl IntoElement) -> Self {
1217        self.empty_state = Some(content.into_any_element());
1218        self
1219    }
1220
1221    /// Called with the row index and click event when a row is clicked.
1222    pub fn on_row_click(
1223        mut self,
1224        f: impl Fn(&usize, &ClickEvent, &mut Window, &mut App) + 'static,
1225    ) -> Self {
1226        self.on_row_click = Some(std::sync::Arc::new(f));
1227        self
1228    }
1229
1230    /// The one slot for caller-owned low-level styling: GPUI's styling methods
1231    /// (`bg`, `text_color`, `w`, `h`, `p`, `rounded`, `border_color`, …)
1232    /// applied to the table's root element after every value the variant and
1233    /// the active theme chose, so they win.
1234    pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
1235        crate::util::refine_sx(&mut self.sx, style);
1236        self
1237    }
1238
1239    /// Every row's selection key, in order.
1240    fn row_keys(&self) -> Vec<SharedString> {
1241        fn collect(rows: &[TableRow], path: &str, out: &mut Vec<SharedString>) {
1242            for (index, row) in rows.iter().enumerate() {
1243                let key = match &row.key {
1244                    Some(key) => key.clone(),
1245                    None if path.is_empty() => SharedString::from(index.to_string()),
1246                    None => SharedString::from(format!("{path}-{index}")),
1247                };
1248                out.push(key.clone());
1249                collect(&row.children, key.as_ref(), out);
1250            }
1251        }
1252
1253        let mut keys = Vec::new();
1254        collect(&self.rows, "", &mut keys);
1255        keys
1256    }
1257}
1258
1259/// A virtual table's projected keys and visible rows, cached between frames.
1260///
1261/// The projection depends only on what [`Table::virtual_rows`] already names —
1262/// the collection's identity and count — plus the expanded set and whether
1263/// tree metadata is projected. Any frame where none of those changed reuses
1264/// the cached projection instead of calling the key and tree closures again;
1265/// the closures are free to return different values only when the caller
1266/// replaces the collection, which is what `identity` is for.
1267#[derive(Default)]
1268struct VirtualProjection {
1269    count: usize,
1270    identity: SharedString,
1271    expanded: std::sync::Arc<std::collections::HashSet<SharedString>>,
1272    tree: bool,
1273    full_keys: std::sync::Arc<Vec<SharedString>>,
1274    /// Behind an `Arc` because the list builders hold it across the frame.
1275    visible: std::sync::Arc<Vec<(usize, SharedString, VirtualTreeMetadata)>>,
1276    selectable_collection_keys: Option<std::sync::Arc<Vec<SharedString>>>,
1277    selectable_disabled_keys: Option<std::sync::Arc<Vec<SharedString>>>,
1278}
1279
1280fn filtered_selectable_keys(
1281    full_keys: &[SharedString],
1282    disabled_keys: &[SharedString],
1283) -> std::sync::Arc<Vec<SharedString>> {
1284    std::sync::Arc::new(
1285        full_keys
1286            .iter()
1287            .filter(|key| !disabled_keys.contains(key))
1288            .cloned()
1289            .collect(),
1290    )
1291}
1292
1293fn resolved_column_widths(
1294    columns: &[TableColumn],
1295    resized: &[Option<Pixels>],
1296    measured: &[Option<Pixels>],
1297    proposal: Option<(usize, Pixels)>,
1298) -> Vec<(SharedString, Pixels)> {
1299    columns
1300        .iter()
1301        .enumerate()
1302        .filter_map(|(index, column)| {
1303            let width = proposal
1304                .filter(|(proposed_index, _)| *proposed_index == index)
1305                .map(|(_, width)| width)
1306                .or(column.width)
1307                .or_else(|| resized.get(index).copied().flatten())
1308                .or(column.default_width)
1309                .or_else(|| measured.get(index).copied().flatten())?;
1310            let width = if column.allows_resizing {
1311                let min = column.min_width.map_or(DEFAULT_COLUMN_MIN_WIDTH, f32::from);
1312                let max = column.max_width.map_or(f32::MAX, f32::from);
1313                px(f32::from(width).floor().min(max).max(min))
1314            } else {
1315                width
1316            };
1317            Some((column.label.clone(), width))
1318        })
1319        .collect()
1320}
1321
1322fn final_column_widths(
1323    columns: &[TableColumn],
1324    resized: &[Option<Pixels>],
1325    measured: &[Option<Pixels>],
1326    column: usize,
1327) -> Vec<(SharedString, Pixels)> {
1328    let proposal = resized
1329        .get(column)
1330        .copied()
1331        .flatten()
1332        .map(|width| (column, width));
1333    resolved_column_widths(columns, resized, measured, proposal)
1334}
1335
1336fn clear_controlled_resize_proposal(
1337    columns: &[TableColumn],
1338    resized: &gpui::Entity<Vec<Option<Pixels>>>,
1339    column: usize,
1340    cx: &mut App,
1341) {
1342    if columns
1343        .get(column)
1344        .is_some_and(|column| column.width.is_some())
1345    {
1346        resized.update(cx, |values, cx| {
1347            if values
1348                .get_mut(column)
1349                .is_some_and(|width| width.take().is_some())
1350            {
1351                cx.notify();
1352            }
1353        });
1354    }
1355}
1356
1357/// The keyed state one render reads: resize and measurement stores, the
1358/// load-more latch, the body's focus and cursor, the selection anchor and
1359/// the typeahead buffer. `use_keyed_state` takes `cx` mutably, so all of it
1360/// is created before the theme tokens are borrowed.
1361struct TableState {
1362    /// Column widths a resize handle has moved, and the drag in progress.
1363    resized: gpui::Entity<Vec<Option<Pixels>>>,
1364    resized_now: Vec<Option<Pixels>>,
1365    measured_widths: gpui::Entity<Vec<Option<Pixels>>>,
1366    measured_widths_now: Vec<Option<Pixels>>,
1367    dragging: gpui::Entity<Option<(usize, f32, f32)>>,
1368    drag_now: Option<(usize, f32, f32)>,
1369    keyboard_resizing: gpui::Entity<Option<usize>>,
1370    keyboard_resize_now: Option<usize>,
1371    load_more_state: gpui::Entity<(bool, Option<LoadMoreCollection>)>,
1372    /// The body is one tab stop with a cursor inside it, the way a list is.
1373    table_focus: gpui::FocusHandle,
1374    row_cursor: gpui::Entity<Option<SharedString>>,
1375    selection_range: gpui::Entity<TableSelectionRange>,
1376    typeahead: gpui::Entity<TableTypeahead>,
1377    selection_own: Option<gpui::Entity<Vec<SharedString>>>,
1378}
1379
1380/// The virtual body for one render: the cached projection and the scroll
1381/// state of whichever virtual path is active.
1382struct VirtualBody {
1383    projection: Option<std::sync::Arc<VirtualProjection>>,
1384    visible_count: usize,
1385    visible_keys: Option<Vec<SharedString>>,
1386    row_heights: Option<gpui::Entity<(Vec<SharedString>, Vec<Option<Pixels>>)>>,
1387    scroll_now: crate::VirtualListHandle,
1388    list_state: Option<gpui::ListState>,
1389}
1390
1391/// The header's focus handles and whether each one wears the ring.
1392struct HeaderFocus {
1393    header_focus: Vec<gpui::FocusHandle>,
1394    header_focused: Vec<bool>,
1395    resize_focus: Vec<Option<gpui::FocusHandle>>,
1396    resize_focused: Vec<bool>,
1397}
1398
1399/// The one column-track computation the header and every row read.
1400struct ColumnTracks {
1401    resize_limits: Vec<(f32, f32)>,
1402    effective_widths: Vec<Option<Pixels>>,
1403    layout_width_bounds: Vec<(Option<Pixels>, Option<Pixels>)>,
1404    resize_columns: std::sync::Arc<Vec<TableColumn>>,
1405    resize_measurements: std::sync::Arc<Vec<Option<Pixels>>>,
1406}
1407
1408/// The body rows after flattening, and what the keyboard reads off them.
1409struct RowCollection {
1410    flat: Vec<FlatRow>,
1411    tree_rows: Vec<(bool, Option<SharedString>)>,
1412    visible_collection_keys: Vec<SharedString>,
1413    typeahead_labels: Vec<String>,
1414    virtual_typeahead_indices: Option<Vec<usize>>,
1415    /// The row the keyboard cursor rings, when the body owns focus.
1416    cursor_at: Option<usize>,
1417    /// Whether any row in this table is expandable at all: a flat table
1418    /// keeps its cells flush, rather than reserving a chevron's width.
1419    tree_column_has_children: bool,
1420}
1421
1422/// The per-render row facts [`Table::row_ctx`] folds into a [`RowCtx`].
1423struct RowCtxRows {
1424    row_keys: Vec<SharedString>,
1425    cursor: Option<usize>,
1426    tree_column_has_children: bool,
1427    expanded: std::rc::Rc<Vec<SharedString>>,
1428    is_tree: bool,
1429    virtualized: bool,
1430    round_top: Option<Pixels>,
1431    round_bottom: Option<Pixels>,
1432    cell: Option<CellCtx>,
1433    frozen: Option<FrozenColumns>,
1434}
1435
1436impl RenderOnce for Table {
1437    fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1438        // The caller's id, as the root every part of this table hangs off.
1439        let base_id: gpui::ElementId = self.id.clone().into();
1440        // HeroGPUI extension: put the columns and cells in display order
1441        // first, so everything below works in display positions.
1442        let column_order = self.apply_column_order(&base_id, window, cx);
1443        let frozen = self.frozen_columns(&base_id, window, cx);
1444        let (selected_keys, selection_own) = crate::util::controlled(
1445            window,
1446            cx,
1447            element_id::scoped(&base_id, "selected"),
1448            self.is_selection_controlled
1449                .then(|| self.selected_keys.clone()),
1450            self.default_selected_keys.clone(),
1451        );
1452        self.selected_keys = selected_keys;
1453        let state = self.table_state(&base_id, selection_own, window, cx);
1454        let mut virtual_body = self.virtual_body(&base_id, window, cx);
1455        let load_more_virtual_scroll = (self.row_height.is_some() && self.virtual_rows.is_some())
1456            .then(|| virtual_body.scroll_now.clone());
1457        let load_more_variable_scroll = virtual_body
1458            .list_state
1459            .clone()
1460            .zip(self.estimated_row_height)
1461            .map(|(list, estimate)| (list, virtual_body.visible_count, estimate));
1462        let focus = self.header_focus_state(&base_id, &state, window, cx);
1463
1464        let resizable = self.columns.iter().any(|c| c.allows_resizing);
1465        let tracks = self.column_tracks(&state, frozen.as_ref());
1466        let colors = cx.colors().clone();
1467        // Copies of the tokens the tail needs: the row builder borrows `cx`
1468        // mutably, which ends the borrow `cx.colors()` holds.
1469        let muted = colors.muted;
1470        let secondary = self.variant == TableVariant::Secondary;
1471        let selectable = self.selection_mode != SelectionMode::None;
1472        let (selectable_collection_keys, load_more_collection) =
1473            self.selection_collections(&virtual_body);
1474
1475        let mut wrapper = self.wrapper_tray(&state.table_focus, &colors, secondary, cx);
1476
1477        // `useTable`'s `state.treeColumn != null` means the collection was
1478        // built as a tree, not merely that a column index was named:
1479        // `Table::tree_column` only picks which column carries the chevron,
1480        // and it defaults to `0` on every flat table. A tree here is a
1481        // collection with nested rows, virtual or not.
1482        let is_tree = self.virtual_tree_metadata.is_some()
1483            || self.rows.iter().any(|row| !row.children.is_empty());
1484        let mut table = self
1485            .grid_element(&base_id, selectable, is_tree, &virtual_body)
1486            .when(frozen.is_some(), |grid| grid.w_full());
1487
1488        // ---- header ------------------------------------------------------
1489        let header_parts = HeaderParts {
1490            base_id: &base_id,
1491            colors: &colors,
1492            secondary,
1493            selectable,
1494            effective_widths: &tracks.effective_widths,
1495            layout_width_bounds: &tracks.layout_width_bounds,
1496            measured_widths: &state.measured_widths,
1497            measured_widths_now: &state.measured_widths_now,
1498            header_focus: &focus.header_focus,
1499            header_focused: &focus.header_focused,
1500            resize_focus: &focus.resize_focus,
1501            resize_focused: &focus.resize_focused,
1502            resized: &state.resized,
1503            dragging: &state.dragging,
1504            drag_now: state.drag_now,
1505            keyboard_resizing: &state.keyboard_resizing,
1506            keyboard_resize_now: state.keyboard_resize_now,
1507            resize_limits: &tracks.resize_limits,
1508            resize_columns: &tracks.resize_columns,
1509            resize_measurements: &tracks.resize_measurements,
1510            frozen: frozen.as_ref(),
1511        };
1512        let header = self.header_row(
1513            &header_parts,
1514            &column_order,
1515            &selectable_collection_keys,
1516            &state,
1517            window,
1518            cx,
1519        );
1520        table = table.child(header);
1521
1522        // `.table__body` rounds to `min(32px, --radius-2xl)` and its cells are
1523        // `bg-surface`: the white block inside the tray.
1524        // `useTableRowGroup.mjs` (through `.../grid/useGridRowGroup.mjs`) is a
1525        // single `role: 'rowgroup'` — the `<tbody>` around the data rows.
1526        let mut body = gpui::div()
1527            .id(element_id::scoped(&base_id, "body"))
1528            .a11y(a11y::Role::RowGroup)
1529            .flex()
1530            .flex_col()
1531            .w_full();
1532        if !secondary {
1533            body = body
1534                .bg(colors.surface.background)
1535                .rounded(cx.layout().radius_2xl().min(px(32.)))
1536                .overflow_hidden();
1537        }
1538
1539        if resizable {
1540            table = self.column_resize_drag(
1541                table,
1542                state.dragging.clone(),
1543                state.resized.clone(),
1544                tracks.resize_columns.clone(),
1545                tracks.resize_measurements.clone(),
1546            );
1547        }
1548        // ---- rows --------------------------------------------------------
1549        let rows = self.row_collection(&mut virtual_body, &state, window, cx);
1550        let expanded_keys = std::rc::Rc::new(std::mem::take(&mut self.expanded_keys));
1551
1552        // Everything a row needs, held once. The virtual path builds its rows
1553        // inside a `'static` closure, so it cannot borrow `self` -- and having
1554        // one row builder for both paths is what keeps a virtual table drawing
1555        // the same row as a short one.
1556        let table_id = self.id.clone();
1557        let cell_selection = self.cell_selection_state(
1558            &base_id,
1559            &column_order,
1560            &state.row_cursor,
1561            &state.table_focus,
1562            window,
1563            cx,
1564        );
1565        wrapper = self.cell_selection_keys(
1566            wrapper,
1567            &cell_selection,
1568            &rows.visible_collection_keys,
1569            frozen.clone(),
1570        );
1571        let (round_top, round_bottom) = self.row_edge_rounding(
1572            secondary,
1573            &virtual_body.scroll_now,
1574            virtual_body.list_state.as_ref(),
1575            virtual_body.visible_count,
1576            cx,
1577        );
1578        let RowCollection {
1579            flat,
1580            tree_rows,
1581            visible_collection_keys,
1582            typeahead_labels,
1583            virtual_typeahead_indices,
1584            cursor_at,
1585            tree_column_has_children,
1586        } = rows;
1587        let ctx = std::rc::Rc::new(self.row_ctx(
1588            &base_id,
1589            &state,
1590            &tracks,
1591            RowCtxRows {
1592                row_keys: visible_collection_keys,
1593                cursor: cursor_at,
1594                tree_column_has_children,
1595                expanded: expanded_keys.clone(),
1596                is_tree,
1597                virtualized: virtual_body.projection.is_some(),
1598                round_top,
1599                round_bottom,
1600                cell: cell_selection,
1601                frozen,
1602            },
1603        ));
1604
1605        wrapper = self.row_keyboard(
1606            wrapper,
1607            RowKeyboard {
1608                ctx: &ctx,
1609                expanded_keys,
1610                header_focus: focus.header_focus,
1611                row_cursor: state.row_cursor,
1612                selectable_collection_keys,
1613                selection_own: state.selection_own,
1614                selection_range: state.selection_range,
1615                table_focus: state.table_focus,
1616                tree_rows,
1617                typeahead_labels,
1618                typeahead: state.typeahead,
1619                virtual_list_state: &virtual_body.list_state,
1620                virtual_row_heights: &virtual_body.row_heights,
1621                virtual_scroll_now: &virtual_body.scroll_now,
1622                virtual_text_value: self.virtual_text_value.clone(),
1623                virtual_typeahead_indices,
1624            },
1625        );
1626
1627        let row_count = if self.virtual_rows.is_some()
1628            && (self.row_height.is_some() || self.estimated_row_height.is_some())
1629        {
1630            virtual_body.visible_count
1631        } else {
1632            flat.len()
1633        };
1634        let virtual_projection = virtual_body
1635            .projection
1636            .map(|projection| std::sync::Arc::clone(&projection.visible))
1637            .unwrap_or_default();
1638
1639        body = self.body_rows(
1640            body,
1641            BodyRows {
1642                base_id: &base_id,
1643                table_id: &table_id,
1644                ctx: &ctx,
1645                flat,
1646                row_count,
1647                virtual_projection,
1648                virtual_list_state: virtual_body.list_state,
1649                virtual_row_heights: virtual_body.row_heights,
1650                virtual_scroll_now: &virtual_body.scroll_now,
1651            },
1652            window,
1653            cx,
1654        );
1655
1656        // ---- empty state -------------------------------------------------
1657        if row_count == 0 {
1658            if let Some(content) = self.empty_state.take() {
1659                body = body.child(Self::empty_state_row(content, muted));
1660            }
1661        }
1662
1663        table = table.child(body);
1664
1665        // ---- load-more sentinel ------------------------------------------
1666        if self.is_pending || self.on_load_more.is_some() {
1667            table = self.load_more(
1668                table,
1669                LoadMore {
1670                    base_id: &base_id,
1671                    state: state.load_more_state,
1672                    collection: load_more_collection,
1673                    virtual_scroll: load_more_virtual_scroll,
1674                    variable_scroll: load_more_variable_scroll,
1675                    muted,
1676                    loading_text: crate::i18n::ui_string(crate::i18n::UiString::LoadingMore, cx),
1677                },
1678            );
1679        }
1680
1681        if let Some(footer) = self.footer.take() {
1682            table = table.child(Self::footer_row(footer));
1683        }
1684
1685        let el = wrapper.child(Self::scroll_container(&base_id, &table_id, table));
1686        crate::util::apply_sx(el, &self.sx)
1687    }
1688}
1689
1690// ---- Table::render setup and chrome ----
1691impl Table {
1692    /// Creates (or reads back) the keyed state one render needs.
1693    fn table_state(
1694        &self,
1695        base_id: &gpui::ElementId,
1696        selection_own: Option<gpui::Entity<Vec<SharedString>>>,
1697        window: &mut Window,
1698        cx: &mut App,
1699    ) -> TableState {
1700        // Column widths a resize handle has moved, and the drag in progress.
1701        // `use_keyed_state` takes `cx` mutably, so both precede the tokens.
1702        let resized = window.use_keyed_state(element_id::scoped(base_id, "resized"), cx, |_, _| {
1703            Vec::<Option<Pixels>>::new()
1704        });
1705        let resized_now = resized.read(cx).clone();
1706        let measured_widths = window.use_keyed_state(
1707            element_id::scoped(base_id, "measured-widths"),
1708            cx,
1709            |_, _| Vec::<Option<Pixels>>::new(),
1710        );
1711        let measured_widths_now = measured_widths.read(cx).clone();
1712        let dragging =
1713            window.use_keyed_state(element_id::scoped(base_id, "resizing"), cx, |_, _| {
1714                None::<(usize, f32, f32)>
1715            });
1716        let drag_now = *dragging.read(cx);
1717        let keyboard_resizing = window.use_keyed_state(
1718            element_id::scoped(base_id, "keyboard-resizing"),
1719            cx,
1720            |_, _| None::<usize>,
1721        );
1722        let keyboard_resize_now = *keyboard_resizing.read(cx);
1723        let load_more_state = window.use_keyed_state(
1724            element_id::scoped(base_id, "load-more-state"),
1725            cx,
1726            |_, _| (false, None::<LoadMoreCollection>),
1727        );
1728        // The body is one tab stop with a cursor inside it, the way a list is.
1729        let table_focus =
1730            crate::util::tab_stop_handle(element_id::scoped(base_id, "focus"), window, cx);
1731        let row_cursor =
1732            window.use_keyed_state(element_id::scoped(base_id, "cursor"), cx, |_, _| {
1733                None::<SharedString>
1734            });
1735        let selection_range = window.use_keyed_state(
1736            element_id::scoped(base_id, "selection-range"),
1737            cx,
1738            |_, _| TableSelectionRange::default(),
1739        );
1740        let typeahead =
1741            window.use_keyed_state(element_id::scoped(base_id, "typeahead"), cx, |_, _| {
1742                TableTypeahead::default()
1743            });
1744        TableState {
1745            resized,
1746            resized_now,
1747            measured_widths,
1748            measured_widths_now,
1749            dragging,
1750            drag_now,
1751            keyboard_resizing,
1752            keyboard_resize_now,
1753            load_more_state,
1754            table_focus,
1755            row_cursor,
1756            selection_range,
1757            typeahead,
1758            selection_own,
1759        }
1760    }
1761
1762    /// The frozen leading columns, once the columns are in display order:
1763    /// `None` when none (or every one) is frozen.
1764    fn frozen_columns(
1765        &self,
1766        base_id: &gpui::ElementId,
1767        window: &mut Window,
1768        cx: &mut App,
1769    ) -> Option<FrozenColumns> {
1770        let count = self.columns.iter().take_while(|c| c.frozen).count();
1771        if count == 0 || count >= self.columns.len() {
1772            return None;
1773        }
1774        let scroll = window
1775            .use_keyed_state(element_id::scoped(base_id, "frozen-scroll"), cx, |_, _| {
1776                gpui::ScrollHandle::new()
1777            })
1778            .read(cx)
1779            .clone();
1780        Some(FrozenColumns {
1781            count,
1782            scroll,
1783            view: window.current_view(),
1784        })
1785    }
1786
1787    /// The virtual projection and the scroll state of both virtual paths.
1788    fn virtual_body(
1789        &self,
1790        base_id: &gpui::ElementId,
1791        window: &mut Window,
1792        cx: &mut App,
1793    ) -> VirtualBody {
1794        let projection = self.virtual_projection(base_id, window, cx);
1795        let visible_count = projection
1796            .as_ref()
1797            .map_or(0, |projection| projection.visible.len());
1798        let visible_keys = projection.as_ref().map(|projection| {
1799            projection
1800                .visible
1801                .iter()
1802                .map(|(_, key, _)| key.clone())
1803                .collect::<Vec<_>>()
1804        });
1805        let row_heights = self.virtual_row_heights(base_id, visible_keys.as_ref(), window, cx);
1806        let virtual_scroll =
1807            window.use_keyed_state(element_id::scoped(base_id, "virtual-scroll"), cx, |_, _| {
1808                crate::VirtualListHandle::uniform(0)
1809            });
1810        let scroll_now = virtual_scroll.read(cx).clone();
1811        let list_state = self.virtual_list_state(base_id, visible_count, window, cx);
1812        VirtualBody {
1813            projection,
1814            visible_count,
1815            visible_keys,
1816            row_heights,
1817            scroll_now,
1818            list_state,
1819        }
1820    }
1821
1822    /// The header's focus handles, and whether each wears the ring. Drops a
1823    /// keyboard resize whose column no longer has a handle.
1824    fn header_focus_state(
1825        &self,
1826        base_id: &gpui::ElementId,
1827        state: &TableState,
1828        window: &mut Window,
1829        cx: &mut App,
1830    ) -> HeaderFocus {
1831        // A sortable header had a click listener and no focus, so sorting was
1832        // mouse-only. v3's grid roves one tab stop across its cells; this port
1833        // gives each sortable header its own stop, which is the part that
1834        // matters. Created before the theme: `use_keyed_state` takes `cx`
1835        // mutably and `cx.colors()` holds a borrow.
1836        let sortable = self.on_sort_change.is_some();
1837        // Every column header is focusable, not just the sortable ones: v3's
1838        // PageUp leaves the body for the first column header whatever it is.
1839        // Only a sortable header is a *tab stop*, though -- it has to be, so
1840        // Enter and Space can sort it -- which keeps every table's Tab order
1841        // exactly as it was while giving the keyboard somewhere to land.
1842        let header_focus: Vec<gpui::FocusHandle> =
1843            self.header_focus_handles(base_id, sortable, window, cx);
1844        let ring_visible = crate::util::focus_visible(cx);
1845        let header_focused: Vec<bool> = header_focus
1846            .iter()
1847            .map(|h| h.is_focused(window) && ring_visible)
1848            .collect();
1849        let resize_focus: Vec<Option<gpui::FocusHandle>> =
1850            self.resize_focus_handles(base_id, window, cx);
1851        let resize_focused: Vec<bool> = resize_focus
1852            .iter()
1853            .map(|h| h.as_ref().is_some_and(|h| h.is_focused(window)) && ring_visible)
1854            .collect();
1855        if state.keyboard_resize_now.is_some_and(|column_index| {
1856            resize_focus
1857                .get(column_index)
1858                .and_then(Option::as_ref)
1859                .is_none()
1860        }) {
1861            state.keyboard_resizing.update(cx, |active, cx| {
1862                *active = None;
1863                cx.notify();
1864            });
1865        }
1866        HeaderFocus {
1867            header_focus,
1868            header_focused,
1869            resize_focus,
1870            resize_focused,
1871        }
1872    }
1873
1874    /// The column tracks: resize bounds, the width each column renders at,
1875    /// and the shared `(min, max)` layout floor.
1876    fn column_tracks(&self, state: &TableState, frozen: Option<&FrozenColumns>) -> ColumnTracks {
1877        let resize_limits = column_resize_limits(&self.columns);
1878        let mut effective_widths =
1879            effective_column_widths(&self.columns, &state.resized_now, &resize_limits);
1880        // The one column-track computation. Upstream draws a `border-separate`
1881        // table, so the header's `<th>` cells resolve tracks every row shares;
1882        // this port stands in for that with one `(width, min, max)` triple per
1883        // column — the explicit width when one wins, otherwise `flex-basis:0;
1884        // flex-grow:1` floored by the widest cell measured across the header
1885        // and every body row. The header cells below and every `RowCtx` row
1886        // must read this same triple, and the sortable and resizable header
1887        // wrappers must carry it too: a wrapper that only grows equally would
1888        // resolve its own track and stagger the header off the body. A table
1889        // narrower than the tracks then overflows `scroll-x` as one grid
1890        // instead of squeezing each row separately.
1891        let layout_width_bounds = layout_width_bounds(
1892            &self.columns,
1893            &state.measured_widths_now,
1894            &effective_widths,
1895            &resize_limits,
1896        );
1897        // A frozen column does not share out the free space: its part is
1898        // sized to its cells, so the header's and every row's frozen parts
1899        // only agree when each frozen column has one width. Once the column
1900        // is measured that is its shared floor; a resizable column's probe
1901        // measures it first.
1902        if let Some(frozen) = frozen {
1903            for (c, column) in self.columns.iter().enumerate().take(frozen.count) {
1904                let measured = state.measured_widths_now.get(c).copied().flatten();
1905                if effective_widths[c].is_none() && (!column.allows_resizing || measured.is_some())
1906                {
1907                    effective_widths[c] = layout_width_bounds[c].0;
1908                }
1909            }
1910        }
1911        ColumnTracks {
1912            resize_limits,
1913            effective_widths,
1914            layout_width_bounds,
1915            resize_columns: std::sync::Arc::new(self.columns.clone()),
1916            resize_measurements: std::sync::Arc::new(state.measured_widths_now.clone()),
1917        }
1918    }
1919
1920    /// The enabled-key set multiple selection consumes, and the identity the
1921    /// load-more latch compares.
1922    fn selection_collections(
1923        &self,
1924        virtual_body: &VirtualBody,
1925    ) -> (
1926        Option<std::sync::Arc<Vec<SharedString>>>,
1927        LoadMoreCollection,
1928    ) {
1929        let needs_selectable_keys = self.selection_mode == SelectionMode::Multiple;
1930        // Only multiple selection consumes the full enabled-key set. The
1931        // virtual projection shares it between frames; None and Single do not
1932        // materialize a collection that no consumer reads.
1933        let non_virtual_keys = virtual_body.projection.is_none().then(|| self.row_keys());
1934        let selectable_collection_keys: Option<std::sync::Arc<Vec<SharedString>>> =
1935            needs_selectable_keys.then(|| match virtual_body.projection.as_ref() {
1936                Some(projection) => projection
1937                    .selectable_collection_keys
1938                    .as_ref()
1939                    .expect("multiple virtual Tables cache selectable keys")
1940                    .clone(),
1941                None => filtered_selectable_keys(
1942                    non_virtual_keys
1943                        .as_ref()
1944                        .expect("multiple non-virtual Tables have row keys")
1945                        .as_slice(),
1946                    &self.disabled_keys,
1947                ),
1948            });
1949        let load_more_collection = match &self.virtual_rows {
1950            Some((count, identity, _, _)) => LoadMoreCollection::Virtual {
1951                count: *count,
1952                identity: identity.clone(),
1953            },
1954            None => LoadMoreCollection::Rows(non_virtual_keys.unwrap_or_default()),
1955        };
1956        (selectable_collection_keys, load_more_collection)
1957    }
1958
1959    /// The table's root: the body's focus stop, and on the primary variant
1960    /// the `.table-root--primary` tray.
1961    fn wrapper_tray(
1962        &self,
1963        table_focus: &gpui::FocusHandle,
1964        colors: &herogpui_theme::ThemeColors,
1965        secondary: bool,
1966        cx: &App,
1967    ) -> gpui::Div {
1968        let mut wrapper = gpui::div()
1969            .w_full()
1970            .flex()
1971            .flex_col()
1972            .track_focus(table_focus)
1973            .overflow_hidden()
1974            .rounded(
1975                self.radius
1976                    .unwrap_or_else(|| crate::util::container_radius(cx)),
1977            )
1978            .text_color(colors.foreground);
1979        // A column, so the scroll container below is a vertical flex item: it
1980        // shrinks with a bounded parent (its `overflow_hidden` zeroes the
1981        // minimum), handing the virtual body its real viewport. A block would
1982        // size it to content and only clip the overflow.
1983
1984        // `.table-root--primary` is a `bg-surface-secondary px-1 pb-1` tray with
1985        // `border-radius: min(32px, --radius * 2.5)`, and the rows sit in a
1986        // `bg-surface` block inside it; `secondary` is flat. This used to draw
1987        // one white card with a border, which is the block without its tray.
1988        if !secondary {
1989            let radius = cx.layout().radius_lg() * 2.5;
1990            wrapper = wrapper
1991                .bg(colors.surface_secondary)
1992                .rounded(radius.min(px(32.)))
1993                .px(px(4.))
1994                .pb(px(4.));
1995        }
1996        wrapper
1997    }
1998
1999    /// `.table__content`: the grid itself, the column the header, body,
2000    /// load-more row and footer stack in.
2001    fn grid_element(
2002        &self,
2003        base_id: &gpui::ElementId,
2004        selectable: bool,
2005        is_tree: bool,
2006        virtual_body: &VirtualBody,
2007    ) -> gpui::Stateful<gpui::Div> {
2008        // The wrapper is a column, so this content column may shrink
2009        // vertically with a bounded parent -- that is what lets a virtual
2010        // body take the laid-out viewport rather than the `max_h` cap.
2011        // Horizontally it must never shrink: the scroll container at the
2012        // bottom of the render is a column with `items_start`, whose cross
2013        // axis leaves this child at its max-content width, so a table wider
2014        // than its box still slides on it. (A row-flex scroller would need
2015        // `flex_shrink_0` here for the same guarantee, which would also
2016        // forbid the vertical shrink.)
2017        //
2018        // The content column, whose width is what the scroller at the bottom
2019        // of the render measures against. A `w_full` child commits to the
2020        // scroller's width, which is exactly the scroller's own -- the scroll
2021        // maxima are then zero and a wide table clips at the tray edge instead
2022        // of sliding. `min_w_full` keeps the column at the viewport when no
2023        // column pins a width. `min_h_0` lets the column shrink vertically
2024        // with a bounded parent: without it the content-based minimum keeps
2025        // the max-content height and a virtual body never sees its real
2026        // viewport.
2027        // `aria-colcount` counts the collection's columns, and the selection
2028        // column is one of them upstream. `aria-rowcount` is only knowable
2029        // when the port virtualizes, which is exactly when upstream sets it.
2030        let grid_column_count = self.columns.len() + usize::from(selectable);
2031        let virtual_row_total = virtual_body
2032            .projection
2033            .as_ref()
2034            .map(|_| virtual_body.visible_count);
2035        let mut table = gpui::div()
2036            .flex()
2037            .flex_col()
2038            .min_w_full()
2039            .min_h_0()
2040            .text_size(px(14.))
2041            .line_height(px(20.))
2042            .when_some(self.gap, |el, g| el.gap(g))
2043            .when_some(self.padding, |el, p| el.p(p))
2044            // The accessibility contract goes on the *end* of this chain:
2045            // `.shots/design_audit.py` reads `.table__content`'s text size
2046            // through a 240-character window opening at `let mut table =
2047            // gpui::div(`, and it reports the rule unreadable when anything
2048            // is spliced in ahead of `.text_size(..)`.
2049            //
2050            // `react-aria/dist/private/table/useTable.mjs` starts from
2051            // `.../grid/useGrid.mjs`'s `role: 'grid'` and overrides it to
2052            // `'treegrid'` only `if (state.treeColumn != null)`. RAC's
2053            // `Table.mjs` adds no role of its own.
2054            //
2055            // `aria-rowcount` / `aria-colcount` are set only
2056            // `if (isVirtualized)` — the DOM then holds a window onto a
2057            // longer collection and the counts cannot be read off the tree —
2058            // and `useTable` then makes the row count
2059            // `state.collection.size + state.collection.headerRows.length`,
2060            // which is why the one header row is added to it. The
2061            // `aria-multiselectable` that `useGrid` sets beside them has no
2062            // gpui builder (see `crate::a11y`).
2063            .id(element_id::scoped(base_id, "grid"))
2064            .a11y(if is_tree {
2065                a11y::Role::TreeGrid
2066            } else {
2067                a11y::Role::Grid
2068            });
2069        if let Some(rows) = virtual_row_total {
2070            table = table.a11y_grid_size(rows + 1, grid_column_count);
2071        }
2072        table
2073    }
2074
2075    /// `.table__header`, whose cells are `.table__column`s and whose
2076    /// sortable ones wrap in `.table__sortable-column-header`.
2077    fn header_row(
2078        &self,
2079        hx: &HeaderParts<'_>,
2080        column_order: &std::rc::Rc<Vec<usize>>,
2081        selectable_collection_keys: &Option<std::sync::Arc<Vec<SharedString>>>,
2082        state: &TableState,
2083        window: &mut Window,
2084        cx: &mut App,
2085    ) -> gpui::Stateful<gpui::Div> {
2086        let base_id = hx.base_id;
2087        let frozen = hx.frozen;
2088        let colors = hx.colors;
2089        let secondary = hx.secondary;
2090        // A frozen header measures its two parts instead (see
2091        // `frozen_header_parts`).
2092        let measured = if frozen.is_some() {
2093            gpui::div()
2094        } else {
2095            self.column_reorder_measure(gpui::div(), base_id, window, cx)
2096        };
2097        let mut header = measured
2098            .id(element_id::scoped(base_id, "header"))
2099            // `useTableHeaderRow.mjs` is a bare `role: 'row'`. Upstream wraps
2100            // it in a `<TableHeader>` whose `useTableRowGroup` gives it
2101            // `role: 'rowgroup'`; this port draws the single header row
2102            // directly into the table, so there is no second element for that
2103            // node to sit on. The body below *is* a container of rows, and it
2104            // carries the rowgroup.
2105            .a11y(a11y::Role::Row)
2106            .flex()
2107            .border_b_1()
2108            .border_color(colors.separator.alpha(0.5))
2109            .when(!secondary, |h| h.bg(colors.surface_secondary));
2110
2111        let mut cells: Vec<AnyElement> = Vec::new();
2112        if hx.selectable {
2113            cells.push(
2114                self.select_all_header_cell(
2115                    base_id,
2116                    colors,
2117                    secondary,
2118                    selectable_collection_keys,
2119                    &state.selection_own,
2120                    &state.selection_range,
2121                    cx,
2122                )
2123                .into_any_element(),
2124            );
2125        }
2126
2127        for (column_index, column) in self.columns.iter().enumerate() {
2128            cells.push(self.column_header_cell(hx, column_index, column, window, cx));
2129        }
2130        header = match frozen {
2131            Some(frozen) => {
2132                let (lead, rest) = self.frozen_header_parts(base_id, frozen, cells, window, cx);
2133                header.child(lead).child(rest)
2134            }
2135            None => header.children(cells),
2136        };
2137        self.column_reorder_header(
2138            header,
2139            base_id,
2140            column_order,
2141            hx.header_focus,
2142            hx.dragging,
2143            window,
2144            cx,
2145        )
2146    }
2147
2148    /// Flattens the plain rows (or reads the virtual projection) into what
2149    /// the row builder and the keyboard read, and resolves the cursor.
2150    fn row_collection(
2151        &mut self,
2152        virtual_body: &mut VirtualBody,
2153        state: &TableState,
2154        window: &mut Window,
2155        cx: &mut App,
2156    ) -> RowCollection {
2157        let virtual_projection = &virtual_body.projection;
2158        let mut flat: Vec<FlatRow> = Vec::new();
2159        flatten_rows(
2160            std::mem::take(&mut self.rows),
2161            0,
2162            "",
2163            None,
2164            &self.expanded_keys,
2165            &mut flat,
2166        );
2167        let tree_rows: Vec<(bool, Option<SharedString>)> = virtual_projection.as_ref().map_or_else(
2168            || {
2169                flat.iter()
2170                    .map(|(_, _, has_children, _, parent)| (*has_children, parent.clone()))
2171                    .collect()
2172            },
2173            |projection| {
2174                projection
2175                    .visible
2176                    .iter()
2177                    .map(|(_, _, metadata)| (metadata.has_children, metadata.parent_key.clone()))
2178                    .collect()
2179            },
2180        );
2181        let visible_collection_keys: Vec<SharedString> = virtual_body
2182            .visible_keys
2183            .take()
2184            .unwrap_or_else(|| flat.iter().map(|(_, _, _, key, _)| key.clone()).collect());
2185        let typeahead_labels: Vec<String> = virtual_projection.as_ref().map_or_else(
2186            || {
2187                flat.iter()
2188                    .map(|(row, _, _, _, _)| {
2189                        row.text_value
2190                            .as_ref()
2191                            .map(ToString::to_string)
2192                            .unwrap_or_default()
2193                    })
2194                    .collect()
2195            },
2196            |_| Vec::new(),
2197        );
2198        let virtual_typeahead_indices = virtual_projection.as_ref().map(|projection| {
2199            projection
2200                .visible
2201                .iter()
2202                .map(|(source_index, _, _)| *source_index)
2203                .collect::<Vec<_>>()
2204        });
2205        let row_cursor = &state.row_cursor;
2206        let table_focus = &state.table_focus;
2207        let cursor_key = row_cursor.read(cx).clone();
2208        let cursor_valid = cursor_key.as_ref().is_none_or(|key| {
2209            visible_collection_keys.contains(key) && !self.disabled_keys.contains(key)
2210        });
2211        if !cursor_valid {
2212            row_cursor.update(cx, |key, cx| {
2213                *key = None;
2214                cx.notify();
2215            });
2216        }
2217        let cursor_at = if window.is_window_active() && table_focus.is_focused(window) {
2218            cursor_key
2219                .as_ref()
2220                .and_then(|key| visible_collection_keys.iter().position(|row| row == key))
2221                .filter(|index| {
2222                    !self
2223                        .disabled_keys
2224                        .contains(&visible_collection_keys[*index])
2225                })
2226                .filter(|_| crate::util::focus_visible(cx))
2227        } else {
2228            None
2229        };
2230        // Whether any row in this table is expandable at all: a flat table
2231        // keeps its cells flush, rather than reserving a chevron's width.
2232        let tree_column_has_children = virtual_projection.as_ref().map_or_else(
2233            || flat.iter().any(|(_, _, has, _, _)| *has),
2234            |projection| {
2235                projection
2236                    .visible
2237                    .iter()
2238                    .any(|(_, _, metadata)| metadata.has_children)
2239            },
2240        );
2241        RowCollection {
2242            flat,
2243            tree_rows,
2244            visible_collection_keys,
2245            typeahead_labels,
2246            virtual_typeahead_indices,
2247            cursor_at,
2248            tree_column_has_children,
2249        }
2250    }
2251
2252    /// Everything one body row reads, held once for both the plain and the
2253    /// virtual paths.
2254    fn row_ctx(
2255        &self,
2256        base_id: &gpui::ElementId,
2257        state: &TableState,
2258        tracks: &ColumnTracks,
2259        rows: RowCtxRows,
2260    ) -> RowCtx {
2261        RowCtx {
2262            id: base_id.clone(),
2263            measured_widths: state.measured_widths.clone(),
2264            measure_intrinsic: self
2265                .columns
2266                .iter()
2267                // `on_children_prepainted` reports the browser canvas child
2268                // bounds as the full flex track during a web rerender. If we
2269                // feed that transient width back as a minimum, the first
2270                // column consumes the whole table after a selection update.
2271                // Native GPUI reports intrinsic child bounds correctly, so
2272                // retain the measurement path there and let web flex tracks
2273                // remain fluid unless a caller pins a width.
2274                .map(|column| !column.allows_resizing && !cfg!(target_arch = "wasm32"))
2275                .collect(),
2276            widths: self
2277                .columns
2278                .iter()
2279                .enumerate()
2280                .map(|(c, _)| {
2281                    let (layout_min, layout_max) = tracks.layout_width_bounds[c];
2282                    (
2283                        tracks.effective_widths.get(c).copied().flatten(),
2284                        layout_min,
2285                        layout_max,
2286                    )
2287                })
2288                .collect(),
2289            row_header_columns: self.columns.iter().map(|c| c.is_row_header).collect(),
2290            tree_column: self.tree_column,
2291            tree_column_has_children: rows.tree_column_has_children,
2292            selectable: self.selection_mode != SelectionMode::None,
2293            selection_mode: self.selection_mode,
2294            selection_behavior: self.selection_behavior,
2295            selected_keys: self.selected_keys.clone(),
2296            row_keys: rows.row_keys,
2297            disabled_keys: self.disabled_keys.clone(),
2298            disallow_empty_selection: self.disallow_empty_selection,
2299            expanded: rows.expanded,
2300            on_expanded_change: self.on_expanded_change.clone(),
2301            on_selection_change: self.on_selection_change.clone(),
2302            selection_own: state.selection_own.clone(),
2303            selection_range: state.selection_range.clone(),
2304            on_row_click: self.on_row_click.clone(),
2305            focus: state.table_focus.clone(),
2306            cursor_own: state.row_cursor.clone(),
2307            cursor: rows.cursor,
2308            secondary: self.variant == TableVariant::Secondary,
2309            row_hover_bg: self.row_hover_bg,
2310            hover_group_prefix: self.id.clone(),
2311            virtualized: rows.virtualized,
2312            round_top: rows.round_top,
2313            round_bottom: rows.round_bottom,
2314            is_tree: rows.is_tree,
2315            cell: rows.cell,
2316            frozen: rows.frozen,
2317        }
2318    }
2319
2320    /// The caller's empty state, centred in the body when there are no rows.
2321    fn empty_state_row(content: AnyElement, muted: gpui::Hsla) -> gpui::Div {
2322        gpui::div()
2323            .flex()
2324            .items_center()
2325            .justify_center()
2326            .w_full()
2327            .py(px(28.))
2328            .text_color(muted)
2329            .child(content)
2330    }
2331
2332    /// `.table__footer` is `flex items-center px-4 py-2.5`.
2333    fn footer_row(footer: AnyElement) -> gpui::Div {
2334        gpui::div()
2335            .flex()
2336            .items_center()
2337            .w_full()
2338            .px(px(16.))
2339            .py(px(10.))
2340            .child(footer)
2341    }
2342
2343    /// `.table__scroll-container` is `overflow-x-auto` around the content.
2344    fn scroll_container(
2345        base_id: &gpui::ElementId,
2346        table_id: &SharedString,
2347        table: gpui::Stateful<gpui::Div>,
2348    ) -> gpui::Stateful<gpui::Div> {
2349        // `.table__scroll-container` is `overflow-x-auto` around the content: a
2350        // column whose cross axis leaves the content column at its
2351        // max-content width, so a table wider than its box is free to exceed
2352        // the scroller and scroll *on* it, while `min_w_full` still fills a
2353        // narrow box. A row flex here would shrink that child to the viewport
2354        // unless it carried `flex_shrink_0`, which would also forbid the
2355        // column's vertical shrink inside a bounded parent; the column
2356        // direction shrinks the content vertically while never touching its
2357        // width. `min_h_0` is what permits that shrink: the scroller only
2358        // scrolls horizontally, so its visible y-overflow would otherwise keep
2359        // the content-based minimum and never yield.
2360        // The headless probe name for the scroll viewport's bounds, so a test
2361        // can read the grid's overflow against it.
2362        let scroll_selector = format!("{table_id}-scroll-x");
2363        gpui::div()
2364            .id(element_id::scoped(base_id, "scroll-x"))
2365            .debug_selector(move || scroll_selector)
2366            .flex()
2367            .flex_col()
2368            .items_start()
2369            .w_full()
2370            .min_h_0()
2371            .overflow_x_scroll()
2372            .restrict_scroll_to_axis()
2373            .child(table)
2374    }
2375}
2376
2377/// Each column's `(min, max)` resize bounds, in pixels.
2378fn column_resize_limits(columns: &[TableColumn]) -> Vec<(f32, f32)> {
2379    columns
2380        .iter()
2381        .map(|column| {
2382            (
2383                column.min_width.map_or(DEFAULT_COLUMN_MIN_WIDTH, f32::from),
2384                column.max_width.map_or(f32::MAX, f32::from),
2385            )
2386        })
2387        .collect()
2388}
2389
2390/// The width each column renders at: a controlled width, else the width a
2391/// resize left it at, else its default — clamped to its resize bounds when it
2392/// resizes. `None` is a flexible track.
2393fn effective_column_widths(
2394    columns: &[TableColumn],
2395    resized_now: &[Option<Pixels>],
2396    resize_limits: &[(f32, f32)],
2397) -> Vec<Option<Pixels>> {
2398    columns
2399        .iter()
2400        .enumerate()
2401        .map(|(column_index, column)| {
2402            let width = column.width.or_else(|| {
2403                resized_now
2404                    .get(column_index)
2405                    .copied()
2406                    .flatten()
2407                    .or(column.default_width)
2408            });
2409            if column.allows_resizing {
2410                let (min, max) = resize_limits[column_index];
2411                width.map(|width| px(f32::from(width).floor().min(max).max(min)))
2412            } else {
2413                width
2414            }
2415        })
2416        .collect()
2417}
2418
2419/// The `(min, max)` track bounds each column's header and body cells share;
2420/// see the comment at its call site in `Table::render`.
2421fn layout_width_bounds(
2422    columns: &[TableColumn],
2423    measured_widths_now: &[Option<Pixels>],
2424    effective_widths: &[Option<Pixels>],
2425    resize_limits: &[(f32, f32)],
2426) -> Vec<(Option<Pixels>, Option<Pixels>)> {
2427    columns
2428        .iter()
2429        .enumerate()
2430        .map(|(column_index, column)| {
2431            let measured = measured_widths_now.get(column_index).copied().flatten();
2432            if column.allows_resizing {
2433                if effective_widths[column_index].is_some() {
2434                    (None, None)
2435                } else {
2436                    let min = measured.map_or(resize_limits[column_index].0, |width| {
2437                        resize_limits[column_index].0.max(f32::from(width))
2438                    });
2439                    (Some(px(min)), column.max_width)
2440                }
2441            } else {
2442                let min = match (column.min_width, measured) {
2443                    (Some(configured), Some(content)) => {
2444                        Some(px(f32::from(configured).max(f32::from(content))))
2445                    }
2446                    (Some(configured), None) => Some(configured),
2447                    (None, Some(content)) => Some(content),
2448                    (None, None) => None,
2449                };
2450                (min, column.max_width)
2451            }
2452        })
2453        .collect()
2454}
2455
2456/// The per-render column state every header cell reads; see
2457/// [`Table::column_header_cell`].
2458#[derive(Clone, Copy)]
2459struct HeaderParts<'a> {
2460    base_id: &'a gpui::ElementId,
2461    colors: &'a herogpui_theme::ThemeColors,
2462    secondary: bool,
2463    selectable: bool,
2464    effective_widths: &'a [Option<Pixels>],
2465    layout_width_bounds: &'a [(Option<Pixels>, Option<Pixels>)],
2466    measured_widths: &'a gpui::Entity<Vec<Option<Pixels>>>,
2467    measured_widths_now: &'a [Option<Pixels>],
2468    header_focus: &'a [gpui::FocusHandle],
2469    header_focused: &'a [bool],
2470    resize_focus: &'a [Option<gpui::FocusHandle>],
2471    resize_focused: &'a [bool],
2472    resized: &'a gpui::Entity<Vec<Option<Pixels>>>,
2473    dragging: &'a gpui::Entity<Option<(usize, f32, f32)>>,
2474    drag_now: Option<(usize, f32, f32)>,
2475    keyboard_resizing: &'a gpui::Entity<Option<usize>>,
2476    keyboard_resize_now: Option<usize>,
2477    resize_limits: &'a [(f32, f32)],
2478    resize_columns: &'a std::sync::Arc<Vec<TableColumn>>,
2479    resize_measurements: &'a std::sync::Arc<Vec<Option<Pixels>>>,
2480    frozen: Option<&'a FrozenColumns>,
2481}
2482
2483/// One plain body row after flattening: the row, its depth, whether it has
2484/// children, its key and its parent's key.
2485type FlatRow = (TableRow, usize, bool, SharedString, Option<SharedString>);
2486
2487/// Depth-first, and only through the parents that are open: a nested row is
2488/// not rendered at all until its parent is expanded.
2489fn flatten_rows(
2490    rows: Vec<TableRow>,
2491    depth: usize,
2492    path: &str,
2493    parent: Option<&SharedString>,
2494    expanded: &[SharedString],
2495    out: &mut Vec<FlatRow>,
2496) {
2497    for (index, mut row) in rows.into_iter().enumerate() {
2498        let key = match &row.key {
2499            Some(k) => k.clone(),
2500            None if path.is_empty() => SharedString::from(index.to_string()),
2501            None => SharedString::from(format!("{path}-{index}")),
2502        };
2503        let children = std::mem::take(&mut row.children);
2504        let has_children = !children.is_empty();
2505        let is_open = expanded.contains(&key);
2506        out.push((row, depth, has_children, key.clone(), parent.cloned()));
2507        if has_children && is_open {
2508            flatten_rows(children, depth + 1, key.as_ref(), Some(&key), expanded, out);
2509        }
2510    }
2511}
2512
2513/// What [`Table::row_keyboard`] reads from the rest of the render.
2514struct RowKeyboard<'a> {
2515    ctx: &'a RowCtx,
2516    expanded_keys: std::rc::Rc<Vec<SharedString>>,
2517    header_focus: Vec<gpui::FocusHandle>,
2518    row_cursor: gpui::Entity<Option<SharedString>>,
2519    selectable_collection_keys: Option<std::sync::Arc<Vec<SharedString>>>,
2520    selection_own: Option<gpui::Entity<Vec<SharedString>>>,
2521    selection_range: gpui::Entity<TableSelectionRange>,
2522    table_focus: gpui::FocusHandle,
2523    tree_rows: Vec<(bool, Option<SharedString>)>,
2524    typeahead_labels: Vec<String>,
2525    typeahead: gpui::Entity<TableTypeahead>,
2526    virtual_list_state: &'a Option<gpui::ListState>,
2527    virtual_row_heights: &'a Option<gpui::Entity<(Vec<SharedString>, Vec<Option<Pixels>>)>>,
2528    virtual_scroll_now: &'a crate::VirtualListHandle,
2529    virtual_text_value: Option<VirtualRowText>,
2530    virtual_typeahead_indices: Option<Vec<usize>>,
2531}
2532
2533/// What [`Table::body_rows`] reads from the rest of the render.
2534struct BodyRows<'a> {
2535    base_id: &'a gpui::ElementId,
2536    table_id: &'a SharedString,
2537    ctx: &'a std::rc::Rc<RowCtx>,
2538    flat: Vec<FlatRow>,
2539    row_count: usize,
2540    virtual_projection: std::sync::Arc<Vec<(usize, SharedString, VirtualTreeMetadata)>>,
2541    virtual_list_state: Option<gpui::ListState>,
2542    virtual_row_heights: Option<gpui::Entity<(Vec<SharedString>, Vec<Option<Pixels>>)>>,
2543    virtual_scroll_now: &'a crate::VirtualListHandle,
2544}
2545
2546/// What [`Table::load_more`] reads from the rest of the render.
2547struct LoadMore<'a> {
2548    base_id: &'a gpui::ElementId,
2549    state: gpui::Entity<(bool, Option<LoadMoreCollection>)>,
2550    collection: LoadMoreCollection,
2551    virtual_scroll: Option<crate::VirtualListHandle>,
2552    variable_scroll: Option<(gpui::ListState, usize, Pixels)>,
2553    muted: gpui::Hsla,
2554    /// The pending row's text, resolved through the i18n catalogue.
2555    loading_text: SharedString,
2556}
2557
2558/// Everything the body's bubbling key handler reads, moved out of the render
2559/// because the handler outlives the frame. See [`Table::row_keyboard`].
2560struct RowKeyNav {
2561    /// The enabled rows' positions: the stops the cursor may rest on.
2562    stops: Vec<usize>,
2563    /// The row cursor.
2564    held: gpui::Entity<Option<SharedString>>,
2565    keys: Vec<SharedString>,
2566    table_focus: gpui::FocusHandle,
2567    typeahead: std::rc::Rc<TableTypeaheadNavigation>,
2568    on_row_click: Option<OnRowClick>,
2569    selection: Option<OnSelectionChange>,
2570    selection_own: Option<gpui::Entity<Vec<SharedString>>>,
2571    selection_range: gpui::Entity<TableSelectionRange>,
2572    selected_now: Vec<SharedString>,
2573    selectable_collection_keys: Option<std::sync::Arc<Vec<SharedString>>>,
2574    mode: SelectionMode,
2575    selection_behavior: SelectionBehavior,
2576    disallow_empty_selection: bool,
2577    select_on_focus: bool,
2578    plain_rows: bool,
2579    fixed_virtual: bool,
2580    fixed_row_height: Option<Pixels>,
2581    fixed_scroll: crate::VirtualListHandle,
2582    variable_scroll: Option<gpui::ListState>,
2583    variable_heights: Option<gpui::Entity<(Vec<SharedString>, Vec<Option<Pixels>>)>>,
2584    variable_estimate: Option<Pixels>,
2585    expanded: std::rc::Rc<Vec<SharedString>>,
2586    on_expanded: Option<OnExpandedChange>,
2587    tree_rows: Vec<(bool, Option<SharedString>)>,
2588    page_up_header: Option<gpui::FocusHandle>,
2589    headers: Vec<gpui::FocusHandle>,
2590}
2591
2592impl RowKeyNav {
2593    /// The body's bubbling key handler.
2594    fn key_down(&self, event: &gpui::KeyDownEvent, window: &mut Window, cx: &mut App) {
2595        if !self.table_focus.contains_focused(window, cx) {
2596            return;
2597        }
2598        let stops = &self.stops;
2599        let keys = &self.keys;
2600        let from = self
2601            .held
2602            .read(cx)
2603            .as_ref()
2604            .and_then(|key| keys.iter().position(|row| row == key))
2605            .filter(|index| stops.contains(index));
2606        // Pinned React Aria `useTableRow`: horizontal keys belong
2607        // to the focused tree row before the list resolver sees
2608        // them. Right opens; Left closes or returns to the parent.
2609        let key_name = event.keystroke.key.as_str();
2610        let typed = event.keystroke.key_char.as_deref().unwrap_or(key_name);
2611        let modifiers = &event.keystroke.modifiers;
2612        let now = web_time::Instant::now();
2613        let is_space = key_name == "space" || typed == " ";
2614        let is_character = {
2615            let mut chars = key_name.chars();
2616            chars.next().is_some() && chars.next().is_none() && !is_space
2617        };
2618        let is_typeahead =
2619            is_character && !modifiers.control && !modifiers.platform && !modifiers.alt;
2620        if is_typeahead {
2621            if self.typeahead.push(typed, now, true, cx) {
2622                cx.stop_propagation();
2623            }
2624            return;
2625        }
2626        // Pinned React Aria 3.51 `useSelectableCollection` binds
2627        // `Mod+A` -- the platform Mod, Control here -- to
2628        // `selectAll`, and only when the selection mode is
2629        // multiple. The shortcut matches its modifiers exactly,
2630        // so any extra modifier lets the event fall through.
2631        if key_name == "a"
2632            && event.keystroke.modifiers.secondary()
2633            && !event.keystroke.modifiers.shift
2634            && !event.keystroke.modifiers.alt
2635            && !event.keystroke.modifiers.function
2636            && if cfg!(target_os = "macos") {
2637                !event.keystroke.modifiers.control
2638            } else {
2639                !event.keystroke.modifiers.platform
2640            }
2641            && self.mode == SelectionMode::Multiple
2642        {
2643            self.select_all(window, cx);
2644            cx.stop_propagation();
2645            return;
2646        }
2647        // Other collection keys belong only to the body's roving
2648        // focus stop. A nested cell action must keep its own Enter
2649        // and Space handling even though Mod+A bubbles to the root.
2650        // Pinned TableKeyboardDelegate crosses header<->body in
2651        // both plain and virtual tables: Down/PageDown from a
2652        // focused header enters the body.
2653        if self.headers.iter().any(|header| header.is_focused(window)) {
2654            self.enter_from_header(key_name, window, cx);
2655            return;
2656        }
2657        if !self.table_focus.is_focused(window) {
2658            return;
2659        }
2660        if key_name == "escape"
2661            && self.mode != SelectionMode::None
2662            && !self.disallow_empty_selection
2663            && !self.selected_now.is_empty()
2664        {
2665            self.commit(&[], window, cx);
2666            self.selection_range.update(cx, |range, _| {
2667                *range = TableSelectionRange::default();
2668            });
2669            cx.stop_propagation();
2670            return;
2671        }
2672        if let Some(index) = from {
2673            if self.tree_key(index, key_name, window, cx) {
2674                return;
2675            }
2676        }
2677        let (fixed_page_move, variable_page_move) = self.page_moves(from, key_name, cx);
2678        let is_variable_page = fixed_page_move.is_none() && variable_page_move.is_some();
2679        if self.page_up_to_header(
2680            from,
2681            key_name,
2682            fixed_page_move,
2683            variable_page_move,
2684            window,
2685            cx,
2686        ) {
2687            return;
2688        }
2689        let plain_page_move = from
2690            .filter(|_| self.plain_rows)
2691            .and_then(|_| match key_name {
2692                "pagedown" => stops.last().copied(),
2693                _ => None,
2694            });
2695        let page_move = fixed_page_move
2696            .or(variable_page_move)
2697            .or(plain_page_move)
2698            .filter(|next| Some(*next) != from);
2699        // The pinned registrations install no Home/End handler
2700        // for an unregistered chord -- Cmd- or Ctrl-bearing on
2701        // macOS, Alt- or platform-bearing elsewhere -- so the
2702        // whole event stays inert: no focus move, no selection,
2703        // no preventDefault, and no first-press settle either.
2704        if matches!(key_name, "home" | "end")
2705            && !home_end_registered(*modifiers, cfg!(target_os = "macos"))
2706        {
2707            return;
2708        }
2709        let initial_home_end_extends =
2710            shift_home_end_extends("home", modifiers.control, cfg!(target_os = "macos"));
2711        let initial_shift_settle = from.is_none()
2712            && modifiers.shift
2713            && (key_name == "up"
2714                || (matches!(key_name, "home" | "end") && !initial_home_end_extends)
2715                || (key_name == "down" && stops.len() < 2));
2716        let navigation = if from.is_none() && modifiers.shift && key_name == "down" {
2717            stops
2718                .get(1)
2719                .or_else(|| stops.first())
2720                .copied()
2721                .map_or(crate::list_nav::Move::Ignore, crate::list_nav::Move::To)
2722        } else if initial_shift_settle {
2723            (if key_name == "end" {
2724                stops.last()
2725            } else {
2726                stops.first()
2727            })
2728            .copied()
2729            .map_or(crate::list_nav::Move::Ignore, crate::list_nav::Move::To)
2730        } else {
2731            page_move.map_or_else(
2732                || crate::list_nav::resolve(stops, from, key_name, false),
2733                crate::list_nav::Move::To,
2734            )
2735        };
2736        match navigation {
2737            crate::list_nav::Move::To(next) => {
2738                self.move_cursor(
2739                    next,
2740                    from,
2741                    key_name,
2742                    *modifiers,
2743                    initial_shift_settle,
2744                    is_variable_page,
2745                    window,
2746                    cx,
2747                );
2748            }
2749            crate::list_nav::Move::Activate => {
2750                let Some(index) = from else { return };
2751                self.activate(index, event, *modifiers, window, cx);
2752            }
2753            crate::list_nav::Move::Ignore => {}
2754        }
2755    }
2756
2757    /// Writes a new selection to the uncontrolled store and reports it.
2758    fn commit(&self, next: &[SharedString], window: &mut Window, cx: &mut App) {
2759        if let Some(held) = &self.selection_own {
2760            held.update(cx, |value, cx| {
2761                *value = next.to_vec();
2762                cx.notify();
2763            });
2764        }
2765        if let Some(cb) = &self.selection {
2766            cb(next, window, cx);
2767        }
2768    }
2769
2770    fn selectable_keys(&self) -> &[SharedString] {
2771        self.selectable_collection_keys
2772            .as_ref()
2773            .expect("multiple Tables have selectable keys")
2774            .as_slice()
2775    }
2776
2777    /// `Mod+A` on a multiple selection.
2778    fn select_all(&self, window: &mut Window, cx: &mut App) {
2779        let selectable_collection_keys = self
2780            .selectable_collection_keys
2781            .as_ref()
2782            .expect("multiple Tables have selectable keys");
2783        let selectable_collection_keys = selectable_collection_keys.clone();
2784        let selectable_collection_keys = selectable_collection_keys.as_slice();
2785        let (all_selected, _) = select_all_flags(selectable_collection_keys, &self.selected_now);
2786        let materializes_all = same_selection(selectable_collection_keys, &self.selected_now);
2787        let already_all =
2788            all_selected || (self.selection_range.read(cx).is_all && materializes_all);
2789        // Pinned React Stately's `selectAll` is idempotent once
2790        // the whole selectable collection is already selected.
2791        if !already_all {
2792            let next = selectable_collection_keys.to_vec();
2793            self.commit(&next, window, cx);
2794            self.selection_range.update(cx, |range, _| {
2795                *range = TableSelectionRange {
2796                    is_all: true,
2797                    ..TableSelectionRange::default()
2798                };
2799            });
2800        }
2801    }
2802
2803    /// Down/PageDown from a focused column header enters the body.
2804    fn enter_from_header(&self, key_name: &str, window: &mut Window, cx: &mut App) {
2805        let next = match key_name {
2806            "down" => self.stops.first(),
2807            "pagedown" => self.stops.last(),
2808            _ => None,
2809        };
2810        if let Some(next) = next {
2811            self.held.update(cx, |value, cx| {
2812                *value = Some(self.keys[*next].clone());
2813                cx.notify();
2814            });
2815            window.focus(&self.table_focus, cx);
2816            cx.stop_propagation();
2817        }
2818    }
2819
2820    /// A tree row's Right and Left. Returns whether the key was consumed.
2821    fn tree_key(&self, index: usize, key_name: &str, window: &mut Window, cx: &mut App) -> bool {
2822        let keys = &self.keys;
2823        let expanded = &self.expanded;
2824        let focused_key = &keys[index];
2825        let Some((has_children, parent)) = self.tree_rows.get(index) else {
2826            return false;
2827        };
2828        if key_name == "right" && *has_children && !expanded.contains(focused_key) {
2829            if let Some(cb) = &self.on_expanded {
2830                let mut next = expanded.as_ref().clone();
2831                next.push(focused_key.clone());
2832                cb(&next, window, cx);
2833            }
2834            crate::util::set_focus_visible(true, cx);
2835            cx.stop_propagation();
2836            return true;
2837        } else if key_name == "left" {
2838            if *has_children && expanded.contains(focused_key) {
2839                if let Some(cb) = &self.on_expanded {
2840                    let mut next = expanded.as_ref().clone();
2841                    next.retain(|key| key != focused_key);
2842                    cb(&next, window, cx);
2843                }
2844                crate::util::set_focus_visible(true, cx);
2845                cx.stop_propagation();
2846                return true;
2847            } else if let Some(parent) = parent {
2848                if let Some(parent_index) = keys.iter().position(|key| key == parent) {
2849                    self.held.update(cx, |value, cx| {
2850                        *value = Some(parent.clone());
2851                        cx.notify();
2852                    });
2853                    if self.fixed_virtual {
2854                        self.fixed_scroll
2855                            .scroll_to_item(parent_index, crate::VirtualListScroll::Center);
2856                    } else if let Some(state) = &self.variable_scroll {
2857                        state.scroll_to(gpui::ListOffset {
2858                            item_ix: parent_index,
2859                            offset_in_item: px(0.),
2860                        });
2861                    }
2862                    crate::util::set_focus_visible(true, cx);
2863                    cx.stop_propagation();
2864                    return true;
2865                }
2866            }
2867        }
2868        false
2869    }
2870
2871    /// The virtual bodies' PageUp/PageDown targets, by one viewport: the
2872    /// uniform body's `(fixed)` and the measured body's `(variable)`.
2873    fn page_moves(
2874        &self,
2875        from: Option<usize>,
2876        key_name: &str,
2877        cx: &App,
2878    ) -> (Option<usize>, Option<usize>) {
2879        let stops = &self.stops;
2880        let keys = &self.keys;
2881        let page_by_step = |from: usize, step: usize| match key_name {
2882            "pagedown" => {
2883                let boundary = from.saturating_add(step).min(keys.len() - 1);
2884                stops
2885                    .iter()
2886                    .copied()
2887                    .find(|stop| *stop >= boundary)
2888                    .or_else(|| stops.last().copied())
2889            }
2890            "pageup" => {
2891                let boundary = from.saturating_sub(step);
2892                stops
2893                    .iter()
2894                    .rev()
2895                    .copied()
2896                    .find(|stop| *stop <= boundary)
2897                    .or_else(|| stops.first().copied())
2898            }
2899            _ => None,
2900        };
2901        let fixed_page_move = from.and_then(|from| {
2902            let row_height = self.fixed_row_height?;
2903            let viewport_height = f32::from(self.fixed_scroll.viewport_bounds().size.height);
2904            if viewport_height <= 0. {
2905                return None;
2906            }
2907            let step =
2908                ((viewport_height / f32::from(row_height)).ceil() as usize).saturating_sub(1);
2909            page_by_step(from, step)
2910        });
2911        let variable_page_move = from.and_then(|from| {
2912            let viewport_height = self.variable_scroll.as_ref()?.viewport_bounds().size.height;
2913            let heights = self.variable_heights.as_ref()?.read(cx);
2914            let estimate = self.variable_estimate?;
2915            let height_at =
2916                |index: usize| heights.1.get(index).copied().flatten().unwrap_or(estimate);
2917            let mut distance = height_at(from);
2918            let mut target = from;
2919            match key_name {
2920                "pagedown" => {
2921                    if distance >= viewport_height {
2922                        return Some(target);
2923                    }
2924                    for next in stops.iter().copied().filter(|next| *next > from) {
2925                        for index in target + 1..=next {
2926                            distance += height_at(index);
2927                        }
2928                        target = next;
2929                        if distance >= viewport_height {
2930                            break;
2931                        }
2932                    }
2933                    Some(target)
2934                }
2935                "pageup" => {
2936                    if distance >= viewport_height {
2937                        return Some(target);
2938                    }
2939                    for previous in stops
2940                        .iter()
2941                        .rev()
2942                        .copied()
2943                        .filter(|previous| *previous < from)
2944                    {
2945                        for index in previous..target {
2946                            distance += height_at(index);
2947                        }
2948                        target = previous;
2949                        if distance >= viewport_height {
2950                            break;
2951                        }
2952                    }
2953                    Some(target)
2954                }
2955                _ => None,
2956            }
2957        });
2958        (fixed_page_move, variable_page_move)
2959    }
2960
2961    /// PageUp out of the body into the first column header. Returns whether
2962    /// the focus left.
2963    fn page_up_to_header(
2964        &self,
2965        from: Option<usize>,
2966        key_name: &str,
2967        fixed_page_move: Option<usize>,
2968        variable_page_move: Option<usize>,
2969        window: &mut Window,
2970        cx: &mut App,
2971    ) -> bool {
2972        // Pinned TableKeyboardDelegate sends PageDown to the last
2973        // enabled row, and PageUp out of the body entirely, into
2974        // the first column header. The header is focusable whether
2975        // or not it sorts, so this leaves the body rather than
2976        // stopping at its first row. Virtual tables page by
2977        // viewport first; only when the upward page move has
2978        // nowhere to go (the cursor is already the first enabled
2979        // stop, or the computed page target equals it) does
2980        // PageUp enter the header. The cursor stays seated so
2981        // Down from the header returns to the first enabled row.
2982        let plain_rows = self.plain_rows;
2983        if plain_rows && key_name == "pageup" {
2984            if let Some(header) = &self.page_up_header {
2985                window.focus(header, cx);
2986                cx.stop_propagation();
2987                return true;
2988            }
2989        } else if !plain_rows && key_name == "pageup" {
2990            let at_top = match from {
2991                Some(position) => {
2992                    self.stops.first().is_some_and(|first| *first == position)
2993                        || fixed_page_move.is_some_and(|target| Some(target) == from)
2994                        || variable_page_move.is_some_and(|target| Some(target) == from)
2995                }
2996                None => false,
2997            };
2998            if at_top {
2999                if let Some(header) = &self.page_up_header {
3000                    window.focus(header, cx);
3001                    cx.stop_propagation();
3002                    return true;
3003                }
3004            }
3005        }
3006        false
3007    }
3008
3009    /// Moves the cursor to `next`, extending or replacing the selection the
3010    /// way the chord asks, and scrolls a virtual body to it.
3011    #[allow(clippy::too_many_arguments)]
3012    fn move_cursor(
3013        &self,
3014        next: usize,
3015        from: Option<usize>,
3016        key_name: &str,
3017        modifiers: gpui::Modifiers,
3018        initial_shift_settle: bool,
3019        is_variable_page: bool,
3020        window: &mut Window,
3021        cx: &mut App,
3022    ) {
3023        let keys = &self.keys;
3024        // Pinned `useSelectableCollection`: Shift extends a
3025        // multiple selection from the anchor with no other
3026        // chord, so plain Shift navigation is exact.
3027        let exact_shift_navigation = if cfg!(target_os = "macos") {
3028            !modifiers.control && !modifiers.platform && !modifiers.function
3029        } else {
3030            !modifiers.alt && !modifiers.platform && !modifiers.function
3031        };
3032        let extends_selection = modifiers.shift
3033            && self.mode == SelectionMode::Multiple
3034            && exact_shift_navigation
3035            && !initial_shift_settle
3036            && Some(next) != from
3037            && shift_home_end_extends(key_name, modifiers.control, cfg!(target_os = "macos"));
3038        if extends_selection {
3039            if let Some(target) = keys.get(next) {
3040                let range = self.selection_range.read(cx).clone();
3041                let next_selection = extend_selection_range(
3042                    &self.selected_now,
3043                    keys,
3044                    self.selectable_keys(),
3045                    &range,
3046                    target,
3047                );
3048                self.selection_range.update(cx, |range, _| {
3049                    if range.anchor.is_none() {
3050                        range.anchor = Some(target.clone());
3051                    }
3052                    range.current = Some(target.clone());
3053                    range.is_all = false;
3054                });
3055                if !same_selection(&next_selection, &self.selected_now) {
3056                    self.commit(&next_selection, window, cx);
3057                }
3058            }
3059        } else if self.select_on_focus
3060            && !modifiers.shift
3061            && !((cfg!(target_os = "macos") && modifiers.alt)
3062                || (!cfg!(target_os = "macos") && modifiers.control))
3063        {
3064            if let Some(target) = keys.get(next) {
3065                let next_selection = vec![target.clone()];
3066                if !same_selection(&next_selection, &self.selected_now) {
3067                    self.commit(&next_selection, window, cx);
3068                }
3069                self.selection_range.update(cx, |range, _| {
3070                    *range = TableSelectionRange {
3071                        anchor: Some(target.clone()),
3072                        current: Some(target.clone()),
3073                        ..TableSelectionRange::default()
3074                    };
3075                });
3076            }
3077        }
3078        self.held.update(cx, |v, cx| {
3079            *v = keys.get(next).cloned();
3080            cx.notify();
3081        });
3082        if self.fixed_virtual {
3083            self.fixed_scroll
3084                .scroll_to_item(next, crate::VirtualListScroll::Center);
3085        } else if let Some(state) = &self.variable_scroll {
3086            if is_variable_page {
3087                state.scroll_to_reveal_item(next);
3088            } else {
3089                // Unmeasured rows have no cached height, so
3090                // `scroll_to_reveal_item` cannot locate a far
3091                // target. Jump by logical index; the next
3092                // layout measures that row at the viewport.
3093                state.scroll_to(gpui::ListOffset {
3094                    item_ix: next,
3095                    offset_in_item: px(0.),
3096                });
3097            }
3098        }
3099    }
3100
3101    /// Enter and Space on the cursor row: its action, or a selection change.
3102    fn activate(
3103        &self,
3104        index: usize,
3105        event: &gpui::KeyDownEvent,
3106        modifiers: gpui::Modifiers,
3107        window: &mut Window,
3108        cx: &mut App,
3109    ) {
3110        let activation = if event.keystroke.key == "enter" {
3111            RowActivation::Enter
3112        } else {
3113            RowActivation::Space
3114        };
3115        match row_intent(
3116            activation,
3117            self.mode,
3118            self.selection_behavior,
3119            self.selected_now.is_empty(),
3120            self.on_row_click.is_some(),
3121        ) {
3122            RowIntent::Action => {
3123                if let Some(cb) = &self.on_row_click {
3124                    cb(&index, &ClickEvent::default(), window, cx);
3125                }
3126            }
3127            RowIntent::Selection => {
3128                if let Some(key) = self.keys.get(index) {
3129                    self.select_row(key, modifiers, window, cx);
3130                }
3131            }
3132            RowIntent::None => {}
3133        }
3134    }
3135
3136    /// The keyboard's row-selection change, anchored the way a press is.
3137    fn select_row(
3138        &self,
3139        key: &SharedString,
3140        modifiers: gpui::Modifiers,
3141        window: &mut Window,
3142        cx: &mut App,
3143    ) {
3144        let mode = self.mode;
3145        let selected_now = &self.selected_now;
3146        let was_selected = selected_now.contains(key);
3147        let extends_selection = modifiers.shift && mode == SelectionMode::Multiple;
3148        let next = if extends_selection {
3149            let range = self.selection_range.read(cx).clone();
3150            extend_selection_range(
3151                selected_now,
3152                &self.keys,
3153                self.selectable_keys(),
3154                &range,
3155                key,
3156            )
3157        } else {
3158            let non_contiguous = if cfg!(target_os = "macos") {
3159                modifiers.alt
3160            } else {
3161                modifiers.control
3162            };
3163            crate::selection::next_selection_with_behavior(
3164                selected_now,
3165                key,
3166                mode,
3167                if non_contiguous {
3168                    SelectionBehavior::Toggle
3169                } else {
3170                    self.selection_behavior
3171                },
3172                self.disallow_empty_selection,
3173            )
3174        };
3175        let changed = !same_selection(&next, selected_now);
3176        if changed {
3177            self.commit(&next, window, cx);
3178        }
3179        if extends_selection && changed {
3180            self.selection_range.update(cx, |range, _| {
3181                if range.anchor.is_none() {
3182                    range.anchor = Some(key.clone());
3183                }
3184                range.current = Some(key.clone());
3185                range.is_all = false;
3186            });
3187        } else if !extends_selection && mode == SelectionMode::Multiple && !was_selected {
3188            self.selection_range.update(cx, |range, _| {
3189                range.anchor = Some(key.clone());
3190                range.current = Some(key.clone());
3191                range.is_all = false;
3192            });
3193        } else if !extends_selection && mode == SelectionMode::Multiple {
3194            self.selection_range.update(cx, |range, _| {
3195                if range.is_all {
3196                    *range = TableSelectionRange::default();
3197                }
3198            });
3199        }
3200    }
3201}
3202
3203/// A zero-paint canvas over a flexible resizable column that records the
3204/// width the column laid out at, which is where a drag or a keyboard resize
3205/// starts from.
3206fn resize_width_probe(
3207    measured: gpui::Entity<Vec<Option<Pixels>>>,
3208    column_index: usize,
3209) -> impl IntoElement {
3210    gpui::canvas(
3211        move |bounds: gpui::Bounds<Pixels>, _, cx| {
3212            let width = px(f32::from(bounds.size.width).floor());
3213            measured.update(cx, |values, cx| {
3214                if values.len() <= column_index {
3215                    values.resize(column_index + 1, None);
3216                }
3217                if values[column_index] != Some(width) {
3218                    values[column_index] = Some(width);
3219                    cx.notify();
3220                }
3221            });
3222            bounds
3223        },
3224        |_, _, _, _| {},
3225    )
3226    .absolute()
3227    .inset_0()
3228}
3229
3230/// What a press on a column's resize handle reads: the column, the width
3231/// the drag starts from, the width stores and the resize callbacks.
3232struct ResizePointerStart {
3233    column_index: usize,
3234    /// The width a resize starts from: the rendered width, else the
3235    /// measured one, else a 160px default.
3236    start_width: Pixels,
3237    /// The pointer drag in progress, as `(column, from_x, from_width)`.
3238    held: gpui::Entity<Option<(usize, f32, f32)>>,
3239    /// The column the keyboard is resizing.
3240    keyboard: gpui::Entity<Option<usize>>,
3241    widths: gpui::Entity<Vec<Option<Pixels>>>,
3242    columns: std::sync::Arc<Vec<TableColumn>>,
3243    measurements: std::sync::Arc<Vec<Option<Pixels>>>,
3244    focus: gpui::FocusHandle,
3245    on_resize_start: Option<OnResize>,
3246    on_resize_end: Option<OnResize>,
3247}
3248
3249impl ResizePointerStart {
3250    /// A press on the handle: ends any keyboard resize, reports the start
3251    /// and seats the pointer drag.
3252    fn press(&self, ev: &gpui::MouseDownEvent, window: &mut Window, cx: &mut App) {
3253        let column_index = self.column_index;
3254        window.focus(&self.focus, cx);
3255        if let Some(active_column) = *self.keyboard.read(cx) {
3256            let current = final_column_widths(
3257                &self.columns,
3258                self.widths.read(cx),
3259                &self.measurements,
3260                active_column,
3261            );
3262            self.keyboard.update(cx, |active, _| *active = None);
3263            clear_controlled_resize_proposal(&self.columns, &self.widths, active_column, cx);
3264            if let Some(callback) = &self.on_resize_end {
3265                callback(&current, window, cx);
3266            }
3267        }
3268        clear_controlled_resize_proposal(&self.columns, &self.widths, column_index, cx);
3269        if let Some(callback) = &self.on_resize_start {
3270            let current = resolved_column_widths(
3271                &self.columns,
3272                self.widths.read(cx),
3273                &self.measurements,
3274                None,
3275            );
3276            callback(&current, window, cx);
3277        }
3278        let x = f32::from(ev.position.x);
3279        let start_width = self.start_width;
3280        self.held.update(cx, |v, _| {
3281            *v = Some((column_index, x, f32::from(start_width)));
3282        });
3283    }
3284}
3285
3286// ---- Table::render part renderers ----
3287impl Table {
3288    /// The visible window onto `virtual_rows`: every key, the rows a tree's
3289    /// open parents expose, and (for multiple selection) the enabled keys.
3290    /// Cached in the window's keyed store until the collection, the expanded
3291    /// set or the disabled keys change.
3292    fn virtual_projection(
3293        &self,
3294        base_id: &gpui::ElementId,
3295        window: &mut Window,
3296        cx: &mut App,
3297    ) -> Option<std::sync::Arc<VirtualProjection>> {
3298        let needs_selectable_keys = self.selection_mode == SelectionMode::Multiple;
3299        self.virtual_rows
3300            .as_ref()
3301            .map(|(count, identity, key_for_row, _)| {
3302                let cache = window.use_keyed_state(
3303                    element_id::scoped(base_id, "virtual-projection"),
3304                    cx,
3305                    |_, _| None::<std::sync::Arc<VirtualProjection>>,
3306                );
3307                let cached = cache.read(cx).clone();
3308                let tree = self.virtual_tree_metadata.is_some();
3309                match cached {
3310                    Some(cached)
3311                        if cached.count == *count
3312                            && cached.identity == *identity
3313                            && cached.tree == tree
3314                            && (!tree
3315                                || (cached.expanded.len() == self.expanded_keys.len()
3316                                    && self
3317                                        .expanded_keys
3318                                        .iter()
3319                                        .all(|key| cached.expanded.contains(key)))) =>
3320                    {
3321                        if !needs_selectable_keys
3322                            || cached
3323                                .selectable_disabled_keys
3324                                .as_ref()
3325                                .is_some_and(|disabled| {
3326                                    disabled.as_slice() == self.disabled_keys.as_slice()
3327                                })
3328                        {
3329                            cached
3330                        } else {
3331                            let projection = std::sync::Arc::new(VirtualProjection {
3332                                count: cached.count,
3333                                identity: cached.identity.clone(),
3334                                expanded: std::sync::Arc::clone(&cached.expanded),
3335                                tree: cached.tree,
3336                                full_keys: std::sync::Arc::clone(&cached.full_keys),
3337                                visible: std::sync::Arc::clone(&cached.visible),
3338                                selectable_collection_keys: Some(filtered_selectable_keys(
3339                                    cached.full_keys.as_slice(),
3340                                    &self.disabled_keys,
3341                                )),
3342                                selectable_disabled_keys: Some(std::sync::Arc::new(
3343                                    self.disabled_keys.clone(),
3344                                )),
3345                            });
3346                            cache.update(cx, |slot, _| {
3347                                *slot = Some(std::sync::Arc::clone(&projection));
3348                            });
3349                            projection
3350                        }
3351                    }
3352                    _ => {
3353                        let mut full_keys = Vec::with_capacity(*count);
3354                        let mut visible = Vec::with_capacity(*count);
3355                        if let Some(project) = &self.virtual_tree_metadata {
3356                            let mut visible_by_key =
3357                                std::collections::HashMap::with_capacity(*count);
3358                            for source_index in 0..*count {
3359                                let key = key_for_row(source_index);
3360                                let metadata = project(source_index);
3361                                let is_visible =
3362                                    metadata.parent_key.as_ref().is_none_or(|parent| {
3363                                        visible_by_key.get(parent).copied().unwrap_or(false)
3364                                            && self.expanded_keys.contains(parent)
3365                                    });
3366                                visible_by_key.insert(key.clone(), is_visible);
3367                                full_keys.push(key.clone());
3368                                if is_visible {
3369                                    visible.push((source_index, key, metadata));
3370                                }
3371                            }
3372                        } else {
3373                            for source_index in 0..*count {
3374                                let key = key_for_row(source_index);
3375                                full_keys.push(key.clone());
3376                                visible.push((source_index, key, VirtualTreeMetadata::default()));
3377                            }
3378                        }
3379                        let full_keys = std::sync::Arc::new(full_keys);
3380                        let selectable_collection_keys = needs_selectable_keys.then(|| {
3381                            filtered_selectable_keys(full_keys.as_slice(), &self.disabled_keys)
3382                        });
3383                        let selectable_disabled_keys = needs_selectable_keys
3384                            .then(|| std::sync::Arc::new(self.disabled_keys.clone()));
3385                        let projection = std::sync::Arc::new(VirtualProjection {
3386                            count: *count,
3387                            identity: identity.clone(),
3388                            expanded: if tree {
3389                                std::sync::Arc::new(self.expanded_keys.iter().cloned().collect())
3390                            } else {
3391                                std::sync::Arc::default()
3392                            },
3393                            tree,
3394                            full_keys,
3395                            visible: std::sync::Arc::new(visible),
3396                            selectable_collection_keys,
3397                            selectable_disabled_keys,
3398                        });
3399                        cache.update(cx, |slot, _| {
3400                            *slot = Some(std::sync::Arc::clone(&projection));
3401                        });
3402                        projection
3403                    }
3404                }
3405            })
3406    }
3407
3408    /// The measured heights of an `estimatedRowHeight` virtual body, keyed by
3409    /// the visible keys so a changed window starts unmeasured.
3410    fn virtual_row_heights(
3411        &self,
3412        base_id: &gpui::ElementId,
3413        virtual_visible_keys: Option<&Vec<SharedString>>,
3414        window: &mut Window,
3415        cx: &mut App,
3416    ) -> Option<gpui::Entity<(Vec<SharedString>, Vec<Option<Pixels>>)>> {
3417        match (
3418            &self.virtual_rows,
3419            self.estimated_row_height,
3420            virtual_visible_keys,
3421        ) {
3422            (Some((_, identity, _, _)), Some(_), Some(keys)) if self.row_height.is_none() => {
3423                let count = keys.len();
3424                let state = window.use_keyed_state(
3425                    element_id::scoped(
3426                        &element_id::scoped(base_id, "row-heights"),
3427                        identity.clone(),
3428                    ),
3429                    cx,
3430                    |_, _| (Vec::<SharedString>::new(), Vec::<Option<Pixels>>::new()),
3431                );
3432                if state.read(cx).0.as_slice() != keys.as_slice() {
3433                    let stored_keys = keys.clone();
3434                    state.update(cx, |stored, _| {
3435                        *stored = (stored_keys, vec![None; count]);
3436                    });
3437                }
3438                Some(state)
3439            }
3440            _ => None,
3441        }
3442    }
3443
3444    /// The intrusive `gpui::list` state of an `estimatedRowHeight` virtual
3445    /// body, reset whenever the visible row count changes.
3446    fn virtual_list_state(
3447        &self,
3448        base_id: &gpui::ElementId,
3449        virtual_visible_count: usize,
3450        window: &mut Window,
3451        cx: &mut App,
3452    ) -> Option<gpui::ListState> {
3453        match (
3454            self.row_height,
3455            self.estimated_row_height,
3456            &self.virtual_rows,
3457        ) {
3458            (None, Some(estimate), Some((_, identity, _, _))) => {
3459                let count = virtual_visible_count;
3460                let overdraw = self.max_h.unwrap_or(px(400.)).max(estimate * 3.);
3461                let state = window
3462                    .use_keyed_state(
3463                        element_id::scoped(
3464                            &element_id::scoped(base_id, "list-state"),
3465                            identity.clone(),
3466                        ),
3467                        cx,
3468                        move |_, _| crate::VirtualListHandle::with_overdraw(count, overdraw),
3469                    )
3470                    .read(cx)
3471                    .list_state()
3472                    .clone();
3473                if state.item_count() != count {
3474                    state.reset(count);
3475                }
3476                Some(state)
3477            }
3478            _ => None,
3479        }
3480    }
3481
3482    /// One focus handle per column header. Only a sortable header is a tab
3483    /// stop (Enter and Space sort it); the rest are focusable so PageUp has a
3484    /// header to land on.
3485    fn header_focus_handles(
3486        &self,
3487        base_id: &gpui::ElementId,
3488        sortable: bool,
3489        window: &mut Window,
3490        cx: &mut App,
3491    ) -> Vec<gpui::FocusHandle> {
3492        self.columns
3493            .iter()
3494            .enumerate()
3495            .map(|(i, column)| {
3496                let id = element_id::scoped(&element_id::indexed(base_id, "sort", i), "focus");
3497                if column.allows_sorting && sortable {
3498                    crate::util::tab_stop_handle(id, window, cx)
3499                } else {
3500                    window
3501                        .use_keyed_state(id, cx, |_, cx| cx.focus_handle())
3502                        .read(cx)
3503                        .clone()
3504                }
3505            })
3506            .collect()
3507    }
3508
3509    /// The tab stop on each resizable column's handle; `None` for a column
3510    /// that does not resize.
3511    fn resize_focus_handles(
3512        &self,
3513        base_id: &gpui::ElementId,
3514        window: &mut Window,
3515        cx: &mut App,
3516    ) -> Vec<Option<gpui::FocusHandle>> {
3517        self.columns
3518            .iter()
3519            .enumerate()
3520            .map(|(i, column)| {
3521                column.allows_resizing.then(|| {
3522                    crate::util::tab_stop_handle(
3523                        element_id::scoped(&element_id::indexed(base_id, "resize", i), "focus"),
3524                        window,
3525                        cx,
3526                    )
3527                })
3528            })
3529            .collect()
3530    }
3531
3532    /// The selection column's header: v3's `columnheader` holding the
3533    /// select-all checkbox for a multiple selection, an empty aligned cell
3534    /// otherwise.
3535    #[allow(clippy::too_many_arguments)]
3536    fn select_all_header_cell(
3537        &self,
3538        base_id: &gpui::ElementId,
3539        colors: &herogpui_theme::ThemeColors,
3540        secondary: bool,
3541        selectable_collection_keys: &Option<std::sync::Arc<Vec<SharedString>>>,
3542        selection_own: &Option<gpui::Entity<Vec<SharedString>>>,
3543        selection_range: &gpui::Entity<TableSelectionRange>,
3544        cx: &mut App,
3545    ) -> gpui::Stateful<gpui::Div> {
3546        // The select-all box only makes sense for a multiple selection; a
3547        // single-selection table keeps the column for alignment.
3548        let mut cell = gpui::div()
3549            .id(element_id::scoped(base_id, "select-all-cell"))
3550            // The selection column is a real column of the collection
3551            // upstream, so its header is a `role: 'columnheader'` like any
3552            // other (`useTableColumnHeader.mjs`), holding the select-all
3553            // checkbox `useTableSelectAllCheckbox` names.
3554            .a11y(a11y::Role::ColumnHeader)
3555            .a11y_column_index(0)
3556            .flex()
3557            .items_center()
3558            .justify_center()
3559            .relative()
3560            .w(px(44.))
3561            .py(px(10.));
3562        if secondary {
3563            let radius = cx.layout().radius_2xl().min(px(32.));
3564            cell = cell
3565                .bg(colors.surface_secondary)
3566                .rounded_tl(radius)
3567                .rounded_bl(radius);
3568        }
3569        if self.selection_mode == SelectionMode::Multiple {
3570            // The `Mod+A` keydown handler reads the same set, so it gets a
3571            // clone rather than the variable itself.
3572            let all = selectable_collection_keys
3573                .as_ref()
3574                .expect("multiple Tables have selectable keys")
3575                .clone();
3576            let (all_selected, indeterminate) =
3577                select_all_flags(all.as_slice(), &self.selected_keys);
3578            let mut box_el = Checkbox::new(element_id::scoped(base_id, "select-all"))
3579                .is_selected(all_selected)
3580                .is_indeterminate(indeterminate);
3581            let cb = self.on_selection_change.clone();
3582            if cb.is_some() || selection_own.is_some() {
3583                let selection_own = selection_own.clone();
3584                let selection_range = selection_range.clone();
3585                let disallow_empty_selection = self.disallow_empty_selection;
3586                box_el = box_el.on_change(move |_next, window, cx| {
3587                    if all_selected && disallow_empty_selection {
3588                        return;
3589                    }
3590                    // Anything short of everything selects everything.
3591                    let next: Vec<SharedString> = if all_selected {
3592                        Vec::new()
3593                    } else {
3594                        all.as_slice().to_vec()
3595                    };
3596                    if let Some(held) = &selection_own {
3597                        held.update(cx, |value, cx| {
3598                            *value = next.clone();
3599                            cx.notify();
3600                        });
3601                    }
3602                    if let Some(cb) = &cb {
3603                        cb(&next, window, cx);
3604                    }
3605                    selection_range.update(cx, |range, _| {
3606                        *range = if all_selected {
3607                            TableSelectionRange::default()
3608                        } else {
3609                            TableSelectionRange {
3610                                is_all: true,
3611                                ..TableSelectionRange::default()
3612                            }
3613                        };
3614                    });
3615                });
3616            }
3617            cell = cell.child(box_el);
3618        }
3619        cell
3620    }
3621
3622    /// One column's header cell: the `.table__column` label with its sort
3623    /// indicator and separator, wrapped in a sort target when the column
3624    /// sorts, and in the resize container when it resizes.
3625    fn column_header_cell(
3626        &self,
3627        hx: &HeaderParts<'_>,
3628        column_index: usize,
3629        column: &TableColumn,
3630        window: &mut Window,
3631        cx: &mut App,
3632    ) -> AnyElement {
3633        let HeaderParts {
3634            base_id,
3635            colors,
3636            secondary,
3637            selectable,
3638            effective_widths,
3639            layout_width_bounds,
3640            measured_widths,
3641            header_focus,
3642            header_focused,
3643            ..
3644        } = *hx;
3645        let sorted = self
3646            .sort_descriptor
3647            .as_ref()
3648            .filter(|d| d.column == column.label);
3649        // A resized column keeps the width the drag left it at.
3650        let effective = effective_widths[column_index];
3651        let (layout_min, layout_max) = layout_width_bounds[column_index];
3652        let first_header = column_index == 0 && !selectable;
3653        let last_header = column_index + 1 == self.columns.len();
3654        let mut cell = gpui::div()
3655            .when(effective.is_none(), flex_cell)
3656            .when_some(effective, |c, w| c.w(w))
3657            .when_some(layout_min, |c, w| c.min_w(w))
3658            .when_some(layout_max, |c, w| c.max_w(w))
3659            .flex()
3660            .items_center()
3661            .gap(px(4.))
3662            // `.table__column` is `px-4 py-2.5 text-xs`.
3663            .px(px(16.))
3664            .py(px(10.))
3665            .text_size(px(12.))
3666            .line_height(px(16.))
3667            .font_weight(gpui::FontWeight::MEDIUM)
3668            .when(column.allows_sorting, |c| c.w_full().justify_between())
3669            .text_color(if sorted.is_some() {
3670                colors.foreground
3671            } else {
3672                colors.muted
3673            })
3674            .relative();
3675        if !column.allows_resizing && !cfg!(target_arch = "wasm32") {
3676            let measurements = measured_widths.clone();
3677            // The label and the sort indicator; a focused header's ring is an
3678            // overlay as wide as the cell (see `RowCtx::data_cell`).
3679            let content_children = 1 + usize::from(sorted.is_some() && self.show_indicator);
3680            cell = cell.on_children_prepainted(move |bounds, _, cx| {
3681                let content =
3682                    intrinsic_children_width(&bounds[..content_children.min(bounds.len())]);
3683                if content <= 0. {
3684                    return;
3685                }
3686                let next = px(content + 32.);
3687                measurements.update(cx, |values, cx| {
3688                    if values.len() <= column_index {
3689                        values.resize(column_index + 1, None);
3690                    }
3691                    if values[column_index].is_none_or(|current| next > current) {
3692                        values[column_index] = Some(next);
3693                        cx.notify();
3694                    }
3695                });
3696            });
3697        }
3698        cell = cell.child(column.label.clone());
3699        let header_selector = format!("table-header-track-{column_index}");
3700        cell = cell.debug_selector(move || header_selector);
3701
3702        if secondary {
3703            let radius = cx.layout().radius_2xl().min(px(32.));
3704            cell = cell.bg(colors.surface_secondary);
3705            if first_header {
3706                cell = cell.rounded_tl(radius).rounded_bl(radius);
3707            }
3708            if last_header {
3709                cell = cell.rounded_tr(radius).rounded_br(radius);
3710            }
3711        }
3712
3713        if let Some(descriptor) = sorted {
3714            if self.show_indicator {
3715                // `indicator` is v3's render prop on
3716                // `Table.SortableColumnHeader`: it receives the direction
3717                // the column is sorted in and replaces the chevron.
3718                cell = cell.child(match &self.indicator {
3719                    Some(render) => render(descriptor.direction),
3720                    None => {
3721                        let indicator_id = element_id::indexed(
3722                            &element_id::scoped(base_id, "sort-indicator"),
3723                            "column",
3724                            column_index,
3725                        );
3726                        crate::anim::rotating_indicator_with_duration(
3727                            &indicator_id,
3728                            descriptor.direction == SortDirection::Descending,
3729                            gpui::svg()
3730                                .size(px(12.))
3731                                .path(icons::CHEVRON_UP)
3732                                // svg() never inherits text colour.
3733                                .text_color(colors.foreground),
3734                            100,
3735                            window,
3736                            cx,
3737                        )
3738                    }
3739                });
3740            }
3741        }
3742
3743        if !last_header && !column.allows_resizing {
3744            cell = cell.child(
3745                gpui::div()
3746                    .absolute()
3747                    .right_0()
3748                    .top(px(10.))
3749                    .h(px(16.))
3750                    .w(px(1.))
3751                    .rounded(crate::util::hairline_radius(cx))
3752                    .bg(colors.separator),
3753            );
3754        }
3755
3756        // A sortable header wraps the cell in a click target, which is a
3757        // different element type, so both branches unify to AnyElement.
3758        let cell = match (column.allows_sorting, self.on_sort_change.clone()) {
3759            (true, Some(cb)) => {
3760                let next =
3761                    SortDescriptor::next(self.sort_descriptor.as_ref(), column.label.clone());
3762                // `.table__column[data-allows-sorting]:hover` recolours the
3763                // header text to `--foreground`; it paints no background.
3764                // A group name is a plain string, not an `ElementId`, so it
3765                // cannot carry structure -- but it still has to start from
3766                // this table's own id, or two tables share the group.
3767                let sort_group: SharedString =
3768                    format!("{}-sort-hover-{}", self.id, column.label).into();
3769                let header_cell = gpui::div()
3770                    .id(element_id::scoped(
3771                        &element_id::scoped(base_id, "sort"),
3772                        column.label.clone(),
3773                    ))
3774                    // `useTableColumnHeader.mjs` is `role: 'columnheader'`
3775                    // with `'aria-colindex'` from the grid cell props
3776                    // underneath it and `'aria-sort'` on a sortable
3777                    // column. The sort direction has no gpui builder —
3778                    // upstream itself drops it on Android Talkback and
3779                    // duplicates it into `aria-describedby` instead — so
3780                    // it is a recorded omission in `crate::a11y`.
3781                    .a11y_named(
3782                        a11y::Role::ColumnHeader,
3783                        &a11y::Name::labelled(column.label.clone()),
3784                    )
3785                    .a11y_column_index(column_index + usize::from(selectable))
3786                    .group(sort_group.clone())
3787                    // The wrapper owns the column's track, so it reads the
3788                    // same triple the body cell read: a fixed width when
3789                    // one wins, otherwise the shared flex floor. Leaving
3790                    // the wrapper an unconditional equal-growth track
3791                    // ignored the width entirely and resolved the header's
3792                    // tracks separately from the body's.
3793                    .when(effective.is_none(), |wrapper| {
3794                        wrapper.flex_basis(px(0.)).flex_grow(1.).flex_shrink(1.)
3795                    })
3796                    .when_some(effective, |wrapper, w| wrapper.w(w))
3797                    .when_some(layout_min, |wrapper, w| wrapper.min_w(w))
3798                    .when_some(layout_max, |wrapper, w| wrapper.max_w(w))
3799                    .flex()
3800                    .cursor(crate::util::interactive_cursor(cx))
3801                    // The focus is what makes Enter and Space sort: gpui
3802                    // fires a *focused* element's click listeners for them.
3803                    .track_focus(&header_focus[column_index])
3804                    .on_click(move |_, window, cx| cb(&next.clone(), window, cx))
3805                    .child(cell.group_hover(sort_group, |s| s.text_color(colors.foreground)));
3806                // `.table__column` rings *inside* itself: the next column
3807                // is flush against this one, and a ring drawn outside bled
3808                // through the transparent cell and filled it.
3809                let header_cell = header_cell
3810                    .relative()
3811                    .when(header_focused[column_index], |c| {
3812                        c.child(crate::util::inset_focus_ring(cx))
3813                    });
3814                crate::util::record_focus_bounds(
3815                    header_cell,
3816                    &header_focus[column_index],
3817                    window,
3818                    cx,
3819                )
3820                .into_any_element()
3821            }
3822            // Not sortable, so nothing to press -- but still focusable, so
3823            // PageUp has a header to land on, and it rings when it does.
3824            _ => {
3825                let header_cell = cell
3826                .id(element_id::indexed(base_id, "header", column_index))
3827                // The same `role: 'columnheader'`; a column that does not
3828                // sort simply has no `aria-sort` upstream either
3829                // (`ariaSort` stays `undefined` unless `allowsSorting`).
3830                .a11y_named(
3831                    a11y::Role::ColumnHeader,
3832                    &a11y::Name::labelled(column.label.clone()),
3833                )
3834                .a11y_column_index(column_index + usize::from(selectable))
3835                .track_focus(&header_focus[column_index])
3836                .relative()
3837                .when(header_focused[column_index], |c| {
3838                    c.child(crate::util::inset_focus_ring(cx))
3839                });
3840                crate::util::record_focus_bounds(
3841                    header_cell,
3842                    &header_focus[column_index],
3843                    window,
3844                    cx,
3845                )
3846                .into_any_element()
3847            }
3848        };
3849
3850        // `allowsResizing` puts a handle on the column's trailing edge. The
3851        // wrapper is what keeps the handle inside the column's box.
3852        // `.table__resizable-container` is the box that keeps the handle
3853        // inside the column, which is what this wrapper is.
3854        if column.allows_resizing {
3855            self.resizable_column_header(hx, column_index, column, cell, cx)
3856        } else {
3857            cell
3858        }
3859    }
3860
3861    /// `.table__resizable-container`: the box that keeps a resizable
3862    /// column's handle inside the column, with the handle's pointer drag
3863    /// start, outside-press exit and keyboard resizing.
3864    fn resizable_column_header(
3865        &self,
3866        hx: &HeaderParts<'_>,
3867        column_index: usize,
3868        column: &TableColumn,
3869        cell: AnyElement,
3870        cx: &mut App,
3871    ) -> AnyElement {
3872        let HeaderParts {
3873            base_id,
3874            colors,
3875            effective_widths,
3876            layout_width_bounds,
3877            measured_widths,
3878            measured_widths_now,
3879            resize_focus,
3880            resize_focused,
3881            resized,
3882            dragging,
3883            drag_now,
3884            keyboard_resizing,
3885            keyboard_resize_now,
3886            resize_limits,
3887            resize_columns,
3888            resize_measurements,
3889            ..
3890        } = *hx;
3891        let effective = effective_widths[column_index];
3892        let (layout_min, layout_max) = layout_width_bounds[column_index];
3893        let start_width = effective
3894            .or_else(|| measured_widths_now.get(column_index).copied().flatten())
3895            .unwrap_or(px(160.));
3896        let keyboard = keyboard_resizing.clone();
3897        let keyboard_out = keyboard.clone();
3898        let widths = resized.clone();
3899        let widths_for_outside = widths.clone();
3900        let (min_width, max_width) = resize_limits[column_index];
3901        let controlled = column.width.is_some();
3902        // As with the sort hover group: a string, but this table's own.
3903        let resizer_group: SharedString = format!("{}-resizer-{column_index}", self.id).into();
3904        let accent_color = colors.accent.color;
3905        let focus_color = colors.focus;
3906        let is_resizing = drag_now.is_some_and(|(index, _, _)| index == column_index)
3907            || keyboard_resize_now == Some(column_index);
3908        let measured = measured_widths.clone();
3909        let columns_for_outside = resize_columns.clone();
3910        let columns_for_keys = resize_columns.clone();
3911        let measurements_for_outside = resize_measurements.clone();
3912        let measurements_for_keys = resize_measurements.clone();
3913        let resize_start_for_keys = self.on_resize_start.clone();
3914        let resize_for_keys = self.on_resize.clone();
3915        let resize_end_for_outside = self.on_resize_end.clone();
3916        let resize_end_for_keys = self.on_resize_end.clone();
3917        let pointer_start = ResizePointerStart {
3918            column_index,
3919            start_width,
3920            held: dragging.clone(),
3921            keyboard: keyboard.clone(),
3922            widths: widths.clone(),
3923            columns: resize_columns.clone(),
3924            measurements: resize_measurements.clone(),
3925            focus: resize_focus[column_index]
3926                .as_ref()
3927                .expect("resizable columns have a focus handle")
3928                .clone(),
3929            on_resize_start: self.on_resize_start.clone(),
3930            on_resize_end: self.on_resize_end.clone(),
3931        };
3932        gpui::div()
3933            .relative()
3934            .when(effective.is_none(), flex_cell)
3935            .when_some(effective, |c, w| c.w(w))
3936            // The wrapper owns the column's track, so it carries the
3937            // shared bounds instead of leaning on the inner cell's
3938            // automatic minimum (see `layout_width_bounds`).
3939            .when_some(layout_min, |wrapper, w| wrapper.min_w(w))
3940            .when_some(layout_max, |wrapper, w| wrapper.max_w(w))
3941            .when(effective.is_none(), |wrapper| {
3942                wrapper.child(resize_width_probe(measured, column_index))
3943            })
3944            .child(cell)
3945            .child(
3946                gpui::div()
3947                    .id(element_id::indexed(base_id, "resize", column_index))
3948                    .track_focus(
3949                        resize_focus[column_index]
3950                            .as_ref()
3951                            .expect("resizable columns have a focus handle"),
3952                    )
3953                    .group(resizer_group.clone())
3954                    .absolute()
3955                    .top(px(0.))
3956                    // `px-2` around a `w-px` line, `box-content`: an
3957                    // 8px grab margin either side of the column edge.
3958                    .right(px(-8.))
3959                    .w(px(17.))
3960                    .h_full()
3961                    .flex()
3962                    .items_center()
3963                    .justify_center()
3964                    .child(
3965                        // `h-4 w-px rounded-sm bg-separator`, and
3966                        // `h-full w-0.5 bg-accent` while hovered.
3967                        gpui::div()
3968                            .w(px(1.))
3969                            .h(px(16.))
3970                            .rounded(crate::util::hairline_radius(cx))
3971                            .bg(colors.separator)
3972                            .group_hover(resizer_group, |s| {
3973                                s.w(px(2.)).h_full().bg(accent_color)
3974                            })
3975                            .when(is_resizing, |s| {
3976                                s.w(px(2.)).h_full().bg(accent_color)
3977                            })
3978                            .when(resize_focused[column_index], |s| {
3979                                s.w(px(2.)).h_full().bg(focus_color)
3980                            }),
3981                    )
3982                    .cursor(gpui::CursorStyle::ResizeLeftRight)
3983                    .on_mouse_down(gpui::MouseButton::Left, move |ev, window, cx| {
3984                        pointer_start.press(ev, window, cx);
3985                    })
3986                    .on_mouse_down_out(move |_, window, cx| {
3987                        if *keyboard_out.read(cx) == Some(column_index) {
3988                            let current = final_column_widths(
3989                                &columns_for_outside,
3990                                widths_for_outside.read(cx),
3991                                &measurements_for_outside,
3992                                column_index,
3993                            );
3994                            keyboard_out.update(cx, |active, cx| {
3995                                *active = None;
3996                                cx.notify();
3997                            });
3998                            clear_controlled_resize_proposal(
3999                                &columns_for_outside,
4000                                &widths_for_outside,
4001                                column_index,
4002                                cx,
4003                            );
4004                            if let Some(callback) = &resize_end_for_outside {
4005                                callback(&current, window, cx);
4006                            }
4007                        }
4008                    })
4009                    .on_key_down(move |event, window, cx| {
4010                        let key = event.keystroke.key.as_str();
4011                        let editing = *keyboard.read(cx) == Some(column_index);
4012                        match key {
4013                            "enter" => {
4014                                if editing {
4015                                    let current = final_column_widths(
4016                                        &columns_for_keys,
4017                                        widths.read(cx),
4018                                        &measurements_for_keys,
4019                                        column_index,
4020                                    );
4021                                    keyboard.update(cx, |active, cx| {
4022                                        *active = None;
4023                                        cx.notify();
4024                                    });
4025                                    clear_controlled_resize_proposal(
4026                                        &columns_for_keys,
4027                                        &widths,
4028                                        column_index,
4029                                        cx,
4030                                    );
4031                                    if let Some(callback) = &resize_end_for_keys {
4032                                        callback(&current, window, cx);
4033                                    }
4034                                } else {
4035                                    clear_controlled_resize_proposal(
4036                                        &columns_for_keys,
4037                                        &widths,
4038                                        column_index,
4039                                        cx,
4040                                    );
4041                                    let current = resolved_column_widths(
4042                                        &columns_for_keys,
4043                                        widths.read(cx),
4044                                        &measurements_for_keys,
4045                                        None,
4046                                    );
4047                                    keyboard.update(cx, |active, cx| {
4048                                        *active = Some(column_index);
4049                                        cx.notify();
4050                                    });
4051                                    if let Some(callback) = &resize_start_for_keys {
4052                                        callback(&current, window, cx);
4053                                    }
4054                                }
4055                                cx.stop_propagation();
4056                            }
4057                            "escape" | "space" | "tab" if editing => {
4058                                let current = final_column_widths(
4059                                    &columns_for_keys,
4060                                    widths.read(cx),
4061                                    &measurements_for_keys,
4062                                    column_index,
4063                                );
4064                                keyboard.update(cx, |active, cx| {
4065                                    *active = None;
4066                                    cx.notify();
4067                                });
4068                                clear_controlled_resize_proposal(
4069                                    &columns_for_keys,
4070                                    &widths,
4071                                    column_index,
4072                                    cx,
4073                                );
4074                                if let Some(callback) = &resize_end_for_keys {
4075                                    callback(&current, window, cx);
4076                                }
4077                                cx.stop_propagation();
4078                            }
4079                            "right" | "up" | "left" | "down" if editing => {
4080                                let delta = if matches!(key, "right" | "up") {
4081                                    10.
4082                                } else {
4083                                    -10.
4084                                };
4085                                let mut proposed = start_width;
4086                                widths.update(cx, |values, cx| {
4087                                    if values.len() <= column_index {
4088                                        values.resize(column_index + 1, None);
4089                                    }
4090                                    let current = if controlled {
4091                                        start_width
4092                                    } else {
4093                                        values[column_index].unwrap_or(start_width)
4094                                    };
4095                                    let next = (f32::from(current) + delta)
4096                                        .floor()
4097                                        .min(max_width)
4098                                        .max(min_width);
4099                                    proposed = px(next);
4100                                    values[column_index] = Some(proposed);
4101                                    cx.notify();
4102                                });
4103                                if let Some(callback) = &resize_for_keys {
4104                                    let current = resolved_column_widths(
4105                                        &columns_for_keys,
4106                                        widths.read(cx),
4107                                        &measurements_for_keys,
4108                                        Some((column_index, proposed)),
4109                                    );
4110                                    callback(&current, window, cx);
4111                                }
4112                                cx.stop_propagation();
4113                            }
4114                            _ => {}
4115                        }
4116                    }),
4117            )
4118            .into_any_element()
4119    }
4120
4121    /// The drag itself: the pointer can leave the table, so paint-time
4122    /// window listeners own the move and release until the drag ends.
4123    fn column_resize_drag(
4124        &self,
4125        mut table: gpui::Stateful<gpui::Div>,
4126        dragging: gpui::Entity<Option<(usize, f32, f32)>>,
4127        resized: gpui::Entity<Vec<Option<Pixels>>>,
4128        resize_columns: std::sync::Arc<Vec<TableColumn>>,
4129        resize_measurements: std::sync::Arc<Vec<Option<Pixels>>>,
4130    ) -> gpui::Stateful<gpui::Div> {
4131        let held = dragging.clone();
4132        let held_up = dragging;
4133        let widths = resized;
4134        let widths_up = widths.clone();
4135        let columns_for_move = resize_columns.clone();
4136        let columns_for_up = resize_columns;
4137        let measurements_for_move = resize_measurements.clone();
4138        let measurements_for_up = resize_measurements;
4139        let resize_callback = self.on_resize.clone();
4140        let resize_end_callback = self.on_resize_end.clone();
4141        table = table.relative().child(
4142            gpui::canvas(
4143                |bounds, _, _| bounds,
4144                move |_, _, window, _| {
4145                    let held = held.clone();
4146                    let widths = widths.clone();
4147                    let columns_for_move = columns_for_move.clone();
4148                    let measurements_for_move = measurements_for_move.clone();
4149                    let resize_callback = resize_callback.clone();
4150                    window.on_mouse_event(
4151                        move |event: &gpui::MouseMoveEvent, phase, window, cx| {
4152                            if phase != gpui::DispatchPhase::Capture
4153                                || event.pressed_button != Some(gpui::MouseButton::Left)
4154                            {
4155                                return;
4156                            }
4157                            let Some((column, from_x, from_w)) = *held.read(cx) else {
4158                                return;
4159                            };
4160                            let raw = (from_w + f32::from(event.position.x) - from_x).floor();
4161                            let min = columns_for_move[column]
4162                                .min_width
4163                                .map_or(DEFAULT_COLUMN_MIN_WIDTH, f32::from);
4164                            let max = columns_for_move[column]
4165                                .max_width
4166                                .map_or(f32::MAX, f32::from);
4167                            let proposed = px(raw.min(max).max(min));
4168                            widths.update(cx, |values, cx| {
4169                                if values.len() <= column {
4170                                    values.resize(column + 1, None);
4171                                }
4172                                values[column] = Some(proposed);
4173                                cx.notify();
4174                            });
4175                            if let Some(callback) = &resize_callback {
4176                                let current = resolved_column_widths(
4177                                    &columns_for_move,
4178                                    widths.read(cx),
4179                                    &measurements_for_move,
4180                                    Some((column, proposed)),
4181                                );
4182                                callback(&current, window, cx);
4183                            }
4184                        },
4185                    );
4186
4187                    let held_up = held_up.clone();
4188                    let widths_up = widths_up.clone();
4189                    let columns_for_up = columns_for_up.clone();
4190                    let measurements_for_up = measurements_for_up.clone();
4191                    let resize_end_callback = resize_end_callback.clone();
4192                    window.on_mouse_event(move |event: &gpui::MouseUpEvent, phase, window, cx| {
4193                        if phase != gpui::DispatchPhase::Capture
4194                            || event.button != gpui::MouseButton::Left
4195                        {
4196                            return;
4197                        }
4198                        let drag = *held_up.read(cx);
4199                        if let Some((column, _, _)) = drag {
4200                            let current = final_column_widths(
4201                                &columns_for_up,
4202                                widths_up.read(cx),
4203                                &measurements_for_up,
4204                                column,
4205                            );
4206                            held_up.update(cx, |value, cx| {
4207                                *value = None;
4208                                cx.notify();
4209                            });
4210                            clear_controlled_resize_proposal(
4211                                &columns_for_up,
4212                                &widths_up,
4213                                column,
4214                                cx,
4215                            );
4216                            if let Some(callback) = &resize_end_callback {
4217                                callback(&current, window, cx);
4218                            }
4219                        }
4220                    });
4221                },
4222            )
4223            .absolute()
4224            .inset_0(),
4225        );
4226        table
4227    }
4228
4229    /// v3 gives a table a roving row focus: the arrows walk it, Home and End
4230    /// jump, Page Up/Down page (PageUp leaves for the header), Enter
4231    /// activates the row, Space and `Mod+A` select, a tree row opens and
4232    /// closes, and typing jumps by typeahead -- the same resolver every list
4233    /// here uses, over the rows that exist.
4234    fn row_keyboard(&self, mut wrapper: gpui::Div, keys: RowKeyboard<'_>) -> gpui::Div {
4235        let RowKeyboard {
4236            ctx,
4237            expanded_keys,
4238            header_focus,
4239            row_cursor,
4240            selectable_collection_keys,
4241            selection_own,
4242            selection_range,
4243            table_focus,
4244            tree_rows,
4245            typeahead_labels,
4246            typeahead,
4247            virtual_list_state,
4248            virtual_row_heights,
4249            virtual_scroll_now,
4250            virtual_text_value,
4251            virtual_typeahead_indices,
4252        } = keys;
4253        let stops: Vec<usize> = ctx
4254            .row_keys
4255            .iter()
4256            .enumerate()
4257            .filter_map(|(index, key)| (!self.disabled_keys.contains(key)).then_some(index))
4258            .collect();
4259        let keys = ctx.row_keys.clone();
4260        if keys.is_empty() {
4261            return wrapper;
4262        }
4263        let mode = self.selection_mode;
4264        let selection_behavior = self.selection_behavior;
4265        let fixed_virtual = self.row_height.is_some() && self.virtual_rows.is_some();
4266        let fixed_scroll = virtual_scroll_now.clone();
4267        let variable_scroll = virtual_list_state.clone();
4268        let typeahead_navigation = std::rc::Rc::new(TableTypeaheadNavigation {
4269            state: typeahead,
4270            labels: typeahead_labels,
4271            virtual_indices: virtual_typeahead_indices,
4272            virtual_text: virtual_text_value,
4273            stops: stops.clone(),
4274            keys: keys.clone(),
4275            cursor: row_cursor.clone(),
4276            fixed_virtual,
4277            fixed_scroll: fixed_scroll.clone(),
4278            variable_scroll: variable_scroll.clone(),
4279        });
4280        let capture_typeahead = typeahead_navigation.clone();
4281        let capture_typeahead_up = typeahead_navigation.clone();
4282        let capture_focus = table_focus.clone();
4283        let capture_focus_up = table_focus.clone();
4284        wrapper = wrapper.capture_key_down(move |event, window, cx| {
4285            if !capture_focus.contains_focused(window, cx) {
4286                return;
4287            }
4288            let key_name = event.keystroke.key.as_str();
4289            let typed = event.keystroke.key_char.as_deref().unwrap_or(key_name);
4290            let modifiers = &event.keystroke.modifiers;
4291            let now = web_time::Instant::now();
4292            let is_space = key_name == "space" || typed == " ";
4293            if is_space
4294                && capture_typeahead.state.read(cx).is_active(now)
4295                && !modifiers.control
4296                && !modifiers.platform
4297                && !modifiers.alt
4298            {
4299                cx.stop_propagation();
4300                capture_typeahead.push(" ", now, false, cx);
4301            }
4302        });
4303        wrapper = wrapper.capture_key_up(move |event, window, cx| {
4304            if !capture_focus_up.contains_focused(window, cx) {
4305                return;
4306            }
4307            let key_name = event.keystroke.key.as_str();
4308            let typed = event.keystroke.key_char.as_deref().unwrap_or(key_name);
4309            let modifiers = &event.keystroke.modifiers;
4310            let is_space = key_name == "space" || typed == " ";
4311            if is_space
4312                && capture_typeahead_up
4313                    .state
4314                    .read(cx)
4315                    .is_active(web_time::Instant::now())
4316                && !modifiers.control
4317                && !modifiers.platform
4318                && !modifiers.alt
4319            {
4320                cx.stop_propagation();
4321            }
4322        });
4323        let nav = RowKeyNav {
4324            stops,
4325            held: row_cursor,
4326            keys,
4327            table_focus,
4328            typeahead: typeahead_navigation,
4329            on_row_click: self.on_row_click.clone(),
4330            selection: self.on_selection_change.clone(),
4331            selection_own,
4332            selection_range,
4333            selected_now: self.selected_keys.clone(),
4334            selectable_collection_keys,
4335            mode,
4336            selection_behavior,
4337            disallow_empty_selection: self.disallow_empty_selection,
4338            // React Aria's collection hook defaults `selectOnFocus` to true
4339            // for `selectionBehavior="replace"`.
4340            select_on_focus: selection_behavior == SelectionBehavior::Replace
4341                && mode != SelectionMode::None,
4342            plain_rows: self.virtual_rows.is_none(),
4343            fixed_virtual,
4344            // Pinned `TableKeyboardDelegate` pages by one visible rectangle, so
4345            // the step reads the virtual body's own laid-out viewport -- the
4346            // uniform handle's `viewport_bounds()` -- and not the configured
4347            // `max_h` cap: a bounded parent (or a resized window) shows fewer
4348            // rows than the cap allows. A zero viewport answers nothing, which
4349            // the shared resolver turns into no movement.
4350            fixed_row_height: self.row_height.filter(|_| fixed_virtual),
4351            fixed_scroll,
4352            variable_scroll,
4353            variable_heights: virtual_row_heights.clone(),
4354            variable_estimate: self.estimated_row_height,
4355            expanded: expanded_keys,
4356            on_expanded: self.on_expanded_change.clone(),
4357            tree_rows,
4358            // The header PageUp hands the focus to. Cloned out because the
4359            // handler outlives this frame's `header_focus`.
4360            page_up_header: header_focus.first().cloned(),
4361            headers: header_focus,
4362        };
4363        wrapper.on_key_down(move |event, window, cx| nav.key_down(event, window, cx))
4364    }
4365
4366    /// Fills `.table__body`: a uniform [`VirtualList`](crate::VirtualList) for fixed-height virtual rows, a
4367    /// measured [`VirtualList`](crate::VirtualList) for `estimatedRowHeight`, or every flattened
4368    /// plain row.
4369    fn body_rows(
4370        &self,
4371        mut body: gpui::Stateful<gpui::Div>,
4372        rows_in: BodyRows<'_>,
4373        window: &Window,
4374        cx: &mut App,
4375    ) -> gpui::Stateful<gpui::Div> {
4376        let BodyRows {
4377            base_id,
4378            table_id,
4379            ctx,
4380            flat,
4381            row_count,
4382            virtual_projection,
4383            virtual_list_state,
4384            virtual_row_heights,
4385            virtual_scroll_now,
4386        } = rows_in;
4387        // `estimatedRowHeight` takes the variable-height path: gpui's `list`
4388        // measures each row it builds, and its state is intrusive, so it lives in
4389        // the window's keyed store and resets when the count changes.
4390        if let (Some(row_height), Some((_, _, _, factory))) =
4391            (self.row_height, self.virtual_rows.clone())
4392        {
4393            // The body scrolls inside a uniform `VirtualList`, which asks for the rows the
4394            // viewport shows and no others. A fixed height caps the roomy-window
4395            // body at the configured value, so an unbounded (natural-height)
4396            // page parent sizes the whole table to header + cap + padding
4397            // instead of the rows' full natural height; `min_h_0` lets that
4398            // fixed height shrink as a flex item with a bounded parent, handing
4399            // the virtual body its real viewport. (`Infer` + `max_h` caps the
4400            // list's own bounds but reports the full natural height upward, so
4401            // the outer table stretches white past the shown rows.)
4402            let height = self.max_h.unwrap_or(px(400.));
4403            let rows = ctx.clone();
4404            let projection = virtual_projection;
4405            // The headless probe name for the virtual viewport's bounds.
4406            let rows_selector = format!("{table_id}-virtual-rows");
4407            let handle = virtual_scroll_now;
4408            if handle.item_count() != row_count {
4409                handle.splice(0..handle.item_count(), row_count);
4410            }
4411            body = body.child(
4412                crate::VirtualList::new(
4413                    element_id::scoped(base_id, "virtual-rows"),
4414                    handle,
4415                    move |i, _window, cx| {
4416                        let (source_index, key, metadata) = &projection[i];
4417                        let row_data = factory(*source_index);
4418                        rows.row(
4419                            i,
4420                            row_data,
4421                            metadata.depth,
4422                            metadata.has_children,
4423                            key,
4424                            Some(row_height),
4425                            _window,
4426                            cx,
4427                        )
4428                    },
4429                )
4430                .height(height)
4431                .debug_selector(rows_selector)
4432                .when(
4433                    ctx.frozen.is_some(),
4434                    crate::VirtualList::restrict_scroll_to_axis,
4435                ),
4436            );
4437        } else if let (Some(state), Some((_, _, _, factory))) =
4438            (virtual_list_state, self.virtual_rows.clone())
4439        {
4440            let height = self.max_h.unwrap_or(px(400.));
4441            let rows = ctx.clone();
4442            let projection = virtual_projection;
4443            let measured_heights =
4444                virtual_row_heights.expect("estimated row height creates a measurement store");
4445            body = body.child(
4446                crate::VirtualList::new(
4447                    element_id::scoped(base_id, "variable-rows"),
4448                    &crate::VirtualListHandle::from_list_state(state),
4449                    move |i, _window, cx| {
4450                        let (source_index, key, metadata) = &projection[i];
4451                        let row_data = factory(*source_index);
4452                        let row = rows.row(
4453                            i,
4454                            row_data,
4455                            metadata.depth,
4456                            metadata.has_children,
4457                            key,
4458                            None,
4459                            _window,
4460                            cx,
4461                        );
4462                        let measured = measured_heights.clone();
4463                        gpui::div()
4464                            .relative()
4465                            .w_full()
4466                            .child(row)
4467                            .child(
4468                                gpui::canvas(
4469                                    move |bounds: gpui::Bounds<Pixels>, _, cx| {
4470                                        measured.update(cx, |(_, heights), cx| {
4471                                            if heights.get(i).copied().flatten()
4472                                                != Some(bounds.size.height)
4473                                            {
4474                                                heights[i] = Some(bounds.size.height);
4475                                                cx.notify();
4476                                            }
4477                                        });
4478                                        bounds
4479                                    },
4480                                    |_, _, _, _| {},
4481                                )
4482                                .absolute()
4483                                .inset_0(),
4484                            )
4485                            .into_any_element()
4486                    },
4487                )
4488                .height(height),
4489            );
4490        } else {
4491            for (i, (row_data, depth, has_children, tree_key, _)) in flat.into_iter().enumerate() {
4492                body = body.child(ctx.row(
4493                    i,
4494                    row_data,
4495                    depth,
4496                    has_children,
4497                    &tree_key,
4498                    None,
4499                    window,
4500                    cx,
4501                ));
4502            }
4503        }
4504
4505        body
4506    }
4507
4508    /// The load-more sentinel: a 1px canvas that reports `onLoadMore` when
4509    /// the end of the collection nears the viewport, and the pending row
4510    /// (`.table__load-more-content`) while `is_pending`.
4511    fn load_more(
4512        &self,
4513        mut table: gpui::Stateful<gpui::Div>,
4514        load_more: LoadMore<'_>,
4515    ) -> gpui::Stateful<gpui::Div> {
4516        let LoadMore {
4517            base_id,
4518            state,
4519            collection: load_more_collection,
4520            virtual_scroll,
4521            variable_scroll,
4522            muted,
4523            loading_text,
4524        } = load_more;
4525        if let Some(cb) = self.on_load_more.clone() {
4526            let scroll_offset = self.load_more_offset;
4527            table = table.child(
4528                gpui::canvas(
4529                    move |bounds, window, cx| {
4530                        // Plain rows place this canvas at the real content
4531                        // end, so ancestor masks provide its viewport
4532                        // intersection. Virtual rows keep that end inside
4533                        // their own scroll state instead.
4534                        let mask = window.content_mask().bounds;
4535                        let in_view = bounds.right() >= mask.left()
4536                            && bounds.left() <= mask.right()
4537                            && bounds.bottom() >= mask.top()
4538                            && bounds.top() <= mask.bottom() + mask.size.height * scroll_offset;
4539                        let virtual_end_is_near = if let Some(handle) = &virtual_scroll {
4540                            // The uniform body knows its tail exactly: the
4541                            // row count times the measured row, less the
4542                            // scroll offset and the viewport.
4543                            let viewport_height = handle.viewport_bounds().size.height;
4544                            handle.remaining_below().is_some_and(|remaining| {
4545                                remaining <= viewport_height * scroll_offset
4546                            })
4547                        } else if let Some((state, count, estimate)) = &variable_scroll {
4548                            let viewport = state.viewport_bounds();
4549                            let viewport_height = viewport.size.height;
4550                            let top = state.logical_scroll_top();
4551                            // Unmeasured ListState rows have zero height,
4552                            // so project the unseen tail from the caller's
4553                            // layout estimate instead.
4554                            let remaining = *estimate * count.saturating_sub(top.item_ix)
4555                                - top.offset_in_item
4556                                - viewport_height;
4557                            let margin = viewport_height * scroll_offset;
4558                            let measured_end_is_near = *count == 0
4559                                || state.bounds_for_item(count.saturating_sub(1)).is_some_and(
4560                                    |bounds| bounds.bottom() <= viewport.bottom() + margin,
4561                                );
4562                            measured_end_is_near || (scroll_offset > 0. && remaining <= margin)
4563                        } else {
4564                            true
4565                        };
4566                        let visible = if virtual_scroll.is_some() || variable_scroll.is_some() {
4567                            virtual_end_is_near
4568                        } else {
4569                            in_view
4570                        };
4571                        let previous = state.read(cx).clone();
4572                        let collection_changed = previous
4573                            .1
4574                            .as_ref()
4575                            .is_none_or(|identity| identity != &load_more_collection);
4576                        let should_load = visible && (!previous.0 || collection_changed);
4577                        if previous.0 != visible || should_load {
4578                            state.update(cx, |value, cx| {
4579                                value.0 = visible;
4580                                if should_load {
4581                                    value.1 = Some(load_more_collection.clone());
4582                                }
4583                                cx.notify();
4584                            });
4585                        }
4586                        if should_load {
4587                            cb(window, cx);
4588                        }
4589                    },
4590                    |_, _, _, _| {},
4591                )
4592                .size(px(1.)),
4593            );
4594        }
4595
4596        if self.is_pending {
4597            let sentinel = gpui::div()
4598            .id(element_id::scoped(base_id, "load-more"))
4599            .flex()
4600            .items_center()
4601            .justify_center()
4602            .gap(px(8.))
4603            .w_full()
4604            // `.table__load-more-content` is `gap-2 py-2`; `loaderHeight`
4605            // fixes the row's height instead when the caller sets it.
4606            .py(px(8.))
4607            .when_some(self.loader_height, |el, h| el.h(h))
4608            .text_size(px(13.))
4609            .text_color(muted)
4610                .child(
4611                    crate::spinner::Spinner::new(element_id::scoped(base_id, "load-spinner"))
4612                        .size(herogpui_core::Size::Sm),
4613                )
4614                .child(loading_text);
4615            table = table.child(sentinel);
4616        }
4617        table
4618    }
4619
4620    /// Which of the row run's edges coincide with a curved edge of the box
4621    /// that clips it. A virtual body only meets its box at an edge it is
4622    /// actually scrolled to; an unresolved scroll state answers "no", which
4623    /// keeps a square fill rather than a wrongly rounded one.
4624    fn row_edge_rounding(
4625        &self,
4626        secondary: bool,
4627        virtual_scroll_now: &crate::VirtualListHandle,
4628        virtual_list_state: Option<&gpui::ListState>,
4629        virtual_visible_count: usize,
4630        cx: &App,
4631    ) -> (Option<Pixels>, Option<Pixels>) {
4632        let (touches_top, touches_bottom) =
4633            if self.row_height.is_some() && self.virtual_rows.is_some() {
4634                // A list that does not scroll at all answers "at the end":
4635                // every row is on screen.
4636                (
4637                    virtual_scroll_now.is_scrolled_to_top(),
4638                    virtual_scroll_now.is_scrolled_to_end(),
4639                )
4640            } else if let Some(state) = virtual_list_state {
4641                let top = state.logical_scroll_top();
4642                let viewport = state.viewport_bounds();
4643                (
4644                    top.item_ix == 0 && top.offset_in_item <= px(0.5),
4645                    virtual_visible_count == 0
4646                        || state
4647                            .bounds_for_item(virtual_visible_count - 1)
4648                            .is_some_and(|bounds| bounds.bottom() <= viewport.bottom() + px(0.5)),
4649                )
4650            } else {
4651                // Plain rows have no scroller of their own, so the run's
4652                // edges are the box's edges.
4653                (true, true)
4654            };
4655        edge_corner_rounding(
4656            secondary,
4657            self.footer.is_some(),
4658            touches_top,
4659            touches_bottom,
4660            cx.layout().radius_2xl().min(px(32.)),
4661            self.radius
4662                .unwrap_or_else(|| crate::util::container_radius(cx)),
4663        )
4664    }
4665}
4666
4667/// Everything one body row needs, so both the plain and the virtualized path can
4668/// draw it from the same code.
4669/// Which corners of a body row's full-bleed fill must be rounded so the fill
4670/// does not square the curved edge it sits against.
4671///
4672/// Vanilla GPUI clips an `overflow_hidden()` child to its parent's rectangle
4673/// rather than to the parent's rounded corners, so the fill has to carry the
4674/// curve itself. Which box owns that curve differs by variant: the primary
4675/// variant's rows sit in the rounded `.table__body` block, while the
4676/// secondary variant is flat and the wrapper's own corners are the only
4677/// curve — and there the header sits above the rows, so the top corners are
4678/// never the rows', and a footer takes the bottom ones when one is present.
4679fn edge_corner_rounding(
4680    secondary: bool,
4681    has_footer: bool,
4682    touches_top: bool,
4683    touches_bottom: bool,
4684    body_radius: Pixels,
4685    wrapper_radius: Pixels,
4686) -> (Option<Pixels>, Option<Pixels>) {
4687    if secondary {
4688        (
4689            None,
4690            (touches_bottom && !has_footer).then_some(wrapper_radius),
4691        )
4692    } else {
4693        (
4694            touches_top.then_some(body_radius),
4695            touches_bottom.then_some(body_radius),
4696        )
4697    }
4698}
4699
4700struct RowCtx {
4701    /// The table's id, so one table's row ids cannot collide with another's.
4702    /// Every row part is scoped off it.
4703    id: gpui::ElementId,
4704    /// Intrinsic content widths for non-resizable columns. The table uses
4705    /// these as shared minimum tracks so a long body cell cannot make its row
4706    /// disagree with the header or its siblings.
4707    measured_widths: gpui::Entity<Vec<Option<Pixels>>>,
4708    measure_intrinsic: Vec<bool>,
4709    /// `(defaultWidth or the resize, minWidth, maxWidth)` per column.
4710    widths: Vec<(Option<Pixels>, Option<Pixels>, Option<Pixels>)>,
4711    row_header_columns: Vec<bool>,
4712    tree_column: usize,
4713    tree_column_has_children: bool,
4714    selectable: bool,
4715    selection_mode: SelectionMode,
4716    selection_behavior: SelectionBehavior,
4717    selected_keys: Vec<SharedString>,
4718    row_keys: Vec<SharedString>,
4719    disabled_keys: Vec<SharedString>,
4720    disallow_empty_selection: bool,
4721    expanded: std::rc::Rc<Vec<SharedString>>,
4722    on_expanded_change: Option<OnExpandedChange>,
4723    on_selection_change: Option<OnSelectionChange>,
4724    selection_own: Option<gpui::Entity<Vec<SharedString>>>,
4725    selection_range: gpui::Entity<TableSelectionRange>,
4726    on_row_click: Option<OnRowClick>,
4727    focus: gpui::FocusHandle,
4728    cursor_own: gpui::Entity<Option<SharedString>>,
4729    /// The row the keyboard is on, which wears `status-focused`.
4730    cursor: Option<usize>,
4731    /// The `.table-root--secondary` flat layout, whose row hover is a
4732    /// different token from the primary's.
4733    secondary: bool,
4734    /// The caller's unselected-row hover fill, when one was named. Selected
4735    /// rows keep their selection fill regardless.
4736    row_hover_bg: Option<gpui::Hsla>,
4737    /// Prefix for per-row hover groups. HeroUI paints hover and selected
4738    /// surfaces on each cell, allowing the cell's radius/clip to own the
4739    /// resulting geometry instead of leaking a row-level fill through gaps.
4740    hover_group_prefix: SharedString,
4741    /// Whether the collection is windowed, which is the guard upstream puts
4742    /// `aria-rowindex` behind (`.../grid/useGridRow.mjs`).
4743    virtualized: bool,
4744    /// The radius the first row's outer cells must round their fill to, when
4745    /// the top of the row run really is the curved edge of its box. `None`
4746    /// leaves the fill square, which is what a scrolled virtual body needs.
4747    round_top: Option<Pixels>,
4748    /// The same for the last row's outer cells and the bottom edge.
4749    round_bottom: Option<Pixels>,
4750    /// Whether the collection is a tree, which is what makes
4751    /// `useTableRow.mjs` add `aria-level` and `aria-expanded` to a row.
4752    is_tree: bool,
4753    /// Cell selection (HeroGPUI extension), when enabled.
4754    cell: Option<CellCtx>,
4755    /// Frozen leading columns (HeroGPUI extension), when any scroll.
4756    frozen: Option<FrozenColumns>,
4757}
4758
4759/// One body row's per-render facts, which every part of the row reads.
4760struct RowPaint {
4761    i: usize,
4762    key: SharedString,
4763    depth: usize,
4764    has_children: bool,
4765    is_selected: bool,
4766    is_disabled: bool,
4767    is_expanded: bool,
4768    selected_bg: Option<gpui::Hsla>,
4769    hover_bg: Option<gpui::Hsla>,
4770    hover_group: SharedString,
4771    /// The row the keyboard is on.
4772    row_focused: bool,
4773    data_count: usize,
4774    round_top: Option<Pixels>,
4775    round_bottom: Option<Pixels>,
4776}
4777
4778impl RowCtx {
4779    /// One body row.
4780    ///
4781    /// `fixed_h` is `Some` only on the virtual path, where every row is one
4782    /// `rowHeight` tall because that is the number the scroll geometry comes
4783    /// from.
4784    #[allow(clippy::too_many_arguments)]
4785    /// One `.table__row`: `relative h-full` with a `border-separator/50`
4786    /// bottom edge except on the final visible row, and `.table__cell`s inside
4787    /// it.
4788    fn row(
4789        &self,
4790        i: usize,
4791        row_data: TableRow,
4792        depth: usize,
4793        has_children: bool,
4794        tree_key: &SharedString,
4795        fixed_h: Option<Pixels>,
4796        window: &Window,
4797        cx: &mut App,
4798    ) -> AnyElement {
4799        let colors = cx.colors();
4800        let key = tree_key.clone();
4801        let is_selected = self.selected_keys.contains(&key);
4802        let is_disabled = self.disabled_keys.contains(&key);
4803        let selected_bg = is_selected.then(|| colors.surface.background.alpha(0.1));
4804        let hover_bg = (!is_selected && !is_disabled).then(|| {
4805            self.row_hover_bg.unwrap_or(if self.secondary {
4806                colors.default.color.alpha(0.5)
4807            } else {
4808                colors.surface.background.alpha(0.4)
4809            })
4810        });
4811        let hover_group: SharedString = format!("{}-row-hover-{i}", self.hover_group_prefix).into();
4812        let row_focused = self.cursor == Some(i);
4813        let row_anchor = self.cursor_own.read(cx).as_ref() == Some(tree_key);
4814
4815        let mut row = gpui::div()
4816            .id(element_id::indexed(&self.id, "row", i))
4817            // `useTableRow.mjs` delegates to `.../grid/useGridRow.mjs`:
4818            // `role: 'row'` with `'aria-selected': selectionMode !== 'none' ?
4819            // isSelected : undefined`, and `aria-rowindex` only
4820            // `if (isVirtualized)`. On a tree table it then adds
4821            // `'aria-expanded': hasChildRows ? ... : undefined` and
4822            // `'aria-level': treeNode.level + 1`.
4823            //
4824            // The `aria-posinset`/`aria-setsize` half of that tree block is
4825            // *not* stated, and not because gpui lacks the builders: upstream
4826            // counts a row's siblings out of the tree collection, and what
4827            // reaches this function is the flattened list of *visible* rows
4828            // plus this row's own depth. The sibling count at that depth is
4829            // not derivable from it, and a guess is worse than silence.
4830            .a11y(a11y::Role::Row)
4831            .when(self.selection_mode != SelectionMode::None, |row| {
4832                row.a11y_selected(is_selected)
4833            })
4834            .when(self.virtualized, |row| row.a11y_row_index(i))
4835            .when(self.is_tree, |row| row.a11y_level(depth))
4836            .when(self.is_tree && has_children, |row| {
4837                row.a11y_expanded(self.expanded.iter().any(|k| k == tree_key))
4838            })
4839            .flex()
4840            .group(hover_group.clone())
4841            // A virtual row is laid out on its own, so it takes the width it is
4842            // *given*: without `w_full` the columns bunch at the left edge. That
4843            // is true of both virtual paths, so it is not conditional on the
4844            // fixed height any more -- `gpui::list`'s rows have none.
4845            .w_full()
4846            .when_some(fixed_h, |e, h| e.h(h));
4847        if !self.secondary && i + 1 < self.row_keys.len() {
4848            row = row.border_b_1().border_color(colors.separator.alpha(0.5));
4849        }
4850
4851        let data_count = row_data.cells.len();
4852        // The row's fill is painted per cell, so the outer cells of the first
4853        // and last rows carry the box's curve on the edge they touch.
4854        let round_top = (i == 0).then_some(self.round_top).flatten();
4855        let round_bottom = (i + 1 == self.row_keys.len())
4856            .then_some(self.round_bottom)
4857            .flatten();
4858        let paint = RowPaint {
4859            i,
4860            key,
4861            depth,
4862            has_children,
4863            is_selected,
4864            is_disabled,
4865            is_expanded: self.expanded.iter().any(|k| k == tree_key),
4866            selected_bg,
4867            hover_bg,
4868            hover_group,
4869            row_focused,
4870            data_count,
4871            round_top,
4872            round_bottom,
4873        };
4874
4875        if let Some(frozen) = &self.frozen {
4876            row = self.frozen_row_parts(row, frozen, &paint, row_data, cx);
4877        } else {
4878            if self.selectable {
4879                row = row.child(self.select_cell(&paint, cx));
4880            }
4881
4882            // Cells are flex rows so inline children (chips, buttons) size to
4883            // their content instead of stretching to the column width.
4884            let cx: &App = cx;
4885            row = row.children(
4886                row_data
4887                    .cells
4888                    .into_iter()
4889                    .enumerate()
4890                    .map(|(c, cell)| self.data_cell(&paint, c, cell, cx)),
4891            );
4892        }
4893        row = self.row_press(row, &paint, cx);
4894
4895        // v3 rings the focused row *inside* itself: each cell carries its own
4896        // inset strips so the outline stays clipped to the cell geometry.
4897        row = row.when(is_disabled, |row| row.opacity(cx.layout().disabled_opacity));
4898        if row_anchor && self.focus.is_focused(window) {
4899            row = crate::util::record_focus_bounds(row, &self.focus, window, cx);
4900        }
4901        row.into_any_element()
4902    }
4903
4904    /// A frozen table's row: the selection cell and the frozen cells in a
4905    /// part that stays put, the rest in a part that scrolls with the header.
4906    /// Both stay children of the row, which keeps the row's hover, press and
4907    /// cursor spanning the two.
4908    fn frozen_row_parts(
4909        &self,
4910        row: gpui::Stateful<gpui::Div>,
4911        frozen: &FrozenColumns,
4912        paint: &RowPaint,
4913        row_data: TableRow,
4914        cx: &App,
4915    ) -> gpui::Stateful<gpui::Div> {
4916        let data_count = row_data.cells.len();
4917        let mut lead = frozen.frozen_part();
4918        if self.selectable {
4919            lead = lead.child(self.select_cell(paint, cx));
4920        }
4921        let mut cells = row_data.cells.into_iter().enumerate();
4922        let lead = lead.children(
4923            cells
4924                .by_ref()
4925                .take(frozen.count)
4926                .map(|(c, cell)| self.data_cell(paint, c, cell, cx)),
4927        );
4928        let lead =
4929            lead.children((data_count..frozen.count).map(|c| self.empty_frozen_track(paint.i, c)));
4930        let rest = frozen
4931            .scrolling_part(
4932                gpui::div(),
4933                element_id::indexed(&self.id, "row-scroll", paint.i),
4934            )
4935            .children(cells.map(|(c, cell)| self.data_cell(paint, c, cell, cx)))
4936            .children(
4937                (data_count.max(frozen.count)..self.widths.len())
4938                    .map(|c| self.empty_frozen_track(paint.i, c)),
4939            );
4940        row.child(lead).child(rest)
4941    }
4942
4943    /// Keep every row's shared horizontal scroll extent aligned with the
4944    /// header, even when a caller supplies fewer cells than columns. The
4945    /// empty track has no accessibility node or interaction target.
4946    fn empty_frozen_track(&self, row: usize, column: usize) -> gpui::Stateful<gpui::Div> {
4947        let (width, min_width, max_width) = self.widths[column];
4948        gpui::div()
4949            .when(width.is_none(), flex_cell)
4950            .when_some(width, |cell, width| cell.w(width))
4951            .when_some(min_width, |cell, width| cell.min_w(width))
4952            .when_some(max_width, |cell, width| cell.max_w(width))
4953            .id(element_id::indexed(
4954                &element_id::indexed(&self.id, "row", row),
4955                "empty-cell",
4956                column,
4957            ))
4958    }
4959
4960    /// The selection column's body cell and its checkbox.
4961    fn select_cell(&self, paint: &RowPaint, cx: &App) -> gpui::Stateful<gpui::Div> {
4962        let RowPaint {
4963            i,
4964            is_selected,
4965            is_disabled,
4966            selected_bg,
4967            hover_bg,
4968            row_focused,
4969            data_count,
4970            round_top,
4971            round_bottom,
4972            ..
4973        } = *paint;
4974        let hover_group = &paint.hover_group;
4975        let mut cell = gpui::div()
4976            .id(element_id::indexed(&self.id, "select-cell", i))
4977            // The selection column's body cell. `useTableCell.mjs` gives
4978            // every data cell `.../grid/useGridCell.mjs`'s
4979            // `role: 'gridcell'` and `'aria-colindex': colIndex + 1`.
4980            .a11y(a11y::Role::GridCell)
4981            .a11y_column_index(0)
4982            .flex()
4983            .items_center()
4984            .justify_center()
4985            .relative()
4986            .w(px(44.))
4987            .py(px(10.))
4988            .when_some(selected_bg, |cell, bg| cell.bg(bg))
4989            .when_some(hover_bg, |cell, bg| {
4990                cell.group_hover(hover_group.clone(), move |s| s.bg(bg))
4991            })
4992            .when_some(round_top, |cell, r| cell.rounded_tl(r))
4993            .when_some(round_bottom, |cell, r| cell.rounded_bl(r))
4994            .when(data_count == 0, |cell| {
4995                cell.when_some(round_top, |cell, r| cell.rounded_tr(r))
4996                    .when_some(round_bottom, |cell, r| cell.rounded_br(r))
4997            })
4998            .on_mouse_down(gpui::MouseButton::Left, |_, _, cx| {
4999                cx.stop_propagation();
5000            });
5001        let mut box_el = Checkbox::new(element_id::indexed(&self.id, "select", i))
5002            .is_selected(is_selected)
5003            .is_disabled(is_disabled);
5004        let cb = self.on_selection_change.clone();
5005        if !is_disabled && (cb.is_some() || self.selection_own.is_some()) {
5006            let current = self.selected_keys.clone();
5007            let key2 = paint.key.clone();
5008            let mode = self.selection_mode;
5009            let disallow_empty_selection = self.disallow_empty_selection;
5010            let selection_own = self.selection_own.clone();
5011            let selection_range = self.selection_range.clone();
5012            box_el = box_el.on_change(move |_next, window, cx| {
5013                cx.stop_propagation();
5014                // A checkbox is the explicit selection affordance. React
5015                // Aria toggles it even when the surrounding row uses
5016                // `selectionBehavior="replace"`.
5017                let next = crate::selection::next_selection(
5018                    &current,
5019                    &key2,
5020                    mode,
5021                    disallow_empty_selection,
5022                );
5023                if let Some(held) = &selection_own {
5024                    held.update(cx, |value, cx| {
5025                        *value = next.clone();
5026                        cx.notify();
5027                    });
5028                }
5029                if let Some(cb) = &cb {
5030                    cb(&next, window, cx);
5031                }
5032                if mode == SelectionMode::Multiple && !is_selected {
5033                    selection_range.update(cx, |range, _| {
5034                        range.anchor = Some(key2.clone());
5035                        range.current = Some(key2.clone());
5036                        range.is_all = false;
5037                    });
5038                } else if mode == SelectionMode::Multiple {
5039                    selection_range.update(cx, |range, _| {
5040                        if range.is_all {
5041                            *range = TableSelectionRange::default();
5042                        }
5043                    });
5044                }
5045            });
5046        }
5047        cell = cell.child(box_el);
5048        if row_focused {
5049            cell = cell.child(table_cell_focus_ring(cx, true, data_count == 0));
5050        }
5051        cell
5052    }
5053
5054    /// One `.table__cell`: the column's shared track, the row's fills, the
5055    /// tree column's indent and chevron, and the cell-selection decoration.
5056    fn data_cell(
5057        &self,
5058        paint: &RowPaint,
5059        c: usize,
5060        cell: AnyElement,
5061        cx: &App,
5062    ) -> gpui::Stateful<gpui::Div> {
5063        let colors = cx.colors();
5064        let RowPaint {
5065            i,
5066            depth,
5067            has_children,
5068            is_disabled,
5069            selected_bg,
5070            hover_bg,
5071            row_focused,
5072            data_count,
5073            round_top,
5074            round_bottom,
5075            ..
5076        } = *paint;
5077        let hover_group = &paint.hover_group;
5078        let key = &paint.key;
5079        let tree_column = self.tree_column;
5080        let row_header_columns = &self.row_header_columns;
5081        let (width, min_width, max_width) =
5082            self.widths.get(c).copied().unwrap_or((None, None, None));
5083        let mut cell_el = gpui::div()
5084            .when(width.is_none(), flex_cell)
5085            .when_some(width, |e, w| e.w(w))
5086            .when_some(min_width, |e, w| e.min_w(w))
5087            .when_some(max_width, |e, w| e.max_w(w))
5088            .flex()
5089            .items_center()
5090            .gap(px(6.))
5091            .relative()
5092            // `.table__cell` is `px-4 py-3`.
5093            .px(px(16.))
5094            .py(px(12.))
5095            .when(self.secondary, |e| {
5096                e.border_b_1()
5097                    .border_color(colors.separator_tertiary().alpha(0.5))
5098            })
5099            .when(row_header_columns.get(c).copied().unwrap_or(false), |e| {
5100                e.font_weight(gpui::FontWeight::MEDIUM)
5101            })
5102            .when_some(selected_bg, |cell, bg| cell.bg(bg))
5103            .when_some(hover_bg, |cell, bg| {
5104                cell.group_hover(hover_group.clone(), move |s| s.bg(bg))
5105            })
5106            .when(!self.selectable && c == 0, |cell| {
5107                cell.when_some(round_top, |cell, r| cell.rounded_tl(r))
5108                    .when_some(round_bottom, |cell, r| cell.rounded_bl(r))
5109            })
5110            .when(c + 1 == data_count, |cell| {
5111                cell.when_some(round_top, |cell, r| cell.rounded_tr(r))
5112                    .when_some(round_bottom, |cell, r| cell.rounded_br(r))
5113            });
5114        if self.measure_intrinsic.get(c).copied().unwrap_or(false) {
5115            let measurements = self.measured_widths.clone();
5116            let tree_indent = if c == tree_column {
5117                tree_column_padding(depth).map_or(0., f32::from)
5118            } else {
5119                0.
5120            };
5121            // Only the cell's own content: the tree column's chevron or
5122            // spacer, then the caller's element. The rings a focused row or a
5123            // selected cell add after them are `inset_0` overlays as wide as
5124            // the cell, and measuring them grew the floor every frame.
5125            let content_children = 1 + usize::from(
5126                c == tree_column && (has_children || depth > 0 || self.tree_column_has_children),
5127            );
5128            cell_el = cell_el.on_children_prepainted(move |bounds, _, cx| {
5129                let content =
5130                    intrinsic_children_width(&bounds[..content_children.min(bounds.len())]);
5131                if content <= 0. {
5132                    return;
5133                }
5134                let next = px(content + 32. + tree_indent);
5135                measurements.update(cx, |values, cx| {
5136                    if values.len() <= c {
5137                        values.resize(c + 1, None);
5138                    }
5139                    if values[c].is_none_or(|current| next > current) {
5140                        values[c] = Some(next);
5141                        cx.notify();
5142                    }
5143                });
5144            });
5145        }
5146        // At the end of the chain, and after `flex_cell` — that helper
5147        // takes a `gpui::Div`, which an `.id(..)` would have turned into a
5148        // `Stateful<Div>` — and clear of the `.table__cell` padding window
5149        // `.shots/design_audit.py` opens on the comment above.
5150        //
5151        // `useTableCell.mjs` is `.../grid/useGridCell.mjs`'s
5152        // `role: 'gridcell'`, swapped for `'rowheader'` when the cell's
5153        // column key is in `rowHeaderColumnKeys` — the port's
5154        // `TableColumn::is_row_header`. The cell's contents are an
5155        // arbitrary `AnyElement` here, so unlike a row (which upstream
5156        // names from `node.textValue`) it carries no name of its own;
5157        // whatever the caller composed inside reports itself.
5158        let mut cell_el = cell_el
5159            .debug_selector(move || format!("table-row-track-{i}-{c}"))
5160            .id(element_id::indexed(
5161                &element_id::indexed(&self.id, "row", i),
5162                "cell",
5163                c,
5164            ))
5165            .a11y(if row_header_columns.get(c).copied().unwrap_or(false) {
5166                a11y::Role::RowHeader
5167            } else {
5168                a11y::Role::GridCell
5169            })
5170            .a11y_column_index(c + usize::from(self.selectable));
5171        // The tree column carries the indent and the chevron; a row with
5172        // no children still indents, so siblings line up.
5173        if c == tree_column {
5174            if let Some(padding) = tree_column_padding(depth) {
5175                cell_el = cell_el.pl(padding);
5176            }
5177            if has_children {
5178                cell_el = cell_el.child(self.tree_chevron(paint, cx));
5179            } else if depth > 0 || self.tree_column_has_children {
5180                // A leaf keeps the chevron's width so its text lines up
5181                // with its expandable siblings'.
5182                cell_el = cell_el.child(gpui::div().size(px(18.)).flex_shrink_0());
5183            }
5184        }
5185        cell_el = cell_el.child(cell);
5186        if let Some(cell_ctx) = &self.cell {
5187            cell_el = cell_ctx.decorate(cell_el, key, c, is_disabled, cx);
5188        }
5189        if row_focused && self.cell.is_none() {
5190            let cell_index = c + usize::from(self.selectable);
5191            let total_cells = data_count + usize::from(self.selectable);
5192            cell_el = cell_el.child(table_cell_focus_ring(
5193                cx,
5194                cell_index == 0,
5195                cell_index + 1 == total_cells,
5196            ));
5197        }
5198        cell_el
5199    }
5200
5201    /// The tree column's expand/collapse chevron on a row with children.
5202    fn tree_chevron(&self, paint: &RowPaint, cx: &App) -> gpui::Stateful<gpui::Div> {
5203        let colors = cx.colors();
5204        let is_disabled = paint.is_disabled;
5205        let is_expanded = paint.is_expanded;
5206        let toggle_key = paint.key.clone();
5207        let mut chevron = gpui::div()
5208            .id(element_id::scoped(
5209                &element_id::scoped(&self.id, "expand"),
5210                toggle_key.clone(),
5211            ))
5212            .flex()
5213            .items_center()
5214            .justify_center()
5215            .size(px(18.))
5216            .flex_shrink_0()
5217            .when(!is_disabled, |chevron| {
5218                chevron
5219                    .cursor(crate::util::interactive_cursor(cx))
5220                    .on_mouse_down(gpui::MouseButton::Left, |_, _, cx| {
5221                        cx.stop_propagation();
5222                    })
5223            })
5224            .child(
5225                gpui::svg()
5226                    .size(px(12.))
5227                    .path(if is_expanded {
5228                        icons::CHEVRON_DOWN
5229                    } else {
5230                        icons::CHEVRON_RIGHT
5231                    })
5232                    .text_color(colors.muted),
5233            );
5234        if !is_disabled {
5235            if let Some(cb) = self.on_expanded_change.clone() {
5236                let key = toggle_key;
5237                let before = self.expanded.clone();
5238                let focus = self.focus.clone();
5239                let cursor = self.cursor_own.clone();
5240                chevron = chevron.on_click(move |_, window, cx| {
5241                    cx.stop_propagation();
5242                    let mut next = before.as_ref().clone();
5243                    if let Some(at) = next.iter().position(|k| *k == key) {
5244                        next.remove(at);
5245                    } else {
5246                        next.push(key.clone());
5247                    }
5248                    cb(&next, window, cx);
5249                    window.focus(&focus, cx);
5250                    cursor.update(cx, |value, cx| {
5251                        *value = Some(key.clone());
5252                        cx.notify();
5253                    });
5254                });
5255            }
5256        }
5257        chevron
5258    }
5259
5260    /// The row's press: focus and cursor on mouse down, then the row's
5261    /// action or a selection change on click.
5262    fn row_press(
5263        &self,
5264        row: gpui::Stateful<gpui::Div>,
5265        paint: &RowPaint,
5266        cx: &App,
5267    ) -> gpui::Stateful<gpui::Div> {
5268        let i = paint.i;
5269        let key = paint.key.clone();
5270        let row_action = self.on_row_click.clone();
5271        let row_selection = self.on_selection_change.clone();
5272        let selection_own = self.selection_own.clone();
5273        let selection_behavior = self.selection_behavior;
5274        let disallow_empty_selection = self.disallow_empty_selection;
5275        if paint.is_disabled
5276            || !(row_action.is_some()
5277                || (self.selectable && (row_selection.is_some() || selection_own.is_some())))
5278        {
5279            return row;
5280        }
5281        let current = self.selected_keys.clone();
5282        let mode = self.selection_mode;
5283        let selection_range = self.selection_range.clone();
5284        let range_collection = self.row_keys.clone();
5285        let range_selectable: Vec<SharedString> = self
5286            .row_keys
5287            .iter()
5288            .filter(|key| !self.disabled_keys.contains(key))
5289            .cloned()
5290            .collect();
5291        let moved = self.cursor_own.clone();
5292        let focus = self.focus.clone();
5293        let focus_for_click = self.focus.clone();
5294        let key_for_cursor = key.clone();
5295        row.cursor(crate::util::interactive_cursor(cx))
5296            .on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
5297                if window.default_prevented() {
5298                    return;
5299                }
5300                window.focus(&focus, cx);
5301                moved.update(cx, |value, cx| {
5302                    *value = Some(key_for_cursor.clone());
5303                    cx.notify();
5304                });
5305            })
5306            .on_click(move |ev, w, cx| {
5307                if !focus_for_click.is_focused(w) {
5308                    return;
5309                }
5310                match row_intent(
5311                    RowActivation::Pointer,
5312                    mode,
5313                    selection_behavior,
5314                    current.is_empty(),
5315                    row_action.is_some(),
5316                ) {
5317                    RowIntent::Action => {
5318                        if let Some(cb) = &row_action {
5319                            cb(&i, ev, w, cx);
5320                        }
5321                    }
5322                    RowIntent::Selection => {
5323                        let was_selected = current.contains(&key);
5324                        let extends_selection =
5325                            ev.modifiers().shift && mode == SelectionMode::Multiple;
5326                        let next = if extends_selection {
5327                            let range = selection_range.read(cx).clone();
5328                            extend_selection_range(
5329                                &current,
5330                                &range_collection,
5331                                &range_selectable,
5332                                &range,
5333                                &key,
5334                            )
5335                        } else {
5336                            let modifiers = ev.modifiers();
5337                            let non_contiguous = if cfg!(target_os = "macos") {
5338                                modifiers.alt
5339                            } else {
5340                                modifiers.control
5341                            };
5342                            crate::selection::next_selection_with_behavior(
5343                                &current,
5344                                &key,
5345                                mode,
5346                                if non_contiguous {
5347                                    SelectionBehavior::Toggle
5348                                } else {
5349                                    selection_behavior
5350                                },
5351                                disallow_empty_selection,
5352                            )
5353                        };
5354                        let changed = !same_selection(&next, &current);
5355                        if changed {
5356                            if let Some(held) = &selection_own {
5357                                held.update(cx, |value, cx| {
5358                                    *value = next.clone();
5359                                    cx.notify();
5360                                });
5361                            }
5362                            if let Some(cb) = &row_selection {
5363                                cb(&next, w, cx);
5364                            }
5365                        }
5366                        if extends_selection && changed {
5367                            selection_range.update(cx, |range, _| {
5368                                if range.anchor.is_none() {
5369                                    range.anchor = Some(key.clone());
5370                                }
5371                                range.current = Some(key.clone());
5372                                range.is_all = false;
5373                            });
5374                        } else if !extends_selection
5375                            && mode == SelectionMode::Multiple
5376                            && !was_selected
5377                        {
5378                            selection_range.update(cx, |range, _| {
5379                                range.anchor = Some(key.clone());
5380                                range.current = Some(key.clone());
5381                                range.is_all = false;
5382                            });
5383                        } else if !extends_selection && mode == SelectionMode::Multiple {
5384                            selection_range.update(cx, |range, _| {
5385                                if range.is_all {
5386                                    *range = TableSelectionRange::default();
5387                                }
5388                            });
5389                        }
5390                    }
5391                    RowIntent::None => {}
5392                }
5393            })
5394    }
5395}
5396
5397// ---------------------------------------------------------------------------
5398// Column reordering and cell selection (HeroGPUI extension, after gpui-kit's
5399// data table: `movable` columns and `cell_selectable`). Neither is a HeroUI
5400// v3 prop; they are kept here, after the v3 surface, so the ported code above
5401// stays as it was.
5402// ---------------------------------------------------------------------------
5403
5404type OnColumnMove = std::sync::Arc<dyn Fn(&ColumnMove, &mut Window, &mut App) + 'static>;
5405type OnCellSelect = std::sync::Arc<dyn Fn(&TableCell, &mut Window, &mut App) + 'static>;
5406
5407/// A column move, as [`Table::on_column_move`] reports it (HeroGPUI
5408/// extension). Positions are display positions *before* the move; `order` is
5409/// the new display order as indices into the columns the table was given.
5410#[derive(Clone, Debug, PartialEq, Eq)]
5411#[non_exhaustive]
5412pub struct ColumnMove {
5413    /// The display position the column left.
5414    pub from: usize,
5415    /// The display position it moved to.
5416    pub to: usize,
5417    /// The whole new order, as indices into the given columns.
5418    pub order: Vec<usize>,
5419}
5420
5421/// One table cell (HeroGPUI extension): the row's selection key and the
5422/// column's index in the columns the table was given, so it stays the same
5423/// cell when the columns are reordered.
5424#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5425pub struct TableCell {
5426    /// The row's selection key (see [`TableRow::key`]).
5427    pub row: SharedString,
5428    /// The column's index in the columns as given.
5429    pub column: usize,
5430}
5431
5432impl TableCell {
5433    /// The cell of row `row` in column `column` (an index into the columns
5434    /// as given).
5435    pub fn new(row: impl Into<SharedString>, column: usize) -> Self {
5436        Self {
5437            row: row.into(),
5438            column,
5439        }
5440    }
5441}
5442
5443/// The column-reordering configuration.
5444#[derive(Default)]
5445struct ColumnReorder {
5446    allowed: bool,
5447    order: Option<Vec<usize>>,
5448    default_order: Option<Vec<usize>>,
5449    on_move: Option<OnColumnMove>,
5450}
5451
5452/// The cell-selection configuration.
5453#[derive(Default)]
5454struct CellSelection {
5455    enabled: bool,
5456    /// `Some` when controlled; the inner `None` is "no cell".
5457    selected: Option<Option<TableCell>>,
5458    default: Option<TableCell>,
5459    on_select: Option<OnCellSelect>,
5460}
5461
5462/// `order` made a permutation of `0..n`: out-of-range and repeated indices
5463/// dropped, missing ones appended in order.
5464fn sanitize_order(order: &[usize], n: usize) -> Vec<usize> {
5465    let mut seen = vec![false; n];
5466    let mut out = Vec::with_capacity(n);
5467    for &ix in order {
5468        if ix < n && !seen[ix] {
5469            seen[ix] = true;
5470            out.push(ix);
5471        }
5472    }
5473    out.extend((0..n).filter(|ix| !seen[*ix]));
5474    out
5475}
5476
5477/// The order after moving the column at display position `from` to `to`.
5478fn moved_order(order: &[usize], from: usize, to: usize) -> Vec<usize> {
5479    let mut next = order.to_vec();
5480    if from < next.len() && to < next.len() && from != to {
5481        let column = next.remove(from);
5482        next.insert(to, column);
5483    }
5484    next
5485}
5486
5487/// Reorders a row's cells (and its children's) into display order. A row
5488/// with fewer cells than columns keeps the ones it has, in display order.
5489fn permute_row(row: &mut TableRow, order: &[usize]) {
5490    let mut cells: Vec<Option<AnyElement>> = std::mem::take(&mut row.cells)
5491        .into_iter()
5492        .map(Some)
5493        .collect();
5494    row.cells = order
5495        .iter()
5496        .filter_map(|&ix| cells.get_mut(ix).and_then(Option::take))
5497        .collect();
5498    for child in &mut row.children {
5499        permute_row(child, order);
5500    }
5501}
5502
5503/// Moves per-position values (resized and measured widths) from the order
5504/// they were stored in to the new one.
5505fn permute_positions<T: Clone>(
5506    values: &[Option<T>],
5507    from: &[usize],
5508    to: &[usize],
5509) -> Vec<Option<T>> {
5510    to.iter()
5511        .map(|column| {
5512            from.iter()
5513                .position(|c| c == column)
5514                .and_then(|at| values.get(at).cloned().flatten())
5515        })
5516        .collect()
5517}
5518
5519/// The header row's left edge and each column header's bounds.
5520type HeaderBounds = std::rc::Rc<std::cell::RefCell<(Pixels, Vec<gpui::Bounds<Pixels>>)>>;
5521
5522/// The drag of a column header in progress.
5523#[derive(Clone, Copy, Debug, PartialEq)]
5524struct ColumnDrag {
5525    from: usize,
5526    start_x: f32,
5527    active: bool,
5528    target: usize,
5529}
5530
5531/// The display position whose header contains `x`, or the nearest end.
5532fn header_at(bounds: &[gpui::Bounds<Pixels>], x: Pixels) -> Option<usize> {
5533    let first = bounds.first()?;
5534    if x < first.left() {
5535        return Some(0);
5536    }
5537    bounds
5538        .iter()
5539        .position(|b| x >= b.left() && x < b.right())
5540        .or(Some(bounds.len() - 1))
5541}
5542
5543impl Table {
5544    /// Lets the user reorder the columns (HeroGPUI extension): drag a header
5545    /// to another column's place, or focus a header and press Alt+Left /
5546    /// Alt+Right. Every move is reported through [`Table::on_column_move`].
5547    pub fn allows_column_reorder(mut self, allowed: bool) -> Self {
5548        self.column_reorder.allowed = allowed;
5549        self
5550    }
5551
5552    /// The controlled display order, as indices into the columns given
5553    /// (HeroGPUI extension). Out-of-range and repeated indices are dropped and
5554    /// missing columns appended.
5555    pub fn column_order(mut self, order: impl IntoIterator<Item = usize>) -> Self {
5556        self.column_reorder.order = Some(order.into_iter().collect());
5557        self
5558    }
5559
5560    /// The initial display order of an uncontrolled table (HeroGPUI
5561    /// extension).
5562    pub fn default_column_order(mut self, order: impl IntoIterator<Item = usize>) -> Self {
5563        self.column_reorder.default_order = Some(order.into_iter().collect());
5564        self
5565    }
5566
5567    /// Reports each column move (HeroGPUI extension).
5568    pub fn on_column_move(
5569        mut self,
5570        f: impl Fn(&ColumnMove, &mut Window, &mut App) + 'static,
5571    ) -> Self {
5572        self.column_reorder.on_move = Some(std::sync::Arc::new(f));
5573        self
5574    }
5575
5576    /// Makes single cells selectable (HeroGPUI extension): a press selects the
5577    /// cell under the pointer, Left / Right move between the cells of the
5578    /// row, Home / End to its first and last cell, and the row keys (Up, Down,
5579    /// PageUp, PageDown, Ctrl+Home, Ctrl+End) move to the same column of
5580    /// another row. Row selection keeps working alongside; in a tree table the
5581    /// chevron, not Left/Right, expands a row.
5582    pub fn cell_selectable(mut self, selectable: bool) -> Self {
5583        self.cell_selection.enabled = selectable;
5584        self
5585    }
5586
5587    /// The controlled selected cell (HeroGPUI extension); `None` for none.
5588    pub fn selected_cell(mut self, cell: Option<TableCell>) -> Self {
5589        self.cell_selection.selected = Some(cell);
5590        self
5591    }
5592
5593    /// The initially selected cell of an uncontrolled table (HeroGPUI
5594    /// extension).
5595    pub fn default_selected_cell(mut self, cell: TableCell) -> Self {
5596        self.cell_selection.default = Some(cell);
5597        self
5598    }
5599
5600    /// Reports each cell a press or a key selected (HeroGPUI extension).
5601    pub fn on_cell_select(
5602        mut self,
5603        f: impl Fn(&TableCell, &mut Window, &mut App) + 'static,
5604    ) -> Self {
5605        self.cell_selection.on_select = Some(std::sync::Arc::new(f));
5606        self
5607    }
5608
5609    /// Resolves the display column order and puts the columns, every row's
5610    /// cells and the tree column into it, so the rest of the render works in
5611    /// display positions. Returns the order (indices into the given columns).
5612    fn apply_column_order(
5613        &mut self,
5614        base_id: &gpui::ElementId,
5615        window: &mut Window,
5616        cx: &mut App,
5617    ) -> std::rc::Rc<Vec<usize>> {
5618        let n = self.columns.len();
5619        let reorder = &self.column_reorder;
5620        let configured =
5621            reorder.allowed || reorder.order.is_some() || reorder.default_order.is_some();
5622        if !configured && !self.columns.iter().any(|c| c.frozen) {
5623            return std::rc::Rc::new((0..n).collect());
5624        }
5625        let order = if configured {
5626            let seed = sanitize_order(reorder.default_order.as_deref().unwrap_or(&[]), n);
5627            let (order, _) = crate::util::controlled(
5628                window,
5629                cx,
5630                element_id::scoped(base_id, "column-order"),
5631                reorder.order.as_ref().map(|order| sanitize_order(order, n)),
5632                seed,
5633            );
5634            sanitize_order(&order, n)
5635        } else {
5636            (0..n).collect()
5637        };
5638        // Frozen columns (HeroGPUI extension) are always displayed first.
5639        let order = frozen_first(&order, &self.columns);
5640        // The resized and measured widths are stored per display position;
5641        // when the order changes they move with their columns.
5642        let rendered = window.use_keyed_state(
5643            element_id::scoped(base_id, "rendered-column-order"),
5644            cx,
5645            |_, _| (0..n).collect::<Vec<usize>>(),
5646        );
5647        let previous = rendered.read(cx).clone();
5648        if previous != order {
5649            for part in ["resized", "measured-widths"] {
5650                let widths =
5651                    window.use_keyed_state(element_id::scoped(base_id, part), cx, |_, _| {
5652                        Vec::<Option<Pixels>>::new()
5653                    });
5654                widths.update(cx, |values, _| {
5655                    *values = permute_positions(values, &previous, &order);
5656                });
5657            }
5658            rendered.update(cx, |value, _| value.clone_from(&order));
5659        }
5660        if order.iter().enumerate().all(|(at, ix)| at == *ix) {
5661            return std::rc::Rc::new(order);
5662        }
5663        let mut given: Vec<Option<TableColumn>> = std::mem::take(&mut self.columns)
5664            .into_iter()
5665            .map(Some)
5666            .collect();
5667        self.columns = order
5668            .iter()
5669            .filter_map(|&ix| given.get_mut(ix).and_then(Option::take))
5670            .collect();
5671        for row in &mut self.rows {
5672            permute_row(row, &order);
5673        }
5674        if let Some((_, _, _, row)) = &mut self.virtual_rows {
5675            let inner = row.clone();
5676            let order = order.clone();
5677            *row = crate::util::shared(move |ix| {
5678                let mut built = inner(ix);
5679                permute_row(&mut built, &order);
5680                built
5681            });
5682        }
5683        if let Some(at) = order.iter().position(|ix| *ix == self.tree_column) {
5684            self.tree_column = at;
5685        }
5686        std::rc::Rc::new(order)
5687    }
5688
5689    /// The header row's left edge and each column header's bounds, stored by
5690    /// the header row's prepaint for the column drag.
5691    fn header_bounds(base_id: &gpui::ElementId, window: &mut Window, cx: &mut App) -> HeaderBounds {
5692        window
5693            .use_keyed_state(element_id::scoped(base_id, "header-bounds"), cx, |_, _| {
5694                std::rc::Rc::new(std::cell::RefCell::new((
5695                    px(0.),
5696                    Vec::<gpui::Bounds<Pixels>>::new(),
5697                )))
5698            })
5699            .read(cx)
5700            .clone()
5701    }
5702
5703    /// Records the header cells' bounds (before the header row takes its id,
5704    /// which `on_children_prepainted` needs) when reordering is allowed.
5705    fn column_reorder_measure(
5706        &self,
5707        header: gpui::Div,
5708        base_id: &gpui::ElementId,
5709        window: &mut Window,
5710        cx: &mut App,
5711    ) -> gpui::Div {
5712        if !self.column_reorder.allowed || self.columns.len() < 2 {
5713            return header;
5714        }
5715        let bounds = Self::header_bounds(base_id, window, cx);
5716        let skip = usize::from(self.selection_mode != SelectionMode::None);
5717        let columns = self.columns.len();
5718        header.on_children_prepainted(move |children, _, _| {
5719            let origin = children.first().map_or(px(0.), |b| b.left());
5720            let headers = children.iter().skip(skip).take(columns).copied().collect();
5721            *bounds.borrow_mut() = (origin, headers);
5722        })
5723    }
5724
5725    /// A frozen header's two parts: the selection cell and the frozen
5726    /// column headers, then the scrolling ones. When reordering is allowed
5727    /// each part records its headers' bounds into the one list the column
5728    /// drag reads, the frozen part first (it prepaints first).
5729    fn frozen_header_parts(
5730        &self,
5731        base_id: &gpui::ElementId,
5732        frozen: &FrozenColumns,
5733        mut cells: Vec<AnyElement>,
5734        window: &mut Window,
5735        cx: &mut App,
5736    ) -> (gpui::Div, gpui::Stateful<gpui::Div>) {
5737        let skip = usize::from(self.selection_mode != SelectionMode::None);
5738        let rest = cells.split_off((skip + frozen.count).min(cells.len()));
5739        let mut lead = frozen.frozen_part();
5740        let mut part = gpui::div();
5741        if self.column_reorder.allowed && self.columns.len() >= 2 {
5742            let bounds = Self::header_bounds(base_id, window, cx);
5743            let scrolled = bounds.clone();
5744            let count = frozen.count;
5745            lead = lead.on_children_prepainted(move |children, _, _| {
5746                let origin = children.first().map_or(px(0.), |b| b.left());
5747                let mut measured = bounds.borrow_mut();
5748                measured.0 = origin;
5749                measured.1.clear();
5750                measured.1.extend(children.iter().skip(skip).copied());
5751            });
5752            part = part.on_children_prepainted(move |children, _, _| {
5753                let mut measured = scrolled.borrow_mut();
5754                measured.1.truncate(count);
5755                measured.1.extend(children.iter().copied());
5756            });
5757        }
5758        (
5759            lead.children(cells),
5760            frozen
5761                .scrolling_part(part, element_id::scoped(base_id, "header-scroll"))
5762                .children(rest),
5763        )
5764    }
5765
5766    /// The header row's column drag (pointer) and Alt+Left / Alt+Right
5767    /// (keyboard) moves, when reordering is allowed.
5768    #[allow(clippy::too_many_arguments)]
5769    fn column_reorder_header(
5770        &self,
5771        header: gpui::Stateful<gpui::Div>,
5772        base_id: &gpui::ElementId,
5773        order: &std::rc::Rc<Vec<usize>>,
5774        header_focus: &[gpui::FocusHandle],
5775        resizing: &gpui::Entity<Option<(usize, f32, f32)>>,
5776        window: &mut Window,
5777        cx: &mut App,
5778    ) -> gpui::Stateful<gpui::Div> {
5779        if !self.column_reorder.allowed || self.columns.len() < 2 {
5780            return header;
5781        }
5782        let bounds = Self::header_bounds(base_id, window, cx);
5783        let drag =
5784            window.use_keyed_state(element_id::scoped(base_id, "column-drag"), cx, |_, _| {
5785                None::<ColumnDrag>
5786            });
5787        let own_order = if self.column_reorder.order.is_none() {
5788            Some(
5789                window.use_keyed_state(element_id::scoped(base_id, "column-order"), cx, |_, _| {
5790                    order.as_ref().clone()
5791                }),
5792            )
5793        } else {
5794            None
5795        };
5796        let on_move = self.column_reorder.on_move.clone();
5797        // A move stays in its region: frozen columns among the frozen ones,
5798        // the rest among the rest (see `TableColumn::frozen`).
5799        let frozen = self.columns.iter().take_while(|c| c.frozen).count();
5800        let frozen = if frozen < self.columns.len() {
5801            frozen
5802        } else {
5803            0
5804        };
5805        let commit = crate::util::shared({
5806            let order = order.clone();
5807            move |from: usize, to: usize, window: &mut Window, cx: &mut App| {
5808                let to = clamp_to_region(from, to, frozen);
5809                if from == to || to >= order.len() {
5810                    return;
5811                }
5812                let next = moved_order(&order, from, to);
5813                if let Some(own) = &own_order {
5814                    own.update(cx, |value, cx| {
5815                        value.clone_from(&next);
5816                        cx.notify();
5817                    });
5818                }
5819                if let Some(cb) = &on_move {
5820                    cb(
5821                        &ColumnMove {
5822                            from,
5823                            to,
5824                            order: next,
5825                        },
5826                        window,
5827                        cx,
5828                    );
5829                }
5830            }
5831        });
5832
5833        let accent = cx.colors().accent.color;
5834        let drag_now = *drag.read(cx);
5835        let mut header = header.relative();
5836        if let Some(active) = drag_now.filter(|d| d.active) {
5837            let measured = bounds.borrow();
5838            if let Some(target) = measured.1.get(active.target) {
5839                // The insertion edge: after the target when moving right,
5840                // before it when moving left.
5841                let x = if active.target > active.from {
5842                    target.right()
5843                } else {
5844                    target.left()
5845                };
5846                header = header.cursor(gpui::CursorStyle::ClosedHand).child(
5847                    gpui::div()
5848                        .absolute()
5849                        .top_0()
5850                        .bottom_0()
5851                        .left(x - measured.0 - px(1.))
5852                        .w(px(2.))
5853                        .bg(accent)
5854                        .debug_selector(|| "table-column-drop-indicator".into()),
5855                );
5856            }
5857        }
5858        let down_bounds = bounds.clone();
5859        let move_bounds = bounds;
5860        let down_drag = drag.clone();
5861        let move_drag = drag.clone();
5862        let up_drag = drag.clone();
5863        let out_drag = drag;
5864        let up_commit = commit.clone();
5865        let out_commit = commit.clone();
5866        let key_commit = commit;
5867        let resizing = resizing.clone();
5868        let focus = header_focus.to_vec();
5869        header
5870            .on_mouse_down(gpui::MouseButton::Left, move |event, _, cx| {
5871                // A resize handle's press started a resize, not a move.
5872                if resizing.read(cx).is_some() {
5873                    return;
5874                }
5875                let Some(from) = header_at(&down_bounds.borrow().1, event.position.x) else {
5876                    return;
5877                };
5878                down_drag.update(cx, |d, _| {
5879                    *d = Some(ColumnDrag {
5880                        from,
5881                        start_x: f32::from(event.position.x),
5882                        active: false,
5883                        target: from,
5884                    });
5885                });
5886            })
5887            .on_mouse_move(move |event, _, cx| {
5888                let Some(mut current) = *move_drag.read(cx) else {
5889                    return;
5890                };
5891                if event.pressed_button != Some(gpui::MouseButton::Left) {
5892                    move_drag.update(cx, |d, cx| {
5893                        *d = None;
5894                        cx.notify();
5895                    });
5896                    return;
5897                }
5898                let x = f32::from(event.position.x);
5899                if !current.active && (x - current.start_x).abs() < 6. {
5900                    return;
5901                }
5902                current.active = true;
5903                if let Some(target) = header_at(&move_bounds.borrow().1, event.position.x) {
5904                    current.target = clamp_to_region(current.from, target, frozen);
5905                }
5906                move_drag.update(cx, |d, cx| {
5907                    if *d != Some(current) {
5908                        *d = Some(current);
5909                        cx.notify();
5910                    }
5911                });
5912            })
5913            .on_mouse_up(gpui::MouseButton::Left, move |_, window, cx| {
5914                let done = up_drag.update(cx, |d, cx| {
5915                    cx.notify();
5916                    d.take()
5917                });
5918                if let Some(d) = done.filter(|d| d.active) {
5919                    up_commit(d.from, d.target, window, cx);
5920                }
5921            })
5922            .on_mouse_up_out(gpui::MouseButton::Left, move |_, window, cx| {
5923                let done = out_drag.update(cx, |d, cx| {
5924                    cx.notify();
5925                    d.take()
5926                });
5927                if let Some(d) = done.filter(|d| d.active) {
5928                    out_commit(d.from, d.target, window, cx);
5929                }
5930            })
5931            .on_key_down(move |event, window, cx| {
5932                let m = &event.keystroke.modifiers;
5933                if !m.alt || m.control || m.platform || m.shift || m.function {
5934                    return;
5935                }
5936                let forward = match event.keystroke.key.as_str() {
5937                    "right" => true,
5938                    "left" => false,
5939                    _ => return,
5940                };
5941                let Some(from) = focus.iter().position(|h| h.is_focused(window)) else {
5942                    return;
5943                };
5944                let to = if forward {
5945                    (from + 1).min(focus.len() - 1)
5946                } else {
5947                    from.saturating_sub(1)
5948                };
5949                let to = clamp_to_region(from, to, frozen);
5950                cx.stop_propagation();
5951                crate::util::set_focus_visible(true, cx);
5952                if to != from {
5953                    key_commit(from, to, window, cx);
5954                    // The focus follows the column it moved.
5955                    window.focus(&focus[to], cx);
5956                }
5957            })
5958    }
5959}
5960
5961/// What the rows need to draw and press the selected cell.
5962#[derive(Clone)]
5963struct CellCtx {
5964    /// The selected cell, as its row key and display position.
5965    selected: Option<(SharedString, usize)>,
5966    /// The selected cell's display position.
5967    column: gpui::Entity<Option<usize>>,
5968    /// The table's row cursor, which is the selected cell's row.
5969    cursor: gpui::Entity<Option<SharedString>>,
5970    focus: gpui::FocusHandle,
5971}
5972
5973impl CellCtx {
5974    /// Marks and rings the selected cell, and makes a press select a cell.
5975    fn decorate(
5976        &self,
5977        el: gpui::Stateful<gpui::Div>,
5978        row: &SharedString,
5979        column: usize,
5980        disabled: bool,
5981        cx: &App,
5982    ) -> gpui::Stateful<gpui::Div> {
5983        let is_selected = self
5984            .selected
5985            .as_ref()
5986            .is_some_and(|(r, c)| r == row && *c == column);
5987        let mut el = el.a11y_selected(is_selected);
5988        if is_selected {
5989            el = el.child(
5990                crate::util::inset_focus_ring(cx).debug_selector(|| "table-selected-cell".into()),
5991            );
5992        }
5993        if disabled {
5994            return el;
5995        }
5996        let (cursor, column_state, focus, row) = (
5997            self.cursor.clone(),
5998            self.column.clone(),
5999            self.focus.clone(),
6000            row.clone(),
6001        );
6002        el.on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
6003            window.focus(&focus, cx);
6004            crate::util::set_focus_visible(false, cx);
6005            column_state.update(cx, |c, cx| {
6006                *c = Some(column);
6007                cx.notify();
6008            });
6009            cursor.update(cx, |c, cx| {
6010                *c = Some(row.clone());
6011                cx.notify();
6012            });
6013        })
6014    }
6015}
6016
6017impl Table {
6018    /// Resolves the selected cell: its row is the row cursor, its column is
6019    /// kept beside it. Reports a change the user made, and snaps a controlled
6020    /// table back to its prop.
6021    fn cell_selection_state(
6022        &self,
6023        base_id: &gpui::ElementId,
6024        order: &[usize],
6025        row_cursor: &gpui::Entity<Option<SharedString>>,
6026        focus: &gpui::FocusHandle,
6027        window: &mut Window,
6028        cx: &mut App,
6029    ) -> Option<CellCtx> {
6030        if !self.cell_selection.enabled || order.is_empty() {
6031            return None;
6032        }
6033        let last = order.len() - 1;
6034        let position = |column: usize| order.iter().position(|c| *c == column);
6035        let column =
6036            window.use_keyed_state(element_id::scoped(base_id, "cell-column"), cx, |_, _| {
6037                None::<usize>
6038            });
6039        let reported =
6040            window.use_keyed_state(element_id::scoped(base_id, "cell-reported"), cx, |_, _| {
6041                None::<TableCell>
6042            });
6043        let seed = match &self.cell_selection.selected {
6044            Some(prop) => prop.clone(),
6045            None => self.cell_selection.default.clone(),
6046        };
6047        crate::util::seed_once(window, cx, element_id::scoped(base_id, "cell-seed"), |cx| {
6048            if let Some(seed) = seed {
6049                row_cursor.update(cx, |c, _| *c = Some(seed.row.clone()));
6050                column.update(cx, |c, _| *c = position(seed.column));
6051                reported.update(cx, |r, _| *r = Some(seed));
6052            }
6053        });
6054        let at = column.read(cx).unwrap_or(0).min(last);
6055        let requested = row_cursor.read(cx).clone().map(|row| TableCell {
6056            row,
6057            column: order[at],
6058        });
6059        if requested.is_some() && requested != *reported.read(cx) {
6060            reported.update(cx, |r, _| r.clone_from(&requested));
6061            if let (Some(cb), Some(cell)) =
6062                (self.cell_selection.on_select.clone(), requested.clone())
6063            {
6064                window.defer(cx, move |window, cx| cb(&cell, window, cx));
6065            }
6066        }
6067        let shown = match &self.cell_selection.selected {
6068            Some(prop) => {
6069                if *prop != requested {
6070                    let prop = prop.clone();
6071                    row_cursor.update(cx, |c, _| *c = prop.as_ref().map(|p| p.row.clone()));
6072                    column.update(cx, |c, _| {
6073                        *c = prop.as_ref().and_then(|p| position(p.column));
6074                    });
6075                    reported.update(cx, |r, _| r.clone_from(&prop));
6076                }
6077                prop.clone()
6078            }
6079            None => requested,
6080        };
6081        Some(CellCtx {
6082            selected: shown.and_then(|cell| Some((cell.row, position(cell.column)?))),
6083            column,
6084            cursor: row_cursor.clone(),
6085            focus: focus.clone(),
6086        })
6087    }
6088
6089    /// Left / Right / Home / End between the cells of a row, ahead of the
6090    /// row keyboard, while the body holds the focus.
6091    fn cell_selection_keys(
6092        &self,
6093        wrapper: gpui::Div,
6094        cell: &Option<CellCtx>,
6095        rows: &[SharedString],
6096        frozen: Option<FrozenColumns>,
6097    ) -> gpui::Div {
6098        let Some(cell) = cell.clone() else {
6099            return wrapper;
6100        };
6101        let last = self.columns.len().saturating_sub(1);
6102        let first_row = rows
6103            .iter()
6104            .find(|key| !self.disabled_keys.contains(key))
6105            .cloned();
6106        wrapper.capture_key_down(move |event, window, cx| {
6107            if !cell.focus.is_focused(window) {
6108                return;
6109            }
6110            let m = &event.keystroke.modifiers;
6111            if m.control || m.alt || m.platform || m.shift || m.function {
6112                return;
6113            }
6114            let at = cell.column.read(cx).unwrap_or(0).min(last);
6115            let to = match event.keystroke.key.as_str() {
6116                "left" => at.saturating_sub(1),
6117                "right" => (at + 1).min(last),
6118                "home" => 0,
6119                "end" => last,
6120                _ => return,
6121            };
6122            cx.stop_propagation();
6123            crate::util::set_focus_visible(true, cx);
6124            if cell.cursor.read(cx).is_none() {
6125                let first = first_row.clone();
6126                cell.cursor.update(cx, |c, cx| {
6127                    *c = first;
6128                    cx.notify();
6129                });
6130            }
6131            cell.column.update(cx, |c, cx| {
6132                *c = Some(to);
6133                cx.notify();
6134            });
6135            if let Some(frozen) = &frozen {
6136                frozen.reveal(to);
6137            }
6138        })
6139    }
6140}
6141
6142#[cfg(test)]
6143mod tests {
6144    use super::*;
6145
6146    #[test]
6147    fn tree_column_padding_uses_one_rem_per_one_based_level() {
6148        assert_eq!(tree_column_padding(0), None);
6149        assert_eq!(tree_column_padding(1), Some(px(32.)));
6150        assert_eq!(tree_column_padding(2), Some(px(48.)));
6151        assert_eq!(tree_column_padding(3), Some(px(64.)));
6152    }
6153
6154    #[test]
6155    fn a_new_column_starts_ascending() {
6156        let next = SortDescriptor::next(None, "name");
6157        assert_eq!(next, SortDescriptor::new("name", SortDirection::Ascending));
6158    }
6159
6160    #[test]
6161    fn clicking_the_sorted_column_flips_it() {
6162        let current = SortDescriptor::new("name", SortDirection::Ascending);
6163        let next = SortDescriptor::next(Some(&current), "name");
6164        assert_eq!(next.direction, SortDirection::Descending);
6165        let back = SortDescriptor::next(Some(&next), "name");
6166        assert_eq!(back.direction, SortDirection::Ascending);
6167    }
6168
6169    #[test]
6170    fn disabled_only_controlled_selection_is_indeterminate() {
6171        let selectable = vec![SharedString::from("alpha")];
6172        let selected = vec![SharedString::from("beta")];
6173        assert_eq!(select_all_flags(&selectable, &selected), (false, true));
6174    }
6175
6176    #[test]
6177    fn switching_column_resets_to_ascending() {
6178        let current = SortDescriptor::new("name", SortDirection::Descending);
6179        let next = SortDescriptor::next(Some(&current), "size");
6180        assert_eq!(next, SortDescriptor::new("size", SortDirection::Ascending));
6181    }
6182
6183    #[test]
6184    fn row_keys_fall_back_to_the_index() {
6185        let table = Table::new(vec!["A".into()])
6186            .row(vec![])
6187            .keyed_row("second", vec![])
6188            .row(vec![]);
6189        assert_eq!(
6190            table.row_keys(),
6191            vec![
6192                SharedString::from("0"),
6193                SharedString::from("second"),
6194                SharedString::from("2"),
6195            ]
6196        );
6197    }
6198
6199    /// The Home/End gate takes the platform as an explicit bool, so this
6200    /// truth table is free of `cfg!` and mechanically proves both maps from
6201    /// any host: no macOS chord ever extends -- Shift and Alt+Shift move the
6202    /// focus alone -- while Windows and Linux extend exactly from
6203    /// Control+Shift.
6204    #[test]
6205    fn shift_home_end_extends_only_from_control_outside_macos() {
6206        for key in ["home", "end"] {
6207            assert!(
6208                !shift_home_end_extends(key, true, true),
6209                "macOS registers no Home/End extension"
6210            );
6211            assert!(!shift_home_end_extends(key, false, true));
6212            assert!(
6213                shift_home_end_extends(key, true, false),
6214                "Control+Shift+{key} must extend on Windows and Linux"
6215            );
6216            assert!(
6217                !shift_home_end_extends(key, false, false),
6218                "plain Shift+{key} must only move the focus"
6219            );
6220        }
6221    }
6222
6223    /// The grid delegate's arrows never consult the Home/End gate: their
6224    /// forbidden extra chords are rejected earlier, by
6225    /// `exact_shift_navigation`.
6226    #[test]
6227    fn shift_navigation_keys_do_not_consult_the_home_end_gate() {
6228        for key in ["left", "right", "up", "down"] {
6229            assert!(shift_home_end_extends(key, false, true));
6230            assert!(shift_home_end_extends(key, true, false));
6231        }
6232    }
6233
6234    /// The registration gate takes `Modifiers`, so the pinned chord map can
6235    /// be spelled out: macOS registers none, Shift, Alt, and Alt+Shift and
6236    /// every Control- or Meta-bearing chord is entirely inert, while
6237    /// Windows and Linux register none, Shift, Control, and Control+Shift
6238    /// and reject every Alt- or Meta-bearing chord. The upstream matcher
6239    /// sees only the browser's Alt/Control/Meta/Shift flags, so GPUI's
6240    /// `function` flag is ignored: `fn` stays registered on both maps, and
6241    /// it never rescues a chord the platform itself rejects.
6242    #[test]
6243    fn home_end_registration_matches_the_pinned_chord_map() {
6244        let none = gpui::Modifiers::none();
6245        let shift = gpui::Modifiers {
6246            shift: true,
6247            ..none
6248        };
6249        let alt = gpui::Modifiers { alt: true, ..none };
6250        let alt_shift = gpui::Modifiers { shift: true, ..alt };
6251        let function = gpui::Modifiers {
6252            function: true,
6253            ..none
6254        };
6255        let function_alt = gpui::Modifiers {
6256            alt: true,
6257            ..function
6258        };
6259        for modifiers in [none, shift, alt, alt_shift, function, function_alt] {
6260            assert!(
6261                home_end_registered(modifiers, true),
6262                "macOS must register {modifiers:?}"
6263            );
6264        }
6265        let control = gpui::Modifiers {
6266            control: true,
6267            ..none
6268        };
6269        let control_shift = gpui::Modifiers {
6270            shift: true,
6271            ..control
6272        };
6273        let platform = gpui::Modifiers {
6274            platform: true,
6275            ..none
6276        };
6277        let platform_shift = gpui::Modifiers {
6278            shift: true,
6279            ..platform
6280        };
6281        for modifiers in [control, control_shift, platform, platform_shift] {
6282            assert!(
6283                !home_end_registered(modifiers, true),
6284                "macOS must not register {modifiers:?}"
6285            );
6286        }
6287        for modifiers in [none, shift, control, control_shift, function] {
6288            assert!(
6289                home_end_registered(modifiers, false),
6290                "Windows and Linux must register {modifiers:?}"
6291            );
6292        }
6293        for modifiers in [alt, alt_shift, function_alt, platform, platform_shift] {
6294            assert!(
6295                !home_end_registered(modifiers, false),
6296                "Windows and Linux must not register {modifiers:?}"
6297            );
6298        }
6299    }
6300
6301    /// The keystroke spellings real events hand the gate: `ctrl` parses to
6302    /// the Control field the Windows/Linux registration admits and macOS
6303    /// vetoes, `cmd` to the platform field macOS vetoes, `alt-shift` to
6304    /// the chord that stays registered (focus-only) on macOS alone, and
6305    /// `fn` to the flag the browser matcher never sees, so it registers
6306    /// exactly like the bare key on both maps.
6307    #[test]
6308    fn keystroke_spellings_reach_the_registration_gate() {
6309        let ctrl_shift_home = gpui::Keystroke::parse("ctrl-shift-home").unwrap();
6310        assert!(home_end_registered(ctrl_shift_home.modifiers, false));
6311        assert!(!home_end_registered(ctrl_shift_home.modifiers, true));
6312        let cmd_shift_home = gpui::Keystroke::parse("cmd-shift-home").unwrap();
6313        assert!(!home_end_registered(cmd_shift_home.modifiers, true));
6314        let alt_shift_end = gpui::Keystroke::parse("alt-shift-end").unwrap();
6315        assert!(home_end_registered(alt_shift_end.modifiers, true));
6316        assert!(!home_end_registered(alt_shift_end.modifiers, false));
6317        let fn_home = gpui::Keystroke::parse("fn-home").unwrap();
6318        assert!(fn_home.modifiers.function);
6319        assert!(home_end_registered(fn_home.modifiers, true));
6320        assert!(home_end_registered(fn_home.modifiers, false));
6321    }
6322
6323    // The pinned sortable column hover only recolours the header text to
6324    // `--foreground`; it paints no background. `soft()` is a lighter, wrong
6325    // wash, so the check is mechanical.
6326    #[test]
6327    fn the_sort_header_hover_paints_no_background() {
6328        // Scan the implementation only; this test's own text names the
6329        // forbidden accessor.
6330        let source = include_str!("table.rs")
6331            .split("#[cfg(test)]")
6332            .next()
6333            .expect("the implementation section is always present");
6334        assert!(
6335            source.contains(".group_hover(sort_group, |s| s.text_color(colors.foreground))"),
6336            "the sortable header hover must recolour the text to `--foreground` \
6337             (pinned `.table__column[data-allows-sorting]:hover`)"
6338        );
6339        assert!(
6340            !source.contains("colors.default.soft()"),
6341            "the sortable header hover must not come back as a soft background"
6342        );
6343    }
6344
6345    // The pinned row hover is variant-specific: `.table-root--primary` rows
6346    // fill `bg-surface/40`, `.table-root--secondary` rows fill
6347    // `bg-default/50`.
6348    #[test]
6349    fn the_row_hover_is_variant_specific() {
6350        // Scan the implementation only.
6351        let source = include_str!("table.rs")
6352            .split("#[cfg(test)]")
6353            .next()
6354            .expect("the implementation section is always present");
6355        assert!(
6356            source.contains("colors.default.color.alpha(0.5)")
6357                && source.contains("colors.surface.background.alpha(0.4)"),
6358            "the row hover must read `bg-default/50` on secondary and \
6359             `bg-surface/40` on primary"
6360        );
6361    }
6362
6363    // The pinned selected row fills `bg-surface/10`, and the rule follows the
6364    // hover rule in the pinned stylesheet, so it wins the cascade: a hovered
6365    // selected row keeps the selected fill. `accent.soft()` is a role wash
6366    // the pinned table never paints, so the check is mechanical.
6367    #[test]
6368    fn the_selected_row_fills_surface_ten_percent_and_outranks_the_hover() {
6369        // Scan the implementation only; this test's own text names the
6370        // forbidden accessor.
6371        let source = include_str!("table.rs")
6372            .split("#[cfg(test)]")
6373            .next()
6374            .expect("the implementation section is always present");
6375        assert!(
6376            source.contains(
6377                "let selected_bg = is_selected.then(|| colors.surface.background.alpha(0.1));"
6378            ),
6379            "the selected row must fill `bg-surface/10` \
6380             (pinned `.table__row[data-selected] .table__cell`)"
6381        );
6382        assert!(
6383            source.contains(".when_some(selected_bg, |cell, bg| cell.bg(bg))")
6384                && source.contains("cell.group_hover(hover_group.clone(), move |s| s.bg(bg))"),
6385            "selected and hover fills must be painted on each cell, with the \
6386             selected row omitting the hover group so HeroUI's selected rule wins"
6387        );
6388        assert!(
6389            !source.contains("accent.soft()"),
6390            "the selected row must not paint a role soft wash"
6391        );
6392    }
6393
6394    #[test]
6395    fn table_focus_ring_is_split_per_cell() {
6396        let source = include_str!("table.rs")
6397            .split("#[cfg(test)]")
6398            .next()
6399            .expect("the implementation section is always present");
6400        assert!(
6401            source.contains("fn table_cell_focus_ring(cx: &App, first: bool, last: bool)")
6402                && source.contains(".top_0()")
6403                && source.contains(".bottom_0()")
6404                && source.contains("cell_el = cell_el.child(table_cell_focus_ring("),
6405            "focused rows must paint clipped edge strips on each cell"
6406        );
6407        assert!(
6408            !source.contains("r.child(crate::util::inset_focus_ring(cx))"),
6409            "the row must not use one rounded overlay that bleeds across cells"
6410        );
6411    }
6412
6413    #[test]
6414    fn table_header_uses_medium_weight() {
6415        let source = include_str!("table.rs")
6416            .split("#[cfg(test)]")
6417            .next()
6418            .expect("the implementation section is always present");
6419        assert!(
6420            source.contains(".text_size(px(12.))")
6421                && source.contains(".line_height(px(16.))")
6422                && source.contains(".font_weight(gpui::FontWeight::MEDIUM)"),
6423            "table column headers must use the pinned `font-medium` weight"
6424        );
6425    }
6426
6427    #[test]
6428    fn table_header_preserves_caller_label_case() {
6429        let source = include_str!("table.rs")
6430            .split("#[cfg(test)]")
6431            .next()
6432            .expect("the implementation section is always present");
6433        assert!(
6434            source.contains(".child(column.label.clone());"),
6435            "table column headers must render the caller's label without uppercasing it"
6436        );
6437    }
6438
6439    #[test]
6440    fn table_header_and_rows_use_half_alpha_separator() {
6441        let source = include_str!("table.rs")
6442            .split("#[cfg(test)]")
6443            .next()
6444            .expect("the implementation section is always present");
6445        assert!(
6446            source.match_indices("colors.separator.alpha(0.5)").count() >= 2,
6447            "the header and row separators must use the separator token at 50% alpha"
6448        );
6449    }
6450
6451    #[test]
6452    fn table_last_row_omits_the_bottom_separator() {
6453        let source = include_str!("table.rs")
6454            .split("#[cfg(test)]")
6455            .next()
6456            .expect("the implementation section is always present");
6457        assert!(
6458            source.contains("if !self.secondary && i + 1 < self.row_keys.len() {")
6459                && source
6460                    .contains("row = row.border_b_1().border_color(colors.separator.alpha(0.5));"),
6461            "the final visible Table row must not receive a bottom separator"
6462        );
6463    }
6464
6465    #[test]
6466    fn secondary_table_uses_surface_headers_and_cell_separators() {
6467        let source = include_str!("table.rs")
6468            .split("#[cfg(test)]")
6469            .next()
6470            .expect("the implementation section is always present");
6471        assert!(
6472            source.contains("cell = cell\n                .bg(colors.surface_secondary)")
6473                && source.contains("colors.separator_tertiary().alpha(0.5)"),
6474            "secondary tables must paint the header surface and per-cell tertiary separators"
6475        );
6476    }
6477
6478    #[test]
6479    fn table_headers_keep_short_separators_and_rotate_builtin_sort_indicator() {
6480        let source = include_str!("table.rs")
6481            .split("#[cfg(test)]")
6482            .next()
6483            .expect("the implementation section is always present");
6484        assert!(
6485            source.contains(".h(px(16.))")
6486                && source.contains(".bg(colors.separator)")
6487                && source.contains("rotating_indicator_with_duration")
6488                && source.contains(".path(icons::CHEVRON_UP)"),
6489            "table headers must draw the pinned separator and use the shared 100ms rotation"
6490        );
6491    }
6492
6493    /// The primary variant's rows sit in the rounded `.table__body`, whose
6494    /// top and bottom curves both bound the row run.
6495    #[test]
6496    fn primary_row_fills_round_to_the_body_on_the_edges_they_touch() {
6497        let body = px(16.);
6498        let wrapper = px(24.);
6499        assert_eq!(
6500            edge_corner_rounding(false, false, true, true, body, wrapper),
6501            (Some(body), Some(body))
6502        );
6503        // A footer sits below the body, so it never takes the body's curve.
6504        assert_eq!(
6505            edge_corner_rounding(false, true, true, true, body, wrapper),
6506            (Some(body), Some(body))
6507        );
6508        // A virtual body scrolled off either edge is cut straight there.
6509        assert_eq!(
6510            edge_corner_rounding(false, false, false, true, body, wrapper),
6511            (None, Some(body))
6512        );
6513        assert_eq!(
6514            edge_corner_rounding(false, false, true, false, body, wrapper),
6515            (Some(body), None)
6516        );
6517        assert_eq!(
6518            edge_corner_rounding(false, false, false, false, body, wrapper),
6519            (None, None)
6520        );
6521    }
6522
6523    /// The secondary variant is flat, so the wrapper's corners are the only
6524    /// curve -- and the header always stands between them and the first row.
6525    #[test]
6526    fn secondary_row_fills_round_only_to_the_wrapper_bottom() {
6527        let body = px(16.);
6528        let wrapper = px(24.);
6529        assert_eq!(
6530            edge_corner_rounding(true, false, true, true, body, wrapper),
6531            (None, Some(wrapper))
6532        );
6533        // A footer takes the wrapper's bottom edge instead.
6534        assert_eq!(
6535            edge_corner_rounding(true, true, true, true, body, wrapper),
6536            (None, None)
6537        );
6538        assert_eq!(
6539            edge_corner_rounding(true, false, true, false, body, wrapper),
6540            (None, None)
6541        );
6542    }
6543
6544    /// The decision is taken once per render and threaded through `RowCtx`,
6545    /// and only the first and last rows' outer cells may consume it.
6546    #[test]
6547    fn only_the_outer_cells_of_the_edge_rows_round_their_fill() {
6548        let source = include_str!("table.rs")
6549            .split("#[cfg(test)]")
6550            .next()
6551            .expect("the implementation section is always present");
6552        assert!(
6553            source.contains("let round_top = (i == 0).then_some(self.round_top).flatten();")
6554                && source.contains("let round_bottom = (i + 1 == self.row_keys.len())"),
6555            "only the first and last body rows may round their cell fills"
6556        );
6557        assert!(
6558            source.contains(".when(!self.selectable && c == 0, |cell| {")
6559                && source.contains(".when(c + 1 == data_count, |cell| {")
6560                && source.contains(".when_some(round_top, |cell, r| cell.rounded_tl(r))")
6561                && source.contains(".when_some(round_bottom, |cell, r| cell.rounded_br(r))"),
6562            "the row's leading and trailing cells must carry the box's corners"
6563        );
6564        assert!(
6565            source.contains("virtual_scroll_now.is_scrolled_to_top()")
6566                && source.contains("virtual_scroll_now.is_scrolled_to_end()")
6567                && source.contains("top.item_ix == 0 && top.offset_in_item <= px(0.5)"),
6568            "a virtual body must only round the edges it is scrolled to"
6569        );
6570    }
6571
6572    #[test]
6573    fn virtual_selectable_keys_are_not_materialized_on_every_render() {
6574        let source = include_str!("table.rs")
6575            .split("#[cfg(test)]")
6576            .next()
6577            .expect("the implementation section is always present");
6578        assert!(
6579            !source.contains("let selectable_collection_keys: Vec<SharedString> ="),
6580            "virtual Table must not allocate a selectable-key Vec for every render"
6581        );
6582    }
6583}
6584
6585crate::util::impl_component_styled!(Table);