1#[cfg(doc)]
9use geop_core_math::geop_error::GeopError;
10use geop_core_math::vector::Vector3;
11use serde::Serialize;
12
13use geop_core_program::{element::PickedElement, numeric::NumericInput, reader::Reader};
14
15use super::{Style, Visual};
16use crate::selectable::{Selectable, previewed};
17
18pub fn is_formula(text: &str) -> bool {
21 text.trim().parse::<f64>().is_err()
22}
23
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
26#[serde(rename_all = "snake_case")]
27pub enum Tone {
28 #[default]
29 Normal,
30 Hint,
31 Error,
32 Success,
33}
34
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
38#[serde(rename_all = "snake_case")]
39pub enum Layout {
40 #[default]
43 Sidebar,
44 Popup,
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
52#[serde(tag = "type", rename_all = "snake_case")]
53pub enum ColumnKind {
54 Text,
56 Number { unit: Unit },
58 Formula,
61}
62
63#[derive(Clone, Debug, PartialEq, Serialize)]
65pub struct Column {
66 pub label: String,
67 pub kind: ColumnKind,
68 pub editable: bool,
72}
73
74impl Column {
75 pub fn new(label: impl Into<String>, kind: ColumnKind) -> Self {
77 Self {
78 label: label.into(),
79 kind,
80 editable: false,
81 }
82 }
83
84 pub fn editable(mut self) -> Self {
86 self.editable = true;
87 self
88 }
89}
90
91#[derive(Clone, Debug, PartialEq, Serialize)]
93pub struct Cell {
94 pub text: String,
96 pub detail: Option<String>,
98 pub tone: Tone,
99}
100
101impl Cell {
102 pub fn new(text: impl Into<String>) -> Self {
103 Self {
104 text: text.into(),
105 detail: None,
106 tone: Tone::Normal,
107 }
108 }
109
110 pub fn number(value: f64) -> Self {
112 Self::new(value.to_string())
113 }
114
115 pub fn detail(mut self, detail: impl Into<String>, tone: Tone) -> Self {
117 self.detail = Some(detail.into());
118 self.tone = tone;
119 self
120 }
121}
122
123#[derive(Clone, Debug, PartialEq, Serialize)]
125pub struct TableRow {
126 pub cells: Vec<Cell>,
127 pub selected: bool,
130 pub removable: bool,
132}
133
134impl TableRow {
135 pub fn new(cells: Vec<Cell>) -> Self {
136 Self {
137 cells,
138 selected: false,
139 removable: true,
140 }
141 }
142}
143
144#[derive(Clone, Debug, PartialEq, Serialize)]
150pub struct Table {
151 pub columns: Vec<Column>,
152 pub rows: Vec<TableRow>,
153 pub empty: String,
155 pub rows_editable: bool,
158 pub columns_editable: bool,
161 pub selectable: bool,
164}
165
166impl Table {
167 pub fn new(columns: Vec<Column>, rows: Vec<TableRow>, empty: impl Into<String>) -> Self {
170 Self {
171 columns,
172 rows,
173 empty: empty.into(),
174 rows_editable: true,
175 columns_editable: false,
176 selectable: false,
177 }
178 }
179
180 pub fn columns_editable(mut self) -> Self {
182 self.columns_editable = true;
183 self
184 }
185
186 pub fn selectable(mut self) -> Self {
188 self.selectable = true;
189 self
190 }
191}
192
193#[derive(Clone, Debug, PartialEq, Serialize)]
195pub struct Action {
196 pub value: String,
198 pub label: String,
199 pub title: Option<String>,
201 pub group: Option<String>,
203 pub enabled: bool,
204 pub active: bool,
206 pub icon: Option<&'static str>,
210}
211
212impl Action {
213 pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
214 Self {
215 value: value.into(),
216 label: label.into(),
217 title: None,
218 group: None,
219 enabled: true,
220 active: false,
221 icon: None,
222 }
223 }
224
225 pub fn icon(mut self, icon: impl Into<Option<&'static str>>) -> Self {
227 self.icon = icon.into();
228 self
229 }
230
231 pub fn title(mut self, title: impl Into<String>) -> Self {
232 self.title = Some(title.into());
233 self
234 }
235
236 pub fn group(mut self, group: impl Into<String>) -> Self {
237 self.group = Some(group.into());
238 self
239 }
240
241 pub fn active(mut self, active: bool) -> Self {
242 self.active = active;
243 self
244 }
245
246 pub fn disabled(mut self, why: impl Into<String>) -> Self {
248 self.enabled = false;
249 self.title = Some(why.into());
250 self
251 }
252}
253
254#[derive(Clone, Debug, PartialEq, Serialize)]
256pub struct Choice {
257 pub value: String,
259 pub label: String,
260}
261
262#[derive(Clone, Debug, PartialEq, Serialize)]
266pub struct FileEntry {
267 pub path: String,
268 pub accepted: bool,
269}
270
271impl Choice {
272 pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
273 Self {
274 value: value.into(),
275 label: label.into(),
276 }
277 }
278}
279
280#[derive(Clone, Debug, PartialEq, Serialize)]
284pub struct ListItem {
285 pub key: String,
286 pub label: String,
287 pub detail: Option<String>,
289 pub tone: Tone,
290 pub selected: bool,
291 pub removable: bool,
292 pub value: Option<f64>,
294 pub text: Option<String>,
297}
298
299impl ListItem {
300 pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
301 Self {
302 key: key.into(),
303 label: label.into(),
304 detail: None,
305 tone: Tone::Normal,
306 selected: false,
307 removable: false,
308 value: None,
309 text: None,
310 }
311 }
312}
313
314#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
316#[serde(rename_all = "snake_case")]
317pub enum Unit {
318 #[default]
319 Length,
320 Angle,
322 Fraction,
324 Count,
326}
327
328impl Unit {
329 fn step(self) -> f64 {
331 match self {
332 Unit::Length => 0.1,
333 Unit::Angle => 1.0,
334 Unit::Fraction => 0.01,
335 Unit::Count => 1.0,
336 }
337 }
338}
339
340#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
343pub struct Track {
344 pub at: Vector3,
345 pub direction: Vector3,
346}
347
348#[derive(Clone, Debug, PartialEq, Serialize)]
351pub struct Number {
352 pub label: String,
353 pub value: f64,
355 pub unit: Unit,
356 pub range: Option<[f64; 2]>,
358 pub step: f64,
359 pub text: Option<String>,
363 pub error: Option<String>,
365 #[serde(skip)]
367 pub handle: Option<Track>,
368}
369
370impl Number {
371 pub fn new(label: impl Into<String>, value: f64, unit: Unit) -> Self {
372 Self {
373 label: label.into(),
374 value,
375 unit,
376 range: None,
377 step: unit.step(),
378 text: None,
379 error: None,
380 handle: None,
381 }
382 }
383
384 pub fn numeric(
394 input: &NumericInput,
395 label: impl Into<String>,
396 reader: &Reader,
397 unit: Unit,
398 ) -> Self {
399 let (value, error) = match input.resolve(reader) {
400 Ok(value) => (value, None),
401 Err(e) => (0.0, Some(e.root_message().to_string())),
402 };
403 Self {
404 text: Some(input.to_string()),
405 error,
406 ..Self::new(label, value, unit)
407 }
408 }
409
410 pub fn follows_formula(&self) -> bool {
412 self.text.as_deref().is_some_and(is_formula)
413 }
414
415 pub fn range(mut self, min: f64, max: f64) -> Self {
418 if !self.follows_formula() {
419 self.range = Some([min, max]);
420 self.step = (max - min) / 200.0;
421 }
422 self
423 }
424
425 pub fn handle(mut self, handle: Option<Track>) -> Self {
427 if !self.follows_formula() {
428 self.handle = handle;
429 }
430 self
431 }
432}
433
434#[derive(Clone, Debug, PartialEq, Serialize)]
436pub struct Picked {
437 pub element: PickedElement,
438 #[serde(skip)]
441 pub drawn: Vec<PickedElement>,
442 #[serde(skip)]
445 pub visuals: Vec<Visual>,
446 pub detail: Option<String>,
448 pub tone: Tone,
449}
450
451impl Picked {
452 pub fn new(element: PickedElement) -> Self {
454 Self {
455 drawn: vec![element.clone()],
456 visuals: Vec::new(),
457 element,
458 detail: None,
459 tone: Tone::Normal,
460 }
461 }
462}
463
464#[derive(Clone, Copy)]
470pub struct Preview(pub fn(&Reader, &PickedElement) -> Option<Picked>);
471
472impl PartialEq for Preview {
473 fn eq(&self, _: &Self) -> bool {
474 true
475 }
476}
477
478impl std::fmt::Debug for Preview {
479 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480 f.write_str("Preview")
481 }
482}
483
484#[derive(Clone, Debug, PartialEq, Serialize)]
494pub struct Reference {
495 pub label: String,
496 pub accepts: Vec<String>,
497 pub value: Vec<Picked>,
498 pub multiple: bool,
499 pub required: bool,
502 pub armed: bool,
504 #[serde(skip)]
507 pub preview: Preview,
508}
509
510impl Reference {
511 pub fn waiting(&self) -> bool {
513 self.required && self.value.is_empty()
514 }
515
516 pub fn elements(&self) -> impl Iterator<Item = &PickedElement> {
518 self.value.iter().map(|p| &p.element)
519 }
520}
521
522#[derive(Clone, Debug, PartialEq, Serialize)]
524#[serde(tag = "type", rename_all = "snake_case")]
525pub enum Control {
526 Heading {
527 text: String,
528 },
529 Text {
530 text: String,
531 tone: Tone,
532 },
533 Input {
535 label: String,
536 value: String,
537 placeholder: Option<String>,
539 },
540 Actions {
543 actions: Vec<Action>,
544 },
545 Checkbox {
546 label: String,
547 value: bool,
548 },
549 Download {
554 exporter: String,
555 formats: Vec<Choice>,
556 },
557 Number(Number),
558 Select {
560 label: String,
561 value: String,
562 options: Vec<Choice>,
563 searchable: bool,
564 },
565 File {
574 label: String,
575 value: String,
576 files: Vec<FileEntry>,
577 upload: Vec<String>,
578 },
579 Color {
581 label: String,
582 value: String,
583 },
584 Reference(Reference),
585 List {
586 items: Vec<ListItem>,
587 empty: String,
589 },
590 Table(Table),
593}
594
595#[derive(Clone, Debug, PartialEq, Serialize)]
597pub struct Field {
598 pub key: String,
599 #[serde(flatten)]
600 pub control: Control,
601 #[serde(skip_serializing_if = "Option::is_none")]
605 pub failure: Option<String>,
606}
607
608#[derive(Clone, Debug, Default, PartialEq, Serialize)]
612#[serde(transparent)]
613pub struct Dialog(pub Vec<Field>);
614
615impl Dialog {
616 pub fn new() -> Self {
617 Self::default()
618 }
619
620 pub fn push(&mut self, key: impl Into<String>, control: Control) -> &mut Self {
622 let key = key.into();
623 debug_assert!(
624 self.0.iter().all(|f| f.key != key),
625 "dialog key {key:?} used twice"
626 );
627 self.0.push(Field {
628 key,
629 control,
630 failure: None,
631 });
632 self
633 }
634
635 pub fn heading(&mut self, key: &str, text: impl Into<String>) -> &mut Self {
636 self.push(key, Control::Heading { text: text.into() })
637 }
638
639 pub fn text(&mut self, key: &str, text: impl Into<String>, tone: Tone) -> &mut Self {
640 self.push(
641 key,
642 Control::Text {
643 text: text.into(),
644 tone,
645 },
646 )
647 }
648
649 pub fn input(
651 &mut self,
652 key: &str,
653 label: impl Into<String>,
654 value: impl Into<String>,
655 placeholder: Option<&str>,
656 ) -> &mut Self {
657 self.push(
658 key,
659 Control::Input {
660 label: label.into(),
661 value: value.into(),
662 placeholder: placeholder.map(str::to_string),
663 },
664 )
665 }
666
667 pub fn table(&mut self, key: &str, table: Table) -> &mut Self {
669 self.push(key, Control::Table(table))
670 }
671
672 pub fn list(&mut self, key: &str, items: Vec<ListItem>, empty: impl Into<String>) -> &mut Self {
674 self.push(
675 key,
676 Control::List {
677 items,
678 empty: empty.into(),
679 },
680 )
681 }
682
683 pub fn download(&mut self, key: &str, exporter: &str, formats: Vec<Choice>) -> &mut Self {
686 self.push(
687 key,
688 Control::Download {
689 exporter: exporter.into(),
690 formats,
691 },
692 )
693 }
694
695 pub fn actions(&mut self, key: &str, actions: Vec<Action>) -> &mut Self {
698 self.push(key, Control::Actions { actions })
699 }
700
701 pub fn checkbox(&mut self, key: &str, label: impl Into<String>, value: bool) -> &mut Self {
702 self.push(
703 key,
704 Control::Checkbox {
705 label: label.into(),
706 value,
707 },
708 )
709 }
710
711 pub fn number(&mut self, key: &str, number: Number) -> &mut Self {
712 self.push(key, Control::Number(number))
713 }
714
715 pub fn select(
717 &mut self,
718 key: &str,
719 label: impl Into<String>,
720 value: impl Into<String>,
721 options: Vec<Choice>,
722 searchable: bool,
723 ) -> &mut Self {
724 self.push(
725 key,
726 Control::Select {
727 label: label.into(),
728 value: value.into(),
729 options,
730 searchable,
731 },
732 )
733 }
734
735 pub fn file(
740 &mut self,
741 key: &str,
742 label: impl Into<String>,
743 value: impl Into<String>,
744 files: Vec<FileEntry>,
745 upload: &[&str],
746 ) -> &mut Self {
747 self.push(
748 key,
749 Control::File {
750 label: label.into(),
751 value: value.into(),
752 files,
753 upload: upload.iter().map(|a| a.to_string()).collect(),
754 },
755 )
756 }
757
758 pub fn color(
760 &mut self,
761 key: &str,
762 label: impl Into<String>,
763 value: impl Into<String>,
764 ) -> &mut Self {
765 self.push(
766 key,
767 Control::Color {
768 label: label.into(),
769 value: value.into(),
770 },
771 )
772 }
773
774 pub fn pick<P: Selectable>(
778 &mut self,
779 key: &str,
780 label: impl Into<String>,
781 reader: &Reader,
782 value: &[P],
783 multiple: bool,
784 ) -> &mut Self {
785 let value = (value.iter())
786 .map(|held| Picked {
787 drawn: held.drawn(reader),
788 visuals: held.visuals(reader),
789 ..Picked::new(held.picked())
790 })
791 .collect();
792 self.reference(
793 key,
794 label,
795 value,
796 &P::picked_from(),
797 multiple,
798 Preview(previewed::<P>),
799 )
800 }
801
802 pub fn reference(
806 &mut self,
807 key: &str,
808 label: impl Into<String>,
809 value: Vec<Picked>,
810 accepts: &[&str],
811 multiple: bool,
812 preview: Preview,
813 ) -> &mut Self {
814 self.push(
815 key,
816 Control::Reference(Reference {
817 label: label.into(),
818 accepts: accepts.iter().map(|k| k.to_string()).collect(),
819 value,
820 multiple,
821 required: true,
822 armed: false,
823 preview,
824 }),
825 )
826 }
827
828 pub fn optional(&mut self, key: &str) -> &mut Self {
832 for (k, reference) in self.references_mut() {
833 if k == key {
834 reference.required = false;
835 }
836 }
837 self
838 }
839
840 pub fn get(&self, key: &str) -> Option<&Control> {
842 self.0.iter().find(|f| f.key == key).map(|f| &f.control)
843 }
844
845 pub fn references(&self) -> impl Iterator<Item = (&str, &Reference)> {
847 self.0.iter().filter_map(|f| match &f.control {
848 Control::Reference(r) => Some((f.key.as_str(), r)),
849 _ => None,
850 })
851 }
852
853 pub fn references_mut(&mut self) -> impl Iterator<Item = (&str, &mut Reference)> {
855 self.0.iter_mut().filter_map(|f| match &mut f.control {
856 Control::Reference(r) => Some((f.key.as_str(), r)),
857 _ => None,
858 })
859 }
860
861 pub fn previewed(&self, reader: &Reader, key: &str, element: &PickedElement) -> Option<Picked> {
865 let (_, reference) = self.references().find(|(k, _)| *k == key)?;
866 (reference.preview.0)(reader, element)
867 }
868
869 pub fn shown(&self, hovered: Option<&Picked>) -> Vec<Visual> {
873 let styled = |mut visual: Visual, style: Style| {
874 visual.style = style;
875 visual
876 };
877 let held = (self.references())
878 .flat_map(|(_, r)| &r.value)
879 .flat_map(|p| p.visuals.iter().cloned())
880 .map(|v| styled(v, Style::Selected));
881 let hovered = (hovered.into_iter())
882 .flat_map(|p| p.visuals.iter().cloned())
883 .map(|v| styled(v, Style::Hover));
884 held.chain(hovered).collect()
885 }
886
887 pub fn concerning(&mut self, key: &str, error: &str) -> bool {
891 let field = self.0.iter_mut().find(|f| f.key == key);
892 field.map(|f| f.failure = Some(error.to_string())).is_some()
893 }
894
895 pub fn picked(&self) -> impl Iterator<Item = &PickedElement> {
896 self.0
897 .iter()
898 .flat_map(|f| match &f.control {
899 Control::Reference(r) => r.value.as_slice(),
900 _ => &[],
901 })
902 .flat_map(|p| &p.drawn)
903 }
904
905 pub fn missing(&self) -> Vec<&str> {
909 self.0
910 .iter()
911 .filter_map(|f| match &f.control {
912 Control::Reference(r) if r.waiting() => Some(r.label.as_str()),
913 _ => None,
914 })
915 .collect()
916 }
917
918 pub fn summary(&self) -> String {
921 self.0
922 .iter()
923 .filter_map(|f| {
924 let value = match &f.control {
925 Control::Checkbox { label, value } => {
926 (label, if *value { "yes" } else { "no" }.to_string())
927 }
928 Control::Number(n) => (
929 &n.label,
930 match &n.text {
931 Some(text) if n.follows_formula() => text.clone(),
932 _ => format!("{:.2}", n.value),
933 },
934 ),
935 Control::Color { label, value } | Control::Input { label, value, .. } => {
936 (label, value.clone())
937 }
938 Control::Table(table) => {
939 return Some(format!(
940 "{} {}",
941 table.rows.len(),
942 if table.rows.len() == 1 { "row" } else { "rows" }
943 ));
944 }
945 Control::File { label, value, .. } => (label, value.clone()),
946 Control::Select {
947 label,
948 value,
949 options,
950 ..
951 } => (
952 label,
953 options
954 .iter()
955 .find(|o| &o.value == value)
956 .map_or(value.clone(), |o| o.label.clone()),
957 ),
958 Control::Reference(r) => (
959 &r.label,
960 r.elements()
961 .map(PickedElement::label)
962 .collect::<Vec<_>>()
963 .join(" & "),
964 ),
965 _ => return None,
966 };
967 Some(format!("{}={}", value.0, value.1))
968 })
969 .collect::<Vec<_>>()
970 .join(", ")
971 }
972}
973
974#[cfg(test)]
975mod tests {
976 use serde_json::json;
977
978 use super::*;
979
980 #[test]
983 fn fields_serialize_flat() {
984 let mut dialog = Dialog::new();
985 dialog.push(
986 "distance",
987 Control::Number(
988 Number::new("distance", 1.0, Unit::Length)
989 .range(-1.0, 1.0)
990 .handle(Some(Track {
991 at: Vector3::zero(),
992 direction: Vector3::zero(),
993 })),
994 ),
995 );
996 dialog.push(
997 "sketch",
998 Control::Reference(Reference {
999 label: "sketch".into(),
1000 accepts: vec!["sketch".into()],
1001 value: vec![Picked::new(PickedElement {
1002 target: geop_core_program::placed::Located::new("k"),
1003 kind: "sketch".into(),
1004 name: String::new(),
1005 })],
1006 multiple: false,
1007 required: true,
1008 armed: false,
1009 preview: Preview(|_, _| None),
1010 }),
1011 );
1012 assert_eq!(
1013 serde_json::to_value(&dialog).unwrap(),
1014 json!([
1015 {
1016 "key": "distance",
1017 "type": "number",
1018 "label": "distance",
1019 "value": 1.0,
1020 "unit": "length",
1021 "range": [-1.0, 1.0],
1022 "step": 0.01,
1023 "text": null,
1024 "error": null,
1025 },
1026 {
1027 "key": "sketch",
1028 "type": "reference",
1029 "label": "sketch",
1030 "accepts": ["sketch"],
1031 "value": [{
1032 "element": {"target": "/:k", "kind": "sketch", "name": ""},
1033 "detail": null,
1034 "tone": "normal",
1035 }],
1036 "multiple": false,
1037 "required": true,
1038 "armed": false,
1039 },
1040 ])
1041 );
1042 }
1043
1044 #[test]
1047 fn tables_and_inputs_serialize() {
1048 let mut dialog = Dialog::new();
1049 dialog.input("name", "Looked up by", "size", Some("size"));
1050 let columns = vec![
1051 Column::new("Row", ColumnKind::Text),
1052 Column::new("d", ColumnKind::Number { unit: Unit::Length }).editable(),
1053 Column::new("Value", ColumnKind::Formula),
1054 ];
1055 let row = TableRow {
1056 cells: vec![
1057 Cell::new("M4"),
1058 Cell::number(4.0),
1059 Cell::new("d / 2").detail("= 2", Tone::Hint),
1060 ],
1061 selected: true,
1062 removable: false,
1063 };
1064 let table = Table::new(columns, vec![row], "No rows").selectable();
1065 dialog.table("table", table);
1066 assert_eq!(dialog.summary(), "Looked up by=size, 1 row");
1067 assert_eq!(
1068 serde_json::to_value(&dialog).unwrap(),
1069 json!([
1070 {
1071 "key": "name",
1072 "type": "input",
1073 "label": "Looked up by",
1074 "value": "size",
1075 "placeholder": "size",
1076 },
1077 {
1078 "key": "table",
1079 "type": "table",
1080 "columns": [
1081 {"label": "Row", "kind": {"type": "text"}, "editable": false},
1082 {"label": "d", "kind": {"type": "number", "unit": "length"}, "editable": true},
1083 {"label": "Value", "kind": {"type": "formula"}, "editable": false},
1084 ],
1085 "rows": [{
1086 "cells": [
1087 {"text": "M4", "detail": null, "tone": "normal"},
1088 {"text": "4", "detail": null, "tone": "normal"},
1089 {"text": "d / 2", "detail": "= 2", "tone": "hint"},
1090 ],
1091 "selected": true,
1092 "removable": false,
1093 }],
1094 "empty": "No rows",
1095 "rows_editable": true,
1096 "columns_editable": false,
1097 "selectable": true,
1098 },
1099 ])
1100 );
1101 }
1102}