reactphysics3d/testbed/nanogui/src/textbox.cpp

672 lines
22 KiB
C++

/*
src/textbox.cpp -- Fancy text box with builtin regular
expression-based validation
The text box widget was contributed by Christian Schueller.
NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>.
The widget drawing code is based on the NanoVG demo application
by Mikko Mononen.
All rights reserved. Use of this source code is governed by a
BSD-style license that can be found in the LICENSE.txt file.
*/
#include <nanogui/window.h>
#include <nanogui/screen.h>
#include <nanogui/textbox.h>
#include <nanogui/opengl.h>
#include <nanogui/theme.h>
#include <nanogui/serializer/core.h>
#include <regex>
#include <iostream>
NAMESPACE_BEGIN(nanogui)
TextBox::TextBox(Widget *parent,const std::string &value)
: Widget(parent),
mEditable(false),
mSpinnable(false),
mCommitted(true),
mValue(value),
mDefaultValue(""),
mAlignment(Alignment::Center),
mUnits(""),
mFormat(""),
mUnitsImage(-1),
mValidFormat(true),
mValueTemp(value),
mCursorPos(-1),
mSelectionPos(-1),
mMousePos(Vector2i(-1,-1)),
mMouseDownPos(Vector2i(-1,-1)),
mMouseDragPos(Vector2i(-1,-1)),
mMouseDownModifier(0),
mTextOffset(0),
mLastClick(0) {
if (mTheme) mFontSize = mTheme->mTextBoxFontSize;
mIconExtraScale = 0.8f;// widget override
}
void TextBox::setEditable(bool editable) {
mEditable = editable;
setCursor(editable ? Cursor::IBeam : Cursor::Arrow);
}
void TextBox::setTheme(Theme *theme) {
Widget::setTheme(theme);
if (mTheme)
mFontSize = mTheme->mTextBoxFontSize;
}
Vector2i TextBox::preferredSize(NVGcontext *ctx) const {
Vector2i size(0, fontSize() * 1.4f);
float uw = 0;
if (mUnitsImage > 0) {
int w, h;
nvgImageSize(ctx, mUnitsImage, &w, &h);
float uh = size(1) * 0.4f;
uw = w * uh / h;
} else if (!mUnits.empty()) {
uw = nvgTextBounds(ctx, 0, 0, mUnits.c_str(), nullptr, nullptr);
}
float sw = 0;
if (mSpinnable) {
sw = 14.f;
}
float ts = nvgTextBounds(ctx, 0, 0, mValue.c_str(), nullptr, nullptr);
size(0) = size(1) + ts + uw + sw;
return size;
}
void TextBox::draw(NVGcontext* ctx) {
Widget::draw(ctx);
NVGpaint bg = nvgBoxGradient(ctx,
mPos.x() + 1, mPos.y() + 1 + 1.0f, mSize.x() - 2, mSize.y() - 2,
3, 4, Color(255, 32), Color(32, 32));
NVGpaint fg1 = nvgBoxGradient(ctx,
mPos.x() + 1, mPos.y() + 1 + 1.0f, mSize.x() - 2, mSize.y() - 2,
3, 4, Color(150, 32), Color(32, 32));
NVGpaint fg2 = nvgBoxGradient(ctx,
mPos.x() + 1, mPos.y() + 1 + 1.0f, mSize.x() - 2, mSize.y() - 2,
3, 4, nvgRGBA(255, 0, 0, 100), nvgRGBA(255, 0, 0, 50));
nvgBeginPath(ctx);
nvgRoundedRect(ctx, mPos.x() + 1, mPos.y() + 1 + 1.0f, mSize.x() - 2,
mSize.y() - 2, 3);
if (mEditable && focused())
mValidFormat ? nvgFillPaint(ctx, fg1) : nvgFillPaint(ctx, fg2);
else if (mSpinnable && mMouseDownPos.x() != -1)
nvgFillPaint(ctx, fg1);
else
nvgFillPaint(ctx, bg);
nvgFill(ctx);
nvgBeginPath(ctx);
nvgRoundedRect(ctx, mPos.x() + 0.5f, mPos.y() + 0.5f, mSize.x() - 1,
mSize.y() - 1, 2.5f);
nvgStrokeColor(ctx, Color(0, 48));
nvgStroke(ctx);
nvgFontSize(ctx, fontSize());
nvgFontFace(ctx, "sans");
Vector2i drawPos(mPos.x(), mPos.y() + mSize.y() * 0.5f + 1);
float xSpacing = mSize.y() * 0.3f;
float unitWidth = 0;
if (mUnitsImage > 0) {
int w, h;
nvgImageSize(ctx, mUnitsImage, &w, &h);
float unitHeight = mSize.y() * 0.4f;
unitWidth = w * unitHeight / h;
NVGpaint imgPaint = nvgImagePattern(
ctx, mPos.x() + mSize.x() - xSpacing - unitWidth,
drawPos.y() - unitHeight * 0.5f, unitWidth, unitHeight, 0,
mUnitsImage, mEnabled ? 0.7f : 0.35f);
nvgBeginPath(ctx);
nvgRect(ctx, mPos.x() + mSize.x() - xSpacing - unitWidth,
drawPos.y() - unitHeight * 0.5f, unitWidth, unitHeight);
nvgFillPaint(ctx, imgPaint);
nvgFill(ctx);
unitWidth += 2;
} else if (!mUnits.empty()) {
unitWidth = nvgTextBounds(ctx, 0, 0, mUnits.c_str(), nullptr, nullptr);
nvgFillColor(ctx, Color(255, mEnabled ? 64 : 32));
nvgTextAlign(ctx, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
nvgText(ctx, mPos.x() + mSize.x() - xSpacing, drawPos.y(),
mUnits.c_str(), nullptr);
unitWidth += 2;
}
float spinArrowsWidth = 0.f;
if (mSpinnable && !focused()) {
spinArrowsWidth = 14.f;
nvgFontFace(ctx, "icons");
nvgFontSize(ctx, ((mFontSize < 0) ? mTheme->mButtonFontSize : mFontSize) * icon_scale());
bool spinning = mMouseDownPos.x() != -1;
/* up button */ {
bool hover = mMouseFocus && spinArea(mMousePos) == SpinArea::Top;
nvgFillColor(ctx, (mEnabled && (hover || spinning)) ? mTheme->mTextColor : mTheme->mDisabledTextColor);
auto icon = utf8(mTheme->mTextBoxUpIcon);
nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
Vector2f iconPos(mPos.x() + 4.f,
mPos.y() + mSize.y()/2.f - xSpacing/2.f);
nvgText(ctx, iconPos.x(), iconPos.y(), icon.data(), nullptr);
}
/* down button */ {
bool hover = mMouseFocus && spinArea(mMousePos) == SpinArea::Bottom;
nvgFillColor(ctx, (mEnabled && (hover || spinning)) ? mTheme->mTextColor : mTheme->mDisabledTextColor);
auto icon = utf8(mTheme->mTextBoxDownIcon);
nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
Vector2f iconPos(mPos.x() + 4.f,
mPos.y() + mSize.y()/2.f + xSpacing/2.f + 1.5f);
nvgText(ctx, iconPos.x(), iconPos.y(), icon.data(), nullptr);
}
nvgFontSize(ctx, fontSize());
nvgFontFace(ctx, "sans");
}
switch (mAlignment) {
case Alignment::Left:
nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
drawPos.x() += xSpacing + spinArrowsWidth;
break;
case Alignment::Right:
nvgTextAlign(ctx, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
drawPos.x() += mSize.x() - unitWidth - xSpacing;
break;
case Alignment::Center:
nvgTextAlign(ctx, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
drawPos.x() += mSize.x() * 0.5f;
break;
}
nvgFontSize(ctx, fontSize());
nvgFillColor(ctx, mEnabled && (!mCommitted || !mValue.empty()) ?
mTheme->mTextColor :
mTheme->mDisabledTextColor);
// clip visible text area
float clipX = mPos.x() + xSpacing + spinArrowsWidth - 1.0f;
float clipY = mPos.y() + 1.0f;
float clipWidth = mSize.x() - unitWidth - spinArrowsWidth - 2 * xSpacing + 2.0f;
float clipHeight = mSize.y() - 3.0f;
nvgSave(ctx);
nvgIntersectScissor(ctx, clipX, clipY, clipWidth, clipHeight);
Vector2i oldDrawPos(drawPos);
drawPos.x() += mTextOffset;
if (mCommitted) {
nvgText(ctx, drawPos.x(), drawPos.y(),
mValue.empty() ? mPlaceholder.c_str() : mValue.c_str(), nullptr);
} else {
const int maxGlyphs = 1024;
NVGglyphPosition glyphs[maxGlyphs];
float textBound[4];
nvgTextBounds(ctx, drawPos.x(), drawPos.y(), mValueTemp.c_str(),
nullptr, textBound);
float lineh = textBound[3] - textBound[1];
// find cursor positions
int nglyphs =
nvgTextGlyphPositions(ctx, drawPos.x(), drawPos.y(),
mValueTemp.c_str(), nullptr, glyphs, maxGlyphs);
updateCursor(ctx, textBound[2], glyphs, nglyphs);
// compute text offset
int prevCPos = mCursorPos > 0 ? mCursorPos - 1 : 0;
int nextCPos = mCursorPos < nglyphs ? mCursorPos + 1 : nglyphs;
float prevCX = cursorIndex2Position(prevCPos, textBound[2], glyphs, nglyphs);
float nextCX = cursorIndex2Position(nextCPos, textBound[2], glyphs, nglyphs);
if (nextCX > clipX + clipWidth)
mTextOffset -= nextCX - (clipX + clipWidth) + 1;
if (prevCX < clipX)
mTextOffset += clipX - prevCX + 1;
drawPos.x() = oldDrawPos.x() + mTextOffset;
// draw text with offset
nvgText(ctx, drawPos.x(), drawPos.y(), mValueTemp.c_str(), nullptr);
nvgTextBounds(ctx, drawPos.x(), drawPos.y(), mValueTemp.c_str(),
nullptr, textBound);
// recompute cursor positions
nglyphs = nvgTextGlyphPositions(ctx, drawPos.x(), drawPos.y(),
mValueTemp.c_str(), nullptr, glyphs, maxGlyphs);
if (mCursorPos > -1) {
if (mSelectionPos > -1) {
float caretx = cursorIndex2Position(mCursorPos, textBound[2],
glyphs, nglyphs);
float selx = cursorIndex2Position(mSelectionPos, textBound[2],
glyphs, nglyphs);
if (caretx > selx)
std::swap(caretx, selx);
// draw selection
nvgBeginPath(ctx);
nvgFillColor(ctx, nvgRGBA(255, 255, 255, 80));
nvgRect(ctx, caretx, drawPos.y() - lineh * 0.5f, selx - caretx,
lineh);
nvgFill(ctx);
}
float caretx = cursorIndex2Position(mCursorPos, textBound[2], glyphs, nglyphs);
// draw cursor
nvgBeginPath(ctx);
nvgMoveTo(ctx, caretx, drawPos.y() - lineh * 0.5f);
nvgLineTo(ctx, caretx, drawPos.y() + lineh * 0.5f);
nvgStrokeColor(ctx, nvgRGBA(255, 192, 0, 255));
nvgStrokeWidth(ctx, 1.0f);
nvgStroke(ctx);
}
}
nvgRestore(ctx);
}
bool TextBox::mouseButtonEvent(const Vector2i &p, int button, bool down,
int modifiers) {
if (button == GLFW_MOUSE_BUTTON_1 && down && !mFocused) {
if (!mSpinnable || spinArea(p) == SpinArea::None) /* not on scrolling arrows */
requestFocus();
}
if (mEditable && focused()) {
if (down) {
mMouseDownPos = p;
mMouseDownModifier = modifiers;
double time = glfwGetTime();
if (time - mLastClick < 0.25) {
/* Double-click: select all text */
mSelectionPos = 0;
mCursorPos = (int) mValueTemp.size();
mMouseDownPos = Vector2i(-1, -1);
}
mLastClick = time;
} else {
mMouseDownPos = Vector2i(-1, -1);
mMouseDragPos = Vector2i(-1, -1);
}
return true;
} else if (mSpinnable && !focused()) {
if (down) {
if (spinArea(p) == SpinArea::None) {
mMouseDownPos = p;
mMouseDownModifier = modifiers;
double time = glfwGetTime();
if (time - mLastClick < 0.25) {
/* Double-click: reset to default value */
mValue = mDefaultValue;
if (mCallback)
mCallback(mValue);
mMouseDownPos = Vector2i(-1, -1);
}
mLastClick = time;
} else {
mMouseDownPos = Vector2i(-1, -1);
mMouseDragPos = Vector2i(-1, -1);
}
} else {
mMouseDownPos = Vector2i(-1, -1);
mMouseDragPos = Vector2i(-1, -1);
}
return true;
}
return false;
}
bool TextBox::mouseMotionEvent(const Vector2i &p, const Vector2i & /* rel */,
int /* button */, int /* modifiers */) {
mMousePos = p;
if (!mEditable)
setCursor(Cursor::Arrow);
else if (mSpinnable && !focused() && spinArea(mMousePos) != SpinArea::None) /* scrolling arrows */
setCursor(Cursor::Hand);
else
setCursor(Cursor::IBeam);
if (mEditable && focused()) {
return true;
}
return false;
}
bool TextBox::mouseDragEvent(const Vector2i &p, const Vector2i &/* rel */,
int /* button */, int /* modifiers */) {
mMousePos = p;
mMouseDragPos = p;
if (mEditable && focused()) {
return true;
}
return false;
}
bool TextBox::focusEvent(bool focused) {
Widget::focusEvent(focused);
std::string backup = mValue;
if (mEditable) {
if (focused) {
mValueTemp = mValue;
mCommitted = false;
mCursorPos = 0;
} else {
if (mValidFormat) {
if (mValueTemp == "")
mValue = mDefaultValue;
else
mValue = mValueTemp;
}
if (mCallback && !mCallback(mValue))
mValue = backup;
mValidFormat = true;
mCommitted = true;
mCursorPos = -1;
mSelectionPos = -1;
mTextOffset = 0;
}
mValidFormat = (mValueTemp == "") || checkFormat(mValueTemp, mFormat);
}
return true;
}
bool TextBox::keyboardEvent(int key, int /* scancode */, int action, int modifiers) {
if (mEditable && focused()) {
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
if (key == GLFW_KEY_LEFT) {
if (modifiers == GLFW_MOD_SHIFT) {
if (mSelectionPos == -1)
mSelectionPos = mCursorPos;
} else {
mSelectionPos = -1;
}
if (mCursorPos > 0)
mCursorPos--;
} else if (key == GLFW_KEY_RIGHT) {
if (modifiers == GLFW_MOD_SHIFT) {
if (mSelectionPos == -1)
mSelectionPos = mCursorPos;
} else {
mSelectionPos = -1;
}
if (mCursorPos < (int) mValueTemp.length())
mCursorPos++;
} else if (key == GLFW_KEY_HOME) {
if (modifiers == GLFW_MOD_SHIFT) {
if (mSelectionPos == -1)
mSelectionPos = mCursorPos;
} else {
mSelectionPos = -1;
}
mCursorPos = 0;
} else if (key == GLFW_KEY_END) {
if (modifiers == GLFW_MOD_SHIFT) {
if (mSelectionPos == -1)
mSelectionPos = mCursorPos;
} else {
mSelectionPos = -1;
}
mCursorPos = (int) mValueTemp.size();
} else if (key == GLFW_KEY_BACKSPACE) {
if (!deleteSelection()) {
if (mCursorPos > 0) {
mValueTemp.erase(mValueTemp.begin() + mCursorPos - 1);
mCursorPos--;
}
}
} else if (key == GLFW_KEY_DELETE) {
if (!deleteSelection()) {
if (mCursorPos < (int) mValueTemp.length())
mValueTemp.erase(mValueTemp.begin() + mCursorPos);
}
} else if (key == GLFW_KEY_ENTER) {
if (!mCommitted)
focusEvent(false);
} else if (key == GLFW_KEY_A && modifiers == SYSTEM_COMMAND_MOD) {
mCursorPos = (int) mValueTemp.length();
mSelectionPos = 0;
} else if (key == GLFW_KEY_X && modifiers == SYSTEM_COMMAND_MOD) {
copySelection();
deleteSelection();
} else if (key == GLFW_KEY_C && modifiers == SYSTEM_COMMAND_MOD) {
copySelection();
} else if (key == GLFW_KEY_V && modifiers == SYSTEM_COMMAND_MOD) {
deleteSelection();
pasteFromClipboard();
}
mValidFormat =
(mValueTemp == "") || checkFormat(mValueTemp, mFormat);
}
return true;
}
return false;
}
bool TextBox::keyboardCharacterEvent(unsigned int codepoint) {
if (mEditable && focused()) {
std::ostringstream convert;
convert << (char) codepoint;
deleteSelection();
mValueTemp.insert(mCursorPos, convert.str());
mCursorPos++;
mValidFormat = (mValueTemp == "") || checkFormat(mValueTemp, mFormat);
return true;
}
return false;
}
bool TextBox::checkFormat(const std::string &input, const std::string &format) {
if (format.empty())
return true;
try {
std::regex regex(format);
return regex_match(input, regex);
} catch (const std::regex_error &) {
#if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 9)
std::cerr << "Warning: cannot validate text field due to lacking regular expression support. please compile with GCC >= 4.9" << std::endl;
return true;
#else
throw;
#endif
}
}
bool TextBox::copySelection() {
if (mSelectionPos > -1) {
Screen *sc = dynamic_cast<Screen *>(this->window()->parent());
if (!sc)
return false;
int begin = mCursorPos;
int end = mSelectionPos;
if (begin > end)
std::swap(begin, end);
glfwSetClipboardString(sc->glfwWindow(),
mValueTemp.substr(begin, end).c_str());
return true;
}
return false;
}
void TextBox::pasteFromClipboard() {
Screen *sc = dynamic_cast<Screen *>(this->window()->parent());
if (!sc)
return;
const char* cbstr = glfwGetClipboardString(sc->glfwWindow());
if (cbstr)
mValueTemp.insert(mCursorPos, std::string(cbstr));
}
bool TextBox::deleteSelection() {
if (mSelectionPos > -1) {
int begin = mCursorPos;
int end = mSelectionPos;
if (begin > end)
std::swap(begin, end);
if (begin == end - 1)
mValueTemp.erase(mValueTemp.begin() + begin);
else
mValueTemp.erase(mValueTemp.begin() + begin,
mValueTemp.begin() + end);
mCursorPos = begin;
mSelectionPos = -1;
return true;
}
return false;
}
void TextBox::updateCursor(NVGcontext *, float lastx,
const NVGglyphPosition *glyphs, int size) {
// handle mouse cursor events
if (mMouseDownPos.x() != -1) {
if (mMouseDownModifier == GLFW_MOD_SHIFT) {
if (mSelectionPos == -1)
mSelectionPos = mCursorPos;
} else
mSelectionPos = -1;
mCursorPos =
position2CursorIndex(mMouseDownPos.x(), lastx, glyphs, size);
mMouseDownPos = Vector2i(-1, -1);
} else if (mMouseDragPos.x() != -1) {
if (mSelectionPos == -1)
mSelectionPos = mCursorPos;
mCursorPos =
position2CursorIndex(mMouseDragPos.x(), lastx, glyphs, size);
} else {
// set cursor to last character
if (mCursorPos == -2)
mCursorPos = size;
}
if (mCursorPos == mSelectionPos)
mSelectionPos = -1;
}
float TextBox::cursorIndex2Position(int index, float lastx,
const NVGglyphPosition *glyphs, int size) {
float pos = 0;
if (index == size)
pos = lastx; // last character
else
pos = glyphs[index].x;
return pos;
}
int TextBox::position2CursorIndex(float posx, float lastx,
const NVGglyphPosition *glyphs, int size) {
int mCursorId = 0;
float caretx = glyphs[mCursorId].x;
for (int j = 1; j < size; j++) {
if (std::abs(caretx - posx) > std::abs(glyphs[j].x - posx)) {
mCursorId = j;
caretx = glyphs[mCursorId].x;
}
}
if (std::abs(caretx - posx) > std::abs(lastx - posx))
mCursorId = size;
return mCursorId;
}
TextBox::SpinArea TextBox::spinArea(const Vector2i & pos) {
if (0 <= pos.x() - mPos.x() && pos.x() - mPos.x() < 14.f) { /* on scrolling arrows */
if (mSize.y() >= pos.y() - mPos.y() && pos.y() - mPos.y() <= mSize.y() / 2.f) { /* top part */
return SpinArea::Top;
} else if (0.f <= pos.y() - mPos.y() && pos.y() - mPos.y() > mSize.y() / 2.f) { /* bottom part */
return SpinArea::Bottom;
}
}
return SpinArea::None;
}
void TextBox::save(Serializer &s) const {
Widget::save(s);
s.set("editable", mEditable);
s.set("spinnable", mSpinnable);
s.set("committed", mCommitted);
s.set("value", mValue);
s.set("defaultValue", mDefaultValue);
s.set("alignment", (int) mAlignment);
s.set("units", mUnits);
s.set("format", mFormat);
s.set("unitsImage", mUnitsImage);
s.set("validFormat", mValidFormat);
s.set("valueTemp", mValueTemp);
s.set("cursorPos", mCursorPos);
s.set("selectionPos", mSelectionPos);
}
bool TextBox::load(Serializer &s) {
if (!Widget::load(s)) return false;
if (!s.get("editable", mEditable)) return false;
if (!s.get("spinnable", mSpinnable)) return false;
if (!s.get("committed", mCommitted)) return false;
if (!s.get("value", mValue)) return false;
if (!s.get("defaultValue", mDefaultValue)) return false;
if (!s.get("alignment", mAlignment)) return false;
if (!s.get("units", mUnits)) return false;
if (!s.get("format", mFormat)) return false;
if (!s.get("unitsImage", mUnitsImage)) return false;
if (!s.get("validFormat", mValidFormat)) return false;
if (!s.get("valueTemp", mValueTemp)) return false;
if (!s.get("cursorPos", mCursorPos)) return false;
if (!s.get("selectionPos", mSelectionPos)) return false;
mMousePos = mMouseDownPos = mMouseDragPos = Vector2i::Constant(-1);
mMouseDownModifier = mTextOffset = 0;
return true;
}
NAMESPACE_END(nanogui)