#include "StarTextBoxWidget.hpp" #include "StarRoot.hpp" #include "StarJsonExtra.hpp" #include "StarAssets.hpp" namespace Star { TextBoxWidget::TextBoxWidget(String const& startingText, String const& hint, WidgetCallbackFunc callback) : m_text(startingText), m_hint(hint), m_callback(callback) { auto assets = Root::singleton().assets(); m_textHidden = false; m_regex = ".*"; m_repeatKeyThreshold = 0; m_repeatCode = SpecialRepeatKeyCodes::None; m_isPressed = false; m_isHover = false; m_cursorOffset = startingText.size(); m_hAnchor = HorizontalAnchor::LeftAnchor; m_vAnchor = VerticalAnchor::BottomAnchor; m_drawBorder = false; m_cursorHoriz = Vec2I(); m_cursorVert = Vec2I(); m_overfillMode = true; m_maxWidth = assets->json("/interface.config:textBoxDefaultWidth").toInt(); auto fontConfig = assets->json("/interface.config:textBoxTextStyle"); m_textStyle = fontConfig; // Meh, padding is hard-coded here setSize({m_maxWidth + 6, m_textStyle.fontSize + 2}); m_cursorHoriz = jsonToVec2I(assets->json("/interface.config:textboxCursorHorizontal")); m_cursorVert = jsonToVec2I(assets->json("/interface.config:textboxCursorVertical")); } void TextBoxWidget::renderImpl() { float blueRate = 0; if (m_isHover && !m_isPressed) blueRate = 0.2f; else blueRate = 0.0f; Vec2F pos(screenPosition()); if (m_hAnchor == HorizontalAnchor::HMidAnchor) pos += Vec2F(size()[0] / 2.0f, 0); else if (m_hAnchor == HorizontalAnchor::RightAnchor) pos += Vec2F(size()[0], 0); context()->setTextStyle(m_textStyle); if ((m_maxWidth != -1) && m_overfillMode) { int shift = std::max(0, getCursorOffset() - m_maxWidth); pos += Vec2F(-shift, 0); } if (m_text.empty()) { context()->setFontColor(Color::rgba(m_textStyle.color).mix(Color::rgbf(0.3f, 0.3f, 0.3f), 0.8f).mix(Color::rgbf(0.0f, 0.0f, 1.0f), blueRate).toRgba()); context()->renderInterfaceText(m_hint, {pos, m_hAnchor, m_vAnchor}); } else { context()->setFontColor(Color::rgba(m_textStyle.color).mix(Color::rgbf(0, 0, 1), blueRate).toRgba()); if (m_textHidden) { String hiddenText('*', m_text.length()); context()->renderInterfaceText(hiddenText, { pos, m_hAnchor, m_vAnchor }); } else context()->renderInterfaceText(m_text, { pos, m_hAnchor, m_vAnchor }); } context()->clearTextStyle(); if (hasFocus()) { // render cursor float cc = 0.6f + 0.4f * std::sin((double)Time::monotonicMilliseconds() / 300.0); Color cursorColor = Color::rgbf(cc, cc, cc); float fontSize = m_textStyle.fontSize; context()->drawInterfaceLine( pos + Vec2F(getCursorOffset(), fontSize * m_cursorVert[0]), pos + Vec2F(getCursorOffset(), fontSize * m_cursorVert[1]), cursorColor.toRgba()); context()->drawInterfaceLine( pos + Vec2F(getCursorOffset() + fontSize * m_cursorHoriz[0], fontSize * m_cursorVert[0]), pos + Vec2F(getCursorOffset() + fontSize * m_cursorHoriz[1], fontSize * m_cursorVert[0]), cursorColor.toRgba()); } if (m_drawBorder) context()->drawInterfacePolyLines(PolyF(screenBoundRect()), Color(Color::White).toRgba()); } int TextBoxWidget::getCursorOffset() { // horizontal only float scale; context()->setTextStyle(m_textStyle); if (m_hAnchor == HorizontalAnchor::LeftAnchor) { scale = 1.0; } else if (m_hAnchor == HorizontalAnchor::HMidAnchor) { scale = 0.5; } else if (m_hAnchor == HorizontalAnchor::RightAnchor) { scale = -1.0; if (m_textHidden) { int width = context()->stringInterfaceWidth("*"); size_t chars = m_text.size(); return (width * chars) * scale + (width * (chars - m_cursorOffset)); } else { return context()->stringInterfaceWidth(m_text) * scale + context()->stringInterfaceWidth(m_text.substr(m_cursorOffset, m_text.size())); } } else { throw GuiException("Somehow, the value of m_hAnchor became bad"); } if (m_textHidden) { int width = context()->stringInterfaceWidth("*"); size_t chars = m_text.size(); return (int)std::ceil((width * chars) * scale - (width * (chars - m_cursorOffset))); } else { return (int)std::ceil(context()->stringInterfaceWidth(m_text) * scale - context()->stringInterfaceWidth(m_text.substr(m_cursorOffset, m_text.size()))); } } void TextBoxWidget::update(float dt) { Widget::update(dt); if (m_repeatCode != SpecialRepeatKeyCodes::None) { if (Time::monotonicMilliseconds() >= m_repeatKeyThreshold) { m_repeatKeyThreshold += 50; if (m_repeatCode == SpecialRepeatKeyCodes::Delete) { if (m_cursorOffset < (int)m_text.size()) modText(m_text.substr(0, m_cursorOffset) + m_text.substr(m_cursorOffset + 1)); } else if (m_repeatCode == SpecialRepeatKeyCodes::Backspace) { if (m_cursorOffset > 0) { if (modText(m_text.substr(0, m_cursorOffset - 1) + m_text.substr(m_cursorOffset))) m_cursorOffset -= 1; } } else if (m_repeatCode == SpecialRepeatKeyCodes::Left) { if (m_cursorOffset > 0) { m_cursorOffset--; if (m_cursorOffset < 0) m_cursorOffset = 0; } } else if (m_repeatCode == SpecialRepeatKeyCodes::Right) { if (m_cursorOffset < (int)m_text.size()) { m_cursorOffset++; if (m_cursorOffset > (int)m_text.size()) m_cursorOffset = m_text.size(); } } } } } String const& TextBoxWidget::getText() const { return m_text; } bool TextBoxWidget::setText(String const& text, bool callback, bool moveCursor) { if (m_text == text) return true; if (!newTextValid(text)) return false; m_text = text; size_t size = m_text.size(); if (moveCursor || m_cursorOffset > size) m_cursorOffset = size; m_repeatCode = SpecialRepeatKeyCodes::None; if (callback) m_callback(this); return true; } bool TextBoxWidget::getHidden() const { return m_textHidden; } void TextBoxWidget::setHidden(bool hidden) { m_textHidden = hidden; } String TextBoxWidget::getRegex() { return m_regex; } void TextBoxWidget::setRegex(String const& regex) { m_regex = regex; } void TextBoxWidget::setColor(Color const& color) { m_textStyle.color = color.toRgba(); } void TextBoxWidget::setDirectives(String const& directives) { m_textStyle.directives = directives; } void TextBoxWidget::setFontSize(int fontSize) { m_textStyle.fontSize = fontSize; } void TextBoxWidget::setMaxWidth(int maxWidth) { m_maxWidth = maxWidth; setSize({m_maxWidth + 6, m_textStyle.fontSize + 2}); } void TextBoxWidget::setOverfillMode(bool overtype) { m_overfillMode = overtype; } void TextBoxWidget::setOnBlurCallback(WidgetCallbackFunc onBlur) { m_onBlur = onBlur; } void TextBoxWidget::setOnEnterKeyCallback(WidgetCallbackFunc onEnterKey) { m_onEnterKey = onEnterKey; } void TextBoxWidget::setOnEscapeKeyCallback(WidgetCallbackFunc onEscapeKey) { m_onEscapeKey = onEscapeKey; } void TextBoxWidget::setNextFocus(Maybe nextFocus) { m_nextFocus = nextFocus; } void TextBoxWidget::setPrevFocus(Maybe prevFocus) { m_prevFocus = prevFocus; } void TextBoxWidget::mouseOver() { Widget::mouseOver(); m_isHover = true; } void TextBoxWidget::mouseOut() { Widget::mouseOut(); m_isHover = false; m_isPressed = false; } void TextBoxWidget::mouseReturnStillDown() { Widget::mouseReturnStillDown(); m_isHover = true; m_isPressed = true; } void TextBoxWidget::blur() { Widget::blur(); if (m_onBlur) m_onBlur(this); m_repeatCode = SpecialRepeatKeyCodes::None; } KeyboardCaptureMode TextBoxWidget::keyboardCaptured() const { if (active() && hasFocus()) return KeyboardCaptureMode::TextInput; return KeyboardCaptureMode::None; } bool TextBoxWidget::sendEvent(InputEvent const& event) { if (!hasFocus()) return false; if (event.is()) { m_repeatCode = SpecialRepeatKeyCodes::None; m_callback(this); return false; } if (innerSendEvent(event)) { m_callback(this); return true; } return false; } bool TextBoxWidget::innerSendEvent(InputEvent const& event) { if (auto keyDown = event.ptr()) { m_repeatKeyThreshold = Time::monotonicMilliseconds() + 300LL; if (keyDown->key == Key::Escape) { if (m_onEscapeKey) { m_onEscapeKey(this); return true; } return false; } if (keyDown->key == Key::Return || keyDown->key == Key::KeypadEnter) { if (m_onEnterKey) { m_onEnterKey(this); return true; } return false; } if (keyDown->key == Key::Tab) { if ((KeyMod::LShift & keyDown->mods) == KeyMod::LShift || (KeyMod::RShift & keyDown->mods) == KeyMod::RShift) { if (m_prevFocus) { if (auto prevWidget = parent()->fetchChild(*m_prevFocus)) { prevWidget->focus(); return true; } } } else { if (m_nextFocus) { if (auto nextWidget = parent()->fetchChild(*m_nextFocus)) { nextWidget->focus(); return true; } } } return false; } if ((keyDown->mods & (KeyMod::LCtrl | KeyMod::RCtrl)) != KeyMod::NoMod) { if (keyDown->key == Key::C) { context()->setClipboard(m_text); return true; } if (keyDown->key == Key::X) { context()->setClipboard(m_text); if (modText("")) m_cursorOffset = 0; return true; } if (keyDown->key == Key::V) { String clipboard = context()->getClipboard(); if (modText(m_text.substr(0, m_cursorOffset) + clipboard + m_text.substr(m_cursorOffset))) m_cursorOffset += clipboard.size(); return true; } } auto calculateSteps = [&](bool dir) { int steps = 1; if ((keyDown->mods & (KeyMod::LCtrl | KeyMod::RCtrl)) != KeyMod::NoMod || (keyDown->mods & (KeyMod::LAlt | KeyMod::RAlt)) != KeyMod::NoMod) { if (dir) // right steps = m_text.findNextBoundary(m_cursorOffset) - m_cursorOffset; else // left steps = m_cursorOffset - m_text.findNextBoundary(m_cursorOffset, true); return steps < 1 ? 1 : steps; } return steps; }; if (keyDown->key == Key::Backspace) { int steps = calculateSteps(false); m_repeatCode = SpecialRepeatKeyCodes::Backspace; for (int i = 0; i < steps; i++) { if (m_cursorOffset > 0) { if (modText(m_text.substr(0, m_cursorOffset - 1) + m_text.substr(m_cursorOffset))) m_cursorOffset -= 1; } } return true; } if (keyDown->key == Key::Delete) { int steps = calculateSteps(true); m_repeatCode = SpecialRepeatKeyCodes::Delete; for (int i = 0; i < steps; i++) { if (m_cursorOffset < (int)m_text.size()) modText(m_text.substr(0, m_cursorOffset) + m_text.substr(m_cursorOffset + 1)); } return true; } if (keyDown->key == Key::Left) { int steps = calculateSteps(false); m_repeatCode = SpecialRepeatKeyCodes::Left; for (int i = 0; i < steps; i++) { m_cursorOffset--; if (m_cursorOffset < 0) m_cursorOffset = 0; } return true; } if (keyDown->key == Key::Right) { int steps = calculateSteps(true); m_repeatCode = SpecialRepeatKeyCodes::Right; for (int i = 0; i < steps; i++) { m_cursorOffset++; if (m_cursorOffset > (int)m_text.size()) m_cursorOffset = m_text.size(); } return true; } if (keyDown->key == Key::Home) { m_cursorOffset = 0; return true; } if (keyDown->key == Key::End) { m_cursorOffset = m_text.size(); return true; } } if (auto textInput = event.ptr()) { if (modText(m_text.substr(0, m_cursorOffset) + textInput->text + m_text.substr(m_cursorOffset))) m_cursorOffset += textInput->text.size(); return true; } return false; } void TextBoxWidget::setDrawBorder(bool drawBorder) { m_drawBorder = drawBorder; } void TextBoxWidget::setTextAlign(HorizontalAnchor hAnchor) { m_hAnchor = hAnchor; } bool TextBoxWidget::modText(String const& text) { if (m_text != text && newTextValid(text)) { m_text = text; return true; } else { return false; } } bool TextBoxWidget::newTextValid(String const& text) const { if (!text.regexMatch(m_regex)) return false; if ((m_maxWidth != -1) && !m_overfillMode) { context()->setTextStyle(m_textStyle); return context()->stringInterfaceWidth(text) <= m_maxWidth; } return true; } }