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 "qquickmaterialtextcontainer_p.h"
5
6#include <QtCore/qpropertyanimation.h>
7#include <QtGui/qpainter.h>
8#include <QtGui/qpainterpath.h>
9#include <QtQml/qqmlinfo.h>
10
11QT_BEGIN_NAMESPACE
12
13/*
14 This class exists because:
15
16 - Rectangle doesn't support individual radii for each corner (QTBUG-48774).
17 - We need to draw an interrupted (where the placeholder text is) line for outlined containers.
18 - We need to animate the focus line for filled containers, and we can't use "Behavior on"
19 syntax because we only want to animate activeFocus becoming true, not also false. To do this
20 requires imperative code, and we want to keep the QML declarative.
21
22 focusAnimationProgress has to be a property even though it's only used internally,
23 because we have to use QPropertyAnimation on it.
24
25 An advantage of doing the animation in C++ is that we avoid the memory
26 overhead of an animation instance even when we're not using it, and instead
27 create it on demand and delete it when it's done. I tried doing the animation
28 declaratively with states and transitions, but it was more difficult to implement
29 and would have been harder to maintain, as well as having more overhead.
30*/
31
32QQuickMaterialTextContainer::QQuickMaterialTextContainer(QQuickItem *parent)
33 : QQuickPaintedItem(parent)
34{
35}
36
37bool QQuickMaterialTextContainer::isFilled() const
38{
39 return m_filled;
40}
41
42void QQuickMaterialTextContainer::setFilled(bool filled)
43{
44 if (filled == m_filled)
45 return;
46
47 m_filled = filled;
48 update();
49}
50
51QColor QQuickMaterialTextContainer::fillColor() const
52{
53 return m_fillColor;
54}
55
56void QQuickMaterialTextContainer::setFillColor(const QColor &fillColor)
57{
58 if (fillColor == m_fillColor)
59 return;
60
61 m_fillColor = fillColor;
62 update();
63}
64
65QColor QQuickMaterialTextContainer::outlineColor() const
66{
67 return m_outlineColor;
68}
69
70void QQuickMaterialTextContainer::setOutlineColor(const QColor &outlineColor)
71{
72 if (outlineColor == m_outlineColor)
73 return;
74
75 m_outlineColor = outlineColor;
76 update();
77}
78
79QColor QQuickMaterialTextContainer::focusedOutlineColor() const
80{
81 return m_outlineColor;
82}
83
84void QQuickMaterialTextContainer::setFocusedOutlineColor(const QColor &focusedOutlineColor)
85{
86 if (focusedOutlineColor == m_focusedOutlineColor)
87 return;
88
89 m_focusedOutlineColor = focusedOutlineColor;
90 update();
91}
92
93qreal QQuickMaterialTextContainer::focusAnimationProgress() const
94{
95 return m_focusAnimationProgress;
96}
97
98void QQuickMaterialTextContainer::setFocusAnimationProgress(qreal progress)
99{
100 if (qFuzzyCompare(p1: progress, p2: m_focusAnimationProgress))
101 return;
102
103 m_focusAnimationProgress = progress;
104 update();
105}
106
107qreal QQuickMaterialTextContainer::placeholderTextWidth() const
108{
109 return m_placeholderTextWidth;
110}
111
112void QQuickMaterialTextContainer::setPlaceholderTextWidth(qreal placeholderTextWidth)
113{
114 if (qFuzzyCompare(p1: placeholderTextWidth, p2: m_placeholderTextWidth))
115 return;
116
117 m_placeholderTextWidth = placeholderTextWidth;
118 update();
119}
120
121QQuickMaterialTextContainer::PlaceHolderHAlignment QQuickMaterialTextContainer::placeholderTextHAlign() const
122{
123 return m_placeholderTextHAlign;
124}
125
126void QQuickMaterialTextContainer::setPlaceholderTextHAlign(PlaceHolderHAlignment placeholderTextHAlign)
127{
128 if (m_placeholderTextHAlign == placeholderTextHAlign)
129 return;
130
131 m_placeholderTextHAlign = placeholderTextHAlign;
132 update();
133}
134
135bool QQuickMaterialTextContainer::controlHasActiveFocus() const
136{
137 return m_controlHasActiveFocus;
138}
139
140void QQuickMaterialTextContainer::setControlHasActiveFocus(bool controlHasActiveFocus)
141{
142 if (m_controlHasActiveFocus == controlHasActiveFocus)
143 return;
144
145 m_controlHasActiveFocus = controlHasActiveFocus;
146 if (m_controlHasActiveFocus)
147 controlGotActiveFocus();
148 else
149 controlLostActiveFocus();
150 emit controlHasActiveFocusChanged();
151}
152
153bool QQuickMaterialTextContainer::controlHasText() const
154{
155 return m_controlHasText;
156}
157
158void QQuickMaterialTextContainer::setControlHasText(bool controlHasText)
159{
160 if (m_controlHasText == controlHasText)
161 return;
162
163 m_controlHasText = controlHasText;
164 // TextArea's text length is updated after component completion,
165 // so account for that here and in setPlaceholderHasText().
166 maybeSetFocusAnimationProgress();
167 update();
168 emit controlHasTextChanged();
169}
170
171bool QQuickMaterialTextContainer::placeholderHasText() const
172{
173 return m_placeholderHasText;
174}
175
176void QQuickMaterialTextContainer::setPlaceholderHasText(bool placeholderHasText)
177{
178 if (m_placeholderHasText == placeholderHasText)
179 return;
180
181 m_placeholderHasText = placeholderHasText;
182 maybeSetFocusAnimationProgress();
183 update();
184 emit placeholderHasTextChanged();
185}
186
187int QQuickMaterialTextContainer::horizontalPadding() const
188{
189 return m_horizontalPadding;
190}
191
192/*!
193 \internal
194
195 The text field's horizontal padding.
196
197 We need this to be a property so that the QML can set it, since we can't
198 access QQuickMaterialStyle's C++ API from this plugin.
199*/
200void QQuickMaterialTextContainer::setHorizontalPadding(int horizontalPadding)
201{
202 if (m_horizontalPadding == horizontalPadding)
203 return;
204 m_horizontalPadding = horizontalPadding;
205 update();
206 emit horizontalPaddingChanged();
207}
208
209void QQuickMaterialTextContainer::paint(QPainter *painter)
210{
211 qreal w = width();
212 qreal h = height();
213 if (w <= 0 || h <= 0)
214 return;
215
216 // Account for pen width.
217 const qreal penWidth = m_filled ? 1 : (m_controlHasActiveFocus ? 2 : 1);
218 w -= penWidth;
219 h -= penWidth;
220
221 const qreal cornerRadius = 4;
222 // This is coincidentally the same as cornerRadius, but use different variable names
223 // to keep the code understandable.
224 const qreal gapPadding = 4;
225 // When animating focus on outlined containers, we need to make a gap
226 // at the top left for the placeholder text.
227 // If the text is too wide for the container, it will be elided, so
228 // we shouldn't need to clamp its width here. TODO: check that this is the case for TextArea.
229 const qreal halfPlaceholderWidth = m_placeholderTextWidth / 2;
230 // Take care of different Alignment cases for the placeholder text.
231 qreal gapCenterX;
232 switch (m_placeholderTextHAlign) {
233 case PlaceHolderHAlignment::AlignHCenter:
234 gapCenterX = width() / 2;
235 break;
236 case PlaceHolderHAlignment::AlignRight:
237 gapCenterX = width() - halfPlaceholderWidth - m_horizontalPadding;
238 break;
239 default:
240 gapCenterX = m_horizontalPadding + halfPlaceholderWidth;
241 break;
242 }
243
244 QPainterPath path;
245
246 QPointF startPos;
247
248 // Top-left rounded corner.
249 if (m_filled || m_focusAnimationProgress == 0) {
250 startPos = QPointF(cornerRadius, 0);
251 } else {
252 // Start at the center of the gap and animate outwards towards the left-hand side.
253 // Subtract gapPadding to account for the gap between the line and the placeholder text.
254 // Also subtract the pen width because otherwise it extends by that distance too much to the right.
255 // Changing the cap style to Qt::FlatCap would only fix this by half the pen width,
256 // but it has no effect anyway (perhaps it literally only affects end points and not "start" points?).
257 startPos = QPointF(gapCenterX - (m_focusAnimationProgress * halfPlaceholderWidth) - gapPadding - penWidth, 0);
258 }
259 path.moveTo(p: startPos);
260 path.arcTo(x: 0, y: 0, w: cornerRadius * 2, h: cornerRadius * 2, startAngle: 90, arcLength: 90);
261
262 // Bottom-left corner.
263 if (m_filled) {
264 path.lineTo(x: 0, y: h);
265 } else {
266 path.lineTo(x: 0, y: h - cornerRadius * 2);
267 path.arcTo(x: 0, y: h - cornerRadius * 2, w: cornerRadius * 2, h: cornerRadius * 2, startAngle: 180, arcLength: 90);
268 }
269
270 // Bottom-right corner.
271 if (m_filled) {
272 path.lineTo(x: w, y: h);
273 } else {
274 path.lineTo(x: w - cornerRadius * 2, y: h);
275 path.arcTo(x: w - cornerRadius * 2, y: h - cornerRadius * 2, w: cornerRadius * 2, h: cornerRadius * 2, startAngle: 270, arcLength: 90);
276 }
277
278 // Top-right rounded corner.
279 path.lineTo(x: w, y: cornerRadius);
280 path.arcTo(x: w - (cornerRadius * 2), y: 0, w: cornerRadius * 2, h: cornerRadius * 2, startAngle: 0, arcLength: 90);
281
282 if (m_filled || qFuzzyIsNull(d: m_focusAnimationProgress)) {
283 // Back to the start.
284 path.lineTo(x: startPos.x(), y: startPos.y());
285 } else {
286 path.lineTo(x: gapCenterX + (m_focusAnimationProgress * halfPlaceholderWidth) + gapPadding, y: startPos.y());
287 }
288
289 // Account for pen width.
290 painter->translate(dx: penWidth / 2, dy: penWidth / 2);
291
292 painter->setRenderHint(hint: QPainter::Antialiasing, on: true);
293
294 auto control = textControl();
295 const bool focused = control && control->hasActiveFocus();
296 // We still want to draw the stroke when it's filled, otherwise it will be a pixel
297 // (the pen width) too narrow on either side.
298 QPen pen;
299 pen.setColor(m_filled ? m_fillColor : (focused ? m_focusedOutlineColor : m_outlineColor));
300 pen.setWidthF(penWidth);
301 painter->setPen(pen);
302 if (m_filled)
303 painter->setBrush(QBrush(m_fillColor));
304
305 // Fill or stroke the container's shape.
306 // If not filling, the default brush will be used, which is Qt::NoBrush.
307 painter->drawPath(path);
308
309 // Draw the focus line at the bottom for filled containers.
310 if (m_filled) {
311 if (!qFuzzyCompare(p1: m_focusAnimationProgress, p2: 1.0)) {
312 // Draw the enabled active indicator line (#10) that's at the bottom when it's not focused:
313 // https://m3.material.io/components/text-fields/specs#6d654d1d-262e-4697-858c-9a75e8e7c81d
314 // Don't bother drawing it when the animation has finished, as the focused active indicator
315 // line below will obscure it.
316 pen.setColor(m_outlineColor);
317 painter->setPen(pen);
318 painter->drawLine(x1: 0, y1: h, x2: w, y2: h);
319 }
320
321 if (!qFuzzyIsNull(d: m_focusAnimationProgress)) {
322 // Draw the focused active indicator line (#6) that's at the bottom when it's focused.
323 // Start at the center and expand outwards.
324 const int lineLength = m_focusAnimationProgress * w;
325 const int horizontalCenter = w / 2;
326 pen.setColor(m_focusedOutlineColor);
327 pen.setWidth(2);
328 painter->setPen(pen);
329 painter->drawLine(x1: horizontalCenter - (lineLength / 2), y1: h,
330 x2: horizontalCenter + (lineLength / 2) + pen.width() / 2, y2: h);
331 }
332 }
333}
334
335bool QQuickMaterialTextContainer::shouldAnimateOutline() const
336{
337 return !m_controlHasText && m_placeholderHasText;
338}
339
340/*!
341 \internal
342
343 \sa QQuickPlaceholderText::textControl().
344*/
345QQuickItem *QQuickMaterialTextContainer::textControl() const
346{
347 return qobject_cast<QQuickItem *>(o: parent());
348}
349
350void QQuickMaterialTextContainer::controlGotActiveFocus()
351{
352 const bool shouldAnimate = m_filled ? !m_controlHasText : shouldAnimateOutline();
353 if (!shouldAnimate) {
354 // It does have focus, but sometimes we don't need to animate anything, just change colors.
355 if (m_filled && m_controlHasText) {
356 // When a filled container has text already entered, we should just immediately change
357 // the color and thickness of the indicator line.
358 m_focusAnimationProgress = 1;
359 }
360 update();
361 return;
362 }
363
364 startFocusAnimation();
365}
366
367void QQuickMaterialTextContainer::controlLostActiveFocus()
368{
369 // We don't want to animate the active indicator line (at the bottom) of filled containers
370 // when the control loses focus, only when it gets it.
371 if (m_filled || !shouldAnimateOutline()) {
372 // Ensure that we set this so that filled containers go back to a non-accent-colored
373 // active indicator line when losing focus.
374 if (m_filled)
375 m_focusAnimationProgress = 0;
376 update();
377 return;
378 }
379
380 QPropertyAnimation *animation = new QPropertyAnimation(this, "focusAnimationProgress", this);
381 animation->setDuration(300);
382 animation->setStartValue(1);
383 animation->setEndValue(0);
384 animation->start(policy: QAbstractAnimation::DeleteWhenStopped);
385}
386
387void QQuickMaterialTextContainer::startFocusAnimation()
388{
389 // Each time setFocusAnimationProgress is called by the animation, it'll call update(),
390 // which will cause us to be re-rendered.
391 QPropertyAnimation *animation = new QPropertyAnimation(this, "focusAnimationProgress", this);
392 animation->setDuration(300);
393 animation->setStartValue(0);
394 animation->setEndValue(1);
395 animation->start(policy: QAbstractAnimation::DeleteWhenStopped);
396}
397
398void QQuickMaterialTextContainer::maybeSetFocusAnimationProgress()
399{
400 if (m_filled)
401 return;
402
403 if (m_controlHasText && m_placeholderHasText) {
404 // Show the interrupted outline when there is text.
405 setFocusAnimationProgress(1);
406 } else if (!m_controlHasText && !m_controlHasActiveFocus) {
407 // If the text was cleared while it didn't have focus, don't animate, just close the gap.
408 setFocusAnimationProgress(0);
409 }
410}
411
412void QQuickMaterialTextContainer::componentComplete()
413{
414 QQuickPaintedItem::componentComplete();
415
416 if (!parentItem())
417 qmlWarning(me: this) << "Expected parent item by component completion!";
418
419 maybeSetFocusAnimationProgress();
420}
421
422QT_END_NAMESPACE
423

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

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