brep_app/panels/expressions.rs
1//! Expressions / parameters panel — the **variable sheet** that drives feature
2//! params. The engine-owned history document carries an `expressions` source
3//! string (a small `name = expr;` DSL) plus a `configurator` object; the pipeline
4//! evaluates it when running features, so a numeric feature field may be the
5//! expression string `"boxW"` — resolved against the variables defined here.
6//!
7//! Following the panel pattern, this owns NO model state: it edits + reads the
8//! engine (`state.expressions_json()` / `state.set_expressions()` /
9//! `state.expression_variables_json()` / `state.configurator_json()`), which write
10//! into the single-source-of-truth `EngineState.history` and re-run the rolled-to
11//! prefix so var-referencing params update live. The panel holds only a transient
12//! editor buffer (mirrored from the engine when unfocused, committed on
13//! Apply / focus-loss) and the per-frame widget hit-rects the headed verifier reads.
14
15use crate::automation::hit_keys::HitKeyDoc;
16use brep_render::engine_state::EngineState;
17use eframe::egui;
18use serde_json::Value;
19use std::collections::HashMap;
20
21/// The expressions panel's transient UI state (the model lives in the engine).
22pub struct ExpressionsPanel {
23 /// The multiline editor buffer. Mirrored from the engine's `expressions`
24 /// whenever the editor is UNFOCUSED (so an Open / undo / redo that changed the
25 /// document reflects here), and committed BACK to the engine on Apply or when
26 /// the editor loses focus.
27 buf: String,
28 /// Per-frame widget hit-rects, published to JS for the headed verifier (wasm).
29 hits: HashMap<String, egui::Rect>,
30}
31
32impl Default for ExpressionsPanel {
33 fn default() -> Self {
34 Self::new()
35 }
36}
37
38impl ExpressionsPanel {
39 pub fn new() -> Self {
40 Self {
41 buf: String::new(),
42 hits: HashMap::new(),
43 }
44 }
45
46 /// Draw the panel under its own collapsing header (matches the sibling panels).
47 pub fn show(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
48 self.hits.clear();
49
50 egui::CollapsingHeader::new("Expressions / parameters")
51 .id_salt("expressions-panel")
52 .default_open(true)
53 .show(ui, |ui| self.body(ui, state));
54 }
55
56 fn body(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
57 ui.label("Variable sheet — feature params may reference these (e.g. a size field set to `boxW`).");
58
59 // --- the editor, bound to the engine-owned `expressions` source --------
60 // Syntax highlighting via egui's BUILT-IN facility
61 // (`egui_extras::syntax_highlighting`). A `layouter` re-lays every
62 // frame: `highlight(...)` returns a colored `LayoutJob`, and the theme
63 // is read from egui memory so it tracks the light/dark visuals. Results
64 // are memoized inside `highlight`, so this is cheap per-frame.
65 //
66 // The language is `"c"`, not `"js"`. We build egui_extras WITHOUT its
67 // `syntect` feature (16 crates natively, 20 on wasm, for this one
68 // call), so `highlight` runs the crate's own fallback lexer, whose
69 // `Language::new` knows only c/cpp, py, rust and toml — `"js"` returns
70 // None there and falls through to unstyled monospace. The sheet is
71 // `name = expr;` lines, so the C lane colours exactly what matters:
72 // `//` comments, strings, numbers, identifiers and punctuation.
73 let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
74 let theme =
75 egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), ui.style());
76 let mut job = egui_extras::syntax_highlighting::highlight(
77 ui.ctx(),
78 ui.style(),
79 &theme,
80 buf.as_str(),
81 "c",
82 );
83 job.wrap.max_width = wrap_width;
84 ui.fonts_mut(|f| f.layout_job(job))
85 };
86 let editor = ui.add(
87 egui::TextEdit::multiline(&mut self.buf)
88 .id_salt("expressions-editor")
89 .code_editor()
90 .desired_rows(5)
91 .desired_width(f32::INFINITY)
92 .hint_text("boxW = 30;\nboxH = boxW / 2;")
93 .layouter(&mut layouter),
94 );
95 self.hits.insert("expr:editor".into(), editor.rect);
96
97 // --- commit: Apply button OR focus-loss; re-run only when text changed --
98 let apply = ui.button("Apply (re-run history)");
99 self.hits.insert("expr:apply".into(), apply.rect);
100
101 let commit = editor.lost_focus() || apply.clicked();
102 if commit {
103 // Re-run only on a real change (each commit re-runs the rolled-to
104 // prefix); a no-op commit must not thrash the kernel.
105 if self.buf != state.expressions_json() {
106 let _ = state.set_expressions(&self.buf);
107 }
108 } else if !editor.has_focus() {
109 // Not editing → mirror the engine (an Open / New / undo / redo may have
110 // replaced the document out from under the buffer).
111 self.buf = state.expressions_json();
112 }
113
114 // --- parsed variable list (name = defining expression) -----------------
115 ui.separator();
116 ui.label(egui::RichText::new("Variables").strong());
117 let vars: Vec<Value> =
118 serde_json::from_str(&state.expression_variables_json()).unwrap_or_default();
119 if vars.is_empty() {
120 ui.weak("(no variables defined)");
121 } else {
122 egui::Grid::new("expr-vars")
123 .num_columns(2)
124 .striped(true)
125 .show(ui, |ui| {
126 for v in &vars {
127 let name = v.get("name").and_then(Value::as_str).unwrap_or("");
128 let expr = v.get("expr").and_then(Value::as_str).unwrap_or("");
129 ui.monospace(name);
130 ui.monospace(format!("= {expr}"));
131 ui.end_row();
132 }
133 });
134 }
135
136 // --- configurator (typed named inputs) — read/display; editing deferred -
137 ui.separator();
138 ui.collapsing("Configurator (read-only)", |ui| {
139 let cfg = state.configurator_json();
140 let pretty = serde_json::from_str::<Value>(&cfg)
141 .and_then(|v| serde_json::to_string_pretty(&v))
142 .unwrap_or(cfg);
143 ui.monospace(pretty);
144 ui.weak("Typed named inputs — a deeper configurator editor is deferred.");
145 });
146 }
147
148 /// The published widget hit-rects (egui points) for the headed verifier.
149 pub fn hits_json(&self) -> String {
150 crate::automation::hit_rects::hits_json(&self.hits)
151 }
152}
153
154// BREP private tests: 218eadaab7b230b7
155
156/// The hit keys this panel publishes (see `automation::hit_keys`).
157pub static HIT_KEYS: &[HitKeyDoc] = &[
158 HitKeyDoc { panel: "expr", prefix: "expr:editor", meaning: "the expressions script editor", command: None },
159 HitKeyDoc { panel: "expr", prefix: "expr:apply", meaning: "apply the script and rerun", command: None },
160];