From ed2e0fbc4ad704a94a915afe0b6457acf6ee36e0 Mon Sep 17 00:00:00 2001 From: Felix Riestra Date: Tue, 15 Sep 2026 01:26:48 +0200 Subject: [PATCH] Wire up the Properties panel's Parameters section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PropValue::ParamRow/ParamAddRow, the PropParamInput/Commit/Delete/AddNew messages, their command_driver handlers (on_prop_param_commit/_delete/ _add_new, resolve_named_parameter_edit), and their render functions all already existed — but nothing ever constructed the "Parameters" section that would route data to them, so the whole inline add/rename/redefine/ delete workflow was unreachable from the UI. Adds that missing section to the no-selection page in refresh_properties, alongside a new PropValue::ParamsVisibilityToggle header row (mirrors the existing show_constraint_values setting) for toggling whether constraint pills show their driven value/parameter name. Users can now manage named parameters inline in the Properties panel (live resolved values, add/rename/redefine/delete) as an alternative to the separate PARAMETERS modal, which continues to work unchanged. Co-Authored-By: Claude Sonnet 5 --- src/app/command_driver.rs | 68 +++++++++++++++++++++++++++++++++++++++ src/app/properties.rs | 39 ++++++++++++++++++++++ src/scene/model/object.rs | 5 +++ src/scene/selection.rs | 6 ++-- src/ui/properties.rs | 38 ++++++++++++++++++++++ 5 files changed, 154 insertions(+), 2 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index af720ea76..f4f19d6d1 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -7676,6 +7676,74 @@ mod sketch_constraint_undo_tests { } } +/// The `PropParamInput`/`PropParamCommit`/`PropParamDelete`/`PropParamAddNew` +/// handlers, `PropValue::ParamRow`/`ParamAddRow`/`ParamsVisibilityToggle`, +/// and their render functions already existed — nothing built the +/// Properties panel's "Parameters" section that would ever construct or +/// route to them, so the whole inline add/rename/redefine/delete workflow +/// was unreachable. These tests exercise it end to end now that the section +/// is wired up in `refresh_properties`. +#[cfg(test)] +mod properties_panel_parameters_tests { + use super::*; + + /// Finds a `PropValue::ParamRow`'s name across every rendered Properties + /// panel section — the panel is a rebuilt-on-demand snapshot + /// (`refresh_properties`), not a live view, so this only sees whatever + /// it was last rebuilt with. + fn param_row_names(app: &OpenCADStudio) -> Vec { + app.tabs[app.active_tab] + .properties + .sections + .iter() + .flat_map(|s| &s.props) + .filter_map(|p| match &p.value { + crate::scene::model::object::PropValue::ParamRow { name, .. } => Some(name.clone()), + _ => None, + }) + .collect() + } + + #[test] + fn no_selection_page_shows_every_named_parameter() { + let mut app = OpenCADStudio::new_for_test(); + let _ = app.automation_op(r#"{"op":"new"}"#); + let _ = app.tabs[app.active_tab].scene.named_parameters_mut().set("hole_dia", "12"); + app.refresh_properties(); + assert_eq!(param_row_names(&app), vec!["hole_dia".to_string()]); + } + + #[test] + fn add_rename_redefine_and_delete_a_parameter_through_the_panel() { + use crate::ui::window::named_parameters::ParamField; + + let mut app = OpenCADStudio::new_for_test(); + let _ = app.automation_op(r#"{"op":"new"}"#); + + // Add: the first row is auto-named "param1", and the panel reflects + // it immediately (on_prop_param_add_new refreshes internally). + let _ = app.update(Message::PropParamAddNew); + assert_eq!(param_row_names(&app), vec!["param1".to_string()]); + let index = app.tabs[app.active_tab].scene.named_parameters().iter().position(|p| p.name == "param1").unwrap(); + + // Rename param1 -> hole_dia (commit-on-submit, not per keystroke). + let _ = app.update(Message::PropParamInput { index, field: ParamField::Name, value: "hole_dia".to_string() }); + assert_eq!(param_row_names(&app), vec!["param1".to_string()], "typing alone must not commit"); + let _ = app.update(Message::PropParamCommit { index, field: ParamField::Name }); + assert_eq!(param_row_names(&app), vec!["hole_dia".to_string()]); + + // Redefine its formula. + let _ = app.update(Message::PropParamInput { index, field: ParamField::Formula, value: "8".to_string() }); + let _ = app.update(Message::PropParamCommit { index, field: ParamField::Formula }); + assert_eq!(app.tabs[app.active_tab].scene.named_parameters().resolve("hole_dia"), Ok(8.0)); + + // Delete: gone from both the table and the panel. + let _ = app.update(Message::PropParamDelete(index)); + assert!(!app.tabs[app.active_tab].scene.named_parameters().contains("hole_dia")); + assert!(param_row_names(&app).is_empty()); + } +} + #[cfg(all(test, not(target_arch = "wasm32")))] mod delobj_tests { use super::*; diff --git a/src/app/properties.rs b/src/app/properties.rs index 7096e7b03..fd7b6b3c3 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -471,6 +471,42 @@ impl OpenCADStudio { }, ], }, + // Document-wide named-parameter table embedded here + // instead of only the separate PARAMETERS modal — + // see `PropValue::ParamRow`'s doc comment. Belongs + // on the no-selection (drawing-level) page, not a + // per-entity one: a parameter isn't owned by any + // one entity. + PropSection { + title: t!("Parameters").into_owned(), + props: { + let mut props: Vec = vec![Property { + label: String::new(), + field: "show_named_parameters", + value: PropValue::ParamsVisibilityToggle(self.show_constraint_values), + }]; + props.extend(scene + .named_parameters() + .iter() + .enumerate() + .map(|(index, p)| Property { + label: String::new(), + field: "named_parameter", + value: PropValue::ParamRow { + index, + name: p.name.clone(), + formula: p.source.clone(), + resolved: scene.named_parameters().resolve(&p.name).map_err(|e| e.to_string()), + }, + })); + props.push(Property { + label: String::new(), + field: "named_parameter_add", + value: PropValue::ParamAddRow, + }); + props + }, + }, ]; ui::PropertiesPanel { title: t!("No selection").into_owned(), @@ -3231,6 +3267,9 @@ fn make_sections_read_only( PropValue::EntityLink { handles, .. } => format!("{} entity link(s)", handles.len()), PropValue::ParamRow { name, formula, .. } => format!("{name} = {formula}"), PropValue::ParamAddRow => String::new(), + PropValue::ParamsVisibilityToggle(value) => { + if *value { t!("Shown") } else { t!("Hidden") }.into_owned() + } }; property.field = "locked_read_only"; property.value = PropValue::ReadOnly(text); diff --git a/src/scene/model/object.rs b/src/scene/model/object.rs index e5aed1d5d..c05f08b83 100644 --- a/src/scene/model/object.rs +++ b/src/scene/model/object.rs @@ -80,6 +80,11 @@ pub enum PropValue { }, /// The trailing "+ Add parameter" row in the Parameters section. ParamAddRow, + /// The Parameters section's leading header row (no-selection page only): + /// a global on/off toggle for whether any constraint pill in the + /// viewport shows its driven value/parameter-name text. Mirrors + /// `App::show_constraint_values`. + ParamsVisibilityToggle(bool), } /// A single property row in the Properties panel. diff --git a/src/scene/selection.rs b/src/scene/selection.rs index 46fdc260f..b4f477023 100644 --- a/src/scene/selection.rs +++ b/src/scene/selection.rs @@ -750,7 +750,8 @@ impl Scene { | PropValue::FieldLwVaries { .. } | PropValue::EntityLink { .. } | PropValue::ParamRow { .. } - | PropValue::ParamAddRow => continue, + | PropValue::ParamAddRow + | PropValue::ParamsVisibilityToggle(_) => continue, } }; out.push(choice(prop.field, prop.label, editor)); @@ -887,7 +888,8 @@ impl Scene { | PropValue::FieldLwVaries { .. } | PropValue::EntityLink { .. } | PropValue::ParamRow { .. } - | PropValue::ParamAddRow => return None, + | PropValue::ParamAddRow + | PropValue::ParamsVisibilityToggle(_) => return None, }) } } diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 507c150d6..c0d319d5e 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -872,6 +872,7 @@ impl PropertiesPanel { self.render_param_row(*index, name, formula, resolved) } PropValue::ParamAddRow => render_param_add_row(), + PropValue::ParamsVisibilityToggle(value) => render_params_visibility_toggle_row(*value), } } @@ -1973,6 +1974,43 @@ fn render_param_add_row<'a>() -> Element<'a, Message> { container(btn).width(Length::Fill).into() } +// ── Parameters section: leading global visibility toggle ─────────────────── + +/// The Parameters section's leading header row (no-selection page): a +/// global on/off toggle for whether any constraint pill in the viewport +/// shows its driven value/parameter-name text — lives next to the +/// named-parameter table it governs. +fn render_params_visibility_toggle_row<'a>(value: bool) -> Element<'a, Message> { + let btn_label = if value { t!("Shown").into_owned() } else { t!("Hidden").into_owned() }; + let btn = button( + row![ + crate::ui::icons::semantic(crate::ui::icons::layer_visible(value), 13.0), + text(btn_label).size(FONT_SZ), + ] + .spacing(6) + .align_y(iced::Center), + ) + .on_press(Message::ShowConstraintValuesChanged(!value)) + .style(move |theme: &Theme, status| { + let palette = theme.palette(); + let pair = match status { + button::Status::Hovered | button::Status::Pressed => palette.background.weak, + _ => palette.background.base, + }; + button::Style { + background: Some(Background::Color(pair.color)), + border: Border { color: palette.background.neutral.color, width: 1.0, radius: 2.0.into() }, + text_color: pair.text, + ..Default::default() + } + }) + .padding([4, 8]) + .width(Length::Fill); + container(row![text(t!("Show values").into_owned()).size(FONT_SZ).width(Length::Fill), btn].align_y(iced::Center)) + .width(Length::Fill) + .into() +} + /// Build a label | widget property row. fn prop_row_widget<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<'a, Message> { prop_row_with_active(label, widget, false)