1use crate::components::config_provider::use_config;
2use crate::components::form::use_form_item_control;
3use dioxus::events::KeyboardEvent;
4use dioxus::prelude::*;
5use serde_json::Value;
6
7#[derive(Clone, Debug, PartialEq)]
9pub struct SegmentedOption {
10 pub label: String,
11 pub value: String,
12 pub icon: Option<Element>,
13 pub tooltip: Option<String>,
14 pub disabled: bool,
15}
16
17impl SegmentedOption {
18 pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
19 Self {
20 label: label.into(),
21 value: value.into(),
22 icon: None,
23 tooltip: None,
24 disabled: false,
25 }
26 }
27}
28
29#[derive(Props, Clone, PartialEq)]
31pub struct SegmentedProps {
32 pub options: Vec<SegmentedOption>,
33 #[props(optional)]
35 pub value: Option<String>,
36 #[props(optional)]
38 pub default_value: Option<String>,
39 #[props(default)]
41 pub block: bool,
42 #[props(default)]
44 pub round: bool,
45 #[props(default)]
46 pub disabled: bool,
47 #[props(optional)]
48 pub class: Option<String>,
49 #[props(optional)]
50 pub style: Option<String>,
51 #[props(optional)]
52 pub on_change: Option<EventHandler<String>>,
53}
54
55#[component]
56pub fn Segmented(props: SegmentedProps) -> Element {
57 let SegmentedProps {
58 options,
59 value,
60 default_value,
61 block,
62 round,
63 disabled,
64 class,
65 style,
66 on_change,
67 } = props;
68
69 let config = use_config();
70 let form_control = use_form_item_control();
71 let controlled = value.is_some();
72
73 let inner = use_signal(|| default_value.clone());
74
75 {
77 let form_ctx = form_control.clone();
78 let prop_val = value.clone();
79 let mut inner_signal = inner.clone();
80 use_effect(move || {
81 let next = resolve_value(prop_val.clone(), &form_ctx, &inner_signal);
82 inner_signal.set(next);
83 });
84 }
85
86 let is_disabled =
87 disabled || config.disabled || form_control.as_ref().is_some_and(|ctx| ctx.is_disabled());
88
89 let mut class_list = vec!["adui-segmented".to_string()];
90 if block {
91 class_list.push("adui-segmented-block".into());
92 }
93 if round {
94 class_list.push("adui-segmented-round".into());
95 }
96 if is_disabled {
97 class_list.push("adui-segmented-disabled".into());
98 }
99 if let Some(extra) = class {
100 class_list.push(extra);
101 }
102 let class_attr = class_list.join(" ");
103 let style_attr = style.unwrap_or_default();
104
105 let current_value = resolve_value(value.clone(), &form_control, &inner).unwrap_or_else(|| {
106 options
107 .iter()
108 .find(|opt| !opt.disabled)
109 .map(|opt| opt.value.clone())
110 .unwrap_or_default()
111 });
112
113 let handle_key = {
114 let opts = options.clone();
115 let form_for_key = form_control.clone();
116 let mut inner_for_key = inner.clone();
117 let on_change_for_key = on_change.clone();
118 let value_for_key = value.clone();
119 let fallback_value = current_value.clone();
120 move |evt: KeyboardEvent| {
121 if is_disabled {
122 return;
123 }
124 let current = resolve_value(value_for_key.clone(), &form_for_key, &inner_for_key)
125 .unwrap_or_else(|| fallback_value.clone());
126 let idx = opts.iter().position(|opt| opt.value == current);
127 let len = opts.len();
128 if len == 0 {
129 return;
130 }
131 let next_idx = match evt.key() {
132 Key::ArrowRight | Key::ArrowDown => idx.map(|i| (i + 1) % len).unwrap_or(0),
133 Key::ArrowLeft | Key::ArrowUp => idx
134 .map(|i| if i == 0 { len - 1 } else { i - 1 })
135 .unwrap_or(0),
136 _ => return,
137 };
138 let target = &opts[next_idx];
139 if target.disabled {
140 return;
141 }
142 apply_segmented(
143 target.value.clone(),
144 controlled,
145 &mut inner_for_key,
146 &form_for_key,
147 &on_change_for_key,
148 );
149 }
150 };
151 rsx! {
152 div {
153 class: "{class_attr}",
154 style: "{style_attr}",
155 role: "tablist",
156 tabindex: if is_disabled { -1 } else { 0 },
157 onkeydown: handle_key,
158 {options.into_iter().map(|opt| {
159 let active = opt.value == current_value;
160 let mut item_class = vec!["adui-segmented-item".to_string()];
161 if active { item_class.push("adui-segmented-item-active".into()); }
162 if opt.disabled { item_class.push("adui-segmented-item-disabled".into()); }
163
164 let tooltip_text = opt.tooltip.clone().unwrap_or_default();
165
166 let on_click = {
167 let value = opt.value.clone();
168 let form_for_click = form_control.clone();
169 let mut inner_for_click = inner.clone();
170 let on_change_for_click = on_change.clone();
171 move |_| {
172 if is_disabled || opt.disabled {
173 return;
174 }
175 apply_segmented(
176 value.clone(),
177 controlled,
178 &mut inner_for_click,
179 &form_for_click,
180 &on_change_for_click,
181 );
182 }
183 };
184
185 rsx! {
186 button {
187 class: "{item_class.join(\" \")}",
188 title: tooltip_text,
189 aria_pressed: active,
190 disabled: is_disabled || opt.disabled,
191 onclick: on_click,
192 if let Some(icon) = opt.icon.clone() {
193 span { class: "adui-segmented-item-icon", {icon} }
194 }
195 span { class: "adui-segmented-item-label", {opt.label.clone()} }
196 }
197 }
198 })}
199 }
200 }
201}
202
203fn resolve_value(
204 value: Option<String>,
205 form_control: &Option<crate::components::form::FormItemControlContext>,
206 inner: &Signal<Option<String>>,
207) -> Option<String> {
208 value
209 .or_else(|| {
210 form_control
211 .as_ref()
212 .and_then(|ctx| value_from_form(ctx.value()))
213 })
214 .or_else(|| (*inner.read()).clone())
215}
216
217fn value_from_form(val: Option<Value>) -> Option<String> {
218 match val {
219 Some(Value::String(s)) => Some(s),
220 Some(Value::Number(n)) => Some(n.to_string()),
221 Some(Value::Bool(b)) => Some(b.to_string()),
222 _ => None,
223 }
224}
225
226fn apply_segmented(
227 next: String,
228 controlled: bool,
229 inner: &mut Signal<Option<String>>,
230 form_control: &Option<crate::components::form::FormItemControlContext>,
231 on_change: &Option<EventHandler<String>>,
232) {
233 if !controlled {
234 inner.set(Some(next.clone()));
235 }
236
237 if let Some(ctx) = form_control.as_ref() {
238 ctx.set_value(Value::String(next.clone()));
239 }
240
241 if let Some(cb) = on_change.as_ref() {
242 cb.call(next);
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use serde_json::Value;
250
251 #[test]
252 fn segmented_option_new() {
253 let option = SegmentedOption::new("Label", "value1");
254 assert_eq!(option.label, "Label");
255 assert_eq!(option.value, "value1");
256 assert_eq!(option.icon, None);
257 assert_eq!(option.tooltip, None);
258 assert_eq!(option.disabled, false);
259 }
260
261 #[test]
262 fn segmented_option_clone() {
263 let option = SegmentedOption::new("Label", "value1");
264 let cloned = option.clone();
265 assert_eq!(option.label, cloned.label);
266 assert_eq!(option.value, cloned.value);
267 assert_eq!(option.disabled, cloned.disabled);
268 }
269
270 #[test]
271 fn segmented_option_equality() {
272 let option1 = SegmentedOption::new("Label", "value1");
273 let option2 = SegmentedOption::new("Label", "value1");
274 let option3 = SegmentedOption::new("Different", "value1");
275 assert_eq!(option1, option2);
276 assert_ne!(option1, option3);
277 }
278
279 #[test]
280 fn segmented_props_defaults() {
281 }
286
287 #[test]
291 fn value_from_form_string() {
292 let val = Some(Value::String("test".to_string()));
293 assert_eq!(value_from_form(val), Some("test".to_string()));
294 }
295
296 #[test]
297 fn value_from_form_number() {
298 let val = Some(Value::Number(serde_json::Number::from(42)));
299 assert_eq!(value_from_form(val), Some("42".to_string()));
300 }
301
302 #[test]
303 fn value_from_form_bool() {
304 let val = Some(Value::Bool(true));
305 assert_eq!(value_from_form(val), Some("true".to_string()));
306 let val_false = Some(Value::Bool(false));
307 assert_eq!(value_from_form(val_false), Some("false".to_string()));
308 }
309
310 #[test]
311 fn value_from_form_other_types() {
312 assert_eq!(value_from_form(Some(Value::Null)), None);
313 assert_eq!(value_from_form(Some(Value::Array(vec![]))), None);
314 assert_eq!(value_from_form(None), None);
315 }
316
317 #[test]
318 fn value_from_form_string_empty() {
319 let val = Some(Value::String(String::new()));
320 assert_eq!(value_from_form(val), Some(String::new()));
321 }
322
323 #[test]
324 fn value_from_form_string_with_spaces() {
325 let val = Some(Value::String(" test ".to_string()));
326 assert_eq!(value_from_form(val), Some(" test ".to_string()));
327 }
328
329 #[test]
330 fn value_from_form_number_zero() {
331 let val = Some(Value::Number(serde_json::Number::from(0)));
332 assert_eq!(value_from_form(val), Some("0".to_string()));
333 }
334
335 #[test]
336 fn value_from_form_number_negative() {
337 let val = Some(Value::Number(serde_json::Number::from(-42)));
338 assert_eq!(value_from_form(val), Some("-42".to_string()));
339 }
340
341 #[test]
342 fn value_from_form_number_large() {
343 let val = Some(Value::Number(serde_json::Number::from(999999)));
344 assert_eq!(value_from_form(val), Some("999999".to_string()));
345 }
346
347 #[test]
348 fn value_from_form_number_float() {
349 let val = Some(Value::Number(serde_json::Number::from_f64(3.14).unwrap()));
350 assert_eq!(value_from_form(val), Some("3.14".to_string()));
351 }
352
353 #[test]
354 fn value_from_form_number_negative_float() {
355 let val = Some(Value::Number(serde_json::Number::from_f64(-2.5).unwrap()));
356 assert_eq!(value_from_form(val), Some("-2.5".to_string()));
357 }
358
359 #[test]
360 fn segmented_option_debug() {
361 let option = SegmentedOption::new("Label", "value1");
362 let debug_str = format!("{:?}", option);
363 assert!(debug_str.contains("Label") || debug_str.contains("value1"));
364 }
365
366 #[test]
367 fn segmented_option_with_all_fields() {
368 let option = SegmentedOption {
369 label: "Test Label".to_string(),
370 value: "test_value".to_string(),
371 icon: None,
372 tooltip: Some("Tooltip text".to_string()),
373 disabled: true,
374 };
375 assert_eq!(option.label, "Test Label");
376 assert_eq!(option.value, "test_value");
377 assert_eq!(option.tooltip, Some("Tooltip text".to_string()));
378 assert_eq!(option.disabled, true);
379 }
380
381 #[test]
382 fn segmented_option_with_tooltip() {
383 let mut option = SegmentedOption::new("Label", "value1");
384 option.tooltip = Some("Help text".to_string());
385 assert_eq!(option.tooltip, Some("Help text".to_string()));
386 }
387
388 #[test]
389 fn segmented_option_disabled() {
390 let mut option = SegmentedOption::new("Label", "value1");
391 option.disabled = true;
392 assert_eq!(option.disabled, true);
393 }
394
395 #[test]
396 fn segmented_option_equality_with_different_fields() {
397 let option1 = SegmentedOption {
398 label: "Label".to_string(),
399 value: "value1".to_string(),
400 icon: None,
401 tooltip: None,
402 disabled: false,
403 };
404 let option2 = SegmentedOption {
405 label: "Label".to_string(),
406 value: "value1".to_string(),
407 icon: None,
408 tooltip: Some("Tooltip".to_string()),
409 disabled: true,
410 };
411 assert_ne!(option1, option2);
413 }
414
415 #[test]
416 fn segmented_option_empty_strings() {
417 let option = SegmentedOption::new("", "");
418 assert_eq!(option.label, "");
419 assert_eq!(option.value, "");
420 }
421
422 #[test]
423 fn segmented_option_long_strings() {
424 let long_label = "a".repeat(100);
425 let long_value = "b".repeat(100);
426 let option = SegmentedOption::new(&long_label, &long_value);
427 assert_eq!(option.label, long_label);
428 assert_eq!(option.value, long_value);
429 }
430
431 }