@@ -54,6 +54,11 @@ pub mut:
5454 // table
5555 table_sort_by int
5656 table_border_style string = 'all'
57+ // data grid
58+ data_grid_query gui.GridQueryState
59+ data_grid_selection gui.GridSelection = gui.GridSelection{
60+ selected_row_ids: map [string ]bool {}
61+ }
5762 // radio
5863 select_radio bool
5964 // expand_pad
@@ -301,6 +306,13 @@ fn demo_entries() []DemoEntry {
301306 summary: 'Declarative and sortable table data'
302307 tags: ['rows' , 'columns' , 'csv' ]
303308 },
309+ DemoEntry{
310+ id: 'data_grid'
311+ label: 'Data Grid'
312+ group: 'data'
313+ summary: 'Controlled virtualized grid for interactive tabular data'
314+ tags: ['grid' , 'virtualized' , 'data' ]
315+ },
304316 DemoEntry{
305317 id: 'text'
306318 label: 'Text'
@@ -479,13 +491,23 @@ fn selected_entry(entries []DemoEntry, selected string) DemoEntry {
479491 return entries[0 ]
480492}
481493
494+ fn preferred_component_for_group (group string , entries []DemoEntry) string {
495+ if entries.len == 0 {
496+ return ''
497+ }
498+ if group == 'data' && has_entry (entries, 'data_grid' ) {
499+ return 'data_grid'
500+ }
501+ return entries[0 ].id
502+ }
503+
482504fn catalog_panel (mut w gui.Window) gui.View {
483505 mut app := w.state[ShowcaseApp]()
484506 entries := filtered_entries (app)
485507 if entries.len == 0 {
486508 app.selected_component = ''
487509 } else if ! has_entry (entries, app.selected_component) {
488- app.selected_component = entries[ 0 ].id
510+ app.selected_component = preferred_component_for_group (app.selected_group, entries)
489511 }
490512 return gui.column (
491513 width: 300
@@ -581,6 +603,10 @@ fn group_picker_item(label string, key string, app &ShowcaseApp) gui.View {
581603 on_click: fn [key] (_ voidptr , mut _ gui.Event, mut w gui.Window) {
582604 mut app := w.state[ShowcaseApp]()
583605 app.selected_group = key
606+ if key == 'data' {
607+ entries := filtered_entries (app)
608+ app.selected_component = preferred_component_for_group (key, entries)
609+ }
584610 w.scroll_vertical_to (id_scroll_catalog, 0 )
585611 w.update_window ()
586612 }
@@ -666,6 +692,14 @@ fn catalog_row(entry DemoEntry, app &ShowcaseApp) gui.View {
666692 )
667693}
668694
695+ fn detail_panel_padding () gui.Padding {
696+ base := gui.theme ().padding_large
697+ return gui.Padding{
698+ ...base
699+ right: base.right + gui.theme ().scrollbar_style.size + f32 (4 )
700+ }
701+ }
702+
669703fn detail_panel (mut w gui.Window) gui.View {
670704 mut app := w.state[ShowcaseApp]()
671705 entries := filtered_entries (app)
@@ -676,14 +710,14 @@ fn detail_panel(mut w gui.Window) gui.View {
676710 gap_edge: 4
677711 }
678712 sizing: gui.fill_fill
679- padding: gui. theme ().padding_large
713+ padding: detail_panel_padding ()
680714 content: [
681715 gui.text (text: 'No component matches filter' , text_style: gui.theme ().b2 ),
682716 ]
683717 )
684718 }
685719 if ! has_entry (entries, app.selected_component) {
686- app.selected_component = entries[ 0 ].id
720+ app.selected_component = preferred_component_for_group (app.selected_group, entries)
687721 }
688722 entry := selected_entry (entries, app.selected_component)
689723 mut content := []gui.View{}
@@ -701,7 +735,7 @@ fn detail_panel(mut w gui.Window) gui.View {
701735 gap_edge: 4
702736 }
703737 sizing: gui.fill_fill
704- padding: gui. theme ().padding_large
738+ padding: detail_panel_padding ()
705739 spacing: gui.spacing_large
706740 content: content
707741 )
@@ -728,6 +762,7 @@ fn component_demo(mut w gui.Window, id string) gui.View {
728762 'text' { demo_text () }
729763 'rtf' { demo_rtf () }
730764 'table' { demo_table (mut w) }
765+ 'data_grid' { demo_data_grid (mut w) }
731766 'date_picker' { demo_date_picker (mut w) }
732767 'input_date' { demo_input_date (mut w) }
733768 'date_picker_roller' { demo_date_picker_roller (mut w) }
@@ -769,6 +804,7 @@ fn related_examples(id string) string {
769804 'text' { 'examples/fonts.v, examples/system_font.v' }
770805 'rtf' { 'examples/rtf.v' }
771806 'table' { 'examples/table_demo.v' }
807+ 'data_grid' { 'examples/data_grid_demo.v, docs/DATA_GRID.md' }
772808 'date_picker' , 'input_date' { 'examples/date_picker_options.v, examples/date_time.v' }
773809 'date_picker_roller' { 'examples/date_picker_roller.v' }
774810 'svg' { 'examples/svg_demo.v, examples/tiger.v' }
@@ -1363,6 +1399,31 @@ fn markdown_preview(window &gui.Window) gui.View {
13631399| Table | Data | Ready |
13641400| Dialog | Overlay | Ready |'
13651401
1402+ const showcase_data_grid_features_source = '# Data Grid Features
1403+
1404+ - Virtual row rendering
1405+ - Single and multi-column sorting (shift-click)
1406+ - Per-column filter row + quick filter input
1407+ - Row selection: single, toggle, range
1408+ - Keyboard navigation + `ctrl/cmd+a`
1409+ - Header keyboard controls (sort/reorder/resize/pin/focus)
1410+ - Column resize drag + double-click auto-fit
1411+ - Controlled column reorder (`<` / `>` header controls)
1412+ - Controlled column pin cycle (`•` -> `↤` -> `↦`)
1413+ - Controlled column chooser (`show_column_chooser`, `hidden_column_ids`)
1414+ - Group headers (`group_by`) with optional aggregates
1415+ - Controlled master-detail rows
1416+ - Controlled row edit mode + typed cell editors (`text/select/date/checkbox`)
1417+ - Conditional cell formatting (`on_cell_format`)
1418+ - Controlled pagination (`page_size`, `page_index`)
1419+ - Controlled top frozen rows (`frozen_top_row_ids`)
1420+ - Optional frozen header row (`freeze_header`)
1421+ - Clipboard copy (`ctrl/cmd+c`) to TSV
1422+ - CSV import helper
1423+ - CSV helper export
1424+ - XLSX helper export
1425+ - PDF helper export'
1426+
13661427fn demo_button (mut w gui.Window) gui.View {
13671428 app := w.state[ShowcaseApp]()
13681429 return gui.column (
@@ -2359,6 +2420,114 @@ fn demo_rtf() gui.View {
23592420 )
23602421}
23612422
2423+ fn showcase_data_grid_columns () []gui.GridColumnCfg {
2424+ return [
2425+ gui.GridColumnCfg{
2426+ id: 'name'
2427+ title: 'Name'
2428+ width: 180
2429+ },
2430+ gui.GridColumnCfg{
2431+ id: 'team'
2432+ title: 'Team'
2433+ width: 140
2434+ },
2435+ gui.GridColumnCfg{
2436+ id: 'status'
2437+ title: 'Status'
2438+ width: 120
2439+ },
2440+ ]
2441+ }
2442+
2443+ fn showcase_data_grid_rows () []gui.GridRow {
2444+ return [
2445+ gui.GridRow{
2446+ id: '1'
2447+ cells: {
2448+ 'name' : 'Alex'
2449+ 'team' : 'Core'
2450+ 'status' : 'Active'
2451+ }
2452+ },
2453+ gui.GridRow{
2454+ id: '2'
2455+ cells: {
2456+ 'name' : 'Mina'
2457+ 'team' : 'Data'
2458+ 'status' : 'Active'
2459+ }
2460+ },
2461+ gui.GridRow{
2462+ id: '3'
2463+ cells: {
2464+ 'name' : 'Noah'
2465+ 'team' : 'Platform'
2466+ 'status' : 'Paused'
2467+ }
2468+ },
2469+ gui.GridRow{
2470+ id: '4'
2471+ cells: {
2472+ 'name' : 'Priya'
2473+ 'team' : 'Core'
2474+ 'status' : 'Active'
2475+ }
2476+ },
2477+ gui.GridRow{
2478+ id: '5'
2479+ cells: {
2480+ 'name' : 'Sam'
2481+ 'team' : 'Security'
2482+ 'status' : 'Offline'
2483+ }
2484+ },
2485+ ]
2486+ }
2487+
2488+ fn showcase_data_grid_apply_query (rows []gui.GridRow, query gui.GridQueryState) []gui.GridRow {
2489+ mut filtered := rows.filter (showcase_data_grid_row_matches_query (it , query))
2490+ for sort_idx in 0 .. query.sorts.len {
2491+ i := query.sorts.len - 1 - sort_idx
2492+ sort := query.sorts[i]
2493+ filtered.sort_with_compare (fn [sort] (a & gui.GridRow, b & gui.GridRow) int {
2494+ a_val := a.cells[sort.col_id] or { '' }
2495+ b_val := b.cells[sort.col_id] or { '' }
2496+ if a_val == b_val {
2497+ return 0
2498+ }
2499+ if sort.dir == .asc {
2500+ return if a_val < b_val { - 1 } else { 1 }
2501+ }
2502+ return if a_val > b_val { - 1 } else { 1 }
2503+ })
2504+ }
2505+ return filtered
2506+ }
2507+
2508+ fn showcase_data_grid_row_matches_query (row gui.GridRow, query gui.GridQueryState) bool {
2509+ if query.quick_filter.len > 0 {
2510+ needle := query.quick_filter.to_lower ()
2511+ mut any := false
2512+ for _, value in row.cells {
2513+ if value.to_lower ().contains (needle) {
2514+ any = true
2515+ break
2516+ }
2517+ }
2518+ if ! any {
2519+ return false
2520+ }
2521+ }
2522+ for filter in query.filters {
2523+ cell := row.cells[filter.col_id] or { '' }
2524+ if ! cell.to_lower ().contains (filter.value.to_lower ()) {
2525+ return false
2526+ }
2527+ }
2528+ return true
2529+ }
2530+
23622531fn showcase_table_rows () [][]string {
23632532 return [
23642533 ['Name' , 'Role' , 'Team' , 'City' ],
@@ -2473,6 +2642,48 @@ fn demo_table(mut w gui.Window) gui.View {
24732642 )
24742643}
24752644
2645+ fn demo_data_grid (mut w gui.Window) gui.View {
2646+ app := w.state[ShowcaseApp]()
2647+ rows := showcase_data_grid_apply_query (showcase_data_grid_rows (), app.data_grid_query)
2648+ return gui.column (
2649+ spacing: gui.theme ().spacing_small
2650+ content: [
2651+ gui.text (
2652+ text: 'Simple controlled grid. Sort, filter, and select rows.'
2653+ text_style: gui.theme ().n5
2654+ ),
2655+ gui.text (
2656+ text: 'Rows: ${rows .len } Selected: ${app .data_grid_selection .selected_row_ids .len }'
2657+ text_style: gui.theme ().n5
2658+ ),
2659+ w.data_grid (
2660+ id: 'catalog_data_grid'
2661+ id_focus: 9162
2662+ sizing: gui.fit_fit
2663+ columns: showcase_data_grid_columns ()
2664+ rows: rows
2665+ query: app.data_grid_query
2666+ selection: app.data_grid_selection
2667+ max_height: 260
2668+ on_query_change: fn (query gui.GridQueryState, mut _ gui.Event, mut w gui.Window) {
2669+ mut app := w.state[ShowcaseApp]()
2670+ app.data_grid_query = query
2671+ }
2672+ on_selection_change: fn (selection gui.GridSelection, mut _ gui.Event, mut w gui.Window) {
2673+ mut app := w.state[ShowcaseApp]()
2674+ app.data_grid_selection = selection
2675+ }
2676+ ),
2677+ w.markdown (
2678+ id: 'catalog_data_grid_features'
2679+ source: showcase_data_grid_features_source
2680+ mode: .wrap
2681+ padding: gui.padding_none
2682+ ),
2683+ ]
2684+ )
2685+ }
2686+
24762687fn demo_date_picker (mut w gui.Window) gui.View {
24772688 app := w.state[ShowcaseApp]()
24782689 return w.date_picker (
0 commit comments