Mercurial
view infinite_canvas/dev_ui.c @ 278:8d560f50ed4c
Improve infinite canvas interactions and browser chrome
Render Lucide icons directly with Raylib, add searchable icon browsing, robust text editing, entity lifecycle animations, z-order-safe input, semantic themes, and animated editable browser controls. Document rendering, pinning, context, and component extension for future agents.
Co-authored-by: Copilot <[email protected]>
Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 22:16:14 -0700 |
| parents | b55c22cff335 |
| children | 49e9e591c9bb |
line wrap: on
line source
#include "infinite_canvas/dev_ui.h" #include <math.h> #define RAYGUI_IMPLEMENTATION #include "third_party/raylib/include/raygui.h" static const Rectangle DEV_PANEL = {18.0f, 18.0f, 264.0f, 478.0f}; static Rectangle Canvas_Dev_UI_Context_Bounds(void) { float width = 610.0f; float x = (float)GetScreenWidth() - width - 18.0f; if (x < DEV_PANEL.x + DEV_PANEL.width + 18.0f) { x = DEV_PANEL.x + DEV_PANEL.width + 18.0f; width = (float)GetScreenWidth() - x - 18.0f; } return (Rectangle){ x, 18.0f, width, (float)GetScreenHeight() - 36.0f, }; } static float Canvas_Dev_UI_Draw_Wrapped_Context( const char *p_text, Font font, Rectangle bounds, float scroll, Color color) { const float font_size = 13.0f; const float line_height = 19.0f; const float spacing = 0.0f; char line[256] = {0}; size_t line_length = 0; float y = bounds.y - scroll; for (const char *p_cursor = p_text;; p_cursor++) { boolean at_end = *p_cursor == '\0'; boolean at_newline = *p_cursor == '\n'; if (!at_end && !at_newline) { line[line_length++] = *p_cursor; line[line_length] = '\0'; if (line_length + 1 < sizeof(line) && MeasureTextEx(font, line, font_size, spacing).x <= bounds.width) { continue; } if (line_length > 1) { char overflow = line[--line_length]; line[line_length] = '\0'; if (y + line_height >= bounds.y && y <= bounds.y + bounds.height) { DrawTextEx( font, line, (Vector2){bounds.x, y}, font_size, spacing, color); } y += line_height; line[0] = overflow; line[1] = '\0'; line_length = 1; } continue; } if (line_length > 0 && y + line_height >= bounds.y && y <= bounds.y + bounds.height) { DrawTextEx( font, line, (Vector2){bounds.x, y}, font_size, spacing, color); } y += line_height; line_length = 0; line[0] = '\0'; if (at_end) break; } return y - bounds.y + scroll; } static void Canvas_Dev_UI_Draw_Context_Inspector( Canvas_Dev_UI *p_ui, const Canvas_Context_Snapshot *p_snapshot, Font font, const Canvas_Theme *p_theme) { Rectangle panel = Canvas_Dev_UI_Context_Bounds(); Canvas_Theme_Draw_Shadow(p_theme, panel, 0.04f, 14, 1.0f, 1.0f); DrawRectangleRounded(panel, 0.04f, 14, p_theme->surface); DrawRectangleRoundedLinesEx(panel, 0.04f, 14, 1.0f, p_theme->border); DrawTextEx( font, "Camera context", (Vector2){panel.x + 20.0f, panel.y + 18.0f}, 21.0f, 0.0f, p_theme->text); DrawTextEx( font, TextFormat( "%d visible entities%s", (int)p_snapshot->visible_count, p_snapshot->truncated ? " - output truncated" : ""), (Vector2){panel.x + 20.0f, panel.y + 48.0f}, 13.0f, 0.0f, p_snapshot->truncated ? p_theme->danger : p_theme->text_muted); Rectangle close = { panel.x + panel.width - 76.0f, panel.y + 16.0f, 56.0f, 30.0f, }; if (GuiButtonRounded(close, "Close", 0.30f, 10)) { p_ui->context_open = FALSE; return; } Rectangle content = { panel.x + 20.0f, panel.y + 82.0f, panel.width - 40.0f, panel.height - 102.0f, }; float max_scroll = fmaxf(0.0f, p_ui->context_content_height - content.height); if (CheckCollisionPointRec(GetMousePosition(), panel)) { p_ui->context_scroll -= GetMouseWheelMove() * 42.0f; } p_ui->context_scroll = fmaxf(0.0f, fminf(p_ui->context_scroll, max_scroll)); BeginScissorMode( (int32)content.x, (int32)content.y, (int32)content.width, (int32)content.height); p_ui->context_content_height = Canvas_Dev_UI_Draw_Wrapped_Context( p_ui->context_buffer, font, content, p_ui->context_scroll, p_theme->text); EndScissorMode(); max_scroll = fmaxf(0.0f, p_ui->context_content_height - content.height); if (max_scroll > 0.0f) { float track_height = content.height; float thumb_height = fmaxf( 36.0f, track_height * content.height / p_ui->context_content_height); float thumb_y = content.y + (p_ui->context_scroll / max_scroll) * (track_height - thumb_height); DrawRectangleRounded( (Rectangle){ panel.x + panel.width - 10.0f, thumb_y, 4.0f, thumb_height, }, 0.5f, 6, p_theme->text_muted); } } static void Canvas_Dev_UI_Set_Style( Font font, const Canvas_Theme *p_theme) { GuiSetFont(font); GuiSetStyle(DEFAULT, TEXT_SIZE, 14); GuiSetStyle(DEFAULT, TEXT_SPACING, 0); GuiSetStyle(DEFAULT, BORDER_WIDTH, 1); GuiSetStyle(DEFAULT, BACKGROUND_COLOR, ColorToInt(p_theme->surface)); int32 controls[] = {BUTTON, DROPDOWNBOX}; for (size_t index = 0; index < sizeof(controls) / sizeof(controls[0]); index++) { int32 control = controls[index]; GuiSetStyle(control, BORDER_WIDTH, 1); GuiSetStyle(control, BORDER_COLOR_NORMAL, ColorToInt(p_theme->border)); GuiSetStyle(control, BASE_COLOR_NORMAL, ColorToInt(p_theme->surface_muted)); GuiSetStyle(control, TEXT_COLOR_NORMAL, ColorToInt(p_theme->text)); GuiSetStyle(control, BORDER_COLOR_FOCUSED, ColorToInt(p_theme->accent)); GuiSetStyle(control, BASE_COLOR_FOCUSED, ColorToInt(p_theme->accent_soft)); GuiSetStyle(control, TEXT_COLOR_FOCUSED, ColorToInt(p_theme->text)); GuiSetStyle(control, BORDER_COLOR_PRESSED, ColorToInt(p_theme->accent)); GuiSetStyle(control, BASE_COLOR_PRESSED, ColorToInt(p_theme->accent_soft)); GuiSetStyle(control, TEXT_COLOR_PRESSED, ColorToInt(p_theme->accent)); } } void Canvas_Dev_UI_Init( Canvas_Dev_UI *p_ui, Font font, const Canvas_Theme *p_theme) { *p_ui = (Canvas_Dev_UI){ .selected_type = CANVAS_ENTITY_RECTANGLE, .dropdown_open = FALSE, .context_open = FALSE, }; Canvas_Dev_UI_Set_Style(font, p_theme); } boolean Canvas_Dev_UI_Contains_Pointer(const Canvas_Dev_UI *p_ui) { // The expanded dropdown is modal: it draws outside DEV_PANEL and also // consumes outside clicks to close itself. if (p_ui->dropdown_open) return TRUE; Vector2 pointer = GetMousePosition(); if (CheckCollisionPointRec(pointer, DEV_PANEL)) return TRUE; if (p_ui->context_open && CheckCollisionPointRec(pointer, Canvas_Dev_UI_Context_Bounds())) { return TRUE; } return FALSE; } void Canvas_Dev_UI_Draw( Canvas_Dev_UI *p_ui, Canvas_Camera *p_camera, Canvas_Scene *p_scene, Font font, Canvas_Theme *p_theme) { Canvas_Context_Snapshot context = Canvas_Scene_Build_Visible_Context( p_scene, p_camera, p_ui->context_buffer, sizeof(p_ui->context_buffer)); Canvas_Theme_Draw_Shadow(p_theme, DEV_PANEL, 0.09f, 16, 1.0f, 1.0f); DrawRectangleRounded(DEV_PANEL, 0.09f, 16, p_theme->surface); DrawRectangleRoundedLinesEx( DEV_PANEL, 0.09f, 16, 1.0f, p_theme->border); DrawTextEx(font, "Canvas", (Vector2){34.0f, 34.0f}, 21.0f, 0.0f, p_theme->text); DrawTextEx( font, "Developer controls", (Vector2){34.0f, 61.0f}, 12.0f, 0.1f, p_theme->text_muted); if (p_ui->dropdown_open) GuiLock(); if (GuiButtonRounded((Rectangle){34.0f, 126.0f, 112.0f, 34.0f}, "Add entity", 0.32f, 10)) { Canvas_Scene_Add( p_scene, (Canvas_Entity_Type)p_ui->selected_type, p_camera->target); } if (GuiButtonRounded((Rectangle){154.0f, 126.0f, 112.0f, 34.0f}, "Clear", 0.32f, 10)) { Canvas_Scene_Fade_Out_All(p_scene); } if (GuiButtonRounded((Rectangle){34.0f, 168.0f, 72.0f, 34.0f}, "Demo", 0.32f, 10)) { Canvas_Scene_Add_Demo(p_scene); } if (GuiButtonRounded((Rectangle){114.0f, 168.0f, 88.0f, 34.0f}, "Reset", 0.32f, 10)) { p_camera->target = (Vector2){0.0f, 0.0f}; p_camera->zoom = 1.0f; } if (GuiButtonRounded( (Rectangle){210.0f, 168.0f, 56.0f, 34.0f}, p_scene->show_grid ? "Grid" : "White", 0.32f, 10)) { p_scene->show_grid = !p_scene->show_grid; } if (GuiButtonRounded( (Rectangle){34.0f, 210.0f, 232.0f, 34.0f}, "10 browser stress scene", 0.32f, 10)) { Canvas_Scene_Add_Browser_Grid(p_scene); p_camera->target = (Vector2){0.0f, 0.0f}; p_camera->zoom = 1.0f; } const char *p_theme_label = p_theme->resolved_mode == CANVAS_THEME_DARK ? "Switch to light mode" : "Switch to dark mode"; if (GuiButtonRounded( (Rectangle){34.0f, 252.0f, 232.0f, 34.0f}, p_theme_label, 0.32f, 10)) { Canvas_Theme_Toggle(p_theme); Canvas_Dev_UI_Set_Style(font, p_theme); } if (GuiButtonRounded( (Rectangle){34.0f, 294.0f, 232.0f, 34.0f}, TextFormat("View camera context (%d)", (int)context.visible_count), 0.32f, 10)) { p_ui->context_open = !p_ui->context_open; p_ui->context_scroll = 0.0f; } if (GuiButtonRounded( (Rectangle){34.0f, 336.0f, 232.0f, 34.0f}, "Lucide icon gallery", 0.32f, 10)) { Canvas_Scene_Show_Lucide_Gallery(p_scene, p_camera); } DrawTextEx(font, TextFormat("%d FPS", GetFPS()), (Vector2){34.0f, 396.0f}, 12.0f, 0.0f, p_theme->text_muted); DrawTextEx(font, TextFormat("%.2fx zoom", p_camera->zoom), (Vector2){104.0f, 396.0f}, 12.0f, 0.0f, p_theme->text_muted); DrawTextEx( font, TextFormat("%d objects", (int)Dowa_Array_Length(p_scene->p_entities)), (Vector2){196.0f, 396.0f}, 12.0f, 0.0f, p_theme->text_muted); DrawTextEx( font, "Drag objects directly on the canvas", (Vector2){34.0f, 442.0f}, 12.0f, 0.0f, p_theme->text_muted); if (p_ui->context_open) { Canvas_Dev_UI_Draw_Context_Inspector( p_ui, &context, font, p_theme); } // Draw last so the expanded list overlays the other developer controls. if (p_ui->dropdown_open) GuiUnlock(); Rectangle dropdown = {34.0f, 84.0f, 232.0f, 34.0f}; if (GuiDropdownBoxRounded( dropdown, "Rectangle;Circle;Line;Text;Button;Text area;Dropdown;Accordion;Card;Calendar;Switch;Table;Notification;Scrollable area;Image;Web content", &p_ui->selected_type, p_ui->dropdown_open, 0.32f, 10)) { p_ui->dropdown_open = !p_ui->dropdown_open; } }