1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
//! EditorHook: the add / edit form's session state, beside its lifecycle in
//! `editing.rs`. The form's layout half (`panels/form_panel.rs`) stays
//! stateless; it draws from a view built over this.
use std::collections::HashSet;
use super::form_extras::FormExtras;
use crate::editor::entry_list::EntryId;
use crate::editor::panels::form::FormField;
use crate::editor::panels::form_panel::FormFocus;
use crate::editor::panels::registry::PanelKey;
// The type of the open form doubles as the open signal: `selected_type` is
// `None` while the panel is closed.
#[derive(Debug)]
pub(in crate::editor::hook) struct FormState {
pub(in crate::editor::hook) selected_type: Option<String>,
// What confirming the open form commits to: a new asset, an existing line,
// or the promotion of a generated asset.
pub(in crate::editor::hook) target: FormTarget,
// The editable arg fields of the open form (derived from the type's default
// args). Empty while the form is closed.
pub(in crate::editor::hook) fields: Vec<FormField>,
// First visible field of the form's scroll window (its physical control pool is
// fixed size, so a form wider than `form::FIELD_POOL` scrolls). Reset on open /
// structural change.
pub(in crate::editor::hook) scroll: usize,
// Which form input has keyboard focus.
pub(in crate::editor::hook) focus: FormFocus,
// A validation message from the last rejected Add, shown under the form.
pub(in crate::editor::hook) error: Option<String>,
// The form arg field whose value dropdown is open (a large enum / ref set),
// and its scroll offset. `None` outside an open dropdown.
pub(in crate::editor::hook) field_dropdown: Option<usize>,
pub(in crate::editor::hook) field_dropdown_scroll: usize,
// The open form's working args tree: the fields are derived from it, and it is
// mutated by add / remove (structure) and, on capture, by the controls. Empty
// outside AddForm.
pub(in crate::editor::hook) args: serde_json::Map<String, serde_json::Value>,
// The template behind the open form when it edits a template-derived asset:
// confirming writes the minimal patch against this baseline, and the rows
// show per-field override state. `None` for plain authored / new assets.
pub(in crate::editor::hook) template: Option<FormTemplate>,
// The field whose override menu (Revert / Apply-to-template) is open, and
// whether the header's entity-level menu is open.
pub(in crate::editor::hook) override_menu: Option<usize>,
pub(in crate::editor::hook) entity_menu_open: bool,
// The paths of the form's non-color vector fields currently disclosed into
// per-element leaves. Cleared when the form opens / closes.
pub(in crate::editor::hook) vec_expanded: HashSet<String>,
// The unapplied-edit marker behind the heading's "*".
pub(in crate::editor::hook) touched: bool,
// The type's rows and commit steps beyond its schema fields, if it has any.
pub(in crate::editor::hook) extras: Option<Box<dyn FormExtras>>,
// The panel the form was opened from; it shows while that panel does.
pub(in crate::editor::hook) host: PanelKey,
}
impl Default for FormState {
fn default() -> Self {
Self {
selected_type: None,
target: FormTarget::New,
fields: Vec::new(),
scroll: 0,
focus: FormFocus::Name,
error: None,
field_dropdown: None,
field_dropdown_scroll: 0,
args: serde_json::Map::new(),
template: None,
override_menu: None,
entity_menu_open: false,
vec_expanded: HashSet::new(),
touched: false,
extras: None,
host: PanelKey::Assets,
}
}
}
impl FormState {
// Close the form panel, discarding every piece of its transient state.
pub(in crate::editor::hook) fn close(&mut self) {
self.selected_type = None;
self.touched = false;
self.target = FormTarget::New;
self.fields.clear();
self.args = serde_json::Map::new();
self.template = None;
self.override_menu = None;
self.entity_menu_open = false;
self.vec_expanded.clear();
self.extras = None;
self.host = PanelKey::Assets;
self.scroll = 0;
self.focus = FormFocus::Name;
self.error = None;
self.field_dropdown = None;
self.field_dropdown_scroll = 0;
}
}
// The template a form-edited asset derives from.
#[derive(Debug, Clone)]
pub(in crate::editor::hook) struct FormTemplate {
pub(in crate::editor::hook) name: String,
// Effective template args: the type's defaults with the generated args
// merged over them, the baseline a field is inherited from.
pub(in crate::editor::hook) baseline: serde_json::Map<String, serde_json::Value>,
// The authored asset or injection pass that produced the asset.
pub(in crate::editor::hook) generated_by: String,
}
// What confirming the open add / edit form commits to.
#[derive(Debug, Clone, PartialEq, Default)]
pub(in crate::editor::hook) enum FormTarget {
// A new asset: confirming appends it under a unique name.
#[default]
New,
// The authored entry with this session key: confirming updates that line
// in place. A key rather than a position, so a line added or removed
// elsewhere in the list cannot retarget the open form onto another entry.
Entry(EntryId),
// An asset the build generates, which has no world.jsonl line of its own.
// The form is seeded from the entry the expansion produced, and confirming
// appends that line -- which then overrides the expansion, since the cook
// drops a generated asset in favor of an authored one of the same name and
// type. Renaming it in the form instead leaves the generated asset in place
// and adds a separate one, which is the honest reading of a rename.
Promote(serde_json::Value),
}
impl FormTarget {
// The authored entry the form updates in place, if any.
pub(in crate::editor::hook) fn entry(&self) -> Option<EntryId> {
match self {
FormTarget::Entry(key) => Some(*key),
_ => None,
}
}
// Whether the form is editing an asset that already exists (in the world or
// in the build), rather than adding a brand-new one.
pub(in crate::editor::hook) fn is_edit(&self) -> bool {
!matches!(self, FormTarget::New)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::editor::entry_list::EntryList;
use crate::editor::panels::form::FieldKind;
#[test]
fn close_discards_every_field_of_the_open_form() {
let mut args = serde_json::Map::new();
args.insert("radius".into(), serde_json::json!(2.0));
let mut s = FormState {
selected_type: Some("Sphere".into()),
target: FormTarget::Entry(
EntryList::new(vec![serde_json::json!({})])
.key_at(0)
.unwrap(),
),
fields: vec![FormField {
key: "radius".into(),
kind: FieldKind::Float,
initial: "2.0".into(),
boolval: false,
variants: Vec::new(),
variant_idx: 0,
choices: Vec::new(),
}],
scroll: 3,
focus: FormFocus::Field(1),
error: Some("bad".into()),
field_dropdown: Some(2),
field_dropdown_scroll: 5,
args,
template: Some(FormTemplate {
name: "t".into(),
baseline: serde_json::Map::new(),
generated_by: "g".into(),
}),
override_menu: Some(1),
entity_menu_open: true,
vec_expanded: HashSet::from(["pos".to_string()]),
touched: true,
extras: None,
host: PanelKey::Shaders,
};
s.close();
assert_eq!(s.selected_type, None);
assert_eq!(s.target, FormTarget::New);
assert!(s.fields.is_empty() && s.args.is_empty() && s.vec_expanded.is_empty());
assert!(s.template.is_none());
assert_eq!((s.override_menu, s.entity_menu_open), (None, false));
assert_eq!((s.scroll, s.focus), (0, FormFocus::Name));
assert_eq!(s.error, None);
assert_eq!((s.field_dropdown, s.field_dropdown_scroll), (None, 0));
assert!(!s.touched);
assert_eq!(s.host, PanelKey::Assets);
}
}