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