Skip to main content

agg_gui/widgets/property_row/
editor.rs

1//! Editor metadata for property rows — what kind of inline / panel
2//! widget the property panel should mount for each field.
3//!
4//! Lives at the agg-gui layer because the schema is shared widget
5//! vocabulary: any host wanting MatterCAD-style reflection-driven
6//! property panels feeds its reflected fields into these types.
7//! Host-side `PropertyInfo` analogues (e.g. atomartist's `PropDef`)
8//! compose these types but keep their own value-typed defaults.
9//!
10//! ## Mapped from MatterCAD attributes
11//!
12//! | MatterCAD attribute             | agg-gui shape                       |
13//! | ------------------------------- | ----------------------------------- |
14//! | `[Slider(min, max, easing, …)]` | `EditorKind::Slider(NumberAttrs)`   |
15//! | `[MaxDecimalPlaces(n)]`         | `NumberAttrs::max_decimal_places`   |
16//! | `[Description("…")]`            | `NodeFieldAttrs::description`       |
17//! | `[ReadOnly(true)]` on string    | `EditorKind::StringReadOnly`        |
18//! | `[MultiLineEdit]`               | `EditorKind::StringMultiLine`       |
19//! | `[EnumDisplay(Mode = Tabs)]`    | `EditorKind::EnumTabs { variants }` |
20//! | `[EnumDisplay(Mode = Buttons)]` | `EditorKind::EnumButtons { … }`     |
21//! | `[EnumDisplay(Mode = IconRow)]` | `EditorKind::EnumDropdown { … }`    |
22//! | `[HideFromEditor]`              | `NodeFieldAttrs::hidden`            |
23
24use std::sync::Arc;
25
26/// Editor hint for a property — how the property panel should render
27/// an editor for the current value.
28///
29/// The variants describe *intent*, not pixels — the row factory at
30/// `widgets/property_row` picks the concrete widget. Keeping the hint
31/// in the schema lets headless callers (tests, serialization, future
32/// inspector ports) reason about the editor shape without depending
33/// on the rendered widget tree.
34#[derive(Clone, Debug, PartialEq)]
35pub enum EditorKind {
36    /// Click-and-drag horizontally to edit a number. Default for
37    /// `Number` properties.
38    NumberDrag(NumberAttrs),
39    /// Horizontal slider between `min` and `max`. NodeDesigner's
40    /// "slider" widget and MatterCAD's `[Slider(...)]` map here.
41    Slider(NumberAttrs),
42    /// Boolean checkbox toggle.
43    Toggle,
44    /// Color swatch + picker.
45    ColorPicker,
46    /// 4×4 matrix — typically rendered as a compact button that opens
47    /// a translation/rotation/scale sub-panel.
48    Matrix,
49    /// Read-only text display. Used when a property's value isn't
50    /// directly editable on the node row (e.g. a derived value).
51    Display,
52    /// Editable single-line string. MatterCAD's default `string`
53    /// editor.
54    StringSingleLine,
55    /// Editable multi-line string. MatterCAD's `[MultiLineEdit]`.
56    StringMultiLine,
57    /// Word-wrapped read-only text. MatterCAD's `[ReadOnly(true)]`.
58    StringReadOnly,
59    /// Enum rendered as a dropdown combo box. The `variants` list is
60    /// the ordered set of allowed values (also the canonical display
61    /// labels). Current value is matched against one of these entries.
62    EnumDropdown { variants: Vec<Arc<str>> },
63    /// Enum rendered as a row of mutually-exclusive buttons.
64    EnumButtons { variants: Vec<Arc<str>> },
65    /// Enum rendered as a full-width tab strip — MatterCAD's
66    /// `EnumDisplay.Tabs`. Best for 2–5 variants the user switches
67    /// between at the top of a panel.
68    EnumTabs { variants: Vec<Arc<str>> },
69    /// Image picker / preview. MatterCAD's `ImageBufferPropertyEditor`.
70    Image,
71}
72
73impl EditorKind {
74    /// `NumberDrag` editor with `[min, max]` range.
75    pub fn drag_range(min: f64, max: f64) -> Self {
76        EditorKind::NumberDrag(NumberAttrs {
77            min: Some(min),
78            max: Some(max),
79            ..Default::default()
80        })
81    }
82
83    /// `Slider` editor with `[min, max]` range.
84    pub fn slider_range(min: f64, max: f64) -> Self {
85        EditorKind::Slider(NumberAttrs {
86            min: Some(min),
87            max: Some(max),
88            ..Default::default()
89        })
90    }
91
92    /// Inclusive numeric range when this editor is numeric, else `None`.
93    pub fn numeric_range(&self) -> (Option<f64>, Option<f64>) {
94        match self {
95            EditorKind::NumberDrag(a) | EditorKind::Slider(a) => (a.min, a.max),
96            _ => (None, None),
97        }
98    }
99
100    /// Numeric editor attributes when this editor is numeric.
101    pub fn number_attrs(&self) -> Option<&NumberAttrs> {
102        match self {
103            EditorKind::NumberDrag(a) | EditorKind::Slider(a) => Some(a),
104            _ => None,
105        }
106    }
107}
108
109impl Default for EditorKind {
110    fn default() -> Self {
111        EditorKind::Display
112    }
113}
114
115/// Numeric editor attributes — used by [`EditorKind::NumberDrag`] and
116/// [`EditorKind::Slider`]. Mirrors NodeDesigner's `addWidget("slider", ...)`
117/// option bag and MatterCAD's `[Slider]` / `[MaxDecimalPlaces]` attributes.
118#[derive(Clone, Debug, Default, PartialEq)]
119pub struct NumberAttrs {
120    /// Inclusive minimum.
121    pub min: Option<f64>,
122    /// Inclusive maximum.
123    pub max: Option<f64>,
124    /// Drag step (smallest delta per pixel of motion). `None` lets the
125    /// editor pick a sensible default for the range.
126    pub step: Option<f64>,
127    /// Display + clamp as an integer.
128    pub integer: bool,
129    /// Power-of-N easing applied to slider drag deltas. NodeDesigner's
130    /// `easeIn: 2` maps here.
131    pub ease_in: Option<f64>,
132    /// Snap drag deltas to a screen-space grid. NodeDesigner's
133    /// `useSnapGrid` maps here.
134    pub snap_grid: bool,
135    /// Limit display precision. MatterCAD's `[MaxDecimalPlaces(n)]` —
136    /// the stored value keeps full precision; this only affects
137    /// rendering.
138    pub max_decimal_places: Option<u8>,
139}
140
141impl NumberAttrs {
142    pub fn with_range(min: f64, max: f64) -> Self {
143        Self {
144            min: Some(min),
145            max: Some(max),
146            ..Default::default()
147        }
148    }
149    pub fn integer(mut self) -> Self {
150        self.integer = true;
151        self
152    }
153    pub fn with_step(mut self, step: f64) -> Self {
154        self.step = Some(step);
155        self
156    }
157    pub fn with_ease_in(mut self, e: f64) -> Self {
158        self.ease_in = Some(e);
159        self
160    }
161    pub fn with_snap_grid(mut self) -> Self {
162        self.snap_grid = true;
163        self
164    }
165    pub fn with_decimal_places(mut self, n: u8) -> Self {
166        self.max_decimal_places = Some(n);
167        self
168    }
169}
170
171/// Conditional visibility for a field — the data-driven analogue of
172/// MatterCAD's `IPropertyGridModifier.UpdateControls(change)` hook.
173///
174/// The host knows which sibling boolean property gates the
175/// `AdvancedOn` / `AdvancedOff` rows (typically a `bool` named
176/// `advanced`); the UI layer filters rows before rendering based on
177/// the live value of that toggle.
178#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
179pub enum VisibleWhen {
180    /// Always render (default).
181    #[default]
182    Always,
183    /// Render only when the node's `advanced` toggle is on. MatterCAD's
184    /// `IPropertyGridModifier.UpdateControls` pattern for advanced
185    /// rows.
186    AdvancedOn,
187    /// Render only when the node's `advanced` toggle is off. Used by
188    /// the easy-mode hint message that nudges the user toward
189    /// Advanced.
190    AdvancedOff,
191    /// Never render. MatterCAD's `[HideFromEditor]`.
192    Never,
193}
194
195/// Field-level metadata — declared once per reflected struct field and
196/// consumed both by the host's `PropDef`-style binding type (which
197/// folds these into the property store) and by the property panel
198/// when it renders the field's editor + label.
199#[derive(Clone, Debug, Default)]
200pub struct NodeFieldAttrs {
201    pub label: Option<Arc<str>>,
202    pub editor: EditorKind,
203    /// When `Some(socket_name)`, the field is paired with the input
204    /// socket of that name (host-side concept): the canvas draws the
205    /// field's inline editor on the socket's row, and the editor is
206    /// hidden when the socket is connected. Hosts without an input-
207    /// socket concept ignore this.
208    pub bound_input: Option<Arc<str>>,
209    /// Free-text description shown in tooltips / the property-panel
210    /// detail view. MatterCAD's `[Description("…")]`.
211    pub description: Option<Arc<str>>,
212    /// Visibility gate — Always, AdvancedOn, AdvancedOff, or Never.
213    /// The UI filters rows by combining this with the live "advanced"
214    /// toggle value.
215    pub visible_when: VisibleWhen,
216}
217
218impl NodeFieldAttrs {
219    pub fn new() -> Self {
220        Self::default()
221    }
222    pub fn with_label(mut self, label: impl Into<Arc<str>>) -> Self {
223        self.label = Some(label.into());
224        self
225    }
226    pub fn with_editor(mut self, editor: EditorKind) -> Self {
227        self.editor = editor;
228        self
229    }
230    pub fn bound_to(mut self, socket: impl Into<Arc<str>>) -> Self {
231        self.bound_input = Some(socket.into());
232        self
233    }
234    pub fn with_description(mut self, text: impl Into<Arc<str>>) -> Self {
235        self.description = Some(text.into());
236        self
237    }
238    /// Set the conditional visibility gate. Same trio as MatterCAD's
239    /// `[HideFromEditor]` + `IPropertyGridModifier.UpdateControls`.
240    pub fn visible_when(mut self, when: VisibleWhen) -> Self {
241        self.visible_when = when;
242        self
243    }
244    /// Shorthand for `visible_when(VisibleWhen::AdvancedOn)` — the
245    /// common case of "this row is only relevant after the user opens
246    /// Advanced".
247    pub fn advanced(mut self) -> Self {
248        self.visible_when = VisibleWhen::AdvancedOn;
249        self
250    }
251    /// Shorthand for `visible_when(VisibleWhen::AdvancedOff)` — used
252    /// by easy-mode hint messages that disappear once Advanced is on.
253    pub fn easy_only(mut self) -> Self {
254        self.visible_when = VisibleWhen::AdvancedOff;
255        self
256    }
257    /// Shorthand for `visible_when(VisibleWhen::Never)`.
258    pub fn hidden(mut self) -> Self {
259        self.visible_when = VisibleWhen::Never;
260        self
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn slider_range_helper_sets_min_max() {
270        let k = EditorKind::slider_range(1.0, 400.0);
271        match k {
272            EditorKind::Slider(a) => {
273                assert_eq!(a.min, Some(1.0));
274                assert_eq!(a.max, Some(400.0));
275            }
276            other => panic!("expected Slider, got {:?}", other),
277        }
278    }
279
280    #[test]
281    fn default_editor_is_display() {
282        assert!(matches!(EditorKind::default(), EditorKind::Display));
283    }
284
285    #[test]
286    fn number_attrs_builder_chains() {
287        let a = NumberAttrs::with_range(0.0, 360.0)
288            .integer()
289            .with_step(1.0)
290            .with_decimal_places(0)
291            .with_ease_in(2.0)
292            .with_snap_grid();
293        assert_eq!(a.min, Some(0.0));
294        assert_eq!(a.max, Some(360.0));
295        assert!(a.integer);
296        assert_eq!(a.step, Some(1.0));
297        assert_eq!(a.max_decimal_places, Some(0));
298        assert_eq!(a.ease_in, Some(2.0));
299        assert!(a.snap_grid);
300    }
301
302    #[test]
303    fn node_field_attrs_builder_chains() {
304        let a = NodeFieldAttrs::new()
305            .with_label("Diameter")
306            .with_editor(EditorKind::slider_range(1.0, 400.0))
307            .with_description("Width across.")
308            .advanced();
309        assert_eq!(a.label.as_deref().map(|x| x.as_ref()), Some("Diameter"));
310        assert!(matches!(a.editor, EditorKind::Slider(_)));
311        assert!(a
312            .description
313            .as_deref()
314            .map(|x| x.contains("Width"))
315            .unwrap_or(false));
316        assert_eq!(a.visible_when, VisibleWhen::AdvancedOn);
317    }
318
319    #[test]
320    fn visible_when_shorthands_set_expected_variant() {
321        assert_eq!(NodeFieldAttrs::new().visible_when, VisibleWhen::Always);
322        assert_eq!(
323            NodeFieldAttrs::new().advanced().visible_when,
324            VisibleWhen::AdvancedOn
325        );
326        assert_eq!(
327            NodeFieldAttrs::new().easy_only().visible_when,
328            VisibleWhen::AdvancedOff
329        );
330        assert_eq!(
331            NodeFieldAttrs::new().hidden().visible_when,
332            VisibleWhen::Never
333        );
334    }
335
336    #[test]
337    fn numeric_range_is_none_for_non_numeric() {
338        assert_eq!(EditorKind::Toggle.numeric_range(), (None, None));
339        assert_eq!(EditorKind::ColorPicker.numeric_range(), (None, None));
340        assert_eq!(EditorKind::StringSingleLine.numeric_range(), (None, None));
341    }
342
343    #[test]
344    fn enum_variants_round_trip() {
345        let k = EditorKind::EnumTabs {
346            variants: vec!["Easy".into(), "Advanced".into()],
347        };
348        if let EditorKind::EnumTabs { variants } = k {
349            assert_eq!(variants.len(), 2);
350            assert_eq!(variants[0].as_ref(), "Easy");
351        } else {
352            panic!("expected EnumTabs");
353        }
354    }
355}