use crate::form;
use brep_render::style::{FieldKind, FormField};
use eframe::egui;
use serde_json::Value;
use std::collections::HashMap;
const COLLAPSED_BY_DEFAULT: &[&str] = &["Transform", "Outputs"];
pub struct FormViewSpec<'a> {
pub title: &'a str,
pub subtitle: Option<&'a str>,
pub fields: &'a [FormField],
pub banner: Option<(&'a str, egui::Color32)>,
pub trailing: Option<&'a [(&'a str, Vec<String>)]>,
pub extra: Option<(&'a str, &'a dyn Fn(&mut egui::Ui))>,
pub exit_label: &'a str,
pub rollback: bool,
pub hits_prefix: &'a str,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RefActivate {
pub path: Vec<String>,
pub label: String,
pub filter: Vec<String>,
pub multiple: bool,
pub seed: Vec<String>,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct FormViewOut {
pub changed: bool,
pub button_clicked: Option<String>,
pub ref_activate: Option<RefActivate>,
pub exit_clicked: bool,
pub roll_to_tip: bool,
pub hovered_entity: Option<String>,
}
const FORM_MARGIN: i8 = 10;
const FORM_OUTER_MARGIN: i8 = 6;
pub fn form_view(
ui: &mut egui::Ui,
spec: &FormViewSpec<'_>,
params: &mut Value,
mut hits: Option<&mut HashMap<String, egui::Rect>>,
) -> FormViewOut {
let mut out = FormViewOut::default();
let arrival = egui::Id::new(("form-view-arrival", spec.hits_prefix));
let pass = ui.ctx().cumulative_pass_nr();
let arrived = ui.ctx().memory_mut(|m| {
let previous: Option<(String, u64)> = m.data.get_temp(arrival);
let continuing = previous
.is_some_and(|(title, drawn)| title == spec.title && pass.saturating_sub(drawn) <= 1);
m.data.insert_temp(arrival, (spec.title.to_string(), pass));
!continuing
});
if arrived {
ui.scroll_to_cursor_animation(None, egui::style::ScrollAnimation::none());
}
egui::Frame::group(ui.style())
.inner_margin(egui::Margin::same(FORM_MARGIN))
.outer_margin(egui::Margin::same(FORM_OUTER_MARGIN))
.show(ui, |ui| {
form_body(ui, spec, params, &mut hits, &mut out);
});
out
}
fn form_body(
ui: &mut egui::Ui,
spec: &FormViewSpec<'_>,
params: &mut Value,
hits: &mut Option<&mut HashMap<String, egui::Rect>>,
out: &mut FormViewOut,
) {
ui.set_width(ui.available_width());
ui.horizontal(|ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let exit = ui.button(spec.exit_label);
publish(spec, hits, "form:return", exit.rect);
out.exit_clicked = exit.clicked();
out.roll_to_tip = out.exit_clicked && spec.rollback;
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
let head = ui.add(
egui::Label::new(egui::RichText::new(spec.title).heading()).truncate(),
);
publish(spec, hits, "form:feature", head.rect);
});
});
});
if let Some(subtitle) = spec.subtitle {
ui.label(egui::RichText::new(subtitle).weak());
}
ui.separator();
if let Some((text, color)) = spec.banner {
egui::Frame::group(ui.style())
.stroke(egui::Stroke::new(1.0, color))
.fill(color.gamma_multiply(0.12))
.show(ui, |ui| {
ui.set_width(ui.available_width());
ui.add(
egui::Label::new(egui::RichText::new(text).color(color))
.wrap_mode(egui::TextWrapMode::Wrap),
);
});
ui.add_space(4.0);
}
let mut references: Vec<&FormField> = Vec::new();
let mut param_leaves: Vec<&FormField> = Vec::new();
let mut groups: Vec<(&str, Vec<&FormField>)> = Vec::new();
for f in spec.fields {
match f.group.as_str() {
"References" => references.push(f),
"Parameters" => {
if matches!(f.kind, FieldKind::Text { read_only: true }) {
continue; }
param_leaves.push(f);
}
group => match groups.iter_mut().find(|(name, _)| *name == group) {
Some(existing) => existing.1.push(f),
None => groups.push((group, vec![f])),
},
}
}
groups.sort_by_key(|(name, _)| usize::from(*name == "Transform"));
for f in references.iter().chain(param_leaves.iter()) {
draw_field(ui, spec, f, params, hits, out);
}
for (name, fields) in &groups {
section(ui, spec, name, hits, |ui, hits| {
for f in fields {
draw_field(ui, spec, f, params, hits, out);
}
});
}
if let Some((name, draw)) = spec.extra {
section(ui, spec, name, hits, |ui, _hits| draw(ui));
}
let mut trailing_hover: Option<String> = None;
for (name, values) in spec.trailing.unwrap_or(&[]) {
section(ui, spec, name, hits, |ui, hits| {
if values.is_empty() {
ui.label(egui::RichText::new("(none)").weak());
}
for (i, value) in values.iter().enumerate() {
let line = ui.label(format!("• {value}"));
if line.hovered() {
trailing_hover = Some(value.clone());
}
publish(spec, hits, &format!("form:trailing:{name}:{i}"), line.rect);
}
});
}
if trailing_hover.is_some() {
out.hovered_entity = trailing_hover;
}
ui.add_space(8.0);
}
fn publish(
spec: &FormViewSpec<'_>,
hits: &mut Option<&mut HashMap<String, egui::Rect>>,
key: &str,
rect: egui::Rect,
) {
if let Some(map) = hits.as_deref_mut() {
map.insert(format!("{}{key}", spec.hits_prefix), rect);
}
}
fn section(
ui: &mut egui::Ui,
spec: &FormViewSpec<'_>,
name: &str,
hits: &mut Option<&mut HashMap<String, egui::Rect>>,
body: impl FnOnce(&mut egui::Ui, &mut Option<&mut HashMap<String, egui::Rect>>),
) {
ui.add_space(6.0);
let open = !COLLAPSED_BY_DEFAULT.contains(&name);
let response = egui::CollapsingHeader::new(egui::RichText::new(name).strong())
.id_salt(("form-view-section", spec.hits_prefix, spec.title, name))
.default_open(open)
.show(ui, |ui| body(ui, hits));
publish(
spec,
hits,
&format!("form:section:{name}"),
response.header_response.rect,
);
}
fn draw_field(
ui: &mut egui::Ui,
spec: &FormViewSpec<'_>,
field: &FormField,
params: &mut Value,
hits: &mut Option<&mut HashMap<String, egui::Rect>>,
out: &mut FormViewOut,
) {
let mut probe: HashMap<String, egui::Rect> = HashMap::new();
let mut rect = egui::Rect::NOTHING;
let mut actions = form::FieldActions::default();
ui.add_space(4.0);
if !matches!(field.kind, FieldKind::Button { .. }) {
ui.label(&field.label);
}
ui.push_id((spec.title, field.key()), |ui| {
let (changed, r) = form::field_input(ui, field, params, Some(&mut probe), &mut actions);
out.changed |= changed;
rect = r;
});
if let Some(map) = hits.as_deref_mut() {
let prefix = spec.hits_prefix;
map.insert(format!("{prefix}field:{}", field.path.join(".")), rect);
for (key, r) in probe {
map.insert(format!("{prefix}field:{key}"), r);
}
}
if actions.hovered_entity.is_some() {
out.hovered_entity = actions.hovered_entity.take();
}
if actions.clicked.is_some() {
match &field.kind {
FieldKind::Reference { filter, multiple } => {
out.ref_activate = Some(RefActivate {
path: field.path.clone(),
label: field.label.clone(),
filter: filter.clone(),
multiple: *multiple,
seed: form::reference_names(form::value_at(params, &field.path)),
});
}
_ => out.button_clicked = actions.clicked.take(),
}
}
}