1use std::rc::Rc;
8
9use gpui_kit::component::checkbox::Checkbox;
10use gpui_kit::component::input::{Input, InputEvent, InputState};
11use gpui_kit::component::{Sizable as _, v_flex};
12use gpui_kit::{App, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, SharedString, Styled as _, Subscription, Window, div};
13use serde::Serialize;
14use serde::de::DeserializeOwned;
15use serde_json::{Value, json};
16
17use crate::theme::{self, color};
18use crate::widgets::{MenuSelect, row};
19
20pub trait Params: Serialize + DeserializeOwned + Clone + PartialEq + 'static {
25 fn choices(_field: &str) -> Option<&'static [&'static str]> {
28 None
29 }
30
31 fn element_label(field: &str, _len: usize, i: usize) -> String {
33 format!("{} {}", label_of(field), i + 1)
34 }
35
36 fn on_choice(_value: &mut Value, _field: &str, _choice: &str) {}
38}
39
40pub type Commit<T> = Rc<dyn Fn(T, &mut App) -> Result<(), String>>;
42
43const MAX_LIST: usize = 2;
45
46struct Field {
48 key: String,
49 index: Option<usize>,
50 label: String,
51 input: Entity<InputState>,
52}
53
54pub struct ParamEditor<T: Params> {
56 fields: Vec<Field>,
57 fields_for: String,
59 field_subs: Vec<Subscription>,
60 error: Option<String>,
61 target: Option<(T, Commit<T>)>,
62}
63
64impl<T: Params> Default for ParamEditor<T> {
65 fn default() -> Self {
66 Self { fields: Vec::new(), fields_for: String::new(), field_subs: Vec::new(), error: None, target: None }
67 }
68}
69
70impl<T: Params> ParamEditor<T> {
71 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn set_target(&mut self, key: &str, op: &T, commit: Commit<T>, window: &mut Window, cx: &mut Context<Self>) {
79 let value = serde_json::to_value(op).unwrap_or(Value::Null);
80 self.target = Some((op.clone(), commit));
81 let fresh = format!("{key}/{value}");
82 if fresh == self.fields_for {
83 return;
84 }
85 if !self.fields_for.starts_with(&format!("{key}/")) {
86 self.error = None;
87 }
88 self.fields_for = fresh;
89 self.fields.clear();
90 self.field_subs.clear();
91 let Value::Object(map) = &value else { return };
92 for (k, v) in map {
93 match v {
94 Value::Number(n) => self.add_field(k, None, label_of(k), n.as_f64().unwrap_or(0.0), window, cx),
95 Value::Array(a) if a.len() <= MAX_LIST && !a.is_empty() && a.iter().all(Value::is_number) => {
96 for (i, n) in a.iter().enumerate() {
97 self.add_field(k, Some(i), T::element_label(k, a.len(), i), n.as_f64().unwrap_or(0.0), window, cx);
98 }
99 }
100 _ => {}
101 }
102 }
103 }
104
105 pub fn target(&self) -> Option<&T> {
107 self.target.as_ref().map(|(op, _)| op)
108 }
109
110 pub fn error(&self) -> Option<&str> {
112 self.error.as_deref()
113 }
114
115 fn add_field(&mut self, key: &str, index: Option<usize>, label: String, v: f64, window: &mut Window, cx: &mut Context<Self>) {
116 let input = cx.new(|cx| InputState::new(window, cx).default_value(trim_number(v)));
117 let sub = cx.subscribe(&input, |this, _, e: &InputEvent, cx| {
118 if matches!(e, InputEvent::PressEnter { .. } | InputEvent::Blur) {
119 this.apply_fields(cx);
120 }
121 });
122 self.field_subs.push(sub);
123 self.fields.push(Field { key: key.to_string(), index, label, input });
124 }
125
126 fn apply_fields(&mut self, cx: &mut Context<Self>) {
129 let Some((op, _)) = &self.target else { return };
130 let mut value = serde_json::to_value(op).unwrap_or(Value::Null);
131 for f in &self.fields {
132 let text = f.input.read(cx).value().to_string();
133 let Ok(n) = text.trim().parse::<f64>() else {
134 self.error = Some(format!("{}: {text:?} is not a number", f.label));
135 cx.notify();
136 return;
137 };
138 let slot = match f.index {
139 Some(i) => value.get_mut(&f.key).and_then(|a| a.get_mut(i)),
140 None => value.get_mut(&f.key),
141 };
142 if let Some(slot) = slot {
143 *slot = number_like(slot, n);
144 }
145 }
146 self.set_value(value, cx);
147 }
148
149 fn set_value(&mut self, value: Value, cx: &mut Context<Self>) {
150 let Some((op, commit)) = self.target.clone() else { return };
151 match serde_json::from_value::<T>(value) {
152 Ok(new) if new == op => self.error = None,
153 Ok(new) => self.error = commit(new, cx).err(),
154 Err(e) => self.error = Some(e.to_string()),
155 }
156 cx.notify();
157 }
158
159 fn set_choice(&mut self, key: &str, choice: &str, cx: &mut Context<Self>) {
160 let Some((op, _)) = &self.target else { return };
161 let mut value = serde_json::to_value(op).unwrap_or(Value::Null);
162 value[key] = json!(choice);
163 T::on_choice(&mut value, key, choice);
164 self.set_value(value, cx);
165 }
166
167 fn set_bool(&mut self, key: &str, on: bool, cx: &mut Context<Self>) {
168 let Some((op, _)) = &self.target else { return };
169 let mut value = serde_json::to_value(op).unwrap_or(Value::Null);
170 value[key] = json!(on);
171 self.set_value(value, cx);
172 }
173}
174
175impl<T: Params> Render for ParamEditor<T> {
176 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
177 let mut out = v_flex().w_full().gap(gpui_kit::px(theme::SM));
178 let Some((op, _)) = &self.target else { return out };
179 let value = serde_json::to_value(op).unwrap_or(Value::Null);
180 for f in &self.fields {
181 out = out.child(row(f.label.clone(), div().w_full().child(Input::new(&f.input).small())));
182 }
183 let mut shown = !self.fields.is_empty();
184 if let Value::Object(map) = &value {
185 for (k, v) in map {
186 if let (Some(opts), Some(cur)) = (T::choices(k), v.as_str()) {
187 let this = cx.entity().downgrade();
188 let key = k.clone();
189 let names: Vec<SharedString> = opts.iter().map(|o| SharedString::from(o.replace('_', " "))).collect();
190 let sel = opts.iter().position(|o| *o == cur);
191 out = out.child(row(
192 label_of(k),
193 MenuSelect::new(SharedString::from(format!("param-{k}")), names, sel, move |i, _, cx| {
194 let _ = this.update(cx, |p, cx| p.set_choice(&key, opts[i], cx));
195 }),
196 ));
197 shown = true;
198 } else if let Some(b) = v.as_bool() {
199 let this = cx.entity().downgrade();
200 let key = k.clone();
201 out = out.child(row(
202 label_of(k),
203 Checkbox::new(SharedString::from(format!("param-{k}"))).checked(b).on_click(move |on: &bool, _, cx| {
204 let on = *on;
205 let _ = this.update(cx, |p, cx| p.set_bool(&key, on, cx));
206 }),
207 ));
208 shown = true;
209 } else if let Some(a) = v.as_array().filter(|a| a.len() > MAX_LIST) {
210 out = out.child(row(label_of(k), div().text_xs().text_color(color::text_muted()).child(format!("{} values (edit in the file)", a.len()))));
211 shown = true;
212 }
213 }
214 }
215 if !shown {
216 out = out.child(div().text_xs().text_color(color::text_muted()).child("No parameters."));
217 }
218 out.children(self.error.clone().map(|e| div().text_xs().text_color(color::danger()).child(e)))
219 }
220}
221
222fn number_like(slot: &Value, n: f64) -> Value {
225 if slot.is_u64() || slot.is_i64() { json!(n.round() as i64) } else { json!(n) }
226}
227
228pub fn label_of(key: &str) -> String {
230 let mut s = key.replace('_', " ");
231 if let Some(c) = s.get_mut(0..1) {
232 c.make_ascii_uppercase();
233 }
234 s
235}
236
237pub fn trim_number(v: f64) -> String {
239 if v.fract() == 0.0 && v.abs() < 1e15 {
240 format!("{}", v as i64)
241 } else {
242 let s = format!("{v:.6}");
243 s.trim_end_matches('0').trim_end_matches('.').to_string()
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn labels_and_numbers() {
253 assert_eq!(label_of("clip_limit"), "Clip limit");
254 assert_eq!(label_of("sigma_samples"), "Sigma samples");
255 assert_eq!(trim_number(300.0), "300");
256 assert_eq!(trim_number(0.25), "0.25");
257 assert_eq!(trim_number(-1.5), "-1.5");
258 }
259
260 #[test]
261 fn integers_stay_integers() {
262 assert_eq!(number_like(&json!(3), 4.6), json!(5));
263 assert_eq!(number_like(&json!(-3), -4.4), json!(-4));
264 assert_eq!(number_like(&json!(0.5), 2.0), json!(2.0));
265 let v = number_like(&json!(3), -2.0);
267 assert!(serde_json::from_value::<u32>(v).is_err());
268 }
269}