use brep_render::engine_state::EngineState;
use eframe::egui;
use serde_json::Value;
#[cfg(target_arch = "wasm32")]
use std::collections::HashMap;
pub struct ExpressionsPanel {
buf: String,
#[cfg(target_arch = "wasm32")]
hits: HashMap<String, egui::Rect>,
}
impl Default for ExpressionsPanel {
fn default() -> Self {
Self::new()
}
}
impl ExpressionsPanel {
pub fn new() -> Self {
Self {
buf: String::new(),
#[cfg(target_arch = "wasm32")]
hits: HashMap::new(),
}
}
pub fn show(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
#[cfg(target_arch = "wasm32")]
self.hits.clear();
egui::CollapsingHeader::new("Expressions / parameters")
.id_salt("expressions-panel")
.default_open(true)
.show(ui, |ui| self.body(ui, state));
}
fn body(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
ui.label("Variable sheet — feature params may reference these (e.g. a size field set to `boxW`).");
let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
let theme =
egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), ui.style());
let mut job = egui_extras::syntax_highlighting::highlight(
ui.ctx(),
ui.style(),
&theme,
buf.as_str(),
"js",
);
job.wrap.max_width = wrap_width;
ui.fonts_mut(|f| f.layout_job(job))
};
let editor = ui.add(
egui::TextEdit::multiline(&mut self.buf)
.id_salt("expressions-editor")
.code_editor()
.desired_rows(5)
.desired_width(f32::INFINITY)
.hint_text("boxW = 30;\nboxH = boxW / 2;")
.layouter(&mut layouter),
);
#[cfg(target_arch = "wasm32")]
self.hits.insert("expr:editor".into(), editor.rect);
let apply = ui.button("Apply (re-run history)");
#[cfg(target_arch = "wasm32")]
self.hits.insert("expr:apply".into(), apply.rect);
let commit = editor.lost_focus() || apply.clicked();
if commit {
if self.buf != state.expressions_json() {
let _ = state.set_expressions(&self.buf);
}
} else if !editor.has_focus() {
self.buf = state.expressions_json();
}
ui.separator();
ui.label(egui::RichText::new("Variables").strong());
let vars: Vec<Value> =
serde_json::from_str(&state.expression_variables_json()).unwrap_or_default();
if vars.is_empty() {
ui.weak("(no variables defined)");
} else {
egui::Grid::new("expr-vars")
.num_columns(2)
.striped(true)
.show(ui, |ui| {
for v in &vars {
let name = v.get("name").and_then(Value::as_str).unwrap_or("");
let expr = v.get("expr").and_then(Value::as_str).unwrap_or("");
ui.monospace(name);
ui.monospace(format!("= {expr}"));
ui.end_row();
}
});
}
ui.separator();
ui.collapsing("Configurator (read-only)", |ui| {
let cfg = state.configurator_json();
let pretty = serde_json::from_str::<Value>(&cfg)
.and_then(|v| serde_json::to_string_pretty(&v))
.unwrap_or(cfg);
ui.monospace(pretty);
ui.weak("Typed named inputs — a deeper configurator editor is deferred.");
});
}
#[cfg(target_arch = "wasm32")]
pub fn hits_json(&self) -> String {
let map: serde_json::Map<String, Value> = self
.hits
.iter()
.map(|(k, r)| {
(
k.clone(),
serde_json::json!([r.min.x, r.min.y, r.width(), r.height()]),
)
})
.collect();
Value::Object(map).to_string()
}
}
#[cfg(test)]
mod tests {
use eframe::egui;
#[test]
fn js_highlighter_colors_keyword_number_comment_distinctly() {
let ctx = egui::Context::default();
let style = egui::Style::default();
let theme = egui_extras::syntax_highlighting::CodeTheme::dark(12.0);
let src = "var boxW = 30; // size";
let job = egui_extras::syntax_highlighting::highlight(&ctx, &style, &theme, src, "js");
let color_at = |byte: usize| -> egui::Color32 {
job.sections
.iter()
.find(|s| byte >= s.byte_range.start.0 && byte < s.byte_range.end.0)
.map(|s| s.format.color)
.expect("some section covers this byte")
};
let kw = color_at(src.find("var").unwrap()); let num = color_at(src.find("30").unwrap()); let com = color_at(src.find("//").unwrap());
assert_ne!(kw, num, "keyword and number share a color — not highlighted");
assert_ne!(kw, com, "keyword and comment share a color — not highlighted");
assert_ne!(num, com, "number and comment share a color — not highlighted");
let distinct: std::collections::HashSet<[u8; 4]> = job
.sections
.iter()
.map(|s| s.format.color.to_array())
.collect();
assert!(
distinct.len() >= 3,
"expected >=3 distinct token colors, got {}",
distinct.len()
);
}
}