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     enum width = 400;
21     /// whether the inspector is open
22     public bool open = false;
23     private Vector2 _panel_scroll;
24     private InspectedComponent[] _components;
25     private Entity entity;
26     private enum btn_close = ['x', '\0'];
27 
28     private class InspectedComponent {
29         public Component obj;
30         public Class obj_class;
31         public string[string] fields;
32 
33         this(Component 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         _components = [];
53         entity = null;
54     }
55 
56     public void update() {
57         // update all inspected components
58         foreach (comp; _components) {
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_padding = 2;
74         enum field_label_width = 120;
75         enum field_value_width = 240;
76         enum header_height = field_height; // for each component
77         enum header_padding = 4;
78         enum header_line_margin = 4;
79         enum title_height = field_height; // for each entity
80         enum title_padding = 8;
81 
82         // calculate panel bounds
83         // this is going to calculate the space required for each component
84         int[] component_section_heights;
85 
86         foreach (comp; _components) {
87             component_section_heights ~= (header_padding + header_padding) // header
88              + ((field_height + field_padding) // field and padding
89                      * ((cast(int) comp.fields.length) + 1)); // number of fields
90         }
91         // total height
92         auto panel_bounds_height = pad + component_section_heights.sum() + (
93                 title_height + title_padding);
94 
95         // bounds of the entire panel
96         auto panel_content_bounds = Rectangle(0, 0, width - pad, panel_bounds_height);
97 
98         auto view = raygui.GuiScrollPanel(panel_bounds, panel_content_bounds, &_panel_scroll);
99 
100         // start scissor
101         raylib.BeginScissorMode(cast(int) view.x, cast(int) view.y,
102                 cast(int) view.width, cast(int) view.height);
103         // end scissor on scope exit
104         scope (exit)
105             raylib.EndScissorMode();
106 
107         // close button
108         enum btn_close_sz = 12;
109         if (raygui.GuiButton(Rectangle(panel_bounds.x + panel_content_bounds.width - pad,
110                 panel_bounds.y + pad, btn_close_sz, btn_close_sz), cast(char*) btn_close)) {
111             close();
112             return; // when closed, cancel this render
113         }
114 
115         // the corner of the inside of the panel (pre-padded)
116         auto panel_corner = Vector2(panel_bounds.x + pad, panel_bounds.y + pad);
117 
118         // entity title
119         auto entity_title = format("Entity %s", entity.name);
120         raygui.GuiLabel(Rectangle(panel_corner.x, panel_corner.y,
121                 field_label_width, title_height), entity_title.c_str());
122         // title underline
123         raylib.DrawRectangleLinesEx(Rectangle(panel_corner.x,
124                 panel_corner.y + title_height, panel_bounds.width - pad * 2, 4), 1, Colors.GRAY);
125 
126         // - now draw each component section
127         auto panel_y_offset = (title_height + title_padding); // the offset from the y start of the panel (this is based on component index)
128         foreach (i, comp; _components) {
129             auto field_names = comp.fields.keys.sort();
130             auto field_index = 0;
131 
132             // corner for the start of this section
133             auto section_corner = Vector2(panel_corner.x, panel_corner.y + panel_y_offset);
134             // header
135             raygui.GuiLabel(Rectangle(section_corner.x, section_corner.y,
136                     field_label_width, header_height), comp.obj_class.getName.c_str());
137             // header underline
138             raylib.DrawRectangleLinesEx(Rectangle(section_corner.x + header_line_margin,
139                     section_corner.y + header_height, panel_bounds.width - header_line_margin * 2,
140                     1), 1, Colors.GRAY);
141             // list of fields
142             foreach (field_name; field_names) {
143                 auto field_val = comp.fields[field_name];
144                 // calculate field corner
145                 auto corner = Vector2(section_corner.x,
146                         section_corner.y + (header_height + header_padding) + field_index * (
147                             field_padding + field_height));
148                 raygui.GuiLabel(Rectangle(corner.x, corner.y,
149                         field_label_width, field_height), field_name.c_str());
150                 raygui.GuiTextBox(Rectangle(corner.x + field_label_width, corner.y,
151                         field_value_width, field_height), field_val.c_str(),
152                         field_value_width, false);
153                 field_index++;
154             }
155             panel_y_offset += component_section_heights[i]; // go to the bottom of this section
156         }
157         // raygui.GuiGrid(Rectangle(panel_bounds.x + _panel_scroll.x, panel_bounds.y + _panel_scroll.y,
158         //         panel_content_bounds.width, panel_content_bounds.height), 16, 4);
159     }
160 
161     /// attach the inspector to an object
162     public void inspect(Entity nt) {
163         assert(_components.length == 0, "only one inspector may be open at a time");
164         open = true;
165         this.entity = nt;
166         // add components
167         _components ~= nt.get_all_components.map!(x => new InspectedComponent(x)).array;
168     }
169 
170     /// close the inspector
171     public void close() {
172         assert(open, "inspector is already closed");
173         open = false;
174         reset();
175     }
176 }