diff --git a/include/libtrx/config.h b/include/libtrx/config.h new file mode 100644 index 0000000..7daa7d0 --- /dev/null +++ b/include/libtrx/config.h @@ -0,0 +1,6 @@ +#pragma once + +#include "config/common.h" +#include "config/file.h" +#include "config/map.h" +#include "config/option.h" diff --git a/include/libtrx/config/common.h b/include/libtrx/config/common.h new file mode 100644 index 0000000..a42c33e --- /dev/null +++ b/include/libtrx/config/common.h @@ -0,0 +1,27 @@ +#pragma once + +#include "../event_manager.h" +#include "../json.h" +#include "./option.h" + +#include +#include + +void Config_Init(void); +void Config_Shutdown(void); + +bool Config_Read(void); +bool Config_Write(void); + +int32_t Config_SubscribeChanges(EVENT_LISTENER listener, void *user_data); +void Config_UnsubscribeChanges(int32_t listener_id); + +extern const char *Config_GetPath(void); + +extern void Config_Sanitize(void); +extern void Config_ApplyChanges(void); + +extern const CONFIG_OPTION *Config_GetOptionMap(void); + +extern void Config_LoadFromJSON(JSON_OBJECT *root_obj); +extern void Config_DumpToJSON(JSON_OBJECT *root_obj); diff --git a/include/libtrx/config/config.h b/include/libtrx/config/config.h deleted file mode 100644 index 5999263..0000000 --- a/include/libtrx/config/config.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "config_option.h" - -bool Config_Read(void); -bool Config_Write(void); -void Config_Sanitize(void); -void Config_ApplyChanges(void); -const CONFIG_OPTION *Config_GetOptionMap(void); diff --git a/include/libtrx/config/config_file.h b/include/libtrx/config/file.h similarity index 96% rename from include/libtrx/config/config_file.h rename to include/libtrx/config/file.h index 7760b28..2ee98ed 100644 --- a/include/libtrx/config/config_file.h +++ b/include/libtrx/config/file.h @@ -2,7 +2,7 @@ #include "../enum_str.h" #include "../json.h" -#include "config_option.h" +#include "./option.h" #include #include diff --git a/include/libtrx/config/config_map.h b/include/libtrx/config/map.h similarity index 98% rename from include/libtrx/config/config_map.h rename to include/libtrx/config/map.h index 91cdce5..d48b55d 100644 --- a/include/libtrx/config/config_map.h +++ b/include/libtrx/config/map.h @@ -1,6 +1,6 @@ #include "../enum_str.h" #include "../utils.h" -#include "config_option.h" +#include "./option.h" #include #include diff --git a/include/libtrx/config/config_option.h b/include/libtrx/config/option.h similarity index 100% rename from include/libtrx/config/config_option.h rename to include/libtrx/config/option.h diff --git a/include/libtrx/event_manager.h b/include/libtrx/event_manager.h new file mode 100644 index 0000000..3516b50 --- /dev/null +++ b/include/libtrx/event_manager.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +typedef struct { + const char *name; + const void *sender; + void *data; +} EVENT; + +typedef void (*EVENT_LISTENER)(const EVENT *, void *user_data); + +typedef struct EVENT_MANAGER EVENT_MANAGER; + +EVENT_MANAGER *EventManager_Create(void); +void EventManager_Free(EVENT_MANAGER *manager); + +int32_t EventManager_Subscribe( + EVENT_MANAGER *manager, const char *event_name, const void *sender, + EVENT_LISTENER listener, void *user_data); + +void EventManager_Unsubscribe(EVENT_MANAGER *manager, int32_t listener_id); + +void EventManager_Fire(EVENT_MANAGER *manager, const EVENT *event); diff --git a/include/libtrx/filesystem.h b/include/libtrx/filesystem.h index 3347ec4..ad7bed7 100644 --- a/include/libtrx/filesystem.h +++ b/include/libtrx/filesystem.h @@ -34,6 +34,8 @@ const char *File_GetGameDirectory(void); // only be necessary when interacting with external libraries. char *File_GetFullPath(const char *path); +char *File_GetParentDirectory(const char *path); + char *File_GuessExtension(const char *path, const char **extensions); MYFILE *File_Open(const char *path, FILE_OPEN_MODE mode); diff --git a/include/libtrx/game/clock.h b/include/libtrx/game/clock.h new file mode 100644 index 0000000..13e8d78 --- /dev/null +++ b/include/libtrx/game/clock.h @@ -0,0 +1,3 @@ +#pragma once + +extern double Clock_GetHighPrecisionCounter(void); diff --git a/include/libtrx/game/console/cmd/config.h b/include/libtrx/game/console/cmd/config.h index 3a552b9..cdf477c 100644 --- a/include/libtrx/game/console/cmd/config.h +++ b/include/libtrx/game/console/cmd/config.h @@ -1,6 +1,6 @@ #pragma once -#include "../../../config/config_option.h" +#include "../../../config/option.h" #include "../common.h" extern CONSOLE_COMMAND g_Console_Cmd_Config; diff --git a/include/libtrx/game/console/common.h b/include/libtrx/game/console/common.h index 80312bf..cb64659 100644 --- a/include/libtrx/game/console/common.h +++ b/include/libtrx/game/console/common.h @@ -3,6 +3,7 @@ #include "../types.h" #include +#include typedef enum { CR_SUCCESS, @@ -22,5 +23,19 @@ typedef struct __PACKING CONSOLE_COMMAND { COMMAND_RESULT (*proc)(const COMMAND_CONTEXT *ctx); } CONSOLE_COMMAND; +void Console_Init(void); +void Console_Shutdown(void); + +void Console_Open(void); +void Console_Close(void); +bool Console_IsOpened(void); + +void Console_ScrollLogs(void); +int32_t Console_GetVisibleLogCount(void); +int32_t Console_GetMaxLogCount(void); + void Console_Log(const char *fmt, ...); COMMAND_RESULT Console_Eval(const char *cmdline); + +void Console_Draw(void); +extern void Console_DrawBackdrop(void); diff --git a/include/libtrx/game/input.h b/include/libtrx/game/input.h new file mode 100644 index 0000000..4f5d68e --- /dev/null +++ b/include/libtrx/game/input.h @@ -0,0 +1,4 @@ +#pragma once + +extern void Input_EnterListenMode(void); +extern void Input_ExitListenMode(void); diff --git a/include/libtrx/game/ui/common.h b/include/libtrx/game/ui/common.h new file mode 100644 index 0000000..1d53742 --- /dev/null +++ b/include/libtrx/game/ui/common.h @@ -0,0 +1,24 @@ +#pragma once + +#include "./events.h" + +typedef enum { + UI_KEY_LEFT, + UI_KEY_RIGHT, + UI_KEY_HOME, + UI_KEY_END, + UI_KEY_BACK, + UI_KEY_RETURN, + UI_KEY_ESCAPE, +} UI_INPUT; + +void UI_Init(void); +void UI_Shutdown(void); + +void UI_HandleKeyDown(uint32_t key); +void UI_HandleKeyUp(uint32_t key); +void UI_HandleTextEdit(const char *text); + +extern int32_t UI_GetCanvasWidth(void); +extern int32_t UI_GetCanvasHeight(void); +extern UI_INPUT UI_TranslateInput(uint32_t system_keycode); diff --git a/include/libtrx/game/ui/events.h b/include/libtrx/game/ui/events.h new file mode 100644 index 0000000..28c699e --- /dev/null +++ b/include/libtrx/game/ui/events.h @@ -0,0 +1,17 @@ +#pragma once + +#include "../../event_manager.h" +#include "./widgets/base.h" + +typedef void (*EVENT_LISTENER)(const 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, EVENT_LISTENER listener, + void *user_data); + +void UI_Events_Unsubscribe(int32_t listener_id); + +void UI_Events_Fire(const EVENT *event); diff --git a/include/libtrx/game/ui/widgets/base.h b/include/libtrx/game/ui/widgets/base.h new file mode 100644 index 0000000..eb6388f --- /dev/null +++ b/include/libtrx/game/ui/widgets/base.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +struct UI_WIDGET; + +typedef void (*UI_WIDGET_CONTROL)(struct UI_WIDGET *self); +typedef void (*UI_WIDGET_DRAW)(struct UI_WIDGET *self); +typedef int32_t (*UI_WIDGET_GET_WIDTH)(const struct UI_WIDGET *self); +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 struct UI_WIDGET { + UI_WIDGET_CONTROL control; + UI_WIDGET_DRAW draw; + UI_WIDGET_GET_WIDTH get_width; + UI_WIDGET_GET_HEIGHT get_height; + UI_WIDGET_SET_POSITION set_position; + UI_WIDGET_FREE free; +} UI_WIDGET; + +typedef UI_WIDGET UI_WIDGET_VTABLE; diff --git a/include/libtrx/game/ui/widgets/console.h b/include/libtrx/game/ui/widgets/console.h new file mode 100644 index 0000000..26ddacd --- /dev/null +++ b/include/libtrx/game/ui/widgets/console.h @@ -0,0 +1,12 @@ +#pragma once + +#include "./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); +int32_t UI_Console_GetVisibleLogCount(UI_WIDGET *widget); +int32_t UI_Console_GetMaxLogCount(UI_WIDGET *widget); diff --git a/include/libtrx/game/ui/widgets/label.h b/include/libtrx/game/ui/widgets/label.h new file mode 100644 index 0000000..6c80119 --- /dev/null +++ b/include/libtrx/game/ui/widgets/label.h @@ -0,0 +1,19 @@ +#pragma once + +#include "./base.h" + +#define UI_LABEL_AUTO_SIZE (-1) + +extern UI_WIDGET *UI_Label_Create( + const char *text, int32_t width, int32_t height); + +extern void UI_Label_ChangeText(UI_WIDGET *widget, const char *text); +extern const char *UI_Label_GetText(UI_WIDGET *widget); +extern void UI_Label_SetSize(UI_WIDGET *widget, int32_t width, int32_t height); + +extern void UI_Label_AddFrame(UI_WIDGET *widget); +extern void UI_Label_RemoveFrame(UI_WIDGET *widget); +extern void UI_Label_Flash(UI_WIDGET *widget, bool enable, int32_t rate); +extern void UI_Label_SetScale(UI_WIDGET *widget, float scale); +extern void UI_Label_SetZIndex(UI_WIDGET *widget, int32_t z_index); +extern int32_t UI_Label_MeasureTextWidth(UI_WIDGET *widget); diff --git a/include/libtrx/game/ui/widgets/prompt.h b/include/libtrx/game/ui/widgets/prompt.h new file mode 100644 index 0000000..fbf387d --- /dev/null +++ b/include/libtrx/game/ui/widgets/prompt.h @@ -0,0 +1,12 @@ +#pragma once + +#include "./base.h" + +UI_WIDGET *UI_Prompt_Create(int32_t width, int32_t height); +void UI_Prompt_SetSize(UI_WIDGET *widget, int32_t width, int32_t height); + +void UI_Prompt_SetFocus(UI_WIDGET *widget, bool is_focused); +void UI_Prompt_Clear(UI_WIDGET *widget); + +extern const char *UI_Prompt_GetPromptChar(void); +extern int32_t UI_Prompt_GetCaretFlashRate(void); diff --git a/include/libtrx/game/ui/widgets/spacer.h b/include/libtrx/game/ui/widgets/spacer.h new file mode 100644 index 0000000..9c20e21 --- /dev/null +++ b/include/libtrx/game/ui/widgets/spacer.h @@ -0,0 +1,6 @@ +#pragma once + +#include "./base.h" + +UI_WIDGET *UI_Spacer_Create(int32_t width, int32_t height); +void UI_Spacer_SetSize(UI_WIDGET *widget, int32_t width, int32_t height); diff --git a/include/libtrx/game/ui/widgets/stack.h b/include/libtrx/game/ui/widgets/stack.h new file mode 100644 index 0000000..46b2699 --- /dev/null +++ b/include/libtrx/game/ui/widgets/stack.h @@ -0,0 +1,30 @@ +#pragma once + +#include "./base.h" + +#define UI_STACK_AUTO_SIZE (-1) + +typedef enum { + UI_STACK_LAYOUT_HORIZONTAL, + UI_STACK_LAYOUT_VERTICAL, +} UI_STACK_LAYOUT; + +typedef enum { + UI_STACK_H_ALIGN_LEFT, + UI_STACK_H_ALIGN_CENTER, + UI_STACK_H_ALIGN_RIGHT, +} UI_STACK_H_ALIGN; + +typedef enum { + UI_STACK_V_ALIGN_TOP, + UI_STACK_V_ALIGN_CENTER, + UI_STACK_V_ALIGN_BOTTOM, +} UI_STACK_V_ALIGN; + +UI_WIDGET *UI_Stack_Create( + UI_STACK_LAYOUT layout, int32_t width, int32_t height); +void UI_Stack_SetHAlign(UI_WIDGET *self, UI_STACK_H_ALIGN align); +void UI_Stack_SetVAlign(UI_WIDGET *self, UI_STACK_V_ALIGN align); +void UI_Stack_AddChild(UI_WIDGET *self, UI_WIDGET *child); +void UI_Stack_SetSize(UI_WIDGET *widget, int32_t width, int32_t height); +void UI_Stack_DoLayout(UI_WIDGET *self); diff --git a/include/libtrx/game/ui/widgets/window.h b/include/libtrx/game/ui/widgets/window.h new file mode 100644 index 0000000..7d7089e --- /dev/null +++ b/include/libtrx/game/ui/widgets/window.h @@ -0,0 +1,7 @@ +#pragma once + +#include "./base.h" + +extern UI_WIDGET *UI_Window_Create( + UI_WIDGET *root, int32_t border_top, int32_t border_right, + int32_t border_bottom, int32_t border_left); diff --git a/meson.build b/meson.build index 51bae37..b740a2d 100644 --- a/meson.build +++ b/meson.build @@ -59,12 +59,14 @@ endif sources = [ 'src/benchmark.c', - 'src/config/config_file.c', + 'src/config/common.c', + 'src/config/file.c', 'src/engine/audio.c', 'src/engine/audio_sample.c', 'src/engine/audio_stream.c', 'src/engine/image.c', 'src/enum_str.c', + 'src/event_manager.c', 'src/filesystem.c', 'src/game/backpack.c', 'src/game/console/cmd/config.c', @@ -81,9 +83,15 @@ sources = [ 'src/game/console/cmd/set_health.c', 'src/game/console/cmd/sfx.c', 'src/game/console/cmd/teleport.c', - 'src/game/game_string.c', 'src/game/console/common.c', + 'src/game/game_string.c', 'src/game/items.c', + 'src/game/ui/common.c', + 'src/game/ui/events.c', + 'src/game/ui/widgets/console.c', + 'src/game/ui/widgets/prompt.c', + 'src/game/ui/widgets/spacer.c', + 'src/game/ui/widgets/stack.c', 'src/gfx/2d/2d_renderer.c', 'src/gfx/2d/2d_surface.c', 'src/gfx/3d/3d_renderer.c', diff --git a/src/config/common.c b/src/config/common.c new file mode 100644 index 0000000..55e8801 --- /dev/null +++ b/src/config/common.c @@ -0,0 +1,60 @@ +#include "config/common.h" + +#include "config/file.h" + +#include + +EVENT_MANAGER *m_EventManager = NULL; + +void Config_Init(void) +{ + m_EventManager = EventManager_Create(); +} + +void Config_Shutdown(void) +{ + EventManager_Free(m_EventManager); + m_EventManager = NULL; +} + +bool Config_Read(void) +{ + const bool result = ConfigFile_Read(Config_GetPath(), &Config_LoadFromJSON); + if (result) { + Config_Sanitize(); + Config_ApplyChanges(); + } + return result; +} + +bool Config_Write(void) +{ + Config_Sanitize(); + const bool updated = ConfigFile_Write(Config_GetPath(), &Config_DumpToJSON); + if (updated) { + Config_ApplyChanges(); + if (m_EventManager != NULL) { + const EVENT event = { + .name = "write", + .sender = NULL, + .data = NULL, + }; + EventManager_Fire(m_EventManager, &event); + } + } + return updated; +} + +int32_t Config_SubscribeChanges( + const EVENT_LISTENER listener, void *const user_data) +{ + assert(m_EventManager != NULL); + return EventManager_Subscribe( + m_EventManager, "write", NULL, listener, user_data); +} + +void Config_UnsubscribeChanges(const int32_t listener_id) +{ + assert(m_EventManager != NULL); + return EventManager_Unsubscribe(m_EventManager, listener_id); +} diff --git a/src/config/config_file.c b/src/config/file.c similarity index 92% rename from src/config/config_file.c rename to src/config/file.c index 187d3b5..84acb54 100644 --- a/src/config/config_file.c +++ b/src/config/file.c @@ -1,4 +1,4 @@ -#include "config/config_file.h" +#include "config/file.h" #include "filesystem.h" #include "log.h" @@ -83,17 +83,24 @@ bool ConfigFile_Write(const char *path, void (*dump)(JSON_OBJECT *root_obj)) { LOG_INFO("Saving user settings"); - MYFILE *fp = File_Open(path, FILE_OPEN_WRITE); - if (!fp) { - return false; - } + char *old_data; + File_Load(path, &old_data, NULL); + bool updated = false; char *data = M_WriteToJSON(dump); - File_WriteData(fp, data, strlen(data)); - File_Close(fp); - Memory_FreePointer(&data); + if (old_data == NULL || strcmp(data, old_data) != 0) { + MYFILE *const fp = File_Open(path, FILE_OPEN_WRITE); + if (fp == NULL) { + LOG_ERROR("Failed to write settings!"); + } else { + File_WriteData(fp, data, strlen(data)); + File_Close(fp); + updated = true; + } + } - return true; + Memory_FreePointer(&data); + return updated; } void ConfigFile_LoadOptions(JSON_OBJECT *root_obj, const CONFIG_OPTION *options) diff --git a/src/event_manager.c b/src/event_manager.c new file mode 100644 index 0000000..0a74ae0 --- /dev/null +++ b/src/event_manager.c @@ -0,0 +1,73 @@ +#include "event_manager.h" + +#include "memory.h" +#include "vector.h" + +#include +#include + +typedef struct { + int32_t listener_id; + const char *event_name; + const void *sender; + EVENT_LISTENER listener; + void *user_data; +} M_LISTENER; + +typedef struct EVENT_MANAGER { + VECTOR *listeners; + int32_t listener_id; +} EVENT_MANAGER; + +EVENT_MANAGER *EventManager_Create(void) +{ + EVENT_MANAGER *manager = Memory_Alloc(sizeof(EVENT_MANAGER)); + manager->listeners = Vector_Create(sizeof(M_LISTENER)); + manager->listener_id = 0; + return manager; +} + +void EventManager_Free(EVENT_MANAGER *const manager) +{ + Vector_Free(manager->listeners); + Memory_Free(manager); +} + +int32_t EventManager_Subscribe( + EVENT_MANAGER *const manager, const char *const event_name, + const void *const sender, const EVENT_LISTENER listener, + void *const user_data) +{ + M_LISTENER entry = { + .listener_id = manager->listener_id++, + .event_name = event_name, + .sender = sender, + .listener = listener, + .user_data = user_data, + }; + Vector_Add(manager->listeners, &entry); + return entry.listener_id; +} + +void EventManager_Unsubscribe( + EVENT_MANAGER *const manager, const int32_t listener_id) +{ + for (int32_t i = 0; i < manager->listeners->count; i++) { + M_LISTENER entry = *(M_LISTENER *)Vector_Get(manager->listeners, i); + if (entry.listener_id == listener_id) { + Vector_RemoveAt(manager->listeners, i); + return; + } + } +} + +void EventManager_Fire(EVENT_MANAGER *const manager, const EVENT *const event) +{ + for (int32_t i = 0; i < manager->listeners->count; i++) { + M_LISTENER entry = *(M_LISTENER *)Vector_Get(manager->listeners, i); + if (strcmp(entry.event_name, event->name) == 0 + && entry.sender == event->sender) { + entry.listener(event, entry.user_data); + } + } +} diff --git a/src/filesystem.c b/src/filesystem.c index 47e27df..a8f03bf 100644 --- a/src/filesystem.c +++ b/src/filesystem.c @@ -3,6 +3,7 @@ #include "log.h" #include "memory.h" #include "strings.h" +#include "utils.h" #include #include @@ -190,6 +191,17 @@ char *File_GetFullPath(const char *path) return full_path; } +char *File_GetParentDirectory(const char *path) +{ + char *full_path = File_GetFullPath(path); + char *const last_delim = + MAX(strrchr(full_path, '/'), strrchr(full_path, '\\')); + if (last_delim != NULL) { + *last_delim = '\0'; + } + return full_path; +} + char *File_GuessExtension(const char *path, const char **extensions) { if (!File_Exists(path)) { @@ -382,9 +394,12 @@ void File_Close(MYFILE *file) bool File_Load(const char *path, char **output_data, size_t *output_size) { + assert(output_data != NULL); + MYFILE *fp = File_Open(path, FILE_OPEN_READ); if (!fp) { LOG_ERROR("Can't open file %s", path); + *output_data = NULL; return false; } @@ -392,6 +407,7 @@ bool File_Load(const char *path, char **output_data, size_t *output_size) char *data = Memory_Alloc(data_size + 1); File_ReadData(fp, data, data_size); if (File_Pos(fp) != data_size) { + *output_data = NULL; LOG_ERROR("Can't read file %s", path); Memory_FreePointer(&data); File_Close(fp); @@ -401,7 +417,7 @@ bool File_Load(const char *path, char **output_data, size_t *output_size) data[data_size] = '\0'; *output_data = data; - if (output_size) { + if (output_size != NULL) { *output_size = data_size; } return true; diff --git a/src/game/console/cmd/config.c b/src/game/console/cmd/config.c index 2c11add..ef184fc 100644 --- a/src/game/console/cmd/config.c +++ b/src/game/console/cmd/config.c @@ -1,7 +1,7 @@ #include "game/console/cmd/config.h" -#include "config/config.h" -#include "config/config_map.h" +#include "config/common.h" +#include "config/map.h" #include "game/game_string.h" #include "memory.h" #include "strings.h" @@ -234,9 +234,7 @@ COMMAND_RESULT Console_Cmd_Config_Helper( } if (M_SetCurrentValue(option, new_value)) { - Config_Sanitize(); Config_Write(); - Config_ApplyChanges(); char final_value[128]; assert(M_GetCurrentValue(option, final_value, 128)); diff --git a/src/game/console/common.c b/src/game/console/common.c index 57de5c6..e1a942b 100644 --- a/src/game/console/common.c +++ b/src/game/console/common.c @@ -2,6 +2,7 @@ #include "./extern.h" #include "game/game_string.h" +#include "game/ui/widgets/console.h" #include "log.h" #include "memory.h" #include "strings.h" @@ -11,6 +12,9 @@ #include #include +static bool m_IsOpened = false; +static UI_WIDGET *m_Console; + static void M_LogMultiline(const char *text); static void M_Log(const char *text); @@ -40,7 +44,57 @@ static void M_LogMultiline(const char *const text) static void M_Log(const char *text) { assert(text != NULL); - Console_LogImpl(text); + UI_Console_HandleLog(m_Console, text); +} + +void Console_Init(void) +{ + m_Console = UI_Console_Create(); +} + +void Console_Shutdown(void) +{ + if (m_Console != NULL) { + m_Console->free(m_Console); + m_Console = NULL; + } + + m_IsOpened = false; +} + +void Console_Open(void) +{ + if (m_IsOpened) { + UI_Console_HandleClose(m_Console); + } + m_IsOpened = true; + UI_Console_HandleOpen(m_Console); +} + +void Console_Close(void) +{ + UI_Console_HandleClose(m_Console); + m_IsOpened = false; +} + +bool Console_IsOpened(void) +{ + return m_IsOpened; +} + +void Console_ScrollLogs(void) +{ + UI_Console_ScrollLogs(m_Console); +} + +int32_t Console_GetVisibleLogCount(void) +{ + return UI_Console_GetVisibleLogCount(m_Console); +} + +int32_t Console_GetMaxLogCount(void) +{ + return UI_Console_GetMaxLogCount(m_Console); } void Console_Log(const char *fmt, ...) @@ -118,3 +172,18 @@ COMMAND_RESULT Console_Eval(const char *const cmdline) } return result; } + +void Console_Draw(void) +{ + if (m_Console == NULL) { + return; + } + + Console_ScrollLogs(); + + if (Console_IsOpened() || Console_GetVisibleLogCount() > 0) { + Console_DrawBackdrop(); + } + + m_Console->draw(m_Console); +} diff --git a/src/game/console/extern.h b/src/game/console/extern.h index 8e46987..c20bc32 100644 --- a/src/game/console/extern.h +++ b/src/game/console/extern.h @@ -5,5 +5,4 @@ #include extern int32_t Console_GetMaxLineLength(void); -extern void Console_LogImpl(const char *text); extern CONSOLE_COMMAND **Console_GetCommands(void); diff --git a/src/game/ui/common.c b/src/game/ui/common.c new file mode 100644 index 0000000..eefa861 --- /dev/null +++ b/src/game/ui/common.c @@ -0,0 +1,43 @@ +#include "game/ui/common.h" + +#include "vector.h" + +void UI_Init(void) +{ + UI_Events_Init(); +} + +void UI_Shutdown(void) +{ + UI_Events_Shutdown(); +} + +void UI_HandleKeyDown(const uint32_t key) +{ + const EVENT event = { + .name = "key_down", + .sender = NULL, + .data = (void *)UI_TranslateInput(key), + }; + UI_Events_Fire(&event); +} + +void UI_HandleKeyUp(const uint32_t key) +{ + const EVENT event = { + .name = "key_up", + .sender = NULL, + .data = (void *)UI_TranslateInput(key), + }; + UI_Events_Fire(&event); +} + +void UI_HandleTextEdit(const char *const text) +{ + const EVENT event = { + .name = "text_edit", + .sender = NULL, + .data = (void *)text, + }; + UI_Events_Fire(&event); +} diff --git a/src/game/ui/events.c b/src/game/ui/events.c new file mode 100644 index 0000000..dba8442 --- /dev/null +++ b/src/game/ui/events.c @@ -0,0 +1,49 @@ +#include "game/ui/events.h" + +#include "config/common.h" + +#include + +static EVENT_MANAGER *m_EventManager = NULL; + +static void M_HandleConfigChange(const EVENT *event, void *data); + +static void M_HandleConfigChange(const EVENT *const event, void *const data) +{ + const EVENT new_event = { + .name = "canvas_resize", + .sender = NULL, + .data = NULL, + }; + EventManager_Fire(m_EventManager, &new_event); +} + +void UI_Events_Init(void) +{ + m_EventManager = EventManager_Create(); + Config_SubscribeChanges(M_HandleConfigChange, NULL); +} + +void UI_Events_Shutdown(void) +{ + EventManager_Free(m_EventManager); + m_EventManager = NULL; +} + +int32_t UI_Events_Subscribe( + const char *const event_name, const UI_WIDGET *const sender, + const EVENT_LISTENER listener, void *const user_data) +{ + return EventManager_Subscribe( + m_EventManager, event_name, sender, listener, user_data); +} + +void UI_Events_Unsubscribe(const int32_t listener_id) +{ + EventManager_Unsubscribe(m_EventManager, listener_id); +} + +void UI_Events_Fire(const EVENT *const event) +{ + EventManager_Fire(m_EventManager, event); +} diff --git a/src/game/ui/widgets/console.c b/src/game/ui/widgets/console.c new file mode 100644 index 0000000..c859a25 --- /dev/null +++ b/src/game/ui/widgets/console.c @@ -0,0 +1,235 @@ +#include "game/ui/widgets/console.h" + +#include "game/clock.h" +#include "game/console/common.h" +#include "game/ui/common.h" +#include "game/ui/events.h" +#include "game/ui/widgets/label.h" +#include "game/ui/widgets/prompt.h" +#include "game/ui/widgets/spacer.h" +#include "game/ui/widgets/stack.h" +#include "memory.h" +#include "utils.h" + +#include + +#define WINDOW_MARGIN 5 +#define LOG_HEIGHT 16 +#define LOG_MARGIN 10 +#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; + UI_WIDGET *spacer; + char *log_lines; + int32_t logs_on_screen; + + int32_t listener1; + int32_t listener2; + int32_t listener3; + + struct { + double expire_at; + UI_WIDGET *label; + } logs[MAX_LOG_LINES]; +} UI_CONSOLE; + +static void M_HandlePromptCancel(const EVENT *event, void *data); +static void M_HandlePromptConfirm(const EVENT *event, void *data); +static void M_HandleCanvasResize(const EVENT *event, void *data); +static void M_UpdateLogCount(UI_CONSOLE *self); + +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_Draw(UI_CONSOLE *self); +static void M_Free(UI_CONSOLE *self); + +static void M_HandlePromptCancel(const EVENT *const event, void *const data) +{ + Console_Close(); +} + +static void M_HandlePromptConfirm(const EVENT *const event, void *const data) +{ + const char *text = event->data; + Console_Eval(text); + Console_Close(); +} + +static void M_HandleCanvasResize(const EVENT *event, void *data) +{ + UI_CONSOLE *const self = (UI_CONSOLE *)data; + UI_Stack_SetSize(self->container, M_GetWidth(self), M_GetHeight(self)); +} + +static void M_UpdateLogCount(UI_CONSOLE *const self) +{ + self->logs_on_screen = 0; + for (int32_t i = MAX_LOG_LINES - 1; i >= 0; i--) { + if (self->logs[i].expire_at) { + self->logs_on_screen = i + 1; + break; + } + } +} + +static int32_t M_GetWidth(const UI_CONSOLE *const self) +{ + return UI_GetCanvasWidth() - 2 * WINDOW_MARGIN; +} + +static int32_t M_GetHeight(const UI_CONSOLE *const self) +{ + return UI_GetCanvasHeight() - 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) +{ + if (self->container->control != NULL) { + self->container->control(self->container); + } +} + +static void M_Draw(UI_CONSOLE *const self) +{ + if (self->container->draw != NULL) { + self->container->draw(self->container); + } +} + +static void M_Free(UI_CONSOLE *const self) +{ + self->spacer->free(self->spacer); + self->prompt->free(self->prompt); + self->container->free(self->container); + UI_Events_Unsubscribe(self->listener1); + UI_Events_Unsubscribe(self->listener2); + UI_Events_Unsubscribe(self->listener3); + Memory_Free(self); +} + +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, + .draw = (UI_WIDGET_DRAW)M_Draw, + .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, M_GetWidth(self), M_GetHeight(self)); + UI_Stack_SetVAlign(self->container, UI_STACK_V_ALIGN_BOTTOM); + + for (int32_t i = MAX_LOG_LINES - 1; i >= 0; i--) { + self->logs[i].label = + UI_Label_Create("", UI_LABEL_AUTO_SIZE, LOG_HEIGHT * LOG_SCALE); + UI_Label_SetScale(self->logs[i].label, LOG_SCALE); + UI_Stack_AddChild(self->container, self->logs[i].label); + } + + self->spacer = UI_Spacer_Create(LOG_MARGIN, LOG_MARGIN); + UI_Stack_AddChild(self->container, self->spacer); + + self->prompt = UI_Prompt_Create(UI_LABEL_AUTO_SIZE, UI_LABEL_AUTO_SIZE); + UI_Stack_AddChild(self->container, self->prompt); + + 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); + self->listener3 = + UI_Events_Subscribe("canvas_resize", NULL, M_HandleCanvasResize, self); + + 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); + M_UpdateLogCount(self); +} + +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--; + } + + if (need_layout) { + M_UpdateLogCount(self); + UI_Stack_DoLayout(self->container); + } +} + +int32_t UI_Console_GetVisibleLogCount(UI_WIDGET *const widget) +{ + UI_CONSOLE *const self = (UI_CONSOLE *)widget; + return self->logs_on_screen; +} + +int32_t UI_Console_GetMaxLogCount(UI_WIDGET *const widget) +{ + return MAX_LOG_LINES; +} diff --git a/src/game/ui/widgets/prompt.c b/src/game/ui/widgets/prompt.c new file mode 100644 index 0000000..2259247 --- /dev/null +++ b/src/game/ui/widgets/prompt.c @@ -0,0 +1,307 @@ +#include "game/ui/widgets/prompt.h" + +#include "game/input.h" +#include "game/ui/common.h" +#include "game/ui/events.h" +#include "game/ui/widgets/label.h" +#include "memory.h" +#include "strings.h" + +#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_Draw(UI_PROMPT *self); +static void M_Free(UI_PROMPT *self); +static void M_HandleKeyDown(const EVENT *event, void *user_data); +static void M_HandleTextEdit(const 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) +{ + if (self->label->control != NULL) { + self->label->control(self->label); + } + if (self->caret->control != NULL) { + self->caret->control(self->caret); + } +} + +static void M_Draw(UI_PROMPT *const self) +{ + if (self->label->draw != NULL) { + self->label->draw(self->label); + } + if (self->caret->draw != NULL) { + self->caret->draw(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 EVENT event = { + .name = "confirm", + .sender = self, + .data = self->current_text, + }; + UI_Events_Fire(&event); + M_Clear(self); + M_UpdateCaretLabel(self); +} + +static void M_Cancel(UI_PROMPT *const self) +{ + const EVENT event = { + .name = "cancel", + .sender = 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 EVENT *const event, void *const user_data) +{ + const UI_INPUT key = (UI_INPUT)(uintptr_t)event->data; + UI_PROMPT *const self = user_data; + + if (!self->is_focused) { + return; + } + + // clang-format off + switch (key) { + case UI_KEY_LEFT: M_MoveCaretLeft(self); break; + case UI_KEY_RIGHT: M_MoveCaretRight(self); break; + case UI_KEY_HOME: M_MoveCaretStart(self); break; + case UI_KEY_END: M_MoveCaretEnd(self); break; + case UI_KEY_BACK: M_DeleteCharBack(self); break; + case UI_KEY_RETURN: M_Confirm(self); break; + case UI_KEY_ESCAPE: M_Cancel(self); break; + } + // clang-format on +} + +static void M_HandleTextEdit(const EVENT *const event, void *const user_data) +{ + const char *insert_string = event->data; + const size_t insert_length = strlen(insert_string); + UI_PROMPT *const self = user_data; + + if (!self->is_focused) { + return; + } + + 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, + .draw = (UI_WIDGET_DRAW)M_Draw, + .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->label = UI_Label_Create(self->current_text, width, height); + self->caret = UI_Label_Create("", width, height); + UI_Label_SetZIndex(self->label, 16); + UI_Label_SetZIndex(self->caret, 8); + self->is_focused = false; + + self->listener1 = + UI_Events_Subscribe("key_down", NULL, M_HandleKeyDown, self); + self->listener2 = + UI_Events_Subscribe("text_edit", NULL, M_HandleTextEdit, self); + + return (UI_WIDGET *)self; +} + +void UI_Prompt_SetSize( + UI_WIDGET *const widget, const int32_t width, const int32_t height) +{ + UI_PROMPT *const self = (UI_PROMPT *)widget; + UI_Label_SetSize(self->label, width, height); +} + +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, UI_Prompt_GetPromptChar()); + UI_Label_Flash(self->caret, 1, UI_Prompt_GetCaretFlashRate()); + } 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/spacer.c b/src/game/ui/widgets/spacer.c new file mode 100644 index 0000000..ac58b38 --- /dev/null +++ b/src/game/ui/widgets/spacer.c @@ -0,0 +1,60 @@ +#include "game/ui/widgets/spacer.h" + +#include "memory.h" + +typedef struct { + UI_WIDGET_VTABLE vtable; + int32_t width; + int32_t height; +} UI_SPACER; + +static int32_t M_GetWidth(const UI_SPACER *self); +static int32_t M_GetHeight(const UI_SPACER *self); +static void M_SetPosition(UI_SPACER *self, int32_t x, int32_t y); +static void M_Control(UI_SPACER *self); +static void M_Draw(UI_SPACER *self); +static void M_Free(UI_SPACER *self); + +static int32_t M_GetWidth(const UI_SPACER *const self) +{ + return self->width; +} + +static int32_t M_GetHeight(const UI_SPACER *const self) +{ + return self->height; +} + +static void M_SetPosition( + UI_SPACER *const self, const int32_t x, const int32_t y) +{ +} + +static void M_Free(UI_SPACER *const self) +{ + Memory_Free(self); +} + +UI_WIDGET *UI_Spacer_Create(const int32_t width, const int32_t height) +{ + UI_SPACER *const self = Memory_Alloc(sizeof(UI_SPACER)); + self->vtable = (UI_WIDGET_VTABLE) { + .control = NULL, + .draw = NULL, + .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->width = width; + self->height = height; + return (UI_WIDGET *)self; +} + +void UI_Spacer_SetSize( + UI_WIDGET *const widget, const int32_t width, const int32_t height) +{ + UI_SPACER *const self = (UI_SPACER *)widget; + self->width = width; + self->height = height; +} diff --git a/src/game/ui/widgets/stack.c b/src/game/ui/widgets/stack.c new file mode 100644 index 0000000..72794b1 --- /dev/null +++ b/src/game/ui/widgets/stack.c @@ -0,0 +1,255 @@ +#include "game/ui/widgets/stack.h" + +#include "memory.h" +#include "utils.h" +#include "vector.h" + +typedef struct { + UI_WIDGET_VTABLE vtable; + + struct { + UI_STACK_H_ALIGN h; + UI_STACK_V_ALIGN v; + } align; + int32_t width; + int32_t height; + int32_t x; + int32_t y; + UI_STACK_LAYOUT layout; + VECTOR *children; +} UI_STACK; + +static int32_t M_GetChildrenWidth(const UI_STACK *self); +static int32_t M_GetChildrenHeight(const UI_STACK *self); +static int32_t M_GetHeight(const UI_STACK *self); +static int32_t M_GetWidth(const UI_STACK *self); +static void M_SetPosition(UI_STACK *self, int32_t x, int32_t y); +static void M_Control(UI_STACK *self); +static void M_Draw(UI_STACK *self); +static void M_Free(UI_STACK *self); + +static int32_t M_GetChildrenWidth(const UI_STACK *const self) +{ + int32_t result = 0; + for (int32_t i = 0; i < self->children->count; i++) { + const UI_WIDGET *const child = + *(UI_WIDGET **)Vector_Get(self->children, i); + switch (self->layout) { + case UI_STACK_LAYOUT_HORIZONTAL: + result += child->get_width(child); + break; + case UI_STACK_LAYOUT_VERTICAL: + result = MAX(result, child->get_width(child)); + break; + } + } + return result; +} + +static int32_t M_GetChildrenHeight(const UI_STACK *const self) +{ + int32_t result = 0; + for (int32_t i = 0; i < self->children->count; i++) { + const UI_WIDGET *const child = + *(UI_WIDGET **)Vector_Get(self->children, i); + switch (self->layout) { + case UI_STACK_LAYOUT_HORIZONTAL: + result = MAX(result, child->get_height(child)); + break; + case UI_STACK_LAYOUT_VERTICAL: + result += child->get_height(child); + break; + } + } + return result; +} + +static int32_t M_GetWidth(const UI_STACK *const self) +{ + if (self->width != UI_STACK_AUTO_SIZE) { + return self->width; + } + return M_GetChildrenWidth(self); +} + +static int32_t M_GetHeight(const UI_STACK *const self) +{ + if (self->height != UI_STACK_AUTO_SIZE) { + return self->height; + } + return M_GetChildrenHeight(self); +} + +static void M_SetPosition( + UI_STACK *const self, const int32_t x, const int32_t y) +{ + self->x = x; + self->y = y; + UI_Stack_DoLayout((UI_WIDGET *)self); +} + +static void M_Control(UI_STACK *const self) +{ + for (int32_t i = 0; i < self->children->count; i++) { + UI_WIDGET *const child = *(UI_WIDGET **)Vector_Get(self->children, i); + if (child->control != NULL) { + child->control(child); + } + } +} + +static void M_Draw(UI_STACK *const self) +{ + for (int32_t i = 0; i < self->children->count; i++) { + UI_WIDGET *const child = *(UI_WIDGET **)Vector_Get(self->children, i); + if (child->draw != NULL) { + child->draw(child); + } + } +} + +static void M_Free(UI_STACK *const self) +{ + Vector_Free(self->children); + Memory_Free(self); +} + +void UI_Stack_AddChild(UI_WIDGET *const widget, UI_WIDGET *const child) +{ + UI_STACK *const self = (UI_STACK *)widget; + Vector_Add(self->children, (void *)&child); +} + +UI_WIDGET *UI_Stack_Create( + const UI_STACK_LAYOUT layout, const int32_t width, const int32_t height) +{ + UI_STACK *const self = Memory_Alloc(sizeof(UI_STACK)); + self->vtable = (UI_WIDGET_VTABLE) { + .control = (UI_WIDGET_CONTROL)M_Control, + .draw = (UI_WIDGET_DRAW)M_Draw, + .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->align.h = UI_STACK_H_ALIGN_LEFT; + self->align.v = UI_STACK_V_ALIGN_TOP; + self->width = width; + self->height = height; + self->layout = layout; + self->children = Vector_Create(sizeof(UI_WIDGET *)); + return (UI_WIDGET *)self; +} + +void UI_Stack_SetHAlign(UI_WIDGET *const widget, const UI_STACK_H_ALIGN align) +{ + UI_STACK *const self = (UI_STACK *)widget; + self->align.h = align; +} + +void UI_Stack_SetVAlign(UI_WIDGET *const widget, const UI_STACK_V_ALIGN align) +{ + UI_STACK *const self = (UI_STACK *)widget; + self->align.v = align; +} + +void UI_Stack_SetSize( + UI_WIDGET *const widget, const int32_t width, const int32_t height) +{ + UI_STACK *const self = (UI_STACK *)widget; + self->width = width; + self->height = height; + UI_Stack_DoLayout(widget); +} + +void UI_Stack_DoLayout(UI_WIDGET *const widget) +{ + UI_STACK *const self = (UI_STACK *)widget; + const int32_t self_width = M_GetWidth(self); + const int32_t self_height = M_GetHeight(self); + const int32_t children_width = M_GetChildrenWidth(self); + const int32_t children_height = M_GetChildrenHeight(self); + + // calculate main axis placement + int32_t x = -999; + int32_t y = -999; + switch (self->layout) { + case UI_STACK_LAYOUT_HORIZONTAL: + switch (self->align.h) { + case UI_STACK_H_ALIGN_LEFT: + x = self->x; + break; + case UI_STACK_H_ALIGN_CENTER: + x = self->x + (self_width - children_width) / 2; + break; + case UI_STACK_H_ALIGN_RIGHT: + x = self->x + self_width - children_width; + break; + } + break; + + case UI_STACK_LAYOUT_VERTICAL: + switch (self->align.v) { + case UI_STACK_V_ALIGN_TOP: + y = self->y; + break; + case UI_STACK_V_ALIGN_CENTER: + y = self->y + (self_height - children_height) / 2; + break; + case UI_STACK_V_ALIGN_BOTTOM: + y = self->y + self_height - children_height; + break; + } + break; + } + + for (int32_t i = 0; i < self->children->count; i++) { + UI_WIDGET *const child = *(UI_WIDGET **)Vector_Get(self->children, i); + const int32_t child_width = child->get_width(child); + const int32_t child_height = child->get_height(child); + + // calculate other axis placement + switch (self->layout) { + case UI_STACK_LAYOUT_HORIZONTAL: + switch (self->align.v) { + case UI_STACK_V_ALIGN_TOP: + y = self->y; + break; + case UI_STACK_V_ALIGN_CENTER: + y = self->y + (self_height - child_height) / 2; + break; + case UI_STACK_V_ALIGN_BOTTOM: + y = self->y + self_height - child_height; + break; + } + break; + + case UI_STACK_LAYOUT_VERTICAL: + switch (self->align.h) { + case UI_STACK_H_ALIGN_LEFT: + x = self->x; + break; + case UI_STACK_H_ALIGN_CENTER: + x = self->x + (self_width - child_width) / 2; + break; + case UI_STACK_H_ALIGN_RIGHT: + x = self->x + self_width - child_width; + break; + } + break; + } + + child->set_position(child, x, y); + + // calculate main axis offset + switch (self->layout) { + case UI_STACK_LAYOUT_HORIZONTAL: + x += child_width; + break; + case UI_STACK_LAYOUT_VERTICAL: + y += child_height; + break; + } + } +}