1 module re.ng.diag.inspector;
2 
3 import re.core;
4 import re.ecs;
5 import re.math;
6 import re.gfx;
7 import std.conv;
8 import std.array;
9 import std.algorithm;
10 import std.typecons;
11 import std..string;
12 import re.util.interop;
13 import witchcraft;
14 static import raylib;
15 static import raygui;
16 
17 /// real-time object inspector
18 debug class Inspector {
19     /// panel width
20     public int width = 400;
21     /// whether the inspector is open
22     public bool open = false;
23     private Vector2 _panel_scroll;
24     private InspectedObject[] _inspected;
25     private Entity entity;
26     private enum btn_close = ['x', '\0'];
27 
28     private class InspectedObject {
29         public ReflectableObject obj;
30         public Class obj_class;
31         public string[string] fields;
32 
33         this(ReflectableObject obj) {
34             this.obj = obj;
35             this.obj_class = obj.getMetaType;
36         }
37 
38         private void update_fields() {
39             foreach (field; obj_class.getFields) {
40                 string field_name = field.getName;
41                 string field_value = to!string(field.get(obj));
42                 this.fields[field_name] = field_value;
43             }
44         }
45     }
46 
47     this() {
48         reset();
49     }
50 
51     private void reset() {
52         _inspected = [];
53         entity = null;
54     }
55 
56     public void update() {
57         // update all inspected components
58         foreach (comp; _inspected) {
59             comp.update_fields();
60         }
61     }
62 
63     public void render() {
64         alias pad = Core.debugger.screen_padding;
65 
66         // this is the (clipped) scrollable panel bounds
67         auto panel_bounds = Rectangle(pad, pad, width, Core.window.height - pad * 2);
68         // draw indicator of panel bounds
69         // raylib.DrawRectangleRec(panel_bounds, Colors.GRAY);
70 
71         // - layout vars
72         enum field_height = 16; // for each field
73         enum field_value_text_size = 12;
74         enum field_padding = 2;
75         enum field_label_width = 120;
76         const auto field_value_width = width - 40;
77         enum header_height = field_height; // for each component
78         enum header_padding = 4;
79         enum header_line_margin = 4;
80         enum title_height = field_height; // for each entity
81         enum title_padding = 8;
82         enum props_count = 1; // transform
83 
84         // calculate panel bounds
85         // this is going to calculate the space required for each component
86         int[] component_section_heights;
87 
88         foreach (comp; _inspected) {
89             component_section_heights ~= (header_padding + header_padding) // header
90              + ((field_height + field_padding) // field and padding
91                      * ((cast(int) comp.fields.length) + 1)); // number of fields
92         }
93         enum title_offset = title_height + title_padding;
94         enum props_offset = (props_count * (field_height + field_padding));
95         // total height
96         auto panel_bounds_height = pad + component_section_heights.sum() + (
97                 title_offset) // title
98          + props_offset; // props
99 
100         // bounds of the entire panel
101         auto panel_content_bounds = Rectangle(0, 0, width - pad, panel_bounds_height);
102 
103         auto view = raygui.GuiScrollPanel(panel_bounds, panel_content_bounds, &_panel_scroll);
104 
105         // start scissor
106         raylib.BeginScissorMode(cast(int) view.x, cast(int) view.y,
107                 cast(int) view.width, cast(int) view.height);
108         // end scissor on scope exit
109         scope (exit)
110             raylib.EndScissorMode();
111 
112         // close button
113         enum btn_close_sz = 12;
114         if (raygui.GuiButton(Rectangle(panel_bounds.x + panel_content_bounds.width - pad,
115                 panel_bounds.y + pad, btn_close_sz, btn_close_sz), cast(char*) btn_close)) {
116             close();
117             return; // when closed, cancel this render
118         }
119 
120         // the corner of the inside of the panel (pre-padded)
121         auto panel_corner = Vector2(panel_bounds.x + pad, panel_bounds.y + pad);
122 
123         // entity title
124         auto entity_title = format("Entity %s", entity.name);
125         raygui.GuiLabel(Rectangle(panel_corner.x, panel_corner.y,
126                 field_label_width, title_height), entity_title.c_str());
127         // title underline
128         raylib.DrawRectangleLinesEx(Rectangle(panel_corner.x,
129                 panel_corner.y + title_height, panel_bounds.width - pad * 2, 4), 1, Colors.GRAY);
130 
131         // entity props
132         // 1. transform
133         auto entity_transform = to!string(entity.transform);
134         raygui.GuiLabel(Rectangle(panel_corner.x, panel_corner.y + title_offset,
135                 field_label_width, title_height), "transform".c_str());
136         raygui.GuiTextBox(Rectangle(panel_corner.x + field_label_width, panel_corner.y + title_offset, field_value_width,
137                 field_height), entity_transform.c_str(), field_value_text_size, false);
138 
139         // - now draw each component section
140         enum first_panel_offset = title_offset + props_offset;
141         auto panel_y_offset = first_panel_offset; // the offset from the y start of the panel (this is based on component index)
142         foreach (i, comp; _inspected) {
143             auto field_names = comp.fields.keys.sort();
144             auto field_index = 0;
145 
146             // corner for the start of this section
147             auto section_corner = Vector2(panel_corner.x, panel_corner.y + panel_y_offset);
148             // header
149             raygui.GuiLabel(Rectangle(section_corner.x, section_corner.y,
150                     field_label_width, header_height), comp.obj_class.getName.c_str());
151             // header underline
152             raylib.DrawRectangleLinesEx(Rectangle(section_corner.x + header_line_margin,
153                     section_corner.y + header_height, panel_bounds.width - header_line_margin * 2,
154                     1), 1, Colors.GRAY);
155             // list of fields
156             foreach (field_name; field_names) {
157                 auto field_val = comp.fields[field_name];
158                 // calculate field corner
159                 auto corner = Vector2(section_corner.x,
160                         section_corner.y + (header_height + header_padding) + field_index * (
161                             field_padding + field_height));
162                 raygui.GuiLabel(Rectangle(corner.x, corner.y,
163                         field_label_width, field_height), field_name.c_str());
164                 raygui.GuiTextBox(Rectangle(corner.x + field_label_width, corner.y, field_value_width,
165                         field_height), field_val.c_str(), field_value_text_size, false);
166                 field_index++;
167             }
168             panel_y_offset += component_section_heights[i]; // go to the bottom of this section
169         }
170         // raygui.GuiGrid(Rectangle(panel_bounds.x + _panel_scroll.x, panel_bounds.y + _panel_scroll.y,
171         //         panel_content_bounds.width, panel_content_bounds.height), 16, 4);
172     }
173 
174     /// attach the inspector to an object
175     public void inspect(Entity nt) {
176         assert(_inspected.length == 0, "only one inspector may be open at a time");
177         open = true;
178         this.entity = nt;
179         // add components
180         _inspected ~= nt.get_all_components.map!(x => new InspectedObject(x)).array;
181     }
182 
183     /// close the inspector
184     public void close() {
185         assert(open, "inspector is already closed");
186         open = false;
187         reset();
188     }
189 }