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

source code of qtdeclarative/src/quickcontrolsimpl/qquickninepatchimage.cpp