use std::rc::Rc;
use dspk_pipeline::Node;
use gpui_kit::component::checkbox::Checkbox;
use gpui_kit::component::input::{Input, InputEvent, InputState};
use gpui_kit::component::{v_flex, ActiveTheme as _, Sizable as _};
use gpui_kit::{div, App, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, SharedString, Styled as _, Subscription, Window};
use serde_json::{json, Value};
use super::widgets::{row, MenuSelect};
pub type Commit = Rc<dyn Fn(Node, &mut App) -> Result<(), String>>;
pub fn choices(field: &str) -> Option<&'static [&'static str]> {
Some(match field {
"band" => &["lowpass", "highpass", "bandpass", "bandstop"],
"design" => &["butterworth", "chebyshev1", "bessel"],
"mode" => &["forward_backward", "forward"],
"start" => &["rest", "steady_state"],
"edge" => &["zeros", "odd", "reflect", "nearest"],
_ => return None,
})
}
struct Field {
key: String,
index: Option<usize>,
label: String,
input: Entity<InputState>,
}
#[derive(Default)]
pub struct ParamEditor {
fields: Vec<Field>,
fields_for: String,
field_subs: Vec<Subscription>,
error: Option<String>,
target: Option<(Node, Commit)>,
}
impl ParamEditor {
pub fn new() -> Self {
Self::default()
}
pub fn set_target(&mut self, key: &str, node: &Node, commit: Commit, window: &mut Window, cx: &mut Context<Self>) {
let value = serde_json::to_value(node).unwrap_or(Value::Null);
self.target = Some((node.clone(), commit));
let fresh = format!("{key}/{value}");
if fresh == self.fields_for {
return;
}
if !self.fields_for.starts_with(&format!("{key}/")) {
self.error = None;
}
self.fields_for = fresh;
self.fields.clear();
self.field_subs.clear();
let Value::Object(map) = &value else { return };
for (k, v) in map {
match v {
Value::Number(n) => self.add_field(k, None, label_of(k), n.as_f64().unwrap_or(0.0), window, cx),
Value::Array(a) if a.len() <= 2 && a.iter().all(Value::is_number) => {
for (i, n) in a.iter().enumerate() {
let label = match (k.as_str(), a.len(), i) {
("cutoff", 2, 0) => "Low (Hz)".to_string(),
("cutoff", 2, 1) => "High (Hz)".to_string(),
("cutoff", _, _) => "Cutoff (Hz)".to_string(),
_ => format!("{} {}", label_of(k), i + 1),
};
self.add_field(k, Some(i), label, n.as_f64().unwrap_or(0.0), window, cx);
}
}
_ => {}
}
}
}
fn add_field(&mut self, key: &str, index: Option<usize>, label: String, v: f64, window: &mut Window, cx: &mut Context<Self>) {
let input = cx.new(|cx| InputState::new(window, cx).default_value(trim_number(v)));
let sub = cx.subscribe(&input, |this, _, e: &InputEvent, cx| {
if matches!(e, InputEvent::PressEnter { .. } | InputEvent::Blur) {
this.apply_fields(cx);
}
});
self.field_subs.push(sub);
self.fields.push(Field { key: key.to_string(), index, label, input });
}
fn apply_fields(&mut self, cx: &mut Context<Self>) {
let Some((node, _)) = &self.target else { return };
let mut value = serde_json::to_value(node).unwrap_or(Value::Null);
for f in &self.fields {
let text = f.input.read(cx).value().to_string();
let Ok(n) = text.trim().parse::<f64>() else {
self.error = Some(format!("{}: {text:?} is not a number", f.label));
cx.notify();
return;
};
let slot = match f.index {
Some(i) => value.get_mut(&f.key).and_then(|a| a.get_mut(i)),
None => value.get_mut(&f.key),
};
if let Some(slot) = slot {
*slot = if slot.is_u64() || slot.is_i64() { json!(n.round() as i64) } else { json!(n) };
}
}
self.set_value(value, cx);
}
fn set_value(&mut self, value: Value, cx: &mut Context<Self>) {
let Some((node, commit)) = self.target.clone() else { return };
match serde_json::from_value::<Node>(value) {
Ok(new) if new == node => {}
Ok(new) => self.error = commit(new, cx).err(),
Err(e) => self.error = Some(e.to_string()),
}
cx.notify();
}
fn set_choice(&mut self, key: &str, choice: &str, cx: &mut Context<Self>) {
let Some((node, _)) = &self.target else { return };
let mut value = serde_json::to_value(node).unwrap_or(Value::Null);
value[key] = json!(choice);
if key == "band" {
let cut: Vec<f64> = value["cutoff"].as_array().map(|a| a.iter().filter_map(Value::as_f64).collect()).unwrap_or_default();
let (lo, hi) = (cut.first().copied().unwrap_or(300.0), cut.last().copied().unwrap_or(6000.0));
value["cutoff"] = match choice {
"lowpass" => json!([hi]),
"highpass" => json!([lo]),
_ if cut.len() == 2 => json!(cut),
_ => json!([lo, if hi > lo { hi } else { lo * 10.0 }]),
};
}
self.set_value(value, cx);
}
fn set_bool(&mut self, key: &str, on: bool, cx: &mut Context<Self>) {
let Some((node, _)) = &self.target else { return };
let mut value = serde_json::to_value(node).unwrap_or(Value::Null);
value[key] = json!(on);
self.set_value(value, cx);
}
}
impl Render for ParamEditor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (muted, danger) = (cx.theme().muted_foreground, cx.theme().danger);
let Some((node, _)) = &self.target else { return v_flex() };
let value = serde_json::to_value(node).unwrap_or(Value::Null);
let mut out = v_flex().gap_2();
for f in &self.fields {
out = out.child(row(f.label.clone(), div().w_full().child(Input::new(&f.input).small()), cx));
}
if let Value::Object(map) = &value {
for (k, v) in map {
if let (Some(opts), Some(cur)) = (choices(k), v.as_str()) {
let this = cx.entity().downgrade();
let key = k.clone();
let names: Vec<SharedString> = opts.iter().map(|o| SharedString::from(o.replace('_', " "))).collect();
let sel = opts.iter().position(|o| *o == cur);
out = out.child(row(
label_of(k),
MenuSelect::new(SharedString::from(format!("param-{k}")), names, sel, move |i, _, cx| {
let _ = this.update(cx, |p, cx| p.set_choice(&key, opts[i], cx));
}),
cx,
));
} else if let Some(b) = v.as_bool() {
let this = cx.entity().downgrade();
let key = k.clone();
out = out.child(row(
label_of(k),
Checkbox::new(SharedString::from(format!("param-{k}"))).checked(b).on_click(move |on: &bool, _, cx| {
let on = *on;
let _ = this.update(cx, |p, cx| p.set_bool(&key, on, cx));
}),
cx,
));
} else if v.as_array().is_some_and(|a| a.len() > 2) {
out = out.child(row(label_of(k), div().text_xs().text_color(muted).child(format!("{} values (edit in the session file)", v.as_array().map_or(0, Vec::len))), cx));
}
}
}
if self.fields.is_empty() && !value.as_object().is_some_and(|m| m.keys().any(|k| k != "op")) {
out = out.child(div().text_xs().text_color(muted).child("No parameters."));
}
out.children(self.error.clone().map(|e| div().text_xs().text_color(danger).child(e)))
}
}
pub fn label_of(key: &str) -> String {
let mut s = key.replace('_', " ");
if let Some(c) = s.get_mut(0..1) {
c.make_ascii_uppercase();
}
s
}
pub fn trim_number(v: f64) -> String {
if v.fract() == 0.0 && v.abs() < 1e15 {
format!("{}", v as i64)
} else {
let s = format!("{v:.6}");
s.trim_end_matches('0').trim_end_matches('.').to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_labels_and_numbers() {
assert_eq!(label_of("sigma_samples"), "Sigma samples");
assert_eq!(trim_number(300.0), "300");
assert_eq!(trim_number(0.25), "0.25");
}
}