1// Copyright (C) 2023 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquickmaterialplaceholdertext_p.h"
5
6#include <QtCore/qpropertyanimation.h>
7#include <QtCore/qparallelanimationgroup.h>
8#include <QtGui/qpainter.h>
9#include <QtGui/qpainterpath.h>
10#include <QtQml/qqmlinfo.h>
11#include <QtQuickTemplates2/private/qquicktheme_p.h>
12#include <QtQuickTemplates2/private/qquicktextarea_p.h>
13#include <QtQuickTemplates2/private/qquicktextfield_p.h>
14
15QT_BEGIN_NAMESPACE
16
17static const qreal floatingScale = 0.8;
18Q_GLOBAL_STATIC(QEasingCurve, animationEasingCurve, QEasingCurve::OutSine);
19
20/*
21 This class makes it easier to animate the various placeholder text changes
22 for each type of text container (filled, outlined).
23
24 By doing animations in C++, we avoid having a bunch of states, transitions,
25 and animations (which are all QObjects) declared in QML, even if that text
26 control never gets focus and hence never needs them.
27*/
28
29QQuickMaterialPlaceholderText::QQuickMaterialPlaceholderText(QQuickItem *parent)
30 : QQuickPlaceholderText(parent)
31{
32 // Ensure that scaling happens on the left side, at the vertical center.
33 setTransformOrigin(QQuickItem::Left);
34}
35
36bool QQuickMaterialPlaceholderText::isFilled() const
37{
38 return m_filled;
39}
40
41void QQuickMaterialPlaceholderText::setFilled(bool filled)
42{
43 if (filled == m_filled)
44 return;
45
46 m_filled = filled;
47 update();
48 void filledChanged();
49}
50
51bool QQuickMaterialPlaceholderText::controlHasActiveFocus() const
52{
53 return m_controlHasActiveFocus;
54}
55
56void QQuickMaterialPlaceholderText::setControlHasActiveFocus(bool controlHasActiveFocus)
57{
58 if (m_controlHasActiveFocus == controlHasActiveFocus)
59 return;
60
61 m_controlHasActiveFocus = controlHasActiveFocus;
62 if (m_controlHasActiveFocus)
63 controlGotActiveFocus();
64 else
65 controlLostActiveFocus();
66 emit controlHasActiveFocusChanged();
67}
68
69bool QQuickMaterialPlaceholderText::controlHasText() const
70{
71 return m_controlHasText;
72}
73
74void QQuickMaterialPlaceholderText::setControlHasText(bool controlHasText)
75{
76 if (m_controlHasText == controlHasText)
77 return;
78
79 m_controlHasText = controlHasText;
80 maybeSetFocusAnimationProgress();
81 emit controlHasTextChanged();
82}
83
84/*
85 Placeholder text of outlined text fields should float when:
86 - There is placeholder text, and
87 - The control has active focus, or
88 - The control has text
89*/
90bool QQuickMaterialPlaceholderText::shouldFloat() const
91{
92 const bool controlHasActiveFocusOrText = m_controlHasActiveFocus || m_controlHasText;
93 return m_filled
94 ? controlHasActiveFocusOrText
95 : !text().isEmpty() && controlHasActiveFocusOrText;
96}
97
98bool QQuickMaterialPlaceholderText::shouldAnimate() const
99{
100 return m_filled
101 ? !m_controlHasText
102 : !m_controlHasText && !text().isEmpty();
103}
104
105void QQuickMaterialPlaceholderText::updateY()
106{
107 setY(shouldFloat() ? floatingTargetY() : normalTargetY());
108}
109
110qreal controlTopInset(QQuickItem *textControl)
111{
112 if (const auto textArea = qobject_cast<QQuickTextArea *>(object: textControl))
113 return textArea->topInset();
114
115 if (const auto textField = qobject_cast<QQuickTextField *>(object: textControl))
116 return textField->topInset();
117
118 return 0;
119}
120
121qreal QQuickMaterialPlaceholderText::normalTargetY() const
122{
123 auto *textArea = qobject_cast<QQuickTextArea *>(object: textControl());
124 if (textArea && m_controlHeight >= textArea->implicitHeight()) {
125 // TextArea can be multiple lines in height, and we want the
126 // placeholder text to sit in the middle of its default-height
127 // (one-line) if its explicit height is greater than or equal to its
128 // implicit height - i.e. if it has room for it. If it doesn't have
129 // room, just do what TextField does.
130 // We should also account for any topInset the user might have specified,
131 // which is useful to ensure that the text doesn't get clipped.
132 return ((m_controlImplicitBackgroundHeight - m_largestHeight) / 2.0)
133 + controlTopInset(textControl: textControl());
134 }
135
136 // When the placeholder text shouldn't float, it should sit in the middle of the TextField.
137 return (m_controlHeight - height()) / 2.0;
138}
139
140qreal QQuickMaterialPlaceholderText::floatingTargetY() const
141{
142 // For filled text fields, the placeholder text sits just above
143 // the text when floating.
144 if (m_filled)
145 return m_verticalPadding;
146
147 // Outlined text fields have the placeaholder vertically centered
148 // along the outline at the top.
149 return (-m_largestHeight / 2.0) + controlTopInset(textControl: textControl());
150}
151
152/*!
153 \internal
154
155 The height of the text at its largest size that we set.
156*/
157int QQuickMaterialPlaceholderText::largestHeight() const
158{
159 return m_largestHeight;
160}
161
162qreal QQuickMaterialPlaceholderText::controlImplicitBackgroundHeight() const
163{
164 return m_controlImplicitBackgroundHeight;
165}
166
167void QQuickMaterialPlaceholderText::setControlImplicitBackgroundHeight(qreal controlImplicitBackgroundHeight)
168{
169 if (qFuzzyCompare(p1: m_controlImplicitBackgroundHeight, p2: controlImplicitBackgroundHeight))
170 return;
171
172 m_controlImplicitBackgroundHeight = controlImplicitBackgroundHeight;
173 updateY();
174 emit controlImplicitBackgroundHeightChanged();
175}
176
177/*!
178 \internal
179
180 Exists so that we can call updateY when the control's height changes,
181 which is necessary for some y position calculations.
182
183 We don't really need it for the actual calculations, since we already
184 have access to the control, from which the property comes, but
185 it's simpler just to use it.
186*/
187qreal QQuickMaterialPlaceholderText::controlHeight() const
188{
189 return m_controlHeight;
190}
191
192void QQuickMaterialPlaceholderText::setControlHeight(qreal controlHeight)
193{
194 if (qFuzzyCompare(p1: m_controlHeight, p2: controlHeight))
195 return;
196
197 m_controlHeight = controlHeight;
198 updateY();
199}
200
201qreal QQuickMaterialPlaceholderText::verticalPadding() const
202{
203 return m_verticalPadding;
204}
205
206void QQuickMaterialPlaceholderText::setVerticalPadding(qreal verticalPadding)
207{
208 if (qFuzzyCompare(p1: m_verticalPadding, p2: verticalPadding))
209 return;
210
211 m_verticalPadding = verticalPadding;
212 emit verticalPaddingChanged();
213}
214
215void QQuickMaterialPlaceholderText::controlGotActiveFocus()
216{
217 if (m_focusOutAnimation)
218 m_focusOutAnimation->stop();
219
220 Q_ASSERT(!m_focusInAnimation);
221 if (shouldAnimate()) {
222 m_focusInAnimation = new QParallelAnimationGroup(this);
223
224 QPropertyAnimation *yAnimation = new QPropertyAnimation(this, "y", this);
225 yAnimation->setDuration(300);
226 yAnimation->setStartValue(y());
227 yAnimation->setEndValue(floatingTargetY());
228 yAnimation->setEasingCurve(*animationEasingCurve);
229 m_focusInAnimation->addAnimation(animation: yAnimation);
230
231 auto *scaleAnimation = new QPropertyAnimation(this, "scale", this);
232 scaleAnimation->setDuration(300);
233 scaleAnimation->setStartValue(1);
234 scaleAnimation->setEndValue(floatingScale);
235 yAnimation->setEasingCurve(*animationEasingCurve);
236 m_focusInAnimation->addAnimation(animation: scaleAnimation);
237
238 m_focusInAnimation->start(policy: QAbstractAnimation::DeleteWhenStopped);
239 } else {
240 updateY();
241 }
242}
243
244void QQuickMaterialPlaceholderText::controlLostActiveFocus()
245{
246 Q_ASSERT(!m_focusOutAnimation);
247 if (shouldAnimate()) {
248 m_focusOutAnimation = new QParallelAnimationGroup(this);
249
250 auto *yAnimation = new QPropertyAnimation(this, "y", this);
251 yAnimation->setDuration(300);
252 yAnimation->setStartValue(y());
253 yAnimation->setEndValue(normalTargetY());
254 yAnimation->setEasingCurve(*animationEasingCurve);
255 m_focusOutAnimation->addAnimation(animation: yAnimation);
256
257 auto *scaleAnimation = new QPropertyAnimation(this, "scale", this);
258 scaleAnimation->setDuration(300);
259 scaleAnimation->setStartValue(floatingScale);
260 scaleAnimation->setEndValue(1);
261 yAnimation->setEasingCurve(*animationEasingCurve);
262 m_focusOutAnimation->addAnimation(animation: scaleAnimation);
263
264 m_focusOutAnimation->start(policy: QAbstractAnimation::DeleteWhenStopped);
265 } else {
266 updateY();
267 }
268}
269
270void QQuickMaterialPlaceholderText::maybeSetFocusAnimationProgress()
271{
272 updateY();
273 setScale(shouldFloat() ? floatingScale : 1.0);
274}
275
276void QQuickMaterialPlaceholderText::componentComplete()
277{
278 // We deliberately do not call QQuickPlaceholderText's implementation here,
279 // as Material 3 placeholder text should always be left-aligned.
280 QQuickText::componentComplete();
281
282 if (!parentItem())
283 qmlWarning(me: this) << "Expected parent item by component completion!";
284
285 m_largestHeight = implicitHeight();
286 if (m_largestHeight > 0) {
287 emit largestHeightChanged();
288 } else {
289 qmlWarning(me: this) << "Expected implicitHeight of placeholder text" << text()
290 << "to be greater than 0 by component completion!";
291 }
292
293 maybeSetFocusAnimationProgress();
294}
295
296QT_END_NAMESPACE
297

source code of qtdeclarative/src/quickcontrols/material/impl/qquickmaterialplaceholdertext.cpp