1use serde::{Deserialize, Serialize};
2use serde_json::{Number, Value};
3
4use crate::models::{GridCellPosition, GridColumnDef, GridColumnType, GridOptions, GridRow};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub enum GridMoveDirection {
9 Left,
10 Right,
11 Up,
12 Down,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct GridEditSession {
18 pub focused_cell: GridCellPosition,
19 pub editing_cell: GridCellPosition,
20 pub editing_value: String,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct GridFocusCellResult {
26 pub focused_cell: GridCellPosition,
27 pub should_begin_edit: bool,
28}
29
30pub fn is_grid_cell_position(
31 position: Option<&GridCellPosition>,
32 row_id: &str,
33 column_name: &str,
34) -> bool {
35 position
36 .is_some_and(|position| position.row_id == row_id && position.column_name == column_name)
37}
38
39pub fn begin_grid_edit_session(
40 row_id: impl Into<String>,
41 column_name: impl Into<String>,
42 editing_value: impl Into<String>,
43) -> GridEditSession {
44 let position = GridCellPosition {
45 row_id: row_id.into(),
46 column_name: column_name.into(),
47 };
48
49 GridEditSession {
50 focused_cell: position.clone(),
51 editing_cell: position,
52 editing_value: editing_value.into(),
53 }
54}
55
56pub fn should_grid_edit_on_focus(options: &GridOptions, column: &GridColumnDef) -> bool {
57 column.enable_cell_edit_on_focus || options.enable_cell_edit_on_focus
58}
59
60pub fn build_grid_focus_cell_result(
61 current_focused_cell: Option<&GridCellPosition>,
62 current_editing_cell: Option<&GridCellPosition>,
63 row_id: impl Into<String>,
64 column_name: impl Into<String>,
65 should_edit_on_focus: bool,
66 is_cell_editable: bool,
67) -> GridFocusCellResult {
68 let focused_cell = GridCellPosition {
69 row_id: row_id.into(),
70 column_name: column_name.into(),
71 };
72
73 let should_begin_edit = should_edit_on_focus
74 && is_cell_editable
75 && !is_grid_cell_position(
76 current_focused_cell,
77 &focused_cell.row_id,
78 &focused_cell.column_name,
79 )
80 && !is_grid_cell_position(
81 current_editing_cell,
82 &focused_cell.row_id,
83 &focused_cell.column_name,
84 );
85
86 GridFocusCellResult {
87 focused_cell,
88 should_begin_edit,
89 }
90}
91
92pub fn clear_grid_edit_session() -> (Option<GridCellPosition>, String) {
93 (None, String::new())
94}
95
96pub fn find_next_grid_cell<F>(
97 rows: &[GridRow],
98 columns: &[GridColumnDef],
99 row_id: &str,
100 column_name: &str,
101 direction: GridMoveDirection,
102 is_cell_allowed: Option<F>,
103) -> Option<GridCellPosition>
104where
105 F: Fn(&GridRow, &GridColumnDef) -> bool,
106{
107 let row_index = rows.iter().position(|candidate| candidate.id == row_id)?;
108 let column_index = columns
109 .iter()
110 .position(|candidate| candidate.name == column_name)?;
111
112 let mut next_row_index = row_index as isize;
113 let mut next_column_index = column_index as isize;
114
115 loop {
116 match direction {
117 GridMoveDirection::Left => {
118 next_column_index -= 1;
119 if next_column_index < 0 {
120 next_row_index -= 1;
121 next_column_index = columns.len() as isize - 1;
122 }
123 }
124 GridMoveDirection::Right => {
125 next_column_index += 1;
126 if next_column_index >= columns.len() as isize {
127 next_row_index += 1;
128 next_column_index = 0;
129 }
130 }
131 GridMoveDirection::Up => next_row_index -= 1,
132 GridMoveDirection::Down => next_row_index += 1,
133 }
134
135 if next_row_index < 0
136 || next_row_index >= rows.len() as isize
137 || next_column_index < 0
138 || next_column_index >= columns.len() as isize
139 {
140 return None;
141 }
142
143 let next_row = &rows[next_row_index as usize];
144 let next_column = &columns[next_column_index as usize];
145 let allowed = is_cell_allowed
146 .as_ref()
147 .is_none_or(|predicate| predicate(next_row, next_column));
148 if allowed {
149 return Some(GridCellPosition {
150 row_id: next_row.id.clone(),
151 column_name: next_column.name.clone(),
152 });
153 }
154 }
155}
156
157pub fn stringify_grid_editor_value(value: &Value) -> String {
158 match value {
159 Value::Null => String::new(),
160 Value::String(value) => value.clone(),
161 Value::Bool(value) => value.to_string(),
162 Value::Number(value) => value.to_string(),
163 other => other.to_string(),
164 }
165}
166
167pub fn parse_grid_edited_value(column: &GridColumnDef, value: &str, old_value: &Value) -> Value {
168 match column.r#type {
169 GridColumnType::Number => value
170 .parse::<f64>()
171 .ok()
172 .filter(|parsed| parsed.is_finite())
173 .and_then(Number::from_f64)
174 .map(Value::Number)
175 .unwrap_or_else(|| old_value.clone()),
176 GridColumnType::Boolean => Value::Bool(value == "true"),
177 GridColumnType::Date | GridColumnType::String | GridColumnType::Object => {
178 Value::String(value.to_string())
179 }
180 }
181}
182
183pub fn is_printable_grid_key(key: &str, ctrl_key: bool, meta_key: bool, alt_key: bool) -> bool {
184 key.chars().count() == 1 && !ctrl_key && !meta_key && !alt_key
185}