Skip to main content

ui_grid_core/
edit.rs

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}