跳转至

ui/widget/material/widget/textfield/textfield.cpp

Material Design 3 TextField Implementation. More...

Namespaces

Name
cf
cf::ui
cf::ui::widget
cf::ui::widget::material

Detailed Description

Material Design 3 TextField Implementation.

Author: CFDesktop Team

Version: 0.1

Since: 0.1

Date: 2026-03-01

Implements a Material Design 3 text field with filled and outlined variants. Supports floating labels, prefix/suffix icons, character counter, helper/error text, and password mode.

Source code

#include "textfield.h"
#include "application_support/application.h"
#include "base/device_pixel.h"
#include "base/geometry_helper.h"
#include "cfmaterial_animation_factory.h"
#include "core/token/material_scheme/cfmaterial_token_literals.h"
#include "widget/material/base/focus_ring.h"
#include "widget/material/base/ripple_helper.h"
#include "widget/material/base/state_machine.h"

#include <QApplication>
#include <QFontMetrics>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QStyle>

namespace cf::ui::widget::material {

using namespace cf::ui::base;
using namespace cf::ui::base::device;
using namespace cf::ui::base::geometry;
using namespace cf::ui::components;
using namespace cf::ui::components::material;
using namespace cf::ui::core;
using namespace cf::ui::core::token::literals;
using namespace cf::ui::widget::material::base;
using namespace cf::ui::widget::application_support;

// ============================================================================
// Constants (Material Design 3 specifications)
// ============================================================================

namespace {
// Fallback colors when theme is not available
inline CFColor fallbackSurface() {
    return CFColor(253, 253, 253);
} // Surface
inline CFColor fallbackOnSurface() {
    return CFColor(27, 27, 31);
} // On Surface
inline CFColor fallbackOnSurfaceVariant() {
    return CFColor(75, 75, 80);
} // On Surface Variant
inline CFColor fallbackOutline() {
    return CFColor(120, 124, 132);
} // Outline
inline CFColor fallbackError() {
    return CFColor(186, 26, 26);
} // Error
inline CFColor fallbackPrimary() {
    return CFColor(103, 80, 164);
} // Purple 700
} // namespace

// ============================================================================
// Constructor / Destructor
// ============================================================================

TextField::TextField(TextFieldVariant variant, QWidget* parent)
    : QLineEdit(parent)
    , m_variant(variant)
    , m_showCharacterCounter(false)
    , m_maxLength(0)
    , m_isFloating(false)
    , m_hasError(false)
    , m_floatingProgress(0.0f)
    , m_outlineWidth(1.0f)
    , m_hoveringClearButton(false)
    , m_pressingClearButton(false) {

    // Disable native frame and background
    setFrame(false);
    setAttribute(Qt::WA_TranslucentBackground);
    setTextMargins(0, 0, 0, 0);

    // Get animation factory from Application
    m_animationFactory =
        cf::WeakPtr<CFMaterialAnimationFactory>::DynamicCast(Application::animationFactory());

    // Initialize behavior components
    m_stateMachine = new StateMachine(m_animationFactory, this);
    m_ripple = new RippleHelper(m_animationFactory, this);
    m_focusIndicator = new MdFocusIndicator(m_animationFactory, this);

    // Set ripple mode
    m_ripple->setMode(RippleHelper::Mode::Bounded);

    // Connect repaint signals
    connect(m_ripple, &RippleHelper::repaintNeeded, this, QOverload<>::of(&TextField::update));
    connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this,
            QOverload<>::of(&TextField::update));

    // Connect text change signal
    connect(this, &QLineEdit::textChanged, this, &TextField::textChanged);

    // Set default font
    setFont(inputFont());

    // Set cursor
    setCursor(Qt::IBeamCursor);
}

TextField::TextField(const QString& text, TextFieldVariant variant, QWidget* parent)
    : TextField(variant, parent) {
    setText(text);
}

TextField::~TextField() {
    // Components are parented to this, Qt will delete them automatically
}

// ============================================================================
// Event Handlers
// ============================================================================

void TextField::mousePressEvent(QMouseEvent* event) {
    // Check if clear button was clicked
    QRectF clearRect = clearButtonRect();
    if (!text().isEmpty() && clearRect.contains(event->pos())) {
        m_pressingClearButton = true;
        update();
        return;
    }

    QLineEdit::mousePressEvent(event);
    if (m_stateMachine)
        m_stateMachine->onPress(event->pos());
    if (m_ripple)
        m_ripple->onPress(event->pos(), rect());
    update();
}

void TextField::mouseReleaseEvent(QMouseEvent* event) {
    // Check if clear button was released
    if (m_pressingClearButton) {
        m_pressingClearButton = false;
        QRectF clearRect = clearButtonRect();
        if (clearRect.contains(event->pos())) {
            clear();
        }
        update();
        return;
    }

    QLineEdit::mouseReleaseEvent(event);
    if (m_stateMachine)
        m_stateMachine->onRelease();
    if (m_ripple)
        m_ripple->onRelease();
    update();
}

void TextField::focusInEvent(QFocusEvent* event) {
    QLineEdit::focusInEvent(event);
    if (m_stateMachine)
        m_stateMachine->onFocusIn();
    if (m_focusIndicator)
        m_focusIndicator->onFocusIn();
    updateFloatingState(true);
    update();
}

void TextField::focusOutEvent(QFocusEvent* event) {
    QLineEdit::focusOutEvent(event);
    if (m_stateMachine)
        m_stateMachine->onFocusOut();
    if (m_focusIndicator)
        m_focusIndicator->onFocusOut();
    updateFloatingState(!text().isEmpty());
    update();
}

void TextField::changeEvent(QEvent* event) {
    QLineEdit::changeEvent(event);
    if (event->type() == QEvent::EnabledChange) {
        if (m_stateMachine) {
            if (isEnabled()) {
                m_stateMachine->onEnable();
            } else {
                m_stateMachine->onDisable();
            }
        }
        update();
    }
}

void TextField::resizeEvent(QResizeEvent* event) {
    QLineEdit::resizeEvent(event);
    update();
}

void TextField::textChanged(const QString& text) {
    // Update floating state based on content
    updateFloatingState(hasFocus() || !text.isEmpty());

    // Update character counter visibility
    if (m_showCharacterCounter) {
        update();
    }
}

void TextField::paintEvent(QPaintEvent* event) {
    Q_UNUSED(event)

    QPainter p(this);
    p.setRenderHint(QPainter::Antialiasing);

    // Calculate layout rectangles
    QRectF field = fieldRect();
    QRectF helper = helperTextRect();

    // Step 1: Draw background (Filled variant only)
    drawBackground(p, field);

    // Step 2: Draw outline (Outlined variant and Filled active state)
    drawOutline(p, field);

    // Step 3: Draw label (floating or resting)
    drawLabel(p, field);

    // Step 4: Draw prefix icon
    drawPrefixIcon(p, textRect());

    // Step 5: Draw text content
    drawText(p, textRect());

    // Step 6: Draw suffix icon
    drawSuffixIcon(p, textRect());

    // Step 7: Draw clear button
    drawClearButton(p, textRect());

    // Step 8: Draw ripple
    drawRipple(p, field);

    // Step 9: Draw focus indicator
    drawFocusIndicator(p, field);

    // Step 10: Draw helper/error text
    drawHelperText(p, helper);

    // Step 11: Draw character counter
    if (m_showCharacterCounter) {
        drawCharacterCounter(p, helper);
    }
}

// ============================================================================
// Property Getters/Setters
// ============================================================================

TextField::TextFieldVariant TextField::variant() const {
    return m_variant;
}

void TextField::setVariant(TextFieldVariant variant) {
    if (m_variant != variant) {
        m_variant = variant;
        updateGeometry();
        update();
    }
}

QString TextField::label() const {
    return m_label;
}

void TextField::setLabel(const QString& label) {
    if (m_label != label) {
        m_label = label;
        updateGeometry();
        update();
    }
}

QString TextField::helperText() const {
    return m_helperText;
}

void TextField::setHelperText(const QString& text) {
    if (m_helperText != text) {
        m_helperText = text;
        updateGeometry();
        update();
    }
}

QString TextField::errorText() const {
    return m_errorText;
}

void TextField::setErrorText(const QString& text) {
    if (m_errorText != text) {
        m_errorText = text;
        m_hasError = !text.isEmpty();
        updateGeometry();
        update();
    }
}

bool TextField::isFloating() const {
    return m_isFloating;
}

QIcon TextField::prefixIcon() const {
    return m_prefixIcon;
}

void TextField::setPrefixIcon(const QIcon& icon) {
    if (m_prefixIcon.cacheKey() != icon.cacheKey()) {
        m_prefixIcon = icon;
        updateGeometry();
        update();
    }
}

QIcon TextField::suffixIcon() const {
    return m_suffixIcon;
}

void TextField::setSuffixIcon(const QIcon& icon) {
    if (m_suffixIcon.cacheKey() != icon.cacheKey()) {
        m_suffixIcon = icon;
        updateGeometry();
        update();
    }
}

bool TextField::showCharacterCounter() const {
    return m_showCharacterCounter;
}

void TextField::setShowCharacterCounter(bool show) {
    if (m_showCharacterCounter != show) {
        m_showCharacterCounter = show;
        updateGeometry();
        update();
    }
}

int TextField::maxLength() const {
    return m_maxLength;
}

void TextField::setMaxLength(int length) {
    if (m_maxLength != length) {
        m_maxLength = length;
        QLineEdit::setMaxLength(length > 0 ? length : 32767);
        update();
    }
}

// ============================================================================
// Size Hints
// ============================================================================

QSize TextField::sizeHint() const {
    CanvasUnitHelper helper(qApp->devicePixelRatio());

    // Material Design text field specifications:
    // - Height: 56dp (filled), 56dp (outlined)
    // - Label height: 16dp
    // - Text height: 24dp
    // - Helper text height: 16dp
    float fieldHeight = helper.dpToPx(56.0f);
    float helperHeight = m_helperText.isEmpty() && m_errorText.isEmpty() ? 0 : helper.dpToPx(16.0f);
    float totalHeight = fieldHeight + helperHeight;

    // Minimum width for touch target
    float minWidth = helper.dpToPx(280.0f);
    float contentWidth = helper.dpToPx(280.0f); // Default width

    return QSize(int(std::ceil(contentWidth)), int(std::ceil(totalHeight)));
}

QSize TextField::minimumSizeHint() const {
    CanvasUnitHelper helper(qApp->devicePixelRatio());

    float fieldHeight = helper.dpToPx(56.0f);
    float helperHeight = m_helperText.isEmpty() && m_errorText.isEmpty() ? 0 : helper.dpToPx(16.0f);
    float totalHeight = fieldHeight + helperHeight;

    // Minimum width for usability
    float minWidth = helper.dpToPx(200.0f);

    return QSize(int(std::ceil(minWidth)), int(std::ceil(totalHeight)));
}

// ============================================================================
// Color Access Methods
// ============================================================================

CFColor TextField::containerColor() const {
    auto* app = Application::instance();
    if (!app) {
        return fallbackSurface();
    }

    try {
        const auto& theme = app->currentTheme();
        auto& colorScheme = theme.color_scheme();
        return CFColor(colorScheme.queryColor(SURFACE));
    } catch (...) {
        return fallbackSurface();
    }
}

CFColor TextField::onContainerColor() const {
    auto* app = Application::instance();
    if (!app) {
        return fallbackOnSurfaceVariant();
    }

    try {
        const auto& theme = app->currentTheme();
        auto& colorScheme = theme.color_scheme();
        return CFColor(colorScheme.queryColor(ON_SURFACE_VARIANT));
    } catch (...) {
        return fallbackOnSurfaceVariant();
    }
}

CFColor TextField::labelColor() const {
    auto* app = Application::instance();
    if (!app) {
        return fallbackOnSurfaceVariant();
    }

    try {
        const auto& theme = app->currentTheme();
        auto& colorScheme = theme.color_scheme();
        return CFColor(colorScheme.queryColor(ON_SURFACE_VARIANT));
    } catch (...) {
        return fallbackOnSurfaceVariant();
    }
}

CFColor TextField::inputTextColor() const {
    auto* app = Application::instance();
    if (!app) {
        return fallbackOnSurface();
    }

    try {
        const auto& theme = app->currentTheme();
        auto& colorScheme = theme.color_scheme();
        return CFColor(colorScheme.queryColor(ON_SURFACE));
    } catch (...) {
        return fallbackOnSurface();
    }
}

CFColor TextField::outlineColor() const {
    auto* app = Application::instance();
    if (!app) {
        return fallbackOutline();
    }

    try {
        const auto& theme = app->currentTheme();
        auto& colorScheme = theme.color_scheme();
        return CFColor(colorScheme.queryColor(OUTLINE));
    } catch (...) {
        return fallbackOutline();
    }
}

CFColor TextField::focusOutlineColor() const {
    auto* app = Application::instance();
    if (!app) {
        return fallbackPrimary();
    }

    try {
        const auto& theme = app->currentTheme();
        auto& colorScheme = theme.color_scheme();
        return CFColor(colorScheme.queryColor(PRIMARY));
    } catch (...) {
        return fallbackPrimary();
    }
}

CFColor TextField::errorColor() const {
    auto* app = Application::instance();
    if (!app) {
        return fallbackError();
    }

    try {
        const auto& theme = app->currentTheme();
        auto& colorScheme = theme.color_scheme();
        return CFColor(colorScheme.queryColor(ERROR));
    } catch (...) {
        return fallbackError();
    }
}

CFColor TextField::helperTextColor() const {
    auto* app = Application::instance();
    if (!app) {
        return fallbackOnSurfaceVariant();
    }

    try {
        const auto& theme = app->currentTheme();
        auto& colorScheme = theme.color_scheme();
        return CFColor(colorScheme.queryColor(ON_SURFACE_VARIANT));
    } catch (...) {
        return fallbackOnSurfaceVariant();
    }
}

float TextField::cornerRadius() const {
    CanvasUnitHelper helper(qApp->devicePixelRatio());
    // Small corner radius (4dp)
    return helper.dpToPx(4.0f);
}

QFont TextField::inputFont() const {
    auto* app = Application::instance();
    if (!app) {
        QFont font = QLineEdit::font();
        font.setPixelSize(16);
        return font;
    }

    try {
        const auto& theme = app->currentTheme();
        auto& fontType = theme.font_type();
        return fontType.queryTargetFont("bodyLarge");
    } catch (...) {
        QFont font = QLineEdit::font();
        font.setPixelSize(16);
        return font;
    }
}

QFont TextField::labelFont() const {
    auto* app = Application::instance();
    if (!app) {
        QFont font = QLineEdit::font();
        font.setPixelSize(16);
        return font;
    }

    try {
        const auto& theme = app->currentTheme();
        auto& fontType = theme.font_type();
        return fontType.queryTargetFont("bodyLarge");
    } catch (...) {
        QFont font = QLineEdit::font();
        font.setPixelSize(16);
        return font;
    }
}

QFont TextField::helperFont() const {
    auto* app = Application::instance();
    if (!app) {
        QFont font = QLineEdit::font();
        font.setPixelSize(12);
        return font;
    }

    try {
        const auto& theme = app->currentTheme();
        auto& fontType = theme.font_type();
        return fontType.queryTargetFont("bodySmall");
    } catch (...) {
        QFont font = QLineEdit::font();
        font.setPixelSize(12);
        return font;
    }
}

// ============================================================================
// Layout Helpers
// ============================================================================

QRectF TextField::fieldRect() const {
    CanvasUnitHelper helper(qApp->devicePixelRatio());
    float fieldHeight = helper.dpToPx(56.0f);
    return QRectF(0, 0, width(), fieldHeight);
}

QRectF TextField::textRect() const {
    CanvasUnitHelper helper(qApp->devicePixelRatio());

    QRectF field = fieldRect();
    float hPadding = helper.dpToPx(16.0f);

    // Account for prefix icon
    float prefixWidth = 0;
    if (!m_prefixIcon.isNull()) {
        prefixWidth = helper.dpToPx(24.0f) + helper.dpToPx(12.0f); // icon + gap
    }

    // Account for suffix icon
    float suffixWidth = 0;
    if (!m_suffixIcon.isNull()) {
        suffixWidth = helper.dpToPx(24.0f) + helper.dpToPx(12.0f); // icon + gap
    }

    // Account for clear button
    float clearWidth = 0;
    if (!text().isEmpty()) {
        clearWidth = helper.dpToPx(24.0f) + helper.dpToPx(8.0f); // button + gap
    }

    // For floating label, adjust top margin
    float topMargin = m_isFloating ? helper.dpToPx(16.0f) : helper.dpToPx(28.0f);

    float left = hPadding + prefixWidth;
    float top = topMargin;
    float availableWidth = field.width() - hPadding * 2 - prefixWidth - suffixWidth - clearWidth;
    float height = helper.dpToPx(24.0f); // Text line height

    return QRectF(left, top, availableWidth, height);
}

QRectF TextField::helperTextRect() const {
    CanvasUnitHelper helper(qApp->devicePixelRatio());

    QRectF field = fieldRect();
    float helperHeight = helper.dpToPx(16.0f);
    float top = field.bottom();

    return QRectF(field.left(), top, field.width(), helperHeight);
}

QRectF TextField::prefixIconRect() const {
    if (m_prefixIcon.isNull()) {
        return QRectF();
    }

    CanvasUnitHelper helper(qApp->devicePixelRatio());

    QRectF field = fieldRect();
    float hPadding = helper.dpToPx(16.0f);
    float iconSize = helper.dpToPx(24.0f);
    float centerY = field.center().y();

    return QRectF(hPadding, centerY - iconSize / 2, iconSize, iconSize);
}

QRectF TextField::suffixIconRect() const {
    if (m_suffixIcon.isNull()) {
        return QRectF();
    }

    CanvasUnitHelper helper(qApp->devicePixelRatio());

    QRectF field = fieldRect();
    float hPadding = helper.dpToPx(16.0f);
    float iconSize = helper.dpToPx(24.0f);
    float gap = helper.dpToPx(12.0f);
    float centerY = field.center().y();

    // Account for clear button
    float clearOffset = 0;
    if (!text().isEmpty()) {
        clearOffset = helper.dpToPx(24.0f) + helper.dpToPx(8.0f);
    }

    float right = field.width() - hPadding - clearOffset;

    return QRectF(right - iconSize, centerY - iconSize / 2, iconSize, iconSize);
}

QRectF TextField::clearButtonRect() const {
    if (text().isEmpty()) {
        return QRectF();
    }

    CanvasUnitHelper helper(qApp->devicePixelRatio());

    QRectF field = fieldRect();
    float hPadding = helper.dpToPx(16.0f);
    float iconSize = helper.dpToPx(24.0f);
    float centerY = field.center().y();

    float right = field.width() - hPadding;

    return QRectF(right - iconSize, centerY - iconSize / 2, iconSize, iconSize);
}

// ============================================================================
// Drawing Helpers
// ============================================================================

void TextField::drawBackground(QPainter& p, const QRectF& fieldRect) {
    if (m_variant == TextFieldVariant::Outlined) {
        return; // Outlined variant has no background
    }

    // Filled variant background
    CFColor bg = containerColor();
    if (!isEnabled()) {
        QColor color = bg.native_color();
        color.setAlphaF(0.38f);
        p.fillRect(fieldRect, color);
        return;
    }

    p.fillRect(fieldRect, bg.native_color());
}

void TextField::drawOutline(QPainter& p, const QRectF& fieldRect) {
    CanvasUnitHelper helper(qApp->devicePixelRatio());
    float radius = cornerRadius();

    if (m_variant == TextFieldVariant::Filled) {
        // Filled variant: draw bottom line only
        float lineWidth = helper.dpToPx(1.0f);

        // Use active width when focused or floating
        float activeWidth = helper.dpToPx(2.0f);
        float currentWidth = lineWidth;
        if (hasFocus() || m_isFloating) {
            currentWidth = lineWidth + (activeWidth - lineWidth) * m_floatingProgress;
        }

        // Determine color
        QColor color;
        if (m_hasError) {
            color = errorColor().native_color();
        } else if (hasFocus()) {
            color = focusOutlineColor().native_color();
        } else {
            color = outlineColor().native_color();
        }

        if (!isEnabled()) {
            color.setAlphaF(0.38f);
        }

        // Draw bottom line
        p.fillRect(QRectF(fieldRect.left(), fieldRect.bottom() - currentWidth,
                          fieldRect.width(), currentWidth),
                   color);
    } else {
        // Outlined variant: draw rounded rectangle with gap for label
        QPainterPath shape = roundedRect(fieldRect, radius);

        // Determine color
        QColor color;
        if (m_hasError) {
            color = errorColor().native_color();
        } else if (hasFocus()) {
            color = focusOutlineColor().native_color();
        } else {
            color = outlineColor().native_color();
        }

        if (!isEnabled()) {
            color.setAlphaF(0.38f);
        }

        // Calculate outline width
        float baseWidth = helper.dpToPx(1.0f);
        float activeWidth = helper.dpToPx(2.0f);
        float currentWidth = hasFocus() ? activeWidth : baseWidth;

        QPen pen(color, currentWidth);
        pen.setCosmetic(true);
        p.setPen(pen);
        p.setBrush(Qt::NoBrush);
        p.drawPath(shape);
    }
}

void TextField::drawLabel(QPainter& p, const QRectF& fieldRect) {
    if (m_label.isEmpty()) {
        return;
    }

    CanvasUnitHelper helper(qApp->devicePixelRatio());

    QFont labelF = labelFont();
    QFontMetricsF fm(labelF);

    // Calculate label position based on floating state
    float floatingScale = 0.75f; // 75% of normal size
    float floatingY = helper.dpToPx(12.0f); // Top position when floating
    float restingY = fieldRect.center().y(); // Center when resting

    // Interpolate between resting and floating positions
    float currentY = restingY + (floatingY - restingY) * m_floatingProgress;
    float currentScale = 1.0f - (1.0f - floatingScale) * m_floatingProgress;

    // Calculate horizontal position
    float leftMargin = helper.dpToPx(16.0f);

    // For outlined variant with focus, add horizontal padding
    if (m_variant == TextFieldVariant::Outlined && hasFocus()) {
        leftMargin += helper.dpToPx(4.0f);
    }

    // Account for prefix icon
    if (!m_prefixIcon.isNull()) {
        leftMargin += helper.dpToPx(24.0f) + helper.dpToPx(12.0f);
    }

    // For filled variant, create background gap when floating
    if (m_variant == TextFieldVariant::Filled && m_floatingProgress > 0.5f) {
        float bgWidth = fm.horizontalAdvance(m_label) * currentScale + helper.dpToPx(8.0f);
        QRectF bgRect(leftMargin - helper.dpToPx(4.0f),
                      floatingY - fm.height() * currentScale / 2 - helper.dpToPx(2.0f),
                      bgWidth, fm.height() * currentScale + helper.dpToPx(4.0f));
        p.fillRect(bgRect, containerColor().native_color());
    }

    p.save();

    // Set font and color
    labelF.setPixelSize(static_cast<int>(labelF.pixelSize() * currentScale));
    p.setFont(labelF);

    QColor labelColor;
    if (m_hasError) {
        labelColor = errorColor().native_color();
    } else if (hasFocus() || m_isFloating) {
        labelColor = focusOutlineColor().native_color();
    } else {
        labelColor = this->labelColor().native_color();
    }

    if (!isEnabled()) {
        labelColor.setAlphaF(0.38f);
    }

    p.setPen(labelColor);

    // Draw label
    QRectF textRect(leftMargin, currentY - fm.height() * currentScale / 2,
                    fieldRect.width() - leftMargin * 2, fm.height() * currentScale);
    p.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, m_label);

    p.restore();
}

void TextField::drawText(QPainter& p, const QRectF& textRect) {
    // Use QLineEdit's default text rendering but clip to our text rect
    p.save();
    p.setClipRect(textRect);

    CanvasUnitHelper helper(qApp->devicePixelRatio());

    QFont f = inputFont();
    p.setFont(f);

    QColor textColor = inputTextColor().native_color();
    if (!isEnabled()) {
        textColor.setAlphaF(0.38f);
    }
    p.setPen(textColor);

    // Get display text
    QString displayText = text();
    if (displayText.isEmpty() && placeholderText().isEmpty() == false) {
        // Show placeholder
        QColor placeholderColor = onContainerColor().native_color();
        placeholderColor.setAlphaF(0.6f);
        p.setPen(placeholderColor);
        displayText = placeholderText();
    }

    // Draw text
    QRectF adjustedRect = textRect;
    adjustedRect.setHeight(QFontMetricsF(f).height());

    // Handle password mode
    if (echoMode() == Password) {
        displayText = QString(displayText.length(), QChar(0x2022)); // Bullet character
    }

    p.drawText(adjustedRect, Qt::AlignLeft | Qt::AlignTop, displayText);

    // Draw cursor
    if (hasFocus() && cursorPosition() >= 0) {
        QFontMetricsF fm(f);
        int cursorPos = qBound(0, cursorPosition(), displayText.length());
        float cursorX = textRect.left() + fm.horizontalAdvance(displayText.left(cursorPos));

        float cursorWidth = helper.dpToPx(1.0f);
        QRectF cursorRect(cursorX, adjustedRect.top(), cursorWidth, adjustedRect.height());

        p.fillRect(cursorRect, focusOutlineColor().native_color());
    }

    p.restore();
}

void TextField::drawPrefixIcon(QPainter& p, const QRectF& textRect) {
    if (m_prefixIcon.isNull()) {
        return;
    }

    QRectF iconRect = prefixIconRect();
    if (iconRect.isEmpty()) {
        return;
    }

    QColor iconColor = onContainerColor().native_color();
    if (!isEnabled()) {
        iconColor.setAlphaF(0.38f);
    }

    QIcon::Mode mode = isEnabled() ? QIcon::Normal : QIcon::Disabled;
    m_prefixIcon.paint(&p, iconRect.toRect(), Qt::AlignCenter, mode);
}

void TextField::drawSuffixIcon(QPainter& p, const QRectF& textRect) {
    if (m_suffixIcon.isNull()) {
        return;
    }

    QRectF iconRect = suffixIconRect();
    if (iconRect.isEmpty()) {
        return;
    }

    QIcon::Mode mode = isEnabled() ? QIcon::Normal : QIcon::Disabled;
    m_suffixIcon.paint(&p, iconRect.toRect(), Qt::AlignCenter, mode);
}

void TextField::drawClearButton(QPainter& p, const QRectF& textRect) {
    Q_UNUSED(textRect)
    if (text().isEmpty()) {
        return;
    }

    QRectF iconRect = clearButtonRect();
    if (iconRect.isEmpty()) {
        return;
    }

    CanvasUnitHelper helper(qApp->devicePixelRatio());

    p.save();

    // Draw clear "X" icon
    QColor iconColor = onContainerColor().native_color();
    if (m_hoveringClearButton || m_pressingClearButton) {
        iconColor = iconColor.lighter(120);
    }
    if (!isEnabled()) {
        iconColor.setAlphaF(0.38f);
    }

    p.setPen(QPen(iconColor, helper.dpToPx(2.0f)));
    p.setRenderHint(QPainter::Antialiasing);

    float cx = iconRect.center().x();
    float cy = iconRect.center().y();
    float size = helper.dpToPx(18.0f);
    float half = size / 2;

    p.drawLine(QPointF(cx - half, cy - half), QPointF(cx + half, cy + half));
    p.drawLine(QPointF(cx + half, cy - half), QPointF(cx - half, cy + half));

    p.restore();
}

void TextField::drawHelperText(QPainter& p, const QRectF& helperRect) {
    QString text = m_hasError ? m_errorText : m_helperText;
    if (text.isEmpty()) {
        return;
    }

    p.save();

    QFont f = helperFont();
    p.setFont(f);

    QColor textColor = m_hasError ? errorColor().native_color() : helperTextColor().native_color();
    if (!isEnabled()) {
        textColor.setAlphaF(0.38f);
    }
    p.setPen(textColor);

    p.drawText(helperRect, Qt::AlignLeft | Qt::AlignVCenter, text);

    p.restore();
}

void TextField::drawCharacterCounter(QPainter& p, const QRectF& helperRect) {
    if (!m_showCharacterCounter) {
        return;
    }

    p.save();

    QFont f = helperFont();
    p.setFont(f);

    QString counterText = QString("%1/%2").arg(text().length()).arg(m_maxLength);
    QFontMetricsF fm(f);
    float textWidth = fm.horizontalAdvance(counterText);

    QColor textColor = helperTextColor().native_color();
    if (text().length() > m_maxLength) {
        textColor = errorColor().native_color();
    }
    if (!isEnabled()) {
        textColor.setAlphaF(0.38f);
    }
    p.setPen(textColor);

    QRectF counterRect(helperRect.right() - textWidth, helperRect.top(), textWidth,
                       helperRect.height());
    p.drawText(counterRect, Qt::AlignRight | Qt::AlignVCenter, counterText);

    p.restore();
}

void TextField::drawFocusIndicator(QPainter& p, const QRectF& fieldRect) {
    if (m_focusIndicator && hasFocus()) {
        QPainterPath shape = roundedRect(fieldRect, cornerRadius());
        m_focusIndicator->paint(&p, shape, focusOutlineColor());
    }
}

void TextField::drawRipple(QPainter& p, const QRectF& fieldRect) {
    if (m_ripple) {
        QPainterPath shape;
        if (m_variant == TextFieldVariant::Outlined) {
            shape = roundedRect(fieldRect, cornerRadius());
        } else {
            // For filled variant, clip to background
            shape.addRect(fieldRect);
        }
        m_ripple->paint(&p, shape);
    }
}

// ============================================================================
// Animation Helpers
// ============================================================================

void TextField::updateFloatingState(bool shouldFloat) {
    if (m_isFloating != shouldFloat) {
        m_isFloating = shouldFloat;
        emit floatingChanged(m_isFloating);
    }
    animateFloatingTo(shouldFloat);
}

void TextField::animateFloatingTo(bool floating) {
    float target = floating ? 1.0f : 0.0f;

    // Skip animation if already at the target state
    if (qFuzzyCompare(m_floatingProgress, target)) {
        return;
    }

    if (!m_animationFactory) {
        m_floatingProgress = target;
        update();
        return;
    }

    // Create property animation for floating progress
    auto anim = m_animationFactory->createPropertyAnimation(
        &m_floatingProgress, m_floatingProgress, target, 200,
        cf::ui::base::Easing::Type::EmphasizedDecelerate, this);

    if (anim) {
        anim->start();
    } else {
        m_floatingProgress = target;
        update();
    }
}

} // namespace cf::ui::widget::material

Updated on 2026-03-09 at 10:14:01 +0000