473 lines
13 KiB
C
473 lines
13 KiB
C
#include "Dengine/UserInterface.h"
|
|
#include "Dengine/AssetManager.h"
|
|
#include "Dengine/Assets.h"
|
|
#include "Dengine/Renderer.h"
|
|
#include "Dengine/Debug.h"
|
|
|
|
i32 userInterface_button(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 label)
|
|
{
|
|
if (math_pointInRect(rect, input.mouseP))
|
|
{
|
|
uiState->hotItem = 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;
|
|
|
|
/* If we have keyboard focus, show it */
|
|
if (uiState->kbdItem == id)
|
|
{
|
|
// Draw outline
|
|
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, buttonColor);
|
|
}
|
|
|
|
renderer_staticRect(renderer, v2_add(buttonOffset, rect.pos), rect.size,
|
|
V2(0, 0), 0, renderTex, buttonColor);
|
|
|
|
if (label)
|
|
{
|
|
v2 labelDim = asset_stringDimInPixels(font, label);
|
|
v2 labelPos = rect.pos;
|
|
|
|
// Initially position the label to half the width of the button
|
|
labelPos.x += (rect.size.w * 0.5f);
|
|
|
|
// Move the label pos back half the length of the string (i.e.
|
|
// center it)
|
|
labelPos.x -= (CAST(f32) labelDim.w * 0.5f);
|
|
|
|
if (labelDim.h < rect.size.h)
|
|
{
|
|
labelPos.y += (rect.size.h * 0.5f);
|
|
labelPos.y -= (CAST(f32)labelDim.h * 0.5f);
|
|
}
|
|
|
|
labelPos = v2_add(labelPos, buttonOffset);
|
|
renderer_staticString(renderer, arena, font, label, labelPos, V2(0, 0),
|
|
0, V4(0, 0, 0, 1));
|
|
}
|
|
|
|
// After renderering before click check, see if we need to process keys
|
|
if (uiState->kbdItem == id)
|
|
{
|
|
switch (uiState->keyEntered)
|
|
{
|
|
case keycode_tab:
|
|
// Set focus to nothing and let next widget get focus
|
|
uiState->kbdItem = 0;
|
|
if (uiState->keyMod == keycode_leftShift)
|
|
uiState->kbdItem = uiState->lastWidget;
|
|
|
|
// Clear key state so next widget doesn't auto grab
|
|
uiState->keyEntered = keycode_null;
|
|
break;
|
|
case keycode_enter:
|
|
return 1;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
uiState->lastWidget = id;
|
|
|
|
// If button is hot and active, but mouse button is not
|
|
// down, the user must have clicked the button.
|
|
if (!input.keys[keycode_mouseLeft].endedDown &&
|
|
uiState->hotItem == id &&
|
|
uiState->activeItem == id)
|
|
{
|
|
return id;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
i32 userInterface_scrollbar(UiState *const uiState,
|
|
AssetManager *const assetManager,
|
|
Renderer *const renderer, const KeyInput input,
|
|
const i32 id, const Rect scrollBarRect,
|
|
i32 *const value, const i32 maxValue)
|
|
{
|
|
#ifdef DENGINE_DEBUG
|
|
ASSERT(*value <= maxValue);
|
|
#endif
|
|
|
|
if (math_pointInRect(scrollBarRect, input.mouseP))
|
|
{
|
|
uiState->hotItem = 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);
|
|
|
|
/* If no widget has keyboard focus, take it */
|
|
if (uiState->kbdItem == 0)
|
|
uiState->kbdItem = id;
|
|
|
|
/* If we have keyboard focus, show it */
|
|
if (uiState->kbdItem == id)
|
|
{
|
|
// Draw outline
|
|
renderer_staticRect(renderer, v2_add(V2(-2, -2), scrollBarRect.pos),
|
|
v2_add(V2(4, 4), scrollBarRect.size), V2(0, 0), 0,
|
|
renderTex, V4(1, 0, 0, 1));
|
|
}
|
|
|
|
// Render scroll bar background
|
|
renderer_staticRect(renderer, scrollBarRect.pos, scrollBarRect.size,
|
|
V2(0, 0), 0, renderTex, V4(0.75f, 0.5f, 0.5f, 1));
|
|
|
|
// Render scroll bar slider
|
|
v2 sliderSize = V2(16, 16);
|
|
v4 sliderColor = V4(0, 0, 0, 1);
|
|
|
|
f32 sliderPercentageOffset = (CAST(f32) *value / CAST(f32) maxValue);
|
|
f32 sliderYOffsetToBar =
|
|
(scrollBarRect.size.h - sliderSize.h) * sliderPercentageOffset;
|
|
v2 sliderPos = v2_add(scrollBarRect.pos, V2(0, sliderYOffsetToBar));
|
|
|
|
if (uiState->hotItem == id || uiState->activeItem == id)
|
|
sliderColor = V4(1.0f, 0, 0, 1);
|
|
else
|
|
sliderColor = V4(0.0f, 1.0f, 0, 1);
|
|
|
|
renderer_staticRect(renderer, sliderPos, sliderSize, V2(0, 0), 0, renderTex,
|
|
sliderColor);
|
|
|
|
if (uiState->kbdItem == id)
|
|
{
|
|
switch (uiState->keyEntered)
|
|
{
|
|
case keycode_tab:
|
|
uiState->kbdItem = 0;
|
|
if (uiState->keyMod == keycode_leftShift)
|
|
uiState->kbdItem = uiState->lastWidget;
|
|
|
|
// Clear key state so next widget doesn't auto grab
|
|
uiState->keyEntered = keycode_null;
|
|
break;
|
|
case keycode_up:
|
|
// TODO(doyle): Fix input for this to work, i.e. proper rate limited input poll
|
|
if (*value < maxValue)
|
|
{
|
|
(*value)++;
|
|
return id;
|
|
}
|
|
case keycode_down:
|
|
if (*value > 0)
|
|
{
|
|
(*value)--;
|
|
return id;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
uiState->lastWidget = id;
|
|
|
|
if (uiState->activeItem == id)
|
|
{
|
|
f32 mouseYRelToRect = input.mouseP.y - scrollBarRect.pos.y;
|
|
|
|
// Bounds check
|
|
if (mouseYRelToRect < 0)
|
|
mouseYRelToRect = 0;
|
|
else if (mouseYRelToRect > scrollBarRect.size.h)
|
|
mouseYRelToRect = scrollBarRect.size.h;
|
|
|
|
f32 newSliderPercentOffset =
|
|
(CAST(f32) mouseYRelToRect / scrollBarRect.size.h);
|
|
|
|
i32 newValue = CAST(i32)(newSliderPercentOffset * CAST(f32)maxValue);
|
|
if (newValue != *value)
|
|
{
|
|
*value = newValue;
|
|
return id;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
i32 userInterface_textField(UiState *const uiState, MemoryArena *const arena,
|
|
AssetManager *const assetManager,
|
|
Renderer *const renderer, Font *const font,
|
|
KeyInput input, const i32 id, const Rect rect,
|
|
char *const string)
|
|
{
|
|
i32 strLen = common_strlen(string);
|
|
b32 changed = FALSE;
|
|
|
|
if (math_pointInRect(rect, input.mouseP))
|
|
{
|
|
uiState->hotItem = id;
|
|
if (uiState->activeItem == uiState->statWindow.id ||
|
|
uiState->activeItem == uiState->debugWindow.id ||
|
|
uiState->activeItem == 0)
|
|
{
|
|
if (input.keys[keycode_mouseLeft].endedDown)
|
|
{
|
|
uiState->activeItem = id;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* If no widget has keyboard focus, take it */
|
|
if (uiState->kbdItem == 0)
|
|
uiState->kbdItem = id;
|
|
|
|
RenderTex renderTex = renderer_createNullRenderTex(assetManager);
|
|
/* If we have keyboard focus, show it */
|
|
if (uiState->kbdItem == id)
|
|
{
|
|
// Draw outline
|
|
renderer_staticRect(renderer, v2_add(V2(-2, -2), rect.pos),
|
|
v2_add(V2(4, 4), rect.size), V2(0, 0), 0,
|
|
renderTex, V4(1.0f, 0, 0, 1));
|
|
}
|
|
|
|
// Render text field
|
|
renderer_staticRect(renderer, rect.pos, rect.size, V2(0, 0), 0,
|
|
renderTex, V4(0.75f, 0.5f, 0.5f, 1));
|
|
|
|
if (uiState->activeItem == id || uiState->hotItem == id)
|
|
{
|
|
renderer_staticRect(renderer, rect.pos, rect.size, V2(0, 0), 0,
|
|
renderTex, V4(0.75f, 0.75f, 0.0f, 1));
|
|
}
|
|
else
|
|
{
|
|
renderer_staticRect(renderer, rect.pos, rect.size, V2(0, 0), 0,
|
|
renderTex, V4(0.5f, 0.5f, 0.5f, 1));
|
|
}
|
|
|
|
v2 strPos = rect.pos;
|
|
renderer_staticString(renderer, arena, font, string, strPos, V2(0, 0), 0,
|
|
V4(0, 0, 0, 1));
|
|
|
|
if (uiState->kbdItem == id)
|
|
{
|
|
switch (uiState->keyEntered)
|
|
{
|
|
case keycode_tab:
|
|
uiState->kbdItem = 0;
|
|
if (uiState->keyMod == keycode_leftShift)
|
|
uiState->kbdItem = uiState->lastWidget;
|
|
|
|
uiState->keyEntered = keycode_null;
|
|
break;
|
|
|
|
case keycode_backspace:
|
|
if (strLen > 0)
|
|
{
|
|
string[--strLen] = 0;
|
|
changed = TRUE;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (uiState->keyChar >= keycode_space &&
|
|
uiState->keyChar <= keycode_tilda && strLen < 30)
|
|
{
|
|
string[strLen++] = uiState->keyChar + ' ';
|
|
string[strLen] = 0;
|
|
changed = TRUE;
|
|
}
|
|
}
|
|
|
|
if (!input.keys[keycode_mouseLeft].endedDown &&
|
|
uiState->hotItem == id &&
|
|
uiState->activeItem == id)
|
|
{
|
|
uiState->kbdItem = id;
|
|
}
|
|
|
|
uiState->lastWidget = id;
|
|
|
|
if (changed) return id;
|
|
return 0;
|
|
}
|
|
|
|
i32 userInterface_window(UiState *const uiState, MemoryArena *const arena,
|
|
AssetManager *const assetManager,
|
|
Renderer *const renderer, Font *const font,
|
|
const KeyInput input, WindowState *window)
|
|
{
|
|
if (math_pointInRect(window->rect, input.mouseP))
|
|
{
|
|
uiState->hotItem = window->id;
|
|
if (uiState->activeItem == 0 && input.keys[keycode_mouseLeft].endedDown)
|
|
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, 0.5f));
|
|
|
|
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));
|
|
|
|
/* Draw window elements */
|
|
i32 firstActiveChildId = -1;
|
|
for (i32 i = 0; i < window->numChildUiItems; i++)
|
|
{
|
|
UiItem *childUi = &window->childUiItems[i];
|
|
|
|
// TODO(doyle): Redundant? If we can only have 1 active child at a time
|
|
// What about overlapping elements?
|
|
i32 getChildActiveState = -1;
|
|
switch(childUi->type)
|
|
{
|
|
case uitype_button:
|
|
// TODO(doyle): Bug in font rendering once button reaches 700-800+
|
|
// pixels
|
|
getChildActiveState = userInterface_button(
|
|
uiState, arena, assetManager, renderer, font, input,
|
|
childUi->id, childUi->rect, childUi->label);
|
|
break;
|
|
|
|
case uitype_scrollbar:
|
|
getChildActiveState = userInterface_scrollbar(
|
|
uiState, assetManager, renderer, input, childUi->id,
|
|
childUi->rect, &childUi->value, childUi->maxValue);
|
|
break;
|
|
|
|
case uitype_textField:
|
|
getChildActiveState = userInterface_textField(
|
|
uiState, arena, assetManager, renderer, font, input,
|
|
childUi->id, childUi->rect, childUi->string);
|
|
break;
|
|
|
|
default:
|
|
DEBUG_LOG(
|
|
"userInterface_window() warning: Enum uitype unrecognised");
|
|
break;
|
|
}
|
|
|
|
// NOTE(doyle): Capture only the first active id, but keep loop going to
|
|
// render the rest of the window ui
|
|
if (firstActiveChildId == -1 && getChildActiveState > 0)
|
|
firstActiveChildId = getChildActiveState;
|
|
}
|
|
|
|
if (firstActiveChildId != -1)
|
|
return firstActiveChildId;
|
|
|
|
// 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);
|
|
|
|
for (i32 i = 0; i < window->numChildUiItems; i++)
|
|
{
|
|
UiItem *childUi = &window->childUiItems[i];
|
|
childUi->rect.pos = v2_add(deltaP, childUi->rect.pos);
|
|
}
|
|
|
|
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 window->id;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|