Mercurial
comparison infinite_canvas/dev_ui.c @ 276:b55c22cff335
Add interactive infinite canvas prototype
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 16:57:56 -0700 |
| parents | |
| children | 8d560f50ed4c |
comparison
equal
deleted
inserted
replaced
| 274:c9be578316a6 | 276:b55c22cff335 |
|---|---|
| 1 #include "infinite_canvas/dev_ui.h" | |
| 2 | |
| 3 #include <math.h> | |
| 4 | |
| 5 #define RAYGUI_IMPLEMENTATION | |
| 6 #include "third_party/raylib/include/raygui.h" | |
| 7 | |
| 8 static const Rectangle DEV_PANEL = {18.0f, 18.0f, 264.0f, 436.0f}; | |
| 9 | |
| 10 static Rectangle Canvas_Dev_UI_Context_Bounds(void) | |
| 11 { | |
| 12 float width = 610.0f; | |
| 13 float x = (float)GetScreenWidth() - width - 18.0f; | |
| 14 if (x < DEV_PANEL.x + DEV_PANEL.width + 18.0f) { | |
| 15 x = DEV_PANEL.x + DEV_PANEL.width + 18.0f; | |
| 16 width = (float)GetScreenWidth() - x - 18.0f; | |
| 17 } | |
| 18 return (Rectangle){ | |
| 19 x, | |
| 20 18.0f, | |
| 21 width, | |
| 22 (float)GetScreenHeight() - 36.0f, | |
| 23 }; | |
| 24 } | |
| 25 | |
| 26 static float Canvas_Dev_UI_Draw_Wrapped_Context( | |
| 27 const char *p_text, | |
| 28 Font font, | |
| 29 Rectangle bounds, | |
| 30 float scroll, | |
| 31 Color color) | |
| 32 { | |
| 33 const float font_size = 13.0f; | |
| 34 const float line_height = 19.0f; | |
| 35 const float spacing = 0.0f; | |
| 36 char line[256] = {0}; | |
| 37 size_t line_length = 0; | |
| 38 float y = bounds.y - scroll; | |
| 39 | |
| 40 for (const char *p_cursor = p_text;; p_cursor++) { | |
| 41 boolean at_end = *p_cursor == '\0'; | |
| 42 boolean at_newline = *p_cursor == '\n'; | |
| 43 if (!at_end && !at_newline) { | |
| 44 line[line_length++] = *p_cursor; | |
| 45 line[line_length] = '\0'; | |
| 46 if (line_length + 1 < sizeof(line) && | |
| 47 MeasureTextEx(font, line, font_size, spacing).x <= bounds.width) { | |
| 48 continue; | |
| 49 } | |
| 50 if (line_length > 1) { | |
| 51 char overflow = line[--line_length]; | |
| 52 line[line_length] = '\0'; | |
| 53 if (y + line_height >= bounds.y && | |
| 54 y <= bounds.y + bounds.height) { | |
| 55 DrawTextEx( | |
| 56 font, | |
| 57 line, | |
| 58 (Vector2){bounds.x, y}, | |
| 59 font_size, | |
| 60 spacing, | |
| 61 color); | |
| 62 } | |
| 63 y += line_height; | |
| 64 line[0] = overflow; | |
| 65 line[1] = '\0'; | |
| 66 line_length = 1; | |
| 67 } | |
| 68 continue; | |
| 69 } | |
| 70 | |
| 71 if (line_length > 0 && | |
| 72 y + line_height >= bounds.y && | |
| 73 y <= bounds.y + bounds.height) { | |
| 74 DrawTextEx( | |
| 75 font, | |
| 76 line, | |
| 77 (Vector2){bounds.x, y}, | |
| 78 font_size, | |
| 79 spacing, | |
| 80 color); | |
| 81 } | |
| 82 y += line_height; | |
| 83 line_length = 0; | |
| 84 line[0] = '\0'; | |
| 85 if (at_end) break; | |
| 86 } | |
| 87 return y - bounds.y + scroll; | |
| 88 } | |
| 89 | |
| 90 static void Canvas_Dev_UI_Draw_Context_Inspector( | |
| 91 Canvas_Dev_UI *p_ui, | |
| 92 const Canvas_Context_Snapshot *p_snapshot, | |
| 93 Font font, | |
| 94 const Canvas_Theme *p_theme) | |
| 95 { | |
| 96 Rectangle panel = Canvas_Dev_UI_Context_Bounds(); | |
| 97 Canvas_Theme_Draw_Shadow(p_theme, panel, 0.04f, 14, 1.0f, 1.0f); | |
| 98 DrawRectangleRounded(panel, 0.04f, 14, p_theme->surface); | |
| 99 DrawRectangleRoundedLinesEx(panel, 0.04f, 14, 1.0f, p_theme->border); | |
| 100 | |
| 101 DrawTextEx( | |
| 102 font, | |
| 103 "Camera context", | |
| 104 (Vector2){panel.x + 20.0f, panel.y + 18.0f}, | |
| 105 21.0f, | |
| 106 0.0f, | |
| 107 p_theme->text); | |
| 108 DrawTextEx( | |
| 109 font, | |
| 110 TextFormat( | |
| 111 "%d visible entities%s", | |
| 112 (int)p_snapshot->visible_count, | |
| 113 p_snapshot->truncated ? " - output truncated" : ""), | |
| 114 (Vector2){panel.x + 20.0f, panel.y + 48.0f}, | |
| 115 13.0f, | |
| 116 0.0f, | |
| 117 p_snapshot->truncated ? (Color){220, 38, 38, 255} : p_theme->text_muted); | |
| 118 | |
| 119 Rectangle close = { | |
| 120 panel.x + panel.width - 76.0f, | |
| 121 panel.y + 16.0f, | |
| 122 56.0f, | |
| 123 30.0f, | |
| 124 }; | |
| 125 if (GuiButtonRounded(close, "Close", 0.30f, 10)) { | |
| 126 p_ui->context_open = FALSE; | |
| 127 return; | |
| 128 } | |
| 129 | |
| 130 Rectangle content = { | |
| 131 panel.x + 20.0f, | |
| 132 panel.y + 82.0f, | |
| 133 panel.width - 40.0f, | |
| 134 panel.height - 102.0f, | |
| 135 }; | |
| 136 float max_scroll = fmaxf(0.0f, p_ui->context_content_height - content.height); | |
| 137 if (CheckCollisionPointRec(GetMousePosition(), panel)) { | |
| 138 p_ui->context_scroll -= GetMouseWheelMove() * 42.0f; | |
| 139 } | |
| 140 p_ui->context_scroll = fmaxf(0.0f, fminf(p_ui->context_scroll, max_scroll)); | |
| 141 | |
| 142 BeginScissorMode( | |
| 143 (int32)content.x, | |
| 144 (int32)content.y, | |
| 145 (int32)content.width, | |
| 146 (int32)content.height); | |
| 147 p_ui->context_content_height = Canvas_Dev_UI_Draw_Wrapped_Context( | |
| 148 p_ui->context_buffer, | |
| 149 font, | |
| 150 content, | |
| 151 p_ui->context_scroll, | |
| 152 p_theme->text); | |
| 153 EndScissorMode(); | |
| 154 | |
| 155 max_scroll = fmaxf(0.0f, p_ui->context_content_height - content.height); | |
| 156 if (max_scroll > 0.0f) { | |
| 157 float track_height = content.height; | |
| 158 float thumb_height = fmaxf( | |
| 159 36.0f, | |
| 160 track_height * content.height / p_ui->context_content_height); | |
| 161 float thumb_y = content.y + | |
| 162 (p_ui->context_scroll / max_scroll) * (track_height - thumb_height); | |
| 163 DrawRectangleRounded( | |
| 164 (Rectangle){ | |
| 165 panel.x + panel.width - 10.0f, | |
| 166 thumb_y, | |
| 167 4.0f, | |
| 168 thumb_height, | |
| 169 }, | |
| 170 0.5f, | |
| 171 6, | |
| 172 p_theme->text_muted); | |
| 173 } | |
| 174 } | |
| 175 | |
| 176 static void Canvas_Dev_UI_Set_Style( | |
| 177 Font font, | |
| 178 const Canvas_Theme *p_theme) | |
| 179 { | |
| 180 GuiSetFont(font); | |
| 181 GuiSetStyle(DEFAULT, TEXT_SIZE, 14); | |
| 182 GuiSetStyle(DEFAULT, TEXT_SPACING, 0); | |
| 183 GuiSetStyle(DEFAULT, BORDER_WIDTH, 1); | |
| 184 GuiSetStyle(DEFAULT, BACKGROUND_COLOR, ColorToInt(p_theme->surface)); | |
| 185 | |
| 186 int32 controls[] = {BUTTON, DROPDOWNBOX}; | |
| 187 for (size_t index = 0; index < sizeof(controls) / sizeof(controls[0]); index++) { | |
| 188 int32 control = controls[index]; | |
| 189 GuiSetStyle(control, BORDER_WIDTH, 1); | |
| 190 GuiSetStyle(control, BORDER_COLOR_NORMAL, ColorToInt(p_theme->border)); | |
| 191 GuiSetStyle(control, BASE_COLOR_NORMAL, ColorToInt(p_theme->surface_muted)); | |
| 192 GuiSetStyle(control, TEXT_COLOR_NORMAL, ColorToInt(p_theme->text)); | |
| 193 GuiSetStyle(control, BORDER_COLOR_FOCUSED, ColorToInt(p_theme->accent)); | |
| 194 GuiSetStyle(control, BASE_COLOR_FOCUSED, ColorToInt(p_theme->accent_soft)); | |
| 195 GuiSetStyle(control, TEXT_COLOR_FOCUSED, ColorToInt(p_theme->text)); | |
| 196 GuiSetStyle(control, BORDER_COLOR_PRESSED, ColorToInt(p_theme->accent)); | |
| 197 GuiSetStyle(control, BASE_COLOR_PRESSED, ColorToInt(p_theme->accent_soft)); | |
| 198 GuiSetStyle(control, TEXT_COLOR_PRESSED, ColorToInt(p_theme->accent)); | |
| 199 } | |
| 200 } | |
| 201 | |
| 202 void Canvas_Dev_UI_Init( | |
| 203 Canvas_Dev_UI *p_ui, | |
| 204 Font font, | |
| 205 const Canvas_Theme *p_theme) | |
| 206 { | |
| 207 *p_ui = (Canvas_Dev_UI){ | |
| 208 .selected_type = CANVAS_ENTITY_RECTANGLE, | |
| 209 .dropdown_open = FALSE, | |
| 210 .context_open = FALSE, | |
| 211 }; | |
| 212 Canvas_Dev_UI_Set_Style(font, p_theme); | |
| 213 } | |
| 214 | |
| 215 boolean Canvas_Dev_UI_Contains_Pointer(const Canvas_Dev_UI *p_ui) | |
| 216 { | |
| 217 Vector2 pointer = GetMousePosition(); | |
| 218 if (CheckCollisionPointRec(pointer, DEV_PANEL)) return TRUE; | |
| 219 if (p_ui->context_open && | |
| 220 CheckCollisionPointRec(pointer, Canvas_Dev_UI_Context_Bounds())) { | |
| 221 return TRUE; | |
| 222 } | |
| 223 return FALSE; | |
| 224 } | |
| 225 | |
| 226 void Canvas_Dev_UI_Draw( | |
| 227 Canvas_Dev_UI *p_ui, | |
| 228 Canvas_Camera *p_camera, | |
| 229 Canvas_Scene *p_scene, | |
| 230 Font font, | |
| 231 Canvas_Theme *p_theme) | |
| 232 { | |
| 233 Canvas_Context_Snapshot context = Canvas_Scene_Build_Visible_Context( | |
| 234 p_scene, | |
| 235 p_camera, | |
| 236 p_ui->context_buffer, | |
| 237 sizeof(p_ui->context_buffer)); | |
| 238 | |
| 239 Canvas_Theme_Draw_Shadow(p_theme, DEV_PANEL, 0.09f, 16, 1.0f, 1.0f); | |
| 240 DrawRectangleRounded(DEV_PANEL, 0.09f, 16, p_theme->surface); | |
| 241 DrawRectangleRoundedLinesEx( | |
| 242 DEV_PANEL, | |
| 243 0.09f, | |
| 244 16, | |
| 245 1.0f, | |
| 246 p_theme->border); | |
| 247 DrawTextEx(font, "Canvas", (Vector2){34.0f, 34.0f}, 21.0f, 0.0f, p_theme->text); | |
| 248 DrawTextEx( | |
| 249 font, | |
| 250 "Developer controls", | |
| 251 (Vector2){34.0f, 61.0f}, | |
| 252 12.0f, | |
| 253 0.1f, | |
| 254 p_theme->text_muted); | |
| 255 | |
| 256 if (GuiButtonRounded((Rectangle){34.0f, 126.0f, 112.0f, 34.0f}, "Add entity", 0.32f, 10)) { | |
| 257 Canvas_Scene_Add( | |
| 258 p_scene, | |
| 259 (Canvas_Entity_Type)p_ui->selected_type, | |
| 260 p_camera->target); | |
| 261 } | |
| 262 if (GuiButtonRounded((Rectangle){154.0f, 126.0f, 112.0f, 34.0f}, "Clear", 0.32f, 10)) { | |
| 263 Canvas_Scene_Clear(p_scene); | |
| 264 } | |
| 265 if (GuiButtonRounded((Rectangle){34.0f, 168.0f, 72.0f, 34.0f}, "Demo", 0.32f, 10)) { | |
| 266 Canvas_Scene_Add_Demo(p_scene); | |
| 267 } | |
| 268 if (GuiButtonRounded((Rectangle){114.0f, 168.0f, 88.0f, 34.0f}, "Reset", 0.32f, 10)) { | |
| 269 p_camera->target = (Vector2){0.0f, 0.0f}; | |
| 270 p_camera->zoom = 1.0f; | |
| 271 } | |
| 272 if (GuiButtonRounded( | |
| 273 (Rectangle){210.0f, 168.0f, 56.0f, 34.0f}, | |
| 274 p_scene->show_grid ? "Grid" : "White", | |
| 275 0.32f, | |
| 276 10)) { | |
| 277 p_scene->show_grid = !p_scene->show_grid; | |
| 278 } | |
| 279 if (GuiButtonRounded( | |
| 280 (Rectangle){34.0f, 210.0f, 232.0f, 34.0f}, | |
| 281 "10 browser stress scene", | |
| 282 0.32f, | |
| 283 10)) { | |
| 284 Canvas_Scene_Add_Browser_Grid(p_scene); | |
| 285 p_camera->target = (Vector2){0.0f, 0.0f}; | |
| 286 p_camera->zoom = 1.0f; | |
| 287 } | |
| 288 | |
| 289 const char *p_theme_label = | |
| 290 p_theme->resolved_mode == CANVAS_THEME_DARK ? | |
| 291 "Switch to light mode" : | |
| 292 "Switch to dark mode"; | |
| 293 if (GuiButtonRounded( | |
| 294 (Rectangle){34.0f, 252.0f, 232.0f, 34.0f}, | |
| 295 p_theme_label, | |
| 296 0.32f, | |
| 297 10)) { | |
| 298 Canvas_Theme_Toggle(p_theme); | |
| 299 Canvas_Dev_UI_Set_Style(font, p_theme); | |
| 300 } | |
| 301 | |
| 302 if (GuiButtonRounded( | |
| 303 (Rectangle){34.0f, 294.0f, 232.0f, 34.0f}, | |
| 304 TextFormat("View camera context (%d)", (int)context.visible_count), | |
| 305 0.32f, | |
| 306 10)) { | |
| 307 p_ui->context_open = !p_ui->context_open; | |
| 308 p_ui->context_scroll = 0.0f; | |
| 309 } | |
| 310 | |
| 311 DrawTextEx(font, TextFormat("%d FPS", GetFPS()), (Vector2){34.0f, 354.0f}, 12.0f, 0.0f, p_theme->text_muted); | |
| 312 DrawTextEx(font, TextFormat("%.2fx zoom", p_camera->zoom), (Vector2){104.0f, 354.0f}, 12.0f, 0.0f, p_theme->text_muted); | |
| 313 DrawTextEx( | |
| 314 font, | |
| 315 TextFormat("%d objects", (int)Dowa_Array_Length(p_scene->p_entities)), | |
| 316 (Vector2){196.0f, 354.0f}, | |
| 317 12.0f, | |
| 318 0.0f, | |
| 319 p_theme->text_muted); | |
| 320 DrawTextEx( | |
| 321 font, | |
| 322 "Drag objects directly on the canvas", | |
| 323 (Vector2){34.0f, 400.0f}, | |
| 324 12.0f, | |
| 325 0.0f, | |
| 326 p_theme->text_muted); | |
| 327 | |
| 328 if (p_ui->context_open) { | |
| 329 Canvas_Dev_UI_Draw_Context_Inspector( | |
| 330 p_ui, | |
| 331 &context, | |
| 332 font, | |
| 333 p_theme); | |
| 334 } | |
| 335 | |
| 336 // Draw last so the expanded list overlays the other developer controls. | |
| 337 Rectangle dropdown = {34.0f, 84.0f, 232.0f, 34.0f}; | |
| 338 if (GuiDropdownBoxRounded( | |
| 339 dropdown, | |
| 340 "Rectangle;Circle;Line;Text;Button;Text area;Dropdown;Accordion;Card;Calendar;Switch;Table;Notification;Scrollable area;Image;Web content", | |
| 341 &p_ui->selected_type, | |
| 342 p_ui->dropdown_open, | |
| 343 0.32f, | |
| 344 10)) { | |
| 345 p_ui->dropdown_open = !p_ui->dropdown_open; | |
| 346 } | |
| 347 } |