From 27ce6526e63b3bb49c73e22e4a8b6e7a9cb7a6e3 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sat, 5 Oct 2024 09:16:41 -0500 Subject: [PATCH] Vastly improve UI --- src/dawn/display/color.c | 13 ++++- src/dawn/display/color.h | 10 ++-- src/dawn/display/frame.c | 73 ++++++++++++++------------- src/dawn/display/frame.h | 9 +++- src/dawn/display/symbol.c | 1 - src/dawn/game.c | 2 + src/dawn/rpg/entity/player.c | 4 +- src/dawn/ui/textbox.c | 84 +++++++++++++++++++++++++++++++- src/dawn/ui/textbox.h | 26 ++++++++-- src/dawnopengl/display/display.c | 32 +----------- 10 files changed, 170 insertions(+), 84 deletions(-) diff --git a/src/dawn/display/color.c b/src/dawn/display/color.c index 8bf34415..d72c2c98 100644 --- a/src/dawn/display/color.c +++ b/src/dawn/display/color.c @@ -8,6 +8,17 @@ #include "color.h" #include "assert/assert.h" +const color4f_t COLOR4F_COLOR_MAP[COLOR_COUNT] = { + COLOR4F_BLACK, + COLOR4F_WHITE, + COLOR4F(0.8f, 0.0f, 0.0f, 1.0f), + COLOR4F(0.0f, 0.5f, 0.0f, 1.0f), + COLOR4F(0.0f, 0.0f, 0.8f, 1.0f), + COLOR4F(0.8f, 0.8f, 0.0f, 1.0f), + COLOR4F(0.8f, 0.0f, 0.8f, 1.0f), + COLOR4F(0.0f, 0.8f, 0.8f, 1.0f) +}; + void color4fCopy(const color4f_t src, color4f_t dest) { memcpy(dest, src, sizeof(color4f_t)); -} +} \ No newline at end of file diff --git a/src/dawn/display/color.h b/src/dawn/display/color.h index aa18f9bc..7e9ad6b6 100644 --- a/src/dawn/display/color.h +++ b/src/dawn/display/color.h @@ -8,6 +8,9 @@ #pragma once #include "dawn.h" +typedef float_t color3f_t[3]; +typedef float_t color4f_t[4]; + // Simple colors, used by the frame system. #define COLOR_BLACK 0x00 #define COLOR_WHITE 0x01 @@ -18,10 +21,7 @@ #define COLOR_MAGENTA 0x06 #define COLOR_CYAN 0x07 -typedef float_t color3f_t[3]; -typedef float_t color4f_t[4]; - -typedef color4f_t color_t; +#define COLOR_COUNT COLOR_CYAN+1 #define COLOR3F(r,g,b) ((color3f_t){ r, g, b }) #define COLOR3F_RED COLOR3F(1, 0, 0) @@ -68,6 +68,8 @@ typedef color4f_t color_t; #define COLOR4F_TRANSPARENT_WHITE COLOR4F(1, 1, 1, 0) #define COLOR4F_TRANSPARENT COLOR4F_TRANSPARENT_BLACK +extern const color4f_t COLOR4F_COLOR_MAP[COLOR_COUNT]; + /** * Copies a color. * diff --git a/src/dawn/display/frame.c b/src/dawn/display/frame.c index 182446c6..4bf09f32 100644 --- a/src/dawn/display/frame.c +++ b/src/dawn/display/frame.c @@ -17,28 +17,7 @@ uint8_t FRAME_COLOR[FRAME_HEIGHT * FRAME_WIDTH]; void frameInit() { memset(FRAME_BUFFER, ' ', FRAME_HEIGHT * FRAME_WIDTH); - - // Draw UI top and bottom borders - for(uint16_t x = 0; x < FRAME_WIDTH; x++) { - char_t c = x == 0 || x == FRAME_WIDTH-1 ? '+' : '-'; - int32_t i = (FRAME_HEIGHT - 1) * FRAME_WIDTH + x; - FRAME_BUFFER[i] = c; - FRAME_COLOR[i] = COLOR_WHITE; - - i = (FRAME_HEIGHT - FRAME_BOTTOM_UI_HEIGHT) * FRAME_WIDTH + x; - FRAME_BUFFER[i] = c; - FRAME_COLOR[i] = COLOR_WHITE; - } - - for(uint16_t y = FRAME_HEIGHT - FRAME_BOTTOM_UI_HEIGHT + 1; y < FRAME_HEIGHT - 1; y++) { - int32_t i = y * FRAME_WIDTH; - FRAME_BUFFER[i] = '|'; - FRAME_COLOR[i] = COLOR_WHITE; - - i = y * FRAME_WIDTH + FRAME_WIDTH - 1; - FRAME_BUFFER[i] = '|'; - FRAME_COLOR[i] = COLOR_WHITE; - } + frameUIReset(); } void frameUpdate() { @@ -74,21 +53,41 @@ void frameUpdate() { FRAME_BUFFER[i++] = symbolGetCharByTile(tile); } } +} - // UI Area. - if(textboxIsOpen()) { - const char_t *text = TEXTBOX.text; - for(uint16_t x = 1; x < FRAME_WIDTH - 1; x++) { - if(*text == '\0') break; - FRAME_BUFFER[(FRAME_HEIGHT - 2) * FRAME_WIDTH + x] = *text++; - FRAME_COLOR[(FRAME_HEIGHT - 2) * FRAME_WIDTH + x] = COLOR_WHITE; - } - FRAME_BUFFER[(FRAME_HEIGHT * FRAME_WIDTH) - 4] = ' '; - FRAME_BUFFER[(FRAME_HEIGHT * FRAME_WIDTH) - 3] = '>'; - FRAME_BUFFER[(FRAME_HEIGHT * FRAME_WIDTH) - 2] = ' '; - } else { - FRAME_BUFFER[(FRAME_HEIGHT * FRAME_WIDTH) - 4] = '-'; - FRAME_BUFFER[(FRAME_HEIGHT * FRAME_WIDTH) - 3] = '-'; - FRAME_BUFFER[(FRAME_HEIGHT * FRAME_WIDTH) - 2] = '-'; +void frameUIReset() { + memset( + FRAME_BUFFER + (FRAME_HEIGHT - FRAME_BOTTOM_UI_HEIGHT) * FRAME_WIDTH, + ' ', + FRAME_WIDTH * FRAME_BOTTOM_UI_HEIGHT + ); + + memset( + FRAME_COLOR + (FRAME_HEIGHT - FRAME_BOTTOM_UI_HEIGHT) * FRAME_WIDTH, + COLOR_WHITE, + FRAME_WIDTH * FRAME_BOTTOM_UI_HEIGHT + ); + + // Draw UI top and bottom borders + for(uint16_t x = 0; x < FRAME_WIDTH; x++) { + char_t c = x == 0 || x == FRAME_WIDTH-1 ? '+' : '-'; + int32_t i = (FRAME_HEIGHT - 1) * FRAME_WIDTH + x; + FRAME_BUFFER[i] = c; + + i = (FRAME_HEIGHT - FRAME_BOTTOM_UI_HEIGHT) * FRAME_WIDTH + x; + FRAME_BUFFER[i] = c; + } + + // Draw UI left and right borders + for( + uint16_t y = FRAME_HEIGHT - FRAME_BOTTOM_UI_HEIGHT + 1; + y < FRAME_HEIGHT - 1; + y++ + ) { + int32_t i = y * FRAME_WIDTH; + FRAME_BUFFER[i] = '|'; + + i = y * FRAME_WIDTH + FRAME_WIDTH - 1; + FRAME_BUFFER[i] = '|'; } } \ No newline at end of file diff --git a/src/dawn/display/frame.h b/src/dawn/display/frame.h index c9441c87..ba8407ac 100644 --- a/src/dawn/display/frame.h +++ b/src/dawn/display/frame.h @@ -14,7 +14,7 @@ #define FRAME_WIDTH FRAME_PIXEL_WIDTH/FRAME_CHAR_SIZE #define FRAME_HEIGHT FRAME_PIXEL_HEIGHT/FRAME_CHAR_SIZE -#define FRAME_BOTTOM_UI_HEIGHT 6 +#define FRAME_BOTTOM_UI_HEIGHT 8 extern char_t FRAME_BUFFER[FRAME_HEIGHT * FRAME_WIDTH]; extern uint8_t FRAME_COLOR[FRAME_HEIGHT * FRAME_WIDTH]; @@ -27,4 +27,9 @@ void frameInit(); /** * Updates the terminal frame. */ -void frameUpdate(); \ No newline at end of file +void frameUpdate(); + +/** + * Resets the UI area of the frame. + */ +void frameUIReset(); \ No newline at end of file diff --git a/src/dawn/display/symbol.c b/src/dawn/display/symbol.c index 53b4e42a..caef0d4b 100644 --- a/src/dawn/display/symbol.c +++ b/src/dawn/display/symbol.c @@ -28,7 +28,6 @@ char_t symbolGetCharByTile(const tile_t tile) { return '#'; case TILE_ID_WATER: - // Take remainder int32_t j = (int32_t)(TIME.time * 2) % 4; switch(j) { case 0: return '/'; diff --git a/src/dawn/game.c b/src/dawn/game.c index 1c1f8c6c..0f282219 100644 --- a/src/dawn/game.c +++ b/src/dawn/game.c @@ -31,6 +31,8 @@ uint8_t gameUpdate(const float_t delta) { timeUpdate(delta); inputUpdate(); + textboxUpdate(); + if(GAME.currentMap) { mapUpdate(GAME.currentMap); } diff --git a/src/dawn/rpg/entity/player.c b/src/dawn/rpg/entity/player.c index 4cbda3df..96b694c5 100644 --- a/src/dawn/rpg/entity/player.c +++ b/src/dawn/rpg/entity/player.c @@ -30,7 +30,7 @@ void playerUpdate(entity_t *entity) { return entityWalk(entity, ENTITY_DIRECTION_EAST); } - if(inputIsDown(INPUT_BIND_ACCEPT)) { + if(inputWasPressed(INPUT_BIND_ACCEPT)) { // Check what the player is trying to interact with uint16_t x = entity->x, y = entity->y; entityDirectionOffsetAdd(entity->direction, &x, &y); @@ -45,7 +45,7 @@ void playerUpdate(entity_t *entity) { target->state = ENTITY_STATE_TALKING; entity->state = ENTITY_STATE_TALKING; - textboxSetText("Hello Player"); + textboxSetText("NPC", "Hello Player"); return; } } diff --git a/src/dawn/ui/textbox.c b/src/dawn/ui/textbox.c index d7bc964d..d999a028 100644 --- a/src/dawn/ui/textbox.c +++ b/src/dawn/ui/textbox.c @@ -7,6 +7,8 @@ #include "textbox.h" #include "assert/assert.h" +#include "input.h" +#include "time.h" textbox_t TEXTBOX; @@ -14,14 +16,92 @@ void textboxInit() { memset(&TEXTBOX, 0, sizeof(textbox_t)); } -void textboxSetText(const char_t *text) { +void textboxSetText( + const char_t *title, + const char_t *text +) { assertNotNull(text, "Text cannot be NULL."); + + // Setup text copies size_t len = strlen(text); - assertTrue(len < TEXTBOX_TEXT_MAX - 1, "Text is too long."); + assertTrue(len < TEXTBOX_TEXT_MAX, "Text is too long."); strcpy(TEXTBOX.text, text); + TEXTBOX.textLength = len; + + // Setup title + if(title) { + len = strlen(title); + assertTrue(len < TEXTBOX_TITLE_MAX, "Title is too long."); + strcpy(TEXTBOX.title, title); + } else { + TEXTBOX.title[0] = '\0'; + } + + // Reset UI. + frameUIReset(); + + // Add > indicator + FRAME_BUFFER[(FRAME_HEIGHT * FRAME_WIDTH) - 4] = ' '; + FRAME_BUFFER[(FRAME_HEIGHT * FRAME_WIDTH) - 3] = '>'; + FRAME_BUFFER[(FRAME_HEIGHT * FRAME_WIDTH) - 2] = ' '; + + // Add title + len = strlen(TEXTBOX.title); + for(size_t i = 0; i < len; i++) { + FRAME_BUFFER[ + (FRAME_HEIGHT - FRAME_BOTTOM_UI_HEIGHT) * (FRAME_WIDTH) + 2 + i + ] = TEXTBOX.title[i]; + } + + // Prepare for scrolling + TEXTBOX.nextChar = (1.0f / TEXTBOX_CHARS_PER_SECOND); + TEXTBOX.textIndex = 0; TEXTBOX.open = true; } bool_t textboxIsOpen() { return TEXTBOX.open; +} + +void textboxUpdate() { + if(!textboxIsOpen()) return; + + int32_t j; + + // Have we finished scrolling? + if(TEXTBOX.textIndex >= TEXTBOX.textLength) { + // Wait for input + if(inputWasPressed(INPUT_BIND_ACCEPT)) { + TEXTBOX.open = false; + frameUIReset(); + return; + } + + // Blinking cursor + j = (int32_t)(TIME.time * TEXTBOX_BLINKS_PER_SECOND) % 2; + if(j == 0) { + FRAME_BUFFER[(FRAME_HEIGHT * FRAME_WIDTH) - 3] = '>'; + FRAME_BUFFER[(FRAME_HEIGHT * FRAME_WIDTH) - 2] = ' '; + } else { + FRAME_BUFFER[(FRAME_HEIGHT * FRAME_WIDTH) - 3] = ' '; + FRAME_BUFFER[(FRAME_HEIGHT * FRAME_WIDTH) - 2] = '>'; + } + + return; + } + + // Update scrolling + TEXTBOX.nextChar -= TIME.delta; + if(inputIsDown(INPUT_BIND_ACCEPT)) TEXTBOX.nextChar -= TIME.delta; + if(TEXTBOX.nextChar > 0.0f) return; + + // Scroll text + uint16_t x = TEXTBOX.textIndex % (FRAME_WIDTH - 2); + uint16_t y = TEXTBOX.textIndex / (FRAME_WIDTH - 2); + FRAME_BUFFER[ + (FRAME_HEIGHT - FRAME_BOTTOM_UI_HEIGHT + 1 + y) * + (FRAME_WIDTH) + 1 + x + ] = TEXTBOX.text[TEXTBOX.textIndex]; + TEXTBOX.textIndex++; + TEXTBOX.nextChar = (1.0f / TEXTBOX_CHARS_PER_SECOND); } \ No newline at end of file diff --git a/src/dawn/ui/textbox.h b/src/dawn/ui/textbox.h index 250a18ef..3186afd9 100644 --- a/src/dawn/ui/textbox.h +++ b/src/dawn/ui/textbox.h @@ -7,12 +7,21 @@ #pragma once #include "dawn.h" +#include "display/frame.h" -#define TEXTBOX_TEXT_MAX 256 +#define TEXTBOX_TITLE_MAX 16 +#define TEXTBOX_TEXT_MAX ((FRAME_BOTTOM_UI_HEIGHT-2) * (FRAME_WIDTH - 2)) +#define TEXTBOX_CHARS_PER_SECOND 15 +#define TEXTBOX_BLINKS_PER_SECOND 2 typedef struct { - char_t text[TEXTBOX_TEXT_MAX]; + char_t title[TEXTBOX_TITLE_MAX+1]; + char_t text[TEXTBOX_TEXT_MAX+1]; bool_t open; + + size_t textIndex; + size_t textLength; + float_t nextChar; } textbox_t; extern textbox_t TEXTBOX; @@ -25,13 +34,22 @@ void textboxInit(); /** * Sets the text of the textbox, and opens it. * + * @param title Title to set. * @param text Text to set. */ -void textboxSetText(const char_t *text); +void textboxSetText( + const char_t *title, + const char_t *text +); /** * Returns whether the textbox is open. * * @return Whether the textbox is open. */ -bool_t textboxIsOpen(); \ No newline at end of file +bool_t textboxIsOpen(); + +/** + * Updates the textbox. + */ +void textboxUpdate(); \ No newline at end of file diff --git a/src/dawnopengl/display/display.c b/src/dawnopengl/display/display.c index dc890b5b..aec13512 100644 --- a/src/dawnopengl/display/display.c +++ b/src/dawnopengl/display/display.c @@ -37,42 +37,12 @@ void displayUpdate() { vec4_t uvs; vec4_t positions; uint8_t color; - color4f_t glColor; for(uint32_t x = 0; x < FRAME_WIDTH; x++) { for(uint32_t y = 0; y < FRAME_HEIGHT; y++) { char_t c = FRAME_BUFFER[y * FRAME_WIDTH + x]; if(c == '\0' || c == ' ') continue; color = FRAME_COLOR[y * FRAME_WIDTH + x]; - switch(color) { - case COLOR_BLACK: - color4fCopy(COLOR4F_BLACK, glColor); - break; - case COLOR_RED: - color4fCopy(COLOR4F_RED, glColor); - break; - case COLOR_GREEN: - color4fCopy(COLOR4F_GREEN, glColor); - break; - case COLOR_BLUE: - color4fCopy(COLOR4F_BLUE, glColor); - break; - case COLOR_YELLOW: - color4fCopy(COLOR4F_YELLOW, glColor); - break; - case COLOR_MAGENTA: - color4fCopy(COLOR4F_MAGENTA, glColor); - break; - case COLOR_CYAN: - color4fCopy(COLOR4F_CYAN, glColor); - break; - case COLOR_WHITE: - color4fCopy(COLOR4F_WHITE, glColor); - break; - default: - assertUnreachable("Unknown color: %d", color); - break; - } fontCharGetUV(c, uvs); positions[0] = x * 8; @@ -86,7 +56,7 @@ void displayUpdate() { (vec2_t){positions[2], positions[3]}, (vec2_t){uvs[0], uvs[1]}, (vec2_t){uvs[2], uvs[3]}, - glColor, + COLOR4F_COLOR_MAP[color], quadIndex * PRIMITIVE_QUAD_VERTICE_COUNT, quadIndex * PRIMITIVE_QUAD_INDICE_COUNT );