Skip to main content

geop_ops/ui/
dialog.rs

1//! [`Dialog`]: the fields an operation shows for a step, in order.
2//!
3//! A field says what its value *means* — a length, an angle, entities of the
4//! part that can fill some [`Role`], things to do — never how to edit it:
5//! that is the editor's, the same for every operation (see
6//! [`super::StepEditor`]).
7
8use geop_core_math::{scalars::Scalar, vector::Vector3};
9use serde::Serialize;
10
11use crate::operation::{EntityRef, Role};
12
13/// How a text reads: plain, a hint, a problem, or good news.
14#[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/// Something to do or to choose, as an [`Control::Actions`] offers it.
25#[derive(Clone, Debug, PartialEq, Serialize)]
26pub struct Action {
27    /// What pressing it sends back, as [`super::Value::Choice`].
28    pub value: String,
29    pub label: String,
30    /// What it does — or, disabled, why it cannot be done now.
31    pub title: Option<String>,
32    /// Actions of one group are shown together under its name.
33    pub group: Option<String>,
34    pub enabled: bool,
35    /// Shown pressed: the tool in hand, the way chosen.
36    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    /// Disabled, saying why.
67    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/// One option of a [`Control::Select`].
75#[derive(Clone, Debug, PartialEq, Serialize)]
76pub struct Choice {
77    /// What choosing it sends back.
78    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/// One entry of a [`Control::List`], with its own key: pressing it sends
92/// [`super::Value::Press`], removing it `Remove`, editing its value
93/// `Number`.
94#[derive(Clone, Debug, PartialEq, Serialize)]
95pub struct ListItem {
96    pub key: String,
97    pub label: String,
98    /// Said about it, after the label.
99    pub detail: Option<String>,
100    pub tone: Tone,
101    pub selected: bool,
102    pub removable: bool,
103    /// A number to show and edit in place.
104    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/// What a number measures, which decides how it reads and steps.
122#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
123#[serde(rename_all = "snake_case")]
124pub enum Unit {
125    #[default]
126    Length,
127    /// In degrees.
128    Angle,
129    /// A fraction of something: `0` its start, `1` its end.
130    Fraction,
131}
132
133impl Unit {
134    /// How far one step of a field without a range goes.
135    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/// Where a number is dragged in the viewport: a handle at `at`, sliding
145/// along `direction` — moving it by `direction` adds one to the number.
146#[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/// A number field.
154#[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    /// What a slider offers, not what is valid.
161    pub range: Option<[f64; 2]>,
162    pub step: f64,
163    /// The handle to drag it by, drawn by the editor.
164    #[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    /// With a slider from `min` to `max`, in 200 steps.
181    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/// An entity a [`Reference`] holds, and what the editor found it to be.
194#[derive(Clone, Debug, PartialEq, Serialize)]
195pub struct Picked {
196    pub entity: EntityRef,
197    /// What it is used as, or why it cannot be.
198    pub detail: Option<String>,
199    pub tone: Tone,
200}
201
202/// Entities of the part a step builds on, picked in the viewport: any
203/// entity that can fill one of `roles` — and, with a `scope`, is part of it,
204/// like a line of one sketch. One entity, or — `multiple` — any number, a
205/// pick adding or taking out one.
206///
207/// The editor does all of the editing (see [`super::StepEditor`]): arming it
208/// for picks, removing and clearing entities, saying what each held is.
209/// An operation only ever gets what it holds now, as
210/// [`super::Value::Entities`].
211#[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    /// Whether a click in the viewport picks for it now.
219    pub armed: bool,
220}
221
222impl Reference {
223    /// The entities it holds.
224    pub fn entities(&self) -> impl Iterator<Item = &EntityRef> {
225        self.value.iter().map(|p| &p.entity)
226    }
227}
228
229/// A dialog primitive.
230#[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    /// Things to do or choose, as buttons: pressing one sends its value as
241    /// [`super::Value::Choice`]. Grouped ones are shown under their group.
242    Actions {
243        actions: Vec<Action>,
244    },
245    Checkbox {
246        label: String,
247        value: bool,
248    },
249    Number(Number<S>),
250    /// One of `options`, by value.
251    Select {
252        label: String,
253        value: String,
254        options: Vec<Choice>,
255    },
256    Reference(Reference),
257    List {
258        items: Vec<ListItem>,
259        /// Shown when there are no items.
260        empty: String,
261    },
262}
263
264/// A control, and the key the events it sends carry.
265#[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/// The fields an operation shows for a step, in order: an ordered
274/// dictionary from key to control, serialized as a list, so a UI renders it
275/// with one `map` and keys each element by it. Keys are unique.
276#[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    /// Appends `control` under `key`.
292    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    /// The field `key`.
303    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    /// Every reference field, by key.
308    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    /// Every entity its reference fields hold: what the step builds on.
316    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    /// The labels of its reference fields that hold nothing yet: what the
327    /// step still needs picked before it can be built at all. Until then,
328    /// that it does not build is no error — it is waiting.
329    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    /// Its values in one line, for a list of steps:
340    /// `sketch=outline, distance=1.00`.
341    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    /// Fields serialize flat, tagged with their type, as a viewer reads
386    /// them; a handle is the editor's to draw, not sent.
387    #[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}