uitextbox update
This commit is contained in:
@@ -33,7 +33,7 @@ namespace Game::Object {
|
|||||||
virtual void update(float deltaTime) = 0;
|
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 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
|
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
|
// Collision calls
|
||||||
// Called once when a collision begins
|
// Called once when a collision begins
|
||||||
|
|||||||
@@ -4,21 +4,45 @@
|
|||||||
#include <renderer/font.hpp>
|
#include <renderer/font.hpp>
|
||||||
#include <renderer/texture.hpp>
|
#include <renderer/texture.hpp>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <string>
|
||||||
#include <game/input.hpp>
|
#include <game/input.hpp>
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
namespace Game::Object {
|
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 {
|
class UITextBox : public Entity {
|
||||||
public:
|
public:
|
||||||
UITextBox(const std::string& name, std::shared_ptr<Renderer::Font> font, const Transform& transform, float x = 0.f, float y = 0.f);
|
UITextBox(const std::string& name, std::shared_ptr<Renderer::Font> font,
|
||||||
|
const Transform& transform,
|
||||||
|
float x = 0.f, float y = 0.f,
|
||||||
|
UITextboxConfig config = {});
|
||||||
~UITextBox() override = default;
|
~UITextBox() override = default;
|
||||||
|
|
||||||
void start() override;
|
void start() override;
|
||||||
void update(float deltaTime) override;
|
void update(float deltaTime) override;
|
||||||
|
void render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config) override;
|
||||||
|
|
||||||
void setText(const std::string& text);
|
void setText(const std::string& text);
|
||||||
std::string getText() const;
|
std::string getText() const;
|
||||||
std::string getValue() const;
|
std::string getValue() const;
|
||||||
bool isFocused() const;
|
bool isFocused() const;
|
||||||
|
|
||||||
void setPosition(float x, float y) { mX = x; mY = y; }
|
void setPosition(float x, float y) { mX = x; mY = y; }
|
||||||
std::pair<float, float> getPosition() const { return {mX, mY}; }
|
std::pair<float, float> getPosition() const { return {mX, mY}; }
|
||||||
@@ -33,5 +57,9 @@ namespace Game::Object {
|
|||||||
float mBoxWidth = 0.f;
|
float mBoxWidth = 0.f;
|
||||||
float mBoxHeight = 0.f;
|
float mBoxHeight = 0.f;
|
||||||
bool mNeedsTextRefresh = true;
|
bool mNeedsTextRefresh = true;
|
||||||
|
UITextboxConfig mConfig;
|
||||||
|
|
||||||
|
float mCursorTimer = 0.f;
|
||||||
|
bool mCursorVisible = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <renderer/renderer.hpp>
|
#include <renderer/renderer.hpp>
|
||||||
#include <renderer/texture.hpp>
|
#include <renderer/texture.hpp>
|
||||||
#include <renderer/font.hpp>
|
#include <renderer/font.hpp>
|
||||||
|
#include <object/ui/uitextbox.hpp>
|
||||||
#include <game/agame/sampletextbox.hpp>
|
#include <game/agame/sampletextbox.hpp>
|
||||||
#include <object/components/boxcollider.hpp>
|
#include <object/components/boxcollider.hpp>
|
||||||
|
|
||||||
@@ -27,6 +28,9 @@ int main() {
|
|||||||
player->addComponent<Object::Components::BoxCollider>();
|
player->addComponent<Object::Components::BoxCollider>();
|
||||||
//State::GameState::getInstance().addEntity(std::make_unique<AGame::Player>("Player2", std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 128, "Roboto"), t1));
|
//State::GameState::getInstance().addEntity(std::make_unique<AGame::Player>("Player2", std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 128, "Roboto"), t1));
|
||||||
|
|
||||||
|
// Sample textbox
|
||||||
|
State::GameState::getInstance().addEntity(std::make_unique<AGame::SampleTextBox>("Sample Text Box", std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 48, "Roboto"), Object::DEFAULT_TRANSFORM, 640.f, 360.f));
|
||||||
|
|
||||||
window.run();
|
window.run();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
#include <object/ui/uitextbox.hpp>
|
#include <object/ui/uitextbox.hpp>
|
||||||
|
#include <renderer/renderer.hpp>
|
||||||
|
|
||||||
namespace Game::Object {
|
namespace Game::Object {
|
||||||
UITextBox::UITextBox(const std::string& name, std::shared_ptr<Renderer::Font> 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<Renderer::Font> font,
|
||||||
|
const Transform& transform, float x, float y, UITextboxConfig config)
|
||||||
|
: Entity(name, font, transform), mX(x), mY(y), mConfig(config) { }
|
||||||
|
|
||||||
void UITextBox::start() {
|
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;
|
mTransform.y -= mTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||||
|
|
||||||
// Keep a stable interaction area even when current text is empty.
|
mBoxWidth = static_cast<float>(mTex->getWidth()) * mTransform.adjustedScaleX();
|
||||||
mBoxWidth = static_cast<float>(mTex->getWidth()) * mTransform.adjustedScaleX();
|
|
||||||
mBoxHeight = static_cast<float>(mTex->getHeight()) * mTransform.adjustedScaleY();
|
mBoxHeight = static_cast<float>(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();
|
refreshVisualText();
|
||||||
}
|
}
|
||||||
@@ -26,34 +28,39 @@ namespace Game::Object {
|
|||||||
mNeedsTextRefresh = true;
|
mNeedsTextRefresh = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mIsFocused) {
|
if (mIsFocused) {
|
||||||
if (mNeedsTextRefresh) {
|
// Cursor blink
|
||||||
refreshVisualText();
|
mCursorTimer += deltaTime;
|
||||||
}
|
if (mCursorTimer >= mConfig.cursorBlinkRate) {
|
||||||
return;
|
mCursorTimer = 0.f;
|
||||||
}
|
mCursorVisible = !mCursorVisible;
|
||||||
|
|
||||||
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<SDL_Scancode>(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<char>(keycode));
|
|
||||||
mNeedsTextRefresh = true;
|
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<SDL_Scancode>(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<int>(mText.size()) < mConfig.maxLength) {
|
||||||
|
mText.push_back(static_cast<char>(keycode));
|
||||||
|
mNeedsTextRefresh = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mNeedsTextRefresh) {
|
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) {
|
void UITextBox::setText(const std::string& text) {
|
||||||
mText = text;
|
mText = text;
|
||||||
mNeedsTextRefresh = true;
|
mNeedsTextRefresh = true;
|
||||||
refreshVisualText();
|
refreshVisualText();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string UITextBox::getText() const {
|
std::string UITextBox::getText() const { return mText; }
|
||||||
return mText;
|
std::string UITextBox::getValue() const { return mText; }
|
||||||
}
|
bool UITextBox::isFocused() const { return mIsFocused; }
|
||||||
|
|
||||||
std::string UITextBox::getValue() const {
|
|
||||||
return mText;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool UITextBox::isFocused() const {
|
|
||||||
return mIsFocused;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool UITextBox::isMouseInsideBox() const {
|
bool UITextBox::isMouseInsideBox() const {
|
||||||
const float mouseX = Input::getMouseX();
|
const float mouseX = Input::getMouseX();
|
||||||
const float mouseY = Input::getMouseY();
|
const float mouseY = Input::getMouseY();
|
||||||
|
|
||||||
const float left = mTransform.x;
|
const float left = mTransform.x - mConfig.paddingX;
|
||||||
const float right = mTransform.x + mBoxWidth;
|
const float right = left + mBoxWidth + 2.f * mConfig.paddingX;
|
||||||
const float top = mTransform.y;
|
const float top = mTransform.y - mConfig.paddingY;
|
||||||
const float bottom = mTransform.y + mBoxHeight;
|
const float bottom = top + mBoxHeight + 2.f * mConfig.paddingY;
|
||||||
|
|
||||||
return mouseX >= left && mouseX <= right && mouseY >= top && mouseY <= bottom;
|
return mouseX >= left && mouseX <= right && mouseY >= top && mouseY <= bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITextBox::refreshVisualText() {
|
void UITextBox::refreshVisualText() {
|
||||||
SDL_Color color = mIsFocused ? SDL_Color{255, 240, 180, 255} : SDL_Color{255, 255, 255, 255};
|
const bool showPlaceholder = mText.empty() && !mIsFocused && !mConfig.placeholder.empty();
|
||||||
std::string rendered = mText.empty() ? " " : mText;
|
|
||||||
if (mIsFocused) {
|
SDL_Color color;
|
||||||
rendered += "_";
|
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<Renderer::Font>(mTex)->build(color, rendered);
|
std::dynamic_pointer_cast<Renderer::Font>(mTex)->build(color, rendered);
|
||||||
|
|||||||
Reference in New Issue
Block a user