1use glam::{Vec2, Vec3, Vec4};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone)]
16pub struct BoolField {
17 pub name: String,
18 pub value: bool,
19 pub tooltip: Option<String>,
20}
21
22impl BoolField {
23 pub fn new(name: impl Into<String>, value: bool) -> Self {
24 Self { name: name.into(), value, tooltip: None }
25 }
26 pub fn with_tooltip(mut self, tip: impl Into<String>) -> Self {
27 self.tooltip = Some(tip.into());
28 self
29 }
30 pub fn toggle(&mut self) {
31 self.value = !self.value;
32 }
33 pub fn render_ascii(&self) -> String {
34 let check = if self.value { "[x]" } else { "[ ]" };
35 format!("{} {}", check, self.name)
36 }
37}
38
39#[derive(Debug, Clone)]
41pub struct IntField {
42 pub name: String,
43 pub value: i64,
44 pub min: Option<i64>,
45 pub max: Option<i64>,
46 pub step: i64,
47}
48
49impl IntField {
50 pub fn new(name: impl Into<String>, value: i64) -> Self {
51 Self { name: name.into(), value, min: None, max: None, step: 1 }
52 }
53 pub fn with_range(mut self, min: i64, max: i64) -> Self {
54 self.min = Some(min);
55 self.max = Some(max);
56 self
57 }
58 pub fn increment(&mut self) {
59 self.value += self.step;
60 if let Some(m) = self.max { self.value = self.value.min(m); }
61 }
62 pub fn decrement(&mut self) {
63 self.value -= self.step;
64 if let Some(m) = self.min { self.value = self.value.max(m); }
65 }
66 pub fn set(&mut self, v: i64) {
67 let v = if let Some(m) = self.min { v.max(m) } else { v };
68 let v = if let Some(m) = self.max { v.min(m) } else { v };
69 self.value = v;
70 }
71 pub fn render_ascii(&self) -> String {
72 format!("{}: {}", self.name, self.value)
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct FloatField {
79 pub name: String,
80 pub value: f64,
81 pub min: Option<f64>,
82 pub max: Option<f64>,
83 pub step: f64,
84 pub precision: usize,
85}
86
87impl FloatField {
88 pub fn new(name: impl Into<String>, value: f64) -> Self {
89 Self { name: name.into(), value, min: None, max: None, step: 0.1, precision: 3 }
90 }
91 pub fn with_range(mut self, min: f64, max: f64) -> Self {
92 self.min = Some(min); self.max = Some(max); self
93 }
94 pub fn with_step(mut self, step: f64) -> Self {
95 self.step = step; self
96 }
97 pub fn set(&mut self, v: f64) {
98 let v = if let Some(m) = self.min { v.max(m) } else { v };
99 let v = if let Some(m) = self.max { v.min(m) } else { v };
100 self.value = v;
101 }
102 pub fn render_ascii(&self) -> String {
103 format!("{}: {:.prec$}", self.name, self.value, prec = self.precision)
104 }
105}
106
107#[derive(Debug, Clone)]
109pub struct StringField {
110 pub name: String,
111 pub value: String,
112 pub max_len: usize,
113 pub multiline: bool,
114 pub placeholder: String,
115}
116
117impl StringField {
118 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
119 Self {
120 name: name.into(),
121 value: value.into(),
122 max_len: 256,
123 multiline: false,
124 placeholder: String::new(),
125 }
126 }
127 pub fn set(&mut self, v: impl Into<String>) {
128 let v: String = v.into();
129 self.value = if v.len() > self.max_len { v[..self.max_len].to_string() } else { v };
130 }
131 pub fn push_char(&mut self, c: char) {
132 if self.value.len() < self.max_len {
133 self.value.push(c);
134 }
135 }
136 pub fn pop_char(&mut self) {
137 self.value.pop();
138 }
139 pub fn render_ascii(&self) -> String {
140 format!("{}: \"{}\"", self.name, self.value)
141 }
142}
143
144#[derive(Debug, Clone)]
146pub struct Vec2Field {
147 pub name: String,
148 pub value: Vec2,
149 pub min: Option<f32>,
150 pub max: Option<f32>,
151}
152
153impl Vec2Field {
154 pub fn new(name: impl Into<String>, value: Vec2) -> Self {
155 Self { name: name.into(), value, min: None, max: None }
156 }
157 pub fn set(&mut self, v: Vec2) {
158 self.value = v;
159 if let Some(lo) = self.min { self.value = self.value.max(Vec2::splat(lo)); }
160 if let Some(hi) = self.max { self.value = self.value.min(Vec2::splat(hi)); }
161 }
162 pub fn render_ascii(&self) -> String {
163 format!("{}: ({:.3}, {:.3})", self.name, self.value.x, self.value.y)
164 }
165}
166
167#[derive(Debug, Clone)]
169pub struct Vec3Field {
170 pub name: String,
171 pub value: Vec3,
172 pub min: Option<f32>,
173 pub max: Option<f32>,
174}
175
176impl Vec3Field {
177 pub fn new(name: impl Into<String>, value: Vec3) -> Self {
178 Self { name: name.into(), value, min: None, max: None }
179 }
180 pub fn set(&mut self, v: Vec3) {
181 self.value = v;
182 if let Some(lo) = self.min { self.value = self.value.max(Vec3::splat(lo)); }
183 if let Some(hi) = self.max { self.value = self.value.min(Vec3::splat(hi)); }
184 }
185 pub fn render_ascii(&self) -> String {
186 format!(
187 "{}: ({:.3}, {:.3}, {:.3})",
188 self.name, self.value.x, self.value.y, self.value.z
189 )
190 }
191}
192
193#[derive(Debug, Clone)]
195pub struct Vec4Field {
196 pub name: String,
197 pub value: Vec4,
198}
199
200impl Vec4Field {
201 pub fn new(name: impl Into<String>, value: Vec4) -> Self {
202 Self { name: name.into(), value }
203 }
204 pub fn set(&mut self, v: Vec4) {
205 self.value = v;
206 }
207 pub fn render_ascii(&self) -> String {
208 format!(
209 "{}: ({:.3}, {:.3}, {:.3}, {:.3})",
210 self.name,
211 self.value.x,
212 self.value.y,
213 self.value.z,
214 self.value.w,
215 )
216 }
217}
218
219#[derive(Debug, Clone)]
221pub struct ColorField {
222 pub name: String,
223 pub color: Vec4, pub popup_open: bool,
225 hue: f32,
227 saturation: f32,
228 brightness: f32,
229}
230
231impl ColorField {
232 pub fn new(name: impl Into<String>, color: Vec4) -> Self {
233 let (h, s, b) = rgb_to_hsv(color.x, color.y, color.z);
234 Self { name: name.into(), color, popup_open: false, hue: h, saturation: s, brightness: b }
235 }
236 pub fn set_rgb(&mut self, r: f32, g: f32, b_: f32) {
237 let (h, s, b) = rgb_to_hsv(r, g, b_);
238 self.color.x = r; self.color.y = g; self.color.z = b_;
239 self.hue = h; self.saturation = s; self.brightness = b;
240 }
241 pub fn set_hsv(&mut self, h: f32, s: f32, v: f32) {
242 let (r, g, b) = hsv_to_rgb(h, s, v);
243 self.hue = h; self.saturation = s; self.brightness = v;
244 self.color.x = r; self.color.y = g; self.color.z = b;
245 }
246 pub fn set_alpha(&mut self, a: f32) {
247 self.color.w = a.clamp(0.0, 1.0);
248 }
249 pub fn render_ascii(&self) -> String {
250 format!(
251 "{}: rgba({:.2},{:.2},{:.2},{:.2})",
252 self.name,
253 self.color.x,
254 self.color.y,
255 self.color.z,
256 self.color.w,
257 )
258 }
259 pub fn open_popup(&mut self) { self.popup_open = true; }
260 pub fn close_popup(&mut self) { self.popup_open = false; }
261 pub fn hue(&self) -> f32 { self.hue }
262 pub fn saturation(&self) -> f32 { self.saturation }
263 pub fn brightness(&self) -> f32 { self.brightness }
264}
265
266#[derive(Debug, Clone)]
268pub struct EnumField {
269 pub name: String,
270 pub variants: Vec<String>,
271 pub selected: usize,
272}
273
274impl EnumField {
275 pub fn new(name: impl Into<String>, variants: Vec<String>, selected: usize) -> Self {
276 let selected = selected.min(variants.len().saturating_sub(1));
277 Self { name: name.into(), variants, selected }
278 }
279 pub fn selected_name(&self) -> &str {
280 self.variants.get(self.selected).map(|s| s.as_str()).unwrap_or("")
281 }
282 pub fn next(&mut self) {
283 if !self.variants.is_empty() {
284 self.selected = (self.selected + 1) % self.variants.len();
285 }
286 }
287 pub fn prev(&mut self) {
288 if !self.variants.is_empty() {
289 self.selected = if self.selected == 0 { self.variants.len() - 1 } else { self.selected - 1 };
290 }
291 }
292 pub fn render_ascii(&self) -> String {
293 format!("{}: [{}]", self.name, self.selected_name())
294 }
295}
296
297#[derive(Debug, Clone)]
299pub struct SliderField {
300 pub name: String,
301 pub value: f32,
302 pub min: f32,
303 pub max: f32,
304 pub display_precision: usize,
305}
306
307impl SliderField {
308 pub fn new(name: impl Into<String>, value: f32, min: f32, max: f32) -> Self {
309 let value = value.clamp(min, max);
310 Self { name: name.into(), value, min, max, display_precision: 2 }
311 }
312 pub fn set(&mut self, v: f32) {
313 self.value = v.clamp(self.min, self.max);
314 }
315 pub fn normalized(&self) -> f32 {
317 if (self.max - self.min).abs() < f32::EPSILON { 0.0 }
318 else { (self.value - self.min) / (self.max - self.min) }
319 }
320 pub fn set_normalized(&mut self, t: f32) {
321 self.value = self.min + t.clamp(0.0, 1.0) * (self.max - self.min);
322 }
323 pub fn render_ascii(&self, bar_width: usize) -> String {
324 let filled = ((self.normalized() * bar_width as f32) as usize).min(bar_width);
325 let bar: String = std::iter::repeat('#').take(filled)
326 .chain(std::iter::repeat('-').take(bar_width - filled))
327 .collect();
328 format!("{}: [{}] {:.prec$}", self.name, bar, self.value, prec = self.display_precision)
329 }
330}
331
332#[derive(Debug, Clone)]
334pub struct AssetRefField {
335 pub name: String,
336 pub asset_type: String,
337 pub asset_path: Option<String>,
338 pub asset_id: Option<u64>,
339}
340
341impl AssetRefField {
342 pub fn new(name: impl Into<String>, asset_type: impl Into<String>) -> Self {
343 Self { name: name.into(), asset_type: asset_type.into(), asset_path: None, asset_id: None }
344 }
345 pub fn set_path(&mut self, path: impl Into<String>) {
346 self.asset_path = Some(path.into());
347 }
348 pub fn clear(&mut self) {
349 self.asset_path = None;
350 self.asset_id = None;
351 }
352 pub fn render_ascii(&self) -> String {
353 let val = self.asset_path.as_deref().unwrap_or("<none>");
354 format!("{} [{}]: {}", self.name, self.asset_type, val)
355 }
356}
357
358#[derive(Debug, Clone)]
360pub struct ScriptField {
361 pub name: String,
362 pub script_path: Option<String>,
363 pub inline_source: String,
364 pub bound_globals: HashMap<String, String>,
365}
366
367impl ScriptField {
368 pub fn new(name: impl Into<String>) -> Self {
369 Self {
370 name: name.into(),
371 script_path: None,
372 inline_source: String::new(),
373 bound_globals: HashMap::new(),
374 }
375 }
376 pub fn set_global(&mut self, key: impl Into<String>, value: impl Into<String>) {
377 self.bound_globals.insert(key.into(), value.into());
378 }
379 pub fn render_ascii(&self) -> String {
380 let src = self.script_path.as_deref().unwrap_or("<inline>");
381 format!("{}: {}", self.name, src)
382 }
383}
384
385#[derive(Debug, Clone)]
387pub struct ListField {
388 pub name: String,
389 pub items: Vec<String>, pub selected_index: Option<usize>,
391 pub collapsed: bool,
392}
393
394impl ListField {
395 pub fn new(name: impl Into<String>) -> Self {
396 Self { name: name.into(), items: Vec::new(), selected_index: None, collapsed: false }
397 }
398 pub fn push(&mut self, item: impl Into<String>) {
399 self.items.push(item.into());
400 }
401 pub fn remove_selected(&mut self) {
402 if let Some(i) = self.selected_index {
403 if i < self.items.len() {
404 self.items.remove(i);
405 self.selected_index = if self.items.is_empty() {
406 None
407 } else {
408 Some(i.min(self.items.len() - 1))
409 };
410 }
411 }
412 }
413 pub fn render_ascii(&self) -> String {
414 let mut out = format!("{}:\n", self.name);
415 for (i, item) in self.items.iter().enumerate() {
416 let cursor = if self.selected_index == Some(i) { ">" } else { " " };
417 out.push_str(&format!(" {}{}: {}\n", cursor, i, item));
418 }
419 out
420 }
421}
422
423#[derive(Debug, Clone)]
425pub struct MapField {
426 pub name: String,
427 pub entries: Vec<(String, String)>,
428 pub collapsed: bool,
429}
430
431impl MapField {
432 pub fn new(name: impl Into<String>) -> Self {
433 Self { name: name.into(), entries: Vec::new(), collapsed: false }
434 }
435 pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
436 let k: String = key.into();
437 if let Some(e) = self.entries.iter_mut().find(|(ek, _)| ek == &k) {
438 e.1 = value.into();
439 } else {
440 self.entries.push((k, value.into()));
441 }
442 }
443 pub fn remove(&mut self, key: &str) {
444 self.entries.retain(|(k, _)| k != key);
445 }
446 pub fn get(&self, key: &str) -> Option<&str> {
447 self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str())
448 }
449 pub fn render_ascii(&self) -> String {
450 let mut out = format!("{}:\n", self.name);
451 for (k, v) in &self.entries {
452 out.push_str(&format!(" {} = {}\n", k, v));
453 }
454 out
455 }
456}
457
458#[derive(Debug, Clone)]
463pub enum InspectorEntry {
464 Bool(BoolField),
465 Int(IntField),
466 Float(FloatField),
467 String(StringField),
468 Vec2(Vec2Field),
469 Vec3(Vec3Field),
470 Vec4(Vec4Field),
471 Color(ColorField),
472 Enum(EnumField),
473 Slider(SliderField),
474 AssetRef(AssetRefField),
475 Script(ScriptField),
476 List(ListField),
477 Map(MapField),
478 Separator,
479 Label(std::string::String),
480}
481
482impl InspectorEntry {
483 pub fn name(&self) -> &str {
484 match self {
485 InspectorEntry::Bool(f) => &f.name,
486 InspectorEntry::Int(f) => &f.name,
487 InspectorEntry::Float(f) => &f.name,
488 InspectorEntry::String(f) => &f.name,
489 InspectorEntry::Vec2(f) => &f.name,
490 InspectorEntry::Vec3(f) => &f.name,
491 InspectorEntry::Vec4(f) => &f.name,
492 InspectorEntry::Color(f) => &f.name,
493 InspectorEntry::Enum(f) => &f.name,
494 InspectorEntry::Slider(f) => &f.name,
495 InspectorEntry::AssetRef(f) => &f.name,
496 InspectorEntry::Script(f) => &f.name,
497 InspectorEntry::List(f) => &f.name,
498 InspectorEntry::Map(f) => &f.name,
499 InspectorEntry::Separator => "",
500 InspectorEntry::Label(s) => s,
501 }
502 }
503
504 pub fn render_ascii(&self) -> String {
506 match self {
507 InspectorEntry::Bool(f) => f.render_ascii(),
508 InspectorEntry::Int(f) => f.render_ascii(),
509 InspectorEntry::Float(f) => f.render_ascii(),
510 InspectorEntry::String(f) => f.render_ascii(),
511 InspectorEntry::Vec2(f) => f.render_ascii(),
512 InspectorEntry::Vec3(f) => f.render_ascii(),
513 InspectorEntry::Vec4(f) => f.render_ascii(),
514 InspectorEntry::Color(f) => f.render_ascii(),
515 InspectorEntry::Enum(f) => f.render_ascii(),
516 InspectorEntry::Slider(f) => f.render_ascii(20),
517 InspectorEntry::AssetRef(f) => f.render_ascii(),
518 InspectorEntry::Script(f) => f.render_ascii(),
519 InspectorEntry::List(f) => f.render_ascii(),
520 InspectorEntry::Map(f) => f.render_ascii(),
521 InspectorEntry::Separator => "────────────────────────────────────".into(),
522 InspectorEntry::Label(s) => s.clone(),
523 }
524 }
525
526 pub fn matches_search(&self, query: &str) -> bool {
527 if query.is_empty() { return true; }
528 self.name().to_lowercase().contains(&query.to_lowercase())
529 }
530}
531
532pub trait Inspectable {
538 fn inspect(&self) -> Vec<PropertyGroup>;
540 fn apply_edit(&mut self, entry_name: &str, serialized_value: &str) -> Result<(), String>;
542 fn type_name(&self) -> &'static str;
544}
545
546#[derive(Debug, Clone)]
552pub struct PropertyGroup {
553 pub name: String,
554 pub entries: Vec<InspectorEntry>,
555 pub collapsed: bool,
556 pub icon: char,
557}
558
559impl PropertyGroup {
560 pub fn new(name: impl Into<String>) -> Self {
561 Self { name: name.into(), entries: Vec::new(), collapsed: false, icon: '▼' }
562 }
563 pub fn with_icon(mut self, icon: char) -> Self {
564 self.icon = icon; self
565 }
566 pub fn add(mut self, entry: InspectorEntry) -> Self {
567 self.entries.push(entry); self
568 }
569 pub fn push(&mut self, entry: InspectorEntry) {
570 self.entries.push(entry);
571 }
572 pub fn toggle_collapsed(&mut self) {
573 self.collapsed = !self.collapsed;
574 self.icon = if self.collapsed { '▶' } else { '▼' };
575 }
576 pub fn visible_entries(&self) -> &[InspectorEntry] {
577 if self.collapsed { &[] } else { &self.entries }
578 }
579 pub fn render_ascii(&self) -> String {
580 let mut out = format!("{} {}\n", self.icon, self.name);
581 if !self.collapsed {
582 for e in &self.entries {
583 out.push_str(" ");
584 out.push_str(&e.render_ascii());
585 out.push('\n');
586 }
587 }
588 out
589 }
590 pub fn filter(&self, query: &str) -> Vec<&InspectorEntry> {
591 self.entries.iter().filter(|e| e.matches_search(query)).collect()
592 }
593}
594
595#[derive(Debug, Clone, Default)]
601pub struct InspectorContext {
602 pub dirty_properties: Vec<String>,
603 pub validation_errors: HashMap<String, String>,
604 pub clipboard: Option<ClipboardEntry>,
605 pub undo_stack: Vec<(String, String, String)>, }
607
608#[derive(Debug, Clone)]
610pub struct ClipboardEntry {
611 pub source_type: String,
612 pub property_name: String,
613 pub serialized_value: String,
614}
615
616impl InspectorContext {
617 pub fn new() -> Self { Self::default() }
618
619 pub fn mark_dirty(&mut self, name: impl Into<String>) {
620 let n = name.into();
621 if !self.dirty_properties.contains(&n) {
622 self.dirty_properties.push(n);
623 }
624 }
625
626 pub fn clear_dirty(&mut self) {
627 self.dirty_properties.clear();
628 }
629
630 pub fn is_dirty(&self, name: &str) -> bool {
631 self.dirty_properties.contains(&name.to_string())
632 }
633
634 pub fn add_validation_error(&mut self, prop: impl Into<String>, msg: impl Into<String>) {
635 self.validation_errors.insert(prop.into(), msg.into());
636 }
637
638 pub fn clear_validation_error(&mut self, prop: &str) {
639 self.validation_errors.remove(prop);
640 }
641
642 pub fn has_errors(&self) -> bool {
643 !self.validation_errors.is_empty()
644 }
645
646 pub fn copy_value(&mut self, entry: &InspectorEntry, source_type: impl Into<String>) {
647 self.clipboard = Some(ClipboardEntry {
648 source_type: source_type.into(),
649 property_name: entry.name().to_string(),
650 serialized_value: entry.render_ascii(),
651 });
652 }
653
654 pub fn paste_value(&self) -> Option<&ClipboardEntry> {
655 self.clipboard.as_ref()
656 }
657}
658
659#[derive(Debug, Clone, Default)]
665pub struct SearchBar {
666 pub query: String,
667 pub focused: bool,
668 pub case_sensitive: bool,
669}
670
671impl SearchBar {
672 pub fn new() -> Self { Self::default() }
673
674 pub fn push(&mut self, c: char) {
675 self.query.push(c);
676 }
677 pub fn pop(&mut self) {
678 self.query.pop();
679 }
680 pub fn clear(&mut self) {
681 self.query.clear();
682 }
683 pub fn matches(&self, text: &str) -> bool {
684 if self.query.is_empty() { return true; }
685 if self.case_sensitive {
686 text.contains(&self.query)
687 } else {
688 text.to_lowercase().contains(&self.query.to_lowercase())
689 }
690 }
691 pub fn render_ascii(&self) -> String {
692 format!("Search: [{}{}]", self.query, if self.focused { "|" } else { "" })
693 }
694}
695
696#[derive(Debug, Clone)]
702pub struct TransformInspector {
703 pub position: Vec3Field,
704 pub rotation: Vec3Field, pub scale: Vec3Field,
706 pub snap_position: f32,
707 pub snap_rotation: f32,
708 pub snap_scale: f32,
709 pub snap_enabled: bool,
710 pub local_space: bool,
711}
712
713impl TransformInspector {
714 pub fn new(pos: Vec3, rot: Vec3, scale: Vec3) -> Self {
715 Self {
716 position: Vec3Field::new("Position", pos),
717 rotation: Vec3Field::new("Rotation", rot),
718 scale: Vec3Field::new("Scale", scale),
719 snap_position: 0.25,
720 snap_rotation: 15.0,
721 snap_scale: 0.1,
722 snap_enabled: false,
723 local_space: true,
724 }
725 }
726
727 pub fn apply_snap_position(&mut self) {
728 if !self.snap_enabled { return; }
729 let s = self.snap_position;
730 let p = self.position.value;
731 self.position.set(Vec3::new(
732 (p.x / s).round() * s,
733 (p.y / s).round() * s,
734 (p.z / s).round() * s,
735 ));
736 }
737
738 pub fn apply_snap_rotation(&mut self) {
739 if !self.snap_enabled { return; }
740 let s = self.snap_rotation;
741 let r = self.rotation.value;
742 self.rotation.set(Vec3::new(
743 (r.x / s).round() * s,
744 (r.y / s).round() * s,
745 (r.z / s).round() * s,
746 ));
747 }
748
749 pub fn to_property_group(&self) -> PropertyGroup {
750 let mut g = PropertyGroup::new("Transform").with_icon('↔');
751 g.push(InspectorEntry::Vec3(self.position.clone()));
752 g.push(InspectorEntry::Vec3(self.rotation.clone()));
753 g.push(InspectorEntry::Vec3(self.scale.clone()));
754 g.push(InspectorEntry::Bool(BoolField::new("Snap", self.snap_enabled)));
755 g
756 }
757}
758
759#[derive(Debug, Clone)]
765pub struct GlyphInspector {
766 pub character: StringField,
767 pub color: ColorField,
768 pub emission: SliderField,
769 pub glow_color: ColorField,
770 pub glow_radius: FloatField,
771 pub layer: EnumField,
772 pub blend_mode: EnumField,
773 pub mass: FloatField,
774 pub charge: FloatField,
775 pub temperature: FloatField,
776 pub entropy: SliderField,
777}
778
779impl GlyphInspector {
780 pub fn new() -> Self {
781 Self {
782 character: StringField::new("Character", "A"),
783 color: ColorField::new("Color", Vec4::ONE),
784 emission: SliderField::new("Emission", 0.0, 0.0, 2.0),
785 glow_color: ColorField::new("Glow Color", Vec4::new(1.0, 0.8, 0.2, 1.0)),
786 glow_radius: FloatField::new("Glow Radius", 1.0).with_range(0.0, 20.0),
787 layer: EnumField::new(
788 "Layer",
789 vec!["Background".into(), "World".into(), "Entity".into(),
790 "Particle".into(), "UI".into(), "Overlay".into()],
791 2,
792 ),
793 blend_mode: EnumField::new(
794 "Blend Mode",
795 vec!["Normal".into(), "Additive".into(), "Multiply".into(), "Screen".into()],
796 0,
797 ),
798 mass: FloatField::new("Mass", 1.0).with_range(0.001, 1000.0),
799 charge: FloatField::new("Charge", 0.0).with_range(-10.0, 10.0),
800 temperature: FloatField::new("Temperature", 0.0).with_range(0.0, 10000.0),
801 entropy: SliderField::new("Entropy", 0.0, 0.0, 1.0),
802 }
803 }
804
805 pub fn to_property_group(&self) -> PropertyGroup {
806 let mut g = PropertyGroup::new("Glyph").with_icon('✦');
807 g.push(InspectorEntry::String(self.character.clone()));
808 g.push(InspectorEntry::Color(self.color.clone()));
809 g.push(InspectorEntry::Slider(self.emission.clone()));
810 g.push(InspectorEntry::Color(self.glow_color.clone()));
811 g.push(InspectorEntry::Float(self.glow_radius.clone()));
812 g.push(InspectorEntry::Enum(self.layer.clone()));
813 g.push(InspectorEntry::Enum(self.blend_mode.clone()));
814 g.push(InspectorEntry::Separator);
815 g.push(InspectorEntry::Float(self.mass.clone()));
816 g.push(InspectorEntry::Float(self.charge.clone()));
817 g.push(InspectorEntry::Float(self.temperature.clone()));
818 g.push(InspectorEntry::Slider(self.entropy.clone()));
819 g
820 }
821}
822
823impl Default for GlyphInspector {
824 fn default() -> Self { Self::new() }
825}
826
827#[derive(Debug, Clone)]
833pub struct ForceFieldInspector {
834 pub field_type: EnumField,
835 pub strength: FloatField,
836 pub radius: FloatField,
837 pub falloff: EnumField,
838 pub position: Vec3Field,
839 pub direction: Vec3Field,
840 pub enabled: BoolField,
841 pub frequency: FloatField,
842 pub amplitude: FloatField,
843 pub phase: FloatField,
844}
845
846impl ForceFieldInspector {
847 pub fn new() -> Self {
848 Self {
849 field_type: EnumField::new(
850 "Type",
851 vec!["Gravity".into(), "Repulsion".into(), "Vortex".into(),
852 "Attractor".into(), "Wind".into(), "Turbulence".into(),
853 "Drag".into(), "Custom".into()],
854 0,
855 ),
856 strength: FloatField::new("Strength", 1.0).with_range(-1000.0, 1000.0),
857 radius: FloatField::new("Radius", 5.0).with_range(0.0, 500.0),
858 falloff: EnumField::new(
859 "Falloff",
860 vec!["None".into(), "Linear".into(), "InvSq".into(), "Exp".into()],
861 1,
862 ),
863 position: Vec3Field::new("Position", Vec3::ZERO),
864 direction: Vec3Field::new("Direction", Vec3::new(0.0, -1.0, 0.0)),
865 enabled: BoolField::new("Enabled", true),
866 frequency: FloatField::new("Frequency", 1.0).with_range(0.0, 100.0),
867 amplitude: FloatField::new("Amplitude", 1.0).with_range(0.0, 100.0),
868 phase: FloatField::new("Phase", 0.0).with_range(0.0, std::f64::consts::TAU),
869 }
870 }
871
872 pub fn to_property_group(&self) -> PropertyGroup {
873 let mut g = PropertyGroup::new("Force Field").with_icon('⊛');
874 g.push(InspectorEntry::Bool(self.enabled.clone()));
875 g.push(InspectorEntry::Enum(self.field_type.clone()));
876 g.push(InspectorEntry::Float(self.strength.clone()));
877 g.push(InspectorEntry::Float(self.radius.clone()));
878 g.push(InspectorEntry::Enum(self.falloff.clone()));
879 g.push(InspectorEntry::Vec3(self.position.clone()));
880 g.push(InspectorEntry::Vec3(self.direction.clone()));
881 g.push(InspectorEntry::Separator);
882 g.push(InspectorEntry::Float(self.frequency.clone()));
883 g.push(InspectorEntry::Float(self.amplitude.clone()));
884 g.push(InspectorEntry::Float(self.phase.clone()));
885 g
886 }
887}
888
889impl Default for ForceFieldInspector {
890 fn default() -> Self { Self::new() }
891}
892
893#[derive(Debug, Clone)]
899pub struct ParticleInspector {
900 pub preset: EnumField,
901 pub origin: Vec3Field,
902 pub active_count: IntField,
903 pub emit_rate: FloatField,
904 pub lifetime: FloatField,
905 pub speed: FloatField,
906 pub spread_angle: SliderField,
907 pub gravity_scale: FloatField,
908 pub color_start: ColorField,
909 pub color_end: ColorField,
910 pub size_start: FloatField,
911 pub size_end: FloatField,
912 pub emitting: BoolField,
913}
914
915impl ParticleInspector {
916 pub fn new() -> Self {
917 Self {
918 preset: EnumField::new(
919 "Preset",
920 vec!["Explosion".into(), "Fire".into(), "Smoke".into(),
921 "Sparkle".into(), "Rain".into(), "Custom".into()],
922 0,
923 ),
924 origin: Vec3Field::new("Origin", Vec3::ZERO),
925 active_count: IntField::new("Active Particles", 0).with_range(0, 100_000),
926 emit_rate: FloatField::new("Emit Rate", 50.0).with_range(0.0, 10000.0),
927 lifetime: FloatField::new("Lifetime", 2.0).with_range(0.0, 60.0),
928 speed: FloatField::new("Speed", 5.0).with_range(0.0, 500.0),
929 spread_angle: SliderField::new("Spread Angle", 45.0, 0.0, 360.0),
930 gravity_scale: FloatField::new("Gravity Scale", 1.0).with_range(-10.0, 10.0),
931 color_start: ColorField::new("Color Start", Vec4::new(1.0, 0.8, 0.2, 1.0)),
932 color_end: ColorField::new("Color End", Vec4::new(1.0, 0.0, 0.0, 0.0)),
933 size_start: FloatField::new("Size Start", 1.0).with_range(0.01, 10.0),
934 size_end: FloatField::new("Size End", 0.0).with_range(0.0, 10.0),
935 emitting: BoolField::new("Emitting", true),
936 }
937 }
938
939 pub fn to_property_group(&self) -> PropertyGroup {
940 let mut g = PropertyGroup::new("Particle Emitter").with_icon('✦');
941 g.push(InspectorEntry::Bool(self.emitting.clone()));
942 g.push(InspectorEntry::Enum(self.preset.clone()));
943 g.push(InspectorEntry::Vec3(self.origin.clone()));
944 g.push(InspectorEntry::Int(self.active_count.clone()));
945 g.push(InspectorEntry::Float(self.emit_rate.clone()));
946 g.push(InspectorEntry::Float(self.lifetime.clone()));
947 g.push(InspectorEntry::Float(self.speed.clone()));
948 g.push(InspectorEntry::Slider(self.spread_angle.clone()));
949 g.push(InspectorEntry::Float(self.gravity_scale.clone()));
950 g.push(InspectorEntry::Separator);
951 g.push(InspectorEntry::Color(self.color_start.clone()));
952 g.push(InspectorEntry::Color(self.color_end.clone()));
953 g.push(InspectorEntry::Float(self.size_start.clone()));
954 g.push(InspectorEntry::Float(self.size_end.clone()));
955 g
956 }
957}
958
959impl Default for ParticleInspector {
960 fn default() -> Self { Self::new() }
961}
962
963#[derive(Debug, Clone)]
969pub struct ScriptInspector {
970 pub script: ScriptField,
971 pub globals: MapField,
972 pub enabled: BoolField,
973 pub log_output: ListField,
974}
975
976impl ScriptInspector {
977 pub fn new() -> Self {
978 Self {
979 script: ScriptField::new("Script"),
980 globals: MapField::new("Globals"),
981 enabled: BoolField::new("Enabled", true),
982 log_output: ListField::new("Output"),
983 }
984 }
985
986 pub fn to_property_group(&self) -> PropertyGroup {
987 let mut g = PropertyGroup::new("Script").with_icon('⌨');
988 g.push(InspectorEntry::Bool(self.enabled.clone()));
989 g.push(InspectorEntry::Script(self.script.clone()));
990 g.push(InspectorEntry::Map(self.globals.clone()));
991 g.push(InspectorEntry::List(self.log_output.clone()));
992 g
993 }
994
995 pub fn log(&mut self, msg: impl Into<String>) {
996 self.log_output.push(msg);
997 }
998
999 pub fn set_global(&mut self, key: impl Into<String>, val: impl Into<String>) {
1000 self.globals.insert(key, val);
1001 self.script.set_global(
1002 self.globals.entries.last().map(|(k, _)| k.as_str()).unwrap_or(""),
1003 self.globals.entries.last().map(|(_, v)| v.as_str()).unwrap_or(""),
1004 );
1005 }
1006}
1007
1008impl Default for ScriptInspector {
1009 fn default() -> Self { Self::new() }
1010}
1011
1012#[derive(Debug, Clone)]
1018pub struct ComponentInspector {
1019 pub entity_name: String,
1020 pub entity_id: u32,
1021 pub transform: TransformInspector,
1022 pub glyph: Option<GlyphInspector>,
1023 pub force_field: Option<ForceFieldInspector>,
1024 pub particle: Option<ParticleInspector>,
1025 pub script: Option<ScriptInspector>,
1026 pub custom_groups: Vec<PropertyGroup>,
1027}
1028
1029impl ComponentInspector {
1030 pub fn new(name: impl Into<String>, id: u32) -> Self {
1031 Self {
1032 entity_name: name.into(),
1033 entity_id: id,
1034 transform: TransformInspector::new(Vec3::ZERO, Vec3::ZERO, Vec3::ONE),
1035 glyph: None,
1036 force_field: None,
1037 particle: None,
1038 script: None,
1039 custom_groups: Vec::new(),
1040 }
1041 }
1042
1043 pub fn all_groups(&self) -> Vec<PropertyGroup> {
1044 let mut groups = vec![self.transform.to_property_group()];
1045 if let Some(ref g) = self.glyph { groups.push(g.to_property_group()); }
1046 if let Some(ref f) = self.force_field { groups.push(f.to_property_group()); }
1047 if let Some(ref p) = self.particle { groups.push(p.to_property_group()); }
1048 if let Some(ref s) = self.script { groups.push(s.to_property_group()); }
1049 groups.extend(self.custom_groups.iter().cloned());
1050 groups
1051 }
1052
1053 pub fn add_custom_group(&mut self, group: PropertyGroup) {
1054 self.custom_groups.push(group);
1055 }
1056
1057 pub fn render_ascii(&self, search: &SearchBar) -> String {
1058 let mut out = format!("┌── Entity: {} (id={})\n", self.entity_name, self.entity_id);
1059 for group in self.all_groups() {
1060 let filtered: Vec<_> = group.filter(&search.query);
1061 if !search.query.is_empty() && filtered.is_empty() { continue; }
1062 out.push_str(&group.render_ascii());
1063 }
1064 out.push_str("└──────────────────────────────\n");
1065 out
1066 }
1067}
1068
1069pub struct Inspector {
1075 pub context: InspectorContext,
1076 pub search: SearchBar,
1077 pub component_inspector: Option<ComponentInspector>,
1078 pub scroll_offset: f32,
1079 pub width: f32,
1080 pub height: f32,
1081}
1082
1083impl Inspector {
1084 pub fn new(width: f32, height: f32) -> Self {
1085 Self {
1086 context: InspectorContext::new(),
1087 search: SearchBar::new(),
1088 component_inspector: None,
1089 scroll_offset: 0.0,
1090 width,
1091 height,
1092 }
1093 }
1094
1095 pub fn load_entity(&mut self, id: u32, name: impl Into<String>) {
1097 self.component_inspector = Some(ComponentInspector::new(name, id));
1098 self.scroll_offset = 0.0;
1099 self.context.clear_dirty();
1100 }
1101
1102 pub fn clear(&mut self) {
1103 self.component_inspector = None;
1104 self.context.clear_dirty();
1105 }
1106
1107 pub fn scroll(&mut self, delta: f32) {
1108 self.scroll_offset = (self.scroll_offset + delta).max(0.0);
1109 }
1110
1111 pub fn render_ascii(&self) -> String {
1113 let mut out = String::new();
1114 out.push_str(&self.search.render_ascii());
1115 out.push('\n');
1116 match &self.component_inspector {
1117 Some(ci) => out.push_str(&ci.render_ascii(&self.search)),
1118 None => out.push_str("(no selection)\n"),
1119 }
1120 out
1121 }
1122
1123 pub fn copy_property(&mut self, entry: &InspectorEntry) {
1125 let type_name = self
1126 .component_inspector
1127 .as_ref()
1128 .map(|ci| ci.entity_name.as_str())
1129 .unwrap_or("unknown");
1130 self.context.copy_value(entry, type_name);
1131 }
1132}
1133
1134fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
1139 let max = r.max(g).max(b);
1140 let min = r.min(g).min(b);
1141 let delta = max - min;
1142 let h = if delta < f32::EPSILON {
1143 0.0
1144 } else if max == r {
1145 60.0 * (((g - b) / delta) % 6.0)
1146 } else if max == g {
1147 60.0 * ((b - r) / delta + 2.0)
1148 } else {
1149 60.0 * ((r - g) / delta + 4.0)
1150 };
1151 let h = if h < 0.0 { h + 360.0 } else { h };
1152 let s = if max < f32::EPSILON { 0.0 } else { delta / max };
1153 let v = max;
1154 (h, s, v)
1155}
1156
1157fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) {
1158 if s < f32::EPSILON {
1159 return (v, v, v);
1160 }
1161 let hi = ((h / 60.0) as i32) % 6;
1162 let f = h / 60.0 - (h / 60.0).floor();
1163 let p = v * (1.0 - s);
1164 let q = v * (1.0 - f * s);
1165 let t = v * (1.0 - (1.0 - f) * s);
1166 match hi {
1167 0 => (v, t, p),
1168 1 => (q, v, p),
1169 2 => (p, v, t),
1170 3 => (p, q, v),
1171 4 => (t, p, v),
1172 _ => (v, p, q),
1173 }
1174}
1175
1176#[cfg(test)]
1181mod tests {
1182 use super::*;
1183
1184 #[test]
1185 fn test_bool_field_toggle() {
1186 let mut f = BoolField::new("visible", true);
1187 assert!(f.value);
1188 f.toggle();
1189 assert!(!f.value);
1190 f.toggle();
1191 assert!(f.value);
1192 }
1193
1194 #[test]
1195 fn test_int_field_clamp() {
1196 let mut f = IntField::new("count", 5).with_range(0, 10);
1197 f.set(15);
1198 assert_eq!(f.value, 10);
1199 f.set(-3);
1200 assert_eq!(f.value, 0);
1201 }
1202
1203 #[test]
1204 fn test_float_field_set() {
1205 let mut f = FloatField::new("speed", 1.0).with_range(0.0, 10.0);
1206 f.set(5.5);
1207 assert!((f.value - 5.5).abs() < 1e-10);
1208 f.set(20.0);
1209 assert!((f.value - 10.0).abs() < 1e-10);
1210 }
1211
1212 #[test]
1213 fn test_slider_normalized() {
1214 let mut s = SliderField::new("vol", 5.0, 0.0, 10.0);
1215 assert!((s.normalized() - 0.5).abs() < 1e-6);
1216 s.set_normalized(1.0);
1217 assert!((s.value - 10.0).abs() < 1e-6);
1218 }
1219
1220 #[test]
1221 fn test_enum_cycle() {
1222 let mut e = EnumField::new("layer", vec!["A".into(), "B".into(), "C".into()], 0);
1223 e.next();
1224 assert_eq!(e.selected_name(), "B");
1225 e.prev();
1226 assert_eq!(e.selected_name(), "A");
1227 e.prev(); assert_eq!(e.selected_name(), "C");
1229 }
1230
1231 #[test]
1232 fn test_color_field_hsv_roundtrip() {
1233 let mut c = ColorField::new("col", Vec4::new(1.0, 0.0, 0.0, 1.0));
1234 let (h, s, v) = (c.hue(), c.saturation(), c.brightness());
1235 c.set_hsv(h, s, v);
1236 assert!((c.color.x - 1.0).abs() < 0.01);
1237 assert!((c.color.y).abs() < 0.01);
1238 }
1239
1240 #[test]
1241 fn test_string_field_max_len() {
1242 let mut f = StringField::new("name", "");
1243 f.max_len = 5;
1244 f.set("hello world");
1245 assert_eq!(f.value, "hello");
1246 }
1247
1248 #[test]
1249 fn test_list_field_remove() {
1250 let mut l = ListField::new("items");
1251 l.push("a");
1252 l.push("b");
1253 l.push("c");
1254 l.selected_index = Some(1);
1255 l.remove_selected();
1256 assert_eq!(l.items, vec!["a", "c"]);
1257 }
1258
1259 #[test]
1260 fn test_map_field_insert_update() {
1261 let mut m = MapField::new("props");
1262 m.insert("x", "1");
1263 m.insert("y", "2");
1264 m.insert("x", "99");
1265 assert_eq!(m.get("x"), Some("99"));
1266 assert_eq!(m.entries.len(), 2);
1267 }
1268
1269 #[test]
1270 fn test_search_bar_filter() {
1271 let mut s = SearchBar::new();
1272 s.push('p');
1273 s.push('o');
1274 s.push('s');
1275 assert!(s.matches("position"));
1276 assert!(!s.matches("rotation"));
1277 }
1278
1279 #[test]
1280 fn test_property_group_collapse() {
1281 let mut g = PropertyGroup::new("Transform")
1282 .add(InspectorEntry::Bool(BoolField::new("visible", true)));
1283 assert_eq!(g.visible_entries().len(), 1);
1284 g.toggle_collapsed();
1285 assert_eq!(g.visible_entries().len(), 0);
1286 }
1287
1288 #[test]
1289 fn test_component_inspector_groups() {
1290 let mut ci = ComponentInspector::new("Hero", 1);
1291 ci.glyph = Some(GlyphInspector::new());
1292 let groups = ci.all_groups();
1293 assert!(groups.len() >= 2);
1294 }
1295
1296 #[test]
1297 fn test_inspector_context_dirty() {
1298 let mut ctx = InspectorContext::new();
1299 ctx.mark_dirty("position");
1300 assert!(ctx.is_dirty("position"));
1301 ctx.clear_dirty();
1302 assert!(!ctx.is_dirty("position"));
1303 }
1304
1305 #[test]
1306 fn test_vec3_field_clamping() {
1307 let mut f = Vec3Field::new("pos", Vec3::ZERO);
1308 f.min = Some(-5.0);
1309 f.max = Some(5.0);
1310 f.set(Vec3::new(10.0, -10.0, 3.0));
1311 assert!((f.value.x - 5.0).abs() < 1e-6);
1312 assert!((f.value.y + 5.0).abs() < 1e-6);
1313 }
1314
1315 #[test]
1316 fn test_transform_inspector_snap() {
1317 let mut ti = TransformInspector::new(
1318 Vec3::new(0.3, 0.7, -0.1),
1319 Vec3::ZERO,
1320 Vec3::ONE,
1321 );
1322 ti.snap_enabled = true;
1323 ti.snap_position = 0.25;
1324 ti.apply_snap_position();
1325 let p = ti.position.value;
1326 assert!((p.x - 0.25).abs() < 1e-5);
1327 assert!((p.y - 0.75).abs() < 1e-5);
1328 assert!((p.z).abs() < 1e-5);
1329 }
1330}