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 }