1use plotly_derive::FieldSetter;
4use serde::Serialize;
5use serde_json::{Map, Value};
6
7use crate::{
8 color::Color,
9 common::{Anchor, Font, Pad},
10 layout::{Animation, ControlBuilderError},
11 Relayout, Restyle,
12};
13
14#[derive(Serialize, Debug, Copy, Clone)]
19#[serde(rename_all = "snake_case")]
20pub enum ButtonMethod {
21 Restyle,
24 Relayout,
27 Update,
30 Animate,
32 Skip,
37}
38
39#[serde_with::skip_serializing_none]
40#[derive(Serialize, Clone, Debug, FieldSetter)]
41pub struct Button {
42 args: Option<Value>,
45 args2: Option<Value>,
49 execute: Option<bool>,
58 label: Option<String>,
60 method: Option<ButtonMethod>,
66 name: Option<String>,
73 #[serde(rename = "templateitemname")]
81 template_item_name: Option<String>,
82 visible: Option<bool>,
84}
85
86impl Button {
87 pub fn new() -> Self {
88 Default::default()
89 }
90}
91
92#[derive(FieldSetter)]
94pub struct ButtonBuilder {
95 label: Option<String>,
96 name: Option<String>,
97 template_item_name: Option<String>,
98 visible: Option<bool>,
99 #[field_setter(default = "Map::new()")]
100 restyles: Map<String, Value>,
101 #[field_setter(default = "Map::new()")]
102 relayouts: Map<String, Value>,
103 #[field_setter(skip)]
104 error: Option<ControlBuilderError>,
105 #[field_setter(skip)]
107 animation: Option<Animation>,
108}
109
110impl ButtonBuilder {
111 pub fn new() -> Self {
112 Default::default()
113 }
114
115 pub fn push_restyle(mut self, restyle: impl Restyle) -> Self {
116 if self.error.is_none() {
117 if let Err(e) = self.try_push_restyle(restyle) {
118 self.error = Some(e);
119 }
120 }
121 self
122 }
123
124 fn try_push_restyle(&mut self, restyle: impl Restyle) -> Result<(), ControlBuilderError> {
125 let restyle_value = serde_json::to_value(&restyle)
126 .map_err(|e| ControlBuilderError::RestyleSerializationError(e.to_string()))?;
127 let restyle_obj = restyle_value
128 .as_object()
129 .ok_or_else(|| ControlBuilderError::InvalidRestyleObject(restyle_value.to_string()))?;
130 for (k, v) in restyle_obj {
131 self.restyles.insert(k.clone(), v.clone());
132 }
133 Ok(())
134 }
135
136 pub fn push_relayout(mut self, relayout: impl Relayout + Serialize) -> Self {
137 if self.error.is_none() {
138 if let Err(e) = self.try_push_relayout(relayout) {
139 self.error = Some(e);
140 }
141 }
142 self
143 }
144
145 fn try_push_relayout(
146 &mut self,
147 relayout: impl Relayout + Serialize,
148 ) -> Result<(), ControlBuilderError> {
149 let relayout_value = serde_json::to_value(&relayout)
150 .map_err(|e| ControlBuilderError::RelayoutSerializationError(e.to_string()))?;
151 let relayout_obj = relayout_value.as_object().ok_or_else(|| {
152 ControlBuilderError::InvalidRelayoutObject(relayout_value.to_string())
153 })?;
154 for (k, v) in relayout_obj {
155 self.relayouts.insert(k.clone(), v.clone());
156 }
157 Ok(())
158 }
159
160 pub fn animation(mut self, animation: Animation) -> Self {
162 self.animation = Some(animation);
163 self
164 }
165
166 pub fn build(self) -> Result<Button, ControlBuilderError> {
167 if let Some(error) = self.error {
168 return Err(error);
169 }
170
171 let (method, args) = match (
172 self.animation,
173 self.restyles.is_empty(),
174 self.relayouts.is_empty(),
175 ) {
176 (Some(animation), _, _) => {
178 let animation_args = serde_json::to_value(animation)
179 .map_err(|e| ControlBuilderError::AnimationSerializationError(e.to_string()))?;
180 (ButtonMethod::Animate, animation_args)
181 }
182 (None, true, true) => (ButtonMethod::Skip, Value::Null),
184 (None, false, true) => (ButtonMethod::Restyle, vec![self.restyles].into()),
185 (None, true, false) => (ButtonMethod::Relayout, vec![self.relayouts].into()),
186 (None, false, false) => (
187 ButtonMethod::Update,
188 vec![self.restyles, self.relayouts].into(),
189 ),
190 };
191
192 Ok(Button {
193 label: self.label,
194 args: Some(args),
195 method: Some(method),
196 name: self.name,
197 template_item_name: self.template_item_name,
198 visible: self.visible,
199 ..Default::default()
200 })
201 }
202}
203
204#[derive(Serialize, Debug, Clone)]
209#[serde(rename_all = "snake_case")]
210pub enum UpdateMenuType {
211 Dropdown,
212 Buttons,
213}
214
215#[derive(Serialize, Debug, Clone)]
221#[serde(rename_all = "snake_case")]
222pub enum UpdateMenuDirection {
223 Left,
224 Right,
225 Up,
226 Down,
227}
228
229#[serde_with::skip_serializing_none]
230#[derive(Serialize, Debug, FieldSetter, Clone)]
231pub struct UpdateMenu {
232 active: Option<i32>,
234 #[serde(rename = "bgcolor")]
236 background_color: Option<Box<dyn Color>>,
237 #[serde(rename = "bordercolor")]
239 border_color: Option<Box<dyn Color>>,
240 #[serde(rename = "borderwidth")]
242 border_width: Option<usize>,
243 buttons: Option<Vec<Button>>,
244 direction: Option<UpdateMenuDirection>,
249 font: Option<Font>,
251 name: Option<String>,
258 pad: Option<Pad>,
260 #[serde(rename = "showactive")]
262 show_active: Option<bool>,
263 template_item_name: Option<String>,
271 #[serde(rename = "type")]
274 ty: Option<UpdateMenuType>,
275 visible: Option<bool>,
277 x: Option<f64>,
281 #[serde(rename = "xanchor")]
285 x_anchor: Option<Anchor>,
286 y: Option<f64>,
290 #[serde(rename = "yanchor")]
294 y_anchor: Option<Anchor>,
295}
296
297impl UpdateMenu {
298 pub fn new() -> Self {
299 Default::default()
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use serde_json::{json, to_value};
306
307 use super::*;
308 use crate::{common::Visible, Layout};
309
310 #[test]
311 fn serialize_button_method() {
312 assert_eq!(to_value(ButtonMethod::Restyle).unwrap(), json!("restyle"));
313 assert_eq!(to_value(ButtonMethod::Relayout).unwrap(), json!("relayout"));
314 assert_eq!(to_value(ButtonMethod::Animate).unwrap(), json!("animate"));
315 assert_eq!(to_value(ButtonMethod::Update).unwrap(), json!("update"));
316 assert_eq!(to_value(ButtonMethod::Skip).unwrap(), json!("skip"));
317 }
318
319 #[test]
320 fn serialize_button() {
321 let button = Button::new()
322 .args(json!([
323 { "visible": [true, false] },
324 { "width": 20},
325 ]))
326 .args2(json!([]))
327 .execute(true)
328 .label("Label")
329 .method(ButtonMethod::Update)
330 .name("Name")
331 .template_item_name("Template")
332 .visible(true);
333
334 let expected = json!({
335 "args": [
336 { "visible": [true, false] },
337 { "width": 20},
338 ],
339 "args2": [],
340 "execute": true,
341 "label": "Label",
342 "method": "update",
343 "name": "Name",
344 "templateitemname": "Template",
345 "visible": true,
346 });
347
348 assert_eq!(to_value(button).unwrap(), expected);
349 }
350
351 #[test]
352 fn serialize_button_builder() {
353 let expected = json!({
354 "args": [
355 { "visible": [true, false] },
356 { "title": {"text": "Hello"}, "width": 20},
357 ],
358 "label": "Label",
359 "method": "update",
360 "name": "Name",
361 "templateitemname": "Template",
362 "visible": true,
363 });
364
365 let button = ButtonBuilder::new()
366 .label("Label")
367 .name("Name")
368 .template_item_name("Template")
369 .visible(true)
370 .push_restyle(crate::Bar::<i32, i32>::modify_visible(vec![
371 Visible::True,
372 Visible::False,
373 ]))
374 .push_relayout(Layout::modify_title("Hello"))
375 .push_relayout(Layout::modify_width(20))
376 .build()
377 .unwrap();
378
379 assert_eq!(to_value(button).unwrap(), expected);
380 }
381
382 #[test]
383 fn test_button_builder_push_restyle_valid() {
384 let button = ButtonBuilder::new()
385 .label("Test Button")
386 .push_restyle(crate::Bar::<i32, i32>::modify_visible(vec![
387 Visible::True,
388 Visible::False,
389 ]))
390 .build()
391 .unwrap();
392
393 let expected = json!({
394 "args": [{ "visible": [true, false] }],
395 "label": "Test Button",
396 "method": "restyle",
397 });
398
399 assert_eq!(to_value(button).unwrap(), expected);
400 }
401
402 #[test]
403 fn test_button_builder_push_relayout_valid() {
404 let button = ButtonBuilder::new()
405 .label("Test Button")
406 .push_relayout(Layout::modify_title("Test Title"))
407 .build()
408 .unwrap();
409
410 let expected = json!({
411 "args": [{ "title": {"text": "Test Title"} }],
412 "label": "Test Button",
413 "method": "relayout",
414 });
415
416 assert_eq!(to_value(button).unwrap(), expected);
417 }
418
419 #[test]
420 fn test_button_builder_push_restyle_invalid() {
421 #[derive(Serialize)]
423 struct InvalidRestyle;
424 impl Restyle for InvalidRestyle {}
425
426 let result = ButtonBuilder::new()
427 .label("Test Button")
428 .push_restyle(InvalidRestyle)
429 .build();
430
431 assert!(result.is_err());
432 match result.unwrap_err() {
433 ControlBuilderError::InvalidRestyleObject(_) => {}
434 _ => panic!("Expected InvalidRestyleObject error"),
435 }
436 }
437
438 #[test]
439 fn test_button_builder_push_relayout_invalid() {
440 #[derive(Serialize)]
442 struct InvalidRelayout;
443 impl Relayout for InvalidRelayout {}
444
445 let result = ButtonBuilder::new()
446 .label("Test Button")
447 .push_relayout(InvalidRelayout)
448 .build();
449
450 assert!(result.is_err());
451 match result.unwrap_err() {
452 ControlBuilderError::InvalidRelayoutObject(_) => {}
453 _ => panic!("Expected InvalidRelayoutObject error"),
454 }
455 }
456
457 #[test]
458 fn serialize_animation_button_args() {
459 let animation = Animation::all_frames();
460
461 let button = ButtonBuilder::new()
462 .label("Animate")
463 .animation(animation)
464 .build()
465 .unwrap();
466
467 let args = button.args.unwrap();
468 assert!(args.is_array(), "Animation button args must be an array");
469
470 assert_eq!(args[0], json!(null)); assert!(
473 args[1].is_object(),
474 "Second arg should be animation options object"
475 );
476 assert_eq!(to_value(button.method.unwrap()).unwrap(), json!("animate"));
477 }
478}