use crate::automation::hit_keys::HitKeyDoc;
use crate::form;
use crate::panels::bom_columns;
use crate::panels::tree::{self, TreeRow};
use crate::store::{ModelStore, SETTINGS_KEY};
use brep_render::engine_state::EngineState;
use brep_render::style::{settings_form_fields, FormField, RenderSettings};
use eframe::egui;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
const PROBLEM_AMBER: egui::Color32 = egui::Color32::from_rgb(0xff, 0x9f, 0x0a);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tab {
Display,
Assemblies,
}
pub struct SettingsPanel {
pub open: bool,
hits: HashMap<String, egui::Rect>,
tab: Tab,
display_collapsed: bool,
closed_groups: HashSet<String>,
assemblies_collapsed: bool,
bom_columns_buf: Option<String>,
}
impl SettingsPanel {
pub fn new() -> Self {
Self {
open: false,
hits: HashMap::new(),
tab: Tab::Display,
display_collapsed: false,
closed_groups: HashSet::new(),
assemblies_collapsed: false,
bom_columns_buf: None,
}
}
pub fn show(&mut self, ctx: &egui::Context, state: &mut EngineState, store: &dyn ModelStore) {
if self.open {
let mut open = true;
egui::Window::new("Settings")
.open(&mut open)
.movable(true)
.resizable(true)
.default_size([320.0, 400.0])
.default_pos([720.0, 56.0])
.show(ctx, |ui| self.body(ui, state, store));
self.open = open;
if crate::automation::registry::enabled() {
crate::automation::registry::publish("__brepSettingsHit", "settings window widget rects (field:*, group:*)", &self.hits_json());
}
}
}
fn body(&mut self, ui: &mut egui::Ui, state: &mut EngineState, store: &dyn ModelStore) {
self.hits.clear();
let tab = self.tab;
ui.horizontal(|ui| {
let display = ui.selectable_value(&mut self.tab, Tab::Display, "Display");
let assemblies = ui.selectable_value(&mut self.tab, Tab::Assemblies, "Assemblies");
self.hits.insert("tab:display".into(), display.rect);
self.hits.insert("tab:assemblies".into(), assemblies.rect);
});
if self.tab != tab {
ui.ctx().request_repaint();
}
ui.separator();
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.show(ui, |ui| {
ui.spacing_mut().item_spacing.y = 2.0;
match tab {
Tab::Display => self.settings_section(ui, state, store),
Tab::Assemblies => self.assemblies_section(ui, state, store),
}
});
}
fn assemblies_section(
&mut self,
ui: &mut egui::Ui,
state: &mut EngineState,
store: &dyn ModelStore,
) {
let open = !self.assemblies_collapsed;
let root_resp = tree::node(
ui,
TreeRow {
guides: &[],
is_last: true,
expandable: true,
expanded: open,
root: true,
glyph: None,
label: "Assemblies",
selected: false,
draggable: false,
tint: None,
},
|_| {},
);
self.hits.insert("box:__assemblies".into(), root_resp.box_rect);
if root_resp.toggled || root_resp.label.clicked() {
self.assemblies_collapsed = !self.assemblies_collapsed;
}
if !open {
self.bom_columns_buf = None;
return;
}
let stored = bom_columns::effective_text(&state.settings.bom_columns);
let buffer = self.bom_columns_buf.get_or_insert_with(|| stored.clone());
ui.label(egui::RichText::new("BOM columns").strong());
ui.label(
egui::RichText::new(
"One per line, in order. A leading * shows it. \
part.<Field> is stored on the part, occurrence.<Field> on one placement. \
A line that is just - freezes the columns above it; the rest scroll.",
)
.weak(),
);
let editor = ui.add(
egui::TextEdit::multiline(buffer)
.id_salt("bom-columns-editor")
.desired_rows(8)
.desired_width(f32::INFINITY)
.code_editor(),
);
self.hits.insert("field:bomColumns".into(), editor.rect);
if editor.lost_focus() {
let mut settings_json: serde_json::Value =
serde_json::from_str(&state.settings_json()).unwrap_or(serde_json::Value::Null);
if let Some(object) = settings_json.as_object_mut() {
object.insert(
"bomColumns".into(),
serde_json::Value::String(buffer.clone()),
);
let json = settings_json.to_string();
let _ = state.apply_settings_json(&json);
let _ = store.write(SETTINGS_KEY, &json);
}
} else if !editor.has_focus() && *buffer != stored {
*buffer = stored;
}
let parsed = bom_columns::parse(buffer);
if parsed.problems.is_empty() {
ui.label(
egui::RichText::new(format!(
"{} columns, {} shown",
parsed.columns.len(),
parsed.columns.iter().filter(|column| column.shown).count()
))
.weak(),
);
} else {
for problem in &parsed.problems {
ui.label(egui::RichText::new(problem).color(PROBLEM_AMBER));
}
}
let reset = ui.button("Reset BOM columns");
self.hits.insert("bom-columns:reset".into(), reset.rect);
if reset.clicked() {
self.bom_columns_buf = None;
let mut settings_json: serde_json::Value =
serde_json::from_str(&state.settings_json()).unwrap_or(serde_json::Value::Null);
if let Some(object) = settings_json.as_object_mut() {
object.insert("bomColumns".into(), serde_json::Value::String(String::new()));
let json = settings_json.to_string();
let _ = state.apply_settings_json(&json);
let _ = store.write(SETTINGS_KEY, &json);
}
}
ui.add_space(4.0);
}
fn settings_section(
&mut self,
ui: &mut egui::Ui,
state: &mut EngineState,
store: &dyn ModelStore,
) {
let mut settings_json: Value =
serde_json::from_str(&state.settings_json()).unwrap_or(Value::Null);
let fields = settings_form_fields();
let mut groups: Vec<(String, Vec<&FormField>)> = Vec::new();
for f in &fields {
if let Some(g) = groups.iter_mut().find(|(n, _)| *n == f.group) {
g.1.push(f);
} else {
groups.push((f.group.clone(), vec![f]));
}
}
let root_open = !self.display_collapsed;
let root_resp = tree::node(
ui,
TreeRow {
guides: &[],
is_last: true,
expandable: true,
expanded: root_open,
root: true,
glyph: None,
label: "Display settings",
selected: false,
draggable: false,
tint: None,
},
|_| {},
);
if root_resp.toggled || root_resp.label.clicked() {
self.display_collapsed = !self.display_collapsed;
}
let mut changed = false;
if root_open {
let n = groups.len();
for (gi, (gname, gfields)) in groups.iter().enumerate() {
let is_last = gi + 1 == n;
let open = !self.closed_groups.contains(gname);
let resp = tree::node(ui, TreeRow::branch(&[], is_last, open, gname), |_| {});
self.hits.insert(format!("group:{gname}"), resp.box_rect);
if resp.toggled || resp.label.clicked() {
if open {
self.closed_groups.insert(gname.clone());
} else {
self.closed_groups.remove(gname);
}
}
if !open {
continue;
}
let base = tree::child_guides(&[], is_last);
let m = gfields.len();
for (fi, &f) in gfields.iter().enumerate() {
changed |= self.settings_leaf(ui, f, &mut settings_json, &base, fi + 1 == m);
}
}
}
if changed {
let json = settings_json.to_string();
let _ = state.apply_settings_json(&json);
let _ = store.write(SETTINGS_KEY, &json);
}
if root_open {
ui.add_space(2.0);
if ui.button("Reset to defaults").clicked() {
state.settings = RenderSettings::default();
let json = state.settings.to_json();
let _ = state.apply_settings_json(&json);
let _ = store.write(SETTINGS_KEY, &json);
}
}
}
fn settings_leaf(
&mut self,
ui: &mut egui::Ui,
field: &FormField,
current: &mut Value,
guides: &[bool],
is_last: bool,
) -> bool {
let mut changed = false;
let mut rect = egui::Rect::NOTHING;
tree::node(ui, TreeRow::leaf(guides, is_last, &field.label), |ui| {
let (ch, r) = form::field_input(ui, field, current, None, &mut form::FieldActions::default());
changed = ch;
rect = r;
});
self.hits.insert(format!("field:{}", field.key()), rect);
changed
}
pub fn hits_json(&self) -> String {
crate::automation::hit_rects::hits_json(&self.hits)
}
}
pub static HIT_KEYS: &[HitKeyDoc] = &[
HitKeyDoc { panel: "settings", prefix: "field:", meaning: "a settings field by key", command: None },
HitKeyDoc { panel: "settings", prefix: "group:", meaning: "a settings group header", command: None },
HitKeyDoc { panel: "settings", prefix: "tab:", meaning: "a settings tab (tab:display, tab:assemblies)", command: None },
HitKeyDoc { panel: "settings", prefix: "box:", meaning: "expand/collapse a section", command: None },
HitKeyDoc { panel: "settings", prefix: "bom-columns:reset", meaning: "reset the BOM columns", command: None },
];