diff --git a/meson.build b/meson.build index 429ce0b8..fcd14a8f 100644 --- a/meson.build +++ b/meson.build @@ -161,12 +161,16 @@ dll_sources = [ 'src/game/shell.c', 'src/game/sound.c', 'src/game/text.c', + 'src/game/ui/common.c', 'src/game/ui/controllers/controls.c', + 'src/game/ui/events.c', + 'src/game/ui/widgets/console.c', 'src/game/ui/widgets/controls_column.c', 'src/game/ui/widgets/controls_dialog.c', 'src/game/ui/widgets/controls_input_selector.c', 'src/game/ui/widgets/controls_layout_selector.c', 'src/game/ui/widgets/label.c', + 'src/game/ui/widgets/prompt.c', 'src/game/ui/widgets/stack.c', 'src/game/ui/widgets/window.c', 'src/global/enum_str.c', diff --git a/src/decomp/decomp.c b/src/decomp/decomp.c index 538693dc..304fb9cc 100644 --- a/src/decomp/decomp.c +++ b/src/decomp/decomp.c @@ -22,6 +22,7 @@ #include "game/shell.h" #include "game/sound.h" #include "game/text.h" +#include "game/ui/common.h" #include "global/const.h" #include "global/funcs.h" #include "global/vars.h" @@ -561,6 +562,7 @@ void __cdecl Shell_Shutdown(void) if (g_ErrorMessage[0]) { MessageBoxA(NULL, g_ErrorMessage, NULL, MB_ICONWARNING); } + UI_Shutdown(); } int16_t __cdecl TitleSequence(void) @@ -749,10 +751,13 @@ bool __cdecl WinVidSpinMessageLoop(bool need_wait) g_MessageLoopCounter--; return 0; } else if (msg.message == WM_KEYDOWN) { - Console_HandleKeyDown(msg.wParam); + UI_HandleKeyDown(msg.wParam); + return 0; + } else if (msg.message == WM_KEYUP) { + UI_HandleKeyUp(msg.wParam); return 0; } else if (msg.message == WM_CHAR) { - Console_HandleChar(msg.wParam); + UI_HandleChar(msg.wParam); return 0; } } diff --git a/src/game/console/common.c b/src/game/console/common.c index 18488a92..5ebce06d 100644 --- a/src/game/console/common.c +++ b/src/game/console/common.c @@ -1,148 +1,43 @@ #include "game/console/common.h" -#include "game/clock.h" -#include "game/game_string.h" +#include "game/console/setup.h" #include "game/input.h" -#include "game/output.h" #include "game/text.h" -#include "global/const.h" -#include "global/types.h" +#include "game/ui/widgets/console.h" #include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#define MAX_LOG_LINES 10 -#define MAX_PROMPT_LENGTH 100 -#define HOVER_DELAY_CPS 5 -#define MARGIN 5 -#define PADDING 3 static bool m_IsOpened = false; -static bool m_AreAnyLogsOnScreen = false; - -static struct { - char text[MAX_PROMPT_LENGTH]; - int32_t caret; - TEXTSTRING *prompt_ts; - TEXTSTRING *caret_ts; -} m_Prompt = { 0 }; - -static struct { - double expire_at; - TEXTSTRING *ts; -} m_Logs[MAX_LOG_LINES] = { 0 }; - -static const double m_PromptScale = 1.0; -static const double m_LogScale = 0.8; -static const char m_ValidPromptChars[] = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.- "; - -static void M_ShutdownPrompt(void); -static void M_ShutdownLogs(void); -static void M_UpdatePromptTextstring(void); -static void M_UpdateCaretTextstring(void); - -extern CONSOLE_COMMAND *g_ConsoleCommands[]; - -static void M_ShutdownPrompt(void) -{ - if (m_Prompt.prompt_ts != NULL) { - Text_Remove(m_Prompt.prompt_ts); - m_Prompt.prompt_ts = NULL; - } - if (m_Prompt.caret_ts != NULL) { - Text_Remove(m_Prompt.caret_ts); - m_Prompt.caret_ts = NULL; - } -} - -static void M_ShutdownLogs(void) -{ - for (int32_t i = 0; i < MAX_LOG_LINES; i++) { - Text_Remove(m_Logs[i].ts); - m_Logs[i].ts = NULL; - } -} - -static void M_UpdatePromptTextstring(void) -{ - Text_ChangeText(m_Prompt.prompt_ts, m_Prompt.text); -} - -static void M_UpdateCaretTextstring(void) -{ - const char old = m_Prompt.prompt_ts->text[m_Prompt.caret]; - m_Prompt.prompt_ts->text[m_Prompt.caret] = '\0'; - const int32_t width = - Text_GetWidth(m_Prompt.prompt_ts) * PHD_ONE / Text_GetScaleH(PHD_ONE); - m_Prompt.prompt_ts->text[m_Prompt.caret] = old; - Text_SetPos(m_Prompt.caret_ts, MARGIN + width, -MARGIN); -} +static UI_WIDGET *m_Console; void Console_Init(void) { - for (int32_t i = 0; i < MAX_LOG_LINES; i++) { - m_Logs[i].expire_at = 0; - m_Logs[i].ts = Text_Create(MARGIN, -MARGIN, 0, ""); - Text_SetScale(m_Logs[i].ts, PHD_ONE * m_LogScale, PHD_ONE * m_LogScale); - Text_AlignBottom(m_Logs[i].ts, true); - Text_SetMultiline(m_Logs[i].ts, true); - } - - // in case this is called after text reinitializes its textstrings, - // fix the broken pointers - if (strcmp(m_Prompt.text, "") != 0) { - Console_Open(); - } + m_Console = UI_Console_Create(); } void Console_Shutdown(void) { + if (m_Console != NULL) { + m_Console->free(m_Console); + m_Console = NULL; + } + m_IsOpened = false; - M_ShutdownPrompt(); - M_ShutdownLogs(); } void Console_Open(void) { if (m_IsOpened) { - M_ShutdownPrompt(); - } else { - LOG_DEBUG("opening console!"); + UI_Console_HandleClose(m_Console); } m_IsOpened = true; - - m_Prompt.caret = strlen(m_Prompt.text); - - m_Prompt.caret_ts = Text_Create(MARGIN, -MARGIN, 0, "\x11"); - Text_SetScale( - m_Prompt.caret_ts, PHD_ONE * m_PromptScale, PHD_ONE * m_PromptScale); - Text_AlignBottom(m_Prompt.caret_ts, true); - Text_Flash(m_Prompt.caret_ts, 1, 20); - - m_Prompt.prompt_ts = Text_Create(MARGIN, -MARGIN, 0, m_Prompt.text); - Text_SetScale( - m_Prompt.prompt_ts, PHD_ONE * m_PromptScale, PHD_ONE * m_PromptScale); - Text_AlignBottom(m_Prompt.prompt_ts, true); - - M_UpdateCaretTextstring(); + UI_Console_HandleOpen(m_Console); } void Console_Close(void) { - LOG_DEBUG("closing console!"); + UI_Console_HandleClose(m_Console); m_IsOpened = false; - strcpy(m_Prompt.text, ""); - M_ShutdownPrompt(); } bool Console_IsOpened(void) @@ -150,116 +45,6 @@ bool Console_IsOpened(void) return m_IsOpened; } -void Console_Confirm(void) -{ - if (strcmp(m_Prompt.text, "") == 0) { - Console_Close(); - return; - } - - Console_Eval(m_Prompt.text); - Console_Close(); -} - -bool Console_HandleKeyDown(const uint32_t key) -{ - // TODO: make it possible to turn the console off -#if 0 - if (!g_Config.enable_console) { - return false; - } -#endif - - switch (key) { - case VK_LEFT: - if (!m_IsOpened) { - return false; - } - if (m_Prompt.caret > 0) { - m_Prompt.caret--; - M_UpdateCaretTextstring(); - } - return true; - - case VK_RIGHT: - if (!m_IsOpened) { - return false; - } - if (m_Prompt.caret < (int32_t)strlen(m_Prompt.text)) { - m_Prompt.caret++; - M_UpdateCaretTextstring(); - } - return true; - - case VK_HOME: - if (!m_IsOpened) { - return false; - } - m_Prompt.caret = 0; - M_UpdateCaretTextstring(); - return true; - - case VK_END: - if (!m_IsOpened) { - return false; - } - m_Prompt.caret = strlen(m_Prompt.text); - M_UpdateCaretTextstring(); - return true; - - case VK_BACK: - if (!m_IsOpened) { - return false; - } - if (m_Prompt.caret > 0) { - for (int32_t i = m_Prompt.caret; i < MAX_PROMPT_LENGTH; i++) { - m_Prompt.text[i - 1] = m_Prompt.text[i]; - } - m_Prompt.caret--; - M_UpdatePromptTextstring(); - M_UpdateCaretTextstring(); - } - return true; - } - - return false; -} - -void Console_HandleChar(const uint32_t char_) -{ - if (!m_IsOpened) { - return; - } - - char insert_string[2]; - insert_string[0] = char_; - insert_string[1] = '\0'; - - if (strlen(insert_string) != 1 - || !strstr(m_ValidPromptChars, insert_string)) { - return; - } - - const size_t insert_length = strlen(insert_string); - const size_t available_space = - MAX_PROMPT_LENGTH - strlen(m_Prompt.text) - 1; - - if (insert_length > available_space) { - return; - } - - for (int32_t i = strlen(m_Prompt.text); i >= m_Prompt.caret; i--) { - m_Prompt.text[i + insert_length] = m_Prompt.text[i]; - } - - memcpy(m_Prompt.text + m_Prompt.caret, insert_string, insert_length); - - m_Prompt.caret += insert_length; - m_Prompt.text[MAX_PROMPT_LENGTH - 1] = '\0'; - M_UpdatePromptTextstring(); - M_UpdateCaretTextstring(); -} - int32_t Console_GetMaxLineLength(void) { return TEXT_MAX_STRING_SIZE - 1; @@ -267,34 +52,7 @@ int32_t Console_GetMaxLineLength(void) void Console_LogImpl(const char *const text) { - int32_t dst_idx = -1; - for (int32_t i = MAX_LOG_LINES - 1; i > 0; i--) { - if (m_Logs[i].ts == NULL) { - continue; - } - Text_ChangeText(m_Logs[i].ts, m_Logs[i - 1].ts->text); - m_Logs[i].expire_at = m_Logs[i - 1].expire_at; - } - - if (m_Logs[0].ts == NULL) { - return; - } - - m_Logs[0].expire_at = - Clock_GetHighPrecisionCounter() + 1000 * strlen(text) / HOVER_DELAY_CPS; - Text_ChangeText(m_Logs[0].ts, text); - int32_t y = -MARGIN - - Text_GetHeight(m_Prompt.prompt_ts) * m_PromptScale * PHD_ONE - / Text_GetScaleV(PHD_ONE); - - for (int32_t i = 0; i < MAX_LOG_LINES; i++) { - y -= PADDING; - y -= Text_GetHeight(m_Logs[i].ts) * m_LogScale * PHD_ONE - / Text_GetScaleV(PHD_ONE); - Text_SetPos(m_Logs[i].ts, MARGIN, y); - } - - m_AreAnyLogsOnScreen = true; + UI_Console_HandleLog(m_Console, text); } CONSOLE_COMMAND **Console_GetCommands(void) @@ -302,58 +60,24 @@ CONSOLE_COMMAND **Console_GetCommands(void) return g_ConsoleCommands; } -void Console_ScrollLogs(void) -{ - int32_t i = MAX_LOG_LINES - 1; - while (i >= 0 && !m_Logs[i].expire_at) { - i--; - } - - while (i >= 0 && m_Logs[i].expire_at - && Clock_GetHighPrecisionCounter() >= m_Logs[i].expire_at) { - m_Logs[i].expire_at = 0; - Text_ChangeText(m_Logs[i].ts, ""); - i--; - } - - m_AreAnyLogsOnScreen = i >= 0; -} - void Console_Draw(void) { - Console_ScrollLogs(); + UI_Console_ScrollLogs(m_Console); #if 0 // TODO: draw screen quad - if (m_IsOpened || m_AreAnyLogsOnScreen) { - int32_t sx = 0; - int32_t sw = Viewport_GetWidth(); - int32_t sh = Screen_GetRenderScale( - // not entirely accurate, but good enough - TEXT_HEIGHT * m_PromptScale - + MAX_LOG_LINES * TEXT_HEIGHT * m_LogScale, - RSR_TEXT); - int32_t sy = Viewport_GetHeight() - sh; - - RGBA_8888 top = { 0, 0, 0, 0 }; - RGBA_8888 bottom = { 0, 0, 0, 196 }; - - Output_DrawScreenGradientQuad(sx, sy, sw, sh, top, top, bottom, bottom); - } -#endif - - // achieved by Text_Draw() -#if 0 - if (m_Prompt.prompt_ts) { - Text_DrawText(m_Prompt.prompt_ts); - } - if (m_Prompt.caret_ts) { - Text_DrawText(m_Prompt.caret_ts); - } - for (int32_t i = 0; i < MAX_LOG_LINES; i++) { - if (m_Logs[i].ts) { - Text_DrawText(m_Logs[i].ts); - } - } + int32_t sx = 0; + int32_t sw = Viewport_GetWidth(); + int32_t sh = Screen_GetRenderScale( + // not entirely accurate, but good enough + TEXT_HEIGHT * m_PromptScale + + MAX_LOG_LINES * TEXT_HEIGHT * m_LogScale, + RSR_TEXT); + int32_t sy = Viewport_GetHeight() - sh; + + RGBA_8888 top = { 0, 0, 0, 0 }; + RGBA_8888 bottom = { 0, 0, 0, 196 }; + + Output_DrawScreenGradientQuad(sx, sy, sw, sh, top, top, bottom, bottom); #endif } diff --git a/src/game/console/common.h b/src/game/console/common.h index 9cb714b6..5c76f62a 100644 --- a/src/game/console/common.h +++ b/src/game/console/common.h @@ -11,10 +11,5 @@ void Console_Open(void); void Console_Close(void); bool Console_IsOpened(void); -void Console_Confirm(void); - -bool Console_HandleKeyDown(uint32_t key); -void Console_HandleChar(uint32_t char_); - void Console_Log(const char *fmt, ...); void Console_ScrollLogs(void); diff --git a/src/game/input.c b/src/game/input.c index 3ffd973d..9925c790 100644 --- a/src/game/input.c +++ b/src/game/input.c @@ -57,16 +57,7 @@ bool Input_Update(void) return true; } - if (Console_IsOpened()) { - if (g_InputDB & IN_DESELECT) { - Console_Close(); - } else if (g_InputDB & IN_SELECT) { - Console_Confirm(); - } - - g_Input = 0; - g_InputDB = 0; - } else if (g_InputDB & IN_CONSOLE) { + if (g_InputDB & IN_CONSOLE) { Console_Open(); g_Input = 0; g_InputDB = 0; diff --git a/src/game/shell.c b/src/game/shell.c index 70791fe1..50db5102 100644 --- a/src/game/shell.c +++ b/src/game/shell.c @@ -10,6 +10,7 @@ #include "game/input.h" #include "game/music.h" #include "game/sound.h" +#include "game/ui/common.h" #include "global/funcs.h" #include "global/vars.h" @@ -27,6 +28,7 @@ BOOL __cdecl Shell_Main(void) g_GameSizerCopy = 1.0; GameString_Init(); + UI_Init(); Config_Read(); if (!S_InitialiseSystem()) { diff --git a/src/game/ui/common.c b/src/game/ui/common.c new file mode 100644 index 00000000..c87fb17b --- /dev/null +++ b/src/game/ui/common.c @@ -0,0 +1,43 @@ +#include "game/ui/common.h" + +#include + +void UI_Init(void) +{ + UI_Events_Init(); +} + +void UI_Shutdown(void) +{ + UI_Events_Shutdown(); +} + +void UI_HandleKeyDown(const uint32_t key) +{ + const UI_EVENT event = { + .name = "key_down", + .sender = NULL, + .data = (void *)(uintptr_t)key, + }; + UI_Events_Fire(&event); +} + +void UI_HandleKeyUp(const uint32_t key) +{ + const UI_EVENT event = { + .name = "key_up", + .sender = NULL, + .data = (void *)(uintptr_t)key, + }; + UI_Events_Fire(&event); +} + +void UI_HandleChar(const uint32_t char_) +{ + const UI_EVENT event = { + .name = "char", + .sender = NULL, + .data = (void *)(uintptr_t)char_, + }; + UI_Events_Fire(&event); +} diff --git a/src/game/ui/common.h b/src/game/ui/common.h new file mode 100644 index 00000000..d89d2504 --- /dev/null +++ b/src/game/ui/common.h @@ -0,0 +1,10 @@ +#pragma once + +#include "game/ui/events.h" + +void UI_Init(void); +void UI_Shutdown(void); + +void UI_HandleKeyDown(uint32_t key); +void UI_HandleKeyUp(uint32_t key); +void UI_HandleChar(uint32_t char_); diff --git a/src/game/ui/events.c b/src/game/ui/events.c new file mode 100644 index 00000000..7c5cb0ca --- /dev/null +++ b/src/game/ui/events.c @@ -0,0 +1,65 @@ +#include "game/ui/events.h" + +#include +#include + +#include +#include + +typedef struct { + int32_t listener_id; + const char *event_name; + const UI_WIDGET *sender; + UI_EVENT_LISTENER listener; + void *user_data; +} M_LISTENER; + +static VECTOR *m_Listeners = NULL; +static int32_t m_ListenerID = 0; + +void UI_Events_Init(void) +{ + m_Listeners = Vector_Create(sizeof(M_LISTENER)); +} + +void UI_Events_Shutdown(void) +{ + Vector_Free(m_Listeners); +} + +int32_t UI_Events_Subscribe( + const char *const event_name, const UI_WIDGET *const sender, + const UI_EVENT_LISTENER listener, void *const user_data) +{ + M_LISTENER entry = { + .listener_id = m_ListenerID++, + .event_name = event_name, + .sender = sender, + .listener = listener, + .user_data = user_data, + }; + Vector_Add(m_Listeners, &entry); + return entry.listener_id; +} + +void UI_Events_Unsubscribe(const int32_t listener_id) +{ + for (int32_t i = 0; i < m_Listeners->count; i++) { + M_LISTENER entry = *(M_LISTENER *)Vector_Get(m_Listeners, i); + if (entry.listener_id == listener_id) { + Vector_RemoveAt(m_Listeners, i); + return; + } + } +} + +void UI_Events_Fire(const UI_EVENT *const event) +{ + for (int32_t i = 0; i < m_Listeners->count; i++) { + M_LISTENER entry = *(M_LISTENER *)Vector_Get(m_Listeners, i); + if (strcmp(entry.event_name, event->name) == 0 + && entry.sender == event->sender) { + entry.listener(event, entry.user_data); + } + } +} diff --git a/src/game/ui/events.h b/src/game/ui/events.h new file mode 100644 index 00000000..0686fa64 --- /dev/null +++ b/src/game/ui/events.h @@ -0,0 +1,22 @@ +#pragma once + +#include "game/ui/widgets/base.h" + +typedef struct { + const char *name; + const UI_WIDGET *sender; + void *data; +} UI_EVENT; + +typedef void (*UI_EVENT_LISTENER)(const UI_EVENT *, void *user_data); + +void UI_Events_Init(void); +void UI_Events_Shutdown(void); + +int32_t UI_Events_Subscribe( + const char *event_name, const UI_WIDGET *sender, UI_EVENT_LISTENER listener, + void *user_data); + +void UI_Events_Unsubscribe(int32_t listener_id); + +void UI_Events_Fire(const UI_EVENT *event); diff --git a/src/game/ui/widgets/base.h b/src/game/ui/widgets/base.h index 7c29f9fe..01c15117 100644 --- a/src/game/ui/widgets/base.h +++ b/src/game/ui/widgets/base.h @@ -11,7 +11,6 @@ typedef int32_t (*UI_WIDGET_GET_HEIGHT)(const struct UI_WIDGET *self); typedef void (*UI_WIDGET_SET_POSITION)( struct UI_WIDGET *self, int32_t x, int32_t y); typedef void (*UI_WIDGET_FREE)(struct UI_WIDGET *self); -typedef void (*UI_WIDGET_HANDLE_CHAR)(struct UI_WIDGET *self, uint32_t char_); typedef struct UI_WIDGET { UI_WIDGET_CONTROL control; @@ -19,7 +18,6 @@ typedef struct UI_WIDGET { UI_WIDGET_GET_HEIGHT get_height; UI_WIDGET_SET_POSITION set_position; UI_WIDGET_FREE free; - UI_WIDGET_HANDLE_CHAR handle_char; } UI_WIDGET; typedef UI_WIDGET UI_WIDGET_VTABLE; diff --git a/src/game/ui/widgets/console.c b/src/game/ui/widgets/console.c new file mode 100644 index 00000000..271d83f2 --- /dev/null +++ b/src/game/ui/widgets/console.c @@ -0,0 +1,183 @@ +#include "game/ui/widgets/console.h" + +#include "game/clock.h" +#include "game/console/common.h" +#include "game/ui/events.h" +#include "game/ui/widgets/label.h" +#include "game/ui/widgets/prompt.h" +#include "game/ui/widgets/stack.h" + +#include +#include + +#include + +#define WINDOW_MARGIN 5 +#define LOG_MARGIN 3 +#define TEXT_HEIGHT 15 +#define MAX_LOG_LINES 20 +#define LOG_SCALE 0.8 +#define DELAY_PER_CHAR 0.2 + +typedef struct { + UI_WIDGET_VTABLE vtable; + UI_WIDGET *container; + UI_WIDGET *prompt; + char *log_lines; + bool any_logs_on_screen; + + int32_t listener1; + int32_t listener2; + + struct { + double expire_at; + UI_WIDGET *label; + } logs[MAX_LOG_LINES]; +} UI_CONSOLE; + +static void M_HandlePromptCancel(const UI_EVENT *event, void *data); +static void M_HandlePromptConfirm(const UI_EVENT *event, void *data); + +static int32_t M_GetWidth(const UI_CONSOLE *self); +static int32_t M_GetHeight(const UI_CONSOLE *self); +static void M_SetPosition(UI_CONSOLE *self, int32_t x, int32_t y); +static void M_Control(UI_CONSOLE *self); +static void M_Free(UI_CONSOLE *self); + +static int32_t M_GetWidth(const UI_CONSOLE *const self) +{ + return 640 - 2 * WINDOW_MARGIN; +} + +static int32_t M_GetHeight(const UI_CONSOLE *const self) +{ + return 480 - 2 * WINDOW_MARGIN; +} + +static void M_SetPosition(UI_CONSOLE *const self, int32_t x, int32_t y) +{ + return self->container->set_position(self->container, x, y); +} + +static void M_Control(UI_CONSOLE *const self) +{ + self->container->control(self->container); +} + +static void M_Free(UI_CONSOLE *const self) +{ + self->prompt->free(self->prompt); + self->container->free(self->container); + UI_Events_Unsubscribe(self->listener1); + UI_Events_Unsubscribe(self->listener2); + Memory_Free(self); +} + +static void M_HandlePromptCancel(const UI_EVENT *const event, void *const data) +{ + Console_Close(); +} + +static void M_HandlePromptConfirm(const UI_EVENT *const event, void *const data) +{ + const char *text = event->data; + Console_Eval(text); + Console_Close(); +} + +UI_WIDGET *UI_Console_Create(void) +{ + UI_CONSOLE *const self = Memory_Alloc(sizeof(UI_CONSOLE)); + self->vtable = (UI_WIDGET_VTABLE) { + .control = (UI_WIDGET_CONTROL)M_Control, + .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, + .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, + .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, + .free = (UI_WIDGET_FREE)M_Free, + }; + + self->container = UI_Stack_Create( + UI_STACK_LAYOUT_VERTICAL_INVERSE, M_GetWidth(self), M_GetHeight(self)); + + self->prompt = UI_Prompt_Create(M_GetWidth(self), TEXT_HEIGHT + LOG_MARGIN); + UI_Stack_AddChild(self->container, self->prompt); + + for (int32_t i = 0; i < MAX_LOG_LINES; i++) { + self->logs[i].label = + UI_Label_Create("", M_GetWidth(self), UI_LABEL_AUTO_SIZE); + UI_Label_SetScale(self->logs[i].label, LOG_SCALE); + UI_Stack_AddChild(self->container, self->logs[i].label); + } + + M_SetPosition(self, WINDOW_MARGIN, WINDOW_MARGIN); + + self->listener1 = UI_Events_Subscribe( + "confirm", self->prompt, M_HandlePromptConfirm, NULL); + self->listener2 = + UI_Events_Subscribe("cancel", self->prompt, M_HandlePromptCancel, NULL); + + return (UI_WIDGET *)self; +} + +void UI_Console_HandleOpen(UI_WIDGET *const widget) +{ + UI_CONSOLE *const self = (UI_CONSOLE *)widget; + UI_Prompt_SetFocus(self->prompt, true); +} + +void UI_Console_HandleClose(UI_WIDGET *const widget) +{ + UI_CONSOLE *const self = (UI_CONSOLE *)widget; + UI_Prompt_SetFocus(self->prompt, false); + UI_Prompt_Clear(self->prompt); +} + +void UI_Console_HandleLog(UI_WIDGET *const widget, const char *const text) +{ + UI_CONSOLE *const self = (UI_CONSOLE *)widget; + + int32_t dst_idx = -1; + for (int32_t i = MAX_LOG_LINES - 1; i > 0; i--) { + if (self->logs[i].label == NULL) { + continue; + } + UI_Label_ChangeText( + self->logs[i].label, UI_Label_GetText(self->logs[i - 1].label)); + self->logs[i].expire_at = self->logs[i - 1].expire_at; + } + + if (self->logs[0].label == NULL) { + return; + } + + self->logs[0].expire_at = + Clock_GetHighPrecisionCounter() + 1000 * strlen(text) * DELAY_PER_CHAR; + UI_Label_ChangeText(self->logs[0].label, text); + + UI_Stack_DoLayout(self->container); + self->any_logs_on_screen = true; +} + +void UI_Console_ScrollLogs(UI_WIDGET *const widget) +{ + UI_CONSOLE *const self = (UI_CONSOLE *)widget; + + int32_t i = MAX_LOG_LINES - 1; + while (i >= 0 && !self->logs[i].expire_at) { + i--; + } + + bool need_layout = false; + while (i >= 0 && self->logs[i].expire_at + && Clock_GetHighPrecisionCounter() >= self->logs[i].expire_at) { + self->logs[i].expire_at = 0; + UI_Label_ChangeText(self->logs[i].label, ""); + need_layout = true; + i--; + } + + self->any_logs_on_screen = i >= 0; + if (need_layout) { + UI_Stack_DoLayout(self->container); + } +} diff --git a/src/game/ui/widgets/console.h b/src/game/ui/widgets/console.h new file mode 100644 index 00000000..9a73c128 --- /dev/null +++ b/src/game/ui/widgets/console.h @@ -0,0 +1,10 @@ +#pragma once + +#include "game/ui/widgets/base.h" + +UI_WIDGET *UI_Console_Create(void); + +void UI_Console_HandleOpen(UI_WIDGET *widget); +void UI_Console_HandleClose(UI_WIDGET *widget); +void UI_Console_HandleLog(UI_WIDGET *widget, const char *text); +void UI_Console_ScrollLogs(UI_WIDGET *widget); diff --git a/src/game/ui/widgets/label.c b/src/game/ui/widgets/label.c index 3bb41101..b47515bb 100644 --- a/src/game/ui/widgets/label.c +++ b/src/game/ui/widgets/label.c @@ -62,6 +62,7 @@ UI_WIDGET *UI_Label_Create( self->has_frame = false; self->text = Text_Create(0, 0, 16, text); + Text_SetMultiline(self->text, true); return (UI_WIDGET *)self; } diff --git a/src/game/ui/widgets/prompt.c b/src/game/ui/widgets/prompt.c new file mode 100644 index 00000000..bc125e4c --- /dev/null +++ b/src/game/ui/widgets/prompt.c @@ -0,0 +1,286 @@ +#include "game/ui/widgets/prompt.h" + +#include "game/input.h" +#include "game/text.h" +#include "game/ui/common.h" +#include "game/ui/events.h" +#include "game/ui/widgets/label.h" + +#include +#include + +#include +#include + +static const char m_ValidPromptChars[] = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.- "; + +typedef struct { + UI_WIDGET_VTABLE vtable; + UI_WIDGET *label; + UI_WIDGET *caret; + + int32_t listener1; + int32_t listener2; + + struct { + int32_t x; + int32_t y; + } pos; + bool is_focused; + int32_t current_text_capacity; + char *current_text; + int32_t caret_pos; +} UI_PROMPT; + +static void M_UpdatePromptLabel(UI_PROMPT *self); +static void M_UpdateCaretLabel(UI_PROMPT *self); +static void M_MoveCaretLeft(UI_PROMPT *self); +static void M_MoveCaretRight(UI_PROMPT *self); +static void M_MoveCaretStart(UI_PROMPT *self); +static void M_MoveCaretEnd(UI_PROMPT *self); +static void M_DeleteCharBack(UI_PROMPT *self); +static void M_Confirm(UI_PROMPT *self); +static void M_Cancel(UI_PROMPT *self); +static void M_Clear(UI_PROMPT *self); + +static int32_t M_GetWidth(const UI_PROMPT *self); +static int32_t M_GetHeight(const UI_PROMPT *self); +static void M_SetPosition(UI_PROMPT *self, int32_t x, int32_t y); +static void M_Control(UI_PROMPT *self); +static void M_Free(UI_PROMPT *self); +static void M_HandleKeyDown(const UI_EVENT *event, void *user_data); +static void M_HandleChar(const UI_EVENT *event, void *user_data); + +static void M_UpdatePromptLabel(UI_PROMPT *const self) +{ + UI_Label_ChangeText(self->label, self->current_text); +} + +static void M_UpdateCaretLabel(UI_PROMPT *const self) +{ + const char old = self->current_text[self->caret_pos]; + self->current_text[self->caret_pos] = '\0'; + UI_Label_ChangeText(self->label, self->current_text); + const int32_t width = UI_Label_MeasureTextWidth(self->label); + self->current_text[self->caret_pos] = old; + UI_Label_ChangeText(self->label, self->current_text); + + self->caret->set_position(self->caret, self->pos.x + width, self->pos.y); +} + +static int32_t M_GetWidth(const UI_PROMPT *const self) +{ + return self->label->get_width(self->label); +} + +static int32_t M_GetHeight(const UI_PROMPT *const self) +{ + return self->label->get_height(self->label); +} + +static void M_SetPosition( + UI_PROMPT *const self, const int32_t x, const int32_t y) +{ + self->pos.x = x; + self->pos.y = y; + self->label->set_position(self->label, x, y); + M_UpdateCaretLabel(self); +} + +static void M_Control(UI_PROMPT *const self) +{ + self->label->control(self->label); + self->caret->control(self->caret); +} + +static void M_Free(UI_PROMPT *const self) +{ + self->label->free(self->label); + self->caret->free(self->caret); + UI_Events_Unsubscribe(self->listener1); + UI_Events_Unsubscribe(self->listener2); + Memory_FreePointer(&self->current_text); + Memory_Free(self); +} + +static void M_MoveCaretLeft(UI_PROMPT *const self) +{ + if (self->caret_pos > 0) { + self->caret_pos--; + M_UpdateCaretLabel(self); + } +} + +static void M_MoveCaretRight(UI_PROMPT *const self) +{ + if (self->caret_pos < (int32_t)strlen(self->current_text)) { + self->caret_pos++; + M_UpdateCaretLabel(self); + } +} + +static void M_MoveCaretStart(UI_PROMPT *const self) +{ + self->caret_pos = 0; + M_UpdateCaretLabel(self); +} + +static void M_MoveCaretEnd(UI_PROMPT *const self) +{ + self->caret_pos = strlen(self->current_text); + M_UpdateCaretLabel(self); +} + +static void M_DeleteCharBack(UI_PROMPT *const self) +{ + if (self->caret_pos <= 0) { + return; + } + + memmove( + self->current_text + self->caret_pos - 1, + self->current_text + self->caret_pos, + strlen(self->current_text) + 1 - self->caret_pos); + + self->caret_pos--; + M_UpdatePromptLabel(self); + M_UpdateCaretLabel(self); +} + +static void M_Confirm(UI_PROMPT *const self) +{ + if (String_IsEmpty(self->current_text)) { + M_Cancel(self); + return; + } + const UI_EVENT event = { + .name = "confirm", + .sender = (const UI_WIDGET *)self, + .data = self->current_text, + }; + UI_Events_Fire(&event); + M_Clear(self); + M_UpdateCaretLabel(self); +} + +static void M_Cancel(UI_PROMPT *const self) +{ + const UI_EVENT event = { + .name = "cancel", + .sender = (const UI_WIDGET *)self, + .data = self->current_text, + }; + UI_Events_Fire(&event); + M_Clear(self); +} + +static void M_Clear(UI_PROMPT *const self) +{ + strcpy(self->current_text, ""); + self->caret_pos = 0; + M_UpdatePromptLabel(self); + M_UpdateCaretLabel(self); +} + +static void M_HandleKeyDown(const UI_EVENT *const event, void *const user_data) +{ + const uint32_t key = (uint32_t)(uintptr_t)event->data; + UI_PROMPT *const self = user_data; + + if (!self->is_focused) { + return; + } + + // clang-format off + switch (key) { + case VK_LEFT: M_MoveCaretLeft(self); break; + case VK_RIGHT: M_MoveCaretRight(self); break; + case VK_HOME: M_MoveCaretStart(self); break; + case VK_END: M_MoveCaretEnd(self); break; + case VK_BACK: M_DeleteCharBack(self); break; + case VK_RETURN: M_Confirm(self); break; + case VK_ESCAPE: M_Cancel(self); break; + } + // clang-format on +} + +static void M_HandleChar(const UI_EVENT *const event, void *const user_data) +{ + const uint32_t char_ = (uint32_t)(uintptr_t)event->data; + UI_PROMPT *const self = user_data; + + if (!self->is_focused) { + return; + } + + char insert_string[2] = { char_, '\0' }; + const size_t insert_length = strlen(insert_string); + + if (strlen(insert_string) != 1 + || !strstr(m_ValidPromptChars, insert_string)) { + return; + } + + const size_t available_space = + self->current_text_capacity - strlen(self->current_text); + if (insert_length >= available_space) { + self->current_text_capacity *= 2; + self->current_text = + Memory_Realloc(self->current_text, self->current_text_capacity); + } + + memmove( + self->current_text + self->caret_pos + insert_length, + self->current_text + self->caret_pos, + strlen(self->current_text) + 1 - self->caret_pos); + memcpy(self->current_text + self->caret_pos, insert_string, insert_length); + + self->caret_pos += insert_length; + M_UpdatePromptLabel(self); + M_UpdateCaretLabel(self); +} + +UI_WIDGET *UI_Prompt_Create(const int32_t width, const int32_t height) +{ + UI_PROMPT *const self = Memory_Alloc(sizeof(UI_PROMPT)); + self->vtable = (UI_WIDGET_VTABLE) { + .control = (UI_WIDGET_CONTROL)M_Control, + .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, + .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, + .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, + .free = (UI_WIDGET_FREE)M_Free, + }; + + self->current_text_capacity = 1; + self->current_text = Memory_Alloc(self->current_text_capacity); + self->caret = UI_Label_Create("", width, height); + self->label = UI_Label_Create(self->current_text, width, height); + self->is_focused = false; + + self->listener1 = + UI_Events_Subscribe("key_down", NULL, M_HandleKeyDown, self); + self->listener2 = UI_Events_Subscribe("char", NULL, M_HandleChar, self); + + return (UI_WIDGET *)self; +} + +void UI_Prompt_SetFocus(UI_WIDGET *const widget, const bool is_focused) +{ + UI_PROMPT *const self = (UI_PROMPT *)widget; + self->is_focused = is_focused; + if (is_focused) { + Input_EnterListenMode(); + UI_Label_ChangeText(self->caret, "\x11"); + UI_Label_Flash(self->caret, 1, 20); + } else { + Input_ExitListenMode(); + UI_Label_ChangeText(self->caret, ""); + } +} + +void UI_Prompt_Clear(UI_WIDGET *const widget) +{ + UI_PROMPT *const self = (UI_PROMPT *)widget; + M_Clear(self); +} diff --git a/src/game/ui/widgets/prompt.h b/src/game/ui/widgets/prompt.h new file mode 100644 index 00000000..4657e6c8 --- /dev/null +++ b/src/game/ui/widgets/prompt.h @@ -0,0 +1,8 @@ +#pragma once + +#include "game/ui/widgets/base.h" + +UI_WIDGET *UI_Prompt_Create(int32_t width, int32_t height); + +void UI_Prompt_SetFocus(UI_WIDGET *widget, bool is_focused); +void UI_Prompt_Clear(UI_WIDGET *widget);