1use std::cell::{Cell, RefCell};
5use std::collections::{BTreeMap, BTreeSet};
6use std::fmt;
7use std::rc::Rc;
8
9use ravenrs_core::{Color, CursorIcon, Insets, Key, Point, Quad, Rect, Role};
10use ravenrs_layout::{Align, Axis, Justify, LayoutStyle};
11use ravenrs_reactive::{Binding, State};
12use ravenrs_style::{Semantic, TextStyle, Theme, Weight};
13
14use super::layout::{self, Arrangement, GridLayout, GridScenario};
15use super::model::{CellAlign, GridColumn, GridData, Row, SortDirection, Summary, Value};
16use super::query::{
17 GridExportFormat, GridQuery, GridTable, QueryResult, ViewRow, check_formula, formula_value, group_ranges,
18 run_query, to_csv, to_table, to_tsv,
19};
20use super::remote::RemoteRows;
21use crate::context::{BuildCx, DragPhase, NodeId, Paint, Placement, QuadPaint, Semantics};
22use crate::overlay::{Entry, MenuList, PanelOf};
23use crate::{
24 AnyView, Badge, Button, ButtonSize, ButtonStyle, Dialog, DropdownMenu, IconName, MenuItem, Pagination,
25 Select, Spacer, TextField, View, util,
26};
27
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
30#[non_exhaustive]
31#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
32pub enum SelectionMode {
33 None,
35 #[default]
37 Single,
38 Multiple,
40}
41
42#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
68pub struct DataGrid {
69 empty_text: Option<String>,
70 data: GridData,
71 columns: Vec<GridColumn>,
72 selection_mode: SelectionMode,
73 selection: Option<Binding<BTreeSet<usize>>>,
74 initial: GridQuery,
75 filter_row: bool,
76 search_panel: bool,
77 toolbar: bool,
78 footer: bool,
79 height: Option<f32>,
80 row_height: f32,
81 striped: bool,
82 even_row: Option<Color>,
83 odd_row: Option<Color>,
84 horizontal_lines: bool,
85 on_export: Option<Rc<dyn Fn(String)>>,
86 on_row_activate: Option<Rc<dyn Fn(usize)>>,
87 remote: Option<RemoteRows>,
88 group_panel: Option<bool>,
89 page_size: Option<usize>,
90 vertical_lines: bool,
91 layout: Option<Binding<GridLayout>>,
92 scenarios: Option<Binding<Vec<GridScenario>>>,
93 key_column: Option<usize>,
94 selected_keys: Option<Binding<BTreeSet<String>>>,
95 on_cell_edit: Option<CellEditHandler>,
96 row_actions: Vec<GridRowAction>,
97 on_add_row: Option<Rc<dyn Fn()>>,
98 on_delete_rows: Option<Rc<dyn Fn(Vec<usize>)>>,
99 row_detail: Option<RowDetail>,
100 detail_height: f32,
101 expanded_rows: Option<Binding<BTreeSet<usize>>>,
102 on_export_file: Option<Rc<dyn Fn(GridExportFormat, GridTable)>>,
103 export_title: Option<String>,
104 filter_builder: bool,
105 cell_style: Option<CellStyler>,
106 name_column: Option<usize>,
107}
108
109type CellStyler = Rc<dyn Fn(&Row, usize) -> GridCellStyle>;
110type RowDetail = Rc<dyn Fn(&Row, usize) -> AnyView>;
111type CellEditHandler = Rc<dyn Fn(&CellEdit) -> Result<(), String>>;
112type RowTest = Rc<dyn Fn(&Row) -> bool>;
113
114const STRIPED_ROW: f32 = 0.4;
117const HOVERED_ROW: f32 = 0.85;
119const SELECTED_ROW: f32 = 0.13;
121const ROW_ACTION_WIDTH: f32 = 34.0;
122const DETAIL_ARROW_WIDTH: f32 = 32.0;
124const CHECK_WIDTH: f32 = 44.0;
125
126#[derive(Clone, Debug, Default, PartialEq)]
129struct RowTops {
130 row_height: f32,
131 detail_height: f32,
132 open: Vec<usize>,
134}
135
136#[cfg_attr(feature = "live", ravenrs_live::expose)]
137impl RowTops {
138 fn top(&self, row: usize) -> f32 {
139 row as f32 * self.row_height + self.open.partition_point(|&o| o < row) as f32 * self.detail_height
140 }
141
142 fn row_at(&self, y: f32, len: usize) -> usize {
144 let (mut low, mut high) = (0, len);
145 while low < high {
146 let middle = (low + high) / 2;
147 if self.top(middle + 1) <= y {
148 low = middle + 1;
149 } else {
150 high = middle;
151 }
152 }
153 low
154 }
155}
156
157#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
159#[non_exhaustive]
160#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
161pub struct GridCellStyle {
162 pub ink: Option<Semantic>,
164 pub fill: Option<Semantic>,
166 pub bold: bool,
168}
169
170#[cfg_attr(feature = "live", ravenrs_live::expose)]
171impl GridCellStyle {
172 pub fn new(ink: Option<Semantic>, fill: Option<Semantic>, bold: bool) -> Self {
174 Self { ink, fill, bold }
175 }
176}
177
178#[derive(Clone, Debug, PartialEq)]
180#[non_exhaustive]
181#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
182pub struct CellEdit {
183 pub row: usize,
185 pub column: usize,
187 pub previous: Value,
189 pub value: Value,
191}
192
193#[derive(Clone)]
195#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
196pub struct GridRowAction {
197 label: String,
198 icon: IconName,
199 on_click: Rc<dyn Fn(&Row)>,
200 enabled: Option<RowTest>,
201}
202
203impl fmt::Debug for GridRowAction {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 f.debug_struct("GridRowAction").field("label", &self.label).finish_non_exhaustive()
206 }
207}
208
209#[cfg_attr(feature = "live", ravenrs_live::expose)]
210impl GridRowAction {
211 pub fn new(label: impl Into<String>, icon: IconName, on_click: impl Fn(&Row) + 'static) -> Self {
215 Self { label: label.into(), icon, on_click: Rc::new(on_click), enabled: None }
216 }
217
218 pub fn enabled(mut self, enabled: impl Fn(&Row) -> bool + 'static) -> Self {
220 self.enabled = Some(Rc::new(enabled));
221 self
222 }
223}
224
225impl fmt::Debug for DataGrid {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 f.debug_struct("DataGrid")
228 .field("columns", &self.columns)
229 .field("rows", &self.data.len())
230 .finish_non_exhaustive()
231 }
232}
233
234#[cfg_attr(feature = "live", ravenrs_live::expose)]
235impl DataGrid {
236 pub fn empty_text(mut self, text: impl Into<String>) -> Self {
238 self.empty_text = Some(text.into());
239 self
240 }
241
242 pub fn new(data: GridData, columns: impl IntoIterator<Item = GridColumn>) -> Self {
244 Self {
245 empty_text: None,
246 data,
247 columns: columns.into_iter().collect(),
248 selection_mode: SelectionMode::Single,
249 selection: None,
250 initial: GridQuery::default(),
251 filter_row: true,
252 search_panel: true,
253 toolbar: true,
254 footer: true,
255 height: None,
256 row_height: 36.0,
257 striped: false,
258 even_row: None,
259 odd_row: None,
260 horizontal_lines: true,
261 on_export: None,
262 on_row_activate: None,
263 remote: None,
264 group_panel: None,
265 page_size: None,
266 vertical_lines: false,
267 layout: None,
268 scenarios: None,
269 key_column: None,
270 selected_keys: None,
271 on_cell_edit: None,
272 row_actions: Vec::new(),
273 on_add_row: None,
274 on_delete_rows: None,
275 row_detail: None,
276 detail_height: 240.0,
277 expanded_rows: None,
278 on_export_file: None,
279 export_title: None,
280 filter_builder: false,
281 cell_style: None,
282 name_column: None,
283 }
284 }
285
286 pub fn name_column(mut self, column: usize) -> Self {
289 self.name_column = Some(column);
290 self
291 }
292
293 pub fn cell_style(mut self, style: impl Fn(&Row, usize) -> GridCellStyle + 'static) -> Self {
295 self.cell_style = Some(Rc::new(style));
296 self
297 }
298
299 pub fn filter_builder(mut self, show: bool) -> Self {
303 self.filter_builder = show;
304 self
305 }
306
307 pub fn row_detail(mut self, detail: impl Fn(&Row, usize) -> AnyView + 'static) -> Self {
311 self.row_detail = Some(Rc::new(detail));
312 self
313 }
314
315 pub fn detail_height(mut self, height: f32) -> Self {
317 self.detail_height = height.max(1.0);
318 self
319 }
320
321 pub fn expanded_rows(mut self, rows: Binding<BTreeSet<usize>>) -> Self {
323 self.expanded_rows = Some(rows);
324 self
325 }
326
327 pub fn on_export_file(mut self, handler: impl Fn(GridExportFormat, GridTable) + 'static) -> Self {
330 self.on_export_file = Some(Rc::new(handler));
331 self
332 }
333
334 pub fn export_title(mut self, title: impl Into<String>) -> Self {
336 self.export_title = Some(title.into());
337 self
338 }
339
340 pub fn remote(rows: RemoteRows, columns: impl IntoIterator<Item = GridColumn>) -> Self {
345 let mut grid = Self::new(GridData::new(Vec::new()), columns);
346 grid.remote = Some(rows);
347 grid.group_panel = Some(false);
348 grid
349 }
350
351 pub fn page_size(mut self, rows: usize) -> Self {
353 self.page_size = Some(rows.max(1));
354 self
355 }
356
357 pub fn group_panel(mut self, show: bool) -> Self {
360 self.group_panel = Some(show);
361 self
362 }
363
364 pub fn vertical_lines(mut self, show: bool) -> Self {
366 self.vertical_lines = show;
367 self
368 }
369
370 pub fn layout(mut self, layout: Binding<GridLayout>) -> Self {
373 self.layout = Some(layout);
374 self
375 }
376
377 pub fn scenarios(mut self, scenarios: Binding<Vec<GridScenario>>) -> Self {
380 self.scenarios = Some(scenarios);
381 self
382 }
383
384 pub fn selection_mode(mut self, mode: SelectionMode) -> Self {
386 self.selection_mode = mode;
387 self
388 }
389
390 pub fn selection(mut self, selection: Binding<BTreeSet<usize>>) -> Self {
392 self.selection = Some(selection);
393 self
394 }
395
396 pub fn key_column(mut self, column: usize) -> Self {
399 self.key_column = Some(column);
400 self
401 }
402
403 pub fn selected_keys(mut self, keys: Binding<BTreeSet<String>>) -> Self {
407 self.selected_keys = Some(keys);
408 self
409 }
410
411 pub fn on_row_open(mut self, handler: impl Fn(&Row) + 'static) -> Self {
414 let (data, remote) = (self.data.clone(), self.remote.clone());
415 self.on_row_activate = Some(Rc::new(move |index| {
416 let row = match &remote {
417 Some(remote) => remote.row(index),
418 None => data.rows().get(index).cloned(),
419 };
420 if let Some(row) = row {
421 handler(&row);
422 }
423 }));
424 self
425 }
426
427 pub fn on_cell_edit(mut self, handler: impl Fn(&CellEdit) -> Result<(), String> + 'static) -> Self {
431 self.on_cell_edit = Some(Rc::new(handler));
432 self
433 }
434
435 pub fn row_action(mut self, action: GridRowAction) -> Self {
437 self.row_actions.push(action);
438 self
439 }
440
441 pub fn on_add_row(mut self, handler: impl Fn() + 'static) -> Self {
443 self.on_add_row = Some(Rc::new(handler));
444 self
445 }
446
447 pub fn on_delete_rows(mut self, handler: impl Fn(Vec<usize>) + 'static) -> Self {
450 self.on_delete_rows = Some(Rc::new(handler));
451 self
452 }
453
454 pub fn sort_by(mut self, column: usize, direction: SortDirection) -> Self {
456 self.initial.sort.push((column, direction));
457 self
458 }
459
460 pub fn group_by(mut self, column: usize) -> Self {
462 self.initial.group_by.push(column);
463 self
464 }
465
466 pub fn filter_row(mut self, show: bool) -> Self {
468 self.filter_row = show;
469 self
470 }
471
472 pub fn search_panel(mut self, show: bool) -> Self {
474 self.search_panel = show;
475 self
476 }
477
478 pub fn toolbar(mut self, show: bool) -> Self {
480 self.toolbar = show;
481 self
482 }
483
484 pub fn footer(mut self, show: bool) -> Self {
486 self.footer = show;
487 self
488 }
489
490 pub fn height(mut self, height: f32) -> Self {
492 self.height = Some(height);
493 self
494 }
495
496 pub fn row_height(mut self, height: f32) -> Self {
498 self.row_height = height.max(24.0);
499 self
500 }
501
502 pub fn striped(mut self, striped: bool) -> Self {
505 self.striped = striped;
506 self
507 }
508
509 pub fn even_row(mut self, color: Color) -> Self {
511 self.even_row = Some(color);
512 self
513 }
514
515 pub fn odd_row(mut self, color: Color) -> Self {
517 self.odd_row = Some(color);
518 self
519 }
520
521 pub fn horizontal_lines(mut self, show: bool) -> Self {
523 self.horizontal_lines = show;
524 self
525 }
526
527 pub fn on_export(mut self, handler: impl Fn(String) + 'static) -> Self {
529 self.on_export = Some(Rc::new(handler));
530 self
531 }
532
533 pub fn on_row_activate(mut self, handler: impl Fn(usize) + 'static) -> Self {
535 self.on_row_activate = Some(Rc::new(handler));
536 self
537 }
538}
539
540struct Cached {
541 summaries: Vec<Option<Summary>>,
542 query: GridQuery,
543 rows: Rc<Vec<Row>>,
544 result: Rc<QueryResult>,
545}
546
547#[derive(Clone)]
551struct Handles {
552 building_filter: State<bool>,
553 expanded: Binding<BTreeSet<usize>>,
554 query: State<GridQuery>,
555 widths: State<Vec<f32>>,
556 selection: Binding<BTreeSet<usize>>,
557 focus_row: State<Option<usize>>,
558 anchor: Rc<Cell<Option<usize>>>,
559 reveal: Rc<Cell<Option<usize>>>,
560 editing: State<Option<(usize, usize)>>,
561 edit_buffer: State<String>,
562 edit_error: State<Option<String>>,
563 resize: Rc<Cell<Option<(usize, f32, f32)>>>,
564 modifiers: Rc<Cell<ravenrs_core::Modifiers>>,
565 arrange: State<Arrangement>,
566 drag: State<Option<GridDrag>>,
567 press: Rc<Cell<(Point, bool)>>,
569 dragged: Rc<Cell<bool>>,
571 zones: Rc<RefCell<Zones>>,
572 menu: State<Option<(usize, Option<Point>)>>,
573 filter_open: State<Option<usize>>,
574 filter_chosen: State<BTreeSet<String>>,
575 filter_search: State<String>,
576 page: State<usize>,
577 saving: State<bool>,
578 scenario_name: State<String>,
579 active_scenario: State<Option<String>>,
580 original: Rc<RefCell<Option<GridLayout>>>,
582 seen_layout: Rc<RefCell<Option<GridLayout>>>,
583 summary_edit: State<Option<usize>>,
585 summary_draft: State<String>,
586 formula_cache: Rc<RefCell<FormulaCache>>,
588}
589
590#[derive(Default)]
591struct FormulaCache {
592 about: (usize, Vec<Option<String>>),
594 answers: BTreeMap<(usize, usize, usize), String>,
595}
596
597struct Shared<'a> {
599 grid: &'a DataGrid,
600 rows: &'a [Row],
601 rows_rc: Rc<Vec<Row>>,
602 result: Rc<QueryResult>,
603 visible: &'a [usize],
604 widths: &'a [f32],
605 total_width: f32,
606 select_width: f32,
607 selected: &'a BTreeSet<usize>,
608 focus_row: Option<usize>,
609 editing: Option<(usize, usize)>,
610 h: Handles,
611 arrangement: &'a Arrangement,
612 fixed: Vec<(usize, f32)>,
614 last_fixed: Option<usize>,
615 fixed_width: f32,
617 offset_x: f32,
618 indent: f32,
620 column_facts: Rc<Vec<(String, bool)>>,
622 drop_before: Option<Option<usize>>,
624 lines: Rc<Vec<f32>>,
626 ranges: Rc<Vec<(usize, usize)>>,
628}
629
630#[cfg_attr(feature = "live", ravenrs_live::expose)]
631impl Shared<'_> {
632 fn fixed_x(&self, column: usize) -> Option<f32> {
633 self.fixed.iter().find(|(c, _)| *c == column).map(|(_, x)| *x)
634 }
635
636 fn formula_text(&self, c: usize, range: (usize, usize)) -> Option<String> {
638 let formula = self.arrangement.formulas.get(c)?.as_ref()?;
639 let mut cache = self.h.formula_cache.borrow_mut();
640 let key = (c, range.0, range.1);
641 if let Some(known) = cache.answers.get(&key) {
642 return Some(known.clone());
643 }
644 let columns = &self.grid.columns;
645 let end = (range.0 + range.1).min(self.result.matched.len());
646 let indices = &self.result.matched[range.0.min(end)..end];
647 let answer = check_formula(columns, formula)
648 .and_then(|parsed| formula_value(columns, self.rows, indices, &parsed))
649 .map_or_else(|_| "#!".to_owned(), |value| formula_display(&columns[c], &value));
650 cache.answers.insert(key, answer.clone());
651 Some(answer)
652 }
653
654 fn x_of(&self, column: usize) -> f32 {
656 let mut x = self.select_width;
657 for &c in self.visible {
658 if c == column {
659 return x;
660 }
661 x += self.widths[c];
662 }
663 x
664 }
665}
666
667fn cell_attrs(theme: &Theme) -> ravenrs_text::TextAttrs {
668 util::attrs(theme, TextStyle::Callout, None)
669}
670
671fn justify(align: CellAlign) -> Justify {
672 match align {
673 CellAlign::Start => Justify::Start,
674 CellAlign::Center => Justify::Center,
675 CellAlign::End => Justify::End,
676 }
677}
678
679fn cell_style(width: f32, height: f32, align: CellAlign) -> LayoutStyle {
681 LayoutStyle {
682 width: Some(width),
683 height: Some(height),
684 padding: Insets::symmetric(0.0, 12.0),
685 spacing: 6.0,
686 align: Align::Center,
687 justify: justify(align),
688 overflow_hidden: true,
689 ..LayoutStyle::stack(Axis::Horizontal)
690 }
691}
692
693fn click_row(shared_result: &QueryResult, h: &Handles, mode: SelectionMode, row: usize, index: usize) {
695 let m = h.modifiers.get();
696 h.focus_row.set(Some(index));
697 match mode {
698 SelectionMode::None => {}
699 SelectionMode::Single => h.selection.set(BTreeSet::from([row])),
700 SelectionMode::Multiple => {
701 if m.shift
702 && let Some(anchor) = h.anchor.get()
703 {
704 let (a, b) = if anchor <= index { (anchor, index) } else { (index, anchor) };
705 let range = shared_result.rows[a..=b.min(shared_result.rows.len() - 1)]
706 .iter()
707 .filter_map(|r| if let ViewRow::Data(i) = r { Some(*i) } else { None })
708 .collect();
709 h.selection.set(range);
710 return;
711 } else if m.ctrl || m.meta {
712 h.selection.update(|s| {
713 if !s.remove(&row) {
714 s.insert(row);
715 }
716 });
717 } else {
718 h.selection.set(BTreeSet::from([row]));
719 }
720 }
721 }
722 h.anchor.set(Some(index));
723}
724
725#[derive(Clone, Copy, Debug, PartialEq, Eq)]
727enum DragSource {
728 Column(usize),
730 Chip(usize),
732}
733
734#[derive(Clone, Copy, Debug, PartialEq, Eq)]
736enum DropTarget {
737 Nothing,
738 Group(usize),
740 Column(Option<usize>),
742 Ungroup,
744}
745
746#[derive(Clone, Copy, Debug, PartialEq)]
747struct GridDrag {
748 source: DragSource,
749 at: Point,
750 target: DropTarget,
751}
752
753#[derive(Default)]
755struct Zones {
756 panel: Rect,
757 header: Rect,
758 chips: BTreeMap<usize, Rect>,
759 cells: BTreeMap<usize, Rect>,
760}
761
762const DRAG_FROM: f32 = 5.0;
764const HEADER_HEIGHT: f32 = 38.0;
765const FOOTER_HEIGHT: f32 = 40.0;
766const FOOTER_NAME_FROM: f32 = 170.0;
767const SUMMARY_EDITOR_WIDTH: f32 = 470.0;
768const GROUP_INDENT: f32 = 20.0;
769const GROUP_PANEL_HEIGHT: f32 = 44.0;
770const MAX_FILTER_VALUES: usize = 500;
772const CHAR_WIDTH: f32 = 7.2;
774const PAGE_SIZES: [usize; 5] = [25, 50, 100, 200, 500];
775
776fn drag_handler(
779 h: &Handles,
780 columns: &Rc<Vec<(String, bool)>>,
781 source: DragSource,
782) -> Rc<dyn Fn(crate::Drag)> {
783 let (h, columns) = (h.clone(), columns.clone());
784 Rc::new(move |d| match d.phase {
785 DragPhase::Start => {
786 h.press.set((d.position, false));
787 h.dragged.set(false);
788 }
789 DragPhase::Move => {
790 let (start, active) = h.press.get();
791 if !active {
792 let (dx, dy) = (d.position.x - start.x, d.position.y - start.y);
793 if dx.hypot(dy) < DRAG_FROM {
794 return;
795 }
796 h.press.set((start, true));
797 h.dragged.set(true);
798 h.drag.set(Some(GridDrag { source, at: d.position, target: DropTarget::Nothing }));
799 }
800 if h.drag.peek().is_none() {
802 return;
803 }
804 let target = drop_target(&h, &columns, source, d.position);
805 h.drag.set(Some(GridDrag { source, at: d.position, target }));
806 }
807 _ => {
808 let (_, active) = h.press.get();
809 h.press.set((d.position, false));
810 let drag = *h.drag.peek();
811 if active && let Some(drag) = drag {
812 h.dragged.set(true);
813 h.drag.set(None);
814 finish_drag(&h, drag);
815 }
816 }
817 })
818}
819
820fn drop_target(h: &Handles, columns: &[(String, bool)], source: DragSource, at: Point) -> DropTarget {
821 let zones = h.zones.borrow();
822 if zones.panel.contains(at) {
823 if let DragSource::Column(c) = source
824 && !columns.get(c).is_some_and(|col| col.1)
825 {
826 return DropTarget::Nothing;
827 }
828 let mut place = 0;
829 let moving = if let DragSource::Chip(c) = source { Some(c) } else { None };
830 for (i, &group) in h.query.peek().group_by.iter().enumerate() {
831 if Some(group) == moving {
832 continue;
833 }
834 let past = zones.chips.get(&i).is_some_and(|chip| at.x > chip.center().x);
835 if past {
836 place += 1;
837 }
838 }
839 return DropTarget::Group(place);
840 }
841 if zones.header.contains(at) {
842 let before = zones
843 .cells
844 .iter()
845 .filter(|(_, r)| at.x < r.center().x)
846 .min_by(|a, b| a.1.x().partial_cmp(&b.1.x()).unwrap_or(std::cmp::Ordering::Equal));
847 return DropTarget::Column(before.map(|(c, _)| *c));
848 }
849 match source {
850 DragSource::Chip(_) => DropTarget::Ungroup,
851 DragSource::Column(_) => DropTarget::Nothing,
852 }
853}
854
855fn finish_drag(h: &Handles, drag: GridDrag) {
856 let column = match drag.source {
857 DragSource::Column(c) | DragSource::Chip(c) => c,
858 };
859 match drag.target {
860 DropTarget::Nothing => {}
861 DropTarget::Group(place) => h.query.update(|q| {
862 q.group_by.retain(|g| *g != column);
863 let place = place.min(q.group_by.len());
864 q.group_by.insert(place, column);
865 q.collapsed.clear();
866 }),
867 DropTarget::Column(before) => {
868 if matches!(drag.source, DragSource::Chip(_)) {
869 h.query.update(|q| q.toggle_group(column));
870 }
871 h.arrange.update(|a| a.move_column(column, before));
872 }
873 DropTarget::Ungroup => h.query.update(|q| q.toggle_group(column)),
874 }
875}
876
877struct ResizeHandle<'a> {
879 column: usize,
880 shared: &'a Shared<'a>,
881}
882
883impl View for ResizeHandle<'_> {
884 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
885 let theme = *cx.theme();
886 let (s, c) = (self.shared, self.column);
887 let col = &s.grid.columns[c];
888 let shown = cx.is_hovered() || s.h.resize.get().is_some_and(|(column, ..)| column == c);
889 let line = theme.color(Semantic::Accent);
890 let handle = cx.container(
891 LayoutStyle { width: Some(6.0), height: Some(HEADER_HEIGHT), ..LayoutStyle::leaf() },
892 &[],
893 Paint::Custom(Rc::new(move |f: Rect, scene| {
894 if shown {
895 scene.push_quad(Quad::new(
896 Rect::new(f.center().x - 1.0, f.y() + 6.0, 2.0, f.height() - 12.0),
897 line,
898 ));
899 }
900 })),
901 );
902 let (widths, resize, min) = (s.h.widths.clone(), s.h.resize.clone(), col.min_width);
903 cx.on_drag(
904 handle,
905 Rc::new(move |d| match d.phase {
906 DragPhase::Start => {
907 let w = widths.peek().get(c).copied().unwrap_or(120.0);
908 resize.set(Some((c, d.position.x, w)));
909 }
910 DragPhase::Move => {
911 if let Some((col, start_x, start_w)) = resize.get()
912 && col == c
913 {
914 let w = (start_w + d.position.x - start_x).max(min);
915 widths.update(|ws| ws[c] = w);
916 }
917 }
918 _ => resize.set(None),
919 }),
920 );
921 let (widths, width) = (s.h.widths.clone(), fit_width(s, c));
922 cx.on_double_click(handle, Rc::new(move || widths.update(|ws| ws[c] = width)));
923 cx.cursor(handle, CursorIcon::ColResize);
924 handle
925 }
926}
927
928fn fit_width(s: &Shared<'_>, c: usize) -> f32 {
930 let col = &s.grid.columns[c];
931 let empty = Value::Empty;
932 let longest = s
933 .rows
934 .iter()
935 .take(2000)
936 .map(|row| col.display_cow(row.get(c).unwrap_or(&empty)).chars().count())
937 .max()
938 .unwrap_or(0)
939 .max(col.title.chars().count() + 4);
940 (longest as f32 * CHAR_WIDTH + 32.0).max(col.min_width)
941}
942
943fn column_menu(s: &Shared<'_>, c: usize, labels: &crate::Labels) -> Vec<Entry> {
945 let (col, query, h) = (&s.grid.columns[c], s.h.query.peek().clone(), &s.h);
946 let mut entries = Vec::new();
947 let item = |text: &str, icon: IconName| MenuItem::new(text.to_owned()).icon(icon);
948 if col.sortable {
949 for (text, icon, direction) in [
950 (&labels.grid_sort_ascending, IconName::ArrowUp, SortDirection::Ascending),
951 (&labels.grid_sort_descending, IconName::ArrowDown, SortDirection::Descending),
952 ] {
953 let q = h.query.clone();
954 entries.push(Entry::Item(item(text, icon).on_select(move || {
955 q.update(|q| {
956 q.sort.retain(|(sorted, _)| *sorted != c);
957 q.sort.insert(0, (c, direction));
958 });
959 })));
960 }
961 if query.sort.iter().any(|(sorted, _)| *sorted == c) {
962 let q = h.query.clone();
963 entries.push(Entry::Item(
964 item(&labels.grid_clear_sort, IconName::X)
965 .on_select(move || q.update(|q| q.sort.retain(|(sorted, _)| *sorted != c))),
966 ));
967 }
968 entries.push(Entry::Separator);
969 }
970 if col.groupable && s.grid.remote.is_none() {
971 let q = h.query.clone();
972 let grouped = query.group_by.contains(&c);
973 let text = if grouped { &labels.grid_ungroup_column } else { &labels.grid_group_by_column };
974 entries.push(Entry::Item(
975 item(text, IconName::Layers).on_select(move || q.update(|q| q.toggle_group(c))),
976 ));
977 }
978 let arrange = s.arrangement;
979 let pinned = arrange.pinned.get(c).copied().unwrap_or(false);
980 let a = h.arrange.clone();
981 entries.push(Entry::Item(
982 item(
983 if pinned { &labels.grid_unpin_column } else { &labels.grid_pin_column },
984 if pinned { IconName::PinOff } else { IconName::Pin },
985 )
986 .on_select(move || a.update(|a| a.pinned[c] = !pinned)),
987 ));
988 if s.grid.remote.is_none() {
989 let edit = h.summary_edit.clone();
990 let (draft, start) = (h.summary_draft.clone(), first_draft(s, c));
991 entries.push(Entry::Item(item(&labels.grid_edit_summary, IconName::Sigma).on_select(move || {
992 draft.set(start.clone());
993 edit.set(Some(c));
994 })));
995 }
996 if s.grid.columns[c].resizable {
997 let (widths, width) = (h.widths.clone(), fit_width(s, c));
998 entries.push(Entry::Item(
999 item(&labels.grid_fit_column, IconName::Columns3)
1000 .on_select(move || widths.update(|ws| ws[c] = width)),
1001 ));
1002 }
1003 let q = h.query.clone();
1004 entries.push(Entry::Item(item(&labels.grid_hide_column, IconName::EyeOff).on_select(move || {
1005 q.update(|q| {
1006 if q.hidden.len() <= c {
1007 q.hidden.resize(c + 1, false);
1008 }
1009 q.hidden[c] = true;
1010 });
1011 })));
1012 if !query.filter(c).trim().is_empty() {
1013 let q = h.query.clone();
1014 entries.push(Entry::Item(
1015 item(&labels.grid_clear_filter, IconName::Filter)
1016 .on_select(move || q.update(|q| q.set_filter(c, String::new()))),
1017 ));
1018 }
1019 if s.grid.remote.is_none() {
1020 entries.push(Entry::Separator);
1021 let a = h.arrange.clone();
1022 let showing = arrange.group_panel;
1023 entries.push(Entry::Item(
1024 item(if showing { &labels.grid_hide_panel } else { &labels.grid_show_panel }, IconName::Rows3)
1025 .on_select(move || a.update(|a| a.group_panel = !showing)),
1026 ));
1027 }
1028 entries
1029}
1030
1031fn formula_display(col: &GridColumn, value: &ravenrs_criteria::Value) -> String {
1033 use ravenrs_criteria::Value as Field;
1034 match value {
1035 Field::Number(n) => col.display_summary(Summary::Sum, *n),
1036 Field::Text(text) => text.clone(),
1037 Field::Bool(b) => b.to_string(),
1038 Field::Null => "โ".to_owned(),
1039 other => format!("{other:?}"),
1040 }
1041}
1042
1043fn first_draft(s: &Shared<'_>, c: usize) -> String {
1045 s.arrangement.formulas.get(c).cloned().flatten().unwrap_or_else(|| {
1046 let title = &s.grid.columns[c].title;
1047 match s.arrangement.summaries.get(c).copied().flatten() {
1048 Some(Summary::Count) => "Count()".to_owned(),
1049 Some(Summary::Average) => format!("Avg([{title}])"),
1050 Some(Summary::Min) => format!("Min([{title}])"),
1051 Some(Summary::Max) => format!("Max([{title}])"),
1052 _ => format!("Sum([{title}])"),
1053 }
1054 })
1055}
1056
1057const SUMMARY_KINDS: [Option<Summary>; 6] = [
1059 None,
1060 Some(Summary::Sum),
1061 Some(Summary::Average),
1062 Some(Summary::Min),
1063 Some(Summary::Max),
1064 Some(Summary::Count),
1065];
1066const FORMULA_CHOICE: usize = SUMMARY_KINDS.len();
1067const FORMULA_FUNCTIONS: [&str; 6] = ["Sum(", "Avg(", "Min(", "Max(", "Count()", "Iif("];
1068
1069struct SummaryEditor {
1071 column: usize,
1072 columns: Vec<GridColumn>,
1073 rows: Rc<Vec<Row>>,
1074 matched: Rc<QueryResult>,
1075 arrange: State<Arrangement>,
1076 draft: State<String>,
1077 close: Rc<dyn Fn()>,
1078}
1079
1080impl View for SummaryEditor {
1081 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1082 let theme = *cx.theme();
1083 let labels = cx.labels();
1084 let c = self.column;
1085 let (ink, muted) = (theme.color(Semantic::Ink), theme.color(Semantic::InkMuted));
1086 let arrangement = self.arrange.get();
1087 let by_formula = arrangement.formulas[c].is_some();
1088 let title = format!("{} {}", labels.grid_summary_of, self.columns[c].title);
1089 let mut lines = vec![util::label(cx, &title, TextStyle::Callout, Some(Weight::SEMIBOLD), ink)];
1090
1091 let mut names: Vec<String> = SUMMARY_KINDS
1092 .iter()
1093 .map(|kind| kind.map_or(labels.grid_summary_none.clone(), |s| labels.summary(s).to_owned()))
1094 .collect();
1095 names.push(labels.grid_summary_formula.clone());
1096 let (read, write, draft) = (self.arrange.clone(), self.arrange.clone(), self.draft.clone());
1097 let choice = Binding::new(
1098 move || {
1099 let now = read.get();
1100 if now.formulas[c].is_some() {
1101 return FORMULA_CHOICE;
1102 }
1103 SUMMARY_KINDS.iter().position(|k| *k == now.summaries[c]).unwrap_or(0)
1104 },
1105 move |i: usize| {
1106 let formula = (i == FORMULA_CHOICE).then(|| draft.get());
1107 write.update(|a| {
1108 a.formulas[c].clone_from(&formula);
1109 if let Some(kind) = SUMMARY_KINDS.get(i) {
1110 a.summaries[c] = *kind;
1111 }
1112 });
1113 },
1114 );
1115 lines.push(cx.child(&crate::SegmentedControl::new(choice, names).label(title.clone())));
1116
1117 if by_formula {
1118 let text = self.draft.get();
1119 let checked = check_formula(&self.columns, &text)
1120 .and_then(|parsed| formula_value(&self.columns, &self.rows, &self.matched.matched, &parsed));
1121 let (write, draft, close) = (self.arrange.clone(), self.draft.clone(), self.close.clone());
1122 let valid = checked.is_ok();
1123 let apply = Rc::new(move || {
1124 if valid {
1125 let formula = draft.get();
1126 write.update(|a| a.formulas[c] = Some(formula.clone()));
1127 close();
1128 }
1129 });
1130 let on_enter = apply.clone();
1131 lines.push(
1132 cx.child(
1133 &TextField::new(self.draft.binding())
1134 .placeholder(labels.grid_formula_hint.clone())
1135 .autofocus()
1136 .on_submit(move |_| on_enter()),
1137 ),
1138 );
1139 lines.push(match &checked {
1140 Ok(value) => {
1141 let shown = format!(
1142 "{}: {}",
1143 labels.grid_formula_result,
1144 formula_display(&self.columns[c], value)
1145 );
1146 util::label(cx, &shown, TextStyle::Callout, Some(Weight::MEDIUM), ink)
1147 }
1148 Err(problem) => {
1149 let shown = format!("{}: {problem}", labels.grid_formula_error);
1150 cx.child(&crate::Text::new(shown).style(TextStyle::Callout).foreground(Semantic::Danger))
1151 }
1152 });
1153 let inserter = |piece: String, name: String, draft: State<String>| {
1154 Button::new(name).style(ButtonStyle::Secondary).size(ButtonSize::Small).on_click(move || {
1155 draft.update(|text| text.push_str(&piece));
1156 })
1157 };
1158 lines.push(util::label(cx, &labels.grid_formula_columns, TextStyle::Caption, None, muted));
1159 let chips: Vec<NodeId> = self
1160 .columns
1161 .iter()
1162 .enumerate()
1163 .map(|(i, col)| {
1164 let piece = format!("[{}]", col.title);
1165 cx.keyed_child(i as u64, &inserter(piece, col.title.clone(), self.draft.clone()))
1166 })
1167 .collect();
1168 lines.push(cx.container(
1169 LayoutStyle { spacing: 4.0, wrap: true, ..LayoutStyle::stack(Axis::Horizontal) },
1170 &chips,
1171 Paint::None,
1172 ));
1173 lines.push(util::label(cx, &labels.grid_formula_functions, TextStyle::Caption, None, muted));
1174 let chips: Vec<NodeId> = FORMULA_FUNCTIONS
1175 .iter()
1176 .enumerate()
1177 .map(|(i, f)| {
1178 cx.keyed_child(
1179 100 + i as u64,
1180 &inserter((*f).to_owned(), (*f).to_owned(), self.draft.clone()),
1181 )
1182 })
1183 .collect();
1184 lines.push(cx.container(
1185 LayoutStyle { spacing: 4.0, wrap: true, ..LayoutStyle::stack(Axis::Horizontal) },
1186 &chips,
1187 Paint::None,
1188 ));
1189 lines.push(
1190 cx.child(
1191 &Button::new(labels.grid_apply.clone())
1192 .disabled(!valid)
1193 .size(ButtonSize::Small)
1194 .on_click(move || apply()),
1195 ),
1196 );
1197 }
1198 cx.container(
1199 LayoutStyle {
1200 spacing: 10.0,
1201 width: Some(SUMMARY_EDITOR_WIDTH),
1202 align: Align::Stretch,
1203 ..LayoutStyle::stack(Axis::Vertical)
1204 },
1205 &lines,
1206 Paint::None,
1207 )
1208 }
1209}
1210
1211struct FooterCell<'a> {
1213 column: usize,
1214 shared: &'a Shared<'a>,
1215}
1216
1217impl View for FooterCell<'_> {
1218 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1219 let theme = *cx.theme();
1220 let (s, c) = (self.shared, self.column);
1221 let col = &s.grid.columns[c];
1222 let labels = cx.labels();
1223 let (ink, muted) = (theme.color(Semantic::Ink), theme.color(Semantic::InkMuted));
1224 let bold = util::attrs(&theme, TextStyle::Callout, Some(Weight::SEMIBOLD));
1225 let hovered = cx.is_hover_within();
1226 let editable = s.grid.remote.is_none();
1227 let figure = match s.formula_text(c, (0, s.result.matched.len())) {
1228 Some(text) => Some((text, labels.grid_summary_formula.clone())),
1229 None => s.arrangement.summaries[c]
1230 .zip(s.result.totals.get(c).copied().flatten())
1231 .map(|(summary, v)| (col.display_summary(summary, v), labels.summary(summary).to_owned())),
1232 };
1233 let mut parts = Vec::new();
1234 let by_formula = s.arrangement.formulas[c].is_some();
1235 match &figure {
1236 Some((text, kind)) => {
1237 if by_formula {
1238 parts.push(util::icon(cx, IconName::Sigma, 13.0, theme.color(Semantic::Accent)));
1239 } else if s.widths[c] >= FOOTER_NAME_FROM {
1240 parts.push(util::label(cx, kind, TextStyle::Caption, None, muted));
1241 }
1242 parts.push(cx.text(text, &bold, ink, LayoutStyle::leaf()));
1243 }
1244 None if hovered && editable => parts.push(util::icon(cx, IconName::Sigma, 14.0, muted)),
1245 None => {}
1246 }
1247 let fixed = s.fixed_x(c);
1248 let mut style = cell_style(s.widths[c], FOOTER_HEIGHT, col.align);
1249 style.padding = Insets { left: 2.0, right: 12.0, ..Insets::ZERO };
1251 style.absolute = fixed.map(|x| (x, 0.0));
1252 let surface = theme.color(Semantic::Surface);
1253 let band = theme.color(Semantic::Control).faded(0.6);
1254 let tint = if hovered && editable { ink.with_alpha(0.04) } else { Color::TRANSPARENT };
1255 let solid = fixed.is_some();
1256 let node = cx.container(
1257 style,
1258 &parts,
1259 Paint::Custom(Rc::new(move |f: Rect, scene| {
1260 if solid {
1261 scene.push_quad(Quad::new(f, surface));
1262 scene.push_quad(Quad::new(f, band));
1263 }
1264 scene.push_quad(Quad::new(f, tint));
1265 })),
1266 );
1267 if fixed.is_some() {
1268 cx.translate(node, Point::new(s.offset_x, 0.0));
1269 }
1270 if editable {
1271 let edit = s.h.summary_edit.clone();
1272 let (draft, start) = (s.h.summary_draft.clone(), first_draft(s, c));
1273 cx.on_click(
1274 node,
1275 Rc::new(move || {
1276 draft.set(start.clone());
1277 edit.set(Some(c));
1278 }),
1279 );
1280 cx.cursor(node, CursorIcon::Pointer);
1281 let value = figure.map_or_else(String::new, |(text, _)| text);
1282 cx.semantics(
1283 node,
1284 Semantics::new(Role::Button)
1285 .name(format!("{} {}", labels.grid_edit_summary, col.title))
1286 .value(value),
1287 );
1288 }
1289 node
1290 }
1291}
1292
1293fn keyed_selection(
1295 keys: Binding<BTreeSet<String>>,
1296 column: usize,
1297 data: GridData,
1298 remote: Option<RemoteRows>,
1299) -> Binding<BTreeSet<usize>> {
1300 let held = move || -> Vec<(usize, Row)> {
1301 match &remote {
1302 Some(remote) => remote.held_rows(),
1303 None => data.rows().iter().cloned().enumerate().collect(),
1304 }
1305 };
1306 let key_of = move |row: &Row| row.get(column).map(Value::key_text).unwrap_or_default();
1307 let (read, write, rows_read, rows_write) = (keys.clone(), keys, held.clone(), held);
1308 Binding::new(
1309 move || {
1310 let wanted = read.get();
1311 if wanted.is_empty() {
1312 return BTreeSet::new();
1313 }
1314 rows_read().iter().filter(|(_, row)| wanted.contains(&key_of(row))).map(|(i, _)| *i).collect()
1315 },
1316 move |picked: BTreeSet<usize>| {
1317 let rows = rows_write();
1318 let at_hand: BTreeSet<String> = rows.iter().map(|(_, row)| key_of(row)).collect();
1320 let mut next: BTreeSet<String> =
1321 write.get().into_iter().filter(|key| !at_hand.contains(key)).collect();
1322 next.extend(rows.iter().filter(|(i, _)| picked.contains(i)).map(|(_, row)| key_of(row)));
1323 write.set(next);
1324 },
1325 )
1326}
1327
1328fn value_literal(value: &Value) -> Option<String> {
1330 match value {
1331 Value::Text(text) => Some(format!("'{}'", text.replace('\'', "''"))),
1332 Value::Number(n) => Some(n.to_string()),
1333 Value::Bool(b) => Some(if *b { "True" } else { "False" }.to_owned()),
1334 Value::Date(date) => Some(format!("#{date}#")),
1335 _ => None,
1336 }
1337}
1338
1339fn values_of(rows: &[Row], col: &GridColumn, c: usize) -> Vec<(String, Value)> {
1341 let mut seen = BTreeMap::new();
1342 for row in rows {
1343 let Some(value) = row.get(c) else { continue };
1344 if matches!(value, Value::Empty) {
1345 continue;
1346 }
1347 let shown = col.display_cow(value);
1348 if !seen.contains_key(shown.as_ref()) {
1349 seen.insert(shown.into_owned(), value.clone());
1350 if seen.len() >= MAX_FILTER_VALUES {
1351 break;
1352 }
1353 }
1354 }
1355 seen.into_iter().collect()
1356}
1357
1358struct ValueFilter {
1360 title: String,
1361 column: usize,
1362 values: Vec<(String, Value)>,
1363 query: State<GridQuery>,
1364 chosen: State<BTreeSet<String>>,
1365 search: State<String>,
1366}
1367
1368#[cfg_attr(feature = "live", ravenrs_live::expose)]
1369impl ValueFilter {
1370 fn write(&self, chosen: &BTreeSet<String>) {
1371 let literals: Vec<String> = self
1372 .values
1373 .iter()
1374 .filter(|(shown, _)| chosen.contains(shown))
1375 .filter_map(|(_, value)| value_literal(value))
1376 .collect();
1377 let text = if literals.is_empty() {
1378 String::new()
1379 } else {
1380 format!("[{}] In ({})", self.title, literals.join(", "))
1381 };
1382 let c = self.column;
1383 self.query.update(|q| q.set_filter(c, text));
1384 }
1385}
1386
1387impl View for ValueFilter {
1388 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1389 let labels = cx.labels();
1390 let chosen = self.chosen.get();
1391 let needle = self.search.get().to_lowercase();
1392 let mut lines = vec![
1393 cx.child(
1394 &TextField::new(self.search.binding())
1395 .placeholder(labels.grid_search.clone())
1396 .icon(IconName::Search)
1397 .compact(),
1398 ),
1399 ];
1400 let mut list = Vec::new();
1401 for (shown, _) in self.values.iter().filter(|(shown, _)| shown.to_lowercase().contains(&needle)) {
1402 let on = chosen.contains(shown);
1403 let (name, this) = (shown.clone(), ValueFilterHandle::of(self));
1404 let binding = Binding::new(move || on, move |checked: bool| this.set(&name, checked));
1405 list.push(cx.keyed_child(hash_of(shown), &crate::Checkbox::new(shown.clone(), binding)));
1406 }
1407 let inner = cx.container(
1408 LayoutStyle { spacing: 6.0, ..LayoutStyle::stack(Axis::Vertical) },
1409 &list,
1410 Paint::None,
1411 );
1412 let scroller = cx.container(
1413 LayoutStyle {
1414 height: Some((list.len() as f32 * 28.0).min(260.0)),
1415 overflow_hidden: true,
1416 align_self: Some(Align::Stretch),
1417 ..LayoutStyle::stack(Axis::Vertical)
1418 },
1419 &[inner],
1420 Paint::None,
1421 );
1422 cx.scrollable(scroller);
1423 lines.push(scroller);
1424 if !chosen.is_empty() {
1425 let this = ValueFilterHandle::of(self);
1426 lines.push(
1427 cx.child(
1428 &Button::new(labels.grid_clear_filter.clone())
1429 .style(ButtonStyle::Ghost)
1430 .size(ButtonSize::Small)
1431 .icon(IconName::X)
1432 .on_click(move || this.clear()),
1433 ),
1434 );
1435 }
1436 cx.container(
1437 LayoutStyle {
1438 spacing: 8.0,
1439 width: Some(240.0),
1440 align: Align::Stretch,
1441 ..LayoutStyle::stack(Axis::Vertical)
1442 },
1443 &lines,
1444 Paint::None,
1445 )
1446 }
1447}
1448
1449#[derive(Clone)]
1451struct ValueFilterHandle {
1452 title: String,
1453 column: usize,
1454 values: Rc<Vec<(String, Value)>>,
1455 query: State<GridQuery>,
1456 chosen: State<BTreeSet<String>>,
1457}
1458
1459#[cfg_attr(feature = "live", ravenrs_live::expose)]
1460impl ValueFilterHandle {
1461 fn of(filter: &ValueFilter) -> Self {
1462 Self {
1463 title: filter.title.clone(),
1464 column: filter.column,
1465 values: Rc::new(filter.values.clone()),
1466 query: filter.query.clone(),
1467 chosen: filter.chosen.clone(),
1468 }
1469 }
1470
1471 fn write(&self, chosen: &BTreeSet<String>) {
1472 ValueFilter {
1473 title: self.title.clone(),
1474 column: self.column,
1475 values: self.values.as_ref().clone(),
1476 query: self.query.clone(),
1477 chosen: self.chosen.clone(),
1478 search: State::new(String::new()),
1479 }
1480 .write(chosen);
1481 }
1482
1483 fn set(&self, name: &str, on: bool) {
1484 let mut chosen = self.chosen.get();
1485 if on {
1486 chosen.insert(name.to_owned());
1487 } else {
1488 chosen.remove(name);
1489 }
1490 self.write(&chosen);
1491 self.chosen.set(chosen);
1492 }
1493
1494 fn clear(&self) {
1495 self.write(&BTreeSet::new());
1496 self.chosen.set(BTreeSet::new());
1497 }
1498}
1499
1500fn hash_of(text: &str) -> u64 {
1501 text.bytes().fold(0xcbf2_9ce4_8422_2325_u64, |h, b| (h ^ u64::from(b)).wrapping_mul(0x100_0000_01b3))
1502}
1503
1504struct HeaderCell<'a> {
1505 column: usize,
1506 shared: &'a Shared<'a>,
1507}
1508
1509impl View for HeaderCell<'_> {
1510 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1511 let theme = *cx.theme();
1512 let s = self.shared;
1513 let c = self.column;
1514 let col = &s.grid.columns[c];
1515 let labels = cx.labels();
1516 let query = s.h.query.peek().clone();
1517 let sort = query.sort.iter().position(|(sc, _)| *sc == c).map(|i| (i, query.sort[i].1));
1518 let hovered = cx.is_hover_within();
1519 let accent = theme.color(Semantic::Accent);
1520 let ink = if sort.is_some() || hovered {
1521 theme.color(Semantic::Ink)
1522 } else {
1523 theme.color(Semantic::InkMuted)
1524 };
1525 let a = util::attrs(&theme, TextStyle::Caption, Some(Weight::SEMIBOLD));
1526 let mut content = Vec::new();
1527 if s.arrangement.pinned.get(c).copied().unwrap_or(false) {
1528 content.push(util::icon(cx, IconName::Pin, 12.0, theme.color(Semantic::InkMuted)));
1529 }
1530 content.push(cx.text(&col.title, &a, ink, LayoutStyle::leaf()));
1531 if let Some((priority, dir)) = sort {
1532 let icon = if dir == SortDirection::Ascending { IconName::ArrowUp } else { IconName::ArrowDown };
1533 content.push(util::icon(cx, icon, 13.0, accent));
1534 if query.sort.len() > 1 {
1535 content.push(util::label(cx, &(priority + 1).to_string(), TextStyle::Caption, None, ink));
1536 }
1537 }
1538 let width = s.widths[c];
1539 let filtered = !query.filter(c).trim().is_empty();
1540 let can_filter = col.filterable && s.grid.remote.is_none();
1541 let opened_filter = s.h.filter_open.get() == Some(c);
1542 let opened_menu = s.h.menu.get().is_some_and(|(column, _)| column == c);
1543 let tools = hovered || opened_filter || opened_menu;
1544 let mut label_style = LayoutStyle {
1545 grow: 1.0,
1546 min_width: Some(0.0),
1547 height: Some(HEADER_HEIGHT),
1548 spacing: 6.0,
1549 align: Align::Center,
1550 justify: justify(col.align),
1551 overflow_hidden: true,
1552 ..LayoutStyle::stack(Axis::Horizontal)
1553 };
1554 label_style.padding = Insets { left: 12.0, right: 4.0, ..Insets::ZERO };
1555 let label = cx.container(label_style, &content, Paint::None);
1556 let mut semantics = Semantics::new(Role::ColumnHeader).name(col.title.clone());
1557 if let Some((_, dir)) = sort {
1558 semantics = semantics.description(if dir == SortDirection::Ascending {
1559 labels.grid_sorted_ascending.clone()
1560 } else {
1561 labels.grid_sorted_descending.clone()
1562 });
1563 }
1564 cx.semantics(label, semantics);
1565 let dragged = s.h.dragged.clone();
1566 if col.sortable {
1567 let q = s.h.query.clone();
1568 let m = s.h.modifiers.clone();
1569 cx.on_click(
1570 label,
1571 Rc::new(move || {
1572 if dragged.replace(false) {
1574 return;
1575 }
1576 let mods = m.get();
1577 q.update(|q| q.toggle_sort(c, mods.shift || mods.ctrl || mods.meta));
1578 }),
1579 );
1580 cx.cursor(label, CursorIcon::Pointer);
1581 }
1582 cx.on_drag(label, drag_handler(&s.h, &s.column_facts, DragSource::Column(c)));
1583 let menu = s.h.menu.clone();
1584 cx.on_context_menu(label, Rc::new(move |p| menu.set(Some((c, Some(p))))));
1585 let mut parts = vec![label];
1586 let mut funnel_node = None;
1587 if can_filter && (tools || filtered) {
1588 let opener = s.h.filter_open.clone();
1589 let chosen = s.h.filter_chosen.clone();
1590 let search = s.h.filter_search.clone();
1591 let starts = if opened_filter { None } else { Some(c) };
1592 let (q2, rows2, col2) = (s.h.query.clone(), s.rows_rc.clone(), col.clone());
1593 let node = cx.keyed_child(
1594 1,
1595 &crate::IconButton::new(IconName::Filter)
1596 .label(format!("{} {}", labels.grid_filter_values, col.title))
1597 .style(if filtered { ButtonStyle::Secondary } else { ButtonStyle::Ghost })
1598 .size(ButtonSize::Small)
1599 .on_click(move || {
1600 let text = q2.peek().filter(c).to_owned();
1602 let values = values_of(&rows2, &col2, c);
1603 chosen.set(
1604 values
1605 .into_iter()
1606 .filter(|(_, v)| {
1607 value_literal(v).is_some_and(|l| {
1608 text.contains(&format!("{l},")) || text.contains(&format!("{l})"))
1609 })
1610 })
1611 .map(|(shown, _)| shown)
1612 .collect(),
1613 );
1614 search.set(String::new());
1615 opener.set(starts);
1616 }),
1617 );
1618 funnel_node = Some(node);
1619 parts.push(node);
1620 }
1621 let mut menu_node = None;
1622 if tools {
1623 let opener = s.h.menu.clone();
1624 let node = cx.keyed_child(
1625 2,
1626 &crate::IconButton::new(IconName::EllipsisVertical)
1627 .label(format!("{} {}", labels.grid_column_menu, col.title))
1628 .style(ButtonStyle::Ghost)
1629 .size(ButtonSize::Small)
1630 .on_click(move || {
1631 opener.update(|m| *m = if m.is_some() { None } else { Some((c, None)) });
1632 }),
1633 );
1634 menu_node = Some(node);
1635 parts.push(node);
1636 }
1637 if col.resizable {
1638 parts.push(cx.keyed_child(3, &ResizeHandle { column: c, shared: s }));
1639 }
1640
1641 let fixed = s.fixed_x(c);
1642 let lit =
1643 s.drop_before == Some(Some(c)) || (s.drop_before == Some(None) && s.visible.last() == Some(&c));
1644 let at_end = s.drop_before == Some(None);
1645 let zones = s.h.zones.clone();
1646 let hover_fill =
1647 if hovered { theme.color(Semantic::Ink).with_alpha(0.03) } else { Color::TRANSPARENT };
1648 let solid = fixed.map(|_| theme.color(Semantic::Surface));
1649 let band = theme.color(Semantic::Control).faded(0.6);
1650 let edge = (s.last_fixed == Some(c)).then(|| theme.color(Semantic::Border));
1651 let node = cx.container(
1652 LayoutStyle {
1653 width: Some(width),
1654 height: Some(HEADER_HEIGHT),
1655 align: Align::Center,
1656 absolute: fixed.map(|x| (x, 0.0)),
1657 ..LayoutStyle::stack(Axis::Horizontal)
1658 },
1659 &parts,
1660 Paint::Custom(Rc::new(move |f: Rect, scene| {
1661 zones.borrow_mut().cells.insert(c, f);
1662 if let Some(solid) = solid {
1663 scene.push_quad(Quad::new(f, solid));
1664 scene.push_quad(Quad::new(f, band));
1665 }
1666 scene.push_quad(Quad::new(f, hover_fill));
1667 if let Some(edge) = edge {
1668 scene.push_quad(Quad::new(Rect::new(f.max_x() - 1.0, f.y(), 1.0, f.height()), edge));
1669 }
1670 if lit {
1671 let x = if at_end { f.max_x() - 2.0 } else { f.x() };
1672 scene.push_quad(Quad::new(Rect::new(x, f.y() + 4.0, 2.0, f.height() - 8.0), accent));
1673 }
1674 })),
1675 );
1676 if fixed.is_some() {
1677 cx.translate(node, Point::new(s.offset_x, 0.0));
1678 }
1679 let opened = *s.h.menu.peek();
1680 if opened_menu && let Some(button_or_point) = opened {
1681 let entries = column_menu(s, c, &labels);
1682 let menu = s.h.menu.clone();
1683 let close: Rc<dyn Fn()> = Rc::new(move || menu.set(None));
1684 let list = MenuList { entries: &entries, close: close.clone(), on_side: None };
1685 match button_or_point.1 {
1686 Some(p) => cx.overlay(node, Placement::At(p), &list, Some(close)),
1687 None => cx.overlay(menu_node.unwrap_or(node), Placement::Below, &list, Some(close)),
1688 }
1689 }
1690 if opened_filter {
1691 let close: Rc<dyn Fn()> = {
1692 let open = s.h.filter_open.clone();
1693 Rc::new(move || open.set(None))
1694 };
1695 let filter = ValueFilter {
1696 title: col.title.clone(),
1697 column: c,
1698 values: values_of(s.rows, col, c),
1699 query: s.h.query.clone(),
1700 chosen: s.h.filter_chosen.clone(),
1701 search: s.h.filter_search.clone(),
1702 };
1703 cx.overlay(funnel_node.unwrap_or(node), Placement::Below, &PanelOf(&filter), Some(close));
1704 }
1705 node
1706 }
1707}
1708
1709struct FilterCell<'a> {
1710 column: usize,
1711 shared: &'a Shared<'a>,
1712}
1713
1714impl View for FilterCell<'_> {
1715 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1716 let theme = *cx.theme();
1717 let s = self.shared;
1718 let c = self.column;
1719 let col = &s.grid.columns[c];
1720 let mut children = Vec::new();
1721 if col.filterable {
1722 let (read, write) = (s.h.query.clone(), s.h.query.clone());
1723 let binding = Binding::new(
1724 move || read.with(|q| q.filter(c).to_owned()),
1725 move |t| write.update(|q| q.set_filter(c, t)),
1726 );
1727 let labels = cx.labels();
1728 let hint =
1729 if col.number.is_some() { &labels.grid_number_filter_hint } else { &labels.grid_filter_hint };
1730 let mut field = TextField::new(binding).placeholder(hint.clone()).compact().flat();
1731 if matches!(col.align, CellAlign::End) {
1732 field = field.end_aligned();
1733 }
1734 let field = cx.child(&field);
1735 cx.label_control(field, &format!("{} {}", labels.grid_filter_column, col.title), Some(hint));
1736 children.push(field);
1737 }
1738 let mut style = cell_style(s.widths[c], HEADER_HEIGHT, CellAlign::Start);
1739 style.padding = Insets::ZERO;
1741 style.align = Align::Stretch;
1742 let fixed = s.fixed_x(c);
1743 style.absolute = fixed.map(|x| (x, 0.0));
1744 let paint =
1745 fixed.map_or(Paint::None, |_| Paint::Quad(QuadPaint::new(theme.color(Semantic::Surface))));
1746 let node = cx.container(LayoutStyle { align: Align::Stretch, ..style }, &children, paint);
1747 if fixed.is_some() {
1748 cx.translate(node, Point::new(s.offset_x, 0.0));
1749 }
1750 node
1751 }
1752}
1753
1754struct GroupChip<'a> {
1757 index: usize,
1758 column: usize,
1759 grid: &'a DataGrid,
1760 h: &'a Handles,
1761 facts: &'a Rc<Vec<(String, bool)>>,
1762}
1763
1764impl View for GroupChip<'_> {
1765 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1766 let theme = *cx.theme();
1767 let labels = cx.labels();
1768 let (c, i, h) = (self.column, self.index, self.h);
1769 let title = self.grid.columns.get(c).map_or("?", |col| col.title.as_str());
1770 let query = h.query.peek().clone();
1771 let descending = query.sort.iter().any(|(s, d)| *s == c && *d == SortDirection::Descending);
1772 let (ink, muted, accent) =
1773 (theme.color(Semantic::Ink), theme.color(Semantic::InkMuted), theme.color(Semantic::Accent));
1774 let grip = util::icon(cx, IconName::EllipsisVertical, 12.0, muted);
1775 let name = util::label(cx, title, TextStyle::Callout, Some(Weight::MEDIUM), ink);
1776 let arrow =
1777 util::icon(cx, if descending { IconName::ArrowDown } else { IconName::ArrowUp }, 13.0, accent);
1778 let body = cx.container(
1779 LayoutStyle {
1780 spacing: 6.0,
1781 align: Align::Center,
1782 height: Some(28.0),
1783 padding: Insets { left: 8.0, right: 2.0, ..Insets::ZERO },
1784 ..LayoutStyle::stack(Axis::Horizontal)
1785 },
1786 &[grip, name, arrow],
1787 Paint::None,
1788 );
1789 cx.semantics(body, Semantics::new(Role::Button).name(format!("{} {title}", labels.grid_grouped_by)));
1790 cx.cursor(body, CursorIcon::Pointer);
1791 let (q, dragged) = (h.query.clone(), h.dragged.clone());
1792 cx.on_click(
1793 body,
1794 Rc::new(move || {
1795 if dragged.replace(false) {
1796 return;
1797 }
1798 q.update(|q| {
1799 q.sort.retain(|(s, _)| *s != c);
1800 if !descending {
1801 q.sort.push((c, SortDirection::Descending));
1802 }
1803 });
1804 }),
1805 );
1806 cx.on_drag(body, drag_handler(h, self.facts, DragSource::Chip(c)));
1807 cx.focusable(body, 6.0);
1808 let q = h.query.clone();
1809 cx.on_key(
1810 body,
1811 Rc::new(move |key, mods| {
1812 match key {
1813 Key::ArrowLeft if (mods.ctrl || mods.meta) && i > 0 => q.update(|q| {
1814 q.group_by.swap(i, i - 1);
1815 q.collapsed.clear();
1816 }),
1817 Key::ArrowRight if mods.ctrl || mods.meta => q.update(|q| {
1818 if i + 1 < q.group_by.len() {
1819 q.group_by.swap(i, i + 1);
1820 q.collapsed.clear();
1821 }
1822 }),
1823 Key::Delete | Key::Backspace => q.update(|q| q.toggle_group(c)),
1824 _ => return false,
1825 }
1826 true
1827 }),
1828 );
1829 let q = h.query.clone();
1830 let remove = cx.keyed_child(
1831 1,
1832 &crate::IconButton::new(IconName::X)
1833 .label(format!("{} {title}", labels.grid_remove_group))
1834 .style(ButtonStyle::Ghost)
1835 .size(ButtonSize::Small)
1836 .on_click(move || q.update(|q| q.toggle_group(c))),
1837 );
1838 let zones = h.zones.clone();
1839 let (fill, line) = (theme.color(Semantic::SurfaceElevated), theme.color(Semantic::Border));
1840 let radius = theme.radii.sm;
1841 let moving = h.drag.peek().is_some_and(|d| d.source == DragSource::Chip(c));
1842 let (fill, line) = if moving { (fill.faded(0.4), line.faded(0.4)) } else { (fill, line) };
1843 cx.container(
1844 LayoutStyle { align: Align::Center, height: Some(30.0), ..LayoutStyle::stack(Axis::Horizontal) },
1845 &[body, remove],
1846 Paint::Custom(Rc::new(move |f: Rect, scene| {
1847 zones.borrow_mut().chips.insert(i, f);
1848 scene.push_quad(Quad::new(f, fill).radius(radius).border(1.0, line));
1849 })),
1850 )
1851 }
1852}
1853
1854struct DragGhost {
1856 title: String,
1857 grouping: bool,
1858 ungrouping: bool,
1859}
1860
1861impl View for DragGhost {
1862 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1863 let theme = *cx.theme();
1864 let accent = theme.color(Semantic::Accent);
1865 let icon = if self.ungrouping {
1866 IconName::X
1867 } else if self.grouping {
1868 IconName::Layers
1869 } else {
1870 IconName::Columns3
1871 };
1872 let mark = util::icon(cx, icon, 14.0, accent);
1873 let name = util::label(
1874 cx,
1875 &self.title,
1876 TextStyle::Callout,
1877 Some(Weight::SEMIBOLD),
1878 theme.color(Semantic::Ink),
1879 );
1880 let node = cx.container(
1881 LayoutStyle {
1882 spacing: 6.0,
1883 align: Align::Center,
1884 padding: Insets::symmetric(6.0, 10.0),
1885 ..LayoutStyle::stack(Axis::Horizontal)
1886 },
1887 &[mark, name],
1888 Paint::Quad(QuadPaint {
1889 corner_radius: theme.radii.sm,
1890 border: Some(ravenrs_core::Border { width: 1.0, color: accent }),
1891 shadow: Some(theme.elevation.raised),
1892 ..QuadPaint::new(theme.color(Semantic::SurfaceElevated))
1893 }),
1894 );
1895 cx.semantics(node, Semantics::hidden());
1896 node
1897 }
1898}
1899
1900struct WaitingRow<'a> {
1902 shared: &'a Shared<'a>,
1903}
1904
1905impl View for WaitingRow<'_> {
1906 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1907 let theme = *cx.theme();
1908 let s = self.shared;
1909 let bar = theme.color(Semantic::Control);
1910 let line = theme.color(Semantic::Border).faded(0.6);
1911 let starts: Vec<(f32, f32)> = {
1912 let mut x = s.select_width;
1913 s.visible
1914 .iter()
1915 .map(|&c| {
1916 let cell = (x, s.widths[c]);
1917 x += s.widths[c];
1918 cell
1919 })
1920 .collect()
1921 };
1922 let node = cx.container(
1923 LayoutStyle {
1924 width: Some(s.total_width),
1925 height: Some(s.grid.row_height),
1926 ..LayoutStyle::leaf()
1927 },
1928 &[],
1929 Paint::Custom(Rc::new(move |f: Rect, scene| {
1930 for (x, width) in &starts {
1931 let inner =
1932 Rect::new(f.x() + x + 12.0, f.center().y - 5.0, (width - 40.0).max(8.0), 10.0);
1933 scene.push_quad(Quad::new(inner, bar).radius(5.0));
1934 }
1935 scene.push_quad(Quad::new(Rect::new(f.x(), f.max_y() - 1.0, f.width(), 1.0), line));
1936 })),
1937 );
1938 cx.semantics(node, Semantics::hidden());
1939 node
1940 }
1941}
1942
1943struct DataRow<'a> {
1944 index: usize,
1945 even: bool,
1947 row: usize,
1948 values: Row,
1949 shared: &'a Shared<'a>,
1950}
1951
1952impl View for DataRow<'_> {
1953 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1954 let theme = *cx.theme();
1955 let s = self.shared;
1956 let (index, row) = (self.index, self.row);
1957 let selected = s.selected.contains(&row);
1958 let hovered = cx.is_hover_within();
1959 let rh = s.grid.row_height;
1960 let mode = s.grid.selection_mode;
1961 let mut cells = Vec::with_capacity(s.visible.len() + 1);
1962 let mut fixed_cells = Vec::new();
1963 let control = theme.color(Semantic::Control);
1964 let fill = if selected {
1965 theme.color(Semantic::Accent).with_alpha(SELECTED_ROW)
1966 } else if hovered {
1967 control.faded(HOVERED_ROW)
1968 } else if self.even {
1969 s.grid.even_row.unwrap_or_else(|| {
1970 if s.grid.striped { control.faded(STRIPED_ROW) } else { Color::TRANSPARENT }
1971 })
1972 } else {
1973 s.grid.odd_row.unwrap_or(Color::TRANSPARENT)
1974 };
1975 let line = if s.grid.horizontal_lines {
1976 theme.color(Semantic::Border).faded(if s.grid.striped { 0.5 } else { 0.6 })
1977 } else {
1978 Color::TRANSPARENT
1979 };
1980 let surface = theme.color(Semantic::Surface);
1982 let solid = move |edge: Option<Color>| -> Paint {
1983 Paint::Custom(Rc::new(move |f: Rect, scene| {
1984 scene.push_quad(Quad::new(f, surface));
1985 scene.push_quad(Quad::new(f, fill));
1986 scene.push_quad(Quad::new(Rect::new(f.x(), f.max_y() - 1.0, f.width(), 1.0), line));
1987 if let Some(edge) = edge {
1988 scene.push_quad(Quad::new(Rect::new(f.max_x() - 1.0, f.y(), 1.0, f.height()), edge));
1989 }
1990 }))
1991 };
1992 let select_fixed = s.fixed_width > 0.0;
1993 if s.select_width > 0.0 {
1994 let mut leading = Vec::with_capacity(2);
1995 if s.grid.row_detail.is_some() {
1996 let open = s.h.expanded.get().contains(&row);
1997 let chevron = if open { IconName::ChevronDown } else { IconName::ChevronRight };
1998 let glyph = util::icon(cx, chevron, 16.0, theme.color(Semantic::InkMuted));
1999 let arrow = cx.container(
2000 LayoutStyle {
2001 width: Some(DETAIL_ARROW_WIDTH),
2002 height: Some(rh),
2003 align: Align::Center,
2004 justify: Justify::Center,
2005 ..LayoutStyle::stack(Axis::Horizontal)
2006 },
2007 &[glyph],
2008 Paint::None,
2009 );
2010 let labels = cx.labels();
2011 let name = if open { &labels.grid_collapse_row } else { &labels.grid_expand_row };
2012 cx.semantics(arrow, Semantics::new(Role::Button).name(name.clone()).expanded(open));
2013 let (expanded, h) = (s.h.expanded.clone(), s.h.clone());
2014 cx.on_click(
2015 arrow,
2016 Rc::new(move || {
2017 h.focus_row.set(Some(index));
2018 expanded.update(|set| {
2019 if !set.remove(&row) {
2020 set.insert(row);
2021 }
2022 });
2023 }),
2024 );
2025 cx.cursor(arrow, CursorIcon::Pointer);
2026 leading.push(arrow);
2027 }
2028 if mode == SelectionMode::Multiple {
2029 let b = crate::inputs::check_box(cx, &theme, selected, false);
2030 let check = cx.container(
2031 LayoutStyle {
2032 width: Some(CHECK_WIDTH),
2033 height: Some(rh),
2034 align: Align::Center,
2035 justify: Justify::Center,
2036 ..LayoutStyle::stack(Axis::Horizontal)
2037 },
2038 &[b],
2039 Paint::None,
2040 );
2041 let name = cx.labels().grid_select_row.clone();
2042 cx.semantics(check, Semantics::new(Role::CheckBox).name(name).toggled(selected));
2043 let (sel, h) = (s.h.selection.clone(), s.h.clone());
2044 cx.on_click(
2045 check,
2046 Rc::new(move || {
2047 sel.update(|set| {
2048 if !set.remove(&row) {
2049 set.insert(row);
2050 }
2051 });
2052 h.focus_row.set(Some(index));
2053 h.anchor.set(Some(index));
2054 }),
2055 );
2056 cx.cursor(check, CursorIcon::Pointer);
2057 leading.push(check);
2058 }
2059 let cell = cx.container(
2060 LayoutStyle {
2061 width: Some(s.select_width),
2062 height: Some(rh),
2063 absolute: select_fixed.then_some((0.0, 0.0)),
2064 ..LayoutStyle::stack(Axis::Horizontal)
2065 },
2066 &leading,
2067 if select_fixed { solid(None) } else { Paint::None },
2068 );
2069 if select_fixed {
2070 cx.translate(cell, Point::new(s.offset_x, 0.0));
2071 fixed_cells.push(cell);
2072 } else {
2073 cells.push(cell);
2074 }
2075 }
2076 let attrs = cell_attrs(&theme);
2077 let first_column = s.visible.first().copied();
2078 for &c in s.visible {
2079 let col = &s.grid.columns[c];
2080 let value = self.values.get(c).cloned().unwrap_or_default();
2081 let looks = s.grid.cell_style.as_ref().map(|style| style(&self.values, c)).unwrap_or_default();
2082 let content = if s.editing == Some((row, c)) {
2083 let (data, buffer, editing, column) =
2084 (s.grid.data.clone(), s.h.edit_buffer.clone(), s.h.editing.clone(), col.clone());
2085 let previous = value.clone();
2086 let (cancel, wrong, right) =
2087 (s.h.editing.clone(), s.h.edit_error.clone(), s.h.edit_error.clone());
2088 let (check, local) = (s.grid.on_cell_edit.clone(), s.grid.remote.is_none());
2089 cx.child(
2090 &TextField::new(s.h.edit_buffer.binding())
2091 .compact()
2092 .autofocus()
2093 .on_submit(move |text| {
2094 let value = column.parse(&text, &previous);
2095 if let Some(check) = &check {
2096 let edit = CellEdit {
2097 row,
2098 column: c,
2099 previous: previous.clone(),
2100 value: value.clone(),
2101 };
2102 if let Err(message) = check(&edit) {
2103 wrong.set(Some(message));
2104 return;
2105 }
2106 }
2107 right.set(None);
2108 if local {
2109 data.set_cell(row, c, value);
2110 }
2111 buffer.set(String::new());
2112 editing.set(None);
2113 })
2114 .on_cancel({
2115 let wrong = s.h.edit_error.clone();
2116 move || {
2117 wrong.set(None);
2118 cancel.set(None);
2119 }
2120 }),
2121 )
2122 } else if let Some(template) = &col.template {
2123 let view = template(&value);
2124 let inner = cx.keyed_child(c as u64, view.as_ref());
2125 cx.container(LayoutStyle::stack(Axis::Horizontal), &[inner], Paint::None)
2127 } else {
2128 let ink = if value.is_empty() {
2129 theme.color(Semantic::InkMuted)
2130 } else {
2131 theme.color(looks.ink.unwrap_or(Semantic::Ink))
2132 };
2133 let text = if value.is_empty() { "โ".to_owned() } else { col.display(&value) };
2134 let mut attrs = attrs.clone();
2135 if looks.bold {
2136 attrs.weight = Weight::SEMIBOLD.0;
2137 }
2138 cx.text(&text, &attrs, ink, LayoutStyle::leaf())
2139 };
2140 let mut style = cell_style(s.widths[c], rh, col.align);
2141 if s.editing == Some((row, c)) {
2142 style.align = Align::Center;
2143 style.padding = Insets::symmetric(0.0, 4.0);
2144 style.justify = Justify::Start;
2145 } else if first_column == Some(c) {
2146 style.padding.left += s.indent;
2147 }
2148 let fixed = s.fixed_x(c);
2149 style.absolute = fixed.map(|x| (x, 0.0));
2150 let paint = if fixed.is_some() {
2151 solid((s.last_fixed == Some(c)).then(|| theme.color(Semantic::Border)))
2152 } else if let Some(fill) = looks.fill {
2153 Paint::Quad(QuadPaint::new(theme.color(fill).with_alpha(0.14)))
2154 } else {
2155 Paint::None
2156 };
2157 let cell = cx.container(LayoutStyle { align: Align::Center, ..style }, &[content], paint);
2158 if s.editing != Some((row, c)) {
2159 cx.semantics(cell, Semantics::new(Role::Cell));
2160 }
2161 let result = s.result.clone();
2162 let h = s.h.clone();
2163 cx.on_click(cell, Rc::new(move || click_row(&result, &h, mode, row, index)));
2164 let (editable, h2, activate) = (
2165 col.editable && (s.grid.remote.is_none() || s.grid.on_cell_edit.is_some()),
2166 s.h.clone(),
2167 s.grid.on_row_activate.clone(),
2168 );
2169 let rows_value = col.display(&value);
2170 cx.on_double_click(
2171 cell,
2172 Rc::new(move || {
2173 if editable {
2174 h2.edit_buffer.set(rows_value.clone());
2175 h2.editing.set(Some((row, c)));
2176 } else if let Some(a) = &activate {
2177 a(row);
2178 }
2179 }),
2180 );
2181 if fixed.is_some() {
2182 cx.translate(cell, Point::new(s.offset_x, 0.0));
2183 fixed_cells.push(cell);
2184 } else {
2185 cells.push(cell);
2186 }
2187 }
2188 if !s.grid.row_actions.is_empty() {
2189 let name_column = s.grid.name_column.or(s.grid.key_column).or(first_column).unwrap_or(0);
2190 let row_name = match (s.grid.columns.get(name_column), self.values.get(name_column)) {
2191 (Some(column), Some(value)) => column.display(value),
2192 _ => String::new(),
2193 };
2194 let mut buttons = Vec::with_capacity(s.grid.row_actions.len());
2195 for (i, action) in s.grid.row_actions.iter().enumerate() {
2196 let (run, values) = (action.on_click.clone(), self.values.clone());
2197 let enabled = action.enabled.as_ref().is_none_or(|f| f(&self.values));
2198 buttons.push(
2199 cx.keyed_child(
2200 (1 << 40) + i as u64,
2201 &crate::IconButton::new(action.icon)
2202 .label(format!("{} {row_name}", action.label))
2203 .size(ButtonSize::Small)
2204 .disabled(!enabled)
2205 .on_click(move || run(&values)),
2206 ),
2207 );
2208 }
2209 cells.push(cx.container(
2210 LayoutStyle {
2211 width: Some(s.grid.row_actions.len() as f32 * ROW_ACTION_WIDTH),
2212 height: Some(rh),
2213 align: Align::Center,
2214 ..LayoutStyle::stack(Axis::Horizontal)
2215 },
2216 &buttons,
2217 Paint::None,
2218 ));
2219 }
2220 if s.fixed_width > 0.0 {
2221 let room = cx.container(
2222 LayoutStyle { width: Some(s.fixed_width), height: Some(rh), ..LayoutStyle::leaf() },
2223 &[],
2224 Paint::None,
2225 );
2226 cells.insert(0, room);
2227 }
2228 cells.extend(fixed_cells);
2229 let focus = (s.focus_row == Some(index)).then(|| theme.color(Semantic::Accent));
2230 let column_lines = s.lines.clone();
2231 let node = cx.container(
2232 LayoutStyle {
2233 width: Some(s.total_width),
2234 height: Some(rh),
2235 ..LayoutStyle::stack(Axis::Horizontal)
2236 },
2237 &cells,
2238 Paint::Custom(Rc::new(move |f: Rect, scene| {
2239 scene.push_quad(Quad::new(f, fill));
2240 scene.push_quad(Quad::new(Rect::new(f.x(), f.max_y() - 1.0, f.width(), 1.0), line));
2241 for x in column_lines.iter() {
2242 scene.push_quad(Quad::new(Rect::new(f.x() + x, f.y(), 1.0, f.height()), line));
2243 }
2244 if let Some(accent) = focus {
2245 scene.push_quad(Quad::new(Rect::new(f.x(), f.y(), 2.0, f.height()), accent));
2246 }
2247 })),
2248 );
2249 let mut semantics = Semantics::new(Role::Row).active(s.focus_row == Some(index));
2251 if mode != SelectionMode::None {
2252 semantics = semantics.selected(selected);
2253 }
2254 cx.semantics(node, semantics);
2255 node
2256 }
2257}
2258
2259struct DetailRow<'a> {
2261 row: usize,
2262 values: &'a Row,
2263 shared: &'a Shared<'a>,
2264 width: f32,
2265}
2266
2267impl View for DetailRow<'_> {
2268 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
2269 let theme = *cx.theme();
2270 let s = self.shared;
2271 let Some(detail) = &s.grid.row_detail else { return cx.empty() };
2272 let content = detail(self.values, self.row);
2273 let inner = cx.child(content.as_ref());
2274 let holder = cx.container(
2275 LayoutStyle {
2276 width: Some(self.width),
2277 height: Some(s.grid.detail_height),
2278 padding: Insets::uniform(12.0),
2279 overflow_hidden: true,
2280 align: Align::Stretch,
2281 ..LayoutStyle::stack(Axis::Vertical)
2282 },
2283 &[inner],
2284 Paint::None,
2285 );
2286 cx.clip(holder);
2287 cx.translate(holder, Point::new(s.offset_x, 0.0));
2288 let (base, line) = (theme.color(Semantic::Canvas), theme.color(Semantic::Border).faded(0.6));
2289 cx.container(
2290 LayoutStyle {
2291 width: Some(s.total_width),
2292 height: Some(s.grid.detail_height),
2293 ..LayoutStyle::stack(Axis::Horizontal)
2294 },
2295 &[holder],
2296 Paint::Custom(Rc::new(move |f: Rect, scene| {
2297 scene.push_quad(Quad::new(f, base));
2298 scene.push_quad(Quad::new(Rect::new(f.x(), f.max_y() - 1.0, f.width(), 1.0), line));
2299 })),
2300 )
2301 }
2302}
2303
2304struct GroupRow<'a> {
2305 index: usize,
2306 shared: &'a Shared<'a>,
2307}
2308
2309impl View for GroupRow<'_> {
2310 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
2311 let theme = *cx.theme();
2312 let s = self.shared;
2313 let ViewRow::Group { level, column, label, key, count, collapsed, summaries } =
2314 &s.result.rows[self.index]
2315 else {
2316 return cx.empty();
2317 };
2318 let col = &s.grid.columns[*column];
2319 let (ink, muted) = (theme.color(Semantic::Ink), theme.color(Semantic::InkMuted));
2320 let chevron = if *collapsed { IconName::ChevronRight } else { IconName::ChevronDown };
2321 let mut children = vec![util::icon(cx, chevron, 16.0, muted)];
2322 let attrs = cell_attrs(&theme);
2323 children.push(cx.text(&format!("{}:", col.title), &attrs, muted, LayoutStyle::leaf()));
2324 let labels = cx.labels();
2325 let shown = if label.is_empty() { labels.grid_empty.as_str() } else { label.as_str() };
2326 let mut bold = attrs.clone();
2327 bold.weight = Weight::SEMIBOLD.0;
2328 children.push(cx.text(shown, &bold, ink, LayoutStyle::leaf()));
2329 let badge = cx.child(&Badge::new(crate::grid::model::NumberFormat::default().format(*count as f64)));
2330 children.push(cx.container(LayoutStyle::stack(Axis::Horizontal), &[badge], Paint::None));
2331 let rh = s.grid.row_height;
2332 let left = 12.0 + *level as f32 * GROUP_INDENT;
2333 let title = cx.container(
2334 LayoutStyle {
2335 height: Some(rh),
2336 padding: Insets { left, ..Insets::ZERO },
2337 spacing: 8.0,
2338 align: Align::Center,
2339 ..LayoutStyle::stack(Axis::Horizontal)
2340 },
2341 &children,
2342 Paint::None,
2343 );
2344 cx.translate(title, Point::new(s.offset_x, 0.0));
2346 let title_ends =
2348 left + 24.0 + (col.title.chars().count() + shown.chars().count() + 8) as f32 * CHAR_WIDTH;
2349 let mut parts = vec![title];
2350 let mut spoken = Vec::new();
2351 let range = s.ranges.get(self.index).copied().unwrap_or_default();
2352 for &c in s.visible {
2353 let other = &s.grid.columns[c];
2354 let figure = match s.formula_text(c, range) {
2355 Some(text) => Some((text, labels.grid_summary_formula.clone())),
2356 None => {
2357 summaries.get(c).copied().flatten().zip(s.arrangement.summaries[c]).map(|(v, summary)| {
2358 (other.display_summary(summary, v), labels.summary(summary).to_owned())
2359 })
2360 }
2361 };
2362 if let Some((text, kind)) = figure {
2363 spoken.push(format!("{kind} {}: {text}", other.title));
2364 let x = s.x_of(c);
2365 if x < title_ends {
2366 continue;
2367 }
2368 let value = cx.text(&text, &bold, ink, LayoutStyle::leaf());
2369 let mut style = cell_style(s.widths[c], rh, other.align);
2370 style.absolute = Some((x, 0.0));
2371 parts.push(cx.container(style, &[value], Paint::None));
2372 }
2373 }
2374 let hovered = cx.is_hover_within();
2375 let base = theme.color(Semantic::Canvas);
2376 let fill = if hovered { base.lerp(ink, 0.04) } else { base };
2377 let line = theme.color(Semantic::Border).faded(0.6);
2378 let node = cx.container(
2379 LayoutStyle {
2380 width: Some(s.total_width),
2381 height: Some(rh),
2382 align: Align::Center,
2383 ..LayoutStyle::stack(Axis::Horizontal)
2384 },
2385 &parts,
2386 Paint::Custom(Rc::new(move |f: Rect, scene| {
2387 scene.push_quad(Quad::new(f, fill));
2388 scene.push_quad(Quad::new(Rect::new(f.x(), f.max_y() - 1.0, f.width(), 1.0), line));
2389 })),
2390 );
2391 let (q, key, focus, index) = (s.h.query.clone(), key.clone(), s.h.focus_row.clone(), self.index);
2392 cx.on_click(
2393 node,
2394 Rc::new(move || {
2395 focus.set(Some(index));
2396 q.update(|q| q.toggle_collapsed(&key));
2397 }),
2398 );
2399 cx.semantics(
2400 node,
2401 Semantics::new(Role::Row)
2402 .name(format!("{}: {shown}, {count} {}", col.title, labels.grid_rows))
2403 .description(spoken.join(", "))
2404 .expanded(!*collapsed)
2405 .active(s.focus_row == Some(self.index)),
2406 );
2407 cx.cursor(node, CursorIcon::Pointer);
2408 node
2409 }
2410}
2411
2412#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
2413impl DataGrid {
2414 fn handles(&self, cx: &mut BuildCx<'_>) -> Handles {
2415 let initial = {
2416 let mut q = self.initial.clone();
2417 q.hidden = self.columns.iter().map(|c| !c.visible).collect();
2418 q
2419 };
2420 let widths0: Vec<f32> = self.columns.iter().map(|c| c.width).collect();
2421 let query = cx.use_state(move || initial);
2422 let widths = cx.use_state(move || widths0);
2423 let first_arrangement =
2424 Arrangement::new(&self.columns, self.group_panel.unwrap_or(false), self.page_size);
2425 let arrange = cx.use_state(move || first_arrangement);
2426 let own_selection: State<BTreeSet<usize>> = cx.use_state(BTreeSet::new);
2427 let focus_row = cx.use_state(|| None);
2428 let anchor = cx.use_state(|| Rc::new(Cell::new(None))).get();
2429 let reveal = cx.use_state(|| Rc::new(Cell::new(None))).get();
2430 let editing = cx.use_state(|| None);
2431 let edit_buffer = cx.use_state(String::new);
2432 let edit_error = cx.use_state(|| None);
2433 let resize = cx.use_state(|| Rc::new(Cell::new(None))).get();
2434 Handles {
2435 query,
2436 widths,
2437 selection: match (&self.selected_keys, self.key_column) {
2438 (Some(keys), Some(column)) => {
2439 keyed_selection(keys.clone(), column, self.data.clone(), self.remote.clone())
2440 }
2441 _ => self.selection.clone().unwrap_or_else(|| own_selection.binding()),
2442 },
2443 focus_row,
2444 anchor,
2445 reveal,
2446 editing,
2447 edit_buffer,
2448 edit_error,
2449 resize,
2450 modifiers: cx.modifiers(),
2451 building_filter: cx.use_state(|| false),
2452 expanded: {
2453 let local = cx.use_state(BTreeSet::new);
2454 self.expanded_rows.clone().unwrap_or_else(|| local.binding())
2455 },
2456 arrange,
2457 drag: cx.use_state(|| None),
2458 press: cx.use_state(|| Rc::new(Cell::new((Point::ZERO, false)))).get(),
2459 dragged: cx.use_state(|| Rc::new(Cell::new(false))).get(),
2460 zones: cx.use_state(|| Rc::new(RefCell::new(Zones::default()))).get(),
2461 menu: cx.use_state(|| None),
2462 filter_open: cx.use_state(|| None),
2463 filter_chosen: cx.use_state(BTreeSet::new),
2464 filter_search: cx.use_state(String::new),
2465 page: cx.use_state(|| 0),
2466 saving: cx.use_state(|| false),
2467 scenario_name: cx.use_state(String::new),
2468 active_scenario: cx.use_state(|| None),
2469 original: cx.use_state(|| Rc::new(RefCell::new(None))).get(),
2470 seen_layout: cx.use_state(|| Rc::new(RefCell::new(None))).get(),
2471 summary_edit: cx.use_state(|| None),
2472 summary_draft: cx.use_state(String::new),
2473 formula_cache: cx.use_state(|| Rc::new(RefCell::new(FormulaCache::default()))).get(),
2474 }
2475 }
2476
2477 fn toolbar_view(
2478 &self,
2479 cx: &mut BuildCx<'_>,
2480 h: &Handles,
2481 result: &QueryResult,
2482 rows: &Rc<Vec<Row>>,
2483 total: usize,
2484 ) -> NodeId {
2485 let theme = *cx.theme();
2486 let labels = cx.labels();
2487 let mut items = Vec::new();
2488 if self.search_panel {
2489 let (read, write) = (h.query.clone(), h.query.clone());
2490 let search = Binding::new(
2491 move || read.with(|q| q.search.clone()),
2492 move |t| write.update(|q| q.search = t),
2493 );
2494 items.push(
2495 cx.child(
2496 &TextField::new(search)
2497 .placeholder(labels.grid_search.clone())
2498 .icon(IconName::Search)
2499 .compact()
2500 .width(240.0),
2501 ),
2502 );
2503 }
2504 let query = h.query.peek().clone();
2505 if self.remote.is_none() && !h.arrange.peek().group_panel {
2506 let mut group =
2507 DropdownMenu::new(labels.grid_group.clone()).icon(IconName::Layers).size(ButtonSize::Small);
2508 for (c, col) in
2509 self.columns.iter().enumerate().filter(|(c, col)| col.groupable && !query.is_hidden(*c))
2510 {
2511 let q = h.query.clone();
2512 let mut item =
2513 MenuItem::new(col.title.clone()).on_select(move || q.update(|q| q.toggle_group(c)));
2514 if query.group_by.contains(&c) {
2515 item = item.icon(IconName::Check);
2516 }
2517 group = group.item(item);
2518 }
2519 if !query.group_by.is_empty() {
2520 let q = h.query.clone();
2521 group = group.separator().item(
2522 MenuItem::new(labels.grid_clear_grouping.clone()).icon(IconName::X).on_select(
2523 move || {
2524 q.update(|q| {
2525 q.group_by.clear();
2526 q.collapsed.clear();
2527 })
2528 },
2529 ),
2530 );
2531 }
2532 items.push(cx.child(&group));
2533 }
2534 if let Some(scenarios) = &self.scenarios {
2535 items.push(self.scenario_menu(cx, h, scenarios));
2536 }
2537 let mut chooser =
2538 DropdownMenu::new(labels.grid_columns.clone()).icon(IconName::Table).size(ButtonSize::Small);
2539 for (c, col) in self.columns.iter().enumerate() {
2540 let q = h.query.clone();
2541 let item = MenuItem::new(col.title.clone()).on_select(move || {
2542 q.update(|q| {
2543 if q.hidden.len() <= c {
2544 q.hidden.resize(c + 1, false);
2545 }
2546 q.hidden[c] = !q.hidden[c];
2547 })
2548 });
2549 chooser = chooser.item(if query.is_hidden(c) { item } else { item.icon(IconName::Check) });
2550 }
2551 items.push(cx.child(&chooser));
2552 if !query.sort.is_empty()
2553 || query.filters.iter().any(|f| !f.trim().is_empty())
2554 || !query.search.is_empty()
2555 || !query.group_by.is_empty()
2556 {
2557 let q = h.query.clone();
2558 items.push(
2559 cx.child(
2560 &Button::new(labels.grid_reset.clone())
2561 .style(ButtonStyle::Ghost)
2562 .size(ButtonSize::Small)
2563 .icon(IconName::RotateCcw)
2564 .on_click(move || {
2565 q.update(|q| {
2566 q.sort.clear();
2567 q.filters.clear();
2568 q.search.clear();
2569 q.group_by.clear();
2570 q.collapsed.clear();
2571 })
2572 }),
2573 ),
2574 );
2575 }
2576 if let Some(add) = self.on_add_row.clone() {
2577 items.push(
2578 cx.child(
2579 &Button::new(labels.grid_add_row.clone())
2580 .style(ButtonStyle::Secondary)
2581 .size(ButtonSize::Small)
2582 .icon(IconName::Plus)
2583 .on_click(move || add()),
2584 ),
2585 );
2586 }
2587 if let (Some(delete), false) = (self.on_delete_rows.clone(), h.selection.get().is_empty()) {
2588 let selection = h.selection.clone();
2589 items.push(
2590 cx.child(
2591 &Button::new(labels.grid_delete_rows.clone())
2592 .style(ButtonStyle::Ghost)
2593 .size(ButtonSize::Small)
2594 .icon(IconName::Trash2)
2595 .on_click(move || delete(selection.get().into_iter().collect())),
2596 ),
2597 );
2598 }
2599 items.push(cx.child(&Spacer::new()));
2600 if let Some(message) = h.edit_error.get() {
2601 items.push(util::label(cx, &message, TextStyle::Callout, None, theme.color(Semantic::Danger)));
2602 }
2603 let selected = h.selection.get().len();
2604 let fmt = crate::grid::model::NumberFormat::default();
2605 let mut status = format!(
2606 "{} {} {} {}",
2607 fmt.format(result.matched.len() as f64),
2608 labels.grid_of,
2609 fmt.format(rows.len() as f64),
2610 labels.grid_rows
2611 );
2612 if self.remote.is_some() {
2613 status = format!("{} {}", fmt.format(total as f64), labels.grid_rows);
2614 }
2615 if selected > 0 {
2618 status = format!("{} {}", fmt.format(selected as f64), labels.grid_selected);
2619 }
2620 items.push(util::label(cx, &status, TextStyle::Callout, None, theme.color(Semantic::InkMuted)));
2621 if let Some(export) = &self.on_export {
2622 let (export, columns, rows, q, matched) =
2623 (export.clone(), self.columns.clone(), rows.clone(), query.clone(), result.matched.clone());
2624 items.push(
2625 cx.child(
2626 &Button::new(labels.grid_export.clone())
2627 .style(ButtonStyle::Secondary)
2628 .size(ButtonSize::Small)
2629 .icon(IconName::Download)
2630 .on_click(move || export(to_csv(&columns, &rows, &q, &matched))),
2631 ),
2632 );
2633 }
2634 if self.filter_builder {
2635 let open = h.building_filter.clone();
2636 items.push(
2637 cx.child(
2638 &Button::new(labels.grid_filter_label.clone())
2639 .style(ButtonStyle::Secondary)
2640 .size(ButtonSize::Small)
2641 .icon(IconName::Filter)
2642 .on_click(move || open.set(true)),
2643 ),
2644 );
2645 }
2646 if let Some(export) = &self.on_export_file {
2647 let title = self.export_title.clone().unwrap_or_else(|| labels.grid_name.clone());
2648 let culture = labels.culture.clone();
2649 let table = Rc::new(move |columns: &[GridColumn], rows: &[Row], q: &GridQuery, m: &[usize]| {
2650 to_table(&title, &culture, columns, rows, q, m)
2651 });
2652 let mut menu = DropdownMenu::new(labels.grid_export_file.clone())
2653 .icon(IconName::Download)
2654 .size(ButtonSize::Small)
2655 .style(ButtonStyle::Secondary);
2656 for (format, text) in [
2657 (GridExportFormat::Xlsx, "Excel (.xlsx)"),
2658 (GridExportFormat::Pdf, "PDF (.pdf)"),
2659 (GridExportFormat::Csv, "CSV (.csv)"),
2660 ] {
2661 let (export, table, columns, rows, q, matched) = (
2662 export.clone(),
2663 table.clone(),
2664 self.columns.clone(),
2665 rows.clone(),
2666 query.clone(),
2667 result.matched.clone(),
2668 );
2669 menu = menu.item(
2670 MenuItem::new(text)
2671 .on_select(move || export(format, table(&columns, &rows, &q, &matched))),
2672 );
2673 }
2674 items.push(cx.child(&menu));
2675 }
2676 cx.container(
2677 LayoutStyle {
2678 padding: Insets::symmetric(10.0, 12.0),
2679 spacing: 8.0,
2680 align: Align::Center,
2681 wrap: true,
2682 ..LayoutStyle::stack(Axis::Horizontal)
2683 },
2684 &items,
2685 Paint::None,
2686 )
2687 }
2688
2689 fn scenario_menu(
2690 &self,
2691 cx: &mut BuildCx<'_>,
2692 h: &Handles,
2693 scenarios: &Binding<Vec<GridScenario>>,
2694 ) -> NodeId {
2695 let labels = cx.labels();
2696 let active = h.active_scenario.get();
2697 let title = match &active {
2698 Some(name) => format!("{}: {name}", labels.grid_scenario),
2699 None => labels.grid_scenario.clone(),
2700 };
2701 let mut menu = DropdownMenu::new(title).icon(IconName::Bookmark).size(ButtonSize::Small);
2702 let columns = self.columns.clone();
2703 for scenario in scenarios.get() {
2704 let on = active.as_deref() == Some(scenario.name.as_str());
2705 let (h2, columns, name) = (h.clone(), columns.clone(), scenario.name.clone());
2706 let item = MenuItem::new(scenario.name.clone()).on_select(move || {
2707 apply_layout(&h2, &columns, &scenario.layout);
2708 h2.active_scenario.set(Some(name.clone()));
2709 });
2710 menu = menu.item(if on { item.icon(IconName::Check) } else { item });
2711 }
2712 if !scenarios.get().is_empty() {
2713 menu = menu.separator();
2714 }
2715 let saving = h.saving.clone();
2716 let name = h.scenario_name.clone();
2717 menu = menu.item(MenuItem::new(labels.grid_scenario_save.clone()).icon(IconName::Save).on_select(
2718 move || {
2719 name.set(String::new());
2720 saving.set(true);
2721 },
2722 ));
2723 if let Some(current) = active {
2724 let (h2, columns, list, shown) = (h.clone(), columns.clone(), scenarios.clone(), current.clone());
2725 menu = menu.item(
2726 MenuItem::new(format!("{} {shown}", labels.grid_scenario_update))
2727 .icon(IconName::Check)
2728 .on_select(move || {
2729 let layout = current_layout(&h2, &columns);
2730 list.update(|all| {
2731 if let Some(s) = all.iter_mut().find(|s| s.name == current) {
2732 s.layout = layout.clone();
2733 }
2734 });
2735 }),
2736 );
2737 let (active, list, shown2) = (h.active_scenario.clone(), scenarios.clone(), shown.clone());
2738 menu = menu.item(
2739 MenuItem::new(format!("{} {shown}", labels.grid_scenario_delete))
2740 .icon(IconName::Trash2)
2741 .destructive()
2742 .on_select(move || {
2743 list.update(|all| all.retain(|s| s.name != shown2));
2744 active.set(None);
2745 }),
2746 );
2747 }
2748 let (h2, columns) = (h.clone(), columns);
2749 menu = menu.separator().item(
2750 MenuItem::new(labels.grid_scenario_original.clone()).icon(IconName::RotateCcw).on_select(
2751 move || {
2752 if let Some(original) = h2.original.borrow().clone() {
2753 apply_layout(&h2, &columns, &original);
2754 }
2755 h2.active_scenario.set(None);
2756 },
2757 ),
2758 );
2759 cx.child(&menu)
2760 }
2761
2762 fn filter_dialog(&self, cx: &mut BuildCx<'_>, h: &Handles, rows: &[Row]) -> NodeId {
2763 let columns: Rc<Vec<GridColumn>> = Rc::new(self.columns.clone());
2764 let open = h.building_filter.get();
2766 let fields = self.columns.iter().enumerate().filter(|(_, c)| c.filterable).map(|(i, column)| {
2767 let kind = if column.number.is_some() {
2768 crate::FilterFieldKind::Number
2769 } else {
2770 match rows.iter().filter_map(|r| r.get(i)).find(|v| !v.is_empty()) {
2771 Some(Value::Bool(_)) => crate::FilterFieldKind::Boolean,
2772 Some(Value::Date(_)) => crate::FilterFieldKind::Date,
2773 Some(Value::Number(_)) => crate::FilterFieldKind::Number,
2774 _ => crate::FilterFieldKind::Text,
2775 }
2776 };
2777 let values = if open && kind != crate::FilterFieldKind::Boolean {
2778 values_of(rows, column, i)
2779 .into_iter()
2780 .filter_map(|(shown, value)| {
2781 let key = match value {
2782 Value::Text(text) => text,
2783 Value::Number(n) => n.to_string(),
2784 Value::Date(date) => date.to_string(),
2785 _ => return None,
2786 };
2787 Some(crate::LookupItem::new(key, shown))
2788 })
2789 .collect()
2790 } else {
2791 Vec::new()
2792 };
2793 crate::FilterField::new(column.title.clone(), column.title.clone(), kind).values(values)
2794 });
2795 let (read, write) = (h.query.clone(), h.query.clone());
2796 let text = Binding::new(
2797 move || read.with(whole_filter),
2798 move |text: String| write.update(|q| set_whole_filter(q, &columns, text)),
2799 );
2800 cx.child(
2801 &Dialog::new(h.building_filter.binding(), cx.labels().grid_edit_filter.clone())
2802 .content(crate::FilterBuilder::new(text, fields))
2803 .width(640.0),
2804 )
2805 }
2806
2807 fn scenario_dialog(
2808 &self,
2809 cx: &mut BuildCx<'_>,
2810 h: &Handles,
2811 scenarios: &Binding<Vec<GridScenario>>,
2812 ) -> NodeId {
2813 let labels = cx.labels();
2814 let (h2, columns, list) = (h.clone(), self.columns.clone(), scenarios.clone());
2815 let save = move || {
2816 let name = h2.scenario_name.get().trim().to_owned();
2817 if name.is_empty() {
2818 return;
2819 }
2820 let layout = current_layout(&h2, &columns);
2821 list.update(|all| {
2822 all.retain(|s| s.name != name);
2823 all.push(GridScenario::new(name.clone(), layout.clone()));
2824 });
2825 h2.active_scenario.set(Some(name));
2826 h2.saving.set(false);
2827 };
2828 let on_enter = save.clone();
2829 cx.child(
2830 &Dialog::new(h.saving.binding(), labels.grid_scenario_save.trim_end_matches('โฆ').to_owned())
2831 .content(
2832 TextField::new(h.scenario_name.binding())
2833 .placeholder(labels.grid_scenario_name.clone())
2834 .autofocus()
2835 .on_submit(move |_| on_enter()),
2836 )
2837 .action(Button::new(labels.grid_save.clone()).on_click(save))
2838 .width(380.0),
2839 )
2840 }
2841
2842 fn group_panel_view(
2843 &self,
2844 cx: &mut BuildCx<'_>,
2845 h: &Handles,
2846 rows: &Rc<Vec<Row>>,
2847 facts: &Rc<Vec<(String, bool)>>,
2848 ) -> Option<NodeId> {
2849 let query = h.query.peek().clone();
2850 if self.remote.is_some() || (!h.arrange.peek().group_panel && query.group_by.is_empty()) {
2852 return None;
2853 }
2854 let theme = *cx.theme();
2855 let labels = cx.labels();
2856 let muted = theme.color(Semantic::InkMuted);
2857 let drag = *h.drag.peek();
2858 let target_place =
2859 drag.and_then(|d| if let DropTarget::Group(i) = d.target { Some(i) } else { None });
2860 let accent = theme.color(Semantic::Accent);
2861 let caret = |cx: &mut BuildCx<'_>| {
2862 cx.container(
2863 LayoutStyle { width: Some(2.0), height: Some(26.0), ..LayoutStyle::leaf() },
2864 &[],
2865 Paint::Quad(QuadPaint { corner_radius: 1.0, ..QuadPaint::new(accent) }),
2866 )
2867 };
2868 let moving = drag.and_then(|d| if let DragSource::Chip(c) = d.source { Some(c) } else { None });
2869 let mut items = Vec::new();
2870 let mut place = 0;
2871 for (i, &c) in query.group_by.iter().enumerate() {
2872 if i > 0 {
2873 items.push(util::icon(cx, IconName::ChevronRight, 14.0, muted));
2874 }
2875 if target_place == Some(place) && moving != Some(c) {
2876 items.push(caret(cx));
2877 }
2878 items
2879 .push(cx.keyed_child(c as u64 + 1, &GroupChip { index: i, column: c, grid: self, h, facts }));
2880 if moving != Some(c) {
2881 place += 1;
2882 }
2883 }
2884 if target_place == Some(place) {
2885 items.push(caret(cx));
2886 }
2887 if query.group_by.is_empty() || drag.is_some() {
2888 items.push(util::label(cx, &labels.grid_drag_hint, TextStyle::Callout, None, muted));
2889 }
2890 items.push(cx.child(&Spacer::new()));
2891 if !query.group_by.is_empty() {
2892 let (q, columns, rows2) = (h.query.clone(), self.columns.clone(), rows.clone());
2893 items.push(
2894 cx.child(
2895 &Button::new(labels.grid_collapse_all.clone())
2896 .style(ButtonStyle::Ghost)
2897 .size(ButtonSize::Small)
2898 .on_click(move || {
2899 let keys = super::query::group_keys(&columns, &rows2, &q.peek());
2900 q.update(|q| q.collapsed = keys.clone());
2901 }),
2902 ),
2903 );
2904 let q = h.query.clone();
2905 items.push(
2906 cx.child(
2907 &Button::new(labels.grid_expand_all.clone())
2908 .style(ButtonStyle::Ghost)
2909 .size(ButtonSize::Small)
2910 .on_click(move || q.update(|q| q.collapsed.clear())),
2911 ),
2912 );
2913 }
2914 let zones = h.zones.clone();
2915 let fill =
2916 if target_place.is_some() { accent.with_alpha(0.08) } else { theme.color(Semantic::Canvas) };
2917 let line = theme.color(Semantic::Border);
2918 Some(cx.container(
2919 LayoutStyle {
2920 min_height: Some(GROUP_PANEL_HEIGHT),
2921 padding: Insets::symmetric(6.0, 12.0),
2922 spacing: 6.0,
2923 align: Align::Center,
2924 wrap: true,
2925 align_self: Some(Align::Stretch),
2926 ..LayoutStyle::stack(Axis::Horizontal)
2927 },
2928 &items,
2929 Paint::Custom(Rc::new(move |f: Rect, scene| {
2930 zones.borrow_mut().panel = f;
2931 scene.push_quad(Quad::new(f, fill));
2932 scene.push_quad(Quad::new(Rect::new(f.x(), f.y(), f.width(), 1.0), line));
2933 scene.push_quad(Quad::new(Rect::new(f.x(), f.max_y() - 1.0, f.width(), 1.0), line));
2934 })),
2935 ))
2936 }
2937
2938 fn band(
2941 cx: &mut BuildCx<'_>,
2942 cells: Vec<NodeId>,
2943 s: &Shared<'_>,
2944 offset_x: f32,
2945 fill: Color,
2946 top_line: bool,
2947 height: f32,
2948 ) -> NodeId {
2949 let theme = *cx.theme();
2950 let inner = cx.container(
2951 LayoutStyle {
2952 width: Some(s.total_width),
2953 height: Some(height),
2954 ..LayoutStyle::stack(Axis::Horizontal)
2955 },
2956 &cells,
2957 Paint::None,
2958 );
2959 let line = theme.color(Semantic::Border);
2960 let outer = cx.container(
2961 LayoutStyle {
2962 height: Some(height),
2963 overflow_hidden: true,
2964 align_self: Some(Align::Stretch),
2965 ..LayoutStyle::stack(Axis::Horizontal)
2966 },
2967 &[inner],
2968 Paint::Custom(Rc::new(move |f: Rect, scene| {
2969 scene.push_quad(Quad::new(f, fill));
2970 let y = if top_line { f.y() } else { f.max_y() - 1.0 };
2971 scene.push_quad(Quad::new(Rect::new(f.x(), y, f.width(), 1.0), line));
2972 })),
2973 );
2974 cx.clip(outer);
2975 cx.translate(outer, Point::new(-offset_x, 0.0));
2976 outer
2977 }
2978}
2979
2980fn key_handler(s: &Shared<'_>, mode: SelectionMode, clipboard: crate::Clipboard) -> crate::KeyHandler {
2981 let (result, h) = (s.result.clone(), s.h.clone());
2982 let visible: Vec<usize> = s.visible.to_vec();
2983 let columns: Vec<(usize, bool)> = s.visible.iter().map(|&c| (c, s.grid.columns[c].editable)).collect();
2984 let rows = s.rows_rc.clone();
2985 let display: Vec<GridColumn> = s.grid.columns.clone();
2986 let activate_row = s.grid.on_row_activate.clone();
2987 let delete_rows = s.grid.on_delete_rows.clone();
2988 let page_size = s.arrangement.page_size;
2989 let has_details = s.grid.row_detail.is_some();
2990 Rc::new(move |key, mods| {
2991 let len = result.rows.len();
2992 if len == 0 {
2993 return false;
2994 }
2995 let current = h.focus_row.get().unwrap_or(0).min(len - 1);
2996 let data_rows =
2997 || result.rows.iter().filter_map(|v| if let ViewRow::Data(r) = v { Some(*r) } else { None });
2998 if mods.ctrl || mods.meta {
2999 match key {
3000 Key::Character(c) if c.eq_ignore_ascii_case("c") => {
3002 let selection = h.selection.get();
3003 let mut picked: Vec<usize> = data_rows().filter(|r| selection.contains(r)).collect();
3004 if picked.is_empty()
3005 && let ViewRow::Data(r) = result.rows[current]
3006 {
3007 picked.push(r);
3008 }
3009 if !picked.is_empty() {
3010 clipboard.write(&to_tsv(&display, &rows, &visible, &picked));
3011 }
3012 return true;
3013 }
3014 Key::Character(c) if c.eq_ignore_ascii_case("a") && mode == SelectionMode::Multiple => {
3015 h.selection.set(data_rows().collect());
3016 return true;
3017 }
3018 _ => return false,
3019 }
3020 }
3021 let activate = |i: usize| match &result.rows[i] {
3023 ViewRow::Data(r) => {
3024 if let Some(&(c, _)) = columns.iter().find(|(_, editable)| *editable) {
3025 let value = rows[*r].get(c).cloned().unwrap_or_default();
3026 h.edit_buffer.set(display[c].display(&value));
3027 h.editing.set(Some((*r, c)));
3028 } else if let Some(a) = &activate_row {
3029 a(*r);
3030 }
3031 }
3032 ViewRow::Group { key, .. } => h.query.update(|q| q.toggle_collapsed(key)),
3033 };
3034 let move_to = |i: usize| {
3035 h.focus_row.set(Some(i));
3036 h.reveal.set(Some(i));
3037 if let Some(size) = page_size {
3038 h.page.set(i / size);
3039 }
3040 if let ViewRow::Data(r) = result.rows[i] {
3041 if mode == SelectionMode::Single {
3042 h.selection.set(BTreeSet::from([r]));
3043 } else if mode == SelectionMode::Multiple && mods.shift {
3044 h.selection.update(|s| {
3045 s.insert(r);
3046 });
3047 }
3048 }
3049 };
3050 match key {
3051 Key::ArrowDown => move_to((current + 1).min(len - 1)),
3052 Key::ArrowUp => move_to(current.saturating_sub(1)),
3053 Key::Home => move_to(0),
3054 Key::End => move_to(len - 1),
3055 Key::PageDown => move_to((current + 15).min(len - 1)),
3056 Key::PageUp => move_to(current.saturating_sub(15)),
3057 Key::Space => match &result.rows[current] {
3058 ViewRow::Data(r) if mode == SelectionMode::Multiple => h.selection.update(|s| {
3059 if !s.remove(r) {
3060 s.insert(*r);
3061 }
3062 }),
3063 ViewRow::Group { key, .. } => h.query.update(|q| q.toggle_collapsed(key)),
3064 _ => return false,
3065 },
3066 Key::ArrowLeft | Key::ArrowRight => match &result.rows[current] {
3067 ViewRow::Group { key: group_key, collapsed, .. } => {
3068 let expand = matches!(key, Key::ArrowRight);
3069 if *collapsed == expand {
3070 h.query.update(|q| q.toggle_collapsed(group_key));
3071 }
3072 }
3073 ViewRow::Data(row) if has_details => {
3074 set_details(&h.expanded, *row, matches!(key, Key::ArrowRight));
3075 }
3076 _ => return false,
3077 },
3078 Key::Enter => activate(current),
3079 Key::Delete if delete_rows.is_some() && !h.selection.get().is_empty() => {
3080 if let Some(delete) = &delete_rows {
3081 delete(h.selection.get().into_iter().collect());
3082 }
3083 }
3084 Key::Other(k) if k.contains("F2") => activate(current),
3085 _ => return false,
3086 }
3087 true
3088 })
3089}
3090
3091fn current_layout(h: &Handles, columns: &[GridColumn]) -> GridLayout {
3092 layout::capture(columns, &h.query.peek(), &h.widths.peek(), &h.arrange.peek())
3093}
3094
3095fn apply_layout(h: &Handles, columns: &[GridColumn], wanted: &GridLayout) {
3096 let (mut query, mut widths, mut arrange) =
3097 (h.query.peek().clone(), h.widths.peek().clone(), h.arrange.peek().clone());
3098 layout::apply(wanted, columns, &mut query, &mut widths, &mut arrange);
3099 h.query.set(query);
3100 h.widths.set(widths);
3101 h.arrange.set(arrange);
3102 h.page.set(0);
3103}
3104
3105fn whole_filter(query: &GridQuery) -> String {
3111 let parts: Vec<&str> = query.filters.iter().map(|f| f.trim()).filter(|f| f.contains('[')).collect();
3112 if parts.len() < 2 {
3113 return parts.first().map_or_else(String::new, |f| (*f).to_owned());
3114 }
3115 use ravenrs_criteria::Criteria;
3116 match parts.iter().map(|f| Criteria::parse(f)).collect::<Result<Vec<_>, _>>() {
3117 Ok(all) => all.into_iter().reduce(Criteria::and).map(|c| c.to_string()).unwrap_or_default(),
3118 Err(_) => parts.iter().map(|f| format!("({f})")).collect::<Vec<_>>().join(" And "),
3119 }
3120}
3121
3122fn set_whole_filter(query: &mut GridQuery, columns: &[GridColumn], text: String) {
3125 let target = builder_column(query, columns);
3126 for f in query.filters.iter_mut().filter(|f| f.contains('[')) {
3127 f.clear();
3128 }
3129 if let Some(c) = target {
3130 query.set_filter(c, text);
3131 }
3132}
3133
3134fn builder_column(query: &GridQuery, columns: &[GridColumn]) -> Option<usize> {
3135 let open = || (0..columns.len()).filter(|&c| columns[c].filterable);
3136 open()
3137 .find(|&c| query.filter(c).contains('['))
3138 .or_else(|| open().find(|&c| query.filter(c).trim().is_empty()))
3139 .or_else(|| open().next())
3140}
3141
3142fn set_details(expanded: &Binding<BTreeSet<usize>>, row: usize, open: bool) {
3143 expanded.update(|set| {
3144 if open {
3145 set.insert(row);
3146 } else {
3147 set.remove(&row);
3148 }
3149 });
3150}
3151
3152fn remote_key_handler(
3153 h: &Handles,
3154 len: usize,
3155 mode: SelectionMode,
3156 details: bool,
3157 edit: Option<(RemoteRows, usize, GridColumn)>,
3158) -> crate::KeyHandler {
3159 let h = h.clone();
3160 Rc::new(move |key, _| {
3161 if len == 0 {
3162 return false;
3163 }
3164 let current = h.focus_row.get().unwrap_or(0).min(len - 1);
3165 let next = match key {
3166 key if edit.is_some()
3167 && (*key == Key::Enter || matches!(key, Key::Other(k) if k.contains("F2"))) =>
3168 {
3169 if let Some((rows, c, column)) = &edit
3170 && let Some(values) = rows.row(current)
3171 {
3172 let value = values.get(*c).cloned().unwrap_or_default();
3173 h.edit_buffer.set(column.display(&value));
3174 h.editing.set(Some((current, *c)));
3175 }
3176 return true;
3177 }
3178 Key::ArrowLeft | Key::ArrowRight if details => {
3179 set_details(&h.expanded, current, matches!(key, Key::ArrowRight));
3180 return true;
3181 }
3182 Key::ArrowDown => (current + 1).min(len - 1),
3183 Key::ArrowUp => current.saturating_sub(1),
3184 Key::Home => 0,
3185 Key::End => len - 1,
3186 Key::PageDown => (current + 15).min(len - 1),
3187 Key::PageUp => current.saturating_sub(15),
3188 _ => return false,
3189 };
3190 h.focus_row.set(Some(next));
3191 h.reveal.set(Some(next));
3192 if mode == SelectionMode::Single {
3193 h.selection.set(BTreeSet::from([next]));
3194 }
3195 true
3196 })
3197}
3198
3199impl View for DataGrid {
3200 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
3201 let theme = *cx.theme();
3202 let h = self.handles(cx);
3203 h.dragged.set(false);
3205 if h.original.borrow().is_none() {
3207 *h.original.borrow_mut() = Some(current_layout(&h, &self.columns));
3208 }
3209 if let Some(binding) = &self.layout {
3210 let outside = binding.get();
3211 let changed = h.seen_layout.borrow().as_ref() != Some(&outside);
3212 if changed && outside != GridLayout::default() {
3213 apply_layout(&h, &self.columns, &outside);
3214 }
3215 *h.seen_layout.borrow_mut() = Some(outside);
3216 }
3217 if let Some(remote) = &self.remote {
3218 remote.watch();
3219 }
3220 let rows = self.data.rows();
3221 let query = h.query.get();
3222 let arrangement = h.arrange.get();
3223 let cache: Rc<RefCell<Option<Cached>>> = cx.use_state(|| Rc::new(RefCell::new(None))).get();
3224 let result = if self.remote.is_some() {
3225 let totals = self.remote.as_ref().map(RemoteRows::totals).unwrap_or_default();
3226 Rc::new(QueryResult { totals, ..QueryResult::default() })
3227 } else {
3228 let mut slot = cache.borrow_mut();
3229 match &*slot {
3230 Some(c)
3231 if c.query == query
3232 && Rc::ptr_eq(&c.rows, &rows)
3233 && c.summaries == arrangement.summaries =>
3234 {
3235 c.result.clone()
3236 }
3237 _ => {
3238 let _span = tracing::trace_span!("ravenrs::datagrid::query", rows = rows.len()).entered();
3239 let mut columns = self.columns.clone();
3241 for (column, summary) in columns.iter_mut().zip(&arrangement.summaries) {
3242 column.summary = *summary;
3243 }
3244 let result = Rc::new(run_query(&columns, &rows, &query));
3245 *slot = Some(Cached {
3246 summaries: arrangement.summaries.clone(),
3247 query: query.clone(),
3248 rows: rows.clone(),
3249 result: result.clone(),
3250 });
3251 result
3252 }
3253 }
3254 };
3255 {
3256 let about = (Rc::as_ptr(&result) as usize, arrangement.formulas.clone());
3258 let mut cache = h.formula_cache.borrow_mut();
3259 if cache.about != about {
3260 cache.about = about;
3261 cache.answers.clear();
3262 }
3263 }
3264 let by_formula = arrangement.formulas.iter().any(Option::is_some);
3265 let ranges = Rc::new(if by_formula { group_ranges(&result) } else { Vec::new() });
3266 let mut widths = h.widths.get();
3267 let visible: Vec<usize> = arrangement.visible(&query);
3268 if let Some(&first) = visible.first() {
3270 widths[first] += query.group_by.len() as f32 * GROUP_INDENT;
3271 }
3272 let select_width = if self.selection_mode == SelectionMode::Multiple { CHECK_WIDTH } else { 0.0 }
3273 + if self.row_detail.is_some() { DETAIL_ARROW_WIDTH } else { 0.0 };
3274 let fill_seen: Rc<Cell<(f32, f32)>> = cx.use_state(|| Rc::new(Cell::new((0.0, 0.0)))).get();
3275 if let Some(&fill) = visible.iter().find(|&&c| self.columns[c].fill) {
3276 match cx.scroll_viewport() {
3277 Some(view) => {
3278 let used = select_width
3279 + visible.iter().map(|&c| widths[c]).sum::<f32>()
3280 + self.row_actions.len() as f32 * ROW_ACTION_WIDTH;
3281 widths[fill] += (view.width - used).max(0.0);
3282 let seen = (view.width, cx.viewport().width);
3286 if fill_seen.replace(seen) != seen {
3287 let now = cx.now();
3288 cx.wake_at(now);
3289 }
3290 }
3291 None => {
3293 let now = cx.now();
3294 cx.wake_at(now);
3295 }
3296 }
3297 }
3298 let total_width = select_width
3299 + visible.iter().map(|&c| widths[c]).sum::<f32>()
3300 + self.row_actions.len() as f32 * ROW_ACTION_WIDTH;
3301 let selected = h.selection.get();
3302 let focus_row = h.focus_row.get();
3303 let editing = h.editing.get();
3304 let offset = cx.scroll_offset();
3305 let mut fixed = Vec::new();
3306 let mut x = select_width;
3307 for &c in &visible {
3308 if arrangement.pinned.get(c).copied().unwrap_or(false) {
3309 fixed.push((c, x));
3310 x += widths[c];
3311 }
3312 }
3313 let last_fixed = fixed.last().map(|(c, _)| *c);
3314 let fixed_width = if fixed.is_empty() { 0.0 } else { x };
3315 let drag = h.drag.get();
3316 let column_facts: Rc<Vec<(String, bool)>> =
3317 Rc::new(self.columns.iter().map(|c| (c.title.clone(), c.groupable)).collect());
3318 let lines: Rc<Vec<f32>> = Rc::new(if self.vertical_lines {
3319 let mut x = select_width;
3320 visible
3321 .iter()
3322 .map(|&c| {
3323 x += widths[c];
3324 x - 1.0
3325 })
3326 .collect()
3327 } else {
3328 Vec::new()
3329 });
3330 let shared = Shared {
3331 grid: self,
3332 rows: &rows,
3333 rows_rc: rows.clone(),
3334 result: result.clone(),
3335 visible: &visible,
3336 widths: &widths,
3337 total_width,
3338 select_width,
3339 selected: &selected,
3340 focus_row,
3341 editing,
3342 h: h.clone(),
3343 arrangement: &arrangement,
3344 fixed,
3345 last_fixed,
3346 fixed_width,
3347 offset_x: offset.x,
3348 indent: query.group_by.len() as f32 * GROUP_INDENT,
3349 column_facts: column_facts.clone(),
3350 drop_before: drag.and_then(|d| if let DropTarget::Column(b) = d.target { Some(b) } else { None }),
3351 lines,
3352 ranges,
3353 };
3354
3355 let total = self.remote.as_ref().map_or(result.rows.len(), RemoteRows::len);
3357 let page_size = arrangement.page_size;
3358 let pages = page_size.map_or(1, |size| total.div_ceil(size).max(1));
3359 let page = h.page.get().min(pages - 1);
3360 let base = page_size.map_or(0, |size| page * size);
3361 let len = page_size.map_or(total, |size| size.min(total.saturating_sub(base)));
3362 let shown_page: Rc<Cell<usize>> = cx.use_state(|| Rc::new(Cell::new(0))).get();
3363
3364 let mut sections = Vec::new();
3365 if self.toolbar {
3366 sections.push(self.toolbar_view(cx, &h, &result, &rows, total));
3367 }
3368 if let Some(panel) = self.group_panel_view(cx, &h, &rows, &column_facts) {
3369 sections.push(panel);
3370 }
3371 if let Some(scenarios) = &self.scenarios {
3372 sections.push(self.scenario_dialog(cx, &h, scenarios));
3373 }
3374 let filtering = query.filters.iter().any(|f| !f.trim().is_empty());
3376 if self.filter_builder || filtering || h.building_filter.get() {
3377 sections.push(self.filter_dialog(cx, &h, &rows));
3378 }
3379
3380 h.zones.borrow_mut().cells.retain(|c, _| visible.contains(c));
3383 let select_fixed = shared.fixed_width > 0.0;
3384 let mut header_cells = Vec::new();
3385 if select_fixed {
3386 header_cells.push(cx.container(
3387 LayoutStyle { width: Some(shared.fixed_width), ..LayoutStyle::leaf() },
3388 &[],
3389 Paint::None,
3390 ));
3391 }
3392 if select_width > 0.0 && self.selection_mode != SelectionMode::Multiple {
3393 header_cells.push(cx.container(
3394 LayoutStyle {
3395 width: Some(select_width),
3396 absolute: select_fixed.then_some((0.0, 0.0)),
3397 ..LayoutStyle::leaf()
3398 },
3399 &[],
3400 Paint::None,
3401 ));
3402 } else if select_width > 0.0 {
3403 let all = !result.matched.is_empty() && result.matched.iter().all(|r| selected.contains(r));
3404 let b = crate::inputs::check_box(cx, &theme, all, false);
3405 let surface = theme.color(Semantic::Surface);
3406 let cell = cx.container(
3407 LayoutStyle {
3408 width: Some(select_width),
3409 height: Some(HEADER_HEIGHT),
3410 align: Align::Center,
3411 justify: Justify::Center,
3412 absolute: select_fixed.then_some((0.0, 0.0)),
3413 ..LayoutStyle::stack(Axis::Horizontal)
3414 },
3415 &[b],
3416 if select_fixed { Paint::Quad(QuadPaint::new(surface)) } else { Paint::None },
3417 );
3418 if select_fixed {
3419 cx.translate(cell, Point::new(offset.x, 0.0));
3420 }
3421 let (sel, matched) = (h.selection.clone(), result.matched.clone());
3422 cx.on_click(
3423 cell,
3424 Rc::new(move || {
3425 sel.set(if all { BTreeSet::new() } else { matched.iter().copied().collect() })
3426 }),
3427 );
3428 cx.cursor(cell, CursorIcon::Pointer);
3429 let name = cx.labels().grid_select_row.clone();
3430 cx.semantics(cell, Semantics::new(Role::CheckBox).name(name).toggled(all));
3431 header_cells.push(cell);
3432 }
3433 let flowing: Vec<usize> = visible.iter().copied().filter(|c| shared.fixed_x(*c).is_none()).collect();
3434 for &c in flowing.iter().chain(shared.fixed.iter().map(|(c, _)| c)) {
3435 header_cells.push(cx.keyed_child(c as u64, &HeaderCell { column: c, shared: &shared }));
3436 }
3437 let zones = h.zones.clone();
3438 let band_fill = theme.color(Semantic::Control).faded(0.6);
3439 let header = Self::band(cx, header_cells, &shared, offset.x, band_fill, false, HEADER_HEIGHT);
3440 let header_zone = cx.container(
3441 LayoutStyle {
3442 align: Align::Stretch,
3443 align_self: Some(Align::Stretch),
3444 ..LayoutStyle::stack(Axis::Vertical)
3445 },
3446 &[header],
3447 Paint::Custom(Rc::new(move |f: Rect, _| zones.borrow_mut().header = f)),
3448 );
3449 sections.push(header_zone);
3450
3451 if self.filter_row {
3452 let mut cells = Vec::new();
3453 if select_fixed {
3454 cells.push(cx.container(
3455 LayoutStyle { width: Some(shared.fixed_width), ..LayoutStyle::leaf() },
3456 &[],
3457 Paint::None,
3458 ));
3459 } else if select_width > 0.0 {
3460 cells.push(cx.container(
3461 LayoutStyle { width: Some(select_width), ..LayoutStyle::leaf() },
3462 &[],
3463 Paint::None,
3464 ));
3465 }
3466 for &c in flowing.iter().chain(shared.fixed.iter().map(|(c, _)| c)) {
3467 cells.push(cx.keyed_child(1000 + c as u64, &FilterCell { column: c, shared: &shared }));
3468 }
3469 sections.push(Self::band(cx, cells, &shared, offset.x, Color::TRANSPARENT, false, HEADER_HEIGHT));
3470 }
3471
3472 let rh = self.row_height;
3474 let viewport_h =
3475 cx.scroll_viewport().map_or(self.height.unwrap_or(cx.viewport().height), |s| s.height);
3476 let viewport_w = cx.scroll_viewport().map_or(cx.viewport().width, |s| s.width);
3477 let expanded = if self.row_detail.is_some() { h.expanded.get() } else { BTreeSet::new() };
3478 let tops = RowTops {
3479 row_height: rh,
3480 detail_height: self.detail_height,
3481 open: if expanded.is_empty() {
3482 Vec::new()
3483 } else {
3484 (0..len)
3485 .filter(|&i| match result.rows.get(base + i) {
3486 Some(ViewRow::Data(r)) => expanded.contains(r),
3487 Some(_) => false,
3488 None => self.remote.is_some() && expanded.contains(&(base + i)),
3489 })
3490 .collect()
3491 },
3492 };
3493 if shown_page.replace(page) != page {
3494 cx.set_scroll_offset(Point::new(offset.x, 0.0));
3495 }
3496 if let Some(i) = h.reveal.take() {
3497 let top = tops.top(i.saturating_sub(base));
3498 let y = if top < offset.y {
3499 Some(top)
3500 } else if top + rh > offset.y + viewport_h {
3501 Some(top + rh - viewport_h)
3502 } else {
3503 None
3504 };
3505 if let Some(y) = y {
3506 cx.set_scroll_offset(Point::new(offset.x, y.max(0.0)));
3507 }
3508 }
3509 let overscan = 6;
3510 let first = tops.row_at(offset.y, len).saturating_sub(overscan).min(len);
3511 let last = (tops.row_at(offset.y + viewport_h, len) + 1 + overscan).min(len);
3512 if let Some(remote) = &self.remote {
3513 let (from, to) = (base + first, base + last.max(first + 1));
3514 let now = cx.now();
3515 if let Some(when) = remote.want(from, to, &query, now) {
3516 cx.wake_at(when);
3517 }
3518 }
3519 let mut body_rows = Vec::with_capacity(last - first + 2);
3520 body_rows.push(cx.container(
3521 LayoutStyle { height: Some(tops.top(first)), width: Some(total_width), ..LayoutStyle::leaf() },
3522 &[],
3523 Paint::None,
3524 ));
3525 let mut since_group = (base + first)
3527 - result.rows[..(base + first).min(result.rows.len())]
3528 .iter()
3529 .rposition(|row| matches!(row, ViewRow::Group { .. }))
3530 .map_or(0, |group| group + 1);
3531 for i in (first..last).map(|i| base + i) {
3532 if let Some(remote) = &self.remote {
3533 let node = match remote.row(i) {
3534 Some(values) => cx.keyed_child(
3535 i as u64,
3536 &DataRow { index: i, even: i % 2 == 1, row: i, values, shared: &shared },
3537 ),
3538 None => cx.keyed_child(i as u64 | (1 << 61), &WaitingRow { shared: &shared }),
3539 };
3540 body_rows.push(node);
3541 if expanded.contains(&i)
3542 && let Some(values) = remote.row(i)
3543 {
3544 let detail = DetailRow { row: i, values: &values, shared: &shared, width: viewport_w };
3545 body_rows.push(cx.keyed_child(i as u64 | (1 << 60), &detail));
3546 }
3547 continue;
3548 }
3549 let node = match &result.rows[i] {
3550 ViewRow::Data(r) => cx.keyed_child(
3551 *r as u64,
3552 &DataRow {
3553 index: i,
3554 even: since_group % 2 == 1,
3555 row: *r,
3556 values: rows[*r].clone(),
3557 shared: &shared,
3558 },
3559 ),
3560 ViewRow::Group { key, .. } => {
3561 since_group = 0;
3562 cx.keyed_child(hash_of(key) | (1 << 62), &GroupRow { index: i, shared: &shared })
3563 }
3564 };
3565 if matches!(result.rows[i], ViewRow::Data(_)) {
3566 since_group += 1;
3567 }
3568 body_rows.push(node);
3569 if let ViewRow::Data(r) = &result.rows[i]
3570 && expanded.contains(r)
3571 {
3572 let detail = DetailRow { row: *r, values: &rows[*r], shared: &shared, width: viewport_w };
3573 body_rows.push(cx.keyed_child(*r as u64 | (1 << 60), &detail));
3574 }
3575 }
3576 body_rows.push(cx.container(
3577 LayoutStyle {
3578 height: Some(tops.top(len) - tops.top(last)),
3579 width: Some(total_width),
3580 ..LayoutStyle::leaf()
3581 },
3582 &[],
3583 Paint::None,
3584 ));
3585 let failure = self.remote.as_ref().and_then(RemoteRows::error);
3586 if let Some(message) = &failure {
3587 let text = util::label(cx, message, TextStyle::Body, None, theme.color(Semantic::Danger));
3588 let remote = self.remote.clone();
3589 let again = cx.child(
3590 &Button::new(cx.labels().grid_try_again.clone())
3591 .style(ButtonStyle::Secondary)
3592 .size(ButtonSize::Small)
3593 .on_click(move || {
3594 if let Some(remote) = &remote {
3595 remote.retry();
3596 }
3597 }),
3598 );
3599 body_rows.insert(
3600 0,
3601 cx.container(
3602 LayoutStyle {
3603 padding: Insets::uniform(16.0),
3604 spacing: 12.0,
3605 align: Align::Center,
3606 ..LayoutStyle::stack(Axis::Horizontal)
3607 },
3608 &[text, again],
3609 Paint::None,
3610 ),
3611 );
3612 } else if total == 0 {
3613 let empty = self
3614 .empty_text
3615 .clone()
3616 .filter(|_| self.remote.is_none() && self.data.rows().is_empty())
3617 .unwrap_or_else(|| cx.labels().grid_no_rows.clone());
3618 let msg = util::label(cx, &empty, TextStyle::Body, None, theme.color(Semantic::InkMuted));
3619 body_rows.push(cx.container(
3620 LayoutStyle { padding: Insets::uniform(24.0), ..LayoutStyle::stack(Axis::Horizontal) },
3621 &[msg],
3622 Paint::None,
3623 ));
3624 }
3625 let content = cx.container(
3626 LayoutStyle { width: Some(total_width), ..LayoutStyle::stack(Axis::Vertical) },
3627 &body_rows,
3628 Paint::None,
3629 );
3630 let body = cx.container(
3631 LayoutStyle {
3632 grow: 1.0,
3633 min_height: Some(0.0),
3634 overflow_hidden: true,
3635 align_self: Some(Align::Stretch),
3636 ..LayoutStyle::stack(Axis::Vertical)
3637 },
3638 &[content],
3639 Paint::None,
3640 );
3641 cx.scrollable_virtual(body, true);
3642 cx.semantics(
3643 body,
3644 Semantics::new(Role::Grid)
3645 .name(cx.labels().grid_name.clone())
3646 .description(format!("{total} {}", cx.labels().grid_rows)),
3647 );
3648 cx.focusable(body, 0.0);
3649 let clipboard = cx.clipboard();
3650 let keys = if self.remote.is_some() {
3651 {
3652 let edit = self.remote.clone().filter(|_| self.on_cell_edit.is_some()).and_then(|rows| {
3654 visible
3655 .iter()
3656 .find(|&&c| self.columns[c].editable)
3657 .map(|&c| (rows, c, self.columns[c].clone()))
3658 });
3659 remote_key_handler(&h, total, self.selection_mode, self.row_detail.is_some(), edit)
3660 }
3661 } else {
3662 key_handler(&shared, self.selection_mode, clipboard)
3663 };
3664 cx.on_key(body, keys);
3665 sections.push(body);
3666
3667 let filters: Vec<String> = (0..self.columns.len())
3669 .filter(|c| !query.filter(*c).trim().is_empty())
3670 .map(|c| {
3671 let text = query.filter(c);
3672 if text.contains('[') {
3673 text.to_owned()
3674 } else {
3675 format!("[{}]: {text}", self.columns[c].title)
3676 }
3677 })
3678 .collect();
3679 if !filters.is_empty() {
3680 let labels = cx.labels();
3681 let muted = theme.color(Semantic::InkMuted);
3682 let icon = util::icon(cx, IconName::Filter, 14.0, theme.color(Semantic::Accent));
3683 let text = format!("{}: {}", labels.grid_filter_label, filters.join(" ยท "));
3684 let label = util::label(cx, &text, TextStyle::Callout, None, muted);
3685 let open = h.building_filter.clone();
3686 let edit = cx.child(
3687 &Button::new(labels.grid_edit_filter.clone())
3688 .style(ButtonStyle::Ghost)
3689 .size(ButtonSize::Small)
3690 .icon(IconName::Pencil)
3691 .on_click(move || open.set(true)),
3692 );
3693 let q = h.query.clone();
3694 let clear = cx.child(
3695 &Button::new(labels.grid_clear_filter.clone())
3696 .style(ButtonStyle::Ghost)
3697 .size(ButtonSize::Small)
3698 .on_click(move || q.update(|q| q.filters.clear())),
3699 );
3700 let line = theme.color(Semantic::Border);
3701 sections.push(cx.container(
3702 LayoutStyle {
3703 padding: Insets::symmetric(4.0, 12.0),
3704 spacing: 8.0,
3705 align: Align::Center,
3706 align_self: Some(Align::Stretch),
3707 ..LayoutStyle::stack(Axis::Horizontal)
3708 },
3709 &[icon, label, edit, clear],
3710 Paint::Custom(Rc::new(move |f: Rect, scene| {
3711 scene.push_quad(Quad::new(Rect::new(f.x(), f.y(), f.width(), 1.0), line));
3712 })),
3713 ));
3714 }
3715
3716 if self.footer
3718 && (arrangement.summaries.iter().any(Option::is_some)
3719 || arrangement.formulas.iter().any(Option::is_some))
3720 {
3721 let mut cells = Vec::new();
3722 if select_fixed {
3723 cells.push(cx.container(
3724 LayoutStyle { width: Some(shared.fixed_width), ..LayoutStyle::leaf() },
3725 &[],
3726 Paint::None,
3727 ));
3728 } else if select_width > 0.0 {
3729 cells.push(cx.container(
3730 LayoutStyle { width: Some(select_width), ..LayoutStyle::leaf() },
3731 &[],
3732 Paint::None,
3733 ));
3734 }
3735 for &c in flowing.iter().chain(shared.fixed.iter().map(|(c, _)| c)) {
3736 cells.push(cx.keyed_child(2000 + c as u64, &FooterCell { column: c, shared: &shared }));
3737 }
3738 sections.push(Self::band(cx, cells, &shared, offset.x, band_fill, true, FOOTER_HEIGHT));
3739 }
3740
3741 if page_size.is_some() {
3743 let labels = cx.labels();
3744 let muted = theme.color(Semantic::InkMuted);
3745 let fmt = crate::grid::model::NumberFormat::default();
3746 let range = if total == 0 {
3747 String::new()
3748 } else {
3749 format!(
3750 "{}โ{} {} {}",
3751 fmt.format((base + 1) as f64),
3752 fmt.format((base + len) as f64),
3753 labels.grid_of,
3754 fmt.format(total as f64)
3755 )
3756 };
3757 let (read, write, reset) = (h.arrange.clone(), h.arrange.clone(), h.page.clone());
3758 let chosen = Binding::new(
3759 move || {
3760 let now = read.peek().page_size.unwrap_or(PAGE_SIZES[0]);
3761 PAGE_SIZES.iter().position(|s| *s == now).unwrap_or(0)
3762 },
3763 move |i: usize| {
3764 write.update(|a| a.page_size = PAGE_SIZES.get(i).copied());
3765 reset.set(0);
3766 },
3767 );
3768 let (per_page, picker, status, pager, gap) = (
3769 util::label(cx, &labels.grid_rows_per_page, TextStyle::Callout, None, muted),
3770 cx.child(
3771 &Select::new(chosen, PAGE_SIZES.map(|s| s.to_string()))
3772 .label(labels.grid_rows_per_page.clone())
3773 .width(84.0),
3774 ),
3775 util::label(cx, &range, TextStyle::Callout, None, muted),
3776 cx.child(&Pagination::new(h.page.binding(), pages).compact().labels(
3777 labels.grid_previous.clone(),
3778 labels.grid_next.clone(),
3779 labels.grid_page.clone(),
3780 )),
3781 cx.child(&Spacer::new()),
3782 );
3783 let line = theme.color(Semantic::Border);
3784 sections.push(cx.container(
3785 LayoutStyle {
3786 padding: Insets::symmetric(6.0, 12.0),
3787 spacing: 8.0,
3788 align: Align::Center,
3789 align_self: Some(Align::Stretch),
3790 ..LayoutStyle::stack(Axis::Horizontal)
3791 },
3792 &[status, gap, per_page, picker, pager],
3793 Paint::Custom(Rc::new(move |f: Rect, scene| {
3794 scene.push_quad(Quad::new(Rect::new(f.x(), f.y(), f.width(), 1.0), line));
3795 })),
3796 ));
3797 }
3798
3799 let border = theme.color(Semantic::Border);
3800 let surface = theme.color(Semantic::Surface);
3801 let root = cx.container(
3802 LayoutStyle {
3803 height: self.height,
3804 grow: if self.height.is_none() { 1.0 } else { 0.0 },
3805 min_height: Some(200.0),
3806 align: Align::Stretch,
3807 align_self: Some(Align::Stretch),
3808 ..LayoutStyle::stack(Axis::Vertical)
3809 },
3810 §ions,
3811 Paint::Quad(QuadPaint {
3812 corner_radius: theme.radii.lg,
3813 border: Some(ravenrs_core::Border { width: 1.0, color: border }),
3814 ..QuadPaint::new(surface)
3815 }),
3816 );
3817 cx.clip(root);
3818
3819 if let Some(c) = h.summary_edit.get().filter(|c| *c < self.columns.len()) {
3821 let close: Rc<dyn Fn()> = {
3822 let edit = h.summary_edit.clone();
3823 Rc::new(move || edit.set(None))
3824 };
3825 let at =
3826 h.zones.borrow().cells.get(&c).map_or_else(
3827 || Point::new(offset.x + 40.0, 120.0),
3828 |r| Point::new(r.x(), r.max_y() + 4.0),
3829 );
3830 let editor = SummaryEditor {
3831 column: c,
3832 columns: self.columns.clone(),
3833 rows: rows.clone(),
3834 matched: result.clone(),
3835 arrange: h.arrange.clone(),
3836 draft: h.summary_draft.clone(),
3837 close: close.clone(),
3838 };
3839 cx.overlay(root, Placement::At(at), &PanelOf(&editor), Some(close));
3840 }
3841
3842 if let Some(d) = drag {
3844 let column = match d.source {
3845 DragSource::Column(c) | DragSource::Chip(c) => c,
3846 };
3847 let ghost = DragGhost {
3848 title: self.columns.get(column).map_or_else(String::new, |c| c.title.clone()),
3849 grouping: matches!(d.target, DropTarget::Group(_)),
3850 ungrouping: d.target == DropTarget::Ungroup,
3851 };
3852 let cancel = h.drag.clone();
3853 cx.overlay(
3854 root,
3855 Placement::At(Point::new(d.at.x + 12.0, d.at.y + 8.0)),
3856 &ghost,
3857 Some(Rc::new(move || cancel.set(None))),
3858 );
3859 }
3860
3861 if let Some(binding) = &self.layout {
3863 let now = current_layout(&h, &self.columns);
3864 let seen = h.seen_layout.borrow().clone();
3865 if seen.as_ref() != Some(&now) {
3866 *h.seen_layout.borrow_mut() = Some(now.clone());
3867 binding.set(now);
3868 }
3869 }
3870 root
3871 }
3872}
3873
3874#[cfg(test)]
3875mod tests {
3876 use super::*;
3877
3878 #[test]
3879 fn rows_stand_lower_after_open_details() {
3880 let tops = RowTops { row_height: 10.0, detail_height: 100.0, open: vec![1, 3] };
3881 assert_eq!(tops.top(0), 0.0);
3882 assert_eq!(tops.top(1), 10.0);
3883 assert_eq!(tops.top(2), 120.0, "row 1 and its details");
3884 assert_eq!(tops.top(4), 240.0);
3885 assert_eq!(tops.row_at(0.0, 6), 0);
3886 assert_eq!(tops.row_at(15.0, 6), 1);
3887 assert_eq!(tops.row_at(119.0, 6), 1, "inside the details of row 1");
3888 assert_eq!(tops.row_at(120.0, 6), 2);
3889 assert_eq!(tops.row_at(10_000.0, 6), 6, "past the end");
3890 }
3891
3892 #[test]
3893 fn the_filter_builder_does_not_take_over_a_filter_in_use() {
3894 let columns = [GridColumn::text("A").filterable(false), GridColumn::text("B"), GridColumn::text("C")];
3895 let mut query = GridQuery::default();
3896 assert_eq!(builder_column(&query, &columns), Some(1), "the first that can filter");
3897 query.set_filter(1, "ana".to_owned());
3898 assert_eq!(builder_column(&query, &columns), Some(2), "not over what someone typed");
3899 query.set_filter(2, "bia".to_owned());
3900 assert_eq!(builder_column(&query, &columns), Some(1), "with no room, the first");
3901 query.set_filter(2, "[B] = 'x'".to_owned());
3902 assert_eq!(builder_column(&query, &columns), Some(2), "its own expression, wherever it is");
3903 }
3904
3905 #[test]
3906 fn the_editor_opens_every_expression_as_one() {
3907 let columns = [GridColumn::text("A"), GridColumn::text("B"), GridColumn::text("C")];
3908 let mut query = GridQuery::default();
3909 assert_eq!(whole_filter(&query), "");
3910 query.set_filter(0, "ana".to_owned());
3911 query.set_filter(1, "[B] In ('x', 'y')".to_owned());
3912 assert_eq!(whole_filter(&query), "[B] In ('x', 'y')", "typed text stays in its column");
3913 query.set_filter(2, "[C] = 'z' Or [C] = 'w'".to_owned());
3914 assert_eq!(whole_filter(&query), "[B] In ('x', 'y') And ([C] = 'z' Or [C] = 'w')");
3915
3916 set_whole_filter(&mut query, &columns, "[C] = 'z'".to_owned());
3917 assert_eq!(query.filter(0), "ana");
3918 assert_eq!(query.filter(1), "[C] = 'z'", "where the first expression was");
3919 assert_eq!(query.filter(2), "");
3920 assert_eq!(whole_filter(&query), "[C] = 'z'");
3921 set_whole_filter(&mut query, &columns, String::new());
3922 assert_eq!(whole_filter(&query), "");
3923 assert_eq!(query.filter(0), "ana");
3924 }
3925
3926 #[test]
3927 fn with_nothing_open_a_row_is_its_number_of_heights_down() {
3928 let tops = RowTops { row_height: 36.0, detail_height: 240.0, open: vec![] };
3929 for row in [0, 1, 5, 100_000] {
3930 assert_eq!(tops.top(row), row as f32 * 36.0);
3931 assert_eq!(tops.row_at(row as f32 * 36.0 + 1.0, 200_000), row);
3932 }
3933 }
3934}