sqlly-datatable 2.2.0

Configurable virtualized data grid component for the GPUI toolkit.
Documentation
#![allow(clippy::expect_used)]

use gpui::{point, px, Keystroke, TestAppContext};
use sqlly_datatable::{
    CellValue, Column, ColumnKind, GridData, MenuAction, Selection, SortDirection, SqllyDataTable,
};

fn grouped_data() -> GridData {
    GridData::new(
        vec![
            Column::new("region", ColumnKind::Text, 120.0),
            Column::new("amount", ColumnKind::Integer, 100.0),
        ],
        vec![
            vec![CellValue::Text("East".into()), CellValue::Integer(30)],
            vec![CellValue::Text("West".into()), CellValue::Integer(20)],
            vec![CellValue::Text("East".into()), CellValue::Integer(10)],
        ],
    )
    .expect("rectangular grouped data")
}

#[gpui::test]
fn grouping_builds_expandable_sections_and_survives_sorting(cx: &mut TestAppContext) {
    let (view, cx) = cx.add_window_view(|_window, cx| {
        SqllyDataTable::builder(grouped_data())
            .group_by_column(0)
            .build(cx)
    });
    cx.run_until_parked();

    view.update(cx, |table, cx| {
        table.state.update(cx, |state, _cx| {
            assert_eq!(state.grouped_column(), Some(0));
            assert_eq!(state.display_row_count(), 5);
            assert_eq!(state.row_groups().len(), 2);
            assert_eq!(state.row_groups()[0].label, "East");
            assert_eq!(state.row_groups()[0].row_count, 2);
            assert!(!state.row_groups()[0].collapsed);

            state.selection = Selection::Cell(1, 1);
            state.set_group_collapsed(0, true);
            assert!(state.row_groups()[0].collapsed);
            assert_eq!(state.display_row_count(), 3);
            assert_eq!(state.selection, Selection::None);

            state.selection = Selection::Cell(1, 0);
            state.sort = Some((1, SortDirection::Ascending));
            state.recompute();
            assert_eq!(state.selection, Selection::None);
            let east = state
                .row_groups()
                .iter()
                .find(|group| group.label == "East")
                .expect("East group remains present");
            assert!(east.collapsed);
            assert_eq!(east.row_count, 2);
        });
    });
}

#[gpui::test]
fn keyboard_and_public_selection_apis_skip_group_headers(cx: &mut TestAppContext) {
    let (view, cx) = cx.add_window_view(|_window, cx| {
        SqllyDataTable::builder(grouped_data())
            .group_by_column(0)
            .build(cx)
    });

    view.update(cx, |table, cx| {
        table.state.update(cx, |state, _cx| {
            state.selection = Selection::Cell(1, 0);
            state.handle_key(&Keystroke {
                key: "down".into(),
                ..Default::default()
            });
            assert_eq!(state.selection, Selection::Cell(2, 0));

            state.handle_key(&Keystroke {
                key: "down".into(),
                ..Default::default()
            });
            assert_eq!(state.selection, Selection::Cell(4, 0));

            state.selection = Selection::Column(0);
            assert_eq!(state.selected_cells(), vec![(1, 0), (2, 0), (4, 0)]);
        });
    });
}

#[gpui::test]
fn appending_while_grouped_rebuilds_sections(cx: &mut TestAppContext) {
    let (view, cx) = cx.add_window_view(|_window, cx| {
        SqllyDataTable::builder(grouped_data())
            .group_by_column(0)
            .build(cx)
    });

    view.update(cx, |table, cx| {
        table.state.update(cx, |state, _cx| {
            state
                .append_rows(vec![
                    vec![CellValue::Text("North".into()), CellValue::Integer(40)],
                    vec![CellValue::Text("East".into()), CellValue::Integer(50)],
                ])
                .expect("valid appended rows");

            assert_eq!(state.row_groups().len(), 3);
            assert_eq!(state.row_groups()[0].row_count, 3);
            assert_eq!(state.display_row_count(), 8);
        });
    });
}

#[gpui::test]
fn grouped_column_copy_omits_section_headers(cx: &mut TestAppContext) {
    let (view, cx) = cx.add_window_view(|_window, cx| {
        SqllyDataTable::builder(grouped_data())
            .group_by_column(0)
            .build(cx)
    });

    view.update(cx, |table, cx| {
        table.state.update(cx, |state, cx| {
            state.selection = Selection::Column(1);
            state.copy_selection(false, cx);
        });
    });
    let text = cx
        .update(|_, cx| cx.read_from_clipboard().and_then(|item| item.text()))
        .expect("clipboard has grouped column text");

    assert_eq!(
        text.lines().collect::<Vec<_>>(),
        ["30.00", "10.00", "20.00"]
    );
}

#[gpui::test]
fn group_header_click_toggles_and_menu_action_can_clear_grouping(cx: &mut TestAppContext) {
    let (view, cx) =
        cx.add_window_view(|_window, cx| SqllyDataTable::builder(grouped_data()).build(cx));
    cx.run_until_parked();

    view.update(cx, |table, cx| {
        table.state.update(cx, |state, cx| {
            state.execute_action(MenuAction::GroupBy, 0, cx);
            assert_eq!(state.grouped_column(), Some(0));

            let group_header = point(px(60.0), px(state.header_height + state.row_height * 0.5));
            state.handle_mouse_down(group_header, false);
            assert!(state.row_groups()[0].collapsed);

            state.execute_action(MenuAction::ClearGrouping, 0, cx);
            assert_eq!(state.grouped_column(), None);
            assert!(state.row_groups().is_empty());
            assert_eq!(state.display_row_count(), 3);
        });
    });
}

#[gpui::test]
fn grouping_is_disabled_for_windowed_rows(cx: &mut TestAppContext) {
    let (view, cx) =
        cx.add_window_view(|_window, cx| SqllyDataTable::builder(grouped_data()).build(cx));

    view.update(cx, |table, cx| {
        table.state.update(cx, |state, _cx| {
            let resident = state.data.rows[..2].to_vec();
            state
                .set_row_window(100, 40, resident)
                .expect("valid resident rows");
            state.set_grouped_column(Some(0));

            assert_eq!(state.grouped_column(), None);
            assert_eq!(state.display_row_count(), 100);
            assert_eq!(state.resident_row_for_display(40), Some(0));
        });
    });
}