diff --git a/src/UserInterface.c b/src/UserInterface.c index 8a37a9a..a7525e1 100644 --- a/src/UserInterface.c +++ b/src/UserInterface.c @@ -4,33 +4,63 @@ #include "Dengine/Renderer.h" #include "Dengine/Debug.h" -i32 userInterface_window(UiState *const uiState, - MemoryArena *const arena, +i32 userInterface_window(UiState *const uiState, MemoryArena *const arena, AssetManager *const assetManager, - Renderer *const renderer, - Font *const font, - const KeyInput input, - const i32 id, const Rect rect, const char *const title) + Renderer *const renderer, Font *const font, + const KeyInput input, WindowState *window) { - if (math_pointInRect(rect, input.mouseP)) + if (math_pointInRect(window->rect, input.mouseP)) { - uiState->hotItem = id; + uiState->hotItem = window->id; if (uiState->activeItem == 0 && input.keys[keycode_mouseLeft].endedDown) - uiState->activeItem = id; + uiState->activeItem = window->id; } + Rect rect = window->rect; RenderTex nullRenderTex = renderer_createNullRenderTex(assetManager); renderer_staticRect(renderer, rect.pos, rect.size, V2(0, 0), 0, - nullRenderTex, V4(0.25f, 0.25f, 0.5f, 1.0f)); + nullRenderTex, V4(0.25f, 0.25f, 0.5f, 0.5f)); - char *menuTitle = "Stat Menu"; - v2 menuTitleP = v2_add(rect.pos, V2(0, rect.size.h - 10)); - renderer_staticString(renderer, arena, font, menuTitle, menuTitleP, + v2 menuTitleP = v2_add(rect.pos, V2(0, rect.size.h - 10)); + renderer_staticString(renderer, arena, font, window->title, menuTitleP, V2(0, 0), 0, V4(0, 0, 0, 1)); - if (input.keys[keycode_mouseLeft].endedDown && - uiState->hotItem == id && - uiState->activeItem == id) + // NOTE(doyle): activeItem captures mouse click within the UI bounds, but if + // the user drags the mouse outside the bounds quicker than the game updates + // then we use a second flag which only "unclicks" when the mouse is let go + if (uiState->activeItem == window->id) window->windowHeld = TRUE; + + if (window->windowHeld) + { + if (!input.keys[keycode_mouseLeft].endedDown) + { + window->windowHeld = FALSE; + window->prevFrameWindowHeld = FALSE; + } + } + + if (window->windowHeld) + { + // NOTE(doyle): If this is the first window click we don't process + // movement and store the current position to delta from next cycle + if (window->prevFrameWindowHeld) + { + // NOTE(doyle): Window clicked and held + v2 deltaP = v2_sub(input.mouseP, window->prevMouseP); + DEBUG_PUSH_VAR("Delta Pos %4.2f, %4.2f", deltaP, "v2"); + window->rect.pos = v2_add(deltaP, window->rect.pos); + } + else + { + window->prevFrameWindowHeld = TRUE; + } + + window->prevMouseP = input.mouseP; + } + + if (!input.keys[keycode_mouseLeft].endedDown && + uiState->hotItem == window->id && + uiState->activeItem == window->id) { return 1; } @@ -49,12 +79,52 @@ i32 userInterface_button(UiState *const uiState, if (math_pointInRect(rect, input.mouseP)) { uiState->hotItem = id; - if (uiState->activeItem == 0 && input.keys[keycode_mouseLeft].endedDown) - uiState->activeItem = id; + + // NOTE(doyle): UI windows are drawn first, they steal focus on mouse + // click since window logic is paired with the window rendering. If + // a UI element resides over our mouse, we allow the element to override + // the windows focus + // TODO(doyle): Make a window list to iterate over window ids + if (uiState->activeItem == uiState->statWindow.id || + uiState->activeItem == uiState->debugWindow.id || + uiState->activeItem == 0) + { + if (input.keys[keycode_mouseLeft].endedDown) + { + uiState->activeItem = id; + } + } + } RenderTex renderTex = renderer_createNullRenderTex(assetManager); +#if 0 + // Draw shadow + renderer_staticRect(renderer, v2_add(V2(1, 1), rect.pos), rect.size, + V2(0, 0), 0, renderTex, V4(0, 0, 0, 1)); +#endif + + v2 buttonOffset = V2(0, 0); + v4 buttonColor = {0}; + if (uiState->hotItem == id) + { + if (uiState->activeItem == id) + { + buttonOffset = V2(1, 1); + buttonColor = V4(1, 1, 1, 1); + } + else + { + // TODO(doyle): Optional add effect on button hover + buttonColor = V4(1, 1, 1, 1); + } + } + else + { + buttonColor = V4(0.5f, 0.5f, 0.5f, 1); + } + /* If no widget has keyboard focus, take it */ if (uiState->kbdItem == 0) uiState->kbdItem = id; @@ -63,37 +133,14 @@ i32 userInterface_button(UiState *const uiState, if (uiState->kbdItem == id) { // Draw outline - renderer_staticRect(renderer, v2_add(V2(-6, -6), rect.pos), - v2_add(V2(20, 20), rect.size), V2(0, 0), 0, renderTex, + renderer_staticRect(renderer, + v2_add(V2(-2, -2), v2_add(buttonOffset, rect.pos)), + v2_add(V2(4, 4), rect.size), V2(0, 0), 0, renderTex, V4(1.0f, 0, 0, 1)); } - // Draw shadow - renderer_staticRect(renderer, v2_add(V2(8, 8), rect.pos), rect.size, - V2(0, 0), 0, renderTex, V4(0, 0, 0, 1)); - - v2 buttonOffset = V2(0, 0); - if (uiState->hotItem == id) - { - if (uiState->activeItem == id) - { - buttonOffset = V2(2, 2); - renderer_staticRect(renderer, v2_add(buttonOffset, rect.pos), - rect.size, V2(0, 0), 0, renderTex, - V4(1, 1, 1, 1)); - } - else - { - renderer_staticRect(renderer, v2_add(buttonOffset, rect.pos), - rect.size, V2(0, 0), 0, renderTex, - V4(1, 1, 1, 1)); - } - } - else - { - renderer_staticRect(renderer, v2_add(buttonOffset, rect.pos), rect.size, - V2(0, 0), 0, renderTex, V4(0.5f, 0.5f, 0.5f, 1)); - } + renderer_staticRect(renderer, v2_add(buttonOffset, rect.pos), rect.size, + V2(0, 0), 0, renderTex, buttonColor); if (label) { @@ -166,8 +213,15 @@ i32 userInterface_scrollBar(UiState *const uiState, if (math_pointInRect(scrollBarRect, input.mouseP)) { uiState->hotItem = id; - if (uiState->activeItem == 0 && input.keys[keycode_mouseLeft].endedDown) - uiState->activeItem = id; + if (uiState->activeItem == uiState->statWindow.id || + uiState->activeItem == uiState->debugWindow.id || + uiState->activeItem == 0) + { + if (input.keys[keycode_mouseLeft].endedDown) + { + uiState->activeItem = id; + } + } } RenderTex renderTex = renderer_createNullRenderTex(assetManager); @@ -180,8 +234,8 @@ i32 userInterface_scrollBar(UiState *const uiState, if (uiState->kbdItem == id) { // Draw outline - renderer_staticRect(renderer, v2_add(V2(-6, -6), scrollBarRect.pos), - v2_add(V2(20, 20), scrollBarRect.size), V2(0, 0), 0, + renderer_staticRect(renderer, v2_add(V2(-2, -2), scrollBarRect.pos), + v2_add(V2(4, 4), scrollBarRect.size), V2(0, 0), 0, renderTex, V4(1.0f, 0, 0, 1)); } @@ -278,9 +332,14 @@ i32 userInterface_textField(UiState *const uiState, MemoryArena *const arena, if (math_pointInRect(textRect, input.mouseP)) { uiState->hotItem = id; - if (uiState->activeItem == 0 && input.keys[keycode_mouseLeft].endedDown) + if (uiState->activeItem == uiState->statWindow.id || + uiState->activeItem == uiState->debugWindow.id || + uiState->activeItem == 0) { - uiState->activeItem = id; + if (input.keys[keycode_mouseLeft].endedDown) + { + uiState->activeItem = id; + } } } @@ -293,8 +352,8 @@ i32 userInterface_textField(UiState *const uiState, MemoryArena *const arena, if (uiState->kbdItem == id) { // Draw outline - renderer_staticRect(renderer, v2_add(V2(-4, -4), textRect.pos), - v2_add(V2(16, 16), textRect.size), V2(0, 0), 0, + renderer_staticRect(renderer, v2_add(V2(-2, -2), textRect.pos), + v2_add(V2(4, 4), textRect.size), V2(0, 0), 0, renderTex, V4(1.0f, 0, 0, 1)); } diff --git a/src/WorldTraveller.c b/src/WorldTraveller.c index 97e6e04..30610c9 100644 --- a/src/WorldTraveller.c +++ b/src/WorldTraveller.c @@ -343,13 +343,21 @@ void worldTraveller_gameInit(GameState *state, v2 windowSize) state->uiState.keyMod = keycode_null; state->uiState.keyChar = keycode_null; - common_strncpy(state->uiState.statMenuState.title, "Stat Menu", + common_strncpy(state->uiState.statWindow.title, "Stat Menu", common_strlen("Stat Menu")); - state->uiState.statMenuState.id = 99; - state->uiState.statMenuState.rect.pos = V2(300, 400); - state->uiState.statMenuState.rect.size = V2(300, 400); - state->uiState.statMenuState.prevFrameWindowHeld = FALSE; - state->uiState.statMenuState.windowHeld = FALSE; + state->uiState.statWindow.id = 99; + state->uiState.statWindow.rect.pos = V2(300, 400); + state->uiState.statWindow.rect.size = V2(300, 400); + state->uiState.statWindow.prevFrameWindowHeld = FALSE; + state->uiState.statWindow.windowHeld = FALSE; + + common_strncpy(state->uiState.debugWindow.title, "Debug Menu", + common_strlen("Debug Menu")); + state->uiState.debugWindow.id = 98; + state->uiState.debugWindow.rect.pos = V2(800, 400); + state->uiState.debugWindow.rect.size = V2(750, 400); + state->uiState.debugWindow.prevFrameWindowHeld = FALSE; + state->uiState.debugWindow.windowHeld = FALSE; state->config.playWorldAudio = FALSE; state->config.showDebugDisplay = TRUE; @@ -1031,6 +1039,71 @@ typedef struct BattleState GLOBAL_VAR BattleState battleState = {0}; +enum RectBaseline +{ + rectbaseline_top, + rectbaseline_topLeft, + rectbaseline_topRight, + rectbaseline_bottom, + rectbaseline_bottomRight, + rectbaseline_bottomLeft, + rectbaseline_left, + rectbaseline_right, + rectbaseline_center, + rectbaseline_count, + +}; + +INTERNAL v2 getPosRelativeToRect(Rect rect, v2 offset, + enum RectBaseline baseline) +{ +#ifdef DENGINE_DEBUG + ASSERT(baseline < rectbaseline_count); +#endif + v2 result = {0}; + + v2 posToOffsetFrom = rect.pos; + switch (baseline) + { + case rectbaseline_top: + posToOffsetFrom.y += (rect.size.h); + posToOffsetFrom.x += (rect.size.w * 0.5f); + break; + case rectbaseline_topLeft: + posToOffsetFrom.y += (rect.size.h); + break; + case rectbaseline_topRight: + posToOffsetFrom.y += (rect.size.h); + posToOffsetFrom.x += (rect.size.w); + break; + case rectbaseline_bottom: + posToOffsetFrom.x += (rect.size.w * 0.5f); + break; + case rectbaseline_bottomRight: + posToOffsetFrom.x += (rect.size.w); + break; + case rectbaseline_left: + posToOffsetFrom.y += (rect.size.h * 0.5f); + break; + case rectbaseline_right: + posToOffsetFrom.x += (rect.size.w); + posToOffsetFrom.y += (rect.size.h * 0.5f); + break; + + case rectbaseline_bottomLeft: + break; + default: +#ifdef DENGINE_DEBUG + DEBUG_LOG( + "getPosRelativeToRect() warning: baseline enum not recognised"); +#endif + break; + } + + result = v2_add(posToOffsetFrom, offset); + return result; +} + void worldTraveller_gameUpdateAndRender(GameState *state, f32 dt) { if (dt >= 1.0f) dt = 1.0f; @@ -1324,8 +1397,30 @@ void worldTraveller_gameUpdateAndRender(GameState *state, f32 dt) state->uiState.hotItem = 0; /* Draw ui */ + if (state->uiState.keyChar == keycode_i) + { + state->config.showStatMenu = + (state->config.showStatMenu == TRUE) ? FALSE : TRUE; + } + + if (state->config.showStatMenu) + { + WindowState *statWindow = &state->uiState.statWindow; + userInterface_window(&state->uiState, &state->arena, assetManager, + renderer, font, state->input, statWindow); + } + + /* Draw debug window */ + WindowState *debugWindow = &state->uiState.debugWindow; + userInterface_window(&state->uiState, &state->arena, assetManager, renderer, + font, state->input, debugWindow); + // TODO(doyle): Bug in font rendering once button reaches 700-800+ pixels - Rect toggleAudioButtonRect = {V2(1000, 800), V2(100, 50)}; + Rect toggleAudioButtonRect = {0}; + toggleAudioButtonRect.size = V2(100, 50); + toggleAudioButtonRect.pos = getPosRelativeToRect( + debugWindow->rect, V2(10, -65.0f), rectbaseline_topLeft); + b32 toggleAudioClicked = userInterface_button( &state->uiState, &state->arena, assetManager, renderer, font, state->input, 1, toggleAudioButtonRect, "Toggle Music"); @@ -1336,7 +1431,10 @@ void worldTraveller_gameUpdateAndRender(GameState *state, f32 dt) (state->config.playWorldAudio == TRUE) ? FALSE : TRUE; } - Rect toggleDebugButtonRect = {V2(1150, 800), V2(100, 50)}; + Rect toggleDebugButtonRect = {0}; + toggleDebugButtonRect.size = V2(100, 50); + toggleDebugButtonRect.pos = getPosRelativeToRect( + toggleAudioButtonRect, V2(25, 0), rectbaseline_bottomRight); b32 toggleDebugClicked = userInterface_button( &state->uiState, &state->arena, assetManager, renderer, font, state->input, 2, toggleDebugButtonRect, "Toggle Debug Display"); @@ -1347,66 +1445,18 @@ void worldTraveller_gameUpdateAndRender(GameState *state, f32 dt) (state->config.showDebugDisplay == TRUE) ? FALSE : TRUE; } - LOCAL_PERSIST toggleShowingStatMenu = FALSE; - if (state->uiState.keyChar == keycode_i) - { - toggleShowingStatMenu = - (toggleShowingStatMenu == TRUE) ? FALSE : TRUE; - } - - if (toggleShowingStatMenu) - { - WindowState *statMenu = &state->uiState.statMenuState; - // TODO(doyle): Define pushing/placing text within a coordinate system, - // i.e. relative to an elements position - b32 windowClickedAndHeld = userInterface_window( - &state->uiState, &state->arena, assetManager, renderer, font, - state->input, statMenu->id, statMenu->rect, statMenu->title); - - // NOTE(doyle): windowClickedAndHeld captures mouse click within the UI - // bounds, but if the user drags the mouse outside the bounds quicker - // than the game updates then we use a second flag which only - // "unclicks" when the mouse is let go - if (windowClickedAndHeld) statMenu->windowHeld = TRUE; - - if (statMenu->windowHeld) - { - if (!state->input.keys[keycode_mouseLeft].endedDown) - { - statMenu->windowHeld = FALSE; - statMenu->prevFrameWindowHeld = FALSE; - } - } - - if (statMenu->windowHeld) - { - // NOTE(doyle): If this is the first window click we don't process - // movement and instead store the frame position to delta from it - if (statMenu->prevFrameWindowHeld) - { - // NOTE(doyle): Window clicked and held - v2 deltaP = v2_sub(state->input.mouseP, statMenu->prevFramePos); - DEBUG_PUSH_VAR("Delta Pos %4.2f, %4.2f", deltaP, "v2"); - statMenu->rect.pos = v2_add(deltaP, statMenu->rect.pos); - } - else - { - statMenu->prevFrameWindowHeld = TRUE; - } - - statMenu->prevFramePos = state->input.mouseP; - } - - } - LOCAL_PERSIST i32 scrollValue = 30; - Rect scrollRectA = {V2(1500, 600), V2(16, 255)}; + Rect scrollRectA = {V2(0, 0), V2(16, 255)}; + scrollRectA.pos = getPosRelativeToRect(debugWindow->rect, V2(-20, -260), + rectbaseline_topRight); userInterface_scrollBar(&state->uiState, assetManager, renderer, state->input, 3, scrollRectA, &scrollValue, 160); LOCAL_PERSIST char fieldString[80] = "Hello world"; + v2 textFieldP = getPosRelativeToRect(toggleAudioButtonRect, V2(0, -20), + rectbaseline_bottomLeft); userInterface_textField(&state->uiState, &state->arena, assetManager, - renderer, font, state->input, 4, V2(1000, 750), + renderer, font, state->input, 4, textFieldP, fieldString); // RESET IMGUI diff --git a/src/include/Dengine/UserInterface.h b/src/include/Dengine/UserInterface.h index fa24f5b..086ec0a 100644 --- a/src/include/Dengine/UserInterface.h +++ b/src/include/Dengine/UserInterface.h @@ -33,11 +33,11 @@ typedef struct WindowState char title[64]; Rect rect; - v2 prevFramePos; + // TODO(doyle): Store this in the input data not window? + v2 prevMouseP; b32 prevFrameWindowHeld; b32 windowHeld; - } WindowState; typedef struct UiState @@ -54,14 +54,14 @@ typedef struct UiState enum KeyCode keyMod; enum KeyCode keyChar; - WindowState statMenuState; + WindowState statWindow; + WindowState debugWindow; } UiState; i32 userInterface_window(UiState *const uiState, MemoryArena *const arena, AssetManager *const assetManager, Renderer *const renderer, Font *const font, - const KeyInput input, const i32 id, const Rect rect, - const char *const title); + const KeyInput input, WindowState *window); i32 userInterface_button(UiState *const uiState, MemoryArena *const arena, diff --git a/src/include/Dengine/WorldTraveller.h b/src/include/Dengine/WorldTraveller.h index 89538c8..c0161f1 100644 --- a/src/include/Dengine/WorldTraveller.h +++ b/src/include/Dengine/WorldTraveller.h @@ -19,6 +19,7 @@ typedef struct Entity Entity; typedef struct Config { b32 playWorldAudio; + b32 showStatMenu; b32 showDebugDisplay; } Config;