diff --git a/src/ui/TextField.cpp b/src/ui/TextField.cpp
new file mode 100644
index 00000000..0c936e69
--- /dev/null
+++ b/src/ui/TextField.cpp
@@ -0,0 +1,363 @@
+#include "TextField.h"
+
+#include <QApplication>
+#include <QEventTransition>
+#include <QFontDatabase>
+#include <QPaintEvent>
+#include <QPainter>
+#include <QPropertyAnimation>
+
+TextField::TextField(QWidget *parent)
+ : QLineEdit(parent)
+{
+ // Get rid of the focus border on macOS.
+ setAttribute(Qt::WA_MacShowFocusRect, 0);
+
+ state_machine_ = new TextFieldStateMachine(this);
+ label_ = 0;
+ label_font_size_ = 15;
+ show_label_ = false;
+ background_color_ = QColor("white");
+
+ setFrame(false);
+ setAttribute(Qt::WA_Hover);
+ setMouseTracking(true);
+ setTextMargins(0, 4, 0, 6);
+
+ QFont font("Open Sans");
+ font.setPixelSize(14);
+ setFont(font);
+
+ state_machine_->start();
+ QCoreApplication::processEvents();
+}
+
+void
+TextField::setBackgroundColor(const QColor &color)
+{
+ background_color_ = color;
+}
+
+QColor
+TextField::backgroundColor() const
+{
+ return background_color_;
+}
+
+void
+TextField::setShowLabel(bool value)
+{
+ if (show_label_ == value) {
+ return;
+ }
+
+ show_label_ = value;
+
+ if (!label_ && value) {
+ label_ = new TextFieldLabel(this);
+ state_machine_->setLabel(label_);
+ }
+
+ if (value) {
+ setContentsMargins(0, 23, 0, 0);
+ } else {
+ setContentsMargins(0, 0, 0, 0);
+ }
+}
+
+bool
+TextField::hasLabel() const
+{
+ return show_label_;
+}
+
+void
+TextField::setLabelFontSize(qreal size)
+{
+ label_font_size_ = size;
+
+ if (label_) {
+ QFont font(label_->font());
+ font.setPixelSize(size);
+ label_->setFont(font);
+ label_->update();
+ }
+}
+
+qreal
+TextField::labelFontSize() const
+{
+ return label_font_size_;
+}
+
+void
+TextField::setLabel(const QString &label)
+{
+ label_text_ = label;
+ setShowLabel(true);
+ label_->update();
+}
+
+QString
+TextField::label() const
+{
+ return label_text_;
+}
+
+void
+TextField::setTextColor(const QColor &color)
+{
+ text_color_ = color;
+ setStyleSheet(QString("QLineEdit { color: %1; }").arg(color.name()));
+}
+
+QColor
+TextField::textColor() const
+{
+ if (!text_color_.isValid()) {
+ return QColor("black");
+ }
+
+ return text_color_;
+}
+
+void
+TextField::setLabelColor(const QColor &color)
+{
+ label_color_ = color;
+ update();
+}
+
+QColor
+TextField::labelColor() const
+{
+ if (!label_color_.isValid()) {
+ return QColor("#abb"); // TODO: Move this into Theme.h
+ }
+
+ return label_color_;
+}
+
+void
+TextField::setInkColor(const QColor &color)
+{
+ ink_color_ = color;
+ update();
+}
+
+QColor
+TextField::inkColor() const
+{
+ if (!ink_color_.isValid()) {
+ return QColor("black");
+ }
+
+ return ink_color_;
+}
+
+void
+TextField::setUnderlineColor(const QColor &color)
+{
+ underline_color_ = color;
+ update();
+}
+
+QColor
+TextField::underlineColor() const
+{
+ if (!underline_color_.isValid()) {
+ return QColor("black");
+ }
+
+ return underline_color_;
+}
+
+bool
+TextField::event(QEvent *event)
+{
+ switch (event->type()) {
+ case QEvent::Resize:
+ case QEvent::Move: {
+ if (label_)
+ label_->setGeometry(rect());
+ break;
+ }
+ default:
+ break;
+ }
+
+ return QLineEdit::event(event);
+}
+
+void
+TextField::paintEvent(QPaintEvent *event)
+{
+ QLineEdit::paintEvent(event);
+
+ QPainter painter(this);
+
+ if (text().isEmpty()) {
+ painter.setOpacity(1 - state_machine_->progress());
+ painter.fillRect(rect(), backgroundColor());
+ }
+
+ const int y = height() - 1;
+ const int wd = width() - 5;
+
+ QPen pen;
+ pen.setWidth(1);
+ pen.setColor(underlineColor());
+ painter.setPen(pen);
+ painter.setOpacity(1);
+ painter.drawLine(2, y, wd, y);
+
+ QBrush brush;
+ brush.setStyle(Qt::SolidPattern);
+ brush.setColor(inkColor());
+
+ const qreal progress = state_machine_->progress();
+
+ if (progress > 0) {
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(brush);
+ const int w = (1 - progress) * static_cast<qreal>(wd / 2);
+ painter.drawRect(w + 2.5, height() - 2, wd - 2 * w, 2);
+ }
+}
+
+TextFieldStateMachine::TextFieldStateMachine(TextField *parent)
+ : QStateMachine(parent)
+ , text_field_(parent)
+{
+ normal_state_ = new QState;
+ focused_state_ = new QState;
+
+ label_ = 0;
+ offset_anim_ = 0;
+ color_anim_ = 0;
+ progress_ = 0.0;
+
+ addState(normal_state_);
+ addState(focused_state_);
+
+ setInitialState(normal_state_);
+
+ QEventTransition *transition;
+ QPropertyAnimation *animation;
+
+ transition = new QEventTransition(parent, QEvent::FocusIn);
+ transition->setTargetState(focused_state_);
+ normal_state_->addTransition(transition);
+
+ animation = new QPropertyAnimation(this, "progress", this);
+ animation->setEasingCurve(QEasingCurve::InCubic);
+ animation->setDuration(310);
+ transition->addAnimation(animation);
+
+ transition = new QEventTransition(parent, QEvent::FocusOut);
+ transition->setTargetState(normal_state_);
+ focused_state_->addTransition(transition);
+
+ animation = new QPropertyAnimation(this, "progress", this);
+ animation->setEasingCurve(QEasingCurve::OutCubic);
+ animation->setDuration(310);
+ transition->addAnimation(animation);
+
+ normal_state_->assignProperty(this, "progress", 0);
+ focused_state_->assignProperty(this, "progress", 1);
+
+ setupProperties();
+
+ connect(text_field_, SIGNAL(textChanged(QString)), this, SLOT(setupProperties()));
+}
+
+void
+TextFieldStateMachine::setLabel(TextFieldLabel *label)
+{
+ if (label_) {
+ delete label_;
+ }
+
+ if (offset_anim_) {
+ removeDefaultAnimation(offset_anim_);
+ delete offset_anim_;
+ }
+
+ if (color_anim_) {
+ removeDefaultAnimation(color_anim_);
+ delete color_anim_;
+ }
+
+ label_ = label;
+
+ if (label_) {
+ offset_anim_ = new QPropertyAnimation(label_, "offset", this);
+ offset_anim_->setDuration(210);
+ offset_anim_->setEasingCurve(QEasingCurve::OutCubic);
+ addDefaultAnimation(offset_anim_);
+
+ color_anim_ = new QPropertyAnimation(label_, "color", this);
+ color_anim_->setDuration(210);
+ addDefaultAnimation(color_anim_);
+ }
+
+ setupProperties();
+}
+
+void
+TextFieldStateMachine::setupProperties()
+{
+ if (label_) {
+ const int m = text_field_->textMargins().top();
+
+ if (text_field_->text().isEmpty()) {
+ normal_state_->assignProperty(label_, "offset", QPointF(0, 26));
+ } else {
+ normal_state_->assignProperty(label_, "offset", QPointF(0, 0 - m));
+ }
+
+ focused_state_->assignProperty(label_, "offset", QPointF(0, 0 - m));
+ focused_state_->assignProperty(label_, "color", text_field_->inkColor());
+ normal_state_->assignProperty(label_, "color", text_field_->labelColor());
+
+ if (0 != label_->offset().y() && !text_field_->text().isEmpty()) {
+ label_->setOffset(QPointF(0, 0 - m));
+ } else if (!text_field_->hasFocus() && label_->offset().y() <= 0 &&
+ text_field_->text().isEmpty()) {
+ label_->setOffset(QPointF(0, 26));
+ }
+ }
+
+ text_field_->update();
+}
+
+TextFieldLabel::TextFieldLabel(TextField *parent)
+ : QWidget(parent)
+ , text_field_(parent)
+{
+ x_ = 0;
+ y_ = 26;
+ scale_ = 1;
+ color_ = parent->labelColor();
+
+ QFont font("Open Sans SemiBold");
+ font.setPixelSize(parent->labelFontSize());
+ font.setLetterSpacing(QFont::PercentageSpacing, 102);
+ setFont(font);
+}
+
+void
+TextFieldLabel::paintEvent(QPaintEvent *)
+{
+ if (!text_field_->hasLabel())
+ return;
+
+ QPainter painter(this);
+ painter.setRenderHint(QPainter::Antialiasing);
+ painter.scale(scale_, scale_);
+ painter.setPen(color_);
+ painter.setOpacity(1);
+
+ QPointF pos(2 + x_, height() - 36 + y_);
+ painter.drawText(pos.x(), pos.y(), text_field_->label());
+}
|