use crate::{InspectorRowsResponse, NodeCtx, NodeUi, response::DynResponse};
use egui::scroll_area::ScrollAreaOutput;
use egui_extras::{Column, TableBuilder};
use gantz_core::node::{self, MetaCtx, Node};
pub struct NodeInspector<'a, N> {
node: &'a mut N,
ctx: NodeCtx<'a>,
immutable: bool,
}
pub struct NodeInspectorResponse {
pub scroll_area_output: ScrollAreaOutput<()>,
pub node_response: Option<egui::Response>,
pub label_response: egui::Response,
pub changed: bool,
pub payloads: Vec<DynResponse>,
}
impl<'a, N> NodeInspector<'a, N>
where
N: Node + NodeUi,
{
pub fn new(node: &'a mut N, ctx: NodeCtx<'a>, immutable: bool) -> Self {
Self {
node,
ctx,
immutable,
}
}
pub fn show(self, ui: &mut egui::Ui) -> NodeInspectorResponse {
let Self {
node,
mut ctx,
immutable,
} = self;
let (scroll_area_output, label_response, rows) = table(node, &mut ctx, immutable, ui);
if immutable {
ui.disable();
}
let extra = node.inspector_ui(ctx, ui);
let mut payloads = rows.payloads;
payloads.extend(extra.payloads);
NodeInspectorResponse {
scroll_area_output,
node_response: extra.inner,
label_response,
changed: rows.changed || extra.changed,
payloads,
}
}
}
pub fn table_row_h(ui: &egui::Ui) -> f32 {
ui.text_style_height(&egui::TextStyle::Body) + ui.spacing().item_spacing.y
}
fn state_triangle(ui: &mut egui::Ui, expanded: bool) -> bool {
let arrow = if expanded { "\u{25BC}" } else { "\u{25B6}" }; ui.add(
egui::Label::new(arrow)
.sense(egui::Sense::click())
.selectable(false),
)
.on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text(if expanded { "collapse" } else { "expand" })
.clicked()
}
pub const DIAL_W: f32 = 44.0;
pub fn bound_col<T: egui::emath::Numeric>(
ui: &mut egui::Ui,
kind: &str,
bound: &mut Option<T>,
) -> bool {
let mut on = bound.is_some();
let mut v = bound.unwrap_or(T::from_f64(0.0));
let dialer = egui::DragValue::new(&mut v).speed(0.1);
let resp = ui
.add(crate::widget::CheckboxEnabled::new(&mut on, dialer).width(DIAL_W))
.on_hover_text(format!("clamp the {kind} value"));
if resp.changed() {
*bound = on.then_some(v);
return true;
}
false
}
pub fn radio_option<T: Copy + PartialEq>(
ui: &mut egui::Ui,
current: &mut T,
value: T,
text: &str,
hover: &str,
) -> bool {
let strong = ui.visuals().strong_text_color();
let mut selected = *current == value;
let resp = ui
.add(crate::widget::LabelToggle::new(text, &mut selected).selected_color(strong))
.on_hover_text(hover);
if resp.changed() && selected {
*current = value;
true
} else {
false
}
}
pub fn table(
node: &mut (impl Node + NodeUi),
ctx: &mut NodeCtx,
immutable: bool,
ui: &mut egui::Ui,
) -> (ScrollAreaOutput<()>, egui::Response, InspectorRowsResponse) {
let registry = ctx.env();
let get_node = |ca: &gantz_ca::ContentAddr| registry.node(ca);
let meta_ctx = MetaCtx::new(&get_node);
let name = node.name(registry);
let path = ctx.path().to_vec();
let n_inputs = node.n_inputs(meta_ctx);
let n_outputs = node.n_outputs(meta_ctx);
let push_eval = !node.push_eval(meta_ctx).is_empty();
let pull_eval = !node.pull_eval(meta_ctx).is_empty();
let is_stateful = node.stateful(meta_ctx);
let state_value = if is_stateful && node.show_state() {
Some(ctx.extract_value())
} else {
None
};
let label_response = ui.add(
egui::Label::new(egui::RichText::new(name).strong())
.selectable(false)
.sense(egui::Sense::click()),
);
ui.add_space(ui.spacing().item_spacing.y);
let row_h = table_row_h(ui);
let state_id = egui::Id::new(("inspector_state_expanded", &path));
let state_texts = match &state_value {
Some(Ok(Some(state))) => Some((format!("{state:?}"), format!("{state:#?}"))),
_ => None,
};
let state_multiline = state_texts
.as_ref()
.is_some_and(|(_, pretty)| pretty.contains('\n'));
let state_expanded =
state_multiline && ui.data(|d| d.get_temp::<bool>(state_id)).unwrap_or(false);
let state_row_h = match (&state_texts, state_expanded) {
(Some((_, pretty)), true) => {
let galley = ui.painter().layout_no_wrap(
pretty.clone(),
egui::TextStyle::Monospace.resolve(ui.style()),
ui.visuals().text_color(),
);
galley.size().y + row_h
}
_ => row_h,
};
let mut state_toggled = false;
let mut rows_resp = InspectorRowsResponse::default();
let scroll_area_output = TableBuilder::new(ui)
.vscroll(false)
.column(Column::auto().at_least(50.0).resizable(true))
.column(Column::remainder().at_least(120.0))
.body(|mut body| {
body.row(row_h, |mut row| {
row.col(|ui| {
ui.label("path");
});
row.col(|ui| {
ui.monospace(path_string(&path));
});
});
body.row(row_h, |mut row| {
row.col(|ui| {
ui.label("i/o");
});
row.col(|ui| {
ui.label(format!("{} inputs, {} outputs", n_inputs, n_outputs));
});
});
let eval = match (push_eval, pull_eval) {
(true, true) => Some("push, pull"),
(true, false) => Some("push"),
(false, true) => Some("pull"),
(false, false) => None,
};
if let Some(eval) = eval {
body.row(row_h, |mut row| {
row.col(|ui| {
ui.label("eval");
});
row.col(|ui| {
ui.label(eval);
});
});
}
if let Some(ref state_result) = state_value {
body.row(state_row_h, |mut row| {
row.col(|ui| {
ui.label("state");
});
row.col(|ui| match state_result {
Ok(Some(_)) => {
let (compact, pretty) = state_texts.as_ref().unwrap();
if state_expanded {
ui.vertical(|ui| {
if state_triangle(ui, true) {
state_toggled = true;
}
ui.add(
egui::Label::new(egui::RichText::new(pretty).monospace())
.wrap_mode(egui::TextWrapMode::Extend),
);
});
} else {
ui.horizontal(|ui| {
if state_multiline && state_triangle(ui, false) {
state_toggled = true;
}
ui.add(
egui::Label::new(egui::RichText::new(compact).monospace())
.truncate()
.show_tooltip_when_elided(false),
)
.on_hover_text(pretty);
});
}
}
Ok(None) => {
ui.weak("None");
}
Err(_) => {
ui.weak("Error");
}
});
});
}
if immutable {
body.ui_mut().disable();
}
rows_resp = node.inspector_rows(ctx, &mut body);
});
if state_toggled {
ui.data_mut(|d| d.insert_temp(state_id, !state_expanded));
}
(scroll_area_output, label_response, rows_resp)
}
pub fn path_string(path: &[node::Id]) -> String {
path.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(" ")
}
#[derive(Clone, Default)]
struct SocketDocEditState {
ty: String,
desc: String,
seeded: (String, String),
}
pub(crate) fn socket_doc_rows(
body: &mut egui_extras::TableBody,
id_salt: impl std::hash::Hash,
ty: &mut String,
description: &mut String,
) -> bool {
let id = egui::Id::new("socket-doc-editor").with(&id_salt);
let mut st: SocketDocEditState = body
.ui_mut()
.memory(|m| m.data.get_temp(id))
.unwrap_or_default();
let cur = (ty.clone(), description.clone());
if st.seeded != cur {
st.ty = cur.0.clone();
st.desc = cur.1.clone();
st.seeded = cur;
}
let row_h = table_row_h(body.ui_mut());
let mut ty_resp = None;
body.row(row_h, |mut row| {
row.col(|ui| {
ui.label("type");
});
row.col(|ui| {
ty_resp = Some(
ui.add(
egui::TextEdit::singleline(&mut st.ty)
.id(id.with("ty"))
.hint_text("type")
.desired_width(f32::INFINITY),
),
);
});
});
let wrap_w = (body.ui_mut().available_width() - 64.0).max(64.0);
let desc_h = doc_field_height(body.ui_mut(), &st.desc, wrap_w);
let mut desc_resp = None;
body.row(desc_h, |mut row| {
row.col(|ui| {
ui.label("desc.");
});
row.col(|ui| {
desc_resp = Some(
ui.add(
egui::TextEdit::multiline(&mut st.desc)
.id(id.with("desc"))
.hint_text("description")
.desired_rows(1)
.desired_width(wrap_w)
.return_key(egui::KeyboardShortcut::new(
egui::Modifiers::COMMAND,
egui::Key::Enter,
)),
),
);
});
});
let ty_resp = ty_resp.expect("value column always rendered");
let desc_resp = desc_resp.expect("value column always rendered");
let desc_enter = desc_resp.has_focus()
&& body
.ui_mut()
.input(|i| i.key_pressed(egui::Key::Enter) && !i.modifiers.any());
if desc_enter {
desc_resp.surrender_focus();
}
let commit = ty_resp.lost_focus() || desc_resp.lost_focus() || desc_enter;
let mut changed = false;
if commit {
let new = (st.ty.trim().to_string(), st.desc.trim().to_string());
changed = new.0 != *ty || new.1 != *description;
*ty = new.0.clone();
*description = new.1.clone();
st.ty = new.0.clone();
st.desc = new.1.clone();
st.seeded = new;
}
body.ui_mut().memory_mut(|m| m.data.insert_temp(id, st));
changed
}
fn doc_field_height(ui: &egui::Ui, text: &str, wrap_width: f32) -> f32 {
let font_id = egui::TextStyle::Body.resolve(ui.style());
let line_h = ui.text_style_height(&egui::TextStyle::Body);
let color = ui.visuals().text_color();
let galley = ui
.painter()
.layout(text.to_owned(), font_id, color, (wrap_width - 8.0).max(1.0));
galley.size().y.max(line_h) + 10.0
}