Skip to main content

geop_core_editor/editing/
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, elements of the
4//! part of some kinds, things to do — never how to edit it:
5//! that is the editor's, the same for every operation (see
6//! the step editor, [`crate::step_editor`]).
7
8#[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
18/// Whether `text`, as a number field holds it, is a formula rather than a
19/// plain number.
20pub fn is_formula(text: &str) -> bool {
21    text.trim().parse::<f64>().is_err()
22}
23
24/// How a text reads: plain, a hint, a problem, or good news.
25#[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/// Where an editor shows a step's dialog, as the operation asks (see
36/// [`super::Form::layout`]).
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
38#[serde(rename_all = "snake_case")]
39pub enum Layout {
40    /// Beside the viewport, one field under the other: the part stays in
41    /// reach, for picks and drags.
42    #[default]
43    Sidebar,
44    /// Over the middle of the viewport, wide: for a form of tables, where
45    /// nothing is picked.
46    Popup,
47}
48
49/// What the cells of a [`Table`]'s column hold, which decides how they are
50/// edited.
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
52#[serde(tag = "type", rename_all = "snake_case")]
53pub enum ColumnKind {
54    /// A name, a word.
55    Text,
56    /// A number, of `unit`.
57    Number { unit: Unit },
58    /// A number, or a formula of the part's parameters: what it comes to,
59    /// or why it does not, is the cell's [`Cell::detail`].
60    Formula,
61}
62
63/// A column of a [`Table`].
64#[derive(Clone, Debug, PartialEq, Serialize)]
65pub struct Column {
66    pub label: String,
67    pub kind: ColumnKind,
68    /// Whether it is the user's: named by typing in its header
69    /// ([`super::TableEdit::Column`]), and taken out
70    /// ([`super::TableEdit::RemoveColumn`]).
71    pub editable: bool,
72}
73
74impl Column {
75    /// A column of `kind`, headed `label`, that is the operation's.
76    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    /// The user's: renamed and taken out (see [`Column::editable`]).
85    pub fn editable(mut self) -> Self {
86        self.editable = true;
87        self
88    }
89}
90
91/// One cell of a [`Table`]: what it holds, as text, and what is said of it.
92#[derive(Clone, Debug, PartialEq, Serialize)]
93pub struct Cell {
94    /// What it holds, as typed: a name, a number, a formula.
95    pub text: String,
96    /// Said beside it: what a formula comes to, or why it does not.
97    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    /// A number, as a cell shows it.
111    pub fn number(value: f64) -> Self {
112        Self::new(value.to_string())
113    }
114
115    /// With `detail` said beside it, in `tone`.
116    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/// One row of a [`Table`]: a cell per column.
124#[derive(Clone, Debug, PartialEq, Serialize)]
125pub struct TableRow {
126    pub cells: Vec<Cell>,
127    /// The row chosen, of a table one row of which is
128    /// ([`Table::selectable`]).
129    pub selected: bool,
130    /// Whether it can be taken out ([`super::TableEdit::RemoveRow`]).
131    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/// A table edited in place: columns with headers and kinds, rows of cells,
145/// rows added, taken out and moved, cells typed into one at a time or
146/// pasted as a block. Everything done to it is sent back as one
147/// [`super::Value::Table`] edit (see [`super::TableEdit`]); the operation
148/// holds the data, and says what each edit does.
149#[derive(Clone, Debug, PartialEq, Serialize)]
150pub struct Table {
151    pub columns: Vec<Column>,
152    pub rows: Vec<TableRow>,
153    /// Shown when there are no rows.
154    pub empty: String,
155    /// Whether rows are added, after the last ([`super::TableEdit::AddRow`]),
156    /// and moved ([`super::TableEdit::MoveRow`]).
157    pub rows_editable: bool,
158    /// Whether columns are added ([`super::TableEdit::AddColumn`]); the
159    /// ones that are the user's say so themselves ([`Column::editable`]).
160    pub columns_editable: bool,
161    /// Whether one row is chosen ([`super::TableEdit::SelectRow`],
162    /// [`TableRow::selected`]).
163    pub selectable: bool,
164}
165
166impl Table {
167    /// A table of `columns` and `rows`, saying `empty` without rows, whose
168    /// rows are the user's to add and move.
169    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    /// With columns the user adds (see [`Table::columns_editable`]).
181    pub fn columns_editable(mut self) -> Self {
182        self.columns_editable = true;
183        self
184    }
185
186    /// With one row chosen (see [`Table::selectable`]).
187    pub fn selectable(mut self) -> Self {
188        self.selectable = true;
189        self
190    }
191}
192
193/// Something to do or to choose, as an [`Control::Actions`] offers it.
194#[derive(Clone, Debug, PartialEq, Serialize)]
195pub struct Action {
196    /// What pressing it sends back, as [`super::Value::Choice`].
197    pub value: String,
198    pub label: String,
199    /// What it does — or, disabled, why it cannot be done now.
200    pub title: Option<String>,
201    /// Actions of one group are shown together under its name.
202    pub group: Option<String>,
203    pub enabled: bool,
204    /// Shown pressed: the tool in hand, the way chosen.
205    pub active: bool,
206    /// The icon it is shown as, with its label as its tooltip — SVG on a
207    /// 20 × 20 grid, drawn in the current colour (see
208    /// [`crate::editable::OperationInfo::icon`]); none to show the label.
209    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    /// Shown as `icon`, if one is given (see [`Action::icon`]).
226    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    /// Disabled, saying why.
247    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/// One option of a [`Control::Select`].
255#[derive(Clone, Debug, PartialEq, Serialize)]
256pub struct Choice {
257    /// What choosing it sends back.
258    pub value: String,
259    pub label: String,
260}
261
262/// A file a [`Control::File`] shows: by its path as the program names
263/// it, `/`-separated — `../parts/bolt.geop`, a shared library's by its
264/// scheme, `std:hex_nut.geop` — and whether the field takes it.
265#[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/// One entry of a [`Control::List`], with its own key: pressing it sends
281/// [`super::Value::Press`], removing it `Remove`, editing its value
282/// `Number`.
283#[derive(Clone, Debug, PartialEq, Serialize)]
284pub struct ListItem {
285    pub key: String,
286    pub label: String,
287    /// Said about it, after the label.
288    pub detail: Option<String>,
289    pub tone: Tone,
290    pub selected: bool,
291    pub removable: bool,
292    /// A number to show and edit in place.
293    pub value: Option<f64>,
294    /// Text to show and edit in place — a value as a formula — sent back
295    /// as [`super::Value::Text`].
296    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/// What a number measures, which decides how it reads and steps.
315#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
316#[serde(rename_all = "snake_case")]
317pub enum Unit {
318    #[default]
319    Length,
320    /// In degrees.
321    Angle,
322    /// A fraction of something: `0` its start, `1` its end.
323    Fraction,
324    /// How many of something: a whole number.
325    Count,
326}
327
328impl Unit {
329    /// How far one step of a field without a range goes.
330    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/// Where a number is dragged in the viewport: a handle at `at`, sliding
341/// along `direction` — moving it by `direction` adds one to the number.
342#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
343pub struct Track {
344    pub at: Vector3,
345    pub direction: Vector3,
346}
347
348/// A number field — of a plain number, or of one that may also be given
349/// as a formula of the part's parameters (see [`Number::text`]).
350#[derive(Clone, Debug, PartialEq, Serialize)]
351pub struct Number {
352    pub label: String,
353    /// The value — of a formula, what it evaluates to now.
354    pub value: f64,
355    pub unit: Unit,
356    /// What a slider offers, not what is valid.
357    pub range: Option<[f64; 2]>,
358    pub step: f64,
359    /// For a field that takes formulas, the value as given — a plain
360    /// number or a formula — shown and edited in place of `value`, and sent
361    /// back as [`super::Value::Text`].
362    pub text: Option<String>,
363    /// Why its formula does not evaluate, if it does not.
364    pub error: Option<String>,
365    /// The handle to drag it by, drawn by the editor.
366    #[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    /// A number field of `input`, labelled `label`, showing what it comes
385    /// to as `reader` reads the parameters — or, where it does not
386    /// evaluate, why.
387    ///
388    /// A value that follows a formula is neither slid nor dragged: moving it
389    /// would have to either break the formula or move the parameters it
390    /// reads, and neither is what a drag means. So such a field has no
391    /// slider and no handle ([`Number::range`] and [`Number::handle`] keep
392    /// none); to drag the value again, type a number.
393    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    /// Whether its value follows a formula.
411    pub fn follows_formula(&self) -> bool {
412        self.text.as_deref().is_some_and(is_formula)
413    }
414
415    /// With a slider from `min` to `max`, in 200 steps — unless it follows
416    /// a formula.
417    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    /// With `handle` to drag it by — unless it follows a formula.
426    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/// An element a [`Reference`] holds, and what the operation says of it.
435#[derive(Clone, Debug, PartialEq, Serialize)]
436pub struct Picked {
437    pub element: PickedElement,
438    /// What an editor draws of it, lit while it is held (see
439    /// [`crate::selectable::Selectable::drawn`]).
440    #[serde(skip)]
441    pub drawn: Vec<PickedElement>,
442    /// What an editor draws of it besides, while it is held (see
443    /// [`crate::selectable::Selectable::visuals`]).
444    #[serde(skip)]
445    pub visuals: Vec<Visual>,
446    /// What it is used as, or why it cannot be.
447    pub detail: Option<String>,
448    pub tone: Tone,
449}
450
451impl Picked {
452    /// `element`, drawn as itself, said nothing of.
453    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/// What a click on an element would pick for a reference field, in the
465/// part a reader reads, lit and drawn as it would be held (see
466/// [`crate::selectable::previewed`]); none if it would pick nothing. It
467/// follows from what the field picks (see [`Reference::accepts`]), so two
468/// previews count as equal: a function's address says nothing.
469#[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/// Elements of the part a step builds on, picked in the viewport: any
485/// element of one of the kinds `accepts` (see
486/// [`geop_core_program::element::Kind::NAME`]). One element, or — `multiple` — any
487/// number, a pick adding or taking out one. Which of them the step can use
488/// is the operation's to say, in what it holds ([`Picked::tone`]).
489///
490/// The editor does all of the editing (see [`crate::step_editor`]): arming it
491/// for picks, removing and clearing elements. An operation only ever gets
492/// what it holds now, as [`super::Value::Elements`].
493#[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    /// Whether the step needs it to hold something before it can be built;
500    /// one that is not is only ever picked for when asked to.
501    pub required: bool,
502    /// Whether a click in the viewport picks for it now.
503    pub armed: bool,
504    /// What an editor draws of what a click would pick for it, while the
505    /// pointer is over it.
506    #[serde(skip)]
507    pub preview: Preview,
508}
509
510impl Reference {
511    /// Whether it is waiting for what the step needs: required, and empty.
512    pub fn waiting(&self) -> bool {
513        self.required && self.value.is_empty()
514    }
515
516    /// The elements it holds.
517    pub fn elements(&self) -> impl Iterator<Item = &PickedElement> {
518        self.value.iter().map(|p| &p.element)
519    }
520}
521
522/// A dialog primitive.
523#[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    /// Text typed — a name, a title — sent back as [`super::Value::Text`].
534    Input {
535        label: String,
536        value: String,
537        /// Shown in place of an empty value.
538        placeholder: Option<String>,
539    },
540    /// Things to do or choose, as buttons: pressing one sends its value as
541    /// [`super::Value::Choice`]. Grouped ones are shown under their group.
542    Actions {
543        actions: Vec<Action>,
544    },
545    Checkbox {
546        label: String,
547        value: bool,
548    },
549    /// Files the step can be saved as, one button per format: pressing
550    /// one asks the editor's exporter `exporter` for the file in that
551    /// format (the choice's value), written as the step now is — a
552    /// drawing — and saves it. It sets nothing.
553    Download {
554        exporter: String,
555        formats: Vec<Choice>,
556    },
557    Number(Number),
558    /// One of `options`, by value — found by typing, if `searchable`.
559    Select {
560        label: String,
561        value: String,
562        options: Vec<Choice>,
563        searchable: bool,
564    },
565    /// A file the program reads or places, by its path relative to the
566    /// program, chosen in an explorer of `files` — every file of the
567    /// workspace but the program's own, those the field does not take
568    /// among them, so the folders read as they are — and sent back as
569    /// [`super::Value::Choice`]. A file of the kinds `upload` (extensions,
570    /// without the dot; none, for a field taking only files there are) may
571    /// be added from elsewhere: the front end stores it next to the
572    /// program and sends its path.
573    File {
574        label: String,
575        value: String,
576        files: Vec<FileEntry>,
577        upload: Vec<String>,
578    },
579    /// A colour, `#rrggbb`, sent back as [`super::Value::Text`].
580    Color {
581        label: String,
582        value: String,
583    },
584    Reference(Reference),
585    List {
586        items: Vec<ListItem>,
587        /// Shown when there are no items.
588        empty: String,
589    },
590    /// A table edited in place, every edit of it sent back as one
591    /// [`super::Value::Table`].
592    Table(Table),
593}
594
595/// A control, and the key the events it sends carry.
596#[derive(Clone, Debug, PartialEq, Serialize)]
597pub struct Field {
598    pub key: String,
599    #[serde(flatten)]
600    pub control: Control,
601    /// Why the step does not build, where it is this field's value that
602    /// it does not build with (see [`GeopError::concerning`]): shown
603    /// beside it.
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub failure: Option<String>,
606}
607
608/// The fields an operation shows for a step, in order: an ordered
609/// dictionary from key to control, serialized as a list, so a UI renders it
610/// with one `map` and keys each element by it. Keys are unique.
611#[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    /// Appends `control` under `key`.
621    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    /// Text typed: a name, a title (see [`Control::Input`]).
650    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    /// A table edited in place (see [`Table`]).
668    pub fn table(&mut self, key: &str, table: Table) -> &mut Self {
669        self.push(key, Control::Table(table))
670    }
671
672    /// A list of entries, each with a key of its own.
673    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    /// Files the step can be saved as by the exporter `exporter`, one
684    /// button per format in `formats` (see [`Control::Download`]).
685    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    /// Things to do or choose: pressing one sends its value as
696    /// [`super::Value::Choice`].
697    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    /// One of `options`, by value — found by typing, if `searchable`.
716    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    /// A file the program reads or places, by its path relative to the
736    /// program: one of `files` it takes, or one the user adds from
737    /// elsewhere of the kinds `upload`, extensions without the dot (see
738    /// [`Control::File`]).
739    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    /// A colour, `#rrggbb`.
759    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    /// Values picked, holding `value`, each selected by a click as `P` says
775    /// (see [`Selectable`]) in the part `reader` reads. What a field so made
776    /// is sent back, [`crate::selectable::selected`] turns into values. Required: see [`Dialog::optional`].
777    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    /// Elements of the kinds `accepts` (see [`Reference`]), holding `value`,
803    /// what a click would pick drawn as `preview` says. Required: see
804    /// [`Dialog::optional`].
805    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    /// Makes the reference field `key` optional: the step builds without
829    /// it, and it is only picked for when pressed (see
830    /// [`Reference::required`]).
831    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    /// The field `key`.
841    pub fn get(&self, key: &str) -> Option<&Control> {
842        self.0.iter().find(|f| f.key == key).map(|f| &f.control)
843    }
844
845    /// Every reference field, by key.
846    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    /// Every reference field, by key, to change.
854    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    /// What a click on `element` would pick for the reference field
862    /// `key`, in the part `reader` reads: as it would be held, to light
863    /// and draw while the pointer is over it (see [`Preview`]).
864    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    /// What an editor draws of every value its reference fields hold
870    /// besides lighting it (see [`Picked::visuals`]), drawn as selected —
871    /// and of `hovered`, what a click would pick now, drawn as hovered.
872    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    /// What an editor draws of every element its reference fields hold:
888    /// what the step builds on, to light.
889    /// Says `error` beside the field `key`, if it has one: whether it did.
890    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    /// The labels of its reference fields that hold nothing yet: what the
906    /// step still needs picked before it can be built at all. Until then,
907    /// that it does not build is no error — it is waiting.
908    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    /// Its values in one line, for a list of steps:
919    /// `sketch=outline, distance=1.00`.
920    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    /// Fields serialize flat, tagged with their type, as a viewer reads
981    /// them; a handle is the editor's to draw, not sent.
982    #[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    /// A table is sent whole — its columns by kind, its rows of cells —
1045    /// and an input as its text; a table of one row is summarised as such.
1046    #[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}