Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qquickninepatchimage.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 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// Qt-Security score:significant reason:default
4
6
7#include <QtCore/qfileinfo.h>
8#include <QtQuick/qsggeometry.h>
9#include <QtQuick/qsgtexturematerial.h>
10#include <QtQuick/private/qsgnode_p.h>
11#include <QtQuick/private/qquickimage_p_p.h>
12
14
16{
17 QList<qreal> coordsForSize(qreal count) const;
18
19 inline bool isNull() const { return data.isEmpty(); }
20 inline int count() const { return data.size(); }
21 inline qreal at(int index) const { return data.at(index); }
22 inline qreal size() const { return data.last(); }
23
24 void fill(const QList<qreal> &coords, qreal count);
25 void clear();
26
27private:
28 bool inverted = false;
29 QList<qreal> data;
30};
31
33{
34 // n = number of stretchable sections
35 // We have to compensate when adding 0 and/or
36 // the source image width to the divs vector.
37 const int l = data.size();
38 const int n = (inverted ? l - 1 : l) / 2;
39 const qreal stretch = (size - data.last()) / n;
40
41 QList<qreal> coords;
42 coords.reserve(l);
43 coords.append(0);
44
45 bool stretched = !inverted;
46 for (int i = 1; i < l; ++i) {
47 qreal advance = data[i] - data[i - 1];
48 if (stretched)
49 advance += stretch;
50 coords.append(coords.last() + advance);
51
52 stretched = !stretched;
53 }
54
55 return coords;
56}
57
58/*
59 Adds the 0 index coordinate if appropriate, and the one at "size".
60*/
61void QQuickNinePatchData::fill(const QList<qreal> &coords, qreal size)
62{
63 data.clear();
64 inverted = coords.isEmpty() || coords.first() != 0;
65
66 // Reserve an extra item in case we need to add the image width/height
67 if (inverted) {
68 data.reserve(coords.size() + 2);
69 data.append(0);
70 } else {
71 data.reserve(coords.size() + 1);
72 }
73
74 data += coords;
75 data.append(size);
76}
77
79{
80 data.clear();
81}
82
84{
85public:
88
89 void initialize(QSGTexture *texture, const QSizeF &targetSize, const QSize &sourceSize,
90 const QQuickNinePatchData &xDivs, const QQuickNinePatchData &yDivs, qreal dpr);
91
92private:
93 QSGGeometry m_geometry;
94 QSGTextureMaterial m_material;
95};
96
99{
100 m_geometry.setDrawingMode(QSGGeometry::DrawTriangles);
101 setGeometry(&m_geometry);
102 setMaterial(&m_material);
103}
104
106{
107 delete m_material.texture();
108}
109
110void QQuickNinePatchNode::initialize(QSGTexture *texture, const QSizeF &targetSize, const QSize &sourceSize,
111 const QQuickNinePatchData &xDivs, const QQuickNinePatchData &yDivs, qreal dpr)
112{
113 delete m_material.texture();
114 m_material.setTexture(texture);
115
116 const int xlen = xDivs.count();
117 const int ylen = yDivs.count();
118
119 if (xlen > 0 && ylen > 0) {
120 const int quads = (xlen - 1) * (ylen - 1);
121 static const int verticesPerQuad = 6;
122 m_geometry.allocate(xlen * ylen, verticesPerQuad * quads);
123
124 QSGGeometry::TexturedPoint2D *vertices = m_geometry.vertexDataAsTexturedPoint2D();
125 QList<qreal> xCoords = xDivs.coordsForSize(targetSize.width());
126 QList<qreal> yCoords = yDivs.coordsForSize(targetSize.height());
127
128 for (int y = 0; y < ylen; ++y) {
129 for (int x = 0; x < xlen; ++x, ++vertices)
130 vertices->set(xCoords[x] / dpr, yCoords[y] / dpr,
131 xDivs.at(x) / sourceSize.width(),
132 yDivs.at(y) / sourceSize.height());
133 }
134
135 quint16 *indices = m_geometry.indexDataAsUShort();
136 int n = quads;
137 for (int q = 0; n--; ++q) {
138 if ((q + 1) % xlen == 0) // next row
139 ++q;
140 // Bottom-left half quad triangle
141 indices[0] = q;
142 indices[1] = q + xlen;
143 indices[2] = q + xlen + 1;
144
145 // Top-right half quad triangle
146 indices[3] = q;
147 indices[4] = q + xlen + 1;
148 indices[5] = q + 1;
149
150 indices += verticesPerQuad;
151 }
152 }
153
154 markDirty(QSGNode::DirtyGeometry | QSGNode::DirtyMaterial);
155}
156
158{
159 Q_DECLARE_PUBLIC(QQuickNinePatchImage)
160
161public:
163 void updatePaddings(const QSizeF &size, const QList<qreal> &horizontal, const QList<qreal> &vertical);
164 void updateInsets(const QList<qreal> &horizontal, const QList<qreal> &vertical);
165
166 bool resetNode = false;
175
179};
180
181/*
182 Examines each pixel in a horizontal or vertical (if offset is equal to the image's width)
183 line, storing the start and end index ("coordinate") of each 9-patch line.
184
185 For instance, in the 7x3 (9x5 actual size) 9-patch image below, which has no horizontal
186 stretchable area, it would return {}:
187
188 +-----+
189 | |
190 +-----+
191
192 If indices 3 to 5 were marked, it would return {2, 5}:
193
194 xxx
195 +-----+
196 | |
197 +-----+
198
199 If indices 3 and 5 were marked, it would store {0, 2, 3, 4, 5, 7}:
200
201 x x
202 +-----+
203 | |
204 +-----+
205*/
206static QList<qreal> readCoords(const QRgb *data, int from, int count, int offset, QRgb color)
207{
208 int p1 = -1;
209 QList<qreal> coords;
210 for (int i = 0; i < count; ++i) {
211 int p2 = from + i * offset;
212 if (data[p2] == color) {
213 // colored pixel
214 if (p1 == -1) {
215 // This is the start of a 9-patch line.
216 p1 = i;
217 }
218 } else {
219 // empty pixel
220 if (p1 != -1) {
221 // This is the end of a 9-patch line; add the start and end indices as coordinates...
222 coords << p1 << i;
223 // ... and reset p1 so that we can search for the next one.
224 p1 = -1;
225 }
226 }
227 }
228 return coords;
229}
230
231/*
232 Called whenever a 9-patch image is set as the image's source.
233
234 Reads the 9-patch lines from the source image and sets the
235 inset and padding properties accordingly.
236*/
237void QQuickNinePatchImagePrivate::updatePatches()
238{
239 if (ninePatch.isNull())
240 return;
241
242 int w = ninePatch.width();
243 int h = ninePatch.height();
244 const QRgb *data = reinterpret_cast<const QRgb *>(ninePatch.constBits());
245
246 const QRgb black = qRgb(0,0,0);
247 const QRgb red = qRgb(255,0,0);
248
249 xDivs.fill(readCoords(data, 1, w - 1, 1, black), w - 2); // top left -> top right
250 yDivs.fill(readCoords(data, w, h - 1, w, black), h - 2); // top left -> bottom left
251
252 QList<qreal> hInsets = readCoords(data, (h - 1) * w + 1, w - 1, 1, red); // bottom left -> bottom right
253 QList<qreal> vInsets = readCoords(data, 2 * w - 1, h - 1, w, red); // top right -> bottom right
254 updateInsets(hInsets, vInsets);
255
256 const QSizeF sz(w - leftInset - rightInset, h - topInset - bottomInset);
257 QList<qreal> hPaddings = readCoords(data, (h - 1) * w + leftInset + 1, sz.width() - 2, 1, black); // bottom left -> bottom right
258 QList<qreal> vPaddings = readCoords(data, (2 + topInset) * w - 1, sz.height() - 2, w, black); // top right -> bottom right
259 updatePaddings(sz, hPaddings, vPaddings);
260}
261
262void QQuickNinePatchImagePrivate::updatePaddings(const QSizeF &size, const QList<qreal> &horizontal, const QList<qreal> &vertical)
263{
264 Q_Q(QQuickNinePatchImage);
265 qreal oldTopPadding = topPadding;
266 qreal oldLeftPadding = leftPadding;
267 qreal oldRightPadding = rightPadding;
268 qreal oldBottomPadding = bottomPadding;
269
270 if (horizontal.size() >= 2) {
271 leftPadding = horizontal.first();
272 rightPadding = size.width() - horizontal.last() - 2;
273 } else {
274 leftPadding = 0;
275 rightPadding = 0;
276 }
277
278 if (vertical.size() >= 2) {
279 topPadding = vertical.first();
280 bottomPadding = size.height() - vertical.last() - 2;
281 } else {
282 topPadding = 0;
283 bottomPadding = 0;
284 }
285
286 if (!qFuzzyCompare(oldTopPadding, topPadding))
287 emit q->topPaddingChanged();
288 if (!qFuzzyCompare(oldBottomPadding, bottomPadding))
289 emit q->bottomPaddingChanged();
290 if (!qFuzzyCompare(oldLeftPadding, leftPadding))
291 emit q->leftPaddingChanged();
292 if (!qFuzzyCompare(oldRightPadding, rightPadding))
293 emit q->rightPaddingChanged();
294}
295
296void QQuickNinePatchImagePrivate::updateInsets(const QList<qreal> &horizontal, const QList<qreal> &vertical)
297{
298 Q_Q(QQuickNinePatchImage);
299 qreal oldTopInset = topInset;
300 qreal oldLeftInset = leftInset;
301 qreal oldRightInset = rightInset;
302 qreal oldBottomInset = bottomInset;
303
304 if (horizontal.size() >= 2 && horizontal.first() == 0)
305 leftInset = horizontal.at(1);
306 else
307 leftInset = 0;
308
309 if (horizontal.size() == 2 && horizontal.first() > 0)
310 rightInset = horizontal.last() - horizontal.first();
311 else if (horizontal.size() == 4)
312 rightInset = horizontal.last() - horizontal.at(2);
313 else
314 rightInset = 0;
315
316 if (vertical.size() >= 2 && vertical.first() == 0)
317 topInset = vertical.at(1);
318 else
319 topInset = 0;
320
321 if (vertical.size() == 2 && vertical.first() > 0)
322 bottomInset = vertical.last() - vertical.first();
323 else if (vertical.size() == 4)
324 bottomInset = vertical.last() - vertical.at(2);
325 else
326 bottomInset = 0;
327
328 if (!qFuzzyCompare(oldTopInset, topInset))
329 emit q->topInsetChanged();
330 if (!qFuzzyCompare(oldBottomInset, bottomInset))
331 emit q->bottomInsetChanged();
332 if (!qFuzzyCompare(oldLeftInset, leftInset))
333 emit q->leftInsetChanged();
334 if (!qFuzzyCompare(oldRightInset, rightInset))
335 emit q->rightInsetChanged();
336}
337
338QQuickNinePatchImage::QQuickNinePatchImage(QQuickItem *parent)
339 : QQuickImage(*(new QQuickNinePatchImagePrivate), parent)
340{
341 Q_D(QQuickNinePatchImage);
342 d->smooth = qEnvironmentVariableIntValue("QT_QUICK_CONTROLS_IMAGINE_SMOOTH");
343}
344
346{
347 Q_D(const QQuickNinePatchImage);
348 return d->topPadding / d->devicePixelRatio;
349}
350
352{
353 Q_D(const QQuickNinePatchImage);
354 return d->leftPadding / d->devicePixelRatio;
355}
356
358{
359 Q_D(const QQuickNinePatchImage);
360 return d->rightPadding / d->devicePixelRatio;
361}
362
364{
365 Q_D(const QQuickNinePatchImage);
366 return d->bottomPadding / d->devicePixelRatio;
367}
368
370{
371 Q_D(const QQuickNinePatchImage);
372 return d->topInset / d->devicePixelRatio;
373}
374
376{
377 Q_D(const QQuickNinePatchImage);
378 return d->leftInset / d->devicePixelRatio;
379}
380
382{
383 Q_D(const QQuickNinePatchImage);
384 return d->rightInset / d->devicePixelRatio;
385}
386
388{
389 Q_D(const QQuickNinePatchImage);
390 return d->bottomInset / d->devicePixelRatio;
391}
392
394{
395 Q_D(QQuickNinePatchImage);
396 if (QFileInfo(d->url.fileName()).completeSuffix().toLower() == QLatin1String("9.png")) {
397 // Keep resetNode if it is already set, we do not want to miss an
398 // ImageNode->NinePatchNode change. Without this there's a chance one gets
399 // an incorrect cast on oldNode every once in a while with source changes.
400 if (!d->resetNode)
401 d->resetNode = d->ninePatch.isNull();
402
403 d->ninePatch = d->currentPix->image();
404 if (d->ninePatch.depth() != 32)
405 d->ninePatch = std::move(d->ninePatch).convertToFormat(QImage::Format_ARGB32);
406
407 int w = d->ninePatch.width();
408 int h = d->ninePatch.height();
409 d->currentPix->setImage(QImage(d->ninePatch.constBits() + 4 * (w + 1), w - 2, h - 2, d->ninePatch.bytesPerLine(), d->ninePatch.format()));
410
411 d->updatePatches();
412 } else {
413 /*
414 Only change resetNode when it's false; i.e. when no reset is pending.
415 updatePaintNode() will take care of setting it to false if it's true.
416
417 Consider the following changes in source:
418
419 normal.png => press.9.png => normal.png => focus.png
420
421 If the last two events happen quickly, pixmapChange() can be called
422 twice with no call to updatePaintNode() inbetween. On the first call,
423 resetNode will be true (because ninePatch is not null since it is still
424 in the process of going from a 9-patch image to a regular image),
425 and on the second call, resetNode would be false if we didn't have this check.
426 This results in the oldNode never being deleted, and QQuickImage
427 tries to static_cast a QQuickNinePatchImage to a QSGInternalImageNode.
428 */
429 if (!d->resetNode)
430 d->resetNode = !d->ninePatch.isNull();
431 d->ninePatch = QImage();
432 }
433 QQuickImage::pixmapChange();
434}
435
436QSGNode *QQuickNinePatchImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *data)
437{
438 Q_D(QQuickNinePatchImage);
439 Q_UNUSED(data);
440
441 if (d->resetNode) {
442 delete oldNode;
443 oldNode = nullptr;
444 d->resetNode = false;
445 }
446
447 if (d->ninePatch.isNull())
448 return QQuickImage::updatePaintNode(oldNode, data);
449
450 QSizeF sz = size();
451 QImage image = d->currentPix->image();
452 if (!sz.isValid() || image.isNull()) {
453 if (d->provider)
454 d->provider->updateTexture(nullptr);
455 delete oldNode;
456 return nullptr;
457 }
458
459 QQuickNinePatchNode *patchNode = static_cast<QQuickNinePatchNode *>(oldNode);
460 if (!patchNode)
461 patchNode = new QQuickNinePatchNode;
462
463#ifdef QSG_RUNTIME_DESCRIPTION
464 qsgnode_set_description(patchNode, QString::fromLatin1("QQuickNinePatchImage: '%1'").arg(d->url.toString()));
465#endif
466
467 // The image may wrap non-owned data (due to pixmapChange). Ensure we never
468 // pass such an image to the scenegraph, because with a separate render
469 // thread the data may become invalid (in a subsequent pixmapChange on the
470 // gui thread) by the time the renderer gets to do something with the QImage
471 // passed in here.
472 image.detach();
473
474 QSGTexture *texture = window()->createTextureFromImage(image);
475 patchNode->initialize(texture, sz * d->devicePixelRatio, image.size(), d->xDivs, d->yDivs, d->devicePixelRatio);
476 auto patchNodeMaterial = static_cast<QSGTextureMaterial *>(patchNode->material());
477 patchNodeMaterial->setFiltering(d->smooth ? QSGTexture::Linear : QSGTexture::Nearest);
478 return patchNode;
479}
480
481QT_END_NAMESPACE
482
483#include "moc_qquickninepatchimage_p.cpp"
void updatePaddings(const QSizeF &size, const QList< qreal > &horizontal, const QList< qreal > &vertical)
void updateInsets(const QList< qreal > &horizontal, const QList< qreal > &vertical)
QSGNode * updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *data) override
Called on the render thread when it is time to sync the state of the item with the scene graph.
void initialize(QSGTexture *texture, const QSizeF &targetSize, const QSize &sourceSize, const QQuickNinePatchData &xDivs, const QQuickNinePatchData &yDivs, qreal dpr)
static QList< qreal > readCoords(const QRgb *data, int from, int count, int offset, QRgb color)
QList< qreal > coordsForSize(qreal count) const
void fill(const QList< qreal > &coords, qreal count)
qreal at(int index) const