ui/widget/material/widget/textarea/textarea.cpp¶
Material Design 3 TextArea Implementation. More...
Namespaces¶
| Name |
|---|
| cf |
| cf::ui |
| cf::ui::widget |
| cf::ui::widget::material |
Detailed Description¶
Material Design 3 TextArea Implementation.
Author: CFDesktop Team
Version: 0.1
Since: 0.1
Date: 2026-03-01
Implements a Material Design 3 text area with filled and outlined variants. Supports floating labels, character counter, helper/error text, and multi-line text input with auto-resize support.
Source code¶
#include "textarea.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 <QKeyEvent>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QTextBlock>
#include <QTextCursor>
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
// ============================================================================
TextArea::TextArea(TextAreaVariant variant, QWidget* parent)
: QTextEdit(parent), m_variant(variant), m_showCharacterCounter(false), m_maxLength(0),
m_minLines(1), m_maxLines(0), m_isFloating(false), m_hasError(false),
m_floatingProgress(0.0f), m_updatingGeometry(false) {
// Disable native frame
setFrameStyle(QFrame::NoFrame);
// Set viewport margins for custom drawing space above text
setViewportMargins(0, 0, 0, 0);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
// 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(&TextArea::update));
connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this,
QOverload<>::of(&TextArea::update));
// Connect text change signal
connect(this, &QTextEdit::textChanged, this, &TextArea::textChanged);
// Set default font
setFont(inputFont());
// Set cursor
setCursor(Qt::IBeamCursor);
// Disable tab focus for proper multi-line editing
setTabChangesFocus(false);
}
TextArea::TextArea(const QString& text, TextAreaVariant variant, QWidget* parent)
: TextArea(variant, parent) {
setText(text);
}
TextArea::~TextArea() {
// Components are parented to this, Qt will delete them automatically
}
// ============================================================================
// Event Handlers
// ============================================================================
void TextArea::mousePressEvent(QMouseEvent* event) {
QTextEdit::mousePressEvent(event);
if (m_stateMachine)
m_stateMachine->onPress(event->pos());
if (m_ripple)
m_ripple->onPress(event->pos(), rect());
update();
}
void TextArea::mouseReleaseEvent(QMouseEvent* event) {
QTextEdit::mouseReleaseEvent(event);
if (m_stateMachine)
m_stateMachine->onRelease();
if (m_ripple)
m_ripple->onRelease();
update();
}
void TextArea::focusInEvent(QFocusEvent* event) {
QTextEdit::focusInEvent(event);
if (m_stateMachine)
m_stateMachine->onFocusIn();
if (m_focusIndicator)
m_focusIndicator->onFocusIn();
updateFloatingState(true);
update();
}
void TextArea::focusOutEvent(QFocusEvent* event) {
QTextEdit::focusOutEvent(event);
if (m_stateMachine)
m_stateMachine->onFocusOut();
if (m_focusIndicator)
m_focusIndicator->onFocusOut();
updateFloatingState(!toPlainText().isEmpty());
update();
}
void TextArea::changeEvent(QEvent* event) {
QTextEdit::changeEvent(event);
if (event->type() == QEvent::EnabledChange) {
if (m_stateMachine) {
if (isEnabled()) {
m_stateMachine->onEnable();
} else {
m_stateMachine->onDisable();
}
}
update();
}
}
void TextArea::resizeEvent(QResizeEvent* event) {
QTextEdit::resizeEvent(event);
update();
}
void TextArea::textChanged() {
// Enforce max length
if (m_maxLength > 0) {
QString text = toPlainText();
if (text.length() > m_maxLength) {
// Truncate to max length
text = text.left(m_maxLength);
m_updatingGeometry = true;
setPlainText(text);
m_updatingGeometry = false;
// Move cursor to end
QTextCursor cursor = textCursor();
cursor.movePosition(QTextCursor::End);
setTextCursor(cursor);
}
}
// Update floating state based on content
updateFloatingState(hasFocus() || !toPlainText().isEmpty());
// Update character counter visibility
if (m_showCharacterCounter) {
update();
}
// Auto-resize based on line count
if (!m_updatingGeometry) {
updateGeometryForLines();
}
}
void TextArea::keyPressEvent(QKeyEvent* event) {
// Block Enter key if maxLines is set and already at limit
if (m_maxLines > 0 && (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)) {
if (document()->blockCount() >= m_maxLines) {
// Ignore the Enter key
return;
}
}
QTextEdit::keyPressEvent(event);
}
void TextArea::paintEvent(QPaintEvent* event) {
// Draw custom decorations on the main widget (behind the viewport)
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 ripple
drawRipple(p, field);
// Step 5: Draw focus indicator
drawFocusIndicator(p, field);
// Step 6: Draw helper/error text
drawHelperText(p, helper);
// Step 7: Draw character counter
if (m_showCharacterCounter) {
drawCharacterCounter(p, helper);
}
// Let QTextEdit handle its own text rendering in the viewport
QTextEdit::paintEvent(event);
}
// ============================================================================
// Property Getters/Setters
// ============================================================================
TextArea::TextAreaVariant TextArea::variant() const {
return m_variant;
}
void TextArea::setVariant(TextAreaVariant variant) {
if (m_variant != variant) {
m_variant = variant;
updateGeometry();
update();
}
}
QString TextArea::label() const {
return m_label;
}
void TextArea::setLabel(const QString& label) {
if (m_label != label) {
m_label = label;
updateGeometry();
update();
}
}
QString TextArea::helperText() const {
return m_helperText;
}
void TextArea::setHelperText(const QString& text) {
if (m_helperText != text) {
m_helperText = text;
updateGeometry();
update();
}
}
QString TextArea::errorText() const {
return m_errorText;
}
void TextArea::setErrorText(const QString& text) {
if (m_errorText != text) {
m_errorText = text;
m_hasError = !text.isEmpty();
updateGeometry();
update();
}
}
bool TextArea::isFloating() const {
return m_isFloating;
}
bool TextArea::showCharacterCounter() const {
return m_showCharacterCounter;
}
void TextArea::setShowCharacterCounter(bool show) {
if (m_showCharacterCounter != show) {
m_showCharacterCounter = show;
updateGeometry();
update();
}
}
int TextArea::maxLength() const {
return m_maxLength;
}
void TextArea::setMaxLength(int length) {
if (m_maxLength != length) {
m_maxLength = length;
update();
}
}
int TextArea::minLines() const {
return m_minLines;
}
void TextArea::setMinLines(int lines) {
if (m_minLines != lines && lines > 0) {
m_minLines = lines;
updateGeometry();
update();
}
}
int TextArea::maxLines() const {
return m_maxLines;
}
void TextArea::setMaxLines(int lines) {
if (m_maxLines != lines && lines >= 0) {
m_maxLines = lines;
updateGeometry();
update();
}
}
// ============================================================================
// Size Hints
// ============================================================================
QSize TextArea::sizeHint() const {
CanvasUnitHelper helper(qApp->devicePixelRatio());
// Calculate line height
QFont f = inputFont();
QFontMetricsF fm(f);
float lineHeight = fm.height() + helper.dpToPx(8.0f); // Line spacing
// Calculate height based on line count
int lineCount = qMax(m_minLines, document()->blockCount());
if (m_maxLines > 0) {
lineCount = qMin(lineCount, m_maxLines);
}
float contentHeight = lineHeight * lineCount;
float labelHeight = m_label.isEmpty() ? 0 : helper.dpToPx(16.0f);
float vPadding = helper.dpToPx(16.0f); // Top and bottom padding
float fieldHeight = contentHeight + vPadding * 2;
// Add space for floating label (it overlays content)
float totalHeight = fieldHeight;
// Add helper text height
float helperHeight = m_helperText.isEmpty() && m_errorText.isEmpty() ? 0 : helper.dpToPx(16.0f);
totalHeight += 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 TextArea::minimumSizeHint() const {
CanvasUnitHelper helper(qApp->devicePixelRatio());
QFont f = inputFont();
QFontMetricsF fm(f);
float lineHeight = fm.height() + helper.dpToPx(8.0f);
float contentHeight = lineHeight * m_minLines;
float vPadding = helper.dpToPx(16.0f);
float fieldHeight = contentHeight + vPadding * 2;
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 TextArea::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 TextArea::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 TextArea::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 TextArea::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 TextArea::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 TextArea::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 TextArea::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 TextArea::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 TextArea::cornerRadius() const {
CanvasUnitHelper helper(qApp->devicePixelRatio());
// Small corner radius (4dp)
return helper.dpToPx(4.0f);
}
QFont TextArea::inputFont() const {
auto* app = Application::instance();
if (!app) {
QFont font = QTextEdit::font();
font.setPixelSize(16);
return font;
}
try {
const auto& theme = app->currentTheme();
auto& fontType = theme.font_type();
return fontType.queryTargetFont("bodyLarge");
} catch (...) {
QFont font = QTextEdit::font();
font.setPixelSize(16);
return font;
}
}
QFont TextArea::labelFont() const {
auto* app = Application::instance();
if (!app) {
QFont font = QTextEdit::font();
font.setPixelSize(16);
return font;
}
try {
const auto& theme = app->currentTheme();
auto& fontType = theme.font_type();
return fontType.queryTargetFont("bodyLarge");
} catch (...) {
QFont font = QTextEdit::font();
font.setPixelSize(16);
return font;
}
}
QFont TextArea::helperFont() const {
auto* app = Application::instance();
if (!app) {
QFont font = QTextEdit::font();
font.setPixelSize(12);
return font;
}
try {
const auto& theme = app->currentTheme();
auto& fontType = theme.font_type();
return fontType.queryTargetFont("bodySmall");
} catch (...) {
QFont font = QTextEdit::font();
font.setPixelSize(12);
return font;
}
}
// ============================================================================
// Layout Helpers
// ============================================================================
QRectF TextArea::fieldRect() const {
CanvasUnitHelper helper(qApp->devicePixelRatio());
// Calculate height based on content
QFont f = inputFont();
QFontMetricsF fm(f);
float lineHeight = fm.height() + helper.dpToPx(8.0f);
int lineCount = qMax(m_minLines, document()->blockCount());
if (m_maxLines > 0) {
lineCount = qMin(lineCount, m_maxLines);
}
float contentHeight = lineHeight * lineCount;
float vPadding = helper.dpToPx(16.0f);
float fieldHeight = contentHeight + vPadding * 2;
return QRectF(0, 0, width(), fieldHeight);
}
QRectF TextArea::textRect() const {
CanvasUnitHelper helper(qApp->devicePixelRatio());
QRectF field = fieldRect();
float hPadding = helper.dpToPx(16.0f);
// Account for floating label
float topMargin = helper.dpToPx(16.0f);
if (m_isFloating && !m_label.isEmpty()) {
topMargin += helper.dpToPx(8.0f);
}
float left = hPadding;
float top = topMargin;
float availableWidth = field.width() - hPadding * 2;
float availableHeight = field.height() - topMargin - helper.dpToPx(16.0f);
return QRectF(left, top, availableWidth, availableHeight);
}
QRectF TextArea::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);
}
// ============================================================================
// Drawing Helpers
// ============================================================================
void TextArea::drawBackground(QPainter& p, const QRectF& fieldRect) {
if (m_variant == TextAreaVariant::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 TextArea::drawOutline(QPainter& p, const QRectF& fieldRect) {
CanvasUnitHelper helper(qApp->devicePixelRatio());
float radius = cornerRadius();
if (m_variant == TextAreaVariant::Filled) {
// Filled variant: draw bottom border only
float lineWidth = helper.dpToPx(1.0f);
// Use active width when focused
float activeWidth = helper.dpToPx(2.0f);
float currentWidth = hasFocus() ? activeWidth : lineWidth;
// 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
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 TextArea::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 = helper.dpToPx(28.0f); // Lower position 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 == TextAreaVariant::Outlined && hasFocus()) {
leftMargin += helper.dpToPx(4.0f);
}
// For filled variant, create background gap when floating
if (m_variant == TextAreaVariant::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 TextArea::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 TextArea::drawCharacterCounter(QPainter& p, const QRectF& helperRect) {
if (!m_showCharacterCounter) {
return;
}
p.save();
QFont f = helperFont();
p.setFont(f);
QString counterText = QString("%1/%2").arg(toPlainText().length()).arg(m_maxLength);
QFontMetricsF fm(f);
float textWidth = fm.horizontalAdvance(counterText);
QColor textColor = helperTextColor().native_color();
if (toPlainText().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 TextArea::drawFocusIndicator(QPainter& p, const QRectF& fieldRect) {
if (m_focusIndicator && hasFocus()) {
QPainterPath shape = roundedRect(fieldRect, cornerRadius());
m_focusIndicator->paint(&p, shape, focusOutlineColor());
}
}
void TextArea::drawRipple(QPainter& p, const QRectF& fieldRect) {
if (m_ripple) {
QPainterPath shape;
if (m_variant == TextAreaVariant::Outlined) {
shape = roundedRect(fieldRect, cornerRadius());
} else {
// For filled variant, clip to background
shape.addRect(fieldRect);
}
m_ripple->paint(&p, shape);
}
}
// ============================================================================
// Animation Helpers
// ============================================================================
void TextArea::updateFloatingState(bool shouldFloat) {
if (m_isFloating != shouldFloat) {
m_isFloating = shouldFloat;
emit floatingChanged(m_isFloating);
}
animateFloatingTo(shouldFloat);
}
void TextArea::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 (same approach as TextField)
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();
}
}
void TextArea::updateGeometryForLines() {
// Calculate new height based on content
CanvasUnitHelper helper(qApp->devicePixelRatio());
QFont f = inputFont();
QFontMetricsF fm(f);
float lineHeight = fm.height() + helper.dpToPx(8.0f);
// Calculate line count
int lineCount = qMax(m_minLines, document()->blockCount());
if (m_maxLines > 0) {
lineCount = qMin(lineCount, m_maxLines);
}
float contentHeight = lineHeight * lineCount;
float vPadding = helper.dpToPx(16.0f);
float fieldHeight = contentHeight + vPadding * 2;
// Add helper text height
float helperHeight = m_helperText.isEmpty() && m_errorText.isEmpty() ? 0 : helper.dpToPx(16.0f);
float totalHeight = fieldHeight + helperHeight;
// Set the new height
int newHeight = int(std::ceil(totalHeight));
if (height() != newHeight) {
setFixedHeight(newHeight);
}
}
} // namespace cf::ui::widget::material
Updated on 2026-03-09 at 10:14:01 +0000