Skip to main content

khora_editor/widgets/inspector/
renderers.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//! Per-shape leaf renderers + numeric helpers.
10//!
11//! Renderers are pure functions matched by JSON shape. The walker (in
12//! `widgets::inspector::walker`) iterates the shape detectors (`is_*`) in
13//! order and dispatches to the matching renderer. Adding a new shape =
14//! adding a `match_shape` predicate and a `render_*` function and wiring
15//! them in `walker::render_object`.
16
17use khora_sdk::editor_ui::{UiBuilder, UiTheme};
18use serde_json::Value;
19
20// ─── shape detection ────────────────────────────────────────────────
21
22pub fn is_vec3(map: &serde_json::Map<String, Value>) -> bool {
23    map.len() == 3
24        && map.get("x").is_some_and(|v| v.is_number())
25        && map.get("y").is_some_and(|v| v.is_number())
26        && map.get("z").is_some_and(|v| v.is_number())
27}
28
29pub fn is_quat(map: &serde_json::Map<String, Value>) -> bool {
30    map.len() == 4
31        && map.get("x").is_some_and(|v| v.is_number())
32        && map.get("y").is_some_and(|v| v.is_number())
33        && map.get("z").is_some_and(|v| v.is_number())
34        && map.get("w").is_some_and(|v| v.is_number())
35}
36
37pub fn is_color(map: &serde_json::Map<String, Value>) -> bool {
38    map.len() == 4
39        && map.get("r").is_some_and(|v| v.is_number())
40        && map.get("g").is_some_and(|v| v.is_number())
41        && map.get("b").is_some_and(|v| v.is_number())
42        && map.get("a").is_some_and(|v| v.is_number())
43}
44
45// ─── leaf widgets ───────────────────────────────────────────────────
46
47pub fn render_vec3(
48    ui: &mut dyn UiBuilder,
49    map: &mut serde_json::Map<String, Value>,
50    _theme: &UiTheme,
51) -> bool {
52    let mut v = [
53        json_to_f32(map.get("x")),
54        json_to_f32(map.get("y")),
55        json_to_f32(map.get("z")),
56    ];
57    let changed = ui.vec3_editor("", &mut v, 0.05);
58    if changed {
59        map.insert("x".into(), f32_to_json(v[0]));
60        map.insert("y".into(), f32_to_json(v[1]));
61        map.insert("z".into(), f32_to_json(v[2]));
62    }
63    changed
64}
65
66pub fn render_quat(ui: &mut dyn UiBuilder, map: &mut serde_json::Map<String, Value>) -> bool {
67    let mut changed = false;
68    for axis in ["x", "y", "z", "w"] {
69        let mut local = json_to_f32(map.get(axis));
70        if ui.drag_value_f32(axis, &mut local, 0.01) {
71            map.insert(axis.into(), f32_to_json(local));
72            changed = true;
73        }
74    }
75    changed
76}
77
78pub fn render_color(
79    ui: &mut dyn UiBuilder,
80    map: &mut serde_json::Map<String, Value>,
81    _theme: &UiTheme,
82) -> bool {
83    let mut rgba = [
84        json_to_f32(map.get("r")),
85        json_to_f32(map.get("g")),
86        json_to_f32(map.get("b")),
87        json_to_f32(map.get("a")),
88    ];
89    let changed = ui.color_edit("", &mut rgba);
90    if changed {
91        map.insert("r".into(), f32_to_json(rgba[0]));
92        map.insert("g".into(), f32_to_json(rgba[1]));
93        map.insert("b".into(), f32_to_json(rgba[2]));
94        map.insert("a".into(), f32_to_json(rgba[3]));
95    }
96    changed
97}
98
99pub fn render_number_row(ui: &mut dyn UiBuilder, label: &str, value: &mut Value) -> bool {
100    let mut local = json_to_f32(Some(&*value));
101    let changed = ui.drag_value_f32(label, &mut local, 0.05);
102    if changed {
103        *value = f32_to_json(local);
104    }
105    changed
106}
107
108pub fn render_numeric_triple(ui: &mut dyn UiBuilder, label: &str, arr: &mut [Value]) -> bool {
109    let mut v = [
110        json_to_f32(Some(&arr[0])),
111        json_to_f32(Some(&arr[1])),
112        json_to_f32(Some(&arr[2])),
113    ];
114    let changed = ui.vec3_editor(label, &mut v, 0.05);
115    if changed {
116        arr[0] = f32_to_json(v[0]);
117        arr[1] = f32_to_json(v[1]);
118        arr[2] = f32_to_json(v[2]);
119    }
120    changed
121}
122
123pub fn render_numeric_quad(
124    ui: &mut dyn UiBuilder,
125    label: &str,
126    arr: &mut [Value],
127    _theme: &UiTheme,
128) -> bool {
129    // Heuristic: 4 numbers all in `[0, 1]` are likely a colour.
130    let all_unit = arr.iter().all(|v| {
131        let n = v.as_f64().unwrap_or(f64::NAN);
132        (0.0..=1.0).contains(&n)
133    });
134    if all_unit {
135        let mut rgba = [
136            json_to_f32(Some(&arr[0])),
137            json_to_f32(Some(&arr[1])),
138            json_to_f32(Some(&arr[2])),
139            json_to_f32(Some(&arr[3])),
140        ];
141        let changed = ui.color_edit(label, &mut rgba);
142        if changed {
143            for (i, v) in rgba.iter().enumerate() {
144                arr[i] = f32_to_json(*v);
145            }
146        }
147        return changed;
148    }
149    // Otherwise — generic 4-component drag row.
150    let mut changed = false;
151    ui.horizontal(&mut |row| {
152        row.label(label);
153        for v in arr.iter_mut() {
154            let mut local = json_to_f32(Some(&*v));
155            if row.drag_value_f32("", &mut local, 0.05) {
156                *v = f32_to_json(local);
157                changed = true;
158            }
159        }
160    });
161    changed
162}
163
164// ─── numeric bridge ─────────────────────────────────────────────────
165
166pub fn json_to_f32(v: Option<&Value>) -> f32 {
167    v.and_then(|v| v.as_f64()).map(|f| f as f32).unwrap_or(0.0)
168}
169
170pub fn f32_to_json(f: f32) -> Value {
171    serde_json::Number::from_f64(f as f64)
172        .map(Value::Number)
173        .unwrap_or(Value::Null)
174}
175
176/// `snake_case` → `"Snake Case"` — keeps the inspector readable without
177/// the engine having to ship a separate display-name table.
178pub fn humanise(field: &str) -> String {
179    let mut out = String::with_capacity(field.len());
180    let mut up = true;
181    for c in field.chars() {
182        if c == '_' {
183            out.push(' ');
184            up = true;
185        } else if up {
186            out.extend(c.to_uppercase());
187            up = false;
188        } else {
189            out.push(c);
190        }
191    }
192    out
193}