1use geop_core_math::{scalars::Scalar, vector::Vector3};
9use serde::Serialize;
10
11use crate::operation::{EntityRef, Role};
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
15#[serde(rename_all = "snake_case")]
16pub enum Tone {
17 #[default]
18 Normal,
19 Hint,
20 Error,
21 Success,
22}
23
24#[derive(Clone, Debug, PartialEq, Serialize)]
26pub struct Action {
27 pub value: String,
29 pub label: String,
30 pub title: Option<String>,
32 pub group: Option<String>,
34 pub enabled: bool,
35 pub active: bool,
37}
38
39impl Action {
40 pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
41 Self {
42 value: value.into(),
43 label: label.into(),
44 title: None,
45 group: None,
46 enabled: true,
47 active: false,
48 }
49 }
50
51 pub fn title(mut self, title: impl Into<String>) -> Self {
52 self.title = Some(title.into());
53 self
54 }
55
56 pub fn group(mut self, group: impl Into<String>) -> Self {
57 self.group = Some(group.into());
58 self
59 }
60
61 pub fn active(mut self, active: bool) -> Self {
62 self.active = active;
63 self
64 }
65
66 pub fn disabled(mut self, why: impl Into<String>) -> Self {
68 self.enabled = false;
69 self.title = Some(why.into());
70 self
71 }
72}
73
74#[derive(Clone, Debug, PartialEq, Serialize)]
76pub struct Choice {
77 pub value: String,
79 pub label: String,
80}
81
82impl Choice {
83 pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
84 Self {
85 value: value.into(),
86 label: label.into(),
87 }
88 }
89}
90
91#[derive(Clone, Debug, PartialEq, Serialize)]
95pub struct ListItem {
96 pub key: String,
97 pub label: String,
98 pub detail: Option<String>,
100 pub tone: Tone,
101 pub selected: bool,
102 pub removable: bool,
103 pub value: Option<f64>,
105}
106
107impl ListItem {
108 pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
109 Self {
110 key: key.into(),
111 label: label.into(),
112 detail: None,
113 tone: Tone::Normal,
114 selected: false,
115 removable: false,
116 value: None,
117 }
118 }
119}
120
121#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
123#[serde(rename_all = "snake_case")]
124pub enum Unit {
125 #[default]
126 Length,
127 Angle,
129 Fraction,
131}
132
133impl Unit {
134 fn step(self) -> f64 {
136 match self {
137 Unit::Length => 0.1,
138 Unit::Angle => 1.0,
139 Unit::Fraction => 0.01,
140 }
141 }
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
147#[serde(bound = "S: Scalar")]
148pub struct Track<S: Scalar> {
149 pub at: Vector3<S>,
150 pub direction: Vector3<S>,
151}
152
153#[derive(Clone, Debug, PartialEq, Serialize)]
155#[serde(bound = "S: Scalar")]
156pub struct Number<S: Scalar> {
157 pub label: String,
158 pub value: f64,
159 pub unit: Unit,
160 pub range: Option<[f64; 2]>,
162 pub step: f64,
163 #[serde(skip)]
165 pub handle: Option<Track<S>>,
166}
167
168impl<S: Scalar> Number<S> {
169 pub fn new(label: impl Into<String>, value: f64, unit: Unit) -> Self {
170 Self {
171 label: label.into(),
172 value,
173 unit,
174 range: None,
175 step: unit.step(),
176 handle: None,
177 }
178 }
179
180 pub fn range(mut self, min: f64, max: f64) -> Self {
182 self.range = Some([min, max]);
183 self.step = (max - min) / 200.0;
184 self
185 }
186
187 pub fn handle(mut self, handle: Option<Track<S>>) -> Self {
188 self.handle = handle;
189 self
190 }
191}
192
193#[derive(Clone, Debug, PartialEq, Serialize)]
195pub struct Picked {
196 pub entity: EntityRef,
197 pub detail: Option<String>,
199 pub tone: Tone,
200}
201
202#[derive(Clone, Debug, PartialEq, Serialize)]
212pub struct Reference {
213 pub label: String,
214 pub roles: Vec<Role>,
215 pub scope: Option<EntityRef>,
216 pub value: Vec<Picked>,
217 pub multiple: bool,
218 pub armed: bool,
220}
221
222impl Reference {
223 pub fn entities(&self) -> impl Iterator<Item = &EntityRef> {
225 self.value.iter().map(|p| &p.entity)
226 }
227}
228
229#[derive(Clone, Debug, PartialEq, Serialize)]
231#[serde(tag = "type", rename_all = "snake_case", bound = "S: Scalar")]
232pub enum Control<S: Scalar> {
233 Heading {
234 text: String,
235 },
236 Text {
237 text: String,
238 tone: Tone,
239 },
240 Actions {
243 actions: Vec<Action>,
244 },
245 Checkbox {
246 label: String,
247 value: bool,
248 },
249 Number(Number<S>),
250 Select {
252 label: String,
253 value: String,
254 options: Vec<Choice>,
255 },
256 Reference(Reference),
257 List {
258 items: Vec<ListItem>,
259 empty: String,
261 },
262}
263
264#[derive(Clone, Debug, PartialEq, Serialize)]
266#[serde(bound = "S: Scalar")]
267pub struct Field<S: Scalar> {
268 pub key: String,
269 #[serde(flatten)]
270 pub control: Control<S>,
271}
272
273#[derive(Clone, Debug, PartialEq, Serialize)]
277#[serde(transparent, bound = "S: Scalar")]
278pub struct Dialog<S: Scalar>(pub Vec<Field<S>>);
279
280impl<S: Scalar> Default for Dialog<S> {
281 fn default() -> Self {
282 Self(Vec::new())
283 }
284}
285
286impl<S: Scalar> Dialog<S> {
287 pub fn new() -> Self {
288 Self::default()
289 }
290
291 pub fn push(&mut self, key: impl Into<String>, control: Control<S>) -> &mut Self {
293 let key = key.into();
294 debug_assert!(
295 self.0.iter().all(|f| f.key != key),
296 "dialog key {key:?} used twice"
297 );
298 self.0.push(Field { key, control });
299 self
300 }
301
302 pub fn get(&self, key: &str) -> Option<&Control<S>> {
304 self.0.iter().find(|f| f.key == key).map(|f| &f.control)
305 }
306
307 pub(crate) fn references_mut(&mut self) -> impl Iterator<Item = (&str, &mut Reference)> {
309 self.0.iter_mut().filter_map(|f| match &mut f.control {
310 Control::Reference(r) => Some((f.key.as_str(), r)),
311 _ => None,
312 })
313 }
314
315 pub fn picked(&self) -> impl Iterator<Item = &EntityRef> {
317 self.0
318 .iter()
319 .flat_map(|f| match &f.control {
320 Control::Reference(r) => r.value.as_slice(),
321 _ => &[],
322 })
323 .map(|p| &p.entity)
324 }
325
326 pub fn missing(&self) -> Vec<&str> {
330 self.0
331 .iter()
332 .filter_map(|f| match &f.control {
333 Control::Reference(r) if r.value.is_empty() => Some(r.label.as_str()),
334 _ => None,
335 })
336 .collect()
337 }
338
339 pub fn summary(&self) -> String {
342 self.0
343 .iter()
344 .filter_map(|f| {
345 let value = match &f.control {
346 Control::Checkbox { label, value } => {
347 (label, if *value { "yes" } else { "no" }.to_string())
348 }
349 Control::Number(n) => (&n.label, format!("{:.2}", n.value)),
350 Control::Select {
351 label,
352 value,
353 options,
354 } => (
355 label,
356 options
357 .iter()
358 .find(|o| &o.value == value)
359 .map_or(value.clone(), |o| o.label.clone()),
360 ),
361 Control::Reference(r) => (
362 &r.label,
363 r.entities()
364 .map(EntityRef::label)
365 .collect::<Vec<_>>()
366 .join(" & "),
367 ),
368 _ => return None,
369 };
370 Some(format!("{}={}", value.0, value.1))
371 })
372 .collect::<Vec<_>>()
373 .join(", ")
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use geop_core_math::scalars::ScalInF64 as S;
380 use serde_json::json;
381
382 use super::*;
383 use crate::ui::Form;
384
385 #[test]
388 fn fields_serialize_flat() {
389 let mut form = Form::<S, (), ()>::new();
390 form.number(
391 "distance",
392 Number::new("distance", 1.0, Unit::Length)
393 .range(-1.0, 1.0)
394 .handle(Some(Track {
395 at: Vector3::zero(),
396 direction: Vector3::zero(),
397 })),
398 |_, _| {},
399 );
400 form.reference(
401 "sketch",
402 "sketch",
403 vec![EntityRef::Sketch { name: "k".into() }],
404 &[Role::Sketch],
405 None,
406 false,
407 |_, _| {},
408 );
409 assert_eq!(
410 serde_json::to_value(&form.dialog).unwrap(),
411 json!([
412 {
413 "key": "distance",
414 "type": "number",
415 "label": "distance",
416 "value": 1.0,
417 "unit": "length",
418 "range": [-1.0, 1.0],
419 "step": 0.01,
420 },
421 {
422 "key": "sketch",
423 "type": "reference",
424 "label": "sketch",
425 "roles": ["sketch"],
426 "scope": null,
427 "value": [{
428 "entity": {"type": "Sketch", "name": "k"},
429 "detail": null,
430 "tone": "normal",
431 }],
432 "multiple": false,
433 "armed": false,
434 },
435 ])
436 );
437 }
438}