From 8be2cea49a36e5be5e69e823b80abb84cbafc8af Mon Sep 17 00:00:00 2001 From: DcruBro Date: Wed, 22 Apr 2026 08:41:29 +0200 Subject: [PATCH] uitextbox update --- include/object/entity.hpp | 2 +- include/object/ui/uitextbox.hpp | 38 ++++++-- src/main.cpp | 4 + src/object/ui/uitextbox.cpp | 148 ++++++++++++++++++++------------ 4 files changed, 133 insertions(+), 59 deletions(-) diff --git a/include/object/entity.hpp b/include/object/entity.hpp index aafb386..f50551b 100644 --- a/include/object/entity.hpp +++ b/include/object/entity.hpp @@ -33,7 +33,7 @@ namespace Game::Object { virtual void update(float deltaTime) = 0; virtual void onWindowResized(int newWidth, int newHeight) {} // Called when the window is resized, with the new width and height in pixels virtual void destroyed() {}; // Pre-destruction call - void render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config); + virtual void render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config); // Collision calls // Called once when a collision begins diff --git a/include/object/ui/uitextbox.hpp b/include/object/ui/uitextbox.hpp index 4814789..ab17666 100644 --- a/include/object/ui/uitextbox.hpp +++ b/include/object/ui/uitextbox.hpp @@ -4,21 +4,45 @@ #include #include #include +#include #include +#include namespace Game::Object { + + struct UITextboxConfig { + SDL_Color bgColor = {20, 20, 20, 210}; + SDL_Color borderColor = {110, 110, 110, 255}; + SDL_Color focusedBorderColor = {200, 175, 70, 255}; + SDL_Color textColor = {255, 255, 255, 255}; + SDL_Color focusedTextColor = {255, 240, 180, 255}; + SDL_Color placeholderColor = {120, 120, 120, 200}; + float borderThickness = 2.f; + float paddingX = 8.f; + float paddingY = 4.f; + float minWidth = 160.f; + float minHeight = 32.f; + int maxLength = 0; // 0 = unlimited + std::string placeholder = ""; + float cursorBlinkRate = 0.53f; // seconds per blink phase + }; + class UITextBox : public Entity { public: - UITextBox(const std::string& name, std::shared_ptr font, const Transform& transform, float x = 0.f, float y = 0.f); + UITextBox(const std::string& name, std::shared_ptr font, + const Transform& transform, + float x = 0.f, float y = 0.f, + UITextboxConfig config = {}); ~UITextBox() override = default; - void start() override; + void start() override; void update(float deltaTime) override; + void render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config) override; - void setText(const std::string& text); - std::string getText() const; + void setText(const std::string& text); + std::string getText() const; std::string getValue() const; - bool isFocused() const; + bool isFocused() const; void setPosition(float x, float y) { mX = x; mY = y; } std::pair getPosition() const { return {mX, mY}; } @@ -33,5 +57,9 @@ namespace Game::Object { float mBoxWidth = 0.f; float mBoxHeight = 0.f; bool mNeedsTextRefresh = true; + UITextboxConfig mConfig; + + float mCursorTimer = 0.f; + bool mCursorVisible = true; }; } diff --git a/src/main.cpp b/src/main.cpp index f4db427..22f4773 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -27,6 +28,9 @@ int main() { player->addComponent(); //State::GameState::getInstance().addEntity(std::make_unique("Player2", std::make_shared("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 128, "Roboto"), t1)); + // Sample textbox + State::GameState::getInstance().addEntity(std::make_unique("Sample Text Box", std::make_shared("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 48, "Roboto"), Object::DEFAULT_TRANSFORM, 640.f, 360.f)); + window.run(); return 0; diff --git a/src/object/ui/uitextbox.cpp b/src/object/ui/uitextbox.cpp index ef7b85c..4a1198b 100644 --- a/src/object/ui/uitextbox.cpp +++ b/src/object/ui/uitextbox.cpp @@ -1,19 +1,21 @@ #include +#include namespace Game::Object { - UITextBox::UITextBox(const std::string& name, std::shared_ptr font, const Transform& transform, float x, float y) - : Entity(name, font, transform), mX(x), mY(y) { } + + UITextBox::UITextBox(const std::string& name, std::shared_ptr font, + const Transform& transform, float x, float y, UITextboxConfig config) + : Entity(name, font, transform), mX(x), mY(y), mConfig(config) { } void UITextBox::start() { - // Center the text box on the position - mTransform.x -= mTex->getWidth() * mTransform.adjustedScaleX() / 2.f; + mTransform.x -= mTex->getWidth() * mTransform.adjustedScaleX() / 2.f; mTransform.y -= mTex->getHeight() * mTransform.adjustedScaleY() / 2.f; - // Keep a stable interaction area even when current text is empty. - mBoxWidth = static_cast(mTex->getWidth()) * mTransform.adjustedScaleX(); + mBoxWidth = static_cast(mTex->getWidth()) * mTransform.adjustedScaleX(); mBoxHeight = static_cast(mTex->getHeight()) * mTransform.adjustedScaleY(); - if (mBoxWidth <= 0.f) mBoxWidth = 180.f; - if (mBoxHeight <= 0.f) mBoxHeight = 36.f; + + if (mBoxWidth < mConfig.minWidth) mBoxWidth = mConfig.minWidth; + if (mBoxHeight < mConfig.minHeight) mBoxHeight = mConfig.minHeight; refreshVisualText(); } @@ -26,34 +28,39 @@ namespace Game::Object { mNeedsTextRefresh = true; } - if (!mIsFocused) { - if (mNeedsTextRefresh) { - refreshVisualText(); - } - return; - } - - if (Input::isKeyJustPressed(SDL_SCANCODE_BACKSPACE) && !mText.empty()) { - mText.pop_back(); - mNeedsTextRefresh = true; - } - - if (Input::isKeyJustPressed(SDL_SCANCODE_RETURN) || Input::isKeyJustPressed(SDL_SCANCODE_KP_ENTER)) { - mIsFocused = false; - mNeedsTextRefresh = true; - } - - for (int key = 0; key < SDL_SCANCODE_COUNT; ++key) { - SDL_Scancode scancode = static_cast(key); - if (!Input::isKeyJustPressed(scancode)) { - continue; - } - - SDL_Keycode keycode = SDL_GetKeyFromScancode(scancode, SDL_GetModState(), true); - if (keycode >= 32 && keycode <= 126) { - mText.push_back(static_cast(keycode)); + if (mIsFocused) { + // Cursor blink + mCursorTimer += deltaTime; + if (mCursorTimer >= mConfig.cursorBlinkRate) { + mCursorTimer = 0.f; + mCursorVisible = !mCursorVisible; mNeedsTextRefresh = true; } + + if (Input::isKeyJustPressed(SDL_SCANCODE_BACKSPACE) && !mText.empty()) { + mText.pop_back(); + mNeedsTextRefresh = true; + } + + if (Input::isKeyJustPressed(SDL_SCANCODE_RETURN) || Input::isKeyJustPressed(SDL_SCANCODE_KP_ENTER)) { + mIsFocused = false; + mCursorTimer = 0.f; + mCursorVisible = true; + mNeedsTextRefresh = true; + } + + for (int key = 0; key < SDL_SCANCODE_COUNT; ++key) { + SDL_Scancode scancode = static_cast(key); + if (!Input::isKeyJustPressed(scancode)) continue; + + SDL_Keycode keycode = SDL_GetKeyFromScancode(scancode, SDL_GetModState(), true); + if (keycode >= 32 && keycode <= 126) { + if (mConfig.maxLength == 0 || static_cast(mText.size()) < mConfig.maxLength) { + mText.push_back(static_cast(keycode)); + mNeedsTextRefresh = true; + } + } + } } if (mNeedsTextRefresh) { @@ -61,41 +68,76 @@ namespace Game::Object { } } + void UITextBox::render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config) { + if (!mIsVisible) return; + + SDL_Renderer* r = renderer->getSDLRenderer(); + + const float bx = mTransform.x - mConfig.paddingX - config.camX + config.screenW / 2.f; + const float by = mTransform.y - mConfig.paddingY - config.camY + config.screenH / 2.f; + const float bw = mBoxWidth + 2.f * mConfig.paddingX; + const float bh = mBoxHeight + 2.f * mConfig.paddingY; + const float t = mConfig.borderThickness; + + // Background + SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND); + const SDL_Color& bg = mConfig.bgColor; + SDL_SetRenderDrawColor(r, bg.r, bg.g, bg.b, bg.a); + const SDL_FRect bgRect = {bx, by, bw, bh}; + SDL_RenderFillRect(r, &bgRect); + + // Border (4 filled rects for configurable thickness) + const SDL_Color& bc = mIsFocused ? mConfig.focusedBorderColor : mConfig.borderColor; + SDL_SetRenderDrawColor(r, bc.r, bc.g, bc.b, bc.a); + const SDL_FRect borders[4] = { + {bx, by, bw, t }, // top + {bx, by + bh - t, bw, t }, // bottom + {bx, by, t, bh}, // left + {bx + bw - t, by, t, bh}, // right + }; + SDL_RenderFillRects(r, borders, 4); + + // Text (or placeholder) via base render + Entity::render(renderer, config); + } + void UITextBox::setText(const std::string& text) { mText = text; mNeedsTextRefresh = true; refreshVisualText(); } - std::string UITextBox::getText() const { - return mText; - } - - std::string UITextBox::getValue() const { - return mText; - } - - bool UITextBox::isFocused() const { - return mIsFocused; - } + std::string UITextBox::getText() const { return mText; } + std::string UITextBox::getValue() const { return mText; } + bool UITextBox::isFocused() const { return mIsFocused; } bool UITextBox::isMouseInsideBox() const { const float mouseX = Input::getMouseX(); const float mouseY = Input::getMouseY(); - const float left = mTransform.x; - const float right = mTransform.x + mBoxWidth; - const float top = mTransform.y; - const float bottom = mTransform.y + mBoxHeight; + const float left = mTransform.x - mConfig.paddingX; + const float right = left + mBoxWidth + 2.f * mConfig.paddingX; + const float top = mTransform.y - mConfig.paddingY; + const float bottom = top + mBoxHeight + 2.f * mConfig.paddingY; return mouseX >= left && mouseX <= right && mouseY >= top && mouseY <= bottom; } void UITextBox::refreshVisualText() { - SDL_Color color = mIsFocused ? SDL_Color{255, 240, 180, 255} : SDL_Color{255, 255, 255, 255}; - std::string rendered = mText.empty() ? " " : mText; - if (mIsFocused) { - rendered += "_"; + const bool showPlaceholder = mText.empty() && !mIsFocused && !mConfig.placeholder.empty(); + + SDL_Color color; + std::string rendered; + + if (showPlaceholder) { + color = mConfig.placeholderColor; + rendered = mConfig.placeholder; + } else { + color = mIsFocused ? mConfig.focusedTextColor : mConfig.textColor; + rendered = mText.empty() ? " " : mText; + if (mIsFocused && mCursorVisible) { + rendered += "_"; + } } std::dynamic_pointer_cast(mTex)->build(color, rendered);