1use std::{any::Any, rc::Rc};
6
7use geop_core_math::{geop_error::GeopResult, vector::Vector3};
8
9use crate::{
10 editable::{Against, Editors},
11 editing::{
12 Action, CanvasEvent, Control, DRAG_SNAP, Dialog, Event, Form, Gizmo, GizmoView, InHand,
13 Orientation, Picked, Pointer, Presentation, Reference, Shape, StepEditEvent, Style, Value,
14 Visual,
15 gizmo::GizmoGrab,
16 hit::{Cycle, Scene, cycled, hit_visuals, visuals_under},
17 },
18};
19use geop_core_program::{element::PickedElement, program::Program, reader::Reader, runner::Runner};
20
21pub const GIZMO_ORIENTATION: &str = "gizmo_orientation";
25
26type Start = (serde_json::Value, Rc<dyn Any>);
29
30enum Grab {
32 Handle { key: String, value: f64 },
34 Visual {
39 key: String,
40 plane: Option<(Vector3, Vector3)>,
41 start: Option<Start>,
42 },
43 Stroke,
45 Gizmo {
50 grab: GizmoGrab,
51 from: Box<Gizmo>,
52 start: Start,
53 readout: Option<String>,
54 },
55}
56
57pub struct StepEditor {
108 step: String,
109 editing: Rc<dyn Any>,
110 armed: Option<String>,
112 hover: Option<PickedElement>,
114 cycle: Option<Cycle>,
117 pointer: Option<Pointer>,
119 grab: Option<(Pointer, Grab)>,
121 orientation: Orientation,
123 references: Option<Vec<String>>,
127}
128
129fn toggle<T: PartialEq>(xs: &mut Vec<T>, x: T) {
131 match xs.iter().position(|y| *y == x) {
132 Some(i) => {
133 xs.remove(i);
134 }
135 None => xs.push(x),
136 }
137}
138
139fn handles(dialog: &Dialog) -> Vec<Visual> {
141 (dialog.0.iter())
142 .filter_map(|f| match &f.control {
143 Control::Number(n) => n.handle.map(|track| {
144 let shape = Shape::Handle {
145 at: track.at,
146 direction: track.direction,
147 };
148 Visual::new(f.key.clone(), shape, Style::Handle)
149 }),
150 _ => None,
151 })
152 .collect()
153}
154
155fn is_handle(visual: &Visual) -> bool {
156 matches!(visual.shape, Shape::Handle { .. })
157}
158
159fn grabs(visual: &Visual, tool: InHand) -> bool {
162 is_handle(visual) || (visual.draggable && tool == InHand::Nothing)
163}
164
165fn waiting(form: &Form) -> impl Iterator<Item = &str> {
167 (form.dialog.0.iter()).filter_map(|f| match &f.control {
168 Control::Reference(r) if r.waiting() => Some(f.key.as_str()),
169 _ => None,
170 })
171}
172
173fn references(form: &Form) -> Vec<String> {
175 (form.dialog.0.iter())
176 .filter(|f| matches!(f.control, Control::Reference(_)))
177 .map(|f| f.key.clone())
178 .collect()
179}
180
181fn candidates(
189 scene: &dyn Scene,
190 reader: &Reader,
191 dialog: &Dialog,
192 key: &str,
193 reference: &Reference,
194 pointer: &Pointer,
195) -> Vec<(PickedElement, Option<Picked>)> {
196 let mut found: Vec<(PickedElement, Option<Picked>)> = Vec::new();
197 for hit in scene.under(pointer, &reference.accepts) {
198 let held = dialog.previewed(reader, key, &hit.element);
199 let behind = !found.is_empty();
200 let known = |held: &Picked| {
201 (found.iter()).any(|(_, h)| h.as_ref().is_some_and(|h| h.element == held.element))
202 };
203 if behind && held.as_ref().is_none_or(known) {
204 continue;
205 }
206 found.push((hit.element, held));
207 }
208 found
209}
210
211pub struct Editing<'a, Sc> {
216 pub editors: &'a Editors,
217 pub runner: &'a Runner,
218 pub part: Against<'a>,
219 pub program: &'a mut Program,
220 pub scene: &'a Sc,
221}
222
223impl StepEditor {
224 pub fn new<'a>(
230 editors: &Editors,
231 runner: &Runner,
232 part: impl Into<Against<'a>>,
233 program: &Program,
234 step: &str,
235 new: bool,
236 ) -> GeopResult<Self> {
237 let editing: Rc<dyn Any> = Rc::from(editors.initial_editing(program, step)?);
238 let armed = match new {
239 true => {
240 let form = editors.presentation(runner, part, program, step, &*editing)?;
241 waiting(&form).next().map(str::to_string)
242 }
243 false => None,
244 };
245 Ok(Self {
246 step: step.to_string(),
247 editing,
248 armed,
249 hover: None,
250 cycle: None,
251 pointer: None,
252 grab: None,
253 orientation: Orientation::default(),
254 references: None,
255 })
256 }
257
258 pub fn step(&self) -> &str {
260 &self.step
261 }
262
263 pub fn editing(&self) -> &dyn Any {
265 &*self.editing
266 }
267
268 pub fn restart<'a>(
275 &mut self,
276 editors: &Editors,
277 runner: &Runner,
278 part: impl Into<Against<'a>>,
279 program: &Program,
280 ) -> GeopResult<()> {
281 self.editing = Rc::from(editors.initial_editing(program, &self.step)?);
282 self.hover = None;
283 self.cycle = None;
284 self.grab = None;
285 let references = references(&self.form(editors, runner, part, program)?);
286 self.armed = (self.armed.take()).filter(|armed| references.contains(armed));
287 self.references = Some(references);
288 Ok(())
289 }
290
291 pub fn form<'a>(
293 &self,
294 editors: &Editors,
295 runner: &Runner,
296 part: impl Into<Against<'a>>,
297 program: &Program,
298 ) -> GeopResult<Form> {
299 editors.presentation(runner, part, program, &self.step, &*self.editing)
300 }
301
302 pub fn at_rest(&self, form: &Form) -> bool {
308 self.armed.is_none()
309 && self.grab.is_none()
310 && form.selection.is_empty()
311 && form.prompt.is_none()
312 && form.tool == InHand::Nothing
313 }
314
315 fn send<Sc: Scene>(&mut self, e: &mut Editing<'_, Sc>, event: Event) -> GeopResult<()> {
321 let before = match self.references.take() {
322 Some(known) => known,
323 None => references(&self.form(e.editors, e.runner, e.part.clone(), e.program)?),
324 };
325 let scene: &dyn Scene = e.scene;
326 let edited = (e.editors).process_user_event(
327 e.runner,
328 e.part.clone(),
329 e.program,
330 &self.step,
331 &*self.editing,
332 event,
333 Some(scene),
334 )?;
335 e.program.step_mut(&self.step)?.args = edited.args;
336 self.editing = Rc::from(edited.editing);
337 let form = self.form(e.editors, e.runner, e.part.clone(), e.program)?;
338 self.references = Some(references(&form));
339 if let Some(armed) = &self.armed
340 && !references(&form).contains(armed)
341 {
342 self.armed = None;
343 self.hover = None;
344 }
345 if let Some(key) = waiting(&form).find(|key| !before.iter().any(|b| b == key)) {
346 self.armed = Some(key.to_string());
347 self.hover = None;
348 }
349 Ok(())
350 }
351
352 fn pass<Sc: Scene>(&mut self, e: &mut Editing<'_, Sc>, event: CanvasEvent) -> GeopResult<()> {
353 self.send(e, Event::Canvas(event))
354 }
355
356 fn select<Sc: Scene>(
358 &mut self,
359 e: &mut Editing<'_, Sc>,
360 form: &Form,
361 selection: Vec<String>,
362 ) -> GeopResult<()> {
363 match selection == form.selection {
364 true => Ok(()),
365 false => self.send(e, Event::Select(selection)),
366 }
367 }
368
369 fn start<Sc: Scene>(&self, e: &Editing<'_, Sc>) -> Start {
371 let args = (e.program.step(&self.step))
372 .map(|s| s.args.clone())
373 .unwrap_or_default();
374 (args, self.editing.clone())
375 }
376
377 fn restore<Sc: Scene>(&mut self, e: &mut Editing<'_, Sc>, start: &Start) {
379 if let Ok(step) = e.program.step_mut(&self.step) {
380 step.args = start.0.clone();
381 }
382 self.editing = start.1.clone();
383 }
384
385 fn gizmo(&self, form: &Form) -> Option<GizmoView> {
387 (form.gizmo.as_ref()).map(|gizmo| GizmoView::of(gizmo, self.orientation))
388 }
389
390 pub fn handle<Sc: Scene>(
394 &mut self,
395 mut e: Editing<'_, Sc>,
396 event: &StepEditEvent,
397 ) -> GeopResult<()> {
398 let e = &mut e;
399 let form = self.form(e.editors, e.runner, e.part.clone(), e.program)?;
400 if let StepEditEvent::Dialog { key, value } = event {
401 return self.dialog(e, &form, key, value.clone());
402 }
403 if let StepEditEvent::View { up } = event {
405 return self.pass(e, CanvasEvent::View { up: *up });
406 }
407 if let (
412 StepEditEvent::Click {
413 pointer,
414 double: false,
415 ..
416 },
417 Some(gizmo),
418 None,
419 ) = (event, self.gizmo(&form), &self.armed)
420 && gizmo.hit(pointer).is_some()
421 {
422 self.pointer = Some(*pointer);
423 return Ok(());
424 }
425 match event {
426 StepEditEvent::Hover { pointer, .. } | StepEditEvent::Click { pointer, .. } => {
427 self.pointer = Some(*pointer)
428 }
429 StepEditEvent::Leave => self.pointer = None,
430 _ => {}
431 }
432 if let Some(key) = self.armed.clone() {
433 if let Some(Control::Reference(reference)) = form.dialog.get(&key) {
434 return match event {
437 StepEditEvent::Drag {
438 from,
439 to,
440 done,
441 shift,
442 } => self.drag(e, &form, from, to, *done, *shift, true),
443 _ => self.pick(e, &form.dialog, event, &key, reference),
444 };
445 }
446 self.armed = None;
447 }
448 match event {
449 StepEditEvent::Dialog { .. } | StepEditEvent::View { .. } => Ok(()),
450 StepEditEvent::Hover { pointer, shift } => self.pass(
451 e,
452 CanvasEvent::Hover {
453 pointer: *pointer,
454 shift: *shift,
455 },
456 ),
457 StepEditEvent::Leave => self.pass(e, CanvasEvent::Leave),
458 StepEditEvent::Click {
459 pointer,
460 double,
461 shift,
462 } => {
463 let in_hand = form.tool != InHand::Nothing;
464 let hit = (!*double && !in_hand)
465 .then(|| {
466 let instance = |name: &str, p: &Pointer| e.scene.hit_instance(name, p);
467 hit_visuals(&form.visuals, pointer, instance, |v| v.selectable)
468 .map(|hit| hit.visual.key.clone())
469 })
470 .flatten();
471 match hit {
472 Some(key) => {
473 let mut selection = form.selection.clone();
474 toggle(&mut selection, key);
475 self.select(e, &form, selection)
476 }
477 None => {
478 if !*shift && !in_hand {
479 self.select(e, &form, Vec::new())?;
480 }
481 self.pass(
482 e,
483 CanvasEvent::Click {
484 pointer: *pointer,
485 double: *double,
486 shift: *shift,
487 },
488 )
489 }
490 }
491 }
492 StepEditEvent::Drag {
493 from,
494 to,
495 done,
496 shift,
497 } => self.drag(e, &form, from, to, *done, *shift, false),
498 StepEditEvent::SelectBox {
499 corners,
500 crossing,
501 add,
502 } => match form.select_box && form.tool == InHand::Nothing {
503 true => self.pass(
504 e,
505 CanvasEvent::SelectBox {
506 corners: **corners,
507 crossing: *crossing,
508 add: *add,
509 },
510 ),
511 false => Ok(()),
512 },
513 StepEditEvent::Key { key } => match key.as_str() {
514 "Escape" => {
515 self.select(e, &form, Vec::new())?;
516 self.pass(e, CanvasEvent::Cancel)
517 }
518 "Enter" => self.pass(e, CanvasEvent::Confirm),
519 "Delete" | "Backspace" => self.pass(e, CanvasEvent::Delete),
520 _ => {
521 let key = key.to_lowercase();
522 self.pass(e, CanvasEvent::Key { key })
523 }
524 },
525 }
526 }
527
528 fn dialog<Sc: Scene>(
531 &mut self,
532 e: &mut Editing<'_, Sc>,
533 form: &Form,
534 key: &str,
535 value: Value,
536 ) -> GeopResult<()> {
537 if key == GIZMO_ORIENTATION {
538 match value {
539 Value::Choice(c) if c == "world" => self.orientation = Orientation::World,
540 Value::Choice(c) if c == "local" => self.orientation = Orientation::Local,
541 _ => {}
542 }
543 return Ok(());
544 }
545 let Some(Control::Reference(reference)) = form.dialog.get(key) else {
546 let key = key.to_string();
547 return self.send(e, Event::Field { key, value });
548 };
549 let mut elements: Vec<PickedElement> = reference.elements().cloned().collect();
550 match value {
551 Value::Press => {
552 self.armed = (self.armed.as_deref() != Some(key)).then(|| key.to_string());
553 self.hover = None;
554 return Ok(());
555 }
556 Value::RemoveAt(i) if i < elements.len() => {
557 elements.remove(i);
558 }
559 Value::Clear => elements.clear(),
560 Value::Elements(set) => elements = set,
561 _ => return Ok(()),
562 }
563 let (key, value) = (key.to_string(), Value::Elements(elements));
564 self.send(e, Event::Field { key, value })
565 }
566
567 fn pick<Sc: Scene>(
574 &mut self,
575 e: &mut Editing<'_, Sc>,
576 dialog: &Dialog,
577 event: &StepEditEvent,
578 key: &str,
579 reference: &Reference,
580 ) -> GeopResult<()> {
581 let reader = self.picked_in(e.editors, e.part.clone(), e.program)?;
582 let scene: &dyn Scene = e.scene;
583 let under = |pointer: &Pointer| {
584 let found = candidates(scene, &reader, dialog, key, reference, pointer);
585 let cycle = Cycle::of(found.len(), pointer);
586 (cycled(found, pointer.cycle).map(|(hit, _)| hit), cycle)
587 };
588 match event {
589 StepEditEvent::Hover { pointer, .. } => (self.hover, self.cycle) = under(pointer),
590 StepEditEvent::Leave => (self.hover, self.cycle) = (None, None),
591 StepEditEvent::Click { pointer, .. } => {
592 let (picked, _) = under(pointer);
593 (self.hover, self.cycle) = (None, None);
594 if let Some(element) = picked {
595 let mut elements: Vec<PickedElement> = reference.elements().cloned().collect();
596 if reference.multiple {
597 toggle(&mut elements, element);
598 } else {
599 elements = vec![element];
600 self.armed = None;
601 }
602 let (key, value) = (key.to_string(), Value::Elements(elements));
603 self.send(e, Event::Field { key, value })?;
604 if self.armed.is_none() {
607 let form = self.form(e.editors, e.runner, e.part.clone(), e.program)?;
608 self.armed = waiting(&form).next().map(str::to_string);
609 }
610 return Ok(());
611 }
612 }
613 StepEditEvent::Key { key } if key == "Escape" => {
614 self.armed = None;
615 (self.hover, self.cycle) = (None, None);
616 }
617 _ => {}
618 }
619 Ok(())
620 }
621
622 fn picked_in<'a>(
629 &self,
630 editors: &Editors,
631 part: Against<'a>,
632 program: &Program,
633 ) -> GeopResult<Reader<'a>> {
634 let info = (program.step(&self.step).ok()).and_then(|step| editors.info(&step.operation));
635 match info.map(|listed| (listed.info.picks_whole, listed.info.picks_built)) {
636 Some((true, _)) => Ok(part.current()),
637 Some((_, true)) => Ok(part.after(&self.step)),
638 _ => part.before(program, &self.step),
639 }
640 }
641
642 #[allow(clippy::too_many_arguments)]
658 fn drag<Sc: Scene>(
659 &mut self,
660 e: &mut Editing<'_, Sc>,
661 form: &Form,
662 from: &Pointer,
663 to: &Pointer,
664 done: bool,
665 shift: bool,
666 handles_only: bool,
667 ) -> GeopResult<()> {
668 let handles = handles(&form.dialog);
669 if self.grab.as_ref().is_some_and(|(down, _)| down != from) {
670 self.grab = None;
671 }
672 if self.grab.is_none()
673 && let (Some(gizmo), Some(asked)) = (self.gizmo(form), &form.gizmo)
674 && let Some(part) = gizmo.hit(from)
675 {
676 let grab = Grab::Gizmo {
677 grab: GizmoGrab::new(gizmo, part, from),
678 from: Box::new(asked.clone()),
679 start: self.start(e),
680 readout: None,
681 };
682 self.grab = Some((*from, grab));
683 }
684 if self.grab.is_none() {
685 let visuals: Vec<Visual> = form.visuals.iter().chain(&handles).cloned().collect();
686 let instance = |name: &str, p: &Pointer| e.scene.hit_instance(name, p);
687 let hit = hit_visuals(&visuals, from, instance, |v| match handles_only {
688 true => is_handle(v),
689 false => grabs(v, form.tool),
690 });
691 let grab = match hit {
692 Some(hit) => {
693 let key = hit.visual.key.clone();
694 match form.dialog.get(&key) {
695 Some(Control::Number(n)) if is_handle(hit.visual) => Grab::Handle {
696 key,
697 value: n.value,
698 },
699 _ => Grab::Visual {
700 key,
701 plane: (form.focus.is_none())
702 .then(|| (from.ray.at(hit.t), *from.ray.dir())),
703 start: hit.visual.from_grab.then(|| self.start(e)),
704 },
705 }
706 }
707 None if form.tool == InHand::Strokes && !handles_only => Grab::Stroke,
708 None => return Ok(()),
709 };
710 self.grab = Some((*from, grab));
711 }
712 let Some((down, mut grab)) = self.grab.take() else {
713 return Ok(());
714 };
715 let moved = self.drag_by(e, form, &handles, &mut grab, from, to, done, shift);
716 if !done {
718 self.grab = Some((down, grab));
719 }
720 moved
721 }
722
723 #[allow(clippy::too_many_arguments)]
725 fn drag_by<Sc: Scene>(
726 &mut self,
727 e: &mut Editing<'_, Sc>,
728 form: &Form,
729 handles: &[Visual],
730 grab: &mut Grab,
731 from: &Pointer,
732 to: &Pointer,
733 done: bool,
734 shift: bool,
735 ) -> GeopResult<()> {
736 match grab {
737 Grab::Handle { key, value } => {
738 let track =
739 (handles.iter())
740 .filter(|v| v.key == *key)
741 .find_map(|v| match v.shape {
742 Shape::Handle { at, direction } => Some((at, direction)),
743 _ => None,
744 });
745 let moved = track.and_then(|(at, direction)| {
746 let along = |p: &Pointer| p.ray.line_parameter(&at, &direction);
747 Some(along(to)?.sub(along(from)?))
748 });
749 match moved {
750 Some(moved) => {
751 let value = *value + moved.to_f64();
752 let snapped = (value / DRAG_SNAP).round() * DRAG_SNAP;
753 let (key, value) = (key.clone(), Value::Number(snapped));
754 self.send(e, Event::Field { key, value })
755 }
756 None => Ok(()),
757 }
758 }
759 Grab::Visual { key, plane, start } => {
760 let in_plane = |pointer: &Pointer| -> Option<Vector3> {
761 let (origin, normal) = match (&form.focus, &plane) {
762 (Some(focus), _) => (focus.origin(), focus.w()),
763 (None, Some((point, normal))) => (point, normal),
764 (None, None) => return None,
765 };
766 pointer
767 .ray
768 .intersect_plane(origin, normal)
769 .map(|(_, at)| at)
770 };
771 match (in_plane(from), in_plane(to)) {
772 (Some(from), Some(at)) => {
773 if let Some(start) = start {
774 self.restore(e, start);
775 }
776 let event = CanvasEvent::Move {
777 key: key.clone(),
778 from,
779 to: at,
780 pointer: *to,
781 done,
782 shift,
783 };
784 self.pass(e, event)
785 }
786 _ => Ok(()),
787 }
788 }
789 Grab::Gizmo {
790 grab,
791 from: asked,
792 start,
793 readout,
794 } => {
795 let Some((drag, text)) = grab.drag(from, to, shift) else {
796 return Ok(());
797 };
798 self.restore(e, start);
799 *readout = Some(text);
800 let from = Gizmo::clone(asked);
801 self.pass(e, CanvasEvent::Gizmo { drag, from, done })
802 }
803 Grab::Stroke => {
804 let event = CanvasEvent::Stroke {
805 from: *from,
806 to: *to,
807 done,
808 shift,
809 };
810 self.pass(e, event)
811 }
812 }
813 }
814
815 pub fn presentation<'a>(
824 &self,
825 editors: &Editors,
826 runner: &Runner,
827 part: impl Into<Against<'a>>,
828 program: &Program,
829 scene: &impl Scene,
830 ) -> GeopResult<Presentation> {
831 let part = part.into();
832 let mut form = self.form(editors, runner, part.clone(), program)?;
833 let mut pickable = Vec::new();
834 for (key, reference) in form.dialog.references_mut() {
835 if self.armed.as_deref() == Some(key) {
836 reference.armed = true;
837 pickable = reference.accepts.clone();
838 }
839 }
840 let mut gizmo = self.gizmo(&form);
841 if form.gizmo.as_ref().is_some_and(|g| g.axes.is_some()) {
842 let choice = |value: &str, label: &str, orientation: Orientation, title: &str| {
843 Action::new(value, label)
844 .group("Gizmo")
845 .title(title)
846 .active(self.orientation == orientation)
847 };
848 let actions = vec![
849 choice(
850 "world",
851 "World",
852 Orientation::World,
853 "Move, turn and scale along the world's axes",
854 ),
855 choice(
856 "local",
857 "Local",
858 Orientation::Local,
859 "Move, turn and scale along its own axes",
860 ),
861 ];
862 form.dialog.actions(GIZMO_ORIENTATION, actions);
863 }
864 let mut visuals = form.visuals;
865 visuals.extend(handles(&form.dialog));
866 let picked_in = self.picked_in(editors, part.clone(), program)?;
872 let previewed = (self.armed.as_deref().zip(self.hover.as_ref()))
873 .and_then(|(key, element)| form.dialog.previewed(&picked_in, key, element));
874 let shown = form.dialog.shown(previewed.as_ref());
875 let tool = form.tool;
876 let waiting = self.armed.is_some();
880 if let Some(gizmo) = &mut gizmo {
881 match &self.grab {
882 Some((_, Grab::Gizmo { grab, readout, .. })) => {
883 gizmo.active = Some(grab.part);
884 gizmo.readout = readout.clone();
885 }
886 _ => gizmo.hover = self.pointer.and_then(|pointer| gizmo.hit(&pointer)),
887 }
888 }
889 let on_gizmo = gizmo.as_ref().is_some_and(|g| g.hover.is_some());
890 let under = self.pointer.filter(|_| !on_gizmo).map(|pointer| {
891 let instance = |name: &str, p: &Pointer| scene.hit_instance(name, p);
892 let under = visuals_under(&visuals, &pointer, instance, |v| match waiting {
893 true => is_handle(v),
894 false => v.selectable || grabs(v, tool),
895 });
896 let cycle = Cycle::of(under.len(), &pointer);
897 let hovered = cycled(under, pointer.cycle)
898 .map(|hit| (hit.visual.key.clone(), grabs(hit.visual, tool)));
899 (hovered, cycle)
900 });
901 let (hovered, cycle) = under.unwrap_or_default();
902 let cycle = match waiting {
904 true => self.cycle,
905 false => cycle,
906 };
907 for visual in &mut visuals {
908 if is_handle(visual) {
909 continue;
910 }
911 if form.selection.contains(&visual.key) {
912 visual.style = Style::Selected;
913 } else if hovered.as_ref().is_some_and(|(key, _)| *key == visual.key) {
914 visual.style = Style::Hover;
915 }
916 }
917 visuals.extend(shown);
918 let highlights = (form.dialog.picked().cloned())
919 .chain(previewed.into_iter().flat_map(|p| p.drawn))
920 .collect();
921 Ok(Presentation {
922 dialog: form.dialog,
923 layout: form.layout,
924 visuals,
925 highlights,
926 pickable,
927 focus: form.focus.filter(|_| !waiting),
928 sheet: form.sheet.filter(|_| !waiting),
929 grab: self.grab.is_some()
931 || on_gizmo
932 || hovered.is_some_and(|(_, grab)| grab)
933 || (tool == InHand::Strokes && !waiting),
934 select_box: form.select_box && tool == InHand::Nothing && !waiting,
935 prompt: form.prompt,
936 gizmo,
937 hidden: form.hidden,
938 cycle,
939 })
940 }
941}
942
943#[cfg(test)]
944mod tests;