1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use dioxus::{events::KeyboardEvent, prelude::*};
5
6use crate::components::form::{form_value_to_radio_key, form_value_to_string_vec};
7use crate::components::overlay::{OverlayKey, OverlayKind, use_overlay};
8
9pub type OptionKey = String;
14
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17pub struct SelectOption {
18 pub key: OptionKey,
19 pub label: String,
20 #[serde(default)]
21 pub disabled: bool,
22}
23
24#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
26pub struct OptionNode {
27 pub key: OptionKey,
28 pub label: String,
29 #[serde(default)]
30 pub disabled: bool,
31 #[serde(default)]
32 pub children: Vec<OptionNode>,
33}
34
35pub type TreeNode = OptionNode;
37
38pub type CascaderNode = OptionNode;
40
41pub fn value_to_option_key(val: Option<Value>) -> Option<OptionKey> {
48 form_value_to_radio_key(val)
49}
50
51pub fn option_key_to_value(key: &OptionKey) -> Value {
53 Value::String(key.clone())
54}
55
56pub fn value_to_option_keys(val: Option<Value>) -> Vec<OptionKey> {
61 form_value_to_string_vec(val)
62}
63
64pub fn option_keys_to_value(keys: &[OptionKey]) -> Value {
66 let items = keys.iter().cloned().map(Value::String).collect();
67 Value::Array(items)
68}
69
70pub fn value_to_path(val: Option<Value>) -> Vec<OptionKey> {
75 match val {
76 Some(Value::Array(items)) => items
77 .into_iter()
78 .filter_map(|v| v.as_str().map(|s| s.to_string()))
79 .collect(),
80 _ => Vec::new(),
81 }
82}
83
84pub fn path_to_value(path: &[OptionKey]) -> Value {
86 option_keys_to_value(path)
87}
88
89pub fn filter_options_by_query(options: &[SelectOption], query: &str) -> Vec<SelectOption> {
95 let trimmed = query.trim();
96 if trimmed.is_empty() {
97 return options.to_vec();
98 }
99 let q = trimmed.to_lowercase();
100 options
101 .iter()
102 .filter(|opt| opt.label.to_lowercase().contains(&q))
103 .cloned()
104 .collect()
105}
106
107pub fn toggle_option_key(current: &[OptionKey], key: &str) -> Vec<OptionKey> {
112 let mut next = current.to_vec();
113 if let Some(pos) = next.iter().position(|v| v == key) {
114 next.remove(pos);
115 } else {
116 next.push(key.to_string());
117 }
118 next
119}
120
121#[derive(Clone, Copy)]
125pub struct DropdownLayer {
126 #[allow(dead_code)] pub key: Signal<Option<OverlayKey>>,
128 pub z_index: Signal<i32>,
129}
130
131#[derive(Clone, Copy)]
133pub struct FloatingLayer {
134 pub key: Signal<Option<OverlayKey>>,
135 pub z_index: Signal<i32>,
136}
137
138pub fn use_floating_layer(kind: OverlayKind, open: bool) -> FloatingLayer {
146 let overlay = use_overlay();
147 let entry_key: Signal<Option<OverlayKey>> = use_signal(|| None);
148 let z_index: Signal<i32> = use_signal(|| 1000);
149
150 {
151 let overlay = overlay.clone();
152 let mut key_signal = entry_key;
153 let mut z_signal = z_index;
154 use_effect(move || {
155 if let Some(handle) = overlay.clone() {
156 let current_key = *key_signal.read();
157 if open {
158 if current_key.is_none() {
159 let (key, meta) = handle.open(kind, false);
160 z_signal.set(meta.z_index);
161 key_signal.set(Some(key));
162 }
163 } else if let Some(key) = current_key {
164 handle.close(key);
165 key_signal.set(None);
166 }
167 }
168 });
169 }
170
171 FloatingLayer {
172 key: entry_key,
173 z_index,
174 }
175}
176
177pub fn use_dropdown_layer(open: bool) -> DropdownLayer {
183 let FloatingLayer { key, z_index } = use_floating_layer(OverlayKind::Dropdown, open);
184
185 DropdownLayer { key, z_index }
186}
187
188pub fn next_active_index(current: Option<usize>, len: usize, direction: i32) -> Option<usize> {
197 if len == 0 {
198 return None;
199 }
200
201 let dir = if direction >= 0 { 1 } else { -1 };
202
203 match current {
204 None => {
205 if dir > 0 {
206 Some(0)
207 } else {
208 Some(len.saturating_sub(1))
209 }
210 }
211 Some(idx) => {
212 if len == 1 {
213 Some(0)
214 } else if dir > 0 {
215 Some((idx + 1) % len)
216 } else {
217 Some((idx + len - 1) % len)
218 }
219 }
220 }
221}
222
223pub fn handle_option_list_key_event(
228 evt: &KeyboardEvent,
229 options_len: usize,
230 active_index_signal: &Signal<Option<usize>>,
231) -> Option<usize> {
232 use dioxus::prelude::Key;
233
234 match evt.key() {
235 Key::ArrowDown => {
236 let mut signal = *active_index_signal;
237 let current = *signal.read();
238 let next = next_active_index(current, options_len, 1);
239 signal.set(next);
240 None
241 }
242 Key::ArrowUp => {
243 let mut signal = *active_index_signal;
244 let current = *signal.read();
245 let next = next_active_index(current, options_len, -1);
246 signal.set(next);
247 None
248 }
249 Key::Enter => *active_index_signal.read(),
250 Key::Escape => {
251 let mut signal = *active_index_signal;
252 signal.set(None);
253 None
254 }
255 _ => None,
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn value_to_and_from_single_key_round_trips() {
265 let original = OptionKey::from("foo");
266 let json = option_key_to_value(&original);
267 let parsed = value_to_option_key(Some(json));
268 assert_eq!(parsed, Some(original));
269 }
270
271 #[test]
272 fn value_to_and_from_multiple_keys_round_trips() {
273 let keys = vec!["a".to_string(), "b".to_string()];
274 let json = option_keys_to_value(&keys);
275 let parsed = value_to_option_keys(Some(json));
276 assert_eq!(parsed, keys);
277 }
278
279 #[test]
280 fn path_conversion_uses_string_array_representation() {
281 let path = vec!["root".to_string(), "child".to_string()];
282 let json = path_to_value(&path);
283 let parsed = value_to_path(Some(json));
284 assert_eq!(parsed, path);
285 }
286
287 #[test]
288 fn toggle_option_key_adds_and_removes_values() {
289 let current: Vec<OptionKey> = vec![];
290 let next = toggle_option_key(¤t, "x");
291 assert_eq!(next, vec!["x".to_string()]);
292
293 let next2 = toggle_option_key(&next, "x");
294 assert!(next2.is_empty());
295 }
296
297 #[test]
298 fn filter_options_by_query_matches_label_case_insensitively() {
299 let options = vec![
300 SelectOption {
301 key: "1".into(),
302 label: "Apple".into(),
303 disabled: false,
304 },
305 SelectOption {
306 key: "2".into(),
307 label: "Banana".into(),
308 disabled: false,
309 },
310 SelectOption {
311 key: "3".into(),
312 label: "Cherry".into(),
313 disabled: false,
314 },
315 ];
316 let filtered = filter_options_by_query(&options, "an");
317 let labels: Vec<String> = filtered.into_iter().map(|o| o.label).collect();
318 assert_eq!(labels, vec!["Banana".to_string()]);
319 }
320
321 #[test]
322 fn next_active_index_wraps_and_handles_empty() {
323 assert_eq!(next_active_index(None, 0, 1), None);
325 assert_eq!(next_active_index(Some(0), 0, 1), None);
326
327 assert_eq!(next_active_index(None, 3, 1), Some(0));
329 assert_eq!(next_active_index(None, 3, -1), Some(2));
330
331 assert_eq!(next_active_index(Some(0), 3, 1), Some(1));
333 assert_eq!(next_active_index(Some(2), 3, 1), Some(0));
334 assert_eq!(next_active_index(Some(0), 3, -1), Some(2));
335 assert_eq!(next_active_index(Some(1), 3, -1), Some(0));
336 }
337}