#include "UITextInput.h" #include "../Core/UIContext.h" #include "../Rendering/UIRenderer.h" #include #include namespace grove { void UITextInput::update(UIContext& ctx, float deltaTime) { // Update state based on enabled/focused flags if (!enabled) { state = TextInputState::Disabled; isFocused = false; } else if (isFocused) { state = TextInputState::Focused; // Update cursor blink animation cursorBlinkTimer += deltaTime; if (cursorBlinkTimer >= CURSOR_BLINK_INTERVAL) { cursorBlinkTimer = 0.0f; cursorVisible = !cursorVisible; } } else { state = TextInputState::Normal; cursorVisible = false; } // Update children (text inputs typically don't have children, but support it) updateChildren(ctx, deltaTime); } void UITextInput::render(UIRenderer& renderer) { const TextInputStyle& style = getCurrentStyle(); // Render background renderer.drawRect(absX, absY, width, height, style.bgColor); // Render border uint32_t borderColor = isFocused ? style.focusBorderColor : style.borderColor; // TODO: Implement proper border rendering // For now, render as thin line at bottom renderer.drawRect(absX, absY + height - style.borderWidth, width, style.borderWidth, borderColor); // Calculate text area float textX = absX + PADDING; float textY = absY + height * 0.5f; float textAreaWidth = width - 2 * PADDING; // Render text or placeholder if (text.empty() && !placeholder.empty() && !isFocused) { // Show placeholder renderer.drawText(textX, textY, placeholder, fontSize, style.placeholderColor); } else { // Show actual text std::string displayText = getDisplayText(); std::string visibleText = getVisibleText(); if (!visibleText.empty()) { renderer.drawText(textX - scrollOffset, textY, visibleText, fontSize, style.textColor); } // Render cursor if focused and visible if (isFocused && cursorVisible) { float cursorX = textX + getCursorPixelOffset() - scrollOffset; renderer.drawRect(cursorX, absY + PADDING, CURSOR_WIDTH, height - 2 * PADDING, style.cursorColor); } } // Render children on top renderChildren(renderer); } bool UITextInput::containsPoint(float px, float py) const { return px >= absX && px < absX + width && py >= absY && py < absY + height; } bool UITextInput::onMouseButton(int button, bool pressed, float x, float y) { if (!enabled) return false; if (button == 0 && pressed) { // Left mouse button down if (containsPoint(x, y)) { // TODO: Calculate click position and set cursor there // For now, just focus return true; // Will trigger focus in UIModule } } return false; } bool UITextInput::onKeyInput(int keyCode, uint32_t character, bool ctrl) { if (!isFocused || !enabled) return false; // Reset cursor blink on input cursorBlinkTimer = 0.0f; cursorVisible = true; // Handle special keys // Key codes (SDL-like): Backspace=8, Delete=127, Enter=13, Left=37, Right=39, Home=36, End=35 if (keyCode == 8) { // Backspace deleteCharBefore(); return true; } else if (keyCode == 127) { // Delete deleteCharAfter(); return true; } else if (keyCode == 13 || keyCode == 10) { // Enter/Return // Submit action - will be published by UIModule return true; } else if (keyCode == 37) { // Left arrow moveCursor(-1); return true; } else if (keyCode == 39) { // Right arrow moveCursor(1); return true; } else if (keyCode == 36) { // Home setCursorPosition(0); return true; } else if (keyCode == 35) { // End setCursorPosition(static_cast(text.length())); return true; } else if (ctrl && keyCode == 'a') { // Select all (future feature) return true; } else if (ctrl && keyCode == 'c') { // Copy (future feature) return true; } else if (ctrl && keyCode == 'v') { // Paste (future feature) return true; } // Handle printable characters if (character >= 32 && character < 127) { if (passesFilter(character)) { std::string charStr(1, static_cast(character)); insertText(charStr); return true; } } return false; } void UITextInput::gainFocus() { if (!isFocused) { isFocused = true; cursorBlinkTimer = 0.0f; cursorVisible = true; } } void UITextInput::loseFocus() { if (isFocused) { isFocused = false; cursorVisible = false; } } void UITextInput::insertText(const std::string& str) { if (text.length() + str.length() > static_cast(maxLength)) { return; // Would exceed max length } text.insert(cursorPosition, str); cursorPosition += static_cast(str.length()); updateScrollOffset(); } void UITextInput::deleteCharBefore() { if (cursorPosition > 0) { text.erase(cursorPosition - 1, 1); cursorPosition--; updateScrollOffset(); } } void UITextInput::deleteCharAfter() { if (cursorPosition < static_cast(text.length())) { text.erase(cursorPosition, 1); updateScrollOffset(); } } void UITextInput::moveCursor(int offset) { int newPos = cursorPosition + offset; newPos = std::clamp(newPos, 0, static_cast(text.length())); setCursorPosition(newPos); } void UITextInput::setCursorPosition(int pos) { cursorPosition = std::clamp(pos, 0, static_cast(text.length())); updateScrollOffset(); } std::string UITextInput::getVisibleText() const { std::string displayText = getDisplayText(); // Simple approach: return full text (scrolling handled by offset) // In a real implementation, we'd clip to visible characters only return displayText; } float UITextInput::getCursorPixelOffset() const { // Approximate pixel position of cursor return cursorPosition * CHAR_WIDTH; } const TextInputStyle& UITextInput::getCurrentStyle() const { switch (state) { case TextInputState::Focused: return focusedStyle; case TextInputState::Disabled: return disabledStyle; case TextInputState::Normal: default: return normalStyle; } } bool UITextInput::passesFilter(uint32_t ch) const { switch (filter) { case TextInputFilter::None: return true; case TextInputFilter::Alphanumeric: return std::isalnum(ch); case TextInputFilter::Numeric: return std::isdigit(ch) || ch == '-'; // Allow negative numbers case TextInputFilter::Float: return std::isdigit(ch) || ch == '.' || ch == '-'; case TextInputFilter::NoSpaces: return !std::isspace(ch); default: return true; } } std::string UITextInput::getDisplayText() const { if (passwordMode && !text.empty()) { // Mask all characters return std::string(text.length(), '*'); } return text; } void UITextInput::updateScrollOffset() { float cursorPixelPos = getCursorPixelOffset(); float textAreaWidth = width - 2 * PADDING; // Scroll to keep cursor visible if (cursorPixelPos - scrollOffset > textAreaWidth - CHAR_WIDTH) { // Cursor would be off the right edge scrollOffset = cursorPixelPos - textAreaWidth + CHAR_WIDTH; } else if (cursorPixelPos < scrollOffset) { // Cursor would be off the left edge scrollOffset = cursorPixelPos; } // Clamp scroll offset scrollOffset = std::max(0.0f, scrollOffset); } } // namespace grove