Skip to main content

fret_ui_kit/declarative/
table.rs

1use fret_core::{Color, Corners, CursorIcon, Edges, KeyCode, Px, SemanticsRole};
2use fret_runtime::{CommandId, Effect, Model, ModelStore, TimerToken};
3use fret_ui::action::{PressablePointerDownResult, PressablePointerUpResult, UiActionHostExt};
4use fret_ui::element::{
5    AnyElement, ContainerProps, HoverRegionProps, LayoutStyle, Length, Overflow,
6    PointerRegionProps, PressableA11y, PressableProps, RingPlacement, RingStyle, ScrollAxis,
7    ScrollProps, SemanticsDecoration, SemanticsProps, SpacerProps, VirtualListOptions,
8};
9use fret_ui::scroll::{ScrollHandle, VirtualListScrollHandle};
10use fret_ui::{
11    ElementContext, ElementContextAccess, GlobalElementId, Theme, UiHost, scroll::ScrollStrategy,
12};
13
14use fret_core::time::Instant;
15use std::cell::{Cell, RefCell};
16use std::rc::Rc;
17use std::sync::Arc;
18use std::time::Duration;
19
20type TypeaheadLabelAt<TData> = dyn Fn(&TData, usize) -> Arc<str> + Send + Sync;
21type CopyTextAtFn = dyn Fn(&ModelStore, usize) -> Option<String> + Send + Sync;
22type RowKeyAt<TData> = dyn Fn(&TData, usize) -> RowKey;
23type HeaderLabelAt<TData> = dyn Fn(&ColumnDef<TData>) -> Arc<str>;
24type HeaderAccessoryAt<H, TData> =
25    dyn for<'a> Fn(&mut dyn ElementContextAccess<'a, H>, &ColumnDef<TData>) -> AnyElement;
26type CellAt<H, TData> =
27    dyn for<'a> Fn(&mut dyn ElementContextAccess<'a, H>, &ColumnDef<TData>, &TData) -> AnyElement;
28type GroupAggsU64 = std::collections::HashMap<RowKey, Arc<[(ColumnId, u64)]>>;
29type GroupAggsAny = std::collections::HashMap<RowKey, Arc<[(ColumnId, TanStackValue)]>>;
30type GroupAggsText = std::collections::HashMap<RowKey, Arc<[(ColumnId, Arc<str>)]>>;
31type SorterFn<TData> = dyn Fn(&TData, &TData) -> std::cmp::Ordering;
32type SorterSpec<TData> = (SortSpec, Arc<SorterFn<TData>>);
33
34/// Narrow interop bridge for table surfaces that still store view state in `Model<TableState>`.
35///
36/// This stays intentionally table-specific so `LocalState<TableState>` can participate in the
37/// public table/data-table authoring lane without widening into a crate-wide `IntoModel<T>` story.
38pub trait IntoTableStateModel {
39    fn into_table_state_model(self) -> Model<TableState>;
40}
41
42impl IntoTableStateModel for Model<TableState> {
43    fn into_table_state_model(self) -> Model<TableState> {
44        self
45    }
46}
47
48impl IntoTableStateModel for &Model<TableState> {
49    fn into_table_state_model(self) -> Model<TableState> {
50        self.clone()
51    }
52}
53
54use crate::declarative::action_hooks::ActionHooksExt;
55use crate::declarative::collection_semantics::CollectionSemanticsExt as _;
56use crate::declarative::model_watch::ModelWatchExt as _;
57use crate::ui;
58use crate::{IntoUiElement, LayoutRefinement, MetricRef, Size, Space, collect_children};
59
60use crate::headless::table::{
61    Aggregation, ColumnDef, ColumnId, ColumnResizeDirection, ColumnResizeMode, ExpandingState,
62    FilteringFnSpec, FlatRowOrderCache, FlatRowOrderDeps, GroupedColumnMode, GroupedRowKind,
63    PaginationBounds, PaginationState, Row, RowId, RowKey, SortSpec, SortToggleColumn, Table,
64    TableOptions, TableState, TanStackValue, begin_column_resize, compute_grouped_u64_aggregations,
65    drag_column_resize, end_column_resize, is_row_expanded, is_row_selected, is_some_rows_pinned,
66    pagination_bounds, sort_grouped_row_indices_in_place, toggle_sorting_state_handler_tanstack,
67};
68use crate::headless::typeahead::{TypeaheadBuffer, match_prefix_arc_str};
69
70const TABLE_TYPEAHEAD_TIMEOUT: Duration = Duration::from_millis(750);
71
72fn resolve_table_colors(theme: &Theme) -> (Color, Color, Color, Color, Color) {
73    let table_bg = theme
74        .color_by_key("table.background")
75        .or_else(|| theme.color_by_key("list.background"))
76        .or_else(|| theme.color_by_key("card"))
77        .unwrap_or_else(|| theme.color_token("card"));
78    let border = theme
79        .color_by_key("table.border")
80        .or_else(|| theme.color_by_key("border"))
81        .or_else(|| theme.color_by_key("list.border"))
82        .unwrap_or_else(|| theme.color_token("border"));
83    let header_bg = theme
84        .color_by_key("table.header.background")
85        .or_else(|| theme.color_by_key("muted"))
86        .unwrap_or(table_bg);
87    let row_hover = theme
88        .color_by_key("table.row.hover")
89        .or_else(|| theme.color_by_key("list.hover.background"))
90        .or_else(|| theme.color_by_key("list.row.hover"))
91        .or_else(|| theme.color_by_key("accent"))
92        .unwrap_or_else(|| theme.color_token("accent"));
93    let row_active = theme
94        .color_by_key("table.row.active")
95        .or_else(|| theme.color_by_key("list.active.background"))
96        .or_else(|| theme.color_by_key("list.row.active"))
97        .or_else(|| theme.color_by_key("accent"))
98        .unwrap_or_else(|| theme.color_token("accent"));
99    (table_bg, border, header_bg, row_hover, row_active)
100}
101
102fn emphasize_border(border: Color, min_alpha: f32) -> Color {
103    Color {
104        a: border.a.max(min_alpha),
105        ..border
106    }
107}
108
109fn resolve_row_height(theme: &Theme, size: Size) -> Px {
110    let base = theme
111        .metric_by_key("component.table.row_height")
112        .or_else(|| theme.metric_by_key("component.list.row_height"))
113        .unwrap_or_else(|| size.list_row_h(theme));
114    Px(base.0.max(0.0))
115}
116
117fn resolve_cell_padding_x(theme: &Theme) -> Px {
118    MetricRef::space(Space::N2p5).resolve(theme)
119}
120
121fn resolve_cell_padding_y(theme: &Theme) -> Px {
122    MetricRef::space(Space::N1p5).resolve(theme)
123}
124
125fn sort_for_column(sorting: &[SortSpec], id: &ColumnId) -> Option<bool> {
126    sorting
127        .iter()
128        .find(|s| s.column.as_ref() == id.as_ref())
129        .map(|s| s.desc)
130}
131
132#[cfg(test)]
133fn next_sort_for_column(current: Option<bool>) -> Option<bool> {
134    match current {
135        None => Some(false),
136        Some(false) => Some(true),
137        Some(true) => None,
138    }
139}
140
141#[cfg(test)]
142fn apply_single_sort_toggle(state: &mut TableState, col_id: &ColumnId) {
143    let current = sort_for_column(&state.sorting, col_id);
144    let next = next_sort_for_column(current);
145    state.sorting.clear();
146    if let Some(desc) = next {
147        state.sorting.push(SortSpec {
148            column: col_id.clone(),
149            desc,
150        });
151    }
152    state.pagination.page_index = 0;
153}
154
155fn with_table_view_column_constraints<TData>(
156    mut col: ColumnDef<TData>,
157    props: &TableViewProps,
158) -> ColumnDef<TData> {
159    let min_w = col.min_size.max(props.min_column_width.0).max(0.0);
160    col.min_size = min_w;
161    col.max_size = col.max_size.max(min_w);
162    if !col.columns.is_empty() {
163        col.columns = col
164            .columns
165            .into_iter()
166            .map(|c| with_table_view_column_constraints(c, props))
167            .collect();
168    }
169    col
170}
171
172fn retained_table_row_fill_layout() -> LayoutStyle {
173    LayoutStyle {
174        size: fret_ui::element::SizeStyle {
175            width: Length::Fill,
176            ..Default::default()
177        },
178        flex: fret_ui::element::FlexItemStyle {
179            grow: 1.0,
180            shrink: 1.0,
181            basis: Length::Px(Px(0.0)),
182            ..Default::default()
183        },
184        ..Default::default()
185    }
186}
187
188fn table_scroll_fill_layout() -> LayoutStyle {
189    let mut layout = LayoutStyle::default();
190    layout.size.width = Length::Fill;
191    layout.size.height = Length::Fill;
192    layout.flex.grow = 1.0;
193    layout.flex.shrink = 1.0;
194    layout.flex.basis = Length::Px(Px(0.0));
195    layout
196}
197
198fn table_clip_fill_layout() -> LayoutStyle {
199    let mut layout = table_scroll_fill_layout();
200    layout.overflow = Overflow::Clip;
201    layout
202}
203
204fn table_fixed_column_layout(col_w: Px) -> LayoutStyle {
205    LayoutStyle {
206        size: fret_ui::element::SizeStyle {
207            width: Length::Px(col_w),
208            min_width: Some(Length::Px(col_w)),
209            max_width: Some(Length::Px(col_w)),
210            ..Default::default()
211        },
212        flex: fret_ui::element::FlexItemStyle {
213            shrink: 0.0,
214            ..Default::default()
215        },
216        ..Default::default()
217    }
218}
219
220fn table_fixed_column_fill_layout(col_w: Px) -> LayoutStyle {
221    let mut layout = table_fixed_column_layout(col_w);
222    layout.size.height = Length::Fill;
223    layout
224}
225
226fn table_fixed_column_clip_fill_layout(col_w: Px) -> LayoutStyle {
227    let mut layout = table_fixed_column_fill_layout(col_w);
228    layout.overflow = Overflow::Clip;
229    layout
230}
231
232fn table_wrap_horizontal_scroll<H: UiHost>(
233    cx: &mut ElementContext<'_, H>,
234    scroll_handle: Option<ScrollHandle>,
235    row: AnyElement,
236) -> AnyElement {
237    if let Some(scroll_handle) = scroll_handle {
238        cx.scroll(
239            ScrollProps {
240                axis: ScrollAxis::X,
241                scroll_handle: Some(scroll_handle),
242                layout: table_scroll_fill_layout(),
243                ..Default::default()
244            },
245            |_| vec![row],
246        )
247    } else {
248        row
249    }
250}
251
252fn take_single_root_test_id(children: &mut [AnyElement]) -> Option<Arc<str>> {
253    let mut found: Option<(usize, Arc<str>)> = None;
254
255    for (idx, child) in children.iter().enumerate() {
256        let Some(test_id) = child
257            .semantics_decoration
258            .as_ref()
259            .and_then(|decoration| decoration.test_id.as_ref())
260            .cloned()
261        else {
262            continue;
263        };
264
265        if found.is_some() {
266            return None;
267        }
268
269        found = Some((idx, test_id));
270    }
271
272    let (idx, test_id) = found?;
273    if let Some(decoration) = children[idx].semantics_decoration.as_mut() {
274        decoration.test_id = None;
275    }
276
277    Some(test_id)
278}
279
280fn table_wrapper_test_id(
281    children: &mut [AnyElement],
282    explicit: Option<Arc<str>>,
283) -> Option<Arc<str>> {
284    explicit.or_else(|| take_single_root_test_id(children))
285}
286
287#[allow(clippy::too_many_arguments)]
288fn retained_table_render_row_visuals<H: UiHost + 'static, TData: 'static>(
289    cx: &mut ElementContext<'_, H>,
290    data: Arc<[TData]>,
291    data_index: usize,
292    row_key: RowKey,
293    bg: Option<Color>,
294    props: TableViewProps,
295    border: Color,
296    cell_px: Px,
297    cell_py: Px,
298    key_handler: fret_ui::action::OnKeyDown,
299    columns: Arc<[ColumnDef<TData>]>,
300    col_widths: Arc<[Px]>,
301    cell_at: Arc<CellAt<H, TData>>,
302    row_cell_test_id_prefix: Option<Arc<str>>,
303    left_col_indices: Arc<[usize]>,
304    center_col_indices: Arc<[usize]>,
305    right_col_indices: Arc<[usize]>,
306    scroll_x: ScrollHandle,
307) -> AnyElement {
308    cx.key_on_key_down_for(cx.root_id(), key_handler);
309
310    cx.container(
311        ContainerProps {
312            background: bg,
313            layout: retained_table_row_fill_layout(),
314            ..Default::default()
315        },
316        move |cx| {
317            let render_row_group =
318                |cx: &mut ElementContext<'_, H>,
319                 col_indices: &[usize],
320                 scroll_x_for_group: Option<ScrollHandle>| {
321                    let columns = columns.clone();
322                    let col_widths = col_widths.clone();
323                    let cell_at = cell_at.clone();
324                    let row_cell_test_id_prefix = row_cell_test_id_prefix.clone();
325                    let data = data.clone();
326                    let props = props.clone();
327
328                    let row = ui::h_row(move |cx| {
329                        let original = &data[data_index];
330
331                        col_indices
332                            .iter()
333                            .map(|col_idx| {
334                                let col = &columns[*col_idx];
335                                let col_w = col_widths[*col_idx];
336                                let cell = (cell_at)(cx, col, original);
337
338                                let cell_test_id = row_cell_test_id_prefix.as_ref().map(|prefix| {
339                                    Arc::<str>::from(format!(
340                                        "{prefix}{row}-cell-{col}",
341                                        row = row_key.0,
342                                        col = col.id.as_ref()
343                                    ))
344                                });
345
346                                let cell = cx.container(
347                                    ContainerProps {
348                                        border: if props.optimize_grid_lines {
349                                            Edges::default()
350                                        } else {
351                                            Edges {
352                                                right: Px(1.0),
353                                                ..Default::default()
354                                            }
355                                        },
356                                        border_color: if props.optimize_grid_lines {
357                                            None
358                                        } else {
359                                            Some(border)
360                                        },
361                                        padding: Edges::symmetric(cell_px, cell_py).into(),
362                                        layout: table_fixed_column_layout(col_w),
363                                        ..Default::default()
364                                    },
365                                    move |_cx| vec![cell],
366                                );
367
368                                if let Some(test_id) = cell_test_id {
369                                    cx.semantics(
370                                        SemanticsProps {
371                                            test_id: Some(test_id),
372                                            ..Default::default()
373                                        },
374                                        move |_cx| vec![cell],
375                                    )
376                                } else {
377                                    cell
378                                }
379                            })
380                            .collect::<Vec<_>>()
381                    })
382                    .gap(Space::N0)
383                    .justify_start()
384                    .items_center()
385                    .into_element(cx);
386
387                    table_wrap_horizontal_scroll(cx, scroll_x_for_group, row)
388                };
389
390            let left = render_row_group(cx, left_col_indices.as_ref(), None);
391            let center = render_row_group(cx, center_col_indices.as_ref(), Some(scroll_x.clone()));
392            let right = render_row_group(cx, right_col_indices.as_ref(), None);
393
394            vec![
395                ui::h_row(|_cx| [left, center, right])
396                    .gap(Space::N0)
397                    .justify_start()
398                    .items_stretch()
399                    .layout(LayoutRefinement::default().w_full())
400                    .into_element(cx),
401            ]
402        },
403    )
404}
405
406#[derive(Debug, Clone)]
407pub struct TableViewProps {
408    pub size: Size,
409    pub row_height: Option<Px>,
410    /// Optional fixed header height (defaults to `row_height` when unset).
411    ///
412    /// This enables shadcn-style tables where the header row height differs from body row height.
413    pub header_height: Option<Px>,
414    /// Controls whether the virtualized body rows are treated as fixed-height or measured.
415    ///
416    /// - `Fixed` (default): fast path; row containers are forced to `row_height` and the virtualizer
417    ///   skips per-row measurement work.
418    /// - `Measured`: enables variable-height rows (e.g. wrapping Markdown) by letting the runtime
419    ///   measure visible rows and write sizes back into the virtualizer.
420    pub row_measure_mode: TableRowMeasureMode,
421    pub overscan: usize,
422    /// Optional retained-subtree budget for overscan window shifts (retained host path).
423    ///
424    /// When `None`, the default heuristic is `overscan * 2`.
425    ///
426    /// Larger values reduce remount/layout churn when the window oscillates across boundaries
427    /// (e.g. scroll bounce patterns), at the cost of retaining more offscreen subtrees.
428    pub keep_alive: Option<usize>,
429    pub default_column_width: Px,
430    pub min_column_width: Px,
431    /// When `true`, clicking a sortable header updates `TableState.sorting`.
432    ///
433    /// This is a UI-side interaction toggle; sorting math still lives in the headless engine.
434    pub enable_sorting: bool,
435    pub enable_column_resizing: bool,
436    pub column_resize_mode: ColumnResizeMode,
437    pub column_resize_direction: ColumnResizeDirection,
438    pub enable_column_grouping: bool,
439    pub grouped_column_mode: GroupedColumnMode,
440    pub enable_row_selection: bool,
441    pub single_row_selection: bool,
442    /// When `true` (default), pointer-activating a row toggles its selection state.
443    ///
444    /// Set this to `false` for shadcn-style recipes where selection is driven by an explicit
445    /// checkbox column (and row clicks should not toggle selection).
446    pub pointer_row_selection: bool,
447    /// Pointer selection policy (when `pointer_row_selection` is enabled).
448    pub pointer_row_selection_policy: PointerRowSelectionPolicy,
449    /// When `false`, pinned rows are only rendered if they are part of the current
450    /// filtered/sorted/paginated row model (TanStack `keepPinnedRows=false`).
451    ///
452    /// When `true` (default), pinned rows can remain visible even when they are outside the
453    /// current row model (TanStack default).
454    pub keep_pinned_rows: bool,
455    /// When `false`, the table does not render an outer border/radius frame.
456    ///
457    /// This is useful when embedding the table inside a higher-level component that owns the
458    /// surrounding chrome (e.g. a shadcn recipe with its own border + radius).
459    pub draw_frame: bool,
460    /// When enabled, paints table cell backgrounds/borders in a separate layer from cell content.
461    ///
462    /// This is a targeted performance knob intended to reduce renderer pipeline switches for
463    /// text-heavy tables by avoiding per-cell interleaving of quads and text draws.
464    ///
465    /// Note: this may increase UI tree complexity because it introduces an overlay layer per row.
466    pub optimize_paint_order: bool,
467    /// When enabled, draws only coarse column-group separators instead of per-cell vertical grid lines.
468    ///
469    /// This reduces quad count and renderer state churn for wide tables, but it also changes the visual
470    /// semantics of the grid (column-level separators are removed).
471    ///
472    /// Limitations / caveats:
473    ///
474    /// - Default: `false`.
475    /// - This intentionally trades per-column dividers for only `{left|center|right}` group dividers.
476    /// - It is not a stable styling contract. Prefer keeping it disabled unless profiling shows that
477    ///   per-cell dividers dominate quad count and pipeline switches for your workload.
478    /// - This may be replaced by a formal style option (e.g. `TableGridLines`) or removed entirely
479    ///   once a better default grid strategy exists.
480    pub optimize_grid_lines: bool,
481    /// Grouped-mode row pinning display policy.
482    ///
483    /// TanStack `table-core` exposes pinning via `getTopRows/getCenterRows/getBottomRows`, and the
484    /// most common UI recipe is to render pinned rows in dedicated top/bottom bands (removing them
485    /// from the paged center rows). `PromotePinnedRows` matches that TanStack-typical behavior and
486    /// is the default.
487    pub grouped_row_pinning_policy: GroupedRowPinningPolicy,
488}
489
490#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
491pub enum TableRowMeasureMode {
492    /// Fixed-height body rows (fast path).
493    #[default]
494    Fixed,
495    /// Variable-height body rows (measurement + write-back).
496    Measured,
497}
498
499#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
500pub enum PointerRowSelectionPolicy {
501    /// Legacy behavior: a row click toggles membership in `row_selection`.
502    #[default]
503    Toggle,
504    /// List-like behavior:
505    /// - no modifiers: exclusive selection (clears then selects)
506    /// - Ctrl/Meta: additive toggle
507    /// - Shift: range selection from anchor
508    ListLike,
509}
510
511#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
512pub enum GroupedRowPinningPolicy {
513    /// Promote pinned rows into top/bottom bands and remove duplicates from the paged center rows.
514    #[default]
515    PromotePinnedRows,
516    /// Keep pinned leaf rows inside their grouped hierarchy and keep the paged center rows
517    /// unchanged (no promotion).
518    PreserveHierarchy,
519}
520
521impl Default for TableViewProps {
522    fn default() -> Self {
523        Self {
524            size: Size::Medium,
525            row_height: None,
526            header_height: None,
527            row_measure_mode: TableRowMeasureMode::Fixed,
528            overscan: 2,
529            keep_alive: None,
530            default_column_width: Px(160.0),
531            min_column_width: Px(40.0),
532            enable_sorting: true,
533            enable_column_resizing: true,
534            column_resize_mode: ColumnResizeMode::OnEnd,
535            column_resize_direction: ColumnResizeDirection::Ltr,
536            enable_column_grouping: true,
537            grouped_column_mode: GroupedColumnMode::Reorder,
538            enable_row_selection: true,
539            single_row_selection: true,
540            pointer_row_selection: true,
541            pointer_row_selection_policy: PointerRowSelectionPolicy::Toggle,
542            keep_pinned_rows: true,
543            draw_frame: true,
544            optimize_paint_order: false,
545            optimize_grid_lines: false,
546            grouped_row_pinning_policy: GroupedRowPinningPolicy::PromotePinnedRows,
547        }
548    }
549}
550
551#[derive(Debug, Clone, Default, PartialEq, Eq)]
552pub struct TableViewOutput {
553    /// Total row count after filters (and grouping expansion), before pagination is applied.
554    pub filtered_row_count: usize,
555    pub pagination: PaginationBounds,
556}
557
558/// Debug/test-only anchors for virtualized table harnesses.
559///
560/// These ids are intended for scripted diagnostics and geometry assertions:
561/// - `header_row_test_id` targets the fixed header viewport row.
562/// - `header_cell_test_id_prefix` targets table-owned header cell layout wrappers.
563/// - `row_test_id_prefix` targets table-owned body row / cell layout wrappers.
564#[derive(Debug, Clone, Default)]
565pub struct TableDebugIds {
566    pub header_row_test_id: Option<Arc<str>>,
567    pub header_cell_test_id_prefix: Option<Arc<str>>,
568    pub row_test_id_prefix: Option<Arc<str>>,
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    const SOURCE: &str = include_str!("table.rs");
575    use fret_app::App;
576    use fret_core::{
577        AppWindowId, Event, Modifiers, MouseButton, PathCommand, PointerEvent, PointerId,
578        PointerType, SvgId, SvgService, TextBlobId, TextConstraints, TextInput, TextMetrics,
579        TextService,
580    };
581    use fret_core::{PathConstraints, PathId, PathMetrics, PathService, PathStyle};
582    use fret_core::{Point, Px, Rect, TextWrap};
583    use fret_ui::ThemeConfig;
584    use fret_ui::{Theme, UiTree, VirtualListScrollHandle};
585
586    #[test]
587    fn table_surfaces_keep_a_narrow_table_state_bridge() {
588        assert!(
589            SOURCE.contains("pub trait IntoTableStateModel {"),
590            "table surfaces should keep a dedicated TableState bridge instead of widening into a generic model conversion story"
591        );
592        assert!(
593            SOURCE.contains("pub fn table_virtualized<H: UiHost, TData, IHeader, TH, ICell, TC>(")
594                && SOURCE.contains("state: impl IntoTableStateModel,"),
595            "table_virtualized should accept the dedicated table-state bridge"
596        );
597        assert!(
598            SOURCE.contains("pub fn table_virtualized_retained_v0<H: UiHost + 'static, TData>("),
599            "retained table surface should accept the dedicated table-state bridge"
600        );
601        assert!(
602            !SOURCE.contains(
603                "pub fn table_virtualized<H: UiHost, TData, IHeader, TH, ICell, TC>(\n    cx: &mut ElementContext<'_, H>,\n    data: &[TData],\n    columns: &[ColumnDef<TData>],\n    state: Model<TableState>,"
604            ),
605            "table_virtualized should not regress to a raw Model<TableState>-only signature"
606        );
607    }
608
609    #[test]
610    fn retained_table_callbacks_prefer_explicit_context_access_capability() {
611        assert!(
612            SOURCE.contains("dyn for<'a> Fn(&mut dyn ElementContextAccess<'a, H>, &ColumnDef<TData>) -> AnyElement;")
613                || SOURCE.contains(
614                    "dyn for<'a> Fn(\n        &mut dyn ElementContextAccess<'a, H>,\n        &ColumnDef<TData>,\n    ) -> AnyElement;"
615                ),
616            "retained table header accessories should accept explicit context access capability"
617        );
618        assert!(
619            SOURCE.contains("dyn for<'a> Fn(&mut dyn ElementContextAccess<'a, H>, &ColumnDef<TData>, &TData) -> AnyElement;")
620                || SOURCE.contains(
621                    "dyn for<'a> Fn(\n        &mut dyn ElementContextAccess<'a, H>,\n        &ColumnDef<TData>,\n        &TData,\n    ) -> AnyElement;"
622                ),
623            "retained table cell renderers should accept explicit context access capability"
624        );
625    }
626
627    #[test]
628    fn table_debug_ids_expose_explicit_header_row_anchor() {
629        assert!(
630            SOURCE.contains("pub struct TableDebugIds {"),
631            "table harnesses should use a structured debug-id surface"
632        );
633        assert!(
634            SOURCE.contains("pub header_row_test_id: Option<Arc<str>>,"),
635            "table debug ids should expose an explicit header-row anchor"
636        );
637        assert!(
638            SOURCE.contains("debug_ids: TableDebugIds,"),
639            "table surfaces should accept a shared structured debug-id contract"
640        );
641    }
642
643    #[test]
644    fn table_virtualized_hoists_single_root_renderer_test_ids_to_layout_anchors() {
645        assert!(
646            SOURCE.contains(
647                "fn take_single_root_test_id(children: &mut [AnyElement]) -> Option<Arc<str>> {"
648            ),
649            "table_virtualized should keep the single-root test-id hoist helper close to the table surface"
650        );
651        assert!(
652            SOURCE.contains(
653                "fn table_wrapper_test_id(\n    children: &mut [AnyElement],\n    explicit: Option<Arc<str>>,\n) -> Option<Arc<str>> {"
654            ),
655            "table_virtualized should resolve explicit debug ids before falling back to renderer-root hoists"
656        );
657        assert!(
658            SOURCE.match_indices("table_wrapper_test_id(").count() >= 4,
659            "table_virtualized should hoist marked header/cell renderer roots onto stable layout anchors"
660        );
661    }
662
663    #[test]
664    fn table_virtualized_retained_accepts_capability_first_cell_renderer() {
665        let window = AppWindowId::default();
666        let mut app = App::new();
667        let mut ui: UiTree<App> = UiTree::new();
668        ui.set_window(window);
669        ui.set_debug_enabled(true);
670
671        Theme::with_global_mut(&mut app, |theme| {
672            theme.apply_config(&ThemeConfig {
673                name: "Test".to_string(),
674                ..ThemeConfig::default()
675            });
676        });
677
678        let mut state_value = TableState::default();
679        state_value.pagination.page_size = 1;
680        let state = app.models_mut().insert(state_value);
681
682        let data: Arc<[u32]> = Arc::from(vec![0u32]);
683        let columns: Arc<[ColumnDef<u32>]> = Arc::from(vec![{
684            let mut col = ColumnDef::new("name");
685            col.size = 220.0;
686            col
687        }]);
688        let scroll = VirtualListScrollHandle::new();
689
690        let bounds = Rect::new(
691            Point::new(Px(0.0), Px(0.0)),
692            fret_core::Size::new(Px(320.0), Px(240.0)),
693        );
694        let mut services = FakeServices;
695
696        let render = |ui: &mut UiTree<App>,
697                      app: &mut App,
698                      services: &mut FakeServices|
699         -> fret_core::NodeId {
700            fret_ui::declarative::render_root(ui, app, services, window, bounds, "test", |cx| {
701                vec![table_virtualized_retained_v0(
702                    cx,
703                    data.clone(),
704                    columns.clone(),
705                    state.clone(),
706                    &scroll,
707                    0,
708                    Arc::new(|_row: &u32, index: usize| RowKey::from_index(index)),
709                    None,
710                    TableViewProps::default(),
711                    Arc::new(|col: &ColumnDef<u32>| Arc::from(col.id.as_ref())),
712                    None,
713                    Arc::new(retained_table_capability_test_cell),
714                    TableDebugIds {
715                        header_cell_test_id_prefix: Some(Arc::<str>::from(
716                            "table-retained-capability-header-",
717                        )),
718                        row_test_id_prefix: Some(Arc::<str>::from(
719                            "table-retained-capability-row-",
720                        )),
721                        ..Default::default()
722                    },
723                )]
724            })
725        };
726
727        for _ in 0..2 {
728            let root = render(&mut ui, &mut app, &mut services);
729            ui.set_root(root);
730            ui.request_semantics_snapshot();
731            ui.layout_all(&mut app, &mut services, bounds, 1.0);
732            let mut scene = fret_core::Scene::default();
733            ui.paint_all(&mut app, &mut services, bounds, &mut scene, 1.0);
734        }
735
736        let snap = ui
737            .semantics_snapshot()
738            .expect("expected semantics snapshot after retained table render");
739
740        assert!(
741            snap.nodes.iter().any(
742                |node| node.test_id.as_deref() == Some("table-retained-capability-cell-name-0")
743            ),
744            "expected retained table capability-first cell renderer to contribute its semantics marker"
745        );
746    }
747
748    #[test]
749    fn single_sort_toggle_cycles_and_resets_page_index() {
750        let id: ColumnId = Arc::from("col");
751        let mut state = TableState::default();
752        state.pagination.page_index = 3;
753        assert!(state.sorting.is_empty());
754
755        apply_single_sort_toggle(&mut state, &id);
756        assert_eq!(state.pagination.page_index, 0);
757        assert_eq!(state.sorting.len(), 1);
758        assert_eq!(state.sorting[0].column.as_ref(), id.as_ref());
759        assert!(!state.sorting[0].desc);
760
761        state.pagination.page_index = 2;
762        apply_single_sort_toggle(&mut state, &id);
763        assert_eq!(state.pagination.page_index, 0);
764        assert_eq!(state.sorting.len(), 1);
765        assert!(state.sorting[0].desc);
766
767        state.pagination.page_index = 1;
768        apply_single_sort_toggle(&mut state, &id);
769        assert_eq!(state.pagination.page_index, 0);
770        assert!(state.sorting.is_empty());
771    }
772
773    #[test]
774    fn collect_leaf_keys_skips_group_rows() {
775        let meta = vec![
776            TableNavRowMeta {
777                row_key: RowKey(1),
778                kind: TableNavRowKind::Leaf,
779                data_index: Some(0),
780                label: Arc::from("Alpha"),
781            },
782            TableNavRowMeta {
783                row_key: RowKey(2),
784                kind: TableNavRowKind::Group,
785                data_index: None,
786                label: Arc::from("Group"),
787            },
788            TableNavRowMeta {
789                row_key: RowKey(3),
790                kind: TableNavRowKind::Leaf,
791                data_index: Some(1),
792                label: Arc::from("Beta"),
793            },
794        ];
795
796        assert_eq!(table_collect_leaf_keys(&meta), vec![RowKey(1), RowKey(3)]);
797        assert_eq!(
798            table_collect_leaf_keys_in_range(&meta, 0, 2),
799            vec![RowKey(1), RowKey(3)]
800        );
801        assert_eq!(
802            table_collect_leaf_keys_in_range(&meta, 1, 1),
803            Vec::<RowKey>::new()
804        );
805        assert_eq!(
806            table_collect_leaf_keys_in_range(&meta, 99, 100),
807            vec![RowKey(3)]
808        );
809    }
810
811    #[test]
812    fn apply_row_pinning_to_paged_rows_surfaces_pinned_outside_page_and_dedupes() {
813        let visible_all = vec![
814            DisplayRow::Leaf {
815                data_index: 0,
816                row_key: RowKey(1),
817                depth: 0,
818            },
819            DisplayRow::Leaf {
820                data_index: 1,
821                row_key: RowKey(2),
822                depth: 0,
823            },
824            DisplayRow::Leaf {
825                data_index: 2,
826                row_key: RowKey(3),
827                depth: 0,
828            },
829        ];
830
831        let page_rows = vec![visible_all[1].clone(), visible_all[2].clone()];
832
833        let row_pinning = crate::headless::table::RowPinningState {
834            top: vec![RowKey(1)],
835            bottom: vec![RowKey(3)],
836        };
837
838        let out = apply_row_pinning_to_paged_rows(&visible_all, &page_rows, &row_pinning);
839        let keys = out.into_iter().map(|r| r.row_key()).collect::<Vec<_>>();
840        assert_eq!(keys, vec![RowKey(1), RowKey(2), RowKey(3)]);
841    }
842
843    #[test]
844    fn grouped_row_pinning_policy_preserve_hierarchy_keeps_page_rows_center_unchanged() {
845        let visible_all = vec![
846            DisplayRow::Leaf {
847                data_index: 0,
848                row_key: RowKey(1),
849                depth: 0,
850            },
851            DisplayRow::Leaf {
852                data_index: 1,
853                row_key: RowKey(2),
854                depth: 0,
855            },
856            DisplayRow::Leaf {
857                data_index: 2,
858                row_key: RowKey(3),
859                depth: 0,
860            },
861        ];
862
863        let page_rows_center = vec![visible_all[1].clone(), visible_all[2].clone()];
864        let row_pinning = crate::headless::table::RowPinningState {
865            top: vec![RowKey(1)],
866            bottom: vec![RowKey(3)],
867        };
868
869        let out = apply_grouped_row_pinning_policy(
870            &visible_all,
871            &page_rows_center,
872            &row_pinning,
873            GroupedRowPinningPolicy::PreserveHierarchy,
874        );
875        let keys = out.into_iter().map(|r| r.row_key()).collect::<Vec<_>>();
876        assert_eq!(keys, vec![RowKey(2), RowKey(3)]);
877    }
878
879    #[test]
880    fn grouped_row_pinning_policy_promote_pinned_rows_matches_legacy_behavior() {
881        let visible_all = vec![
882            DisplayRow::Leaf {
883                data_index: 0,
884                row_key: RowKey(1),
885                depth: 0,
886            },
887            DisplayRow::Leaf {
888                data_index: 1,
889                row_key: RowKey(2),
890                depth: 0,
891            },
892            DisplayRow::Leaf {
893                data_index: 2,
894                row_key: RowKey(3),
895                depth: 0,
896            },
897        ];
898
899        let page_rows_center = vec![visible_all[1].clone(), visible_all[2].clone()];
900        let row_pinning = crate::headless::table::RowPinningState {
901            top: vec![RowKey(1)],
902            bottom: vec![RowKey(3)],
903        };
904
905        let out = apply_grouped_row_pinning_policy(
906            &visible_all,
907            &page_rows_center,
908            &row_pinning,
909            GroupedRowPinningPolicy::PromotePinnedRows,
910        );
911        let keys = out.into_iter().map(|r| r.row_key()).collect::<Vec<_>>();
912        assert_eq!(keys, vec![RowKey(1), RowKey(2), RowKey(3)]);
913    }
914
915    #[derive(Default)]
916    struct FakeServices;
917
918    impl TextService for FakeServices {
919        fn prepare(
920            &mut self,
921            _input: &TextInput,
922            _constraints: TextConstraints,
923        ) -> (TextBlobId, TextMetrics) {
924            (
925                TextBlobId::default(),
926                TextMetrics {
927                    size: fret_core::Size::new(Px(0.0), Px(0.0)),
928                    baseline: Px(0.0),
929                },
930            )
931        }
932
933        fn release(&mut self, _blob: TextBlobId) {}
934    }
935
936    impl PathService for FakeServices {
937        fn prepare(
938            &mut self,
939            _commands: &[PathCommand],
940            _style: PathStyle,
941            _constraints: PathConstraints,
942        ) -> (PathId, PathMetrics) {
943            (PathId::default(), PathMetrics::default())
944        }
945
946        fn release(&mut self, _path: PathId) {}
947    }
948
949    impl SvgService for FakeServices {
950        fn register_svg(&mut self, _bytes: &[u8]) -> SvgId {
951            SvgId::default()
952        }
953
954        fn unregister_svg(&mut self, _svg: SvgId) -> bool {
955            true
956        }
957    }
958
959    impl fret_core::MaterialService for FakeServices {
960        fn register_material(
961            &mut self,
962            _desc: fret_core::MaterialDescriptor,
963        ) -> Result<fret_core::MaterialId, fret_core::MaterialRegistrationError> {
964            Err(fret_core::MaterialRegistrationError::Unsupported)
965        }
966
967        fn unregister_material(&mut self, _id: fret_core::MaterialId) -> bool {
968            true
969        }
970    }
971
972    fn retained_table_capability_test_cell<'a>(
973        cx: &mut dyn ElementContextAccess<'a, App>,
974        col: &ColumnDef<u32>,
975        row: &u32,
976    ) -> AnyElement {
977        let cx = cx.elements();
978        let label = format!("{}-{row}", col.id.as_ref());
979        let test_id = Arc::<str>::from(format!("table-retained-capability-cell-{label}"));
980        cx.semantics(
981            SemanticsProps {
982                test_id: Some(test_id),
983                ..Default::default()
984            },
985            move |cx| vec![cx.text(label.clone())],
986        )
987    }
988
989    fn capture_layout_sidecar(
990        ui: &UiTree<App>,
991        app: &mut App,
992        window: AppWindowId,
993        root: fret_core::NodeId,
994        bounds: Rect,
995    ) -> serde_json::Value {
996        let nonce = std::time::SystemTime::now()
997            .duration_since(std::time::UNIX_EPOCH)
998            .expect("system clock should be after unix epoch")
999            .as_nanos();
1000        let out_dir = std::env::temp_dir().join(format!(
1001            "fret-ui-kit-table-layout-sidecar-{}-{nonce}",
1002            std::process::id()
1003        ));
1004        let _ = std::fs::remove_dir_all(&out_dir);
1005
1006        let path = ui
1007            .debug_write_layout_sidecar_taffy_v1_json(
1008                app, window, root, bounds, 1.0, None, &out_dir, 0,
1009            )
1010            .expect("layout sidecar should be written");
1011
1012        let sidecar: serde_json::Value =
1013            serde_json::from_slice(&std::fs::read(&path).expect("sidecar should be readable"))
1014                .expect("sidecar json should parse");
1015        let _ = std::fs::remove_dir_all(&out_dir);
1016        sidecar
1017    }
1018
1019    fn layout_sidecar_abs_rect(sidecar: &serde_json::Value, test_id: &str) -> Rect {
1020        let mut candidates: Vec<&serde_json::Value> = sidecar["taffy"]["nodes"]
1021            .as_array()
1022            .into_iter()
1023            .flat_map(|nodes| nodes.iter())
1024            .collect();
1025        if let Some(roots) = sidecar["taffy"]["roots"].as_array() {
1026            for root in roots {
1027                if let Some(nodes) = root["dump"]["nodes"].as_array() {
1028                    candidates.extend(nodes.iter());
1029                }
1030            }
1031        }
1032
1033        let matches = candidates
1034            .iter()
1035            .filter(|node| node["debug"]["test_id"].as_str() == Some(test_id))
1036            .copied()
1037            .collect::<Vec<_>>();
1038        let matched = matches
1039            .into_iter()
1040            .max_by(|a, b| {
1041                let area = |node: &serde_json::Value| {
1042                    let rect = &node["abs_rect"];
1043                    rect["w"].as_f64().unwrap_or(0.0) * rect["h"].as_f64().unwrap_or(0.0)
1044                };
1045                area(a)
1046                    .partial_cmp(&area(b))
1047                    .unwrap_or(std::cmp::Ordering::Equal)
1048            })
1049            .unwrap_or_else(|| {
1050                let available = candidates
1051                    .iter()
1052                    .filter_map(|node| node["debug"]["test_id"].as_str())
1053                    .collect::<Vec<_>>();
1054                panic!(
1055                    "expected layout sidecar node with test_id `{test_id}`; available_test_ids={available:?}"
1056                );
1057            });
1058        let abs = &matched["abs_rect"];
1059        Rect::new(
1060            Point::new(
1061                Px(abs["x"].as_f64().expect("abs_rect.x should be a number") as f32),
1062                Px(abs["y"].as_f64().expect("abs_rect.y should be a number") as f32),
1063            ),
1064            fret_core::Size::new(
1065                Px(abs["w"].as_f64().expect("abs_rect.w should be a number") as f32),
1066                Px(abs["h"].as_f64().expect("abs_rect.h should be a number") as f32),
1067            ),
1068        )
1069    }
1070
1071    #[test]
1072    fn table_virtualized_copyable_reports_availability_and_emits_clipboard_text() {
1073        let window = AppWindowId::default();
1074        let mut app = App::new();
1075        let mut caps = fret_runtime::PlatformCapabilities::default();
1076        caps.clipboard.text.read = true;
1077        caps.clipboard.text.write = true;
1078        app.set_global(caps);
1079
1080        let mut ui: UiTree<App> = UiTree::new();
1081        ui.set_window(window);
1082
1083        Theme::with_global_mut(&mut app, |theme| {
1084            theme.apply_config(&ThemeConfig {
1085                name: "Test".to_string(),
1086                ..ThemeConfig::default()
1087            });
1088        });
1089
1090        let state = app.models_mut().insert(TableState::default());
1091        let data = vec![0u32, 1u32, 2u32];
1092        let columns = vec![ColumnDef::new("col")];
1093        let scroll = VirtualListScrollHandle::new();
1094
1095        let bounds = Rect::new(
1096            Point::new(Px(0.0), Px(0.0)),
1097            fret_core::Size::new(Px(320.0), Px(200.0)),
1098        );
1099        let mut services = FakeServices;
1100
1101        let render = |ui: &mut UiTree<App>,
1102                      app: &mut App,
1103                      services: &mut FakeServices|
1104         -> fret_core::NodeId {
1105            fret_ui::declarative::render_root(ui, app, services, window, bounds, "test", |cx| {
1106                vec![table_virtualized_copyable(
1107                    cx,
1108                    &data,
1109                    &columns,
1110                    state.clone(),
1111                    &scroll,
1112                    0,
1113                    &|_row, i| RowKey::from_index(i),
1114                    None,
1115                    TableViewProps::default(),
1116                    Arc::new(|_models, i| Some(format!("Row {i}"))),
1117                    |_row| None,
1118                    |cx, _col, _sort| [cx.text("Header")],
1119                    |cx, row, _col| [cx.text(format!("Cell {}", row.index))],
1120                    None,
1121                    TableDebugIds::default(),
1122                )]
1123            })
1124        };
1125
1126        // VirtualList computes the visible window based on viewport metrics populated during layout,
1127        // so it takes two frames for the first set of rows to mount.
1128        let mut root = fret_core::NodeId::default();
1129        for _ in 0..2 {
1130            root = render(&mut ui, &mut app, &mut services);
1131            ui.set_root(root);
1132            ui.layout_all(&mut app, &mut services, bounds, 1.0);
1133            let mut scene = fret_core::Scene::default();
1134            ui.paint_all(&mut app, &mut services, bounds, &mut scene, 1.0);
1135        }
1136
1137        let table_node = ui.children(root)[0];
1138        ui.set_focus(Some(table_node));
1139
1140        let copy = CommandId::from("edit.copy");
1141        assert!(
1142            !ui.is_command_available(&mut app, &copy),
1143            "expected edit.copy to be unavailable when selection is empty"
1144        );
1145        assert!(
1146            ui.dispatch_command(&mut app, &mut services, &copy),
1147            "expected edit.copy to be handled by the table surface"
1148        );
1149        let effects = app.flush_effects();
1150        assert!(
1151            !effects
1152                .iter()
1153                .any(|e| matches!(e, fret_runtime::Effect::ClipboardWriteText { .. })),
1154            "expected edit.copy to not emit ClipboardWriteText when selection is empty"
1155        );
1156
1157        let _ = app.models_mut().update(&state, |st| {
1158            st.row_selection.insert(RowKey::from_index(1));
1159        });
1160
1161        assert!(
1162            ui.is_command_available(&mut app, &copy),
1163            "expected edit.copy to be available when selection is non-empty"
1164        );
1165        assert!(
1166            ui.dispatch_command(&mut app, &mut services, &copy),
1167            "expected edit.copy to be handled by the table surface"
1168        );
1169        let effects = app.flush_effects();
1170        assert!(
1171            effects.iter().any(|e| {
1172                matches!(e, fret_runtime::Effect::ClipboardWriteText { text, .. } if text == "Row 1")
1173            }),
1174            "expected edit.copy to emit ClipboardWriteText for the selected row"
1175        );
1176    }
1177
1178    #[test]
1179    fn table_virtualized_clamps_cell_width_for_wide_text() {
1180        let window = AppWindowId::default();
1181        let mut app = App::new();
1182        let mut ui: UiTree<App> = UiTree::new();
1183        ui.set_window(window);
1184
1185        Theme::with_global_mut(&mut app, |theme| {
1186            theme.apply_config(&ThemeConfig {
1187                name: "Test".to_string(),
1188                ..ThemeConfig::default()
1189            });
1190        });
1191
1192        let mut state_value = TableState::default();
1193        state_value.pagination.page_size = 1;
1194        let state = app.models_mut().insert(state_value);
1195
1196        let data = vec![0u32];
1197        let mut col = ColumnDef::new("col");
1198        col.size = 80.0;
1199        let columns = vec![col];
1200        let scroll = VirtualListScrollHandle::new();
1201
1202        let bounds = Rect::new(
1203            Point::new(Px(0.0), Px(0.0)),
1204            fret_core::Size::new(Px(320.0), Px(200.0)),
1205        );
1206        let mut services = FakeServices;
1207
1208        let render = |ui: &mut UiTree<App>,
1209                      app: &mut App,
1210                      services: &mut FakeServices|
1211         -> fret_core::NodeId {
1212            fret_ui::declarative::render_root(ui, app, services, window, bounds, "test", |cx| {
1213                vec![table_virtualized(
1214                    cx,
1215                    &data,
1216                    &columns,
1217                    state.clone(),
1218                    &scroll,
1219                    0,
1220                    &|_row, i| RowKey::from_index(i),
1221                    None,
1222                    TableViewProps::default(),
1223                    |_row| None,
1224                    move |cx, _col, _sort| {
1225                        let header = cx.semantics(
1226                            SemanticsProps {
1227                                test_id: Some(Arc::<str>::from("table-test-header")),
1228                                ..Default::default()
1229                            },
1230                            |cx| {
1231                                vec![cx.container(
1232                                    ContainerProps {
1233                                        layout: LayoutStyle {
1234                                            size: fret_ui::element::SizeStyle {
1235                                                width: Length::Fill,
1236                                                height: Length::Fill,
1237                                                ..Default::default()
1238                                            },
1239                                            overflow: Overflow::Clip,
1240                                            ..Default::default()
1241                                        },
1242                                        ..Default::default()
1243                                    },
1244                                    |cx| vec![crate::ui::text("Header").into_element(cx)],
1245                                )]
1246                            },
1247                        );
1248                        [header]
1249                    },
1250                    |cx, row, _col| {
1251                        let long = format!("Row{}-{}", row.index, "x".repeat(4096));
1252                        let cell = crate::ui::text(long).wrap(TextWrap::Grapheme);
1253                        let cell = cx.container(
1254                            ContainerProps {
1255                                layout: LayoutStyle {
1256                                    size: fret_ui::element::SizeStyle {
1257                                        width: Length::Fill,
1258                                        height: Length::Fill,
1259                                        ..Default::default()
1260                                    },
1261                                    overflow: Overflow::Clip,
1262                                    ..Default::default()
1263                                },
1264                                ..Default::default()
1265                            },
1266                            |cx| vec![cell.into_element(cx)],
1267                        );
1268
1269                        [cx.semantics(
1270                            SemanticsProps {
1271                                test_id: Some(Arc::<str>::from("table-test-cell")),
1272                                ..Default::default()
1273                            },
1274                            move |_cx| vec![cell],
1275                        )]
1276                    },
1277                    None,
1278                    TableDebugIds::default(),
1279                )]
1280            })
1281        };
1282
1283        // VirtualList computes the visible window based on viewport metrics populated during layout,
1284        // so it takes two frames for the first set of rows to mount.
1285        for _ in 0..3 {
1286            let root = render(&mut ui, &mut app, &mut services);
1287            ui.set_root(root);
1288            ui.request_semantics_snapshot();
1289            ui.layout_all(&mut app, &mut services, bounds, 1.0);
1290            let mut scene = fret_core::Scene::default();
1291            ui.paint_all(&mut app, &mut services, bounds, &mut scene, 1.0);
1292        }
1293
1294        let snap = ui
1295            .semantics_snapshot()
1296            .expect("expected a semantics snapshot");
1297
1298        let cell_bounds = snap
1299            .nodes
1300            .iter()
1301            .find(|n| n.test_id.as_deref() == Some("table-test-cell"))
1302            .map(|n| n.bounds)
1303            .expect("expected to find table-test-cell");
1304
1305        assert!(
1306            cell_bounds.size.width.0 <= 80.0,
1307            "expected the cell subtree to be clamped to the column width (got {:.2}px)",
1308            cell_bounds.size.width.0
1309        );
1310    }
1311
1312    #[test]
1313    fn table_virtualized_alignment_gate_header_matches_rows_under_overflow_and_variable_height() {
1314        let window = AppWindowId::default();
1315        let mut app = App::new();
1316        let mut ui: UiTree<App> = UiTree::new();
1317        ui.set_window(window);
1318
1319        Theme::with_global_mut(&mut app, |theme| {
1320            theme.apply_config(&ThemeConfig {
1321                name: "Test".to_string(),
1322                ..ThemeConfig::default()
1323            });
1324        });
1325
1326        let mut state_value = TableState::default();
1327        state_value.pagination.page_size = 3;
1328        let state = app.models_mut().insert(state_value);
1329
1330        let data = vec![0u32, 1u32, 2u32];
1331        let columns = vec![
1332            {
1333                let mut col = ColumnDef::new("name");
1334                col.size = 220.0;
1335                col
1336            },
1337            {
1338                let mut col = ColumnDef::new("status");
1339                col.size = 140.0;
1340                col
1341            },
1342            {
1343                let mut col = ColumnDef::new("cpu%");
1344                col.size = 90.0;
1345                col
1346            },
1347            {
1348                let mut col = ColumnDef::new("mem_mb");
1349                col.size = 110.0;
1350                col
1351            },
1352        ];
1353        let scroll = VirtualListScrollHandle::new();
1354
1355        let bounds = Rect::new(
1356            Point::new(Px(0.0), Px(0.0)),
1357            fret_core::Size::new(Px(320.0), Px(240.0)),
1358        );
1359        let mut services = FakeServices;
1360
1361        let props = TableViewProps {
1362            draw_frame: false,
1363            row_measure_mode: TableRowMeasureMode::Measured,
1364            ..Default::default()
1365        };
1366
1367        let render = |ui: &mut UiTree<App>,
1368                      app: &mut App,
1369                      services: &mut FakeServices|
1370         -> fret_core::NodeId {
1371            fret_ui::declarative::render_root(ui, app, services, window, bounds, "test", |cx| {
1372                vec![table_virtualized(
1373                    cx,
1374                    &data,
1375                    &columns,
1376                    state.clone(),
1377                    &scroll,
1378                    0,
1379                    &|_row, i| RowKey::from_index(i),
1380                    None,
1381                    props.clone(),
1382                    |_row| None,
1383                    |cx, col, _sort| {
1384                        let label = Arc::<str>::from(col.id.as_ref());
1385                        [cx.container(
1386                            ContainerProps {
1387                                layout: table_clip_fill_layout(),
1388                                ..Default::default()
1389                            },
1390                            move |_cx| vec![crate::ui::text(label.clone()).into_element(_cx)],
1391                        )]
1392                    },
1393                    |cx, row, col| {
1394                        let text = match col.id.as_ref() {
1395                            "name" => {
1396                                if row.index == 1 {
1397                                    format!("Row {} (details)\nMore text to force wrap", row.index)
1398                                } else {
1399                                    format!("Row {}", row.index)
1400                                }
1401                            }
1402                            "status" => "Running".to_string(),
1403                            "cpu%" => "42%".to_string(),
1404                            "mem_mb" => "256 MB".to_string(),
1405                            _ => "?".to_string(),
1406                        };
1407                        let cell = crate::ui::text(text).wrap(TextWrap::Grapheme);
1408                        [cx.container(
1409                            ContainerProps {
1410                                layout: table_clip_fill_layout(),
1411                                ..Default::default()
1412                            },
1413                            move |_cx| vec![cell.clone().into_element(_cx)],
1414                        )]
1415                    },
1416                    None,
1417                    TableDebugIds {
1418                        header_cell_test_id_prefix: Some(Arc::<str>::from("table-align-header-")),
1419                        row_test_id_prefix: Some(Arc::<str>::from("table-align-row-")),
1420                        ..Default::default()
1421                    },
1422                )]
1423            })
1424        };
1425
1426        // VirtualList computes the visible window based on viewport metrics populated during layout,
1427        // so it takes two frames for the first set of rows to mount.
1428        let mut root = fret_core::NodeId::default();
1429        for _ in 0..2 {
1430            root = render(&mut ui, &mut app, &mut services);
1431            ui.set_root(root);
1432            ui.request_semantics_snapshot();
1433            ui.layout_all(&mut app, &mut services, bounds, 1.0);
1434            let mut scene = fret_core::Scene::default();
1435            ui.paint_all(&mut app, &mut services, bounds, &mut scene, 1.0);
1436        }
1437
1438        let snap = ui
1439            .semantics_snapshot()
1440            .expect("expected semantics snapshot after table render");
1441        let col_ids = ["name", "status", "cpu%", "mem_mb"];
1442        for col_id in col_ids {
1443            let header_test_id = format!("table-align-header-{col_id}");
1444            let header_count = snap
1445                .nodes
1446                .iter()
1447                .filter(|node| node.test_id.as_deref() == Some(header_test_id.as_str()))
1448                .count();
1449            assert_eq!(
1450                header_count, 1,
1451                "expected exactly one semantics node for hoisted header anchor {header_test_id}"
1452            );
1453
1454            for row in 0..3 {
1455                let cell_test_id = format!("table-align-row-{row}-cell-{col_id}");
1456                let cell_count = snap
1457                    .nodes
1458                    .iter()
1459                    .filter(|node| node.test_id.as_deref() == Some(cell_test_id.as_str()))
1460                    .count();
1461                assert_eq!(
1462                    cell_count, 1,
1463                    "expected exactly one semantics node for hoisted cell anchor {cell_test_id}"
1464                );
1465            }
1466        }
1467
1468        let sidecar = capture_layout_sidecar(&ui, &mut app, window, root, bounds);
1469
1470        // `table_virtualized` composes header/body content under slightly different chrome
1471        // (e.g. grid line dividers vs resize handles). We want a strict gate for x alignment
1472        // (columns must not shift across rows), while allowing a small tolerance for the
1473        // content-box width when borders are involved.
1474        let eps_x = 0.5;
1475        let eps_w = 1.0;
1476        for col_id in col_ids {
1477            let header = layout_sidecar_abs_rect(&sidecar, &format!("table-align-header-{col_id}"));
1478            for row in 0..3 {
1479                let cell = layout_sidecar_abs_rect(
1480                    &sidecar,
1481                    &format!("table-align-row-{row}-cell-{col_id}"),
1482                );
1483                assert!(
1484                    (cell.origin.x.0 - header.origin.x.0).abs() <= eps_x,
1485                    "expected header/cell x alignment for col={col_id} row={row} (header_x={:.2}px cell_x={:.2}px)",
1486                    header.origin.x.0,
1487                    cell.origin.x.0
1488                );
1489                assert!(
1490                    (cell.size.width.0 - header.size.width.0).abs() <= eps_w,
1491                    "expected header/cell width alignment for col={col_id} row={row} (header_w={:.2}px cell_w={:.2}px)",
1492                    header.size.width.0,
1493                    cell.size.width.0
1494                );
1495            }
1496        }
1497    }
1498
1499    #[test]
1500    fn table_virtualized_pointer_select_does_not_shift_row_bounds() {
1501        let window = AppWindowId::default();
1502        let mut app = App::new();
1503        let mut ui: UiTree<App> = UiTree::new();
1504        ui.set_window(window);
1505
1506        Theme::with_global_mut(&mut app, |theme| {
1507            theme.apply_config(&ThemeConfig {
1508                name: "Test".to_string(),
1509                ..ThemeConfig::default()
1510            });
1511        });
1512
1513        let mut state_value = TableState::default();
1514        state_value.pagination.page_size = 5;
1515        let state = app.models_mut().insert(state_value);
1516
1517        let data = vec![0u32, 1u32, 2u32, 3u32, 4u32];
1518        let mut col = ColumnDef::new("name");
1519        col.size = 220.0;
1520        let columns = vec![col];
1521        let scroll = VirtualListScrollHandle::new();
1522
1523        let bounds = Rect::new(
1524            Point::new(Px(0.0), Px(0.0)),
1525            fret_core::Size::new(Px(320.0), Px(200.0)),
1526        );
1527        let mut services = FakeServices;
1528
1529        let props = TableViewProps {
1530            draw_frame: false,
1531            enable_column_resizing: false,
1532            enable_row_selection: true,
1533            single_row_selection: true,
1534            row_height: Some(Px(40.0)),
1535            header_height: Some(Px(40.0)),
1536            ..Default::default()
1537        };
1538
1539        let render = |ui: &mut UiTree<App>,
1540                      app: &mut App,
1541                      services: &mut FakeServices|
1542         -> fret_core::NodeId {
1543            fret_ui::declarative::render_root(ui, app, services, window, bounds, "test", |cx| {
1544                vec![table_virtualized(
1545                    cx,
1546                    &data,
1547                    &columns,
1548                    state.clone(),
1549                    &scroll,
1550                    0,
1551                    &|_row, i| RowKey::from_index(i),
1552                    None,
1553                    props.clone(),
1554                    |_row| None,
1555                    |cx, _col, _sort| [cx.text("")],
1556                    |cx, row, _col| {
1557                        let mut fill = LayoutStyle::default();
1558                        fill.size.width = Length::Fill;
1559                        fill.size.height = Length::Fill;
1560                        let marker = cx.container(
1561                            ContainerProps {
1562                                layout: fill,
1563                                ..Default::default()
1564                            },
1565                            |_| Vec::new(),
1566                        );
1567                        let _ = row;
1568                        [marker]
1569                    },
1570                    None,
1571                    TableDebugIds {
1572                        row_test_id_prefix: Some(Arc::<str>::from("table-test-row-")),
1573                        ..Default::default()
1574                    },
1575                )]
1576            })
1577        };
1578
1579        for _ in 0..2 {
1580            let root = render(&mut ui, &mut app, &mut services);
1581            ui.set_root(root);
1582            ui.request_semantics_snapshot();
1583            ui.layout_all(&mut app, &mut services, bounds, 1.0);
1584            let mut scene = fret_core::Scene::default();
1585            ui.paint_all(&mut app, &mut services, bounds, &mut scene, 1.0);
1586        }
1587
1588        let snap = ui
1589            .semantics_snapshot()
1590            .expect("expected a semantics snapshot");
1591        let before = snap
1592            .nodes
1593            .iter()
1594            .find(|n| n.test_id.as_deref() == Some("table-test-row-1"))
1595            .map(|n| n.bounds)
1596            .expect("expected marker node");
1597
1598        let click_pos = Point::new(
1599            Px(before.origin.x.0 + before.size.width.0 * 0.5),
1600            Px(before.origin.y.0 + before.size.height.0 * 0.5),
1601        );
1602
1603        ui.dispatch_event(
1604            &mut app,
1605            &mut services,
1606            &Event::Pointer(PointerEvent::Down {
1607                position: click_pos,
1608                button: MouseButton::Left,
1609                modifiers: Modifiers::default(),
1610                click_count: 1,
1611                pointer_id: PointerId(0),
1612                pointer_type: PointerType::Mouse,
1613            }),
1614        );
1615        ui.dispatch_event(
1616            &mut app,
1617            &mut services,
1618            &Event::Pointer(PointerEvent::Up {
1619                position: click_pos,
1620                button: MouseButton::Left,
1621                modifiers: Modifiers::default(),
1622                click_count: 1,
1623                is_click: true,
1624                pointer_id: PointerId(0),
1625                pointer_type: PointerType::Mouse,
1626            }),
1627        );
1628
1629        for _ in 0..2 {
1630            let root = render(&mut ui, &mut app, &mut services);
1631            ui.set_root(root);
1632            ui.request_semantics_snapshot();
1633            ui.layout_all(&mut app, &mut services, bounds, 1.0);
1634            let mut scene = fret_core::Scene::default();
1635            ui.paint_all(&mut app, &mut services, bounds, &mut scene, 1.0);
1636        }
1637
1638        let snap = ui
1639            .semantics_snapshot()
1640            .expect("expected a semantics snapshot");
1641        let after = snap
1642            .nodes
1643            .iter()
1644            .find(|n| n.test_id.as_deref() == Some("table-test-row-1"))
1645            .map(|n| n.bounds)
1646            .expect("expected marker node");
1647
1648        assert!(
1649            (after.origin.y.0 - before.origin.y.0).abs() <= 0.1,
1650            "expected row marker to keep stable y; before={:?} after={:?}",
1651            before,
1652            after
1653        );
1654    }
1655
1656    #[test]
1657    fn table_virtualized_can_disable_pointer_row_selection() {
1658        let window = AppWindowId::default();
1659        let mut app = App::new();
1660        let mut ui: UiTree<App> = UiTree::new();
1661        ui.set_window(window);
1662
1663        Theme::with_global_mut(&mut app, |theme| {
1664            theme.apply_config(&ThemeConfig {
1665                name: "Test".to_string(),
1666                ..ThemeConfig::default()
1667            });
1668        });
1669
1670        let mut state_value = TableState::default();
1671        state_value.pagination.page_size = 5;
1672        let state = app.models_mut().insert(state_value);
1673
1674        let data = vec![0u32, 1u32, 2u32, 3u32, 4u32];
1675        let mut col = ColumnDef::new("name");
1676        col.size = 220.0;
1677        let columns = vec![col];
1678        let scroll = VirtualListScrollHandle::new();
1679
1680        let bounds = Rect::new(
1681            Point::new(Px(0.0), Px(0.0)),
1682            fret_core::Size::new(Px(320.0), Px(200.0)),
1683        );
1684        let mut services = FakeServices;
1685
1686        let props = TableViewProps {
1687            draw_frame: false,
1688            enable_column_resizing: false,
1689            enable_row_selection: true,
1690            pointer_row_selection: false,
1691            single_row_selection: true,
1692            row_height: Some(Px(40.0)),
1693            header_height: Some(Px(40.0)),
1694            ..Default::default()
1695        };
1696
1697        let render = |ui: &mut UiTree<App>,
1698                      app: &mut App,
1699                      services: &mut FakeServices|
1700         -> fret_core::NodeId {
1701            fret_ui::declarative::render_root(ui, app, services, window, bounds, "test", |cx| {
1702                vec![table_virtualized(
1703                    cx,
1704                    &data,
1705                    &columns,
1706                    state.clone(),
1707                    &scroll,
1708                    0,
1709                    &|_row, i| RowKey::from_index(i),
1710                    None,
1711                    props.clone(),
1712                    |_row| None,
1713                    |cx, _col, _sort| [cx.text("")],
1714                    |cx, row, _col| {
1715                        let mut fill = LayoutStyle::default();
1716                        fill.size.width = Length::Fill;
1717                        fill.size.height = Length::Fill;
1718                        let marker = cx.container(
1719                            ContainerProps {
1720                                layout: fill,
1721                                ..Default::default()
1722                            },
1723                            |_| Vec::new(),
1724                        );
1725                        let _ = row;
1726                        [marker]
1727                    },
1728                    None,
1729                    TableDebugIds {
1730                        row_test_id_prefix: Some(Arc::<str>::from("table-test-row-")),
1731                        ..Default::default()
1732                    },
1733                )]
1734            })
1735        };
1736
1737        for _ in 0..2 {
1738            let root = render(&mut ui, &mut app, &mut services);
1739            ui.set_root(root);
1740            ui.request_semantics_snapshot();
1741            ui.layout_all(&mut app, &mut services, bounds, 1.0);
1742            let mut scene = fret_core::Scene::default();
1743            ui.paint_all(&mut app, &mut services, bounds, &mut scene, 1.0);
1744        }
1745
1746        let snap = ui
1747            .semantics_snapshot()
1748            .expect("expected a semantics snapshot");
1749        let marker_bounds = snap
1750            .nodes
1751            .iter()
1752            .find(|n| n.test_id.as_deref() == Some("table-test-row-1"))
1753            .map(|n| n.bounds)
1754            .expect("expected marker node");
1755
1756        let click_pos = Point::new(
1757            Px(marker_bounds.origin.x.0 + marker_bounds.size.width.0 * 0.5),
1758            Px(marker_bounds.origin.y.0 + marker_bounds.size.height.0 * 0.5),
1759        );
1760
1761        ui.dispatch_event(
1762            &mut app,
1763            &mut services,
1764            &Event::Pointer(PointerEvent::Down {
1765                position: click_pos,
1766                button: MouseButton::Left,
1767                modifiers: Modifiers::default(),
1768                click_count: 1,
1769                pointer_id: PointerId(0),
1770                pointer_type: PointerType::Mouse,
1771            }),
1772        );
1773        ui.dispatch_event(
1774            &mut app,
1775            &mut services,
1776            &Event::Pointer(PointerEvent::Up {
1777                position: click_pos,
1778                button: MouseButton::Left,
1779                modifiers: Modifiers::default(),
1780                click_count: 1,
1781                is_click: true,
1782                pointer_id: PointerId(0),
1783                pointer_type: PointerType::Mouse,
1784            }),
1785        );
1786
1787        let selection = app
1788            .models()
1789            .read(&state, |st| st.row_selection.clone())
1790            .ok()
1791            .unwrap_or_default();
1792        assert!(
1793            !selection.contains(&RowKey::from_index(1)),
1794            "expected row click not to toggle selection when pointer_row_selection=false"
1795        );
1796    }
1797
1798    #[test]
1799    fn table_virtualized_nested_pressable_remains_hittable_when_pointer_row_selection_disabled() {
1800        use std::cell::Cell;
1801        use std::rc::Rc;
1802
1803        let window = AppWindowId::default();
1804        let mut app = App::new();
1805        let mut ui: UiTree<App> = UiTree::new();
1806        ui.set_window(window);
1807
1808        Theme::with_global_mut(&mut app, |theme| {
1809            theme.apply_config(&ThemeConfig {
1810                name: "Test".to_string(),
1811                ..ThemeConfig::default()
1812            });
1813        });
1814
1815        let mut state_value = TableState::default();
1816        state_value.pagination.page_size = 3;
1817        let state = app.models_mut().insert(state_value);
1818        let child_activated = app.models_mut().insert(false);
1819        let child_element: Rc<Cell<Option<fret_ui::GlobalElementId>>> = Rc::new(Cell::new(None));
1820
1821        let data = vec![0u32, 1u32, 2u32];
1822        let columns = vec![
1823            {
1824                let mut col = ColumnDef::new("name");
1825                col.size = 180.0;
1826                col
1827            },
1828            {
1829                let mut col = ColumnDef::new("actions");
1830                col.size = 80.0;
1831                col
1832            },
1833        ];
1834        let scroll = VirtualListScrollHandle::new();
1835
1836        let bounds = Rect::new(
1837            Point::new(Px(0.0), Px(0.0)),
1838            fret_core::Size::new(Px(320.0), Px(180.0)),
1839        );
1840        let mut services = FakeServices;
1841
1842        let props = TableViewProps {
1843            draw_frame: false,
1844            enable_column_resizing: false,
1845            enable_row_selection: true,
1846            pointer_row_selection: false,
1847            single_row_selection: true,
1848            row_height: Some(Px(40.0)),
1849            header_height: Some(Px(40.0)),
1850            ..Default::default()
1851        };
1852
1853        let render = |ui: &mut UiTree<App>,
1854                      app: &mut App,
1855                      services: &mut FakeServices|
1856         -> fret_core::NodeId {
1857            let child_element = child_element.clone();
1858            fret_ui::declarative::render_root(ui, app, services, window, bounds, "test", |cx| {
1859                vec![table_virtualized(
1860                    cx,
1861                    &data,
1862                    &columns,
1863                    state.clone(),
1864                    &scroll,
1865                    0,
1866                    &|_row, i| RowKey::from_index(i),
1867                    None,
1868                    props.clone(),
1869                    |_row| None,
1870                    |cx, col, _sort| [cx.text(col.id.as_ref())],
1871                    |cx, row, col| match col.id.as_ref() {
1872                        "name" => [cx.text(format!("Row {}", row.index))],
1873                        "actions" if row.index == 1 => {
1874                            let child_element = child_element.clone();
1875                            let child_activated = child_activated.clone();
1876                            [cx.pressable_with_id(
1877                                PressableProps {
1878                                    focusable: false,
1879                                    layout: LayoutStyle {
1880                                        size: fret_ui::element::SizeStyle {
1881                                            width: Length::Px(Px(24.0)),
1882                                            height: Length::Px(Px(24.0)),
1883                                            ..Default::default()
1884                                        },
1885                                        ..Default::default()
1886                                    },
1887                                    a11y: PressableA11y {
1888                                        role: Some(SemanticsRole::Button),
1889                                        test_id: Some(Arc::<str>::from("table-test-child-button")),
1890                                        ..Default::default()
1891                                    },
1892                                    ..Default::default()
1893                                },
1894                                move |cx, _st, id| {
1895                                    child_element.set(Some(id));
1896                                    let child_activated = child_activated.clone();
1897                                    cx.pressable_on_activate(Arc::new(
1898                                        move |host, _acx, _reason| {
1899                                            let _ = host
1900                                                .models_mut()
1901                                                .update(&child_activated, |value| *value = true);
1902                                        },
1903                                    ));
1904                                    vec![cx.spacer(SpacerProps::default())]
1905                                },
1906                            )]
1907                        }
1908                        "actions" => [cx.text("-")],
1909                        _ => [cx.text("?")],
1910                    },
1911                    None,
1912                    TableDebugIds::default(),
1913                )]
1914            })
1915        };
1916
1917        for _ in 0..2 {
1918            let root = render(&mut ui, &mut app, &mut services);
1919            ui.set_root(root);
1920            ui.request_semantics_snapshot();
1921            ui.layout_all(&mut app, &mut services, bounds, 1.0);
1922            let mut scene = fret_core::Scene::default();
1923            ui.paint_all(&mut app, &mut services, bounds, &mut scene, 1.0);
1924        }
1925
1926        let child_element = child_element
1927            .get()
1928            .expect("expected nested child pressable element");
1929        let child_node = fret_ui::elements::node_for_element(&mut app, window, child_element)
1930            .expect("expected nested child pressable node");
1931        let child_bounds = ui
1932            .debug_node_bounds(child_node)
1933            .expect("expected nested child bounds");
1934        assert!(
1935            child_bounds.size.width.0 > 0.0 && child_bounds.size.height.0 > 0.0,
1936            "expected nested child pressable to have non-zero bounds, got {child_bounds:?}"
1937        );
1938        let click_pos = Point::new(
1939            Px(child_bounds.origin.x.0 + child_bounds.size.width.0 * 0.5),
1940            Px(child_bounds.origin.y.0 + child_bounds.size.height.0 * 0.5),
1941        );
1942
1943        let hit = ui.debug_hit_test_routing(click_pos);
1944        let hit_node = hit.hit.expect("expected nested child hit");
1945        let path = ui.debug_node_path(hit_node);
1946        let child_path = ui.debug_node_path(child_node);
1947        let child_path_debug = child_path
1948            .iter()
1949            .map(|node| {
1950                (
1951                    *node,
1952                    ui.debug_node_bounds(*node),
1953                    ui.debug_node_clips_hit_test(*node),
1954                    ui.debug_node_can_scroll_descendant_into_view(*node),
1955                )
1956            })
1957            .collect::<Vec<_>>();
1958
1959        ui.dispatch_event(
1960            &mut app,
1961            &mut services,
1962            &Event::Pointer(PointerEvent::Down {
1963                position: click_pos,
1964                button: MouseButton::Left,
1965                modifiers: Modifiers::default(),
1966                click_count: 1,
1967                pointer_id: PointerId(0),
1968                pointer_type: PointerType::Mouse,
1969            }),
1970        );
1971        ui.dispatch_event(
1972            &mut app,
1973            &mut services,
1974            &Event::Pointer(PointerEvent::Up {
1975                position: click_pos,
1976                button: MouseButton::Left,
1977                modifiers: Modifiers::default(),
1978                click_count: 1,
1979                is_click: true,
1980                pointer_id: PointerId(0),
1981                pointer_type: PointerType::Mouse,
1982            }),
1983        );
1984
1985        assert_eq!(
1986            app.models().get_copied(&child_activated),
1987            Some(true),
1988            "expected nested child pressable to activate; hit={hit:?} path={path:?} child={child_node:?} child_path={child_path:?} child_path_debug={child_path_debug:?} child_bounds={child_bounds:?}"
1989        );
1990
1991        let selection = app
1992            .models()
1993            .read(&state, |st| st.row_selection.clone())
1994            .ok()
1995            .unwrap_or_default();
1996        assert!(
1997            selection.is_empty(),
1998            "expected nested child pressable click not to toggle row selection when pointer_row_selection=false"
1999        );
2000    }
2001
2002    #[test]
2003    fn table_virtualized_pointer_row_selection_policy_list_like() {
2004        let window = AppWindowId::default();
2005        let mut app = App::new();
2006        let mut ui: UiTree<App> = UiTree::new();
2007        ui.set_window(window);
2008
2009        Theme::with_global_mut(&mut app, |theme| {
2010            theme.apply_config(&ThemeConfig {
2011                name: "Test".to_string(),
2012                ..ThemeConfig::default()
2013            });
2014        });
2015
2016        let mut state_value = TableState::default();
2017        state_value.pagination.page_size = 5;
2018        let state = app.models_mut().insert(state_value);
2019
2020        let data = vec![0u32, 1u32, 2u32, 3u32, 4u32];
2021        let mut col = ColumnDef::new("name");
2022        col.size = 220.0;
2023        let columns = vec![col];
2024        let scroll = VirtualListScrollHandle::new();
2025
2026        let bounds = Rect::new(
2027            Point::new(Px(0.0), Px(0.0)),
2028            fret_core::Size::new(Px(320.0), Px(240.0)),
2029        );
2030        let mut services = FakeServices;
2031
2032        let props = TableViewProps {
2033            draw_frame: false,
2034            enable_column_resizing: false,
2035            enable_row_selection: true,
2036            pointer_row_selection: true,
2037            pointer_row_selection_policy: PointerRowSelectionPolicy::ListLike,
2038            single_row_selection: false,
2039            row_height: Some(Px(40.0)),
2040            header_height: Some(Px(40.0)),
2041            ..Default::default()
2042        };
2043
2044        let render = |ui: &mut UiTree<App>,
2045                      app: &mut App,
2046                      services: &mut FakeServices|
2047         -> fret_core::NodeId {
2048            fret_ui::declarative::render_root(ui, app, services, window, bounds, "test", |cx| {
2049                vec![table_virtualized(
2050                    cx,
2051                    &data,
2052                    &columns,
2053                    state.clone(),
2054                    &scroll,
2055                    0,
2056                    &|_row, i| RowKey::from_index(i),
2057                    None,
2058                    props.clone(),
2059                    |_row| None,
2060                    |cx, _col, _sort| [cx.text("")],
2061                    |cx, row, _col| {
2062                        let mut fill = LayoutStyle::default();
2063                        fill.size.width = Length::Fill;
2064                        fill.size.height = Length::Fill;
2065                        let marker = cx.container(
2066                            ContainerProps {
2067                                layout: fill,
2068                                ..Default::default()
2069                            },
2070                            |_| Vec::new(),
2071                        );
2072                        let _ = row;
2073                        [marker]
2074                    },
2075                    None,
2076                    TableDebugIds {
2077                        row_test_id_prefix: Some(Arc::<str>::from("table-test-row-")),
2078                        ..Default::default()
2079                    },
2080                )]
2081            })
2082        };
2083
2084        // VirtualList computes the visible window based on viewport metrics populated during layout,
2085        // so it takes two frames for the first set of rows to mount.
2086        for _ in 0..2 {
2087            let root = render(&mut ui, &mut app, &mut services);
2088            ui.set_root(root);
2089            ui.request_semantics_snapshot();
2090            ui.layout_all(&mut app, &mut services, bounds, 1.0);
2091            let mut scene = fret_core::Scene::default();
2092            ui.paint_all(&mut app, &mut services, bounds, &mut scene, 1.0);
2093        }
2094
2095        let click_pos_for_row: Vec<Point> = {
2096            let snap = ui
2097                .semantics_snapshot()
2098                .expect("expected a semantics snapshot");
2099            (0..5)
2100                .map(|row_index| {
2101                    let id = format!("table-test-row-{row_index}");
2102                    let bounds = snap
2103                        .nodes
2104                        .iter()
2105                        .find(|n| n.test_id.as_deref() == Some(id.as_str()))
2106                        .map(|n| n.bounds)
2107                        .unwrap_or_else(|| panic!("expected marker node {id}"));
2108                    Point::new(
2109                        Px(bounds.origin.x.0 + bounds.size.width.0 * 0.5),
2110                        Px(bounds.origin.y.0 + bounds.size.height.0 * 0.5),
2111                    )
2112                })
2113                .collect()
2114        };
2115
2116        let click_row = |ui: &mut UiTree<App>,
2117                         app: &mut App,
2118                         services: &mut FakeServices,
2119                         row_index: usize,
2120                         modifiers: Modifiers| {
2121            let click_pos = click_pos_for_row[row_index];
2122            ui.dispatch_event(
2123                app,
2124                services,
2125                &Event::Pointer(PointerEvent::Down {
2126                    position: click_pos,
2127                    button: MouseButton::Left,
2128                    modifiers,
2129                    click_count: 1,
2130                    pointer_id: PointerId(0),
2131                    pointer_type: PointerType::Mouse,
2132                }),
2133            );
2134            ui.dispatch_event(
2135                app,
2136                services,
2137                &Event::Pointer(PointerEvent::Up {
2138                    position: click_pos,
2139                    button: MouseButton::Left,
2140                    modifiers,
2141                    click_count: 1,
2142                    is_click: true,
2143                    pointer_id: PointerId(0),
2144                    pointer_type: PointerType::Mouse,
2145                }),
2146            );
2147        };
2148
2149        let assert_selected = |app: &App, expected: &[RowKey]| {
2150            let selection = app
2151                .models()
2152                .read(&state, |st| st.row_selection.clone())
2153                .ok()
2154                .unwrap_or_default();
2155            assert_eq!(
2156                selection.len(),
2157                expected.len(),
2158                "expected selection len {} but got {}",
2159                expected.len(),
2160                selection.len()
2161            );
2162            for k in expected {
2163                assert!(selection.contains(k), "expected selection to contain {k:?}");
2164            }
2165        };
2166
2167        click_row(&mut ui, &mut app, &mut services, 1, Modifiers::default());
2168        assert_selected(&app, &[RowKey::from_index(1)]);
2169
2170        click_row(
2171            &mut ui,
2172            &mut app,
2173            &mut services,
2174            3,
2175            Modifiers {
2176                shift: true,
2177                ..Default::default()
2178            },
2179        );
2180        assert_selected(
2181            &app,
2182            &[
2183                RowKey::from_index(1),
2184                RowKey::from_index(2),
2185                RowKey::from_index(3),
2186            ],
2187        );
2188
2189        click_row(
2190            &mut ui,
2191            &mut app,
2192            &mut services,
2193            4,
2194            Modifiers {
2195                ctrl: true,
2196                ..Default::default()
2197            },
2198        );
2199        assert_selected(
2200            &app,
2201            &[
2202                RowKey::from_index(1),
2203                RowKey::from_index(2),
2204                RowKey::from_index(3),
2205                RowKey::from_index(4),
2206            ],
2207        );
2208
2209        click_row(&mut ui, &mut app, &mut services, 2, Modifiers::default());
2210        assert_selected(&app, &[RowKey::from_index(2)]);
2211    }
2212
2213    #[test]
2214    fn table_virtualized_allows_header_height_override() {
2215        let window = AppWindowId::default();
2216        let mut app = App::new();
2217        let mut ui: UiTree<App> = UiTree::new();
2218        ui.set_window(window);
2219
2220        Theme::with_global_mut(&mut app, |theme| {
2221            theme.apply_config(&ThemeConfig {
2222                name: "Test".to_string(),
2223                ..ThemeConfig::default()
2224            });
2225        });
2226
2227        let mut state_value = TableState::default();
2228        state_value.pagination.page_size = 1;
2229        let state = app.models_mut().insert(state_value);
2230
2231        let data: Arc<[u32]> = Arc::from(vec![0u32]);
2232        let columns: Arc<[ColumnDef<u32>]> = Arc::from(vec![{
2233            let mut col = ColumnDef::new("name");
2234            col.size = 220.0;
2235            col
2236        }]);
2237        let scroll = VirtualListScrollHandle::new();
2238
2239        let bounds = Rect::new(
2240            Point::new(Px(0.0), Px(0.0)),
2241            fret_core::Size::new(Px(320.0), Px(240.0)),
2242        );
2243        let mut services = FakeServices;
2244
2245        let props = TableViewProps {
2246            draw_frame: false,
2247            row_height: Some(Px(36.0)),
2248            header_height: Some(Px(40.0)),
2249            ..Default::default()
2250        };
2251
2252        let render = |ui: &mut UiTree<App>,
2253                      app: &mut App,
2254                      services: &mut FakeServices|
2255         -> fret_core::NodeId {
2256            fret_ui::declarative::render_root(ui, app, services, window, bounds, "test", |cx| {
2257                vec![table_virtualized_retained_v0(
2258                    cx,
2259                    data.clone(),
2260                    columns.clone(),
2261                    state.clone(),
2262                    &scroll,
2263                    0,
2264                    Arc::new(|_row: &u32, index: usize| RowKey::from_index(index)),
2265                    None,
2266                    props.clone(),
2267                    Arc::new(|col: &ColumnDef<u32>| Arc::from(col.id.as_ref())),
2268                    None,
2269                    Arc::new(
2270                        |cx: &mut dyn ElementContextAccess<'_, App>,
2271                         _col: &ColumnDef<u32>,
2272                         _row: &u32| {
2273                            crate::ui::text("Row 0").into_element(cx.elements())
2274                        },
2275                    ),
2276                    TableDebugIds {
2277                        header_row_test_id: Some(Arc::<str>::from("table-test-header-row")),
2278                        header_cell_test_id_prefix: Some(Arc::<str>::from("table-test-header-")),
2279                        row_test_id_prefix: Some(Arc::<str>::from("table-test-row-")),
2280                    },
2281                )]
2282            })
2283        };
2284
2285        // VirtualList computes the visible window based on viewport metrics populated during layout,
2286        // so it takes two frames for the first set of rows to mount.
2287        for _ in 0..2 {
2288            let root = render(&mut ui, &mut app, &mut services);
2289            ui.set_root(root);
2290            ui.request_semantics_snapshot();
2291            ui.layout_all(&mut app, &mut services, bounds, 1.0);
2292            let mut scene = fret_core::Scene::default();
2293            ui.paint_all(&mut app, &mut services, bounds, &mut scene, 1.0);
2294        }
2295
2296        let snap = ui
2297            .semantics_snapshot()
2298            .expect("expected a semantics snapshot");
2299
2300        let find_height = |id: &str| -> f32 {
2301            snap.nodes
2302                .iter()
2303                .find(|n| n.test_id.as_deref() == Some(id))
2304                .map(|n| n.bounds.size.height.0)
2305                .unwrap_or_else(|| panic!("expected to find {id}"))
2306        };
2307
2308        let header_h = find_height("table-test-header-row");
2309        let body_h = find_height("table-test-row-0");
2310
2311        let eps = 2.0;
2312        assert!(
2313            (header_h - 40.0).abs() <= eps,
2314            "expected header height ~40px (got {header_h:.2}px)"
2315        );
2316        assert!(
2317            (body_h - 36.0).abs() <= eps,
2318            "expected body height ~36px (got {body_h:.2}px)"
2319        );
2320    }
2321
2322    #[test]
2323    fn table_virtualized_retained_pointer_row_selection_policy_list_like() {
2324        let window = AppWindowId::default();
2325        let mut app = App::new();
2326        let mut ui: UiTree<App> = UiTree::new();
2327        ui.set_window(window);
2328
2329        Theme::with_global_mut(&mut app, |theme| {
2330            theme.apply_config(&ThemeConfig {
2331                name: "Test".to_string(),
2332                ..ThemeConfig::default()
2333            });
2334        });
2335
2336        let mut state_value = TableState::default();
2337        state_value.pagination.page_size = 5;
2338        let state = app.models_mut().insert(state_value);
2339
2340        let data: Arc<[u32]> = Arc::from((0u32..5).collect::<Vec<_>>());
2341        let columns: Arc<[ColumnDef<u32>]> = Arc::from(vec![{
2342            let mut col = ColumnDef::new("name");
2343            col.size = 220.0;
2344            col
2345        }]);
2346
2347        let scroll = VirtualListScrollHandle::new();
2348        let bounds = Rect::new(
2349            Point::new(Px(0.0), Px(0.0)),
2350            fret_core::Size::new(Px(320.0), Px(240.0)),
2351        );
2352        let mut services = FakeServices;
2353
2354        let props = TableViewProps {
2355            draw_frame: false,
2356            enable_column_resizing: false,
2357            enable_row_selection: true,
2358            pointer_row_selection: true,
2359            pointer_row_selection_policy: PointerRowSelectionPolicy::ListLike,
2360            single_row_selection: false,
2361            row_height: Some(Px(40.0)),
2362            header_height: Some(Px(40.0)),
2363            ..Default::default()
2364        };
2365
2366        let render = |ui: &mut UiTree<App>,
2367                      app: &mut App,
2368                      services: &mut FakeServices|
2369         -> fret_core::NodeId {
2370            fret_ui::declarative::render_root(ui, app, services, window, bounds, "test", |cx| {
2371                vec![table_virtualized_retained_v0(
2372                    cx,
2373                    data.clone(),
2374                    columns.clone(),
2375                    state.clone(),
2376                    &scroll,
2377                    0,
2378                    Arc::new(|_row: &u32, index: usize| RowKey::from_index(index)),
2379                    None,
2380                    props.clone(),
2381                    Arc::new(|col: &ColumnDef<u32>| Arc::from(col.id.as_ref())),
2382                    None,
2383                    Arc::new(
2384                        |cx: &mut dyn ElementContextAccess<'_, App>,
2385                         _col: &ColumnDef<u32>,
2386                         row: &u32| {
2387                            crate::ui::text(format!("Row {row}")).into_element(cx.elements())
2388                        },
2389                    ),
2390                    TableDebugIds {
2391                        header_cell_test_id_prefix: Some(Arc::<str>::from("table-test-header-")),
2392                        row_test_id_prefix: Some(Arc::<str>::from("table-test-row-")),
2393                        ..Default::default()
2394                    },
2395                )]
2396            })
2397        };
2398
2399        // VirtualList computes the visible window based on viewport metrics populated during layout,
2400        // so it takes two frames for the first set of rows to mount.
2401        for _ in 0..2 {
2402            let root = render(&mut ui, &mut app, &mut services);
2403            ui.set_root(root);
2404            ui.request_semantics_snapshot();
2405            ui.layout_all(&mut app, &mut services, bounds, 1.0);
2406            let mut scene = fret_core::Scene::default();
2407            ui.paint_all(&mut app, &mut services, bounds, &mut scene, 1.0);
2408        }
2409
2410        let click_pos_for_row: Vec<Point> = {
2411            let snap = ui
2412                .semantics_snapshot()
2413                .expect("expected a semantics snapshot");
2414            (0..5)
2415                .map(|row_index| {
2416                    let id = format!("table-test-row-{row_index}");
2417                    let bounds = snap
2418                        .nodes
2419                        .iter()
2420                        .find(|n| n.test_id.as_deref() == Some(id.as_str()))
2421                        .map(|n| n.bounds)
2422                        .unwrap_or_else(|| panic!("expected row node {id}"));
2423                    Point::new(
2424                        Px(bounds.origin.x.0 + bounds.size.width.0 * 0.5),
2425                        Px(bounds.origin.y.0 + bounds.size.height.0 * 0.5),
2426                    )
2427                })
2428                .collect()
2429        };
2430
2431        let click_row = |ui: &mut UiTree<App>,
2432                         app: &mut App,
2433                         services: &mut FakeServices,
2434                         row_index: usize,
2435                         modifiers: Modifiers| {
2436            let click_pos = click_pos_for_row[row_index];
2437            ui.dispatch_event(
2438                app,
2439                services,
2440                &Event::Pointer(PointerEvent::Down {
2441                    position: click_pos,
2442                    button: MouseButton::Left,
2443                    modifiers,
2444                    click_count: 1,
2445                    pointer_id: PointerId(0),
2446                    pointer_type: PointerType::Mouse,
2447                }),
2448            );
2449            ui.dispatch_event(
2450                app,
2451                services,
2452                &Event::Pointer(PointerEvent::Up {
2453                    position: click_pos,
2454                    button: MouseButton::Left,
2455                    modifiers,
2456                    click_count: 1,
2457                    is_click: true,
2458                    pointer_id: PointerId(0),
2459                    pointer_type: PointerType::Mouse,
2460                }),
2461            );
2462        };
2463
2464        let assert_selected = |app: &App, expected: &[RowKey]| {
2465            let selection = app
2466                .models()
2467                .read(&state, |st| st.row_selection.clone())
2468                .ok()
2469                .unwrap_or_default();
2470            assert_eq!(
2471                selection.len(),
2472                expected.len(),
2473                "expected selection len {} but got {}",
2474                expected.len(),
2475                selection.len()
2476            );
2477            for k in expected {
2478                assert!(selection.contains(k), "expected selection to contain {k:?}");
2479            }
2480        };
2481
2482        click_row(&mut ui, &mut app, &mut services, 1, Modifiers::default());
2483        assert_selected(&app, &[RowKey::from_index(1)]);
2484
2485        click_row(
2486            &mut ui,
2487            &mut app,
2488            &mut services,
2489            3,
2490            Modifiers {
2491                shift: true,
2492                ..Default::default()
2493            },
2494        );
2495        assert_selected(
2496            &app,
2497            &[
2498                RowKey::from_index(1),
2499                RowKey::from_index(2),
2500                RowKey::from_index(3),
2501            ],
2502        );
2503
2504        click_row(
2505            &mut ui,
2506            &mut app,
2507            &mut services,
2508            4,
2509            Modifiers {
2510                ctrl: true,
2511                ..Default::default()
2512            },
2513        );
2514        assert_selected(
2515            &app,
2516            &[
2517                RowKey::from_index(1),
2518                RowKey::from_index(2),
2519                RowKey::from_index(3),
2520                RowKey::from_index(4),
2521            ],
2522        );
2523
2524        click_row(&mut ui, &mut app, &mut services, 2, Modifiers::default());
2525        assert_selected(&app, &[RowKey::from_index(2)]);
2526    }
2527
2528    #[test]
2529    fn table_virtualized_retained_nested_pressable_remains_hittable_when_pointer_row_selection_disabled()
2530     {
2531        use std::cell::Cell;
2532        use std::rc::Rc;
2533
2534        let window = AppWindowId::default();
2535        let mut app = App::new();
2536        let mut ui: UiTree<App> = UiTree::new();
2537        ui.set_window(window);
2538        ui.set_debug_enabled(true);
2539
2540        Theme::with_global_mut(&mut app, |theme| {
2541            theme.apply_config(&ThemeConfig {
2542                name: "Test".to_string(),
2543                ..ThemeConfig::default()
2544            });
2545        });
2546
2547        let mut state_value = TableState::default();
2548        state_value.pagination.page_size = 3;
2549        let state = app.models_mut().insert(state_value);
2550        let child_activated = app.models_mut().insert(false);
2551        let child_element: Rc<Cell<Option<fret_ui::GlobalElementId>>> = Rc::new(Cell::new(None));
2552
2553        let data: Arc<[u32]> = Arc::from(vec![0u32, 1u32, 2u32]);
2554        let columns: Arc<[ColumnDef<u32>]> = Arc::from(vec![
2555            {
2556                let mut col = ColumnDef::new("name");
2557                col.size = 180.0;
2558                col
2559            },
2560            {
2561                let mut col = ColumnDef::new("actions");
2562                col.size = 80.0;
2563                col
2564            },
2565        ]);
2566        let scroll = VirtualListScrollHandle::new();
2567
2568        let bounds = Rect::new(
2569            Point::new(Px(0.0), Px(0.0)),
2570            fret_core::Size::new(Px(320.0), Px(180.0)),
2571        );
2572        let mut services = FakeServices;
2573
2574        let props = TableViewProps {
2575            draw_frame: false,
2576            enable_column_resizing: false,
2577            enable_row_selection: true,
2578            pointer_row_selection: false,
2579            single_row_selection: true,
2580            row_height: Some(Px(40.0)),
2581            header_height: Some(Px(40.0)),
2582            ..Default::default()
2583        };
2584
2585        let render = |ui: &mut UiTree<App>,
2586                      app: &mut App,
2587                      services: &mut FakeServices|
2588         -> fret_core::NodeId {
2589            let child_element = child_element.clone();
2590            let child_activated = child_activated.clone();
2591            fret_ui::declarative::render_root(ui, app, services, window, bounds, "test", |cx| {
2592                vec![table_virtualized_retained_v0(
2593                    cx,
2594                    data.clone(),
2595                    columns.clone(),
2596                    state.clone(),
2597                    &scroll,
2598                    0,
2599                    Arc::new(|_row: &u32, index: usize| RowKey::from_index(index)),
2600                    None,
2601                    props.clone(),
2602                    Arc::new(|col: &ColumnDef<u32>| Arc::from(col.id.as_ref())),
2603                    None,
2604                    Arc::new(
2605                        move |cx: &mut dyn ElementContextAccess<'_, App>,
2606                              col: &ColumnDef<u32>,
2607                              row: &u32| {
2608                            let cx = cx.elements();
2609                            match col.id.as_ref() {
2610                                "name" => crate::ui::text(format!("Row {row}")).into_element(cx),
2611                                "actions" if *row == 1 => {
2612                                    let child_element = child_element.clone();
2613                                    let child_activated = child_activated.clone();
2614                                    cx.pressable_with_id(
2615                                        PressableProps {
2616                                            focusable: false,
2617                                            layout: LayoutStyle {
2618                                                size: fret_ui::element::SizeStyle {
2619                                                    width: Length::Px(Px(24.0)),
2620                                                    height: Length::Px(Px(24.0)),
2621                                                    ..Default::default()
2622                                                },
2623                                                ..Default::default()
2624                                            },
2625                                            a11y: PressableA11y {
2626                                                role: Some(SemanticsRole::Button),
2627                                                test_id: Some(Arc::<str>::from(
2628                                                    "table-retained-test-child-button",
2629                                                )),
2630                                                ..Default::default()
2631                                            },
2632                                            ..Default::default()
2633                                        },
2634                                        move |cx, _st, id| {
2635                                            child_element.set(Some(id));
2636                                            let child_activated = child_activated.clone();
2637                                            cx.pressable_on_activate(Arc::new(
2638                                                move |host, _acx, _reason| {
2639                                                    let _ = host
2640                                                        .models_mut()
2641                                                        .update(&child_activated, |value| {
2642                                                            *value = true
2643                                                        });
2644                                                },
2645                                            ));
2646                                            vec![cx.spacer(SpacerProps::default())]
2647                                        },
2648                                    )
2649                                }
2650                                "actions" => cx.text("-"),
2651                                _ => cx.text("?"),
2652                            }
2653                        },
2654                    ),
2655                    TableDebugIds {
2656                        header_cell_test_id_prefix: Some(Arc::<str>::from(
2657                            "table-retained-test-header-",
2658                        )),
2659                        row_test_id_prefix: Some(Arc::<str>::from("table-retained-test-row-")),
2660                        ..Default::default()
2661                    },
2662                )]
2663            })
2664        };
2665
2666        for _ in 0..2 {
2667            let root = render(&mut ui, &mut app, &mut services);
2668            ui.set_root(root);
2669            ui.request_semantics_snapshot();
2670            ui.layout_all(&mut app, &mut services, bounds, 1.0);
2671            let mut scene = fret_core::Scene::default();
2672            ui.paint_all(&mut app, &mut services, bounds, &mut scene, 1.0);
2673        }
2674
2675        let child_element = child_element
2676            .get()
2677            .expect("expected nested child pressable element");
2678        let child_node = fret_ui::elements::node_for_element(&mut app, window, child_element)
2679            .expect("expected nested child pressable node");
2680        let child_bounds = ui
2681            .debug_node_bounds(child_node)
2682            .expect("expected nested child bounds");
2683        assert!(
2684            child_bounds.size.width.0 > 0.0 && child_bounds.size.height.0 > 0.0,
2685            "expected nested child pressable to have non-zero bounds, got {child_bounds:?}"
2686        );
2687        let click_pos = Point::new(
2688            Px(child_bounds.origin.x.0 + child_bounds.size.width.0 * 0.5),
2689            Px(child_bounds.origin.y.0 + child_bounds.size.height.0 * 0.5),
2690        );
2691
2692        let hit = ui.debug_hit_test_routing(click_pos);
2693        let hit_node = hit.hit.expect("expected nested child hit");
2694        let path = ui.debug_node_path(hit_node);
2695        let child_path = ui.debug_node_path(child_node);
2696        let child_path_debug = child_path
2697            .iter()
2698            .map(|node| {
2699                (
2700                    *node,
2701                    ui.debug_node_bounds(*node),
2702                    ui.debug_node_clips_hit_test(*node),
2703                    ui.debug_node_can_scroll_descendant_into_view(*node),
2704                )
2705            })
2706            .collect::<Vec<_>>();
2707
2708        ui.dispatch_event(
2709            &mut app,
2710            &mut services,
2711            &Event::Pointer(PointerEvent::Down {
2712                position: click_pos,
2713                button: MouseButton::Left,
2714                modifiers: Modifiers::default(),
2715                click_count: 1,
2716                pointer_id: PointerId(0),
2717                pointer_type: PointerType::Mouse,
2718            }),
2719        );
2720        ui.dispatch_event(
2721            &mut app,
2722            &mut services,
2723            &Event::Pointer(PointerEvent::Up {
2724                position: click_pos,
2725                button: MouseButton::Left,
2726                modifiers: Modifiers::default(),
2727                click_count: 1,
2728                is_click: true,
2729                pointer_id: PointerId(0),
2730                pointer_type: PointerType::Mouse,
2731            }),
2732        );
2733
2734        assert_eq!(
2735            app.models().get_copied(&child_activated),
2736            Some(true),
2737            "expected nested child pressable to activate; hit={hit:?} path={path:?} child={child_node:?} child_path={child_path:?} child_path_debug={child_path_debug:?} child_bounds={child_bounds:?}"
2738        );
2739
2740        let selection = app
2741            .models()
2742            .read(&state, |st| st.row_selection.clone())
2743            .ok()
2744            .unwrap_or_default();
2745        assert!(
2746            selection.is_empty(),
2747            "expected nested child pressable click not to toggle row selection when pointer_row_selection=false"
2748        );
2749    }
2750
2751    #[test]
2752    fn table_active_descendant_semantics_resolves_from_declarative_active_row_relation() {
2753        let window = AppWindowId::default();
2754        let mut app = App::new();
2755        let mut ui: UiTree<App> = UiTree::new();
2756        ui.set_window(window);
2757
2758        Theme::with_global_mut(&mut app, |theme| {
2759            theme.apply_config(&ThemeConfig {
2760                name: "Test".to_string(),
2761                ..ThemeConfig::default()
2762            });
2763        });
2764
2765        let mut state_value = TableState::default();
2766        state_value.pagination.page_size = 3;
2767        let state = app.models_mut().insert(state_value);
2768
2769        let data: Arc<[u32]> = Arc::from(vec![0u32, 1u32, 2u32]);
2770        let columns: Arc<[ColumnDef<u32>]> = Arc::from(vec![{
2771            let mut col = ColumnDef::new("name");
2772            col.size = 220.0;
2773            col
2774        }]);
2775        let scroll = VirtualListScrollHandle::new();
2776
2777        let bounds = Rect::new(
2778            Point::new(Px(0.0), Px(0.0)),
2779            fret_core::Size::new(Px(320.0), Px(240.0)),
2780        );
2781        let mut services = FakeServices;
2782
2783        let props = TableViewProps {
2784            draw_frame: false,
2785            row_height: Some(Px(36.0)),
2786            header_height: Some(Px(40.0)),
2787            ..Default::default()
2788        };
2789
2790        let render = |ui: &mut UiTree<App>,
2791                      app: &mut App,
2792                      services: &mut FakeServices|
2793         -> fret_core::NodeId {
2794            fret_ui::declarative::render_root(ui, app, services, window, bounds, "test", |cx| {
2795                vec![table_virtualized_retained_v0(
2796                    cx,
2797                    data.clone(),
2798                    columns.clone(),
2799                    state.clone(),
2800                    &scroll,
2801                    0,
2802                    Arc::new(|_row: &u32, index: usize| RowKey::from_index(index)),
2803                    None,
2804                    props.clone(),
2805                    Arc::new(|col: &ColumnDef<u32>| Arc::from(col.id.as_ref())),
2806                    None,
2807                    Arc::new(
2808                        |cx: &mut dyn ElementContextAccess<'_, App>,
2809                         _col: &ColumnDef<u32>,
2810                         row: &u32| {
2811                            crate::ui::text(format!("Row {row}")).into_element(cx.elements())
2812                        },
2813                    ),
2814                    TableDebugIds {
2815                        row_test_id_prefix: Some(Arc::<str>::from("table-active-desc-row-")),
2816                        ..Default::default()
2817                    },
2818                )]
2819            })
2820        };
2821
2822        let pump =
2823            |ui: &mut UiTree<App>, app: &mut App, services: &mut FakeServices, frames: usize| {
2824                for _ in 0..frames {
2825                    let root = render(ui, app, services);
2826                    ui.set_root(root);
2827                    ui.request_semantics_snapshot();
2828                    ui.layout_all(app, services, bounds, 1.0);
2829                    let mut scene = fret_core::Scene::default();
2830                    ui.paint_all(app, services, bounds, &mut scene, 1.0);
2831                }
2832            };
2833
2834        let row_center = |snap: &fret_core::SemanticsSnapshot, row_index: usize| {
2835            let id = format!("table-active-desc-row-{row_index}");
2836            let bounds = snap
2837                .nodes
2838                .iter()
2839                .find(|node| node.test_id.as_deref() == Some(id.as_str()))
2840                .map(|node| node.bounds)
2841                .unwrap_or_else(|| panic!("expected row semantics node `{id}`"));
2842            Point::new(
2843                Px(bounds.origin.x.0 + bounds.size.width.0 * 0.5),
2844                Px(bounds.origin.y.0 + bounds.size.height.0 * 0.5),
2845            )
2846        };
2847
2848        pump(&mut ui, &mut app, &mut services, 2);
2849
2850        let initial_snap = ui
2851            .semantics_snapshot()
2852            .expect("expected semantics snapshot after initial table render");
2853        let click_pos = row_center(initial_snap, 1);
2854        ui.dispatch_event(
2855            &mut app,
2856            &mut services,
2857            &Event::Pointer(PointerEvent::Down {
2858                position: click_pos,
2859                button: MouseButton::Left,
2860                modifiers: Modifiers::default(),
2861                click_count: 1,
2862                pointer_id: PointerId(0),
2863                pointer_type: PointerType::Mouse,
2864            }),
2865        );
2866        ui.dispatch_event(
2867            &mut app,
2868            &mut services,
2869            &Event::Pointer(PointerEvent::Up {
2870                position: click_pos,
2871                button: MouseButton::Left,
2872                modifiers: Modifiers::default(),
2873                click_count: 1,
2874                is_click: true,
2875                pointer_id: PointerId(0),
2876                pointer_type: PointerType::Mouse,
2877            }),
2878        );
2879
2880        // Frame N+1 records the active row element while rebuilding the list body.
2881        // Frame N+2 lets the parent list semantics resolve the declarative relation against the
2882        // now-mounted row node for the current frame.
2883        pump(&mut ui, &mut app, &mut services, 2);
2884
2885        let snap = ui
2886            .semantics_snapshot()
2887            .expect("expected semantics snapshot after activating a row");
2888        let row = snap
2889            .nodes
2890            .iter()
2891            .find(|node| node.test_id.as_deref() == Some("table-active-desc-row-1"))
2892            .expect("expected active row semantics node");
2893        let list = snap
2894            .nodes
2895            .iter()
2896            .find(|node| node.role == SemanticsRole::List)
2897            .expect("expected table list semantics node after activation");
2898
2899        assert_eq!(
2900            list.active_descendant,
2901            Some(row.id),
2902            "expected table list semantics to resolve active_descendant to the mounted active row node"
2903        );
2904    }
2905
2906    #[test]
2907    fn table_virtualized_retained_colpin_alignment_gate_across_pin_resize_and_overflow() {
2908        let window = AppWindowId::default();
2909        let mut app = App::new();
2910        let mut ui: UiTree<App> = UiTree::new();
2911        ui.set_window(window);
2912
2913        Theme::with_global_mut(&mut app, |theme| {
2914            theme.apply_config(&ThemeConfig {
2915                name: "Test".to_string(),
2916                ..ThemeConfig::default()
2917            });
2918        });
2919
2920        let mut initial = TableState::default();
2921        initial.pagination.page_size = 8;
2922        initial.column_pinning.left = vec!["a".into()];
2923        initial.column_pinning.right = vec!["d".into()];
2924        let state = app.models_mut().insert(initial);
2925
2926        let data: Arc<[u32]> = Arc::from((0u32..32).collect::<Vec<_>>());
2927        let mut col_a = ColumnDef::new("a");
2928        col_a.size = 120.0;
2929        let mut col_b = ColumnDef::new("b");
2930        col_b.size = 280.0;
2931        let mut col_c = ColumnDef::new("c");
2932        col_c.size = 240.0;
2933        let mut col_d = ColumnDef::new("d");
2934        col_d.size = 140.0;
2935        let columns: Arc<[ColumnDef<u32>]> = Arc::from(vec![col_a, col_b, col_c, col_d]);
2936
2937        let scroll = VirtualListScrollHandle::new();
2938        let bounds = Rect::new(
2939            Point::new(Px(0.0), Px(0.0)),
2940            fret_core::Size::new(Px(360.0), Px(220.0)),
2941        );
2942        let mut services = FakeServices;
2943
2944        let render = |ui: &mut UiTree<App>,
2945                      app: &mut App,
2946                      services: &mut FakeServices|
2947         -> fret_core::NodeId {
2948            fret_ui::declarative::render_root(ui, app, services, window, bounds, "test", |cx| {
2949                let props = TableViewProps {
2950                    overscan: 4,
2951                    enable_column_grouping: false,
2952                    ..Default::default()
2953                };
2954
2955                let table = table_virtualized_retained_v0(
2956                    cx,
2957                    data.clone(),
2958                    columns.clone(),
2959                    state.clone(),
2960                    &scroll,
2961                    0,
2962                    Arc::new(|_row: &u32, index: usize| RowKey::from_index(index)),
2963                    None,
2964                    props,
2965                    Arc::new(|col: &ColumnDef<u32>| Arc::from(col.id.as_ref())),
2966                    None,
2967                    Arc::new(
2968                        |cx: &mut dyn ElementContextAccess<'_, App>,
2969                         col: &ColumnDef<u32>,
2970                         row: &u32| {
2971                            let cx = cx.elements();
2972                            cx.text(format!("{}-{row}", col.id.as_ref()))
2973                        },
2974                    ),
2975                    TableDebugIds {
2976                        header_cell_test_id_prefix: Some(Arc::<str>::from(
2977                            "table-retained-colpin-header-",
2978                        )),
2979                        row_test_id_prefix: Some(Arc::<str>::from("table-retained-colpin-row-")),
2980                        ..Default::default()
2981                    },
2982                );
2983
2984                vec![cx.semantics(
2985                    SemanticsProps {
2986                        test_id: Some(Arc::<str>::from("table-retained-colpin-root")),
2987                        ..Default::default()
2988                    },
2989                    move |_cx| vec![table],
2990                )]
2991            })
2992        };
2993
2994        let pump = |ui: &mut UiTree<App>,
2995                    app: &mut App,
2996                    services: &mut FakeServices,
2997                    root: &mut fret_core::NodeId| {
2998            for _ in 0..2 {
2999                *root = render(ui, app, services);
3000                ui.set_root(*root);
3001                ui.request_semantics_snapshot();
3002                ui.layout_all(app, services, bounds, 1.0);
3003                let mut scene = fret_core::Scene::default();
3004                ui.paint_all(app, services, bounds, &mut scene, 1.0);
3005            }
3006        };
3007
3008        let find_bounds = |snap: &fret_core::SemanticsSnapshot, id: &str| {
3009            snap.nodes
3010                .iter()
3011                .find(|n| n.test_id.as_deref() == Some(id))
3012                .map(|n| n.bounds)
3013                .unwrap_or_else(|| panic!("expected semantics node `{id}`"))
3014        };
3015
3016        let assert_aligned = |snap: &fret_core::SemanticsSnapshot, col: &str| {
3017            let header_id = format!("table-retained-colpin-header-{col}");
3018            let row_id = format!("table-retained-colpin-row-0-cell-{col}");
3019            let header = find_bounds(snap, &header_id);
3020            let body = find_bounds(snap, &row_id);
3021            let dx = (header.origin.x.0 - body.origin.x.0).abs();
3022            let dw = (header.size.width.0 - body.size.width.0).abs();
3023            assert!(
3024                dx <= 1.0,
3025                "expected header/body x alignment for `{col}` (dx={dx:.2})"
3026            );
3027            assert!(
3028                dw <= 1.0,
3029                "expected header/body width alignment for `{col}` (dw={dw:.2})"
3030            );
3031        };
3032
3033        let mut root = fret_core::NodeId::default();
3034        pump(&mut ui, &mut app, &mut services, &mut root);
3035
3036        let mut snap = ui
3037            .semantics_snapshot()
3038            .expect("expected semantics snapshot after initial render");
3039        for col in ["a", "b", "c", "d"] {
3040            assert_aligned(snap, col);
3041        }
3042
3043        let _ = app.models_mut().update(&state, |st| {
3044            st.column_pinning.left = vec!["a".into()];
3045            st.column_pinning.right = vec!["d".into()];
3046            st.column_sizing.insert("b".into(), 320.0);
3047            st.column_sizing.insert("c".into(), 260.0);
3048        });
3049        pump(&mut ui, &mut app, &mut services, &mut root);
3050        snap = ui
3051            .semantics_snapshot()
3052            .expect("expected semantics snapshot after resize update");
3053        for col in ["a", "b", "c", "d"] {
3054            assert_aligned(snap, col);
3055        }
3056
3057        let root_bounds = find_bounds(snap, "table-retained-colpin-root");
3058        let center_bounds = find_bounds(snap, "table-retained-colpin-header-c");
3059        let root_right = root_bounds.origin.x.0 + root_bounds.size.width.0;
3060        let center_right = center_bounds.origin.x.0 + center_bounds.size.width.0;
3061        assert!(
3062            center_right > root_right + 1.0,
3063            "expected center region overflow to exist (root_right={root_right:.2}, center_right={center_right:.2})"
3064        );
3065
3066        let _ = app.models_mut().update(&state, |st| {
3067            st.column_pinning.left = vec!["a".into()];
3068            st.column_pinning.right = vec!["c".into(), "d".into()];
3069        });
3070        pump(&mut ui, &mut app, &mut services, &mut root);
3071        snap = ui
3072            .semantics_snapshot()
3073            .expect("expected semantics snapshot after pin/unpin update");
3074
3075        for col in ["a", "b", "c", "d"] {
3076            assert_aligned(snap, col);
3077        }
3078    }
3079
3080    #[test]
3081    fn table_virtualized_retained_colpin_alignment_gate_measured_rows_do_not_shrink_width() {
3082        let window = AppWindowId::default();
3083        let mut app = App::new();
3084        let mut ui: UiTree<App> = UiTree::new();
3085        ui.set_window(window);
3086
3087        Theme::with_global_mut(&mut app, |theme| {
3088            theme.apply_config(&ThemeConfig {
3089                name: "Test".to_string(),
3090                ..ThemeConfig::default()
3091            });
3092        });
3093
3094        let mut initial = TableState::default();
3095        initial.pagination.page_size = 8;
3096        initial.column_pinning.left = vec!["a".into()];
3097        initial.column_pinning.right = vec!["d".into()];
3098        let state = app.models_mut().insert(initial);
3099
3100        let data: Arc<[u32]> = Arc::from((0u32..32).collect::<Vec<_>>());
3101        let mut col_a = ColumnDef::new("a");
3102        col_a.size = 120.0;
3103        let mut col_b = ColumnDef::new("b");
3104        col_b.size = 280.0;
3105        let mut col_c = ColumnDef::new("c");
3106        col_c.size = 240.0;
3107        let mut col_d = ColumnDef::new("d");
3108        col_d.size = 140.0;
3109        let columns: Arc<[ColumnDef<u32>]> = Arc::from(vec![col_a, col_b, col_c, col_d]);
3110
3111        let scroll = VirtualListScrollHandle::new();
3112        let bounds = Rect::new(
3113            Point::new(Px(0.0), Px(0.0)),
3114            fret_core::Size::new(Px(360.0), Px(220.0)),
3115        );
3116        let mut services = FakeServices;
3117
3118        let render = |ui: &mut UiTree<App>,
3119                      app: &mut App,
3120                      services: &mut FakeServices|
3121         -> fret_core::NodeId {
3122            fret_ui::declarative::render_root(ui, app, services, window, bounds, "test", |cx| {
3123                let props = TableViewProps {
3124                    overscan: 4,
3125                    enable_column_grouping: false,
3126                    row_height: Some(Px(28.0)),
3127                    row_measure_mode: TableRowMeasureMode::Measured,
3128                    ..Default::default()
3129                };
3130
3131                let table = table_virtualized_retained_v0(
3132                    cx,
3133                    data.clone(),
3134                    columns.clone(),
3135                    state.clone(),
3136                    &scroll,
3137                    0,
3138                    Arc::new(|_row: &u32, index: usize| RowKey::from_index(index)),
3139                    None,
3140                    props,
3141                    Arc::new(|col: &ColumnDef<u32>| Arc::from(col.id.as_ref())),
3142                    None,
3143                    Arc::new(
3144                        |cx: &mut dyn ElementContextAccess<'_, App>,
3145                         col: &ColumnDef<u32>,
3146                         row: &u32| {
3147                            let cx = cx.elements();
3148                            if *row == 0 && col.id.as_ref() == "b" {
3149                                ui::v_stack(|cx| [cx.text("b-0"), cx.text("extra line")])
3150                                    .gap(Space::N0)
3151                                    .into_element(cx)
3152                            } else {
3153                                cx.text(format!("{}-{row}", col.id.as_ref()))
3154                            }
3155                        },
3156                    ),
3157                    TableDebugIds {
3158                        header_cell_test_id_prefix: Some(Arc::<str>::from(
3159                            "table-retained-colpin-header-",
3160                        )),
3161                        row_test_id_prefix: Some(Arc::<str>::from("table-retained-colpin-row-")),
3162                        ..Default::default()
3163                    },
3164                );
3165
3166                vec![cx.semantics(
3167                    SemanticsProps {
3168                        test_id: Some(Arc::<str>::from("table-retained-colpin-root")),
3169                        ..Default::default()
3170                    },
3171                    move |_cx| vec![table],
3172                )]
3173            })
3174        };
3175
3176        let pump = |ui: &mut UiTree<App>,
3177                    app: &mut App,
3178                    services: &mut FakeServices,
3179                    root: &mut fret_core::NodeId| {
3180            for _ in 0..2 {
3181                *root = render(ui, app, services);
3182                ui.set_root(*root);
3183                ui.request_semantics_snapshot();
3184                ui.layout_all(app, services, bounds, 1.0);
3185                let mut scene = fret_core::Scene::default();
3186                ui.paint_all(app, services, bounds, &mut scene, 1.0);
3187            }
3188        };
3189
3190        let find_bounds = |snap: &fret_core::SemanticsSnapshot, id: &str| {
3191            snap.nodes
3192                .iter()
3193                .find(|n| n.test_id.as_deref() == Some(id))
3194                .map(|n| n.bounds)
3195                .unwrap_or_else(|| panic!("expected semantics node `{id}`"))
3196        };
3197
3198        let assert_aligned = |snap: &fret_core::SemanticsSnapshot, col: &str| {
3199            let header_id = format!("table-retained-colpin-header-{col}");
3200            let row_id = format!("table-retained-colpin-row-0-cell-{col}");
3201            let header = find_bounds(snap, &header_id);
3202            let body = find_bounds(snap, &row_id);
3203            let dx = (header.origin.x.0 - body.origin.x.0).abs();
3204            let dw = (header.size.width.0 - body.size.width.0).abs();
3205            assert!(
3206                dx <= 1.0,
3207                "expected header/body x alignment for `{col}` (dx={dx:.2})"
3208            );
3209            assert!(
3210                dw <= 1.0,
3211                "expected header/body width alignment for `{col}` (dw={dw:.2})"
3212            );
3213        };
3214
3215        let mut root = fret_core::NodeId::default();
3216        pump(&mut ui, &mut app, &mut services, &mut root);
3217
3218        let snap = ui
3219            .semantics_snapshot()
3220            .expect("expected semantics snapshot after initial render");
3221
3222        for col in ["a", "b", "c", "d"] {
3223            assert_aligned(snap, col);
3224        }
3225    }
3226}
3227
3228#[derive(Debug, Clone)]
3229enum DisplayRow {
3230    Leaf {
3231        data_index: usize,
3232        row_key: RowKey,
3233        depth: usize,
3234    },
3235    Group {
3236        grouping_column: ColumnId,
3237        row_key: RowKey,
3238        depth: usize,
3239        label: Arc<str>,
3240        expanded: bool,
3241        aggregations: Arc<[(ColumnId, Arc<str>)]>,
3242    },
3243}
3244
3245impl DisplayRow {
3246    fn row_key(&self) -> RowKey {
3247        match self {
3248            DisplayRow::Leaf { row_key, .. } | DisplayRow::Group { row_key, .. } => *row_key,
3249        }
3250    }
3251}
3252
3253fn apply_row_pinning_to_paged_rows(
3254    visible_all: &[DisplayRow],
3255    page_rows: &[DisplayRow],
3256    row_pinning: &crate::headless::table::RowPinningState,
3257) -> Vec<DisplayRow> {
3258    if row_pinning.top.is_empty() && row_pinning.bottom.is_empty() {
3259        return page_rows.to_vec();
3260    }
3261
3262    let mut pinned: std::collections::HashSet<RowKey> = Default::default();
3263    pinned.extend(row_pinning.top.iter().copied());
3264    pinned.extend(row_pinning.bottom.iter().copied());
3265
3266    let mut by_key: std::collections::HashMap<RowKey, DisplayRow> =
3267        std::collections::HashMap::new();
3268    for row in visible_all {
3269        by_key.entry(row.row_key()).or_insert_with(|| row.clone());
3270    }
3271
3272    let mut out: Vec<DisplayRow> =
3273        Vec::with_capacity(row_pinning.top.len() + page_rows.len() + row_pinning.bottom.len());
3274
3275    for row_key in &row_pinning.top {
3276        if let Some(row) = by_key.get(row_key) {
3277            out.push(row.clone());
3278        }
3279    }
3280
3281    out.extend(
3282        page_rows
3283            .iter()
3284            .filter(|row| !pinned.contains(&row.row_key()))
3285            .cloned(),
3286    );
3287
3288    for row_key in &row_pinning.bottom {
3289        if let Some(row) = by_key.get(row_key) {
3290            out.push(row.clone());
3291        }
3292    }
3293
3294    out
3295}
3296
3297fn apply_grouped_row_pinning_policy(
3298    visible_all: &[DisplayRow],
3299    page_rows_center: &[DisplayRow],
3300    row_pinning: &crate::headless::table::RowPinningState,
3301    policy: GroupedRowPinningPolicy,
3302) -> Vec<DisplayRow> {
3303    match policy {
3304        GroupedRowPinningPolicy::PreserveHierarchy => page_rows_center.to_vec(),
3305        GroupedRowPinningPolicy::PromotePinnedRows => {
3306            apply_row_pinning_to_paged_rows(visible_all, page_rows_center, row_pinning)
3307        }
3308    }
3309}
3310
3311#[derive(Debug, Clone, PartialEq)]
3312struct GroupedBaseDeps {
3313    items_revision: u64,
3314    data_len: usize,
3315    columns_fingerprint: u64,
3316    grouping: Vec<ColumnId>,
3317    column_filters: crate::headless::table::ColumnFiltersState,
3318    global_filter: crate::headless::table::GlobalFilterState,
3319}
3320
3321#[derive(Debug, Clone, PartialEq)]
3322struct GroupedDisplayDeps {
3323    base: GroupedBaseDeps,
3324    sorting: crate::headless::table::SortingState,
3325    expanding: ExpandingState,
3326    row_pinning: crate::headless::table::RowPinningState,
3327    grouped_row_pinning_policy: GroupedRowPinningPolicy,
3328    page_index: usize,
3329    page_size: usize,
3330}
3331
3332#[derive(Debug, Default)]
3333struct GroupedDisplayCache {
3334    base_deps: Option<GroupedBaseDeps>,
3335    grouped: crate::headless::table::GroupedRowModel,
3336    row_index_by_key: std::collections::HashMap<RowKey, usize>,
3337    group_labels: std::collections::HashMap<RowKey, Arc<str>>,
3338    group_aggs_u64: std::collections::HashMap<RowKey, Arc<[(ColumnId, u64)]>>,
3339    group_aggs_any: std::collections::HashMap<RowKey, Arc<[(ColumnId, TanStackValue)]>>,
3340    group_aggs_text: GroupAggsText,
3341
3342    deps: Option<GroupedDisplayDeps>,
3343    page_rows: Vec<DisplayRow>,
3344    output: TableViewOutput,
3345}
3346
3347fn fnv1a64_bytes(bytes: &[u8]) -> u64 {
3348    const OFFSET: u64 = 0xcbf29ce484222325;
3349    const PRIME: u64 = 0x00000100000001B3;
3350
3351    let mut h = OFFSET;
3352    for &b in bytes {
3353        h ^= b as u64;
3354        h = h.wrapping_mul(PRIME);
3355    }
3356    h
3357}
3358
3359fn columns_fingerprint<TData>(columns: &[ColumnDef<TData>]) -> u64 {
3360    let mut h = fnv1a64_bytes(b"fret.table.columns.v1");
3361    for c in columns {
3362        h ^= fnv1a64_bytes(c.id.as_bytes());
3363        h = h.wrapping_mul(0x00000100000001B3);
3364        h ^= c.enable_grouping as u64;
3365        h = h.wrapping_mul(0x00000100000001B3);
3366        h ^= c.facet_key_fn.is_some() as u64;
3367        h = h.wrapping_mul(0x00000100000001B3);
3368        h ^= c.facet_str_fn.is_some() as u64;
3369        h = h.wrapping_mul(0x00000100000001B3);
3370        h ^= c.value_u64_fn.is_some() as u64;
3371        h = h.wrapping_mul(0x00000100000001B3);
3372        h ^= match c.aggregation {
3373            Aggregation::None => 0,
3374            Aggregation::Count => 1,
3375            Aggregation::SumU64 => 2,
3376            Aggregation::MinU64 => 3,
3377            Aggregation::MaxU64 => 4,
3378            Aggregation::MeanU64 => 5,
3379        };
3380        h = h.wrapping_mul(0x00000100000001B3);
3381    }
3382    h
3383}
3384
3385#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3386enum TableNavRowKind {
3387    Leaf,
3388    Group,
3389}
3390
3391#[derive(Debug, Clone, PartialEq, Eq)]
3392struct TableNavRowMeta {
3393    row_key: RowKey,
3394    kind: TableNavRowKind,
3395    data_index: Option<usize>,
3396    label: Arc<str>,
3397}
3398
3399fn table_collect_leaf_keys(meta: &[TableNavRowMeta]) -> Vec<RowKey> {
3400    meta.iter()
3401        .filter(|m| m.kind == TableNavRowKind::Leaf)
3402        .map(|m| m.row_key)
3403        .collect()
3404}
3405
3406fn table_collect_leaf_keys_in_range(meta: &[TableNavRowMeta], a: usize, b: usize) -> Vec<RowKey> {
3407    if meta.is_empty() {
3408        return Vec::new();
3409    }
3410
3411    let a = a.min(meta.len().saturating_sub(1));
3412    let b = b.min(meta.len().saturating_sub(1));
3413    let (a, b) = if a <= b { (a, b) } else { (b, a) };
3414
3415    meta.iter()
3416        .enumerate()
3417        .filter_map(|(idx, m)| {
3418            if idx >= a && idx <= b && m.kind == TableNavRowKind::Leaf {
3419                Some(m.row_key)
3420            } else {
3421                None
3422            }
3423        })
3424        .collect()
3425}
3426
3427struct TableKeyboardNavState {
3428    active_index: Rc<Cell<Option<usize>>>,
3429    anchor_index: Rc<Cell<Option<RowKey>>>,
3430    row_meta: Rc<RefCell<Arc<[TableNavRowMeta]>>>,
3431    active_element: Rc<Cell<Option<fret_ui::GlobalElementId>>>,
3432    active_command: Rc<RefCell<Option<CommandId>>>,
3433    typeahead: Rc<RefCell<TypeaheadBuffer>>,
3434    typeahead_timer: Rc<Cell<Option<TimerToken>>>,
3435}
3436
3437impl Default for TableKeyboardNavState {
3438    fn default() -> Self {
3439        Self {
3440            active_index: Rc::default(),
3441            anchor_index: Rc::default(),
3442            row_meta: Rc::default(),
3443            active_element: Rc::default(),
3444            active_command: Rc::default(),
3445            typeahead: Rc::new(RefCell::new(TypeaheadBuffer::new(0))),
3446            typeahead_timer: Rc::default(),
3447        }
3448    }
3449}
3450
3451#[allow(clippy::too_many_arguments)]
3452#[track_caller]
3453pub fn table_virtualized<H: UiHost, TData, IHeader, TH, ICell, TC>(
3454    cx: &mut ElementContext<'_, H>,
3455    data: &[TData],
3456    columns: &[ColumnDef<TData>],
3457    state: impl IntoTableStateModel,
3458    vertical_scroll: &VirtualListScrollHandle,
3459    items_revision: u64,
3460    row_key_at: &RowKeyAt<TData>,
3461    typeahead_label_at: Option<Arc<TypeaheadLabelAt<TData>>>,
3462    props: TableViewProps,
3463    on_row_activate: impl Fn(&Row<'_, TData>) -> Option<CommandId>,
3464    render_header_cell: impl FnMut(
3465        &mut ElementContext<'_, H>,
3466        &ColumnDef<TData>,
3467        Option<bool>,
3468    ) -> IHeader,
3469    render_cell: impl FnMut(&mut ElementContext<'_, H>, &Row<'_, TData>, &ColumnDef<TData>) -> ICell,
3470    output: Option<Model<TableViewOutput>>,
3471    debug_ids: TableDebugIds,
3472) -> AnyElement
3473where
3474    IHeader: IntoIterator<Item = TH>,
3475    TH: IntoUiElement<H>,
3476    ICell: IntoIterator<Item = TC>,
3477    TC: IntoUiElement<H>,
3478{
3479    let state = state.into_table_state_model();
3480    table_virtualized_impl(
3481        cx,
3482        data,
3483        columns,
3484        state,
3485        vertical_scroll,
3486        items_revision,
3487        row_key_at,
3488        typeahead_label_at,
3489        props,
3490        None,
3491        on_row_activate,
3492        render_header_cell,
3493        render_cell,
3494        output,
3495        debug_ids,
3496    )
3497}
3498
3499/// Virtualized table helper that participates in cross-surface clipboard commands (`edit.copy`).
3500///
3501/// `copy_text_at` receives the data index for the selected/active leaf row.
3502#[allow(clippy::too_many_arguments)]
3503#[track_caller]
3504pub fn table_virtualized_copyable<H: UiHost, TData, IHeader, TH, ICell, TC>(
3505    cx: &mut ElementContext<'_, H>,
3506    data: &[TData],
3507    columns: &[ColumnDef<TData>],
3508    state: impl IntoTableStateModel,
3509    vertical_scroll: &VirtualListScrollHandle,
3510    items_revision: u64,
3511    row_key_at: &RowKeyAt<TData>,
3512    typeahead_label_at: Option<Arc<TypeaheadLabelAt<TData>>>,
3513    props: TableViewProps,
3514    copy_text_at: Arc<CopyTextAtFn>,
3515    on_row_activate: impl Fn(&Row<'_, TData>) -> Option<CommandId>,
3516    render_header_cell: impl FnMut(
3517        &mut ElementContext<'_, H>,
3518        &ColumnDef<TData>,
3519        Option<bool>,
3520    ) -> IHeader,
3521    render_cell: impl FnMut(&mut ElementContext<'_, H>, &Row<'_, TData>, &ColumnDef<TData>) -> ICell,
3522    output: Option<Model<TableViewOutput>>,
3523    debug_ids: TableDebugIds,
3524) -> AnyElement
3525where
3526    IHeader: IntoIterator<Item = TH>,
3527    TH: IntoUiElement<H>,
3528    ICell: IntoIterator<Item = TC>,
3529    TC: IntoUiElement<H>,
3530{
3531    let state = state.into_table_state_model();
3532    table_virtualized_impl(
3533        cx,
3534        data,
3535        columns,
3536        state,
3537        vertical_scroll,
3538        items_revision,
3539        row_key_at,
3540        typeahead_label_at,
3541        props,
3542        Some(copy_text_at),
3543        on_row_activate,
3544        render_header_cell,
3545        render_cell,
3546        output,
3547        debug_ids,
3548    )
3549}
3550
3551/// Retained-host virtualized table helper (virt-003 / ADR 0177).
3552///
3553/// This is an opt-in surface intended for perf/correctness harnesses. v0 is intentionally minimal:
3554/// - fixed-height or measured body rows (controlled by `props.row_measure_mode`)
3555/// - flat (non-grouped) tables only
3556/// - sorting (including multi-sort state) is supported
3557/// - focusable "List" semantics with keyboard navigation and typeahead (opt-in labels)
3558///
3559/// Typeahead labels:
3560/// - When `typeahead_label_at` is `Some`, it is used to compute per-row labels for prefix
3561///   typeahead navigation.
3562/// - The `usize` argument is the row's `data_index` into `data`.
3563///
3564/// The key benefit is that overscan window boundary updates can attach/detach body row subtrees
3565/// without forcing a parent cache-root rerender under view-cache reuse.
3566#[allow(clippy::too_many_arguments)]
3567#[track_caller]
3568pub fn table_virtualized_retained_v0<H: UiHost + 'static, TData>(
3569    cx: &mut ElementContext<'_, H>,
3570    data: Arc<[TData]>,
3571    columns: Arc<[ColumnDef<TData>]>,
3572    state: impl IntoTableStateModel,
3573    vertical_scroll: &VirtualListScrollHandle,
3574    items_revision: u64,
3575    row_key_at: Arc<RowKeyAt<TData>>,
3576    typeahead_label_at: Option<Arc<TypeaheadLabelAt<TData>>>,
3577    props: TableViewProps,
3578    header_label: Arc<HeaderLabelAt<TData>>,
3579    header_accessory_at: Option<Arc<HeaderAccessoryAt<H, TData>>>,
3580    cell_at: Arc<CellAt<H, TData>>,
3581    debug_ids: TableDebugIds,
3582) -> AnyElement
3583where
3584    TData: 'static,
3585{
3586    let state = state.into_table_state_model();
3587    let TableDebugIds {
3588        header_row_test_id: debug_header_row_test_id,
3589        header_cell_test_id_prefix: debug_header_cell_test_id_prefix,
3590        row_test_id_prefix: debug_row_test_id_prefix,
3591    } = debug_ids;
3592
3593    #[derive(Debug, Clone, Copy)]
3594    struct RowEntry {
3595        key: RowKey,
3596        data_index: usize,
3597    }
3598
3599    #[derive(Default)]
3600    struct RetainedTableRowsState {
3601        last_items_revision: Option<u64>,
3602        entries: Arc<[RowEntry]>,
3603    }
3604
3605    let theme = Theme::global(&*cx.app);
3606    let (table_bg, border, header_bg, row_hover, row_active) = resolve_table_colors(theme);
3607    let ring = theme
3608        .color_by_key("ring")
3609        .or_else(|| theme.color_by_key("focus.ring"))
3610        .or_else(|| theme.color_by_key("primary"))
3611        .unwrap_or(row_active);
3612    let ring = emphasize_border(ring, 0.9);
3613    let row_hover_bg = Color {
3614        a: row_hover.a.min(0.12),
3615        ..row_hover
3616    };
3617    let row_active_bg = Color {
3618        a: row_active.a.min(0.18),
3619        ..row_active
3620    };
3621    let radius = theme.metric_token("metric.radius.md");
3622
3623    let row_h = props
3624        .row_height
3625        .unwrap_or_else(|| resolve_row_height(theme, props.size));
3626    let header_h = props.header_height.unwrap_or(row_h);
3627
3628    let cell_px = resolve_cell_padding_x(theme);
3629    let cell_py = resolve_cell_padding_y(theme);
3630
3631    let state_value = cx.watch_model(&state).layout().cloned_or_default();
3632    let sorting = state_value.sorting.clone();
3633
3634    let empty: &[TData] = &[];
3635    let mut sizing_state = state_value.clone();
3636    if !props.enable_column_grouping {
3637        sizing_state.grouping.clear();
3638    }
3639
3640    let sizing_columns: Vec<ColumnDef<TData>> = columns
3641        .iter()
3642        .cloned()
3643        .map(|c| with_table_view_column_constraints(c, &props))
3644        .collect();
3645
3646    let sizing_options = TableOptions {
3647        grouped_column_mode: props.grouped_column_mode,
3648        ..Default::default()
3649    };
3650
3651    let sizing_table = Table::builder(empty)
3652        .columns(sizing_columns)
3653        .state(sizing_state)
3654        .options(sizing_options)
3655        .build();
3656    let core_snapshot = sizing_table.core_model_snapshot();
3657
3658    let columns: Arc<[ColumnDef<TData>]> = Arc::from(sizing_table.columns().to_vec());
3659
3660    let visible_columns: Arc<[ColumnDef<TData>]> = Arc::from(
3661        core_snapshot
3662            .leaf_columns
3663            .visible
3664            .iter()
3665            .filter_map(|id| sizing_table.column(id.as_ref()).cloned())
3666            .collect::<Vec<_>>(),
3667    );
3668
3669    let col_widths: Arc<[Px]> = Arc::from(
3670        visible_columns
3671            .iter()
3672            .map(|col| {
3673                let w = core_snapshot
3674                    .leaf_column_sizing
3675                    .size
3676                    .get(&col.id)
3677                    .copied()
3678                    .unwrap_or(col.size);
3679                Px(w)
3680            })
3681            .collect::<Vec<_>>(),
3682    );
3683
3684    let mut visible_column_index_by_id: std::collections::HashMap<ColumnId, usize> =
3685        std::collections::HashMap::new();
3686    for (idx, col) in visible_columns.iter().enumerate() {
3687        visible_column_index_by_id.insert(col.id.clone(), idx);
3688    }
3689
3690    let left_col_indices: Arc<[usize]> = Arc::from(
3691        core_snapshot
3692            .leaf_columns
3693            .left_visible
3694            .iter()
3695            .filter_map(|id| visible_column_index_by_id.get(id).copied())
3696            .collect::<Vec<_>>(),
3697    );
3698    let center_col_indices: Arc<[usize]> = Arc::from(
3699        core_snapshot
3700            .leaf_columns
3701            .center_visible
3702            .iter()
3703            .filter_map(|id| visible_column_index_by_id.get(id).copied())
3704            .collect::<Vec<_>>(),
3705    );
3706    let right_col_indices: Arc<[usize]> = Arc::from(
3707        core_snapshot
3708            .leaf_columns
3709            .right_visible
3710            .iter()
3711            .filter_map(|id| visible_column_index_by_id.get(id).copied())
3712            .collect::<Vec<_>>(),
3713    );
3714
3715    let scroll_x = cx.slot_state(ScrollHandle::default, |h| h.clone());
3716
3717    let entries = cx.slot_state(RetainedTableRowsState::default, |st| {
3718        if st.last_items_revision != Some(items_revision) {
3719            st.last_items_revision = Some(items_revision);
3720
3721            let mut entries: Vec<RowEntry> = (0..data.len())
3722                .map(|i| RowEntry {
3723                    key: (row_key_at)(&data[i], i),
3724                    data_index: i,
3725                })
3726                .collect();
3727
3728            if !sorting.is_empty() {
3729                let mut sorters: Vec<SorterSpec<TData>> = Vec::new();
3730                for spec in &sorting {
3731                    if let Some(col) = columns
3732                        .iter()
3733                        .find(|c| c.id.as_ref() == spec.column.as_ref())
3734                        && let Some(cmp) = col.sort_cmp.as_ref()
3735                    {
3736                        sorters.push((spec.clone(), Arc::clone(cmp)));
3737                    }
3738                }
3739
3740                if !sorters.is_empty() {
3741                    entries.sort_by(|a, b| {
3742                        let a_row = &data[a.data_index];
3743                        let b_row = &data[b.data_index];
3744                        for (spec, cmp) in &sorters {
3745                            let ord = (cmp)(a_row, b_row);
3746                            let ord = if spec.desc { ord.reverse() } else { ord };
3747                            if ord != std::cmp::Ordering::Equal {
3748                                return ord;
3749                            }
3750                        }
3751
3752                        a.key.cmp(&b.key)
3753                    });
3754                }
3755            }
3756
3757            st.entries = Arc::from(entries);
3758        }
3759
3760        st.entries.clone()
3761    });
3762
3763    let mut fill_layout = LayoutStyle::default();
3764    fill_layout.size.width = Length::Fill;
3765    fill_layout.size.height = Length::Fill;
3766    fill_layout.flex.grow = 1.0;
3767    fill_layout.flex.basis = Length::Px(Px(0.0));
3768
3769    let mut options = VirtualListOptions::new(row_h, props.overscan);
3770    options.items_revision = items_revision;
3771    options.keep_alive = props
3772        .keep_alive
3773        .unwrap_or_else(|| props.overscan.saturating_mul(2));
3774    match props.row_measure_mode {
3775        TableRowMeasureMode::Fixed => {
3776            options.measure_mode = fret_ui::element::VirtualListMeasureMode::Fixed;
3777            options.key_cache = fret_ui::element::VirtualListKeyCacheMode::VisibleOnly;
3778        }
3779        TableRowMeasureMode::Measured => {
3780            options.measure_mode = fret_ui::element::VirtualListMeasureMode::Measured;
3781            options.key_cache = fret_ui::element::VirtualListKeyCacheMode::AllKeys;
3782        }
3783    }
3784
3785    let header = {
3786        let state = state.clone();
3787        let header_label = Arc::clone(&header_label);
3788        let debug_header_row_test_id = debug_header_row_test_id.clone();
3789        let debug_header_cell_test_id_prefix = debug_header_cell_test_id_prefix.clone();
3790        let visible_columns = visible_columns.clone();
3791        let col_widths = col_widths.clone();
3792        let left_col_indices = left_col_indices.clone();
3793        let center_col_indices = center_col_indices.clone();
3794        let right_col_indices = right_col_indices.clone();
3795        let scroll_x = scroll_x.clone();
3796        let sorting = sorting.clone();
3797        let data_for_header = data.clone();
3798
3799        let header = cx.container(
3800            ContainerProps {
3801                background: Some(header_bg),
3802                border: Edges {
3803                    bottom: Px(1.0),
3804                    ..Default::default()
3805                },
3806                border_color: Some(border),
3807                corner_radii: Corners {
3808                    top_left: radius,
3809                    top_right: radius,
3810                    ..Default::default()
3811                },
3812                layout: LayoutStyle {
3813                    size: fret_ui::element::SizeStyle {
3814                        width: Length::Fill,
3815                        height: Length::Px(header_h),
3816                        min_height: Some(Length::Px(header_h)),
3817                        max_height: Some(Length::Px(header_h)),
3818                        ..Default::default()
3819                    },
3820                    flex: fret_ui::element::FlexItemStyle {
3821                        shrink: 0.0,
3822                        basis: Length::Px(header_h),
3823                        ..Default::default()
3824                    },
3825                    ..Default::default()
3826                },
3827                ..Default::default()
3828            },
3829            move |cx| {
3830                let render_header_group =
3831                    |cx: &mut ElementContext<'_, H>,
3832                     col_indices: Arc<[usize]>,
3833                     scroll_x_for_group: Option<ScrollHandle>| {
3834                        let visible_columns = visible_columns.clone();
3835                        let col_widths = col_widths.clone();
3836                        let header_label = header_label.clone();
3837                        let header_accessory_at = header_accessory_at.clone();
3838                        let debug_header_cell_test_id_prefix =
3839                            debug_header_cell_test_id_prefix.clone();
3840                        let sorting = sorting.clone();
3841                        let state = state.clone();
3842                        let enable_sorting = props.enable_sorting;
3843                        let data = data_for_header.clone();
3844
3845                        let row = ui::h_row(move |cx| {
3846                            col_indices
3847                                    .iter()
3848                                    .map(|col_idx| {
3849                                        let col = &visible_columns[*col_idx];
3850                                        let col_w = col_widths[*col_idx];
3851                                        let label = (header_label)(col);
3852                                        let header_accessory_at = header_accessory_at.clone();
3853                                        let sort_state = sort_for_column(&sorting, &col.id);
3854                                        let col_id = col.id.clone();
3855                                        let state = state.clone();
3856                                        let sorting_for_cell = sorting.clone();
3857                                        let enabled = enable_sorting
3858                                            && col.enable_sorting
3859                                            && (col.sort_cmp.is_some() || col.sort_value.is_some());
3860                                        let sort_options = TableOptions {
3861                                            enable_sorting,
3862                                            ..TableOptions::default()
3863                                        };
3864                                        let sort_toggle_column = SortToggleColumn {
3865                                            id: col_id.clone(),
3866                                            enable_sorting: col.enable_sorting,
3867                                            enable_multi_sort: col.enable_multi_sort,
3868                                            sort_desc_first: col.sort_desc_first,
3869                                            has_sort_value_source: col.sort_cmp.is_some()
3870                                                || col.sort_value.is_some(),
3871                                        };
3872                                        let auto_sort_dir_desc = col
3873                                            .sort_value
3874                                            .as_ref()
3875                                            .and_then(|f| data.first().map(|r| f(r)))
3876                                            .map(|v| !matches!(v, TanStackValue::String(_)))
3877                                            .unwrap_or(false);
3878                                        let debug_test_id: Option<Arc<str>> =
3879                                            debug_header_cell_test_id_prefix.as_ref().map(
3880                                                |prefix| {
3881                                                    Arc::<str>::from(format!(
3882                                                        "{prefix}{id}",
3883                                                        id = col_id.as_ref()
3884                                                    ))
3885                                                },
3886                                            );
3887
3888                                        cx.container(
3889                                            ContainerProps {
3890                                                border: if props.optimize_grid_lines {
3891                                                    Edges::default()
3892                                                } else {
3893                                                    Edges {
3894                                                        right: Px(1.0),
3895                                                        ..Default::default()
3896                                                    }
3897                                                },
3898                                                border_color: if props.optimize_grid_lines {
3899                                                    None
3900                                                } else {
3901                                                    Some(border)
3902                                                },
3903                                                layout: table_fixed_column_fill_layout(col_w),
3904                                                ..Default::default()
3905                                            },
3906                                            move |cx| {
3907                                                vec![cx.pressable(
3908                                                    PressableProps {
3909                                                        layout: {
3910                                                            let mut layout = LayoutStyle::default();
3911                                                            layout.size.width = Length::Fill;
3912                                                            layout.size.height = Length::Fill;
3913                                                            layout
3914                                                        },
3915                                                        enabled,
3916                                                        a11y: PressableA11y {
3917                                                            role: Some(SemanticsRole::Button),
3918                                                            label: Some(label.clone()),
3919                                                            test_id: debug_test_id.clone(),
3920                                                            ..Default::default()
3921                                                        },
3922                                                        ..Default::default()
3923                                                    },
3924                                                    move |cx, _st| {
3925                                                        if enabled {
3926                                                            let state_model_for_pointer =
3927                                                                state.clone();
3928                                                            let sort_toggle_column_for_pointer =
3929                                                                sort_toggle_column.clone();
3930                                                            let sort_options_for_pointer =
3931                                                                sort_options;
3932                                                            cx.pressable_on_pointer_up(Arc::new(
3933                                                                move |host, acx, up| {
3934                                                                    if !up.is_click
3935                                                                        || up.button
3936                                                                            != fret_core::MouseButton::Left
3937                                                                    {
3938                                                                        return PressablePointerUpResult::Continue;
3939                                                                    }
3940
3941                                                                    let multi =
3942                                                                        up.modifiers.shift;
3943                                                                    let _ = host.update_model(
3944                                                                        &state_model_for_pointer,
3945                                                                        |st| {
3946                                                                            toggle_sorting_state_handler_tanstack(
3947                                                                                &mut st.sorting,
3948                                                                                &sort_toggle_column_for_pointer,
3949                                                                                sort_options_for_pointer,
3950                                                                                multi,
3951                                                                                auto_sort_dir_desc,
3952                                                                            );
3953                                                                            st.pagination.page_index = 0;
3954                                                                        },
3955                                                                    );
3956                                                                    host.notify(acx);
3957                                                                    PressablePointerUpResult::SkipActivate
3958                                                                },
3959                                                            ));
3960
3961                                                            cx.pressable_update_model(
3962                                                                &state,
3963                                                                move |st| {
3964                                                                    toggle_sorting_state_handler_tanstack(
3965                                                                        &mut st.sorting,
3966                                                                        &sort_toggle_column,
3967                                                                        sort_options,
3968                                                                        false,
3969                                                                        auto_sort_dir_desc,
3970                                                                    );
3971                                                                    st.pagination.page_index = 0;
3972                                                                },
3973                                                            );
3974                                                        }
3975
3976                                                        let header_text: Arc<str> = match sort_state
3977                                                        {
3978                                                            None => label.clone(),
3979                                                            Some(desc) => {
3980                                                                let order = if sorting_for_cell
3981                                                                    .len()
3982                                                                    > 1
3983                                                                {
3984                                                                    sorting_for_cell
3985                                                                        .iter()
3986                                                                        .position(|s| {
3987                                                                            s.column.as_ref()
3988                                                                                == col_id.as_ref()
3989                                                                        })
3990                                                                        .map(|v| v + 1)
3991                                                                } else {
3992                                                                    None
3993                                                                };
3994                                                                match order {
3995                                                                    Some(order) => {
3996                                                                        Arc::<str>::from(format!(
3997                                                                            "{} {}{}",
3998                                                                            label,
3999                                                                            if desc { "â–¼" } else { "â–²" },
4000                                                                            order
4001                                                                        ))
4002                                                                    }
4003                                                                    None => Arc::<str>::from(format!(
4004                                                                        "{} {}",
4005                                                                        label,
4006                                                                        if desc { "â–¼" } else { "â–²" }
4007                                                                    )),
4008                                                                }
4009                                                            }
4010                                                        };
4011
4012                                                        vec![cx.container(
4013                                                            ContainerProps {
4014                                                                padding: Edges::symmetric(
4015                                                                    cell_px, cell_py,
4016                                                                )
4017                                                                .into(),
4018                                                                layout: {
4019                                                                    let mut layout =
4020                                                                        LayoutStyle::default();
4021                                                                    layout.size.width =
4022                                                                        Length::Fill;
4023                                                                    layout.size.height =
4024                                                                        Length::Fill;
4025                                                                    layout
4026                                                                },
4027                                                                ..Default::default()
4028                                                            },
4029                                                            move |_cx| {
4030                                                                let accessory =
4031                                                                    header_accessory_at.as_ref().map(|f| f(_cx, col));
4032                                                                match accessory {
4033                                                                    None => {
4034                                                                        vec![_cx.text(header_text.as_ref())]
4035                                                                    }
4036                                                                    Some(accessory) => {
4037                                                                        vec![ui::h_row(move |_cx| {
4038                                                                            [
4039                                                                                _cx.text(header_text.as_ref()),
4040                                                                                _cx.spacer(
4041                                                                                    SpacerProps::default(),
4042                                                                                ),
4043                                                                                accessory,
4044                                                                            ]
4045                                                                        })
4046                                                                        .gap(Space::N1)
4047                                                                        .justify_start()
4048                                                                        .items_center()
4049                                                                        .into_element(_cx)]
4050                                                                    }
4051                                                                }
4052                                                            },
4053                                                        )]
4054                                                    },
4055                                                )]
4056                                            },
4057                                        )
4058                                    })
4059                                    .collect::<Vec<_>>()
4060                        })
4061                        .gap(Space::N0)
4062                        .justify_start()
4063                        .items_center()
4064                        .into_element(cx);
4065
4066                        table_wrap_horizontal_scroll(cx, scroll_x_for_group, row)
4067                    };
4068
4069                vec![ui::h_row(|cx| {
4070                    let left = render_header_group(cx, left_col_indices.clone(), None);
4071                    let center =
4072                        render_header_group(cx, center_col_indices.clone(), Some(scroll_x.clone()));
4073                    let right = render_header_group(cx, right_col_indices.clone(), None);
4074                    [left, center, right]
4075                })
4076                .gap(Space::N0)
4077                .justify_start()
4078                .items_stretch()
4079                .layout(LayoutRefinement::default().w_full())
4080                .into_element(cx)]
4081            },
4082        );
4083
4084        if let Some(test_id) = debug_header_row_test_id {
4085            header.test_id(test_id)
4086        } else {
4087            header
4088        }
4089    };
4090
4091    let key_at: Arc<dyn Fn(usize) -> fret_ui::ItemKey> = {
4092        let entries = entries.clone();
4093        Arc::new(move |i| entries[i].key.0)
4094    };
4095
4096    struct RetainedTableKeyboardNavState {
4097        active_index: Rc<Cell<Option<usize>>>,
4098        anchor_index: Rc<Cell<Option<RowKey>>>,
4099        active_element: Rc<Cell<Option<fret_ui::GlobalElementId>>>,
4100        labels: Rc<RefCell<Arc<[Arc<str>]>>>,
4101        disabled: Rc<RefCell<Arc<[bool]>>>,
4102        last_labels_revision: Cell<Option<u64>>,
4103        typeahead: Rc<RefCell<TypeaheadBuffer>>,
4104        typeahead_timer: Rc<Cell<Option<TimerToken>>>,
4105    }
4106
4107    impl Default for RetainedTableKeyboardNavState {
4108        fn default() -> Self {
4109            Self {
4110                active_index: Rc::default(),
4111                anchor_index: Rc::default(),
4112                active_element: Rc::default(),
4113                labels: Rc::new(RefCell::new(Arc::from([]))),
4114                disabled: Rc::new(RefCell::new(Arc::from([]))),
4115                last_labels_revision: Cell::new(None),
4116                typeahead: Rc::new(RefCell::new(TypeaheadBuffer::new(0))),
4117                typeahead_timer: Rc::default(),
4118            }
4119        }
4120    }
4121
4122    let (
4123        active_index,
4124        anchor_index,
4125        active_element,
4126        labels,
4127        disabled,
4128        _last_labels_revision,
4129        typeahead,
4130        typeahead_timer,
4131    ) = cx.slot_state(RetainedTableKeyboardNavState::default, |nav| {
4132        if nav.last_labels_revision.get() != Some(items_revision) {
4133            nav.last_labels_revision.set(Some(items_revision));
4134
4135            if let Some(typeahead_label_at) = &typeahead_label_at {
4136                let mut next_labels: Vec<Arc<str>> = Vec::with_capacity(entries.len());
4137                let mut next_disabled: Vec<bool> = Vec::with_capacity(entries.len());
4138                for entry in entries.iter() {
4139                    let i = entry.data_index;
4140                    let label = (typeahead_label_at)(&data[i], i);
4141                    let disabled = label.trim().is_empty();
4142                    next_labels.push(label);
4143                    next_disabled.push(disabled);
4144                }
4145                *nav.labels.borrow_mut() = Arc::from(next_labels);
4146                *nav.disabled.borrow_mut() = Arc::from(next_disabled);
4147            } else {
4148                let mut next_labels: Vec<Arc<str>> = Vec::with_capacity(entries.len());
4149                let mut next_disabled: Vec<bool> = Vec::with_capacity(entries.len());
4150                for entry in entries.iter() {
4151                    let label: Arc<str> = Arc::from(entry.key.0.to_string());
4152                    next_labels.push(label);
4153                    next_disabled.push(false);
4154                }
4155                *nav.labels.borrow_mut() = Arc::from(next_labels);
4156                *nav.disabled.borrow_mut() = Arc::from(next_disabled);
4157            }
4158        }
4159
4160        (
4161            nav.active_index.clone(),
4162            nav.anchor_index.clone(),
4163            nav.active_element.clone(),
4164            nav.labels.clone(),
4165            nav.disabled.clone(),
4166            nav.last_labels_revision.clone(),
4167            nav.typeahead.clone(),
4168            nav.typeahead_timer.clone(),
4169        )
4170    });
4171
4172    let row_builder =
4173        {
4174            let state = state.clone();
4175            let entries = entries.clone();
4176            let data = data.clone();
4177            let props = props.clone();
4178            let visible_columns = visible_columns.clone();
4179            let col_widths = col_widths.clone();
4180            let cell_at = Arc::clone(&cell_at);
4181            let debug_row_test_id_prefix = debug_row_test_id_prefix.clone();
4182            let left_col_indices = left_col_indices.clone();
4183            let center_col_indices = center_col_indices.clone();
4184            let right_col_indices = right_col_indices.clone();
4185            let scroll_x = scroll_x.clone();
4186            let active_index_for_row_builder = active_index.clone();
4187            let active_element_for_row_builder = active_element.clone();
4188            let anchor_index_for_row_builder = anchor_index.clone();
4189
4190            move |key_handler: fret_ui::action::OnKeyDown, focus_target: GlobalElementId| {
4191                let active_index_for_row = active_index_for_row_builder.clone();
4192                let active_element_for_row = active_element_for_row_builder.clone();
4193                let anchor_index_for_row = anchor_index_for_row_builder.clone();
4194                Arc::new(move |cx: &mut ElementContext<'_, H>, i: usize| {
4195                    let entry = entries[i];
4196                    let row_key = entry.key;
4197                    let data_index = entry.data_index;
4198
4199                    let selected = cx
4200                        .watch_model(&state)
4201                        .paint()
4202                        .read_ref(|s| s.row_selection.contains(&row_key))
4203                        .ok()
4204                        .unwrap_or(false);
4205
4206                    let test_id = debug_row_test_id_prefix
4207                        .as_ref()
4208                        .map(|prefix| Arc::<str>::from(format!("{}{id}", prefix, id = row_key.0)));
4209
4210                    let state_model = state.clone();
4211                    let entries = entries.clone();
4212                    let anchor_index = anchor_index_for_row.clone();
4213                    let data_for_row = Arc::clone(&data);
4214                    let columns_for_row = Arc::clone(&visible_columns);
4215                    let col_widths_for_row = col_widths.clone();
4216                    let cell_at_for_row = Arc::clone(&cell_at);
4217                    let key_handler_for_row = key_handler.clone();
4218                    let focus_target_for_row = focus_target;
4219                    let row_cell_test_id_prefix = debug_row_test_id_prefix.clone();
4220                    let left_col_indices_for_row = left_col_indices.clone();
4221                    let center_col_indices_for_row = center_col_indices.clone();
4222                    let right_col_indices_for_row = right_col_indices.clone();
4223                    let scroll_x_for_row = scroll_x.clone();
4224                    let active_index = active_index_for_row.clone();
4225                    let active_element = active_element_for_row.clone();
4226                    let focus_target = focus_target_for_row;
4227                    let single = props.single_row_selection;
4228                    let policy = props.pointer_row_selection_policy;
4229                    let pointer_row_selection_enabled =
4230                        props.enable_row_selection && props.pointer_row_selection;
4231                    let row_wrapper_layout = retained_table_row_fill_layout();
4232
4233                    let render_row_visuals =
4234                        |cx: &mut ElementContext<'_, H>, hovered: bool, pressed: bool| {
4235                            let bg = if selected || pressed {
4236                                Some(row_active_bg)
4237                            } else if hovered {
4238                                Some(row_hover_bg)
4239                            } else {
4240                                None
4241                            };
4242
4243                            vec![retained_table_render_row_visuals(
4244                                cx,
4245                                data_for_row.clone(),
4246                                data_index,
4247                                row_key,
4248                                bg,
4249                                props.clone(),
4250                                border,
4251                                cell_px,
4252                                cell_py,
4253                                key_handler_for_row.clone(),
4254                                columns_for_row.clone(),
4255                                col_widths_for_row.clone(),
4256                                cell_at_for_row.clone(),
4257                                row_cell_test_id_prefix.clone(),
4258                                left_col_indices_for_row.clone(),
4259                                center_col_indices_for_row.clone(),
4260                                right_col_indices_for_row.clone(),
4261                                scroll_x_for_row.clone(),
4262                            )]
4263                        };
4264
4265                    if pointer_row_selection_enabled {
4266                        cx.pressable_with_id(
4267                            PressableProps {
4268                                enabled: true,
4269                                focusable: false,
4270                                a11y: PressableA11y {
4271                                    role: Some(SemanticsRole::ListItem),
4272                                    test_id,
4273                                    selected,
4274                                    ..Default::default()
4275                                },
4276                                ..Default::default()
4277                            },
4278                            move |cx, st, id| {
4279                                let active_index_for_pointer_down = active_index.clone();
4280                                cx.pressable_add_on_pointer_down(Arc::new(
4281                                    move |_host, action_cx, down| {
4282                                        if down.button != fret_core::MouseButton::Left {
4283                                            return PressablePointerDownResult::Continue;
4284                                        }
4285                                        if down
4286                                            .hit_pressable_target
4287                                            .is_some_and(|t| t != action_cx.target)
4288                                        {
4289                                            return PressablePointerDownResult::Continue;
4290                                        }
4291                                        active_index_for_pointer_down.set(Some(i));
4292                                        PressablePointerDownResult::Continue
4293                                    },
4294                                ));
4295                                if policy == PointerRowSelectionPolicy::ListLike {
4296                                    let anchor_index_for_pointer_down = anchor_index.clone();
4297                                    let row_key_for_anchor = row_key;
4298                                    cx.pressable_add_on_pointer_down(Arc::new(
4299                                        move |_host, action_cx, down| {
4300                                            if down.button != fret_core::MouseButton::Left {
4301                                                return PressablePointerDownResult::Continue;
4302                                            }
4303                                            if down
4304                                                .hit_pressable_target
4305                                                .is_some_and(|t| t != action_cx.target)
4306                                            {
4307                                                return PressablePointerDownResult::Continue;
4308                                            }
4309                                            let next_anchor = if down.modifiers.shift {
4310                                                anchor_index_for_pointer_down
4311                                                    .get()
4312                                                    .or(Some(row_key_for_anchor))
4313                                            } else {
4314                                                Some(row_key_for_anchor)
4315                                            };
4316                                            anchor_index_for_pointer_down.set(next_anchor);
4317                                            PressablePointerDownResult::Continue
4318                                        },
4319                                    ));
4320                                }
4321
4322                                cx.pressable_on_pointer_up(Arc::new(move |host, action_cx, up| {
4323                                    if up.button != fret_core::MouseButton::Left || !up.is_click {
4324                                        return PressablePointerUpResult::Continue;
4325                                    }
4326                                    if up
4327                                        .down_hit_pressable_target
4328                                        .is_some_and(|t| t != action_cx.target)
4329                                    {
4330                                        return PressablePointerUpResult::Continue;
4331                                    }
4332                                    host.request_focus(focus_target);
4333                                    let modifiers = up.modifiers;
4334                                    let mut range_keys: Option<Vec<RowKey>> = None;
4335                                    if policy == PointerRowSelectionPolicy::ListLike
4336                                        && !single
4337                                        && modifiers.shift
4338                                    {
4339                                        let anchor_key = anchor_index.get().unwrap_or(row_key);
4340                                        let anchor = entries
4341                                            .iter()
4342                                            .position(|entry| entry.key == anchor_key)
4343                                            .unwrap_or(i);
4344                                        let (a, b) = if anchor <= i {
4345                                            (anchor, i)
4346                                        } else {
4347                                            (i, anchor)
4348                                        };
4349                                        let keys = entries
4350                                            .iter()
4351                                            .enumerate()
4352                                            .filter_map(|(idx, entry)| {
4353                                                (idx >= a && idx <= b).then_some(entry.key)
4354                                            })
4355                                            .collect::<Vec<_>>();
4356                                        if !keys.is_empty() {
4357                                            range_keys = Some(keys);
4358                                        }
4359                                    }
4360
4361                                    let anchor_index_for_update = anchor_index.clone();
4362                                    let _ = host.models_mut().update(&state_model, move |st| {
4363                                        match policy {
4364                                            PointerRowSelectionPolicy::Toggle => {
4365                                                let selected = st.row_selection.contains(&row_key);
4366                                                if single {
4367                                                    st.row_selection.clear();
4368                                                }
4369                                                if selected {
4370                                                    st.row_selection.remove(&row_key);
4371                                                } else {
4372                                                    st.row_selection.insert(row_key);
4373                                                }
4374                                            }
4375                                            PointerRowSelectionPolicy::ListLike => {
4376                                                if let Some(range_keys) = range_keys.clone() {
4377                                                    if modifiers.ctrl || modifiers.meta {
4378                                                        st.row_selection
4379                                                            .extend(range_keys.iter().copied());
4380                                                    } else {
4381                                                        st.row_selection.clear();
4382                                                        st.row_selection
4383                                                            .extend(range_keys.iter().copied());
4384                                                    }
4385                                                } else if !single
4386                                                    && (modifiers.ctrl || modifiers.meta)
4387                                                {
4388                                                    if st.row_selection.contains(&row_key) {
4389                                                        st.row_selection.remove(&row_key);
4390                                                    } else {
4391                                                        st.row_selection.insert(row_key);
4392                                                    }
4393                                                } else {
4394                                                    st.row_selection.clear();
4395                                                    st.row_selection.insert(row_key);
4396                                                }
4397                                            }
4398                                        }
4399                                    });
4400                                    if policy == PointerRowSelectionPolicy::ListLike {
4401                                        let next_anchor = if modifiers.shift {
4402                                            anchor_index_for_update.get().or(Some(row_key))
4403                                        } else {
4404                                            Some(row_key)
4405                                        };
4406                                        anchor_index_for_update.set(next_anchor);
4407                                    } else {
4408                                        anchor_index_for_update.set(Some(row_key));
4409                                    }
4410                                    host.request_redraw(action_cx.window);
4411                                    PressablePointerUpResult::SkipActivate
4412                                }));
4413
4414                                if active_index.get() == Some(i) {
4415                                    active_element.set(Some(id));
4416                                }
4417                                render_row_visuals(cx, st.hovered, st.pressed)
4418                            },
4419                        )
4420                    } else {
4421                        cx.semantics_with_id(
4422                            SemanticsProps {
4423                                layout: row_wrapper_layout,
4424                                role: SemanticsRole::ListItem,
4425                                test_id,
4426                                selected,
4427                                ..Default::default()
4428                            },
4429                            move |cx, id| {
4430                                if active_index.get() == Some(i) {
4431                                    active_element.set(Some(id));
4432                                }
4433                                vec![cx.hover_region(
4434                                    HoverRegionProps {
4435                                        layout: row_wrapper_layout,
4436                                    },
4437                                    move |cx, hovered| render_row_visuals(cx, hovered, false),
4438                                )]
4439                            },
4440                        )
4441                    }
4442                })
4443            }
4444        };
4445
4446    let list = cx.semantics_with_id(
4447        SemanticsProps {
4448            role: SemanticsRole::List,
4449            focusable: true,
4450            ..Default::default()
4451        },
4452        move |cx, list_id| {
4453            let state_for_keys = state.clone();
4454            let vertical_scroll_for_keys = vertical_scroll.clone();
4455            let entries_for_keys = entries.clone();
4456            let entries_for_list = entries.clone();
4457            let labels_for_keys = labels.clone();
4458            let disabled_for_keys = disabled.clone();
4459            let active_index_for_keys = active_index.clone();
4460            let anchor_index_for_keys = anchor_index.clone();
4461            let typeahead_for_keys = typeahead.clone();
4462            let typeahead_timer_for_keys = typeahead_timer.clone();
4463
4464            let key_handler: fret_ui::action::OnKeyDown = Arc::new(move |host, action_cx, down| {
4465                let Some(len) = entries_for_keys.len().checked_sub(1) else {
4466                    return false;
4467                };
4468
4469                let current = active_index_for_keys.get().unwrap_or(0).min(len);
4470
4471                let cancel_typeahead_timer =
4472                    |host: &mut dyn fret_ui::action::UiActionHost,
4473                     typeahead_timer: &Rc<Cell<Option<TimerToken>>>| {
4474                        if let Some(token) = typeahead_timer.get() {
4475                            host.push_effect(Effect::CancelTimer { token });
4476                            typeahead_timer.set(None);
4477                        }
4478                    };
4479
4480                match down.key {
4481                    KeyCode::ArrowDown => {
4482                        let next = (current + 1).min(len);
4483                        active_index_for_keys.set(Some(next));
4484                        anchor_index_for_keys.set(Some(entries_for_keys[next].key));
4485                        cancel_typeahead_timer(host, &typeahead_timer_for_keys);
4486                        typeahead_for_keys.borrow_mut().clear();
4487                        vertical_scroll_for_keys.scroll_to_item(next, ScrollStrategy::Nearest);
4488                        host.request_redraw(action_cx.window);
4489                        true
4490                    }
4491                    KeyCode::ArrowUp => {
4492                        let next = current.saturating_sub(1);
4493                        active_index_for_keys.set(Some(next));
4494                        anchor_index_for_keys.set(Some(entries_for_keys[next].key));
4495                        cancel_typeahead_timer(host, &typeahead_timer_for_keys);
4496                        typeahead_for_keys.borrow_mut().clear();
4497                        vertical_scroll_for_keys.scroll_to_item(next, ScrollStrategy::Nearest);
4498                        host.request_redraw(action_cx.window);
4499                        true
4500                    }
4501                    KeyCode::Home => {
4502                        active_index_for_keys.set(Some(0));
4503                        anchor_index_for_keys.set(Some(entries_for_keys[0].key));
4504                        cancel_typeahead_timer(host, &typeahead_timer_for_keys);
4505                        typeahead_for_keys.borrow_mut().clear();
4506                        vertical_scroll_for_keys.scroll_to_item(0, ScrollStrategy::Nearest);
4507                        host.request_redraw(action_cx.window);
4508                        true
4509                    }
4510                    KeyCode::End => {
4511                        active_index_for_keys.set(Some(len));
4512                        anchor_index_for_keys.set(Some(entries_for_keys[len].key));
4513                        cancel_typeahead_timer(host, &typeahead_timer_for_keys);
4514                        typeahead_for_keys.borrow_mut().clear();
4515                        vertical_scroll_for_keys.scroll_to_item(len, ScrollStrategy::Nearest);
4516                        host.request_redraw(action_cx.window);
4517                        true
4518                    }
4519                    KeyCode::Escape => {
4520                        cancel_typeahead_timer(host, &typeahead_timer_for_keys);
4521                        typeahead_for_keys.borrow_mut().clear();
4522                        host.request_redraw(action_cx.window);
4523                        true
4524                    }
4525                    KeyCode::Enter | KeyCode::NumpadEnter | KeyCode::Space => {
4526                        if !props.enable_row_selection {
4527                            return false;
4528                        }
4529                        let row_key = entries_for_keys[current].key;
4530                        let _ = host.models_mut().update(&state_for_keys, move |st| {
4531                            let selected = st.row_selection.contains(&row_key);
4532                            if props.single_row_selection {
4533                                st.row_selection.clear();
4534                            }
4535                            if selected {
4536                                st.row_selection.remove(&row_key);
4537                            } else {
4538                                st.row_selection.insert(row_key);
4539                            }
4540                        });
4541                        anchor_index_for_keys.set(Some(row_key));
4542                        cancel_typeahead_timer(host, &typeahead_timer_for_keys);
4543                        typeahead_for_keys.borrow_mut().clear();
4544                        host.request_redraw(action_cx.window);
4545                        true
4546                    }
4547                    _ => {
4548                        if down.repeat {
4549                            return false;
4550                        }
4551                        let Some(input) = fret_core::keycode_to_ascii_lowercase(down.key) else {
4552                            return false;
4553                        };
4554
4555                        typeahead_for_keys.borrow_mut().push_char(input, 0);
4556                        let typeahead_buf = typeahead_for_keys.borrow();
4557                        let Some(query) = typeahead_buf.query(0) else {
4558                            return false;
4559                        };
4560
4561                        let labels = labels_for_keys.borrow().clone();
4562                        let disabled = disabled_for_keys.borrow().clone();
4563                        let next =
4564                            match_prefix_arc_str(&labels, &disabled, query, Some(current), true);
4565                        if let Some(next) = next
4566                            && next != current
4567                        {
4568                            active_index_for_keys.set(Some(next));
4569                            anchor_index_for_keys.set(Some(entries_for_keys[next].key));
4570                            // Typeahead should ensure the matched row becomes *visibly* in-view,
4571                            // not just "present in overscan".
4572                            vertical_scroll_for_keys.scroll_to_item(next, ScrollStrategy::Start);
4573                        }
4574
4575                        cancel_typeahead_timer(host, &typeahead_timer_for_keys);
4576                        let token = host.next_timer_token();
4577                        typeahead_timer_for_keys.set(Some(token));
4578                        host.push_effect(Effect::SetTimer {
4579                            window: Some(action_cx.window),
4580                            token,
4581                            after: TABLE_TYPEAHEAD_TIMEOUT,
4582                            repeat: None,
4583                        });
4584
4585                        host.request_redraw(action_cx.window);
4586                        true
4587                    }
4588                }
4589            });
4590
4591            cx.key_on_key_down_for(list_id, key_handler.clone());
4592            let row = row_builder(key_handler, list_id);
4593
4594            {
4595                let typeahead = typeahead.clone();
4596                let typeahead_timer = typeahead_timer.clone();
4597                cx.timer_on_timer_for(
4598                    list_id,
4599                    Arc::new(move |host, action_cx, token| {
4600                        if typeahead_timer.get() != Some(token) {
4601                            return false;
4602                        }
4603                        typeahead_timer.set(None);
4604                        typeahead.borrow_mut().clear();
4605                        host.request_redraw(action_cx.window);
4606                        true
4607                    }),
4608                );
4609            }
4610
4611            let content =
4612                cx.pointer_region(fret_ui::element::PointerRegionProps::default(), move |cx| {
4613                    let focus_id = list_id;
4614                    cx.pointer_region_on_pointer_up(Arc::new(move |host, action_cx, up| {
4615                        if up.button != fret_core::MouseButton::Left || !up.is_click {
4616                            return false;
4617                        }
4618                        if up.down_hit_pressable_target.is_some() {
4619                            return false;
4620                        }
4621                        host.request_focus(focus_id);
4622                        host.request_redraw(action_cx.window);
4623                        false
4624                    }));
4625
4626                    vec![
4627                        ui::v_flex(move |cx| {
4628                            [
4629                                header,
4630                                cx.virtual_list_keyed_retained_with_layout(
4631                                    fill_layout,
4632                                    entries_for_list.len(),
4633                                    options,
4634                                    vertical_scroll,
4635                                    key_at,
4636                                    row,
4637                                ),
4638                            ]
4639                        })
4640                        .layout(LayoutRefinement::default().w_full().h_full())
4641                        .gap(Space::N0)
4642                        .justify_start()
4643                        .items_stretch()
4644                        .into_element(cx),
4645                    ]
4646                });
4647
4648            let focus_ring = RingStyle {
4649                placement: RingPlacement::Inset,
4650                width: Px(2.0),
4651                offset: Px(0.0),
4652                color: ring,
4653                offset_color: None,
4654                corner_radii: Corners::all(radius),
4655            };
4656
4657            vec![cx.container(
4658                ContainerProps {
4659                    layout: fill_layout,
4660                    background: Some(table_bg),
4661                    border: if props.draw_frame {
4662                        Edges::all(Px(1.0))
4663                    } else {
4664                        Edges::all(Px(0.0))
4665                    },
4666                    border_color: if props.draw_frame { Some(border) } else { None },
4667                    focus_border_color: if props.draw_frame { Some(ring) } else { None },
4668                    focus_ring: Some(focus_ring),
4669                    focus_within: true,
4670                    corner_radii: if props.draw_frame {
4671                        Corners::all(radius)
4672                    } else {
4673                        Corners::all(Px(0.0))
4674                    },
4675                    ..Default::default()
4676                },
4677                move |_cx| vec![content],
4678            )]
4679        },
4680    );
4681
4682    if let Some(active_element) = active_element.get() {
4683        list.attach_semantics(
4684            SemanticsDecoration::default().active_descendant_element(active_element.0),
4685        )
4686    } else {
4687        list
4688    }
4689}
4690
4691#[allow(clippy::too_many_arguments)]
4692#[track_caller]
4693fn table_virtualized_impl<H: UiHost, TData, IHeader, TH, ICell, TC>(
4694    cx: &mut ElementContext<'_, H>,
4695    data: &[TData],
4696    columns: &[ColumnDef<TData>],
4697    state: Model<TableState>,
4698    vertical_scroll: &VirtualListScrollHandle,
4699    items_revision: u64,
4700    row_key_at: &RowKeyAt<TData>,
4701    typeahead_label_at: Option<Arc<TypeaheadLabelAt<TData>>>,
4702    props: TableViewProps,
4703    copy_text_at: Option<Arc<CopyTextAtFn>>,
4704    on_row_activate: impl Fn(&Row<'_, TData>) -> Option<CommandId>,
4705    mut render_header_cell: impl FnMut(
4706        &mut ElementContext<'_, H>,
4707        &ColumnDef<TData>,
4708        Option<bool>,
4709    ) -> IHeader,
4710    mut render_cell: impl FnMut(&mut ElementContext<'_, H>, &Row<'_, TData>, &ColumnDef<TData>) -> ICell,
4711    output: Option<Model<TableViewOutput>>,
4712    debug_ids: TableDebugIds,
4713) -> AnyElement
4714where
4715    IHeader: IntoIterator<Item = TH>,
4716    TH: IntoUiElement<H>,
4717    ICell: IntoIterator<Item = TC>,
4718    TC: IntoUiElement<H>,
4719{
4720    let profile = std::env::var_os("FRET_TABLE_PROFILE").is_some();
4721    let TableDebugIds {
4722        header_row_test_id: debug_header_row_test_id,
4723        header_cell_test_id_prefix: debug_header_cell_test_id_prefix,
4724        row_test_id_prefix: debug_row_test_id_prefix,
4725    } = debug_ids;
4726    let state_value = cx.watch_model(&state).layout().cloned_or_default();
4727
4728    let theme = Theme::global(&*cx.app);
4729    let (table_bg, border, header_bg, row_hover, row_active) = resolve_table_colors(theme);
4730    let resize_grip = emphasize_border(border, 0.35);
4731    let resize_preview = emphasize_border(border, 0.75);
4732    let ring = theme
4733        .color_by_key("ring")
4734        .or_else(|| theme.color_by_key("focus.ring"))
4735        .or_else(|| theme.color_by_key("primary"))
4736        .unwrap_or(row_active);
4737    let ring = emphasize_border(ring, 0.9);
4738    let row_hover_bg = Color {
4739        a: row_hover.a.min(0.12),
4740        ..row_hover
4741    };
4742    let row_active_bg = Color {
4743        a: row_active.a.min(0.18),
4744        ..row_active
4745    };
4746    let radius = theme.metric_token("metric.radius.md");
4747
4748    let row_h = props
4749        .row_height
4750        .unwrap_or_else(|| resolve_row_height(theme, props.size));
4751    let header_h = props.header_height.unwrap_or(row_h);
4752    let body_row_height = match props.row_measure_mode {
4753        TableRowMeasureMode::Fixed => Length::Px(row_h),
4754        TableRowMeasureMode::Measured => Length::Auto,
4755    };
4756    let cell_px = resolve_cell_padding_x(theme);
4757    let cell_py = resolve_cell_padding_y(theme);
4758
4759    let scroll_x = cx.slot_state(ScrollHandle::default, |h| h.clone());
4760
4761    let grouping = if props.enable_column_grouping {
4762        state_value.grouping.as_slice()
4763    } else {
4764        &[]
4765    };
4766
4767    let empty: &[TData] = &[];
4768    let mut sizing_state = state_value.clone();
4769    if !props.enable_column_grouping {
4770        sizing_state.grouping.clear();
4771    }
4772
4773    let sizing_columns: Vec<ColumnDef<TData>> = columns
4774        .iter()
4775        .cloned()
4776        .map(|c| with_table_view_column_constraints(c, &props))
4777        .collect();
4778    let sizing_options = TableOptions {
4779        grouped_column_mode: props.grouped_column_mode,
4780        ..Default::default()
4781    };
4782
4783    let sizing_table = Table::builder(empty)
4784        .columns(sizing_columns)
4785        .state(sizing_state)
4786        .options(sizing_options)
4787        .build();
4788    let core_snapshot = sizing_table.core_model_snapshot();
4789
4790    let mut column_width_by_id: std::collections::HashMap<ColumnId, Px> =
4791        std::collections::HashMap::new();
4792    for col in columns {
4793        let w = core_snapshot
4794            .leaf_column_sizing
4795            .size
4796            .get(&col.id)
4797            .copied()
4798            .unwrap_or(col.size);
4799        column_width_by_id.insert(col.id.clone(), Px(w));
4800    }
4801    let column_width_by_id: Arc<std::collections::HashMap<ColumnId, Px>> =
4802        Arc::new(column_width_by_id);
4803
4804    let col_by_id_for_layout: std::collections::HashMap<&str, &ColumnDef<TData>> =
4805        columns.iter().map(|c| (c.id.as_ref(), c)).collect();
4806
4807    let visible_columns: Vec<&ColumnDef<TData>> = core_snapshot
4808        .leaf_columns
4809        .visible
4810        .iter()
4811        .filter_map(|id| col_by_id_for_layout.get(id.as_ref()).copied())
4812        .collect();
4813    let left_cols: Vec<&ColumnDef<TData>> = core_snapshot
4814        .leaf_columns
4815        .left_visible
4816        .iter()
4817        .filter_map(|id| col_by_id_for_layout.get(id.as_ref()).copied())
4818        .collect();
4819    let center_cols: Vec<&ColumnDef<TData>> = core_snapshot
4820        .leaf_columns
4821        .center_visible
4822        .iter()
4823        .filter_map(|id| col_by_id_for_layout.get(id.as_ref()).copied())
4824        .collect();
4825    let right_cols: Vec<&ColumnDef<TData>> = core_snapshot
4826        .leaf_columns
4827        .right_visible
4828        .iter()
4829        .filter_map(|id| col_by_id_for_layout.get(id.as_ref()).copied())
4830        .collect();
4831
4832    let sorting_key = if grouping.is_empty() {
4833        state_value.sorting.clone()
4834    } else {
4835        Vec::new()
4836    };
4837
4838    let has_row_pinning =
4839        grouping.is_empty() && is_some_rows_pinned(&state_value.row_pinning, None);
4840    let row_order = if grouping.is_empty() && !has_row_pinning {
4841        Some(cx.slot_state(FlatRowOrderCache::default, |cache| {
4842            let deps = FlatRowOrderDeps {
4843                items_revision,
4844                data_len: data.len(),
4845                sorting: sorting_key.clone(),
4846                column_filters: state_value.column_filters.clone(),
4847                global_filter: state_value.global_filter.clone(),
4848            };
4849
4850            let started = Instant::now();
4851            let (order, recomputed) = cache.row_order(data, columns, deps);
4852            let elapsed = started.elapsed();
4853
4854            if profile && recomputed {
4855                tracing::info!(
4856                    "table_virtualized: recompute row_order len={} sorting={} took {:.2}ms",
4857                    data.len(),
4858                    sorting_key.len(),
4859                    elapsed.as_secs_f64() * 1000.0
4860                );
4861            }
4862
4863            order.clone()
4864        }))
4865    } else {
4866        None
4867    };
4868
4869    let page_display_rows: Vec<DisplayRow> = if grouping.is_empty() {
4870        if has_row_pinning {
4871            let options = crate::headless::table::TableOptions {
4872                keep_pinned_rows: props.keep_pinned_rows,
4873                ..Default::default()
4874            };
4875
4876            let table_pre = Table::builder(data)
4877                .columns(columns.to_vec())
4878                .global_filter_fn(FilteringFnSpec::Auto)
4879                .get_row_key(|row, idx, _parent| row_key_at(row, idx))
4880                .state(state_value.clone())
4881                .options(options)
4882                .build();
4883
4884            let total_rows = table_pre.pre_pagination_row_model().root_rows().len();
4885            let bounds = pagination_bounds(total_rows, state_value.pagination);
4886            if bounds.page_index != state_value.pagination.page_index {
4887                let _ = cx.app.models_mut().update(&state, |st| {
4888                    st.pagination.page_index = bounds.page_index;
4889                });
4890            }
4891            if let Some(out) = output.clone() {
4892                let next = TableViewOutput {
4893                    filtered_row_count: total_rows,
4894                    pagination: bounds,
4895                };
4896                let _ = cx.app.models_mut().update(&out, |v| {
4897                    if *v != next {
4898                        *v = next;
4899                    }
4900                });
4901            }
4902
4903            let mut render_state = state_value.clone();
4904            render_state.pagination.page_index = bounds.page_index;
4905            let table = Table::builder(data)
4906                .columns(columns.to_vec())
4907                .global_filter_fn(FilteringFnSpec::Auto)
4908                .get_row_key(|row, idx, _parent| row_key_at(row, idx))
4909                .state(render_state)
4910                .options(options)
4911                .build();
4912
4913            let core = table.core_row_model();
4914            table
4915                .top_row_keys()
4916                .into_iter()
4917                .chain(table.center_row_keys())
4918                .chain(table.bottom_row_keys())
4919                .filter_map(|row_key| {
4920                    let row_index = core.row_by_key(row_key)?;
4921                    let row = core.row(row_index)?;
4922                    if row.parent.is_some() {
4923                        return None;
4924                    }
4925                    let data_index = row.index;
4926                    if data_index >= data.len() {
4927                        return None;
4928                    }
4929                    Some(DisplayRow::Leaf {
4930                        data_index,
4931                        row_key,
4932                        depth: row.depth as usize,
4933                    })
4934                })
4935                .collect()
4936        } else {
4937            let row_order = row_order.expect("row_order");
4938            let total_rows = row_order.len();
4939            let bounds = pagination_bounds(total_rows, state_value.pagination);
4940            if bounds.page_index != state_value.pagination.page_index {
4941                let _ = cx.app.models_mut().update(&state, |st| {
4942                    st.pagination.page_index = bounds.page_index;
4943                });
4944            }
4945            if let Some(out) = output.clone() {
4946                let next = TableViewOutput {
4947                    filtered_row_count: total_rows,
4948                    pagination: bounds,
4949                };
4950                let _ = cx.app.models_mut().update(&out, |v| {
4951                    if *v != next {
4952                        *v = next;
4953                    }
4954                });
4955            }
4956
4957            let page_rows: &[usize] = if bounds.page_count == 0 {
4958                &[]
4959            } else {
4960                row_order
4961                    .get(bounds.page_start..bounds.page_end)
4962                    .unwrap_or_default()
4963            };
4964            page_rows
4965                .iter()
4966                .map(|&data_index| DisplayRow::Leaf {
4967                    data_index,
4968                    row_key: row_key_at(&data[data_index], data_index),
4969                    depth: 0,
4970                })
4971                .collect()
4972        }
4973    } else {
4974        let deps = GroupedDisplayDeps {
4975            base: GroupedBaseDeps {
4976                items_revision,
4977                data_len: data.len(),
4978                columns_fingerprint: columns_fingerprint(columns),
4979                grouping: grouping.to_vec(),
4980                column_filters: state_value.column_filters.clone(),
4981                global_filter: state_value.global_filter.clone(),
4982            },
4983            sorting: state_value.sorting.clone(),
4984            expanding: state_value.expanding.clone(),
4985            row_pinning: state_value.row_pinning.clone(),
4986            grouped_row_pinning_policy: props.grouped_row_pinning_policy,
4987            page_index: state_value.pagination.page_index,
4988            page_size: state_value.pagination.page_size,
4989        };
4990
4991        let (page_rows, view_output, clamp_to_page): (
4992            Vec<DisplayRow>,
4993            TableViewOutput,
4994            Option<usize>,
4995        ) = cx.slot_state(GroupedDisplayCache::default, |cache| {
4996            if cache.deps.as_ref() == Some(&deps) {
4997                return (cache.page_rows.clone(), cache.output.clone(), None);
4998            }
4999
5000            if cache.base_deps.as_ref() == Some(&deps.base) {
5001                let grouped = &cache.grouped;
5002                let row_index_by_key = &cache.row_index_by_key;
5003                let group_labels = &cache.group_labels;
5004                let group_aggs_text = &cache.group_aggs_text;
5005                let group_aggs_u64 = &cache.group_aggs_u64;
5006                let group_aggs_any = &cache.group_aggs_any;
5007
5008                let mut visible: Vec<DisplayRow> = Vec::new();
5009                let mut roots: Vec<crate::headless::table::GroupedRowIndex> =
5010                    grouped.root_rows().to_vec();
5011                sort_grouped_row_indices_in_place(
5012                    grouped,
5013                    &mut roots,
5014                    deps.sorting.as_slice(),
5015                    columns,
5016                    data,
5017                    row_index_by_key,
5018                    group_aggs_u64,
5019                    group_aggs_any,
5020                );
5021
5022                for root in roots {
5023                    push_visible(
5024                        grouped,
5025                        root,
5026                        row_index_by_key,
5027                        group_labels,
5028                        group_aggs_text,
5029                        group_aggs_u64,
5030                        group_aggs_any,
5031                        deps.sorting.as_slice(),
5032                        columns,
5033                        data,
5034                        &deps.expanding,
5035                        &mut visible,
5036                    );
5037                }
5038
5039                let total_rows = visible.len();
5040                let bounds = pagination_bounds(
5041                    total_rows,
5042                    PaginationState {
5043                        page_index: deps.page_index,
5044                        page_size: deps.page_size,
5045                    },
5046                );
5047                cache.output = TableViewOutput {
5048                    filtered_row_count: total_rows,
5049                    pagination: bounds,
5050                };
5051
5052                let page_rows_center: Vec<DisplayRow> = if bounds.page_count == 0 {
5053                    Vec::new()
5054                } else {
5055                    visible
5056                        .get(bounds.page_start..bounds.page_end)
5057                        .unwrap_or_default()
5058                        .to_vec()
5059                };
5060
5061                let page_rows = apply_grouped_row_pinning_policy(
5062                    &visible,
5063                    &page_rows_center,
5064                    &deps.row_pinning,
5065                    deps.grouped_row_pinning_policy,
5066                );
5067
5068                cache.deps = Some(deps.clone());
5069                cache.page_rows = page_rows.clone();
5070                return (
5071                    page_rows,
5072                    cache.output.clone(),
5073                    (bounds.page_index != deps.page_index).then_some(bounds.page_index),
5074                );
5075            }
5076
5077            let mut row_index_by_key: std::collections::HashMap<RowKey, usize> =
5078                std::collections::HashMap::with_capacity(data.len());
5079            for (i, item) in data.iter().enumerate() {
5080                let key = row_key_at(item, i);
5081                row_index_by_key.entry(key).or_insert(i);
5082            }
5083
5084            let col_by_id: std::collections::HashMap<&str, &ColumnDef<TData>> =
5085                columns.iter().map(|c| (c.id.as_ref(), c)).collect();
5086
5087            let agg_columns: Vec<&ColumnDef<TData>> = columns
5088                .iter()
5089                .filter(|c| c.aggregation != Aggregation::None)
5090                .collect();
5091
5092            let options = crate::headless::table::TableOptions {
5093                manual_sorting: true,
5094                manual_pagination: true,
5095                manual_expanding: true,
5096                keep_pinned_rows: props.keep_pinned_rows,
5097                ..Default::default()
5098            };
5099
5100            let mut state_for_grouping = state_value.clone();
5101            state_for_grouping.sorting.clear();
5102            state_for_grouping.pagination = Default::default();
5103
5104            let table = Table::builder(data)
5105                .columns(columns.to_vec())
5106                .state(state_for_grouping)
5107                .options(options)
5108                .get_row_key(|row, index, _parent| row_key_at(row, index))
5109                .build();
5110
5111            let grouped = table.grouped_row_model().clone();
5112            fn compute_group_aggregations<TData>(
5113                model: &crate::headless::table::GroupedRowModel,
5114                data: &[TData],
5115                row_index_by_key: &std::collections::HashMap<RowKey, usize>,
5116                agg_columns: &[&ColumnDef<TData>],
5117            ) -> (GroupAggsU64, GroupAggsText) {
5118                if agg_columns.is_empty() {
5119                    return (Default::default(), Default::default());
5120                }
5121                let out_u64 =
5122                    compute_grouped_u64_aggregations(model, data, row_index_by_key, agg_columns);
5123
5124                let mut out_text: GroupAggsText = Default::default();
5125                for (&row_key, entries) in &out_u64 {
5126                    let mut text_values: Vec<(ColumnId, Arc<str>)> =
5127                        Vec::with_capacity(entries.len());
5128                    for (col_id, v) in entries.iter() {
5129                        text_values.push((col_id.clone(), Arc::from(v.to_string())));
5130                    }
5131                    out_text.insert(row_key, Arc::from(text_values.into_boxed_slice()));
5132                }
5133
5134                (out_u64, out_text)
5135            }
5136
5137            fn group_label_for_key<TData>(
5138                kind: &GroupedRowKind,
5139                data: &[TData],
5140                row_index_by_key: &std::collections::HashMap<RowKey, usize>,
5141                col_by_id: &std::collections::HashMap<&str, &ColumnDef<TData>>,
5142            ) -> Arc<str> {
5143                let GroupedRowKind::Group {
5144                    grouping_column,
5145                    grouping_value,
5146                    first_leaf_row_key,
5147                    leaf_row_count,
5148                } = kind
5149                else {
5150                    return Arc::from("");
5151                };
5152
5153                let mut value: Arc<str> = Arc::from(format!("{:x}", grouping_value));
5154                if let Some(column) = col_by_id.get(grouping_column.as_ref()).copied()
5155                    && let Some(f) = column.facet_str_fn.as_ref()
5156                    && let Some(&i) = row_index_by_key.get(first_leaf_row_key)
5157                {
5158                    value = Arc::from(f(&data[i]));
5159                }
5160
5161                Arc::from(format!("{value} ({leaf_row_count})"))
5162            }
5163
5164            fn push_visible<TData>(
5165                model: &crate::headless::table::GroupedRowModel,
5166                index: crate::headless::table::GroupedRowIndex,
5167                row_index_by_key: &std::collections::HashMap<RowKey, usize>,
5168                group_labels: &std::collections::HashMap<RowKey, Arc<str>>,
5169                group_aggs_text: &GroupAggsText,
5170                group_aggs_u64: &GroupAggsU64,
5171                group_aggs_any: &GroupAggsAny,
5172                sorting: &[SortSpec],
5173                columns: &[ColumnDef<TData>],
5174                data: &[TData],
5175                expanded: &ExpandingState,
5176                out: &mut Vec<DisplayRow>,
5177            ) {
5178                let Some(row) = model.row(index) else {
5179                    return;
5180                };
5181
5182                match &row.kind {
5183                    GroupedRowKind::Group {
5184                        grouping_column, ..
5185                    } => {
5186                        let expanded_here = is_row_expanded(row.key, expanded);
5187                        let grouping_column = grouping_column.clone();
5188                        let aggregations = group_aggs_text
5189                            .get(&row.key)
5190                            .cloned()
5191                            .unwrap_or_else(|| Arc::from([]));
5192                        out.push(DisplayRow::Group {
5193                            grouping_column,
5194                            row_key: row.key,
5195                            depth: row.depth,
5196                            label: group_labels
5197                                .get(&row.key)
5198                                .cloned()
5199                                .unwrap_or_else(|| Arc::from("")),
5200                            expanded: expanded_here,
5201                            aggregations,
5202                        });
5203
5204                        if expanded_here {
5205                            let mut children: Option<Vec<crate::headless::table::GroupedRowIndex>> =
5206                                None;
5207
5208                            if !sorting.is_empty() {
5209                                let mut owned = row.sub_rows.clone();
5210                                sort_grouped_row_indices_in_place(
5211                                    model,
5212                                    &mut owned,
5213                                    sorting,
5214                                    columns,
5215                                    data,
5216                                    row_index_by_key,
5217                                    group_aggs_u64,
5218                                    group_aggs_any,
5219                                );
5220                                children = Some(owned);
5221                            }
5222
5223                            let child_iter: Box<
5224                                dyn Iterator<Item = crate::headless::table::GroupedRowIndex>,
5225                            > = if let Some(children) = children {
5226                                Box::new(children.into_iter())
5227                            } else {
5228                                Box::new(row.sub_rows.iter().copied())
5229                            };
5230
5231                            for child in child_iter {
5232                                push_visible(
5233                                    model,
5234                                    child,
5235                                    row_index_by_key,
5236                                    group_labels,
5237                                    group_aggs_text,
5238                                    group_aggs_u64,
5239                                    group_aggs_any,
5240                                    sorting,
5241                                    columns,
5242                                    data,
5243                                    expanded,
5244                                    out,
5245                                );
5246                            }
5247                        }
5248                    }
5249                    GroupedRowKind::Leaf { row_key } => {
5250                        let Some(&data_index) = row_index_by_key.get(row_key) else {
5251                            return;
5252                        };
5253                        out.push(DisplayRow::Leaf {
5254                            data_index,
5255                            row_key: *row_key,
5256                            depth: row.depth,
5257                        });
5258                    }
5259                }
5260            }
5261
5262            let (group_aggs_u64, group_aggs_text) =
5263                compute_group_aggregations(&grouped, data, &row_index_by_key, &agg_columns);
5264            let group_aggs_any = table.grouped_aggregations_any().clone();
5265
5266            let mut group_labels: std::collections::HashMap<RowKey, Arc<str>> = Default::default();
5267            for &node in grouped.flat_rows() {
5268                let Some(row) = grouped.row(node) else {
5269                    continue;
5270                };
5271                if matches!(row.kind, GroupedRowKind::Group { .. }) {
5272                    group_labels.insert(
5273                        row.key,
5274                        group_label_for_key(&row.kind, data, &row_index_by_key, &col_by_id),
5275                    );
5276                }
5277            }
5278
5279            let mut visible: Vec<DisplayRow> = Vec::new();
5280            let mut roots: Vec<crate::headless::table::GroupedRowIndex> =
5281                grouped.root_rows().to_vec();
5282            sort_grouped_row_indices_in_place(
5283                &grouped,
5284                &mut roots,
5285                deps.sorting.as_slice(),
5286                columns,
5287                data,
5288                &row_index_by_key,
5289                &group_aggs_u64,
5290                &group_aggs_any,
5291            );
5292
5293            for root in roots {
5294                push_visible(
5295                    &grouped,
5296                    root,
5297                    &row_index_by_key,
5298                    &group_labels,
5299                    &group_aggs_text,
5300                    &group_aggs_u64,
5301                    &group_aggs_any,
5302                    deps.sorting.as_slice(),
5303                    columns,
5304                    data,
5305                    &state_value.expanding,
5306                    &mut visible,
5307                );
5308            }
5309
5310            let total_rows = visible.len();
5311            let bounds = pagination_bounds(total_rows, state_value.pagination);
5312
5313            cache.output = TableViewOutput {
5314                filtered_row_count: total_rows,
5315                pagination: bounds,
5316            };
5317
5318            let page_rows_center: Vec<DisplayRow> = if bounds.page_count == 0 {
5319                Vec::new()
5320            } else {
5321                visible
5322                    .get(bounds.page_start..bounds.page_end)
5323                    .unwrap_or_default()
5324                    .to_vec()
5325            };
5326
5327            let page_rows = apply_grouped_row_pinning_policy(
5328                &visible,
5329                &page_rows_center,
5330                &deps.row_pinning,
5331                deps.grouped_row_pinning_policy,
5332            );
5333
5334            cache.base_deps = Some(deps.base.clone());
5335            cache.grouped = grouped;
5336            cache.row_index_by_key = row_index_by_key;
5337            cache.group_labels = group_labels;
5338            cache.group_aggs_u64 = group_aggs_u64;
5339            cache.group_aggs_any = group_aggs_any;
5340            cache.group_aggs_text = group_aggs_text;
5341            cache.deps = Some(deps.clone());
5342            cache.page_rows = page_rows.clone();
5343            (
5344                page_rows,
5345                cache.output.clone(),
5346                (bounds.page_index != deps.page_index).then_some(bounds.page_index),
5347            )
5348        });
5349
5350        if let Some(page_index) = clamp_to_page {
5351            let _ = cx.app.models_mut().update(&state, |st| {
5352                st.pagination.page_index = page_index;
5353            });
5354        }
5355        if let Some(out) = output {
5356            let _ = cx.app.models_mut().update(&out, |v| {
5357                if *v != view_output {
5358                    *v = view_output;
5359                }
5360            });
5361        }
5362
5363        page_rows
5364    };
5365
5366    let set_size = page_display_rows.len();
5367
5368    let mut list_options = fret_ui::element::VirtualListOptions::new(row_h, props.overscan);
5369    list_options.items_revision = items_revision;
5370    list_options.keep_alive = props
5371        .keep_alive
5372        .unwrap_or_else(|| props.overscan.saturating_mul(2));
5373    match props.row_measure_mode {
5374        TableRowMeasureMode::Fixed => {
5375            list_options.measure_mode = fret_ui::element::VirtualListMeasureMode::Fixed;
5376            list_options.key_cache = fret_ui::element::VirtualListKeyCacheMode::VisibleOnly;
5377        }
5378        TableRowMeasureMode::Measured => {
5379            list_options.measure_mode = fret_ui::element::VirtualListMeasureMode::Measured;
5380            list_options.key_cache = fret_ui::element::VirtualListKeyCacheMode::AllKeys;
5381        }
5382    }
5383
5384    let rendered_rows = Cell::new(0usize);
5385
5386    let (
5387        active_index,
5388        anchor_index,
5389        row_meta,
5390        active_element,
5391        active_command,
5392        typeahead,
5393        typeahead_timer,
5394    ) = cx.slot_state(TableKeyboardNavState::default, |st| {
5395        (
5396            st.active_index.clone(),
5397            st.anchor_index.clone(),
5398            st.row_meta.clone(),
5399            st.active_element.clone(),
5400            st.active_command.clone(),
5401            st.typeahead.clone(),
5402            st.typeahead_timer.clone(),
5403        )
5404    });
5405
5406    {
5407        let typeahead_label_at = typeahead_label_at.clone();
5408        let typeahead_facet_str_fn = visible_columns.iter().find_map(|c| c.facet_str_fn.clone());
5409        let next_meta: Arc<[TableNavRowMeta]> = page_display_rows
5410            .iter()
5411            .map(|row| match row {
5412                DisplayRow::Leaf {
5413                    data_index,
5414                    row_key,
5415                    ..
5416                } => {
5417                    let label = typeahead_label_at
5418                        .as_ref()
5419                        .map(|f| f(&data[*data_index], *data_index))
5420                        .or_else(|| {
5421                            typeahead_facet_str_fn
5422                                .as_ref()
5423                                .map(|f| Arc::<str>::from(f(&data[*data_index]).to_string()))
5424                        })
5425                        .unwrap_or_else(|| Arc::from(""));
5426                    TableNavRowMeta {
5427                        row_key: *row_key,
5428                        kind: TableNavRowKind::Leaf,
5429                        data_index: Some(*data_index),
5430                        label,
5431                    }
5432                }
5433                DisplayRow::Group { row_key, label, .. } => TableNavRowMeta {
5434                    row_key: *row_key,
5435                    kind: TableNavRowKind::Group,
5436                    data_index: None,
5437                    label: label.clone(),
5438                },
5439            })
5440            .collect::<Vec<_>>()
5441            .into();
5442        *row_meta.borrow_mut() = next_meta;
5443    }
5444
5445    if set_size == 0 {
5446        if active_index.get().is_some() {
5447            active_index.set(None);
5448        }
5449    } else {
5450        let next = Some(
5451            active_index
5452                .get()
5453                .unwrap_or(0)
5454                .min(set_size.saturating_sub(1)),
5455        );
5456        if active_index.get() != next {
5457            active_index.set(next);
5458        }
5459    }
5460
5461    let key_handler: fret_ui::action::OnKeyDown = {
5462        let active_index = active_index.clone();
5463        let anchor_index = anchor_index.clone();
5464        let row_meta = row_meta.clone();
5465        let active_command = active_command.clone();
5466        let vertical_scroll = vertical_scroll.clone();
5467        let state = state.clone();
5468        let enable_row_selection = props.enable_row_selection;
5469        let single_row_selection = props.single_row_selection;
5470        let typeahead = typeahead.clone();
5471        let typeahead_timer = typeahead_timer.clone();
5472
5473        Arc::new(move |host, action_cx, down| {
5474            let meta = row_meta.borrow().clone();
5475            let len = meta.len();
5476            if len == 0 {
5477                if active_index.get().is_some() {
5478                    active_index.set(None);
5479                    host.request_redraw(action_cx.window);
5480                }
5481                return false;
5482            }
5483
5484            let current = active_index.get().unwrap_or(0).min(len.saturating_sub(1));
5485            let has_disallowed_mods = down.modifiers.alt || down.modifiers.alt_gr;
5486            if has_disallowed_mods {
5487                return false;
5488            }
5489            let primary = (down.modifiers.ctrl || down.modifiers.meta) && !down.modifiers.alt_gr;
5490
5491            let cancel_typeahead_timer = |host: &mut dyn fret_ui::action::UiFocusActionHost| {
5492                if let Some(token) = typeahead_timer.get() {
5493                    host.push_effect(Effect::CancelTimer { token });
5494                    typeahead_timer.set(None);
5495                }
5496            };
5497            let clear_typeahead = |host: &mut dyn fret_ui::action::UiFocusActionHost| {
5498                typeahead.borrow_mut().clear();
5499                cancel_typeahead_timer(host);
5500            };
5501
5502            match down.key {
5503                KeyCode::KeyA if primary && enable_row_selection && !single_row_selection => {
5504                    let leaf_keys = table_collect_leaf_keys(&meta);
5505                    if leaf_keys.is_empty() {
5506                        return false;
5507                    }
5508
5509                    let all_selected = host
5510                        .models_mut()
5511                        .read(&state, |st| {
5512                            leaf_keys.iter().all(|k| st.row_selection.contains(k))
5513                        })
5514                        .ok()
5515                        .unwrap_or(false);
5516
5517                    let _ = host.models_mut().update(&state, move |st| {
5518                        if down.modifiers.shift {
5519                            st.row_selection.clear();
5520                            return;
5521                        }
5522
5523                        if all_selected {
5524                            for k in &leaf_keys {
5525                                st.row_selection.remove(k);
5526                            }
5527                        } else {
5528                            for k in &leaf_keys {
5529                                st.row_selection.insert(*k);
5530                            }
5531                        }
5532                    });
5533                    clear_typeahead(host);
5534                    host.request_redraw(action_cx.window);
5535                    true
5536                }
5537                KeyCode::ArrowDown
5538                | KeyCode::ArrowUp
5539                | KeyCode::Home
5540                | KeyCode::End
5541                | KeyCode::PageDown
5542                | KeyCode::PageUp => {
5543                    if primary {
5544                        return false;
5545                    }
5546
5547                    let page = 10usize;
5548                    let next = match down.key {
5549                        KeyCode::ArrowDown => (current + 1).min(len.saturating_sub(1)),
5550                        KeyCode::ArrowUp => current.saturating_sub(1),
5551                        KeyCode::Home => 0,
5552                        KeyCode::End => len.saturating_sub(1),
5553                        KeyCode::PageDown => (current + page).min(len.saturating_sub(1)),
5554                        KeyCode::PageUp => current.saturating_sub(page),
5555                        _ => current,
5556                    };
5557
5558                    if next != current {
5559                        active_index.set(Some(next));
5560                        *active_command.borrow_mut() = None;
5561                        vertical_scroll.scroll_to_item(next, ScrollStrategy::Nearest);
5562
5563                        if down.modifiers.shift {
5564                            if enable_row_selection
5565                                && let Some(m) = meta.get(next)
5566                                && m.kind == TableNavRowKind::Leaf
5567                            {
5568                                let anchor_key =
5569                                    anchor_index.get().unwrap_or(meta[current].row_key);
5570                                if anchor_index.get().is_none() {
5571                                    anchor_index.set(Some(anchor_key));
5572                                }
5573                                let anchor = meta
5574                                    .iter()
5575                                    .position(|m| m.row_key == anchor_key)
5576                                    .unwrap_or(current);
5577                                let (a, b) = if anchor <= next {
5578                                    (anchor, next)
5579                                } else {
5580                                    (next, anchor)
5581                                };
5582
5583                                let leaf_range: Vec<RowKey> = if single_row_selection {
5584                                    vec![m.row_key]
5585                                } else {
5586                                    table_collect_leaf_keys_in_range(&meta, a, b)
5587                                };
5588
5589                                if !leaf_range.is_empty() {
5590                                    let _ = host.models_mut().update(&state, move |st| {
5591                                        st.row_selection.clear();
5592                                        st.row_selection.extend(leaf_range.iter().copied());
5593                                    });
5594                                }
5595                            }
5596                        } else {
5597                            anchor_index.set(Some(meta[next].row_key));
5598                        }
5599
5600                        clear_typeahead(host);
5601                        host.request_redraw(action_cx.window);
5602                    }
5603                    true
5604                }
5605                KeyCode::Enter | KeyCode::NumpadEnter | KeyCode::Space => {
5606                    if primary {
5607                        return false;
5608                    }
5609
5610                    let Some(meta) = meta.get(current).cloned() else {
5611                        return false;
5612                    };
5613
5614                    if let Some(cmd) = active_command.borrow().clone() {
5615                        host.dispatch_command(Some(action_cx.window), cmd);
5616                    }
5617
5618                    match meta.kind {
5619                        TableNavRowKind::Group => {
5620                            let row_key = meta.row_key;
5621                            let _ = host.models_mut().update(&state, move |st| {
5622                                match &mut st.expanding {
5623                                    ExpandingState::All => {
5624                                        st.expanding = ExpandingState::default();
5625                                    }
5626                                    ExpandingState::Keys(keys) => {
5627                                        if keys.contains(&row_key) {
5628                                            keys.remove(&row_key);
5629                                        } else {
5630                                            keys.insert(row_key);
5631                                        }
5632                                    }
5633                                }
5634                            });
5635                            clear_typeahead(host);
5636                            host.request_redraw(action_cx.window);
5637                            true
5638                        }
5639                        TableNavRowKind::Leaf => {
5640                            if !enable_row_selection {
5641                                return false;
5642                            }
5643
5644                            let row_key = meta.row_key;
5645                            let _ = host.models_mut().update(&state, move |st| {
5646                                let selected = st.row_selection.contains(&row_key);
5647                                if single_row_selection {
5648                                    st.row_selection.clear();
5649                                }
5650                                if selected {
5651                                    st.row_selection.remove(&row_key);
5652                                } else {
5653                                    st.row_selection.insert(row_key);
5654                                }
5655                            });
5656                            anchor_index.set(Some(row_key));
5657                            clear_typeahead(host);
5658                            host.request_redraw(action_cx.window);
5659                            true
5660                        }
5661                    }
5662                }
5663                _ => {
5664                    if primary {
5665                        return false;
5666                    }
5667                    if down.repeat {
5668                        return false;
5669                    }
5670                    let Some(input) = fret_core::keycode_to_ascii_lowercase(down.key) else {
5671                        return false;
5672                    };
5673
5674                    typeahead.borrow_mut().push_char(input, 0);
5675                    let typeahead_buf = typeahead.borrow();
5676                    let Some(query) = typeahead_buf.query(0) else {
5677                        return false;
5678                    };
5679
5680                    let labels: Vec<Arc<str>> = meta.iter().map(|m| m.label.clone()).collect();
5681                    let disabled: Vec<bool> =
5682                        meta.iter().map(|m| m.label.trim().is_empty()).collect();
5683
5684                    let next = match_prefix_arc_str(&labels, &disabled, query, Some(current), true);
5685                    if let Some(next) = next
5686                        && next != current
5687                    {
5688                        active_index.set(Some(next));
5689                        anchor_index.set(Some(meta[next].row_key));
5690                        *active_command.borrow_mut() = None;
5691                        vertical_scroll.scroll_to_item(next, ScrollStrategy::Nearest);
5692                    }
5693
5694                    cancel_typeahead_timer(host);
5695                    let token = host.next_timer_token();
5696                    typeahead_timer.set(Some(token));
5697                    host.push_effect(Effect::SetTimer {
5698                        window: Some(action_cx.window),
5699                        token,
5700                        after: TABLE_TYPEAHEAD_TIMEOUT,
5701                        repeat: None,
5702                    });
5703
5704                    host.request_redraw(action_cx.window);
5705                    true
5706                }
5707            }
5708        })
5709    };
5710
5711    let list = cx.semantics_with_id(
5712        SemanticsProps {
5713            role: SemanticsRole::List,
5714            focusable: true,
5715            ..Default::default()
5716        },
5717        |cx, list_id| {
5718            cx.key_on_key_down_for(list_id, key_handler.clone());
5719            if let Some(copy_text_at) = copy_text_at.clone() {
5720                let copy_text_for_command = copy_text_at.clone();
5721                let state_for_command = state.clone();
5722                let row_meta_for_command = row_meta.clone();
5723                cx.command_on_command_for(
5724                    list_id,
5725                    Arc::new(move |host, acx, command| {
5726                        if command.as_str() != "edit.copy" {
5727                            return false;
5728                        }
5729                        let meta = row_meta_for_command.borrow().clone();
5730                        let selected_keys = host
5731                            .models_mut()
5732                            .read(&state_for_command, |st| st.row_selection.clone())
5733                            .ok()
5734                            .unwrap_or_default();
5735                        let models = host.models_mut();
5736                        let mut lines = Vec::new();
5737                        if !selected_keys.is_empty() {
5738                            for m in meta.iter() {
5739                                if m.kind != TableNavRowKind::Leaf {
5740                                    continue;
5741                                }
5742                                if !selected_keys.contains(&m.row_key) {
5743                                    continue;
5744                                }
5745                                if let Some(data_index) = m.data_index
5746                                    && let Some(text) = (copy_text_for_command)(&*models, data_index)
5747                                {
5748                                    lines.push(text);
5749                                }
5750                            }
5751                        }
5752
5753                        if lines.is_empty() {
5754                            return true;
5755                        }
5756                        let token = host.next_clipboard_token();
5757                        host.push_effect(Effect::ClipboardWriteText {
5758                            window: acx.window,
5759                            token,
5760                            text: lines.join("\n"),
5761                        });
5762                        true
5763                    }),
5764                );
5765
5766                let state_for_availability = state.clone();
5767                let row_meta_for_availability = row_meta.clone();
5768                cx.command_on_command_availability_for(
5769                    list_id,
5770                    Arc::new(move |host, acx, command| {
5771                        if command.as_str() != "edit.copy" {
5772                            return fret_ui::CommandAvailability::NotHandled;
5773                        }
5774                        if !acx.focus_in_subtree {
5775                            return fret_ui::CommandAvailability::NotHandled;
5776                        }
5777                        if !acx.input_ctx.caps.clipboard.text.write {
5778                            return fret_ui::CommandAvailability::Blocked;
5779                        }
5780
5781                        let meta = row_meta_for_availability.borrow().clone();
5782                        let selected_keys = host
5783                            .models_mut()
5784                            .read(&state_for_availability, |st| st.row_selection.clone())
5785                            .ok()
5786                            .unwrap_or_default();
5787                        if !selected_keys.is_empty() {
5788                            for m in meta.iter() {
5789                                if m.kind == TableNavRowKind::Leaf && selected_keys.contains(&m.row_key) {
5790                                    return fret_ui::CommandAvailability::Available;
5791                                }
5792                            }
5793                            return fret_ui::CommandAvailability::Blocked;
5794                        }
5795
5796                        fret_ui::CommandAvailability::Blocked
5797                    }),
5798                );
5799            }
5800            {
5801                let typeahead = typeahead.clone();
5802                let typeahead_timer = typeahead_timer.clone();
5803                cx.timer_on_timer_for(
5804                    list_id,
5805                    Arc::new(move |host, action_cx, token| {
5806                        if typeahead_timer.get() != Some(token) {
5807                            return false;
5808                        }
5809                        typeahead_timer.set(None);
5810                        typeahead.borrow_mut().clear();
5811                        host.request_redraw(action_cx.window);
5812                        true
5813                    }),
5814                );
5815            }
5816            let focus_ring = RingStyle {
5817                placement: RingPlacement::Inset,
5818                width: Px(2.0),
5819                offset: Px(0.0),
5820                color: ring,
5821                offset_color: None,
5822                corner_radii: Corners::all(radius),
5823            };
5824            vec![cx.container(
5825                ContainerProps {
5826                    layout: table_clip_fill_layout(),
5827                    background: Some(table_bg),
5828                    border: if props.draw_frame {
5829                        Edges::all(Px(1.0))
5830                    } else {
5831                        Edges::all(Px(0.0))
5832                    },
5833                    border_color: if props.draw_frame { Some(border) } else { None },
5834                    focus_border_color: if props.draw_frame { Some(ring) } else { None },
5835                    focus_ring: Some(focus_ring),
5836                    focus_within: true,
5837                    corner_radii: if props.draw_frame {
5838                        Corners::all(radius)
5839                    } else {
5840                        Corners::all(Px(0.0))
5841                    },
5842                    ..Default::default()
5843                },
5844                |cx| {
5845                    vec![ui::v_flex(|cx| {
5846                                    let debug_header_row_test_id = debug_header_row_test_id.clone();
5847                                    let debug_header_cell_test_id_prefix =
5848                                        debug_header_cell_test_id_prefix.clone();
5849                                    let header = cx.container(
5850                                        ContainerProps {
5851                                            background: Some(header_bg),
5852                                            border: Edges {
5853                                                bottom: Px(1.0),
5854                                                ..Default::default()
5855                                            },
5856                                            border_color: Some(border),
5857                                            layout: LayoutStyle {
5858                                                size: fret_ui::element::SizeStyle {
5859                                                    width: Length::Fill,
5860                                                    height: Length::Px(header_h),
5861                                                    min_height: Some(Length::Px(header_h)),
5862                                                    max_height: Some(Length::Px(header_h)),
5863                                                    ..Default::default()
5864                                                },
5865                                                flex: fret_ui::element::FlexItemStyle {
5866                                                    shrink: 0.0,
5867                                                    basis: Length::Px(header_h),
5868                                                    ..Default::default()
5869                                                },
5870                                                ..Default::default()
5871                                            },
5872                                                        ..Default::default()
5873                                                    },
5874                                        |cx| {
5875                                            let mut render_header_group =
5876                                                |cx: &mut ElementContext<'_, H>,
5877                                                 cols: &[&ColumnDef<TData>],
5878                                                 scroll_x: Option<ScrollHandle>| {
5879                                                    let column_width_by_id =
5880                                                        column_width_by_id.clone();
5881                                                    let debug_header_cell_test_id_prefix =
5882                                                        debug_header_cell_test_id_prefix.clone();
5883                                                    let row = ui::h_row(|cx| {
5884                                                            let out: Vec<AnyElement> = cols.iter()
5885                                                                .map(|col| {
5886                                                                    let sort_state = sort_for_column(
5887                                                                        &state_value.sorting,
5888                                                                        &col.id,
5889                                                                    );
5890
5891                                                                    let col_w = column_width_by_id
5892                                                                        .get(&col.id)
5893                                                                        .copied()
5894                                                                        .unwrap_or(Px(col.size));
5895
5896                                                                    let cell_props = ContainerProps {
5897                                                                        padding: Edges::all(Px(0.0)).into(),
5898                                                                        border: if props.optimize_grid_lines {
5899                                                                            Edges::default()
5900                                                                        } else {
5901                                                                            Edges {
5902                                                                                right: if props
5903                                                                                    .enable_column_resizing
5904                                                                                    && col.enable_resizing
5905                                                                                {
5906                                                                                    Px(0.0)
5907                                                                                } else {
5908                                                                                    Px(1.0)
5909                                                                                },
5910                                                                                ..Default::default()
5911                                                                            }
5912                                                                        },
5913                                                                        border_color: if props.optimize_grid_lines {
5914                                                                            None
5915                                                                        } else {
5916                                                                            Some(border)
5917                                                                        },
5918                                                                        layout: LayoutStyle {
5919                                                                            size: fret_ui::element::SizeStyle {
5920                                                                                width: Length::Px(col_w),
5921                                                                                min_width: Some(Length::Px(col_w)),
5922                                                                                max_width: Some(Length::Px(col_w)),
5923                                                                                ..Default::default()
5924                                                                            },
5925                                                                            flex: fret_ui::element::FlexItemStyle {
5926                                                                                shrink: 0.0,
5927                                                                                ..Default::default()
5928                                                                            },
5929                                                                            ..Default::default()
5930                                                                        },
5931                                                                        ..Default::default()
5932                                                                    };
5933
5934                                                                    let hoisted_test_id =
5935                                                                        Rc::new(RefCell::new(None));
5936                                                                    let hoisted_test_id_for_cell =
5937                                                                        hoisted_test_id.clone();
5938                                                                    let explicit_test_id =
5939                                                                        debug_header_cell_test_id_prefix
5940                                                                            .as_ref()
5941                                                                            .map(|prefix| {
5942                                                                                Arc::<str>::from(format!(
5943                                                                                    "{prefix}{id}",
5944                                                                                    id = col.id.as_ref()
5945                                                                                ))
5946                                                                            });
5947                                                                    let header_cell =
5948                                                                        cx.container(cell_props, |cx| {
5949                                                                        let mut out = Vec::new();
5950
5951                                                                        out.push(ui::h_row(|cx| {
5952                                                                                let mut pieces = Vec::new();
5953
5954                                                                                let enabled = props.enable_sorting
5955                                                                                    && col.enable_sorting
5956                                                                                    && (col.sort_cmp.is_some()
5957                                                                                        || col.sort_value.is_some());
5958                                                                                let col_id = col.id.clone();
5959                                                                                let state_model =
5960                                                                                    state.clone();
5961                                                                                let sort_options =
5962                                                                                    TableOptions {
5963                                                                                        enable_sorting: props
5964                                                                                            .enable_sorting,
5965                                                                                        ..TableOptions::default()
5966                                                                                    };
5967                                                                                let sort_toggle_column =
5968                                                                                    SortToggleColumn {
5969                                                                                        id: col_id.clone(),
5970                                                                                        enable_sorting: col
5971                                                                                            .enable_sorting,
5972                                                                                        enable_multi_sort: col
5973                                                                                            .enable_multi_sort,
5974                                                                                        sort_desc_first: col
5975                                                                                            .sort_desc_first,
5976                                                                                        has_sort_value_source: col
5977                                                                                            .sort_cmp
5978                                                                                            .is_some()
5979                                                                                            || col.sort_value.is_some(),
5980                                                                                    };
5981                                                                                let auto_sort_dir_desc = col
5982                                                                                    .sort_value
5983                                                                                    .as_ref()
5984                                                                                    .and_then(|f| {
5985                                                                                        data.first()
5986                                                                                            .map(|r| f(r))
5987                                                                                    })
5988                                                                                    .map(|v| {
5989                                                                                        !matches!(
5990                                                                                            v,
5991                                                                                            TanStackValue::String(_)
5992                                                                                        )
5993                                                                                    })
5994                                                                                    .unwrap_or(false);
5995
5996                                                                                pieces.push(cx.pressable(
5997                                                                                    PressableProps {
5998                                                                                        layout: {
5999                                                                                            let mut layout = LayoutStyle::default();
6000                                                                                            layout.size.width = Length::Fill;
6001                                                                                            layout.size.height = Length::Fill;
6002                                                                                            layout.flex.grow = 1.0;
6003                                                                                            layout.flex.shrink = 1.0;
6004                                                                                            layout.flex.basis = Length::Px(Px(0.0));
6005                                                                                            layout
6006                                                                                        },
6007                                                                                        enabled,
6008                                                                                        a11y: PressableA11y {
6009                                                                                            role: Some(
6010                                                                                                SemanticsRole::Button,
6011                                                                                            ),
6012                                                                                        ..Default::default()
6013                                                                                    },
6014                                                                                    ..Default::default()
6015                                                                                },
6016                                                                                    |cx, _| {
6017                                                                                        if enabled {
6018                                                                                            let state_model_for_pointer =
6019                                                                                                state_model.clone();
6020                                                                                            let sort_toggle_column_for_pointer =
6021                                                                                                sort_toggle_column.clone();
6022                                                                                            let sort_options_for_pointer =
6023                                                                                                sort_options;
6024                                                                                            cx.pressable_on_pointer_up(Arc::new(
6025                                                                                                move |host, _acx, up| {
6026                                                                                                    if !up.is_click
6027                                                                                                        || up.button
6028                                                                                                            != fret_core::MouseButton::Left
6029                                                                                                    {
6030                                                                                                        return PressablePointerUpResult::Continue;
6031                                                                                                    }
6032
6033                                                                                                    let multi =
6034                                                                                                        up.modifiers.shift;
6035                                                                                                    let _ = host
6036                                                                                                        .update_model(
6037                                                                                                            &state_model_for_pointer,
6038                                                                                                            |st| {
6039                                                                                                                toggle_sorting_state_handler_tanstack(
6040                                                                                                                    &mut st.sorting,
6041                                                                                                                    &sort_toggle_column_for_pointer,
6042                                                                                                                    sort_options_for_pointer,
6043                                                                                                                    multi,
6044                                                                                                                    auto_sort_dir_desc,
6045                                                                                                                );
6046                                                                                                                st.pagination.page_index = 0;
6047                                                                                                            },
6048                                                                                                        );
6049                                                                                                    host.notify(_acx);
6050                                                                                                    PressablePointerUpResult::SkipActivate
6051                                                                                                },
6052                                                                                            ));
6053
6054                                                                                            cx.pressable_update_model(
6055                                                                                                &state_model,
6056                                                                                                move |st| {
6057                                                                                                    toggle_sorting_state_handler_tanstack(
6058                                                                                                        &mut st.sorting,
6059                                                                                                        &sort_toggle_column,
6060                                                                                                        sort_options,
6061                                                                                                        false,
6062                                                                                                        auto_sort_dir_desc,
6063                                                                                                    );
6064                                                                                                    st.pagination.page_index = 0;
6065                                                                                                },
6066                                                                                            );
6067                                                                                        }
6068
6069                                                                                        vec![cx.container(
6070                                                                                            ContainerProps {
6071                                                                                                padding: Edges::symmetric(
6072                                                                                                    cell_px, cell_py,
6073                                                                                                )
6074                                                                                                .into(),
6075                                                                                                layout: {
6076                                                                                                    let mut layout =
6077                                                                                                        LayoutStyle::default();
6078                                                                                                    layout.size.width =
6079                                                                                                        Length::Fill;
6080                                                                                                    layout.size.height =
6081                                                                                                        Length::Fill;
6082                                                                                                    layout
6083                                                                                                },
6084                                                                                                ..Default::default()
6085                                                                                            },
6086                                                                                            |cx| {
6087                                                                                                let items =
6088                                                                                                    render_header_cell(
6089                                                                                                        cx,
6090                                                                                                        col,
6091                                                                                                        sort_state,
6092                                                                                                    );
6093                                                                                                let mut children =
6094                                                                                                    collect_children(
6095                                                                                                        cx, items,
6096                                                                                                    );
6097                                                                                                *hoisted_test_id_for_cell
6098                                                                                                    .borrow_mut() =
6099                                                                                                    table_wrapper_test_id(
6100                                                                                                        &mut children,
6101                                                                                                        explicit_test_id.clone(),
6102                                                                                                    );
6103                                                                                                children
6104                                                                                            },
6105                                                                                        )]
6106                                                                                    },
6107                                                                                ));
6108
6109                                                                                if props.enable_column_resizing
6110                                                                                    && col.enable_resizing
6111                                                                                {
6112                                                                                    let col_id = col.id.clone();
6113                                                                                    let state_model = state.clone();
6114                                                                                    let default_w = Px(col.size);
6115                                                                                    let min_w = col.min_size.max(props.min_column_width.0).max(0.0);
6116                                                                                    let max_w = col.max_size.max(min_w);
6117                                                                                    let resize_mode = props.column_resize_mode;
6118                                                                                    let resize_direction = props.column_resize_direction;
6119                                                                                    let grip_color = resize_grip;
6120
6121                                                                                    if props.enable_column_resizing
6122                                                                                        && props.column_resize_mode
6123                                                                                            == ColumnResizeMode::OnEnd
6124                                                                                        && state_value
6125                                                                                            .column_sizing_info
6126                                                                                            .is_resizing_column
6127                                                                                            .as_ref()
6128                                                                                            .is_some_and(|active| {
6129                                                                                                active.as_ref()
6130                                                                                                    == col_id.as_ref()
6131                                                                                            })
6132                                                                                    {
6133                                                                                        let delta = state_value
6134                                                                                            .column_sizing_info
6135                                                                                            .delta_offset
6136                                                                                            .unwrap_or(0.0);
6137                                                                                        pieces.push(cx.container(
6138                                                                                            ContainerProps {
6139                                                                                                background: Some(
6140                                                                                                    resize_preview,
6141                                                                                                ),
6142                                                                                                layout: LayoutStyle {
6143                                                                                                    size:
6144                                                                                                        fret_ui::element::SizeStyle {
6145                                                                                                            width: Length::Px(Px(2.0)),
6146                                                                                                            height: Length::Fill,
6147                                                                                                            ..Default::default()
6148                                                                                                        },
6149                                                                                                    position: fret_ui::element::PositionStyle::Absolute,
6150                                                                                                    inset: fret_ui::element::InsetStyle {
6151                                                                                                        top: Some(Px(0.0)).into(),
6152                                                                                                        right: Some(Px(-delta - 1.0)).into(),
6153                                                                                                        bottom: Some(Px(0.0)).into(),
6154                                                                                                        left: None.into(),
6155                                                                                                    },
6156                                                                                                    ..Default::default()
6157                                                                                                },
6158                                                                                                ..Default::default()
6159                                                                                            },
6160                                                                                            |_| Vec::new(),
6161                                                                                        ));
6162                                                                                    }
6163
6164                                                                                    pieces.push(cx.pointer_region(
6165                                                                                        PointerRegionProps {
6166                                                                                            layout: LayoutStyle {
6167                                                                                                size: fret_ui::element::SizeStyle {
6168                                                                                                    width: Length::Px(Px(12.0)),
6169                                                                                                    height: Length::Fill,
6170                                                                                                    ..Default::default()
6171                                                                                                },
6172                                                                                                position:
6173                                                                                                    fret_ui::element::PositionStyle::Absolute,
6174                                                                                                inset: fret_ui::element::InsetStyle {
6175                                                                                                    top: Some(Px(0.0)).into(),
6176                                                                                                    right: Some(Px(0.0)).into(),
6177                                                                                                    bottom: Some(Px(0.0)).into(),
6178                                                                                                    left: None.into(),
6179                                                                                                },
6180                                                                                                ..Default::default()
6181                                                                                            },
6182                                                                                            enabled: true,
6183                                                                                            capture_phase_pointer_moves: false,
6184                                                                                        },
6185                                                                                        |cx| {
6186                                                                                            let state_model_down = state_model.clone();
6187                                                                                            let state_model_move = state_model.clone();
6188                                                                                            let state_model_up = state_model.clone();
6189                                                                                            let col_id_down = col_id.clone();
6190                                                                                            let col_id_move = col_id.clone();
6191                                                                                            let col_id_up = col_id.clone();
6192
6193                                                                                            cx.pointer_region_on_pointer_down(
6194                                                                                                std::sync::Arc::new(move |host, _acx, down| {
6195                                                                                                    if down.button != fret_core::MouseButton::Left {
6196                                                                                                        return false;
6197                                                                                                    }
6198                                                                                                    host.capture_pointer();
6199                                                                                                    host.set_cursor_icon(CursorIcon::ColResize);
6200                                                                                                    let _ = host.models_mut().update(&state_model_down, |st| {
6201                                                                                                        let start = st
6202                                                                                                            .column_sizing
6203                                                                                                            .get(&col_id_down)
6204                                                                                                            .copied()
6205                                                                                                            .unwrap_or(default_w.0)
6206                                                                                                            .clamp(min_w, max_w);
6207                                                                                                        st.column_sizing.insert(col_id_down.clone(), start);
6208                                                                                                        begin_column_resize(
6209                                                                                                            &mut st.column_sizing_info,
6210                                                                                                            col_id_down.clone(),
6211                                                                                                            down.position.x.0,
6212                                                                                                            start,
6213                                                                                                            vec![(col_id_down.clone(), start)],
6214                                                                                                        );
6215                                                                                                    });
6216                                                                                                    true
6217                                                                                                }),
6218                                                                                            );
6219                                                                                            cx.pointer_region_on_pointer_move(
6220                                                                                                std::sync::Arc::new(move |host, _acx, mv| {
6221                                                                                                    host.set_cursor_icon(CursorIcon::ColResize);
6222                                                                                                    if !mv.buttons.left {
6223                                                                                                        return false;
6224                                                                                                    }
6225                                                                                                    let _ = host.models_mut().update(&state_model_move, |st| {
6226                                                                                                        let Some(active) = &st.column_sizing_info.is_resizing_column else { return; };
6227                                                                                                        if active.as_ref() != col_id_move.as_ref() { return; }
6228                                                                                                        drag_column_resize(
6229                                                                                                            resize_mode,
6230                                                                                                            resize_direction,
6231                                                                                                            &mut st.column_sizing,
6232                                                                                                            &mut st.column_sizing_info,
6233                                                                                                            mv.position.x.0,
6234                                                                                                        );
6235                                                                                                        if let Some(next) = st.column_sizing.get(&col_id_move).copied() {
6236                                                                                                            st.column_sizing.insert(col_id_move.clone(), next.clamp(min_w, max_w));
6237                                                                                                        }
6238                                                                                                    });
6239                                                                                                    true
6240                                                                                                }),
6241                                                                                            );
6242                                                                                            cx.pointer_region_on_pointer_up(
6243                                                                                                std::sync::Arc::new(move |host, _acx, up| {
6244                                                                                                    if up.button != fret_core::MouseButton::Left {
6245                                                                                                        return false;
6246                                                                                                    }
6247                                                                                                    host.release_pointer_capture();
6248                                                                                                    let _ = host.models_mut().update(&state_model_up, |st| {
6249                                                                                                        if st
6250                                                                                                            .column_sizing_info
6251                                                                                                            .is_resizing_column
6252                                                                                                            .as_deref()
6253                                                                                                            != Some(col_id_up.as_ref())
6254                                                                                                        {
6255                                                                                                            return;
6256                                                                                                        }
6257                                                                                                        end_column_resize(
6258                                                                                                            resize_mode,
6259                                                                                                            resize_direction,
6260                                                                                                            &mut st.column_sizing,
6261                                                                                                            &mut st.column_sizing_info,
6262                                                                                                            Some(up.position.x.0),
6263                                                                                                        );
6264                                                                                                        if let Some(next) = st.column_sizing.get(&col_id_up).copied() {
6265                                                                                                            st.column_sizing.insert(col_id_up.clone(), next.clamp(min_w, max_w));
6266                                                                                                        }
6267                                                                                                    });
6268                                                                                                    true
6269                                                                                                }),
6270                                                                                            );
6271                                                                                            vec![ui::h_row(|cx| {
6272                                                                                                [cx.container(
6273                                                                                                        ContainerProps {
6274                                                                                                            background: Some(grip_color),
6275                                                                                                            layout: LayoutStyle {
6276                                                                                                                size: fret_ui::element::SizeStyle {
6277                                                                                                                    width: Length::Px(Px(1.0)),
6278                                                                                                                    height: Length::Fill,
6279                                                                                                                    ..Default::default()
6280                                                                                                                },
6281                                                                                                                flex: fret_ui::element::FlexItemStyle {
6282                                                                                                                    shrink: 0.0,
6283                                                                                                                    ..Default::default()
6284                                                                                                                },
6285                                                                                                                ..Default::default()
6286                                                                                                            },
6287                                                                                                            ..Default::default()
6288                                                                                                        },
6289                                                                                                        |_| Vec::new(),
6290                                                                                                    )]
6291                                                                                            })
6292                                                                                            .gap(Space::N0)
6293                                                                                            .justify_end()
6294                                                                                            .items_stretch()
6295                                                                                            .into_element(cx)]
6296                                                                                        },
6297                                                                                    ));
6298                                                                                }
6299
6300                                                                                pieces
6301                                                                            })
6302                                                                            .layout(
6303                                                                                LayoutRefinement::default()
6304                                                                                    .size_full()
6305                                                                                    .relative(),
6306                                                                            )
6307                                                                            .gap(Space::N0)
6308                                                                            .justify_start()
6309                                                                            .items_center()
6310                                                                            .into_element(cx));
6311
6312                                                                                out
6313                                                                    });
6314
6315                                                                    if let Some(test_id) =
6316                                                                        hoisted_test_id.borrow_mut().take()
6317                                                                    {
6318                                                                        header_cell.test_id(test_id)
6319                                                                    } else {
6320                                                                        header_cell
6321                                                                    }
6322                                                                })
6323                                                                .collect();
6324
6325                                                            out
6326                                                        })
6327                                                        .gap(Space::N0)
6328                                                        .justify_start()
6329                                                        .items_center()
6330                                                        .into_element(cx);
6331
6332                                                            if let Some(scroll_x) = scroll_x {
6333                                                                cx.scroll(
6334                                                                    ScrollProps {
6335                                                                        axis: ScrollAxis::X,
6336                                                                        scroll_handle: Some(scroll_x),
6337                                                                        layout: LayoutStyle {
6338                                                                            size: fret_ui::element::SizeStyle {
6339                                                                                width: Length::Fill,
6340                                                                                height: Length::Fill,
6341                                                                                ..Default::default()
6342                                                                            },
6343                                                                            flex: fret_ui::element::FlexItemStyle {
6344                                                                                grow: 1.0,
6345                                                                                shrink: 1.0,
6346                                                                                basis: Length::Px(Px(0.0)),
6347                                                                                ..Default::default()
6348                                                                            },
6349                                                                            ..Default::default()
6350                                                                        },
6351                                                                        ..Default::default()
6352                                                                    },
6353                                                                    |_| vec![row],
6354                                                                )
6355                                                            } else {
6356                                                                row
6357                                                            }
6358                                                };
6359
6360                                            vec![ui::h_row(|cx| {
6361                                                    let has_left = !left_cols.is_empty();
6362                                                    let has_center = !center_cols.is_empty();
6363                                                    let has_right = !right_cols.is_empty();
6364
6365                                                    let divider_after_left = props.optimize_grid_lines
6366                                                        && has_left
6367                                                        && (has_center || has_right);
6368                                                    let divider_after_center =
6369                                                        props.optimize_grid_lines && has_center && has_right;
6370
6371                                                    let left = render_header_group(cx, &left_cols, None);
6372                                                    let left = if divider_after_left {
6373                                                        cx.container(
6374                                                            ContainerProps {
6375                                                                border: Edges {
6376                                                                    right: Px(1.0),
6377                                                                    ..Default::default()
6378                                                                },
6379                                                                border_color: Some(border),
6380                                                                layout: LayoutStyle {
6381                                                                    size: fret_ui::element::SizeStyle {
6382                                                                        height: Length::Fill,
6383                                                                        ..Default::default()
6384                                                                    },
6385                                                                    ..Default::default()
6386                                                                },
6387                                                                ..Default::default()
6388                                                            },
6389                                                            move |_| vec![left],
6390                                                        )
6391                                                    } else {
6392                                                        left
6393                                                    };
6394
6395                                                    let center = render_header_group(
6396                                                        cx,
6397                                                        &center_cols,
6398                                                        Some(scroll_x.clone()),
6399                                                    );
6400                                                    let center = if divider_after_center {
6401                                                        cx.container(
6402                                                            ContainerProps {
6403                                                                border: Edges {
6404                                                                    right: Px(1.0),
6405                                                                    ..Default::default()
6406                                                                },
6407                                                                border_color: Some(border),
6408                                                                layout: LayoutStyle {
6409                                                                    size: fret_ui::element::SizeStyle {
6410                                                                        height: Length::Fill,
6411                                                                        ..Default::default()
6412                                                                    },
6413                                                                    ..Default::default()
6414                                                                },
6415                                                                ..Default::default()
6416                                                            },
6417                                                            move |_| vec![center],
6418                                                        )
6419                                                    } else {
6420                                                        center
6421                                                    };
6422
6423                                                    let right = render_header_group(cx, &right_cols, None);
6424                                                    [left, center, right]
6425                                            })
6426                                            .gap(Space::N0)
6427                                            .justify_start()
6428                                            .items_stretch()
6429                                            .into_element(cx)]
6430                                        },
6431                                    );
6432                                    let header = if let Some(test_id) = debug_header_row_test_id {
6433                                        header.test_id(test_id)
6434                                    } else {
6435                                        header
6436                                    };
6437
6438                                    let debug_row_test_id_prefix = debug_row_test_id_prefix.clone();
6439                                    let body = cx.virtual_list_keyed_with_layout(
6440                                        {
6441                                            let mut layout = LayoutStyle::default();
6442                                            layout.size.width = Length::Fill;
6443                                            layout.flex.grow = 1.0;
6444                                            layout.flex.shrink = 1.0;
6445                                            layout.flex.basis = Length::Px(Px(0.0));
6446                                            layout
6447                                        },
6448                                        set_size,
6449                                        list_options,
6450                                        vertical_scroll,
6451                                        |i| page_display_rows[i].row_key().0,
6452                                        |cx, i| {
6453                                            rendered_rows.set(rendered_rows.get().saturating_add(1));
6454                                            let (data_index, row_key, depth) =
6455                                                match &page_display_rows[i] {
6456                                                    DisplayRow::Leaf {
6457                                                        data_index,
6458                                                        row_key,
6459                                                        depth,
6460                                                    } => (
6461                                                        *data_index,
6462                                                        *row_key,
6463                                                        u16::try_from(*depth).unwrap_or(u16::MAX),
6464                                                    ),
6465                                                    DisplayRow::Group {
6466                                                        grouping_column,
6467                                                        row_key,
6468                                                        depth,
6469                                                        label,
6470                                                        expanded,
6471                                                        aggregations,
6472                                                    } => {
6473                                                        let row_key = *row_key;
6474                                                        let depth = *depth;
6475                                                        let expanded = *expanded;
6476                                                        let label = label.clone();
6477                                                        let grouping_column = grouping_column.clone();
6478                                                        let aggregations = aggregations.clone();
6479
6480                                                        let label_target: ColumnId = if left_cols
6481                                                            .iter()
6482                                                            .chain(center_cols.iter())
6483                                                            .chain(right_cols.iter())
6484                                                            .any(|c| c.id.as_ref() == grouping_column.as_ref())
6485                                                        {
6486                                                            grouping_column.clone()
6487                                                        } else {
6488                                                            left_cols
6489                                                                .first()
6490                                                                .or_else(|| center_cols.first())
6491                                                                .or_else(|| right_cols.first())
6492                                                                .map(|c| c.id.clone())
6493                                                                .unwrap_or_else(|| grouping_column.clone())
6494                                                        };
6495
6496                                                        let enabled = true;
6497                                                        let active_index = active_index.clone();
6498                                                        let anchor_index = anchor_index.clone();
6499                                                        let active_element = active_element.clone();
6500                                                        let active_command = active_command.clone();
6501                                                        let typeahead = typeahead.clone();
6502                                                        let typeahead_timer = typeahead_timer.clone();
6503                                                        let focus_target = list_id;
6504                                                        let row_test_id = debug_row_test_id_prefix
6505                                                            .as_ref()
6506                                                            .map(|prefix| {
6507                                                                Arc::<str>::from(format!(
6508                                                                    "{prefix}{id}",
6509                                                                    id = row_key.0
6510                                                                ))
6511                                                            });
6512
6513                                                        return cx.pressable_with_id(
6514                                                            PressableProps {
6515                                                                enabled,
6516                                                                focusable: false,
6517                                                                a11y: PressableA11y {
6518                                                                    role: Some(
6519                                                                        SemanticsRole::ListItem,
6520                                                                    ),
6521                                                                    expanded: Some(expanded),
6522                                                                    test_id: row_test_id,
6523                                                                    ..Default::default()
6524                                                                }
6525                                                                .with_collection_position(i, set_size),
6526                                                                ..Default::default()
6527                                                            },
6528															|cx, st, id| {
6529																let active_index_for_pointer =
6530																	active_index.clone();
6531																let anchor_index_for_pointer =
6532																	anchor_index.clone();
6533																let active_command_for_pointer =
6534																	active_command.clone();
6535																let typeahead_for_pointer =
6536																	typeahead.clone();
6537																let typeahead_timer_for_pointer =
6538																	typeahead_timer.clone();
6539                                                            cx.pressable_on_pointer_down(Arc::new(
6540                                                                move |host, action_cx, down| {
6541                                                                    active_index_for_pointer.set(Some(i));
6542                                                                    let next_anchor = if down.modifiers.shift {
6543                                                                        anchor_index_for_pointer
6544                                                                            .get()
6545                                                                            .or(Some(row_key))
6546                                                                    } else {
6547                                                                        Some(row_key)
6548                                                                    };
6549                                                                    anchor_index_for_pointer
6550                                                                        .set(next_anchor);
6551                                                                    typeahead_for_pointer
6552                                                                        .borrow_mut()
6553                                                                        .clear();
6554                                                                    if let Some(token) =
6555                                                                        typeahead_timer_for_pointer.get()
6556                                                                    {
6557                                                                        host.push_effect(Effect::CancelTimer { token });
6558                                                                        typeahead_timer_for_pointer.set(None);
6559                                                                    }
6560                                                                    *active_command_for_pointer.borrow_mut() = None;
6561                                                                    host.request_redraw(action_cx.window);
6562                                                                    PressablePointerDownResult::Continue
6563                                                                },
6564                                                            ));
6565																cx.pressable_add_on_pointer_up(Arc::new(
6566																	move |host, action_cx, up| {
6567																		if up.button
6568																			!= fret_core::MouseButton::Left
6569																			|| !up.is_click
6570																		{
6571																			return PressablePointerUpResult::Continue;
6572																		}
6573																		host.request_focus(focus_target);
6574																		host.request_redraw(action_cx.window);
6575																		PressablePointerUpResult::Continue
6576																	},
6577																));
6578
6579																if active_index.get() == Some(i) {
6580																	active_element.set(Some(id));
6581																	*active_command.borrow_mut() = None;
6582																}
6583																let state_model = state.clone();
6584																cx.pressable_update_model(
6585																	&state_model,
6586																	move |st| match &mut st.expanding
6587                                                                    {
6588                                                                        ExpandingState::All => {
6589                                                                            st.expanding =
6590                                                                                ExpandingState::default();
6591                                                                        }
6592                                                                        ExpandingState::Keys(keys) => {
6593                                                                            if keys.contains(&row_key)
6594                                                                            {
6595                                                                                keys.remove(&row_key);
6596                                                                            } else {
6597                                                                                keys.insert(row_key);
6598                                                                            }
6599                                                                        }
6600                                                                    },
6601                                                                );
6602
6603                                                                let is_active = active_index.get() == Some(i);
6604                                                                let bg = if st.pressed {
6605                                                                    Some(row_active_bg)
6606                                                                } else if st.hovered {
6607                                                                    Some(row_hover_bg)
6608                                                                } else {
6609                                                                    None
6610                                                                };
6611
6612                                                                let indent_step = 12.0_f32;
6613                                                                let indent_px =
6614                                                                    Px((depth as f32) * indent_step);
6615                                                                let glyph: Arc<str> = if expanded {
6616                                                                    Arc::from("v")
6617                                                                } else {
6618                                                                    Arc::from(">")
6619                                                                };
6620                                                                let text: Arc<str> = Arc::from(
6621                                                                    format!("{glyph} {label}"),
6622                                                                );
6623
6624                                                                vec![cx.container(
6625                                                                    ContainerProps {
6626                                                                        background: bg,
6627                                                                        layout: LayoutStyle {
6628                                                                            size: fret_ui::element::SizeStyle {
6629                                                                                width: Length::Fill,
6630                                                                                height: body_row_height,
6631                                                                                ..Default::default()
6632                                                                            },
6633                                                                            position:
6634                                                                                fret_ui::element::PositionStyle::Relative,
6635                                                                            ..Default::default()
6636                                                                        },
6637                                                                        ..Default::default()
6638                                                                    },
6639                                                                    |cx| {
6640                                                                        let mut out = Vec::new();
6641                                                                        if is_active {
6642                                                                            out.push(cx.container(
6643                                                                                ContainerProps {
6644                                                                                    background: Some(ring),
6645                                                                                    layout: LayoutStyle {
6646                                                                                        size: fret_ui::element::SizeStyle {
6647                                                                                            width: Length::Px(Px(2.0)),
6648                                                                                            height: Length::Fill,
6649                                                                                            ..Default::default()
6650                                                                                        },
6651                                                                                        position:
6652                                                                                            fret_ui::element::PositionStyle::Absolute,
6653                                                                                        inset: fret_ui::element::InsetStyle {
6654                                                                                            top: Some(Px(0.0)).into(),
6655                                                                                            right: None.into(),
6656                                                                                            bottom: Some(Px(0.0)).into(),
6657                                                                                            left: Some(Px(0.0)).into(),
6658                                                                                        },
6659                                                                                        ..Default::default()
6660                                                                                    },
6661                                                                                    ..Default::default()
6662                                                                                },
6663                                                                                |_| Vec::new(),
6664                                                                            ));
6665                                                                        }
6666                                                                        let render_group =
6667                                                                            |cx: &mut ElementContext<
6668                                                                                '_,
6669                                                                                H,
6670                                                                            >,
6671                                                                             cols: &[&ColumnDef<
6672                                                                                TData,
6673                                                                            >],
6674                                                                             scroll_x: Option<
6675                                                                                ScrollHandle,
6676                                                                            >| {
6677                                                                                let column_width_by_id =
6678                                                                                    column_width_by_id.clone();
6679                                                                                let row = if props
6680                                                                                    .optimize_paint_order
6681                                                                                {
6682                                                                                    cx.container(
6683                                                                                        ContainerProps {
6684                                                                                            layout: LayoutStyle {
6685                                                                                                size:
6686                                                                                                    fret_ui::element::SizeStyle {
6687                                                                                                        height: Length::Fill,
6688                                                                                                        ..Default::default()
6689                                                                                                    },
6690                                                                                                ..Default::default()
6691                                                                                            },
6692                                                                                            ..Default::default()
6693                                                                                        },
6694                                                                                        |cx| {
6695                                                                                            let col_widths: Vec<Px> = cols
6696                                                                                                .iter()
6697                                                                                                .map(|col| {
6698                                                                                                    column_width_by_id
6699                                                                                                        .get(&col.id)
6700                                                                                                        .copied()
6701                                                                                                        .unwrap_or(Px(col.size))
6702                                                                                                })
6703                                                                                                .collect();
6704                                                                                            let background_row = ui::h_row(|cx| {
6705                                                                                                cols.iter()
6706                                                                                                    .zip(col_widths.iter().copied())
6707                                                                                                    .map(|(_col, col_w)| {
6708                                                                                                        cx.container(
6709                                                                                                            ContainerProps {
6710                                                                                                                border: if props.optimize_grid_lines {
6711                                                                                                                    Edges::default()
6712                                                                                                                } else {
6713                                                                                                                    Edges {
6714                                                                                                                        right: Px(1.0),
6715                                                                                                                        ..Default::default()
6716                                                                                                                    }
6717                                                                                                                },
6718                                                                                                                border_color: if props.optimize_grid_lines {
6719                                                                                                                    None
6720                                                                                                                } else {
6721                                                                                                                    Some(border)
6722                                                                                                                },
6723                                                                                                                layout: LayoutStyle {
6724                                                                                                                    size: fret_ui::element::SizeStyle {
6725                                                                                                                        width: Length::Px(col_w),
6726                                                                                                                        min_width: Some(Length::Px(col_w)),
6727                                                                                                                        max_width: Some(Length::Px(col_w)),
6728                                                                                                                        height: Length::Fill,
6729                                                                                                                        ..Default::default()
6730                                                                                                                    },
6731                                                                                                                    flex: fret_ui::element::FlexItemStyle {
6732                                                                                                                        shrink: 0.0,
6733                                                                                                                        ..Default::default()
6734                                                                                                                    },
6735                                                                                                                    ..Default::default()
6736                                                                                                                },
6737                                                                                                                ..Default::default()
6738                                                                                                            },
6739                                                                                                            |_| Vec::new(),
6740                                                                                                        )
6741                                                                                                    })
6742                                                                                                    .collect::<Vec<_>>()
6743                                                                                            })
6744                                                                                            .gap(Space::N0)
6745                                                                                            .justify_start()
6746                                                                                            .items_stretch()
6747                                                                                            .into_element(cx);
6748
6749                                                                                            let content_overlay = cx.container(
6750                                                                                                ContainerProps {
6751                                                                                                    layout: LayoutStyle {
6752                                                                                                        size: fret_ui::element::SizeStyle {
6753                                                                                                            width: Length::Fill,
6754                                                                                                            height: Length::Fill,
6755                                                                                                            ..Default::default()
6756                                                                                                        },
6757                                                                                                        position:
6758                                                                                                            fret_ui::element::PositionStyle::Absolute,
6759                                                                                                        inset: fret_ui::element::InsetStyle {
6760                                                                                                            top: Some(Px(0.0)).into(),
6761                                                                                                            right: Some(Px(0.0)).into(),
6762                                                                                                            bottom: Some(Px(0.0)).into(),
6763                                                                                                            left: Some(Px(0.0)).into(),
6764                                                                                                        },
6765                                                                                                        ..Default::default()
6766                                                                                                    },
6767                                                                                                    ..Default::default()
6768                                                                                                },
6769                                                                                                |cx| {
6770                                                                                                    vec![ui::h_row(|cx| {
6771                                                                                                        cols.iter()
6772                                                                                                            .zip(col_widths.iter().copied())
6773                                                                                                            .map(|(col, col_w)| {
6774
6775                                                                                                                    let is_label_target = col
6776                                                                                                                        .id
6777                                                                                                                        .as_ref()
6778                                                                                                                        == label_target.as_ref();
6779                                                                                                                    let is_placeholder =
6780                                                                                                                        !is_label_target
6781                                                                                                                            && grouping.iter().any(|id| {
6782                                                                                                                                id.as_ref() == col.id.as_ref()
6783                                                                                                                            });
6784
6785                                                                                                                    let padding = if is_label_target {
6786                                                                                                                        Edges {
6787                                                                                                                            left: Px(
6788                                                                                                                                cell_px.0
6789                                                                                                                                    + indent_px.0,
6790                                                                                                                            ),
6791                                                                                                                            right: cell_px,
6792                                                                                                                            top: cell_py,
6793                                                                                                                            bottom: cell_py,
6794                                                                                                                        }
6795                                                                                                                    } else {
6796                                                                                                                        Edges::symmetric(cell_px, cell_py)
6797                                                                                                                    };
6798
6799                                                                                                                    cx.container(
6800                                                                                                                        ContainerProps {
6801                                                                                                                            padding: padding.into(),
6802                                                                                                                           layout: LayoutStyle {
6803                                                                                                                                size: fret_ui::element::SizeStyle {
6804                                                                                                                                    width: Length::Px(col_w),
6805                                                                                                                                    min_width: Some(Length::Px(col_w)),
6806                                                                                                                                    max_width: Some(Length::Px(col_w)),
6807                                                                                                                                    height: Length::Fill,
6808                                                                                                                                    ..Default::default()
6809                                                                                                                                },
6810                                                                                                                                flex: fret_ui::element::FlexItemStyle {
6811                                                                                                                                    shrink: 0.0,
6812                                                                                                                                    ..Default::default()
6813                                                                                                                                },
6814                                                                                                                                ..Default::default()
6815                                                                                                                            },
6816                                                                                                                            ..Default::default()
6817                                                                                                                        },
6818                                                                                                                        |cx| {
6819                                                                                                                            if is_placeholder {
6820                                                                                                                                return Vec::new();
6821                                                                                                                            }
6822                                                                                                                            if is_label_target {
6823                                                                                                                                vec![cx.text(text.clone())]
6824                                                                                                                            } else {
6825                                                                                                                                let v = aggregations
6826                                                                                                                                    .iter()
6827                                                                                                                                    .find(|entry| {
6828                                                                                                                                        entry.0.as_ref()
6829                                                                                                                                            == col
6830                                                                                                                                                .id
6831                                                                                                                                                .as_ref()
6832                                                                                                                                    })
6833                                                                                                                                    .map(|entry| entry.1.clone());
6834                                                                                                                                v.map(|v| vec![cx.text(v)])
6835                                                                                                                                    .unwrap_or_default()
6836                                                                                                                            }
6837                                                                                                                        },
6838                                                                                                                    )
6839                                                                                                            })
6840                                                                                                            .collect::<Vec<_>>()
6841                                                                                                    })
6842                                                                                                    .gap(Space::N0)
6843                                                                                                    .justify_start()
6844                                                                                                    .items_center()
6845                                                                                                    .into_element(cx)]
6846                                                                                                },
6847                                                                                            );
6848
6849                                                                                            vec![background_row, content_overlay]
6850                                                                                        },
6851                                                                                    )
6852                                                                                } else {
6853                                                                                    ui::h_row(|cx| {
6854                                                                                            cols.iter()
6855                                                                                                .map(|col| {
6856                                                                                                    let col_w = column_width_by_id
6857                                                                                                        .get(&col.id)
6858                                                                                                        .copied()
6859                                                                                                        .unwrap_or(Px(col.size));
6860
6861                                                                                                    let is_label_target =
6862                                                                                                        col.id.as_ref()
6863                                                                                                            == label_target.as_ref();
6864                                                                                                    let is_placeholder = !is_label_target
6865                                                                                                        && grouping.iter().any(|id| {
6866                                                                                                            id.as_ref() == col.id.as_ref()
6867                                                                                                        });
6868
6869                                                                                                    let padding = if is_label_target {
6870                                                                                                        Edges {
6871                                                                                                            left: Px(
6872                                                                                                                cell_px.0
6873                                                                                                                    + indent_px.0,
6874                                                                                                            ),
6875                                                                                                            right: cell_px,
6876                                                                                                            top: cell_py,
6877                                                                                                            bottom: cell_py,
6878                                                                                                        }
6879                                                                                                    } else {
6880                                                                                                        Edges::symmetric(
6881                                                                                                            cell_px,
6882                                                                                                            cell_py,
6883                                                                                                        )
6884                                                                                                    };
6885
6886                                                                                                    cx.container(
6887                                                                                                        ContainerProps {
6888                                                                                                            padding: padding.into(),
6889                                                                                                            border: if props.optimize_grid_lines {
6890                                                                                                                Edges::default()
6891                                                                                                            } else {
6892                                                                                                                Edges {
6893                                                                                                                    right: Px(1.0),
6894                                                                                                                    ..Default::default()
6895                                                                                                                }
6896                                                                                                            },
6897                                                                                                            border_color: if props.optimize_grid_lines {
6898                                                                                                                None
6899                                                                                                            } else {
6900                                                                                                                Some(border)
6901                                                                                                            },
6902                                                                                                            layout: LayoutStyle {
6903                                                                                                       size: fret_ui::element::SizeStyle {
6904                                                                                                           width: Length::Px(col_w),
6905                                                                                                            min_width: Some(Length::Px(col_w)),
6906                                                                                                            max_width: Some(Length::Px(col_w)),
6907                                                                                                           ..Default::default()
6908                                                                                                       },
6909                                                                                                                flex: fret_ui::element::FlexItemStyle {
6910                                                                                                                    shrink: 0.0,
6911                                                                                                                    ..Default::default()
6912                                                                                                                },
6913                                                                                                                ..Default::default()
6914                                                                                                            },
6915                                                                                                            ..Default::default()
6916                                                                                                        },
6917                                                                                                        |cx| {
6918                                                                                                            if is_placeholder {
6919                                                                                                                return Vec::new();
6920                                                                                                            }
6921                                                                                                            if is_label_target {
6922                                                                                                                vec![cx.text(text.clone())]
6923                                                                                                            } else {
6924                                                                                                                let v = aggregations
6925                                                                                                                    .iter()
6926                                                                                                                    .find(|entry| {
6927                                                                                                                        entry.0.as_ref()
6928                                                                                                                            == col
6929                                                                                                                                .id
6930                                                                                                                                .as_ref()
6931                                                                                                                    })
6932                                                                                                                    .map(|entry| entry.1.clone());
6933                                                                                                                v.map(|v| vec![cx.text(v)])
6934                                                                                                                    .unwrap_or_default()
6935                                                                                                            }
6936                                                                                                        },
6937                                                                                                    )
6938                                                                                                })
6939                                                                                                .collect::<Vec<_>>()
6940                                                                                    })
6941                                                                                    .gap(Space::N0)
6942                                                                                    .justify_start()
6943                                                                                    .items_center()
6944                                                                                    .into_element(cx)
6945                                                                                };
6946
6947                                                                                if let Some(scroll_x) =
6948                                                                                    scroll_x
6949                                                                                {
6950                                                                                    cx.scroll(
6951                                                                                        ScrollProps {
6952                                                                                            axis: ScrollAxis::X,
6953                                                                                            scroll_handle: Some(scroll_x),
6954                                                                                            layout: LayoutStyle {
6955                                                                                                size: fret_ui::element::SizeStyle {
6956                                                                                                    width: Length::Fill,
6957                                                                                                    height: Length::Fill,
6958                                                                                                    ..Default::default()
6959                                                                                                },
6960                                                                                                flex: fret_ui::element::FlexItemStyle {
6961                                                                                                    grow: 1.0,
6962                                                                                                    shrink: 1.0,
6963                                                                                                    basis: Length::Px(Px(0.0)),
6964                                                                                                    ..Default::default()
6965                                                                                                },
6966                                                                                                ..Default::default()
6967                                                                                            },
6968                                                                                            ..Default::default()
6969                                                                                        },
6970                                                                                        |_| vec![row],
6971                                                                                    )
6972                                                                                } else {
6973                                                                                    row
6974                                                                                }
6975                                                                            };
6976
6977                                                                        out.push(ui::h_row(|cx| {
6978                                                                                let has_left =
6979                                                                                    !left_cols.is_empty();
6980                                                                                let has_center =
6981                                                                                    !center_cols.is_empty();
6982                                                                                let has_right =
6983                                                                                    !right_cols.is_empty();
6984
6985                                                                                let divider_after_left = props
6986                                                                                    .optimize_grid_lines
6987                                                                                    && has_left
6988                                                                                    && (has_center || has_right);
6989                                                                                let divider_after_center = props
6990                                                                                    .optimize_grid_lines
6991                                                                                    && has_center
6992                                                                                    && has_right;
6993
6994                                                                                let left = render_group(
6995                                                                                    cx,
6996                                                                                    &left_cols,
6997                                                                                    None,
6998                                                                                );
6999                                                                                let left = if divider_after_left {
7000                                                                                    cx.container(
7001                                                                                        ContainerProps {
7002                                                                                            border: Edges {
7003                                                                                                right: Px(1.0),
7004                                                                                                ..Default::default()
7005                                                                                            },
7006                                                                                            border_color: Some(border),
7007                                                                                            layout: LayoutStyle {
7008                                                                                                size: fret_ui::element::SizeStyle {
7009                                                                                                    height: Length::Fill,
7010                                                                                                    ..Default::default()
7011                                                                                                },
7012                                                                                                ..Default::default()
7013                                                                                            },
7014                                                                                            ..Default::default()
7015                                                                                        },
7016                                                                                        move |_| vec![left],
7017                                                                                    )
7018                                                                                } else {
7019                                                                                    left
7020                                                                                };
7021
7022                                                                                let center = render_group(
7023                                                                                    cx,
7024                                                                                    &center_cols,
7025                                                                                    Some(scroll_x.clone()),
7026                                                                                );
7027                                                                                let center = if divider_after_center {
7028                                                                                    cx.container(
7029                                                                                        ContainerProps {
7030                                                                                            border: Edges {
7031                                                                                                right: Px(1.0),
7032                                                                                                ..Default::default()
7033                                                                                            },
7034                                                                                            border_color: Some(border),
7035                                                                                            layout: LayoutStyle {
7036                                                                                                size: fret_ui::element::SizeStyle {
7037                                                                                                    height: Length::Fill,
7038                                                                                                    ..Default::default()
7039                                                                                                },
7040                                                                                                ..Default::default()
7041                                                                                            },
7042                                                                                            ..Default::default()
7043                                                                                        },
7044                                                                                        move |_| vec![center],
7045                                                                                    )
7046                                                                                } else {
7047                                                                                    center
7048                                                                                };
7049
7050                                                                                let right = render_group(
7051                                                                                    cx,
7052                                                                                    &right_cols,
7053                                                                                    None,
7054                                                                                );
7055
7056                                                                                vec![left, center, right]
7057                                                                        })
7058                                                                        .gap(Space::N0)
7059                                                                        .justify_start()
7060                                                                        .items_stretch()
7061                                                                        .into_element(cx));
7062                                                                        out
7063                                                                    },
7064                                                                )]
7065                                                            },
7066                                                        );
7067                                                    }
7068                                                };
7069
7070                                            let data_row = Row {
7071                                                id: RowId::new(row_key.0.to_string()),
7072                                                key: row_key,
7073                                                original: &data[data_index],
7074                                                index: data_index,
7075                                                depth,
7076                                                parent: None,
7077                                                parent_key: None,
7078                                                sub_rows: Vec::new(),
7079                                            };
7080
7081                                            let cmd = on_row_activate(&data_row);
7082                                            let enabled = cmd.is_some() || props.enable_row_selection;
7083                                            let is_selected =
7084                                                is_row_selected(data_row.key, &state_value.row_selection);
7085                                            let row_test_id = debug_row_test_id_prefix
7086                                                .as_ref()
7087                                                .map(|prefix| {
7088                                                    Arc::<str>::from(format!(
7089                                                        "{prefix}{id}",
7090                                                        id = data_row.key.0
7091                                                    ))
7092                                                });
7093
7094                                            let active_index = active_index.clone();
7095                                            let anchor_index = anchor_index.clone();
7096                                            let active_element = active_element.clone();
7097                                            let active_command = active_command.clone();
7098                                            let typeahead = typeahead.clone();
7099                                            let typeahead_timer = typeahead_timer.clone();
7100                                            let focus_target = list_id;
7101
7102                                            cx.pressable_with_id(
7103                                                PressableProps {
7104                                                    enabled,
7105                                                    focusable: false,
7106                                                    a11y: PressableA11y {
7107                                                        role: Some(SemanticsRole::ListItem),
7108                                                        selected: is_selected,
7109                                                        test_id: row_test_id,
7110                                                        ..Default::default()
7111                                                    }
7112                                                    .with_collection_position(i, set_size),
7113                                                    ..Default::default()
7114                                                },
7115                                                |cx, st, id| {
7116                                                    let active_index_for_pointer =
7117                                                        active_index.clone();
7118                                                    let anchor_index_for_pointer_down =
7119                                                        anchor_index.clone();
7120                                                    let anchor_index_for_pointer_up =
7121                                                        anchor_index.clone();
7122                                                    let row_meta_for_pointer = row_meta.clone();
7123                                                    let typeahead_for_pointer = typeahead.clone();
7124                                                    let typeahead_timer_for_pointer =
7125                                                        typeahead_timer.clone();
7126                                                    let state_model_for_pointer = state.clone();
7127                                                    let row_key_for_pointer = data_row.key;
7128
7129                                                    cx.pressable_on_pointer_down(Arc::new(
7130                                                        move |host, action_cx, down| {
7131                                                            active_index_for_pointer.set(Some(i));
7132                                                            let next_anchor = if down.modifiers.shift {
7133                                                                anchor_index_for_pointer_down
7134                                                                    .get()
7135                                                                    .or(Some(row_key_for_pointer))
7136                                                            } else {
7137                                                                Some(row_key_for_pointer)
7138                                                            };
7139                                                            anchor_index_for_pointer_down
7140                                                                .set(next_anchor);
7141                                                            typeahead_for_pointer
7142                                                                .borrow_mut()
7143                                                                .clear();
7144                                                            if let Some(token) =
7145                                                                typeahead_timer_for_pointer.get()
7146                                                            {
7147                                                                host.push_effect(
7148                                                                    Effect::CancelTimer { token },
7149                                                                );
7150                                                                typeahead_timer_for_pointer
7151                                                                    .set(None);
7152                                                            }
7153                                                            host.request_redraw(action_cx.window);
7154                                                            PressablePointerDownResult::Continue
7155                                                        },
7156                                                    ));
7157
7158                                                    cx.pressable_add_on_pointer_up(Arc::new(
7159                                                        move |host, action_cx, up| {
7160															if up.button
7161																!= fret_core::MouseButton::Left
7162																|| !up.is_click
7163															{
7164																return PressablePointerUpResult::Continue;
7165															}
7166															host.request_focus(focus_target);
7167                                                            let pointer_row_selection_enabled = props.enable_row_selection
7168                                                                && props.pointer_row_selection;
7169															if pointer_row_selection_enabled {
7170																let policy =
7171																	props.pointer_row_selection_policy;
7172																let modifiers = up.modifiers;
7173																let row_key = row_key_for_pointer;
7174																let single = props.single_row_selection;
7175															let meta = row_meta_for_pointer
7176																	.borrow()
7177																	.clone();
7178																let range_keys = if policy
7179																	== PointerRowSelectionPolicy::ListLike
7180																	&& !single
7181																	&& modifiers.shift
7182																{
7183                                                                    let anchor_key = anchor_index_for_pointer_up
7184                                                                        .get()
7185                                                                        .unwrap_or(row_key);
7186                                                                    let anchor = meta
7187                                                                        .iter()
7188                                                                        .position(|m| m.row_key == anchor_key)
7189                                                                        .unwrap_or(i);
7190																	let (a, b) = if anchor <= i {
7191																		(anchor, i)
7192																	} else {
7193																		(i, anchor)
7194																	};
7195																	let keys = if single {
7196																		vec![row_key]
7197																	} else {
7198																		table_collect_leaf_keys_in_range(
7199																			&meta, a, b,
7200																		)
7201																	};
7202																	(!keys.is_empty()).then_some(keys)
7203																} else {
7204																	None
7205																};
7206
7207																let _ = host.models_mut().update(
7208																	&state_model_for_pointer,
7209																	move |st| match policy {
7210																		PointerRowSelectionPolicy::Toggle => {
7211																			let selected =
7212																				st.row_selection
7213																					.contains(&row_key);
7214																			if single {
7215																				st.row_selection.clear();
7216																			}
7217																			if selected {
7218																				st.row_selection
7219																					.remove(&row_key);
7220																			} else {
7221																				st.row_selection
7222																					.insert(row_key);
7223																			}
7224																		}
7225																		PointerRowSelectionPolicy::ListLike => {
7226																			if let Some(range_keys) =
7227																				range_keys.as_ref()
7228																			{
7229																				if modifiers.ctrl
7230																					|| modifiers.meta
7231																				{
7232																					st.row_selection.extend(
7233																						range_keys
7234																							.iter()
7235																							.copied(),
7236																					);
7237																				} else {
7238																					st.row_selection.clear();
7239																					st.row_selection.extend(
7240																						range_keys
7241																							.iter()
7242																							.copied(),
7243																					);
7244																				}
7245																			} else if !single
7246																				&& (modifiers.ctrl
7247																					|| modifiers.meta)
7248																			{
7249																				if st.row_selection
7250																					.contains(&row_key)
7251																				{
7252																					st.row_selection
7253																						.remove(&row_key);
7254																				} else {
7255																					st.row_selection
7256																						.insert(row_key);
7257																				}
7258																			} else {
7259																				st.row_selection.clear();
7260																				st.row_selection
7261																					.insert(row_key);
7262																			}
7263																		}
7264																	},
7265																);
7266
7267																	let next_anchor = if policy
7268																		== PointerRowSelectionPolicy::ListLike
7269																		&& modifiers.shift
7270																{
7271																	anchor_index_for_pointer_up
7272																		.get()
7273																		.or(Some(row_key))
7274																} else {
7275																	Some(row_key)
7276																};
7277																anchor_index_for_pointer_up
7278																	.set(next_anchor);
7279															}
7280															host.request_redraw(action_cx.window);
7281                                                            // When pointer-driven row selection is enabled, a click should not
7282                                                            // also activate the row command (avoid "selection + activate" conflicts).
7283															PressablePointerUpResult::SkipActivate
7284	                                                        },
7285	                                                    ));
7286
7287														if active_index.get() == Some(i) {
7288															active_element.set(Some(id));
7289															*active_command.borrow_mut() = cmd.clone();
7290														}
7291													cx.pressable_dispatch_command_if_enabled_opt(cmd.clone());
7292
7293													let is_active = active_index.get() == Some(i);
7294													let bg = if is_selected || (enabled && st.pressed) {
7295														Some(row_active_bg)
7296                                                    } else if enabled && st.hovered {
7297                                                        Some(row_hover_bg)
7298                                                    } else {
7299                                                        None
7300                                                    };
7301
7302                                                        vec![cx.container(
7303                                                            ContainerProps {
7304                                                                background: bg,
7305                                                                layout: LayoutStyle {
7306                                                                    size: fret_ui::element::SizeStyle {
7307                                                                        width: Length::Fill,
7308                                                                        height: body_row_height,
7309                                                                        ..Default::default()
7310                                                                    },
7311                                                                    position:
7312                                                                        fret_ui::element::PositionStyle::Relative,
7313                                                                    ..Default::default()
7314                                                                },
7315                                                                ..Default::default()
7316                                                            },
7317                                                            |cx| {
7318                                                            let mut out = Vec::new();
7319                                                            if is_active {
7320                                                                out.push(cx.container(
7321                                                                    ContainerProps {
7322                                                                        background: Some(ring),
7323                                                                        layout: LayoutStyle {
7324                                                                            size:
7325                                                                                fret_ui::element::SizeStyle {
7326                                                                                    width: Length::Px(Px(2.0)),
7327                                                                                    height: Length::Fill,
7328                                                                                    ..Default::default()
7329                                                                                },
7330                                                                            position:
7331                                                                                fret_ui::element::PositionStyle::Absolute,
7332                                                                            inset: fret_ui::element::InsetStyle {
7333                                                                                top: Some(Px(0.0)).into(),
7334                                                                                right: None.into(),
7335                                                                                bottom: Some(Px(0.0)).into(),
7336                                                                                left: Some(Px(0.0)).into(),
7337                                                                            },
7338                                                                            ..Default::default()
7339                                                                        },
7340                                                                        ..Default::default()
7341                                                                    },
7342                                                                    |_| Vec::new(),
7343                                                                ));
7344                                                            }
7345                                                            let mut render_row_group =
7346                                                                |cx: &mut ElementContext<'_, H>,
7347                                                                 cols: &[&ColumnDef<TData>],
7348                                                                 scroll_x: Option<ScrollHandle>| {
7349                                                                    let column_width_by_id =
7350                                                                        column_width_by_id.clone();
7351                                                                let row = if props.optimize_paint_order {
7352                                                                        cx.container(
7353                                                                            ContainerProps {
7354                                                                                layout: LayoutStyle {
7355                                                                                    size: fret_ui::element::SizeStyle {
7356                                                                                        height: Length::Fill,
7357                                                                                        ..Default::default()
7358                                                                                    },
7359                                                                                    ..Default::default()
7360                                                                                },
7361                                                                                ..Default::default()
7362                                                                            },
7363                                                                            |cx| {
7364                                                                                let col_widths: Vec<Px> = cols
7365                                                                                    .iter()
7366                                                                                    .map(|col| {
7367                                                                                        column_width_by_id
7368                                                                                            .get(&col.id)
7369                                                                                            .copied()
7370                                                                                            .unwrap_or(Px(col.size))
7371                                                                                    })
7372                                                                                    .collect();
7373                                                                                let background_row = ui::h_row(|cx| {
7374                                                                                        cols.iter()
7375                                                                                            .zip(col_widths.iter().copied())
7376                                                                                            .map(|(_col, col_w)| {
7377                                                                                                cx.container(
7378                                                                                                    ContainerProps {
7379                                                                                                        border: if props.optimize_grid_lines {
7380                                                                                                            Edges::default()
7381                                                                                                        } else {
7382                                                                                                            Edges {
7383                                                                                                                right: Px(1.0),
7384                                                                                                                ..Default::default()
7385                                                                                                            }
7386                                                                                                        },
7387                                                                                                        border_color: if props.optimize_grid_lines {
7388                                                                                                            None
7389                                                                                                        } else {
7390                                                                                                            Some(border)
7391                                                                                                        },
7392                                                                                                        layout: table_fixed_column_fill_layout(col_w),
7393                                                                                                        ..Default::default()
7394                                                                                                    },
7395                                                                                                    |_| Vec::new(),
7396                                                                                                )
7397                                                                                            })
7398                                                                                            .collect::<Vec<_>>()
7399                                                                                })
7400                                                                                .gap(Space::N0)
7401                                                                                .justify_start()
7402                                                                                .items_stretch()
7403                                                                                .into_element(cx);
7404
7405                                                                                let content_overlay = cx.container(
7406                                                                                    ContainerProps {
7407                                                                                        layout: LayoutStyle {
7408                                                                                            size: fret_ui::element::SizeStyle {
7409                                                                                                width: Length::Fill,
7410                                                                                                height: Length::Fill,
7411                                                                                                ..Default::default()
7412                                                                                            },
7413                                                                                            position:
7414                                                                                                fret_ui::element::PositionStyle::Absolute,
7415                                                                                            inset: fret_ui::element::InsetStyle {
7416                                                                                                top: Some(Px(0.0)).into(),
7417                                                                                                right: Some(Px(0.0)).into(),
7418                                                                                                bottom: Some(Px(0.0)).into(),
7419                                                                                                left: Some(Px(0.0)).into(),
7420                                                                                            },
7421                                                                                            ..Default::default()
7422                                                                                        },
7423                                                                                        ..Default::default()
7424                                                                                    },
7425                                                                                    |cx| {
7426                                                                                        vec![ui::h_row(|cx| {
7427                                                                                                cols.iter()
7428                                                                                                    .zip(col_widths.iter().copied())
7429                                                                                                    .map(|(col, col_w)| {
7430                                                                                                        let hoisted_test_id =
7431                                                                                                            Rc::new(RefCell::new(None));
7432                                                                                                        let hoisted_test_id_for_cell =
7433                                                                                                            hoisted_test_id.clone();
7434                                                                                                        let explicit_test_id =
7435                                                                                                            debug_row_test_id_prefix
7436                                                                                                                .as_ref()
7437                                                                                                                .map(|prefix| {
7438                                                                                                                    Arc::<str>::from(format!(
7439                                                                                                                        "{prefix}{row}-cell-{col}",
7440                                                                                                                        row = data_row.key.0,
7441                                                                                                                        col = col.id.as_ref()
7442                                                                                                                    ))
7443                                                                                                                });
7444                                                                                                        let cell =
7445                                                                                                            cx.container(
7446                                                                                                            ContainerProps {
7447                                                                                                                padding: Edges::symmetric(
7448                                                                                                                    cell_px,
7449                                                                                                                    cell_py,
7450                                                                                                                )
7451                                                                                                                .into(),
7452                                                                                                                layout:
7453                                                                                                                    table_fixed_column_clip_fill_layout(
7454                                                                                                                        col_w,
7455                                                                                                                    ),
7456                                                                                                                ..Default::default()
7457                                                                                                            },
7458                                                                                                            |cx| {
7459                                                                                                                let items =
7460                                                                                                                    render_cell(
7461                                                                                                                        cx,
7462                                                                                                                        &data_row,
7463                                                                                                                        col,
7464                                                                                                                    );
7465                                                                                                                let mut children =
7466                                                                                                                    collect_children(
7467                                                                                                                        cx, items,
7468                                                                                                                    );
7469                                                                                                                *hoisted_test_id_for_cell
7470                                                                                                                    .borrow_mut() =
7471                                                                                                                    table_wrapper_test_id(
7472                                                                                                                        &mut children,
7473                                                                                                                        explicit_test_id
7474                                                                                                                            .clone(),
7475                                                                                                                    );
7476                                                                                                                let content = ui::h_row(
7477                                                                                                                    move |_cx| {
7478                                                                                                                        children
7479                                                                                                                    },
7480                                                                                                                )
7481                                                                                                                .layout(
7482                                                                                                                    LayoutRefinement::default()
7483                                                                                                                        .w_full()
7484                                                                                                                        .h_full(),
7485                                                                                                                )
7486                                                                                                                .gap(Space::N0)
7487                                                                                                                .justify_start()
7488                                                                                                                .items_center()
7489                                                                                                                .into_element(cx);
7490                                                                                                                vec![content]
7491                                                                                                            },
7492                                                                                                        );
7493                                                                                                        if let Some(test_id) =
7494                                                                                                            hoisted_test_id.borrow_mut().take()
7495                                                                                                        {
7496                                                                                                            cell.test_id(test_id)
7497                                                                                                        } else {
7498                                                                                                            cell
7499                                                                                                        }
7500                                                                                                    })
7501                                                                                                    .collect::<Vec<_>>()
7502                                                                                        })
7503                                                                                        .gap(Space::N0)
7504                                                                                        .justify_start()
7505                                                                                        .items_center()
7506                                                                                        .into_element(cx)]
7507                                                                                    },
7508                                                                                );
7509
7510                                                                                vec![background_row, content_overlay]
7511                                                                            },
7512                                                                        )
7513                                                                    } else {
7514                                                                        ui::h_row(|cx| {
7515                                                                                    cols.iter()
7516                                                                                        .map(|col| {
7517                                                                                            let col_w = column_width_by_id
7518                                                                                                .get(&col.id)
7519                                                                                                .copied()
7520                                                                                                .unwrap_or(Px(col.size));
7521                                                                                            let hoisted_test_id =
7522                                                                                                Rc::new(RefCell::new(None));
7523                                                                                            let hoisted_test_id_for_cell =
7524                                                                                                hoisted_test_id.clone();
7525                                                                                            let explicit_test_id =
7526                                                                                                debug_row_test_id_prefix
7527                                                                                                    .as_ref()
7528                                                                                                    .map(|prefix| {
7529                                                                                                        Arc::<str>::from(format!(
7530                                                                                                            "{prefix}{row}-cell-{col}",
7531                                                                                                            row = data_row.key.0,
7532                                                                                                            col = col.id.as_ref()
7533                                                                                                        ))
7534                                                                                                    });
7535                                                                                            let cell = cx.container(
7536                                                                                                ContainerProps {
7537                                                                                                    padding: Edges::symmetric(
7538                                                                                                        cell_px, cell_py,
7539                                                                                                    )
7540                                                                                                    .into(),
7541                                                                                                    border: if props.optimize_grid_lines {
7542                                                                                                        Edges::default()
7543                                                                                                    } else {
7544                                                                                                        Edges {
7545                                                                                                            right: Px(1.0),
7546                                                                                                            ..Default::default()
7547                                                                                                        }
7548                                                                                                    },
7549                                                                                                    border_color: if props.optimize_grid_lines {
7550                                                                                                        None
7551                                                                                                    } else {
7552                                                                                                        Some(border)
7553                                                                                                    },
7554                                                                                                    layout:
7555                                                                                                        table_fixed_column_clip_fill_layout(
7556                                                                                                            col_w,
7557                                                                                                        ),
7558                                                                                                ..Default::default()
7559                                                                                            },
7560                                                                                            |cx| {
7561                                                                                                let items =
7562                                                                                                    render_cell(
7563                                                                                                        cx,
7564                                                                                                        &data_row,
7565                                                                                                        col,
7566                                                                                                    );
7567                                                                                                let mut children =
7568                                                                                                    collect_children(
7569                                                                                                        cx, items,
7570                                                                                                    );
7571                                                                                                *hoisted_test_id_for_cell
7572                                                                                                    .borrow_mut() =
7573                                                                                                    table_wrapper_test_id(
7574                                                                                                        &mut children,
7575                                                                                                        explicit_test_id
7576                                                                                                            .clone(),
7577                                                                                                    );
7578                                                                                                let content = ui::h_row(
7579                                                                                                    move |_cx| {
7580                                                                                                        children
7581                                                                                                    },
7582                                                                                                )
7583                                                                                                .layout(
7584                                                                                                    LayoutRefinement::default()
7585                                                                                                        .w_full()
7586                                                                                                        .h_full(),
7587                                                                                                )
7588                                                                                                .gap(Space::N0)
7589                                                                                                .justify_start()
7590                                                                                                .items_center()
7591                                                                                                .into_element(cx);
7592                                                                                                vec![content]
7593                                                                                            },
7594                                                                                        );
7595                                                                                            if let Some(test_id) =
7596                                                                                                hoisted_test_id.borrow_mut().take()
7597                                                                                            {
7598                                                                                                cell.test_id(test_id)
7599                                                                                            } else {
7600                                                                                                cell
7601                                                                                            }
7602                                                                                    })
7603                                                                                    .collect::<Vec<_>>()
7604                                                                        })
7605                                                                        .gap(Space::N0)
7606                                                                        .justify_start()
7607                                                                        .items_center()
7608                                                                        .into_element(cx)
7609                                                                    };
7610
7611                                                                    table_wrap_horizontal_scroll(
7612                                                                        cx, scroll_x, row,
7613                                                                    )
7614                                                                };
7615
7616                                                            out.push(ui::h_row(|cx| {
7617                                                                    let has_left = !left_cols.is_empty();
7618                                                                    let has_center = !center_cols.is_empty();
7619                                                                    let has_right = !right_cols.is_empty();
7620
7621                                                                    let divider_after_left = props.optimize_grid_lines
7622                                                                        && has_left
7623                                                                        && (has_center || has_right);
7624                                                                    let divider_after_center =
7625                                                                        props.optimize_grid_lines && has_center && has_right;
7626
7627                                                                    let left =
7628                                                                        render_row_group(cx, &left_cols, None);
7629                                                                    let left = if divider_after_left {
7630                                                                        cx.container(
7631                                                                            ContainerProps {
7632                                                                                border: Edges {
7633                                                                                    right: Px(1.0),
7634                                                                                    ..Default::default()
7635                                                                                },
7636                                                                                border_color: Some(border),
7637                                                                                layout: LayoutStyle {
7638                                                                                    size: fret_ui::element::SizeStyle {
7639                                                                                        height: Length::Fill,
7640                                                                                        ..Default::default()
7641                                                                                    },
7642                                                                                    ..Default::default()
7643                                                                                },
7644                                                                                ..Default::default()
7645                                                                            },
7646                                                                            move |_| vec![left],
7647                                                                        )
7648                                                                    } else {
7649                                                                        left
7650                                                                    };
7651
7652                                                                    let center = render_row_group(
7653                                                                        cx,
7654                                                                        &center_cols,
7655                                                                        Some(scroll_x.clone()),
7656                                                                    );
7657                                                                    let center = if divider_after_center {
7658                                                                        cx.container(
7659                                                                            ContainerProps {
7660                                                                                border: Edges {
7661                                                                                    right: Px(1.0),
7662                                                                                    ..Default::default()
7663                                                                                },
7664                                                                                border_color: Some(border),
7665                                                                                layout: LayoutStyle {
7666                                                                                    size: fret_ui::element::SizeStyle {
7667                                                                                        height: Length::Fill,
7668                                                                                        ..Default::default()
7669                                                                                    },
7670                                                                                    ..Default::default()
7671                                                                                },
7672                                                                                ..Default::default()
7673                                                                            },
7674                                                                            move |_| vec![center],
7675                                                                        )
7676                                                                    } else {
7677                                                                        center
7678                                                                    };
7679
7680                                                                    let right =
7681                                                                        render_row_group(cx, &right_cols, None);
7682                                                                    vec![left, center, right]
7683                                                            })
7684                                                            .gap(Space::N0)
7685                                                            .justify_start()
7686                                                            .items_stretch()
7687                                                            .into_element(cx));
7688                                                            out
7689                                                        },
7690                                                    )]
7691                                                },
7692                                            )
7693                                        },
7694                                    );
7695
7696                                    if profile {
7697                                        tracing::info!(
7698                                            "table_virtualized: list len={} page_rows={} rendered_rows={} row_h={:.1}px",
7699                                            data.len(),
7700                                            set_size,
7701                                            rendered_rows.get(),
7702                                            row_h.0
7703                                        );
7704                                    }
7705
7706                                    vec![header, body]
7707                        })
7708                    .h_full()
7709                    .into_element(cx)]
7710                },
7711            )]
7712        },
7713    );
7714
7715    if let Some(active_element) = active_element.get() {
7716        // The active row element is discovered while the table body mounts. Keep the relationship
7717        // declarative here and let the semantics pass resolve it against the final mounted node
7718        // once the current frame commits.
7719        list.attach_semantics(
7720            SemanticsDecoration::default().active_descendant_element(active_element.0),
7721        )
7722    } else {
7723        list
7724    }
7725}