use crate::{
Env, InspectorRowsResponse, NodeCtx, NodeUi, NodeUiResponse, NodeViewResponse, SocketDoc,
SocketKind,
};
pub struct ExprEdit<'a> {
expr: &'a mut gantz_core::node::Expr,
pub id: egui::Id,
fill: bool,
}
#[derive(Clone, Default)]
struct ExprEditState {
expr_hash: u64,
code: String,
}
impl<'a> egui::Widget for ExprEdit<'a> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
let Self { expr, id, fill } = self;
let code_id = id.with("code");
let mut state: ExprEditState = ui
.memory_mut(|m| m.data.remove_temp(code_id))
.unwrap_or_default();
let expr_hash = expr_hash(expr);
if expr_hash != state.expr_hash {
state.expr_hash = expr_hash;
state.code = expr.src().to_string();
}
let language = "scm";
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), ui.style());
let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
let mut layout_job = egui_extras::syntax_highlighting::highlight(
ui.ctx(),
ui.style(),
&theme,
buf.as_str(),
language,
);
layout_job.wrap.max_width = wrap_width;
ui.fonts_mut(|fonts| fonts.layout_job(layout_job))
};
let font_id = egui::FontSelection::from(egui::TextStyle::Monospace).resolve(ui.style());
let margin = egui::Margin::symmetric(4, 2);
let (desired_width, desired_rows) = if fill {
let row_h = ui.text_style_height(&egui::TextStyle::Monospace);
let rows = ((ui.available_height() / row_h).floor() as usize).max(1);
(ui.available_width(), rows)
} else {
(
super::code_edit_desired_width(ui, &theme, &state.code, language, margin),
1,
)
};
let response = ui.add(
egui::TextEdit::multiline(&mut state.code)
.id(id)
.code_editor()
.font(font_id)
.margin(margin)
.desired_rows(desired_rows)
.desired_width(desired_width)
.hint_text("(+ $l $r)")
.layouter(&mut layouter),
);
if response.changed() {
if let Ok(new_expr) = gantz_core::node::expr(&state.code) {
*expr = new_expr.with_outputs(expr.outputs());
}
}
ui.memory_mut(|m| m.data.insert_temp(code_id, state));
response
}
}
impl NodeUi for gantz_core::node::Expr {
fn name(&self, _: &crate::Env<'_>) -> std::borrow::Cow<'_, str> {
"expr".into()
}
fn description(&self) -> Option<&'static str> {
Some("Evaluate a Steel expression")
}
fn ui(&mut self, ctx: NodeCtx, uictx: egui_graph::NodeCtx) -> NodeUiResponse {
let before = expr_hash(self);
let framed = uictx.framed(|ui, _sockets| {
let id = egui::Id::new("ExprEdit").with(ctx.path());
ui.add(ExprEdit::new(self, id))
});
let mut resp = NodeUiResponse::new(framed);
resp.set_changed(expr_hash(self) != before);
resp
}
fn view_ui(&mut self, _ctx: NodeCtx, ui: &mut egui::Ui) -> NodeViewResponse {
let before = expr_hash(self);
let id = ui.id().with("expr-view");
let res = ui.add(ExprEdit::new(self, id).fill(true));
let mut resp = NodeViewResponse::default();
resp.inner = Some(res);
resp.set_changed(expr_hash(self) != before);
resp
}
fn inspector_rows(
&mut self,
_ctx: &mut NodeCtx,
body: &mut egui_extras::TableBody,
) -> InspectorRowsResponse {
let mut resp = InspectorRowsResponse::default();
let row_h = crate::widget::node_inspector::table_row_h(body.ui_mut());
body.row(row_h, |mut row| {
row.col(|ui| {
ui.label("outputs");
});
row.col(|ui| {
let mut n = self.outputs() as i32;
if ui
.add(egui::DragValue::new(&mut n).range(1..=16).speed(0.1))
.changed()
{
self.set_outputs(n.clamp(1, 16) as u8);
resp.mark_changed();
}
});
});
resp
}
fn socket_doc(&self, _: &Env<'_>, kind: SocketKind, ix: usize) -> Option<SocketDoc> {
match kind {
SocketKind::Input => match self.vars().get(ix) {
Some(var) => {
let desc = if var.starts_with("$?") {
"optional input; bound as (Some value) or (None)"
} else {
"substituted into the expression"
};
Some(SocketDoc::ty(var.clone()).with_description(desc))
}
None => Some(
SocketDoc::ty("trigger")
.with_description("ignored; forces the expression to evaluate"),
),
},
SocketKind::Output if self.outputs() <= 1 => {
Some(SocketDoc::ty("any").with_description("expression result"))
}
SocketKind::Output => {
Some(SocketDoc::ty("any").with_description(format!("result element {ix}")))
}
}
}
}
impl<'a> ExprEdit<'a> {
pub fn new(expr: &'a mut gantz_core::node::Expr, id: egui::Id) -> Self {
Self {
expr,
id,
fill: false,
}
}
pub fn fill(mut self, fill: bool) -> Self {
self.fill = fill;
self
}
}
fn expr_hash(expr: &gantz_core::node::Expr) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::default();
expr.hash(&mut hasher);
hasher.finish()
}