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 }