Skip to main content

khora_editor/widgets/inspector/
walker.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8
9//! Generic JSON walker — single source of truth for "show a component".
10//!
11//! Dispatch by JSON shape:
12//!
13//! | Shape                                 | Widget |
14//! |---------------------------------------|--------|
15//! | `Object { x, y, z }`                  | Vec3 with X/Y/Z badges |
16//! | `Object { x, y, z, w }`               | Quaternion (4 drag values) |
17//! | `Object { r, g, b, a }`               | colour swatch + RGBA dragvalues |
18//! | `Object { "Variant": <inner> }`       | enum: variant title + recurse |
19//! | `Object { … }` (other)                | indented row of children |
20//! | `Number`                              | DragValue |
21//! | `Bool`                                | checkbox |
22//! | `String`                              | text input |
23//! | `Array<f64>` (3 or 4)                 | falls through to Vec3 / colour |
24//! | `Null`                                | dim "null" label |
25
26use khora_sdk::editor_ui::{UiBuilder, UiTheme};
27use serde_json::Value;
28
29use crate::widgets::enum_variants::editable_variants;
30
31use super::renderers::{
32    humanise, is_color, is_quat, is_vec3, render_color, render_number_row, render_numeric_quad,
33    render_numeric_triple, render_quat, render_vec3,
34};
35
36/// Renders a top-level component value. Returns true if the user mutated
37/// any leaf — the caller queues a `SetComponentJson` edit in that case.
38pub fn render_value(ui: &mut dyn UiBuilder, value: &mut Value, theme: &UiTheme) -> bool {
39    match value {
40        Value::Object(map) => render_object(ui, map, theme),
41        Value::Array(arr) => render_array(ui, "", arr, theme),
42        // A bare scalar component (unit struct, newtype) — wrap in a
43        // single unnamed row so the generic code path handles it
44        // identically.
45        _ => render_field(ui, "(value)", value, theme),
46    }
47}
48
49/// Walks a JSON object, picking the right widget for each shape.
50fn render_object(
51    ui: &mut dyn UiBuilder,
52    map: &mut serde_json::Map<String, Value>,
53    theme: &UiTheme,
54) -> bool {
55    // 1. Single-key objects are serde-tagged enum variants.
56    if map.len() == 1 {
57        let key = map.keys().next().cloned().unwrap();
58
59        // If this enum is registered as editor-switchable, render a
60        // combo-box and replace the payload with the new variant's
61        // defaults on selection.
62        if let Some(variants) = editable_variants(&key) {
63            let names: Vec<&str> = variants.iter().map(|(n, _)| *n).collect();
64            let mut current = variants
65                .iter()
66                .position(|(n, _)| *n == key.as_str())
67                .unwrap_or(0);
68            // Salted by the variant set, which identifies the enum *type* and
69            // stays put when the selection changes — unlike the current
70            // variant name, and unlike the shared "Variant" label.
71            let combo_salt = format!("variant::{}", names.join("|"));
72            let combo_changed = ui.combo_box(&combo_salt, "Variant", &mut current, &names);
73            if combo_changed {
74                if let Some((_, Value::Object(new_map))) = variants.get(current) {
75                    map.clear();
76                    for (k, v) in new_map.iter() {
77                        map.insert(k.clone(), v.clone());
78                    }
79                    return true;
80                }
81            }
82            // Fall through to recurse into the (possibly newly-typed)
83            // inner.
84            let inner_key = map.keys().next().cloned().unwrap();
85            let inner = map.get_mut(&inner_key).unwrap();
86            return match inner {
87                Value::Object(m) => render_object(ui, m, theme),
88                Value::Null => false,
89                _ => render_field(ui, "value", inner, theme),
90            };
91        }
92
93        // Unknown enum — read-only label fallback.
94        let inner = map.get_mut(&key).unwrap();
95        ui.colored_label(theme.text_dim, &format!("Variant: {}", key));
96        return match inner {
97            Value::Object(m) => render_object(ui, m, theme),
98            Value::Null => false,
99            _ => render_field(ui, "value", inner, theme),
100        };
101    }
102
103    // 2. Specialised shapes detected by field-name signature.
104    if is_color(map) {
105        return render_color(ui, map, theme);
106    }
107    if is_vec3(map) {
108        return render_vec3(ui, map, theme);
109    }
110    if is_quat(map) {
111        return render_quat(ui, map);
112    }
113
114    // 3. Generic object — one row per field.
115    let mut changed = false;
116    let keys: Vec<String> = map.keys().cloned().collect();
117    for key in keys {
118        let label = humanise(&key);
119        let val = map.get_mut(&key).unwrap();
120        if render_field(ui, &label, val, theme) {
121            changed = true;
122        }
123    }
124    changed
125}
126
127/// Renders a single labelled row. Picks a widget by leaf type or
128/// recurses for nested structure.
129fn render_field(ui: &mut dyn UiBuilder, label: &str, value: &mut Value, theme: &UiTheme) -> bool {
130    match value {
131        Value::Bool(b) => {
132            let mut local = *b;
133            let changed = ui.checkbox(&mut local, label);
134            if changed {
135                *value = Value::Bool(local);
136            }
137            changed
138        }
139        Value::Number(_) => render_number_row(ui, label, value),
140        Value::String(s) => {
141            let mut local = s.clone();
142            let mut changed = false;
143            ui.horizontal(&mut |row| {
144                row.label(label);
145                if row.text_edit_singleline(&mut local) {
146                    changed = true;
147                }
148            });
149            if changed {
150                *value = Value::String(local);
151            }
152            changed
153        }
154        Value::Null => {
155            ui.colored_label(theme.text_muted, &format!("{}: null", label));
156            false
157        }
158        Value::Array(arr) => render_array(ui, label, arr, theme),
159        Value::Object(_) => {
160            // Nested struct / enum — collapsing block keeps the layout
161            // readable for deeply nested components.
162            let mut changed = false;
163            ui.collapsing(label, true, &mut |inner| {
164                if let Value::Object(m) = value {
165                    if render_object(inner, m, theme) {
166                        changed = true;
167                    }
168                }
169            });
170            changed
171        }
172    }
173}
174
175/// Generic array row. Numeric arrays of length 3 / 4 are treated like
176/// Vec3 / Vec4 with axis colours; everything else falls back to "[i] = …"
177/// rows.
178fn render_array(ui: &mut dyn UiBuilder, label: &str, arr: &mut [Value], theme: &UiTheme) -> bool {
179    if arr.iter().all(|v| v.is_number()) {
180        match arr.len() {
181            3 => return render_numeric_triple(ui, label, arr),
182            4 => return render_numeric_quad(ui, label, arr, theme),
183            _ => {}
184        }
185    }
186
187    let mut changed = false;
188    ui.collapsing(label, true, &mut |inner| {
189        for (i, v) in arr.iter_mut().enumerate() {
190            if render_field(inner, &format!("[{}]", i), v, theme) {
191                changed = true;
192            }
193        }
194    });
195    changed
196}