1// Copyright (C) 2016 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 "qquickborderimage_p.h"
5#include "qquickborderimage_p_p.h"
6
7#include <QtQml/qqmlinfo.h>
8#include <QtQml/qqmlfile.h>
9#include <QtQml/qqmlengine.h>
10#if QT_CONFIG(qml_network)
11#include <QtNetwork/qnetworkreply.h>
12#endif
13#include <QtCore/qfile.h>
14#include <QtCore/qmath.h>
15#include <QtGui/qguiapplication.h>
16
17#include <private/qqmlglobal_p.h>
18#include <private/qsgadaptationlayer_p.h>
19
20QT_BEGIN_NAMESPACE
21
22
23/*!
24 \qmltype BorderImage
25 \nativetype QQuickBorderImage
26 \inqmlmodule QtQuick
27 \brief Paints a border based on an image.
28 \inherits Item
29 \ingroup qtquick-visual
30
31 The BorderImage type is used to create borders out of images by scaling or tiling
32 parts of each image.
33
34 A BorderImage breaks a source image, specified using the \l source property,
35 into 9 regions, as shown below:
36
37 \image declarative-scalegrid.png
38
39 When the image is scaled, regions of the source image are scaled or tiled to
40 create the displayed border image in the following way:
41
42 \list
43 \li The corners (regions 1, 3, 7, and 9) are not scaled at all.
44 \li Regions 2 and 8 are scaled according to
45 \l{BorderImage::horizontalTileMode}{horizontalTileMode}.
46 \li Regions 4 and 6 are scaled according to
47 \l{BorderImage::verticalTileMode}{verticalTileMode}.
48 \li The middle (region 5) is scaled according to both
49 \l{BorderImage::horizontalTileMode}{horizontalTileMode} and
50 \l{BorderImage::verticalTileMode}{verticalTileMode}.
51 \endlist
52
53 The regions of the image are defined using the \l border property group, which
54 describes the distance from each edge of the source image to use as a border.
55
56 \section1 Example Usage
57
58 The following examples show the effects of the different modes on an image.
59 Guide lines are overlaid onto the image to show the different regions of the
60 image as described above.
61
62 \beginfloatleft
63 \image qml-borderimage-normal-image.png
64 \endfloat
65
66 For comparison, an unscaled image is displayed using a simple Image item.
67 Here we have overlaid lines to show how we'd like to break it up with BorderImage:
68
69 \snippet qml/borderimage/normal-image.qml normal image
70
71 \clearfloat
72 \beginfloatleft
73 \image qml-borderimage-scaled.png
74 \endfloat
75
76 But when a BorderImage is used to display the image, the \l border property is
77 used to determine the parts of the image that will lie inside the unscaled corner
78 areas, and the parts that will be stretched horizontally and vertically.
79 Then, you can give it a size that is
80 larger than the original image. Since the \l horizontalTileMode property is set to
81 \l{BorderImage::horizontalTileMode}{BorderImage.Stretch}, the parts of image in
82 regions 2 and 8 are stretched horizontally. Since the \l verticalTileMode property
83 is set to \l{BorderImage::verticalTileMode}{BorderImage.Stretch}, the parts of image
84 in regions 4 and 6 are stretched vertically:
85
86 \snippet qml/borderimage/borderimage-scaled.qml scaled border image
87
88 \clearfloat
89 \beginfloatleft
90 \image qml-borderimage-tiled.png
91 \endfloat
92
93 Again, a large BorderImage is used to display the image. With the
94 \l horizontalTileMode property set to \l{BorderImage::horizontalTileMode}{BorderImage.Repeat},
95 the parts of image in regions 2 and 8 are tiled so that they fill the space at the
96 top and bottom of the item. Similarly, the \l verticalTileMode property is set to
97 \l{BorderImage::verticalTileMode}{BorderImage.Repeat}, so the parts of image in regions
98 4 and 6 are tiled to fill the space at the left and right of the item:
99
100 \snippet qml/borderimage/borderimage-tiled.qml tiled border image
101
102 \clearfloat
103 \beginfloatleft
104 \image qml-borderimage-rounded.png
105 \endfloat
106
107 In some situations, the width of regions 2 and 8 may not be an exact multiple of the width
108 of the corresponding regions in the source image. Similarly, the height of regions 4 and 6
109 may not be an exact multiple of the height of the corresponding regions. If you use
110 \l{BorderImage::horizontalTileMode}{BorderImage.Round} mode, it will choose an integer
111 number of tiles and shrink them to fit:
112
113 \snippet qml/borderimage/borderimage-rounded.qml tiled border image
114
115 \clearfloat
116
117 The Border Image example in \l{Qt Quick Examples - Image Elements} shows how a BorderImage
118 can be used to simulate a shadow effect on a rectangular item.
119
120 \section1 Image Loading
121
122 The source image may not be loaded instantaneously, depending on its original location.
123 Loading progress can be monitored with the \l progress property.
124
125 \sa Image, AnimatedImage
126 */
127
128/*!
129 \qmlproperty bool QtQuick::BorderImage::asynchronous
130
131 Specifies that images on the local filesystem should be loaded
132 asynchronously in a separate thread. The default value is
133 false, causing the user interface thread to block while the
134 image is loaded. Setting \a asynchronous to true is useful where
135 maintaining a responsive user interface is more desirable
136 than having images immediately visible.
137
138 Note that this property is only valid for images read from the
139 local filesystem. Images loaded via a network resource (e.g. HTTP)
140 are always loaded asynchronously.
141*/
142QQuickBorderImage::QQuickBorderImage(QQuickItem *parent)
143: QQuickImageBase(*(new QQuickBorderImagePrivate), parent)
144{
145 connect(sender: this, signal: &QQuickImageBase::sourceSizeChanged, context: this, slot: &QQuickBorderImage::sourceSizeChanged);
146}
147
148QQuickBorderImage::~QQuickBorderImage()
149{
150#if QT_CONFIG(qml_network)
151 Q_D(QQuickBorderImage);
152 if (d->sciReply)
153 d->sciReply->deleteLater();
154#endif
155}
156
157/*!
158 \qmlproperty enumeration QtQuick::BorderImage::status
159
160 This property describes the status of image loading. It can be one of:
161
162 \value BorderImage.Null No image has been set
163 \value BorderImage.Ready The image has been loaded
164 \value BorderImage.Loading The image is currently being loaded
165 \value BorderImage.Error An error occurred while loading the image
166
167 \sa progress
168*/
169
170/*!
171 \qmlproperty real QtQuick::BorderImage::progress
172
173 This property holds the progress of image loading, from 0.0 (nothing loaded)
174 to 1.0 (finished).
175
176 \sa status
177*/
178
179/*!
180 \qmlproperty bool QtQuick::BorderImage::smooth
181
182 This property holds whether the image is smoothly filtered when scaled or
183 transformed. Smooth filtering gives better visual quality, but it may be slower
184 on some hardware. If the image is displayed at its natural size, this property
185 has no visual or performance effect.
186
187 By default, this property is set to true.
188*/
189
190/*!
191 \qmlproperty bool QtQuick::BorderImage::cache
192
193 Specifies whether the image should be cached. The default value is
194 true. Setting \a cache to false is useful when dealing with large images,
195 to make sure that they aren't cached at the expense of small 'ui element' images.
196*/
197
198/*!
199 \qmlproperty bool QtQuick::BorderImage::mirror
200
201 This property holds whether the image should be horizontally inverted
202 (effectively displaying a mirrored image).
203
204 The default value is false.
205*/
206
207/*!
208 \qmlproperty url QtQuick::BorderImage::source
209
210 This property holds the URL that refers to the source image.
211
212 BorderImage can handle any image format supported by Qt, loaded from any
213 URL scheme supported by Qt.
214
215 This property can also be used to refer to .sci files, which are
216 written in a QML-specific, text-based format that specifies the
217 borders, the image file and the tile rules for a given border image.
218
219 The following .sci file sets the borders to 10 on each side for the
220 image \c picture.png:
221
222 \code
223 border.left: 10
224 border.top: 10
225 border.bottom: 10
226 border.right: 10
227 source: "picture.png"
228 \endcode
229
230 The URL may be absolute, or relative to the URL of the component.
231
232 \sa QQuickImageProvider
233*/
234
235/*!
236 \qmlproperty QSize QtQuick::BorderImage::sourceSize
237
238 This property holds the actual width and height of the loaded image.
239
240 In BorderImage, this property is read-only.
241
242 \sa Image::sourceSize
243*/
244void QQuickBorderImage::setSource(const QUrl &url)
245{
246 Q_D(QQuickBorderImage);
247
248 if (url == d->url)
249 return;
250
251#if QT_CONFIG(qml_network)
252 if (d->sciReply) {
253 d->sciReply->deleteLater();
254 d->sciReply = nullptr;
255 }
256#endif
257
258 d->url = url;
259 d->sciurl = QUrl();
260 emit sourceChanged(d->url);
261
262 if (isComponentComplete())
263 load();
264}
265
266void QQuickBorderImage::load()
267{
268 Q_D(QQuickBorderImage);
269
270 if (d->url.isEmpty()) {
271 loadEmptyUrl();
272 } else {
273 if (d->url.path().endsWith(s: QLatin1String("sci"))) {
274 const QQmlContext *context = qmlContext(this);
275 QString lf = QQmlFile::urlToLocalFileOrQrc(context ? context->resolvedUrl(d->url)
276 : d->url);
277 if (!lf.isEmpty()) {
278 QFile file(lf);
279 if (!file.open(flags: QIODevice::ReadOnly))
280 d->setStatus(Error);
281 else
282 setGridScaledImage(QQuickGridScaledImage(&file));
283 } else {
284#if QT_CONFIG(qml_network)
285 d->setProgress(0);
286 d->setStatus(Loading);
287
288 QNetworkRequest req(d->url);
289 d->sciReply = qmlEngine(this)->networkAccessManager()->get(request: req);
290 qmlobject_connect(d->sciReply, QNetworkReply, SIGNAL(finished()),
291 this, QQuickBorderImage, SLOT(sciRequestFinished()));
292#endif
293 }
294 } else {
295 loadPixmap(url: d->url, loadOptions: LoadPixmapOptions(HandleDPR | UseProviderOptions));
296 }
297 }
298}
299
300/*!
301 \qmlpropertygroup QtQuick::BorderImage::border
302 \qmlproperty int QtQuick::BorderImage::border.left
303 \qmlproperty int QtQuick::BorderImage::border.right
304 \qmlproperty int QtQuick::BorderImage::border.top
305 \qmlproperty int QtQuick::BorderImage::border.bottom
306
307 The 4 border lines (2 horizontal and 2 vertical) break the image into 9 sections,
308 as shown below:
309
310 \image declarative-scalegrid.png
311
312 Each border line (left, right, top, and bottom) specifies an offset in pixels
313 from the respective edge of the source image. By default, each border line has
314 a value of 0.
315
316 For example, the following definition sets the bottom line 10 pixels up from
317 the bottom of the image:
318
319 \qml
320 BorderImage {
321 border.bottom: 10
322 // ...
323 }
324 \endqml
325
326 The border lines can also be specified using a
327 \l {BorderImage::source}{.sci file}.
328*/
329
330QQuickScaleGrid *QQuickBorderImage::border()
331{
332 Q_D(QQuickBorderImage);
333 return d->getScaleGrid();
334}
335
336/*!
337 \qmlproperty enumeration QtQuick::BorderImage::horizontalTileMode
338 \qmlproperty enumeration QtQuick::BorderImage::verticalTileMode
339
340 This property describes how to repeat or stretch the middle parts of the border image.
341
342 \value BorderImage.Stretch Scales the image to fit to the available area.
343 \value BorderImage.Repeat Tile the image until there is no more space. May crop the last image.
344 \value BorderImage.Round Like Repeat, but scales the images down to ensure that the last image is not cropped.
345
346 The default tile mode for each property is BorderImage.Stretch.
347*/
348QQuickBorderImage::TileMode QQuickBorderImage::horizontalTileMode() const
349{
350 Q_D(const QQuickBorderImage);
351 return d->horizontalTileMode;
352}
353
354void QQuickBorderImage::setHorizontalTileMode(TileMode t)
355{
356 Q_D(QQuickBorderImage);
357 if (t != d->horizontalTileMode) {
358 d->horizontalTileMode = t;
359 emit horizontalTileModeChanged();
360 update();
361 }
362}
363
364QQuickBorderImage::TileMode QQuickBorderImage::verticalTileMode() const
365{
366 Q_D(const QQuickBorderImage);
367 return d->verticalTileMode;
368}
369
370void QQuickBorderImage::setVerticalTileMode(TileMode t)
371{
372 Q_D(QQuickBorderImage);
373 if (t != d->verticalTileMode) {
374 d->verticalTileMode = t;
375 emit verticalTileModeChanged();
376 update();
377 }
378}
379
380void QQuickBorderImage::setGridScaledImage(const QQuickGridScaledImage& sci)
381{
382 Q_D(QQuickBorderImage);
383 if (!sci.isValid()) {
384 d->setStatus(Error);
385 } else {
386 QQuickScaleGrid *sg = border();
387 sg->setTop(sci.gridTop());
388 sg->setBottom(sci.gridBottom());
389 sg->setLeft(sci.gridLeft());
390 sg->setRight(sci.gridRight());
391 d->horizontalTileMode = sci.horizontalTileRule();
392 d->verticalTileMode = sci.verticalTileRule();
393
394 d->sciurl = d->url.resolved(relative: QUrl(sci.pixmapUrl()));
395 loadPixmap(url: d->sciurl);
396 }
397}
398
399void QQuickBorderImage::requestFinished()
400{
401 Q_D(QQuickBorderImage);
402
403 if (d->pendingPix != d->currentPix) {
404 std::swap(a&: d->pendingPix, b&: d->currentPix);
405 d->pendingPix->clear(this); // Clear the old image
406 }
407
408 const QSize impsize = d->currentPix->implicitSize();
409 setImplicitSize(impsize.width() / d->devicePixelRatio, impsize.height() / d->devicePixelRatio);
410
411 if (d->currentPix->isError()) {
412 qmlWarning(me: this) << d->currentPix->error();
413 d->setStatus(Error);
414 d->setProgress(0);
415 } else {
416 d->setStatus(Ready);
417 d->setProgress(1);
418 }
419
420 if (sourceSize() != d->oldSourceSize) {
421 d->oldSourceSize = sourceSize();
422 emit sourceSizeChanged();
423 }
424 if (d->frameCount != d->currentPix->frameCount()) {
425 d->frameCount = d->currentPix->frameCount();
426 emit frameCountChanged();
427 }
428
429 pixmapChange();
430}
431
432#if QT_CONFIG(qml_network)
433void QQuickBorderImage::sciRequestFinished()
434{
435 Q_D(QQuickBorderImage);
436
437 if (d->sciReply->error() != QNetworkReply::NoError) {
438 d->setStatus(Error);
439 d->sciReply->deleteLater();
440 d->sciReply = nullptr;
441 } else {
442 QQuickGridScaledImage sci(d->sciReply);
443 d->sciReply->deleteLater();
444 d->sciReply = nullptr;
445 setGridScaledImage(sci);
446 }
447}
448#endif // qml_network
449
450void QQuickBorderImage::doUpdate()
451{
452 update();
453}
454
455void QQuickBorderImagePrivate::calculateRects(const QQuickScaleGrid *border,
456 const QSize &sourceSize,
457 const QSizeF &targetSize,
458 int horizontalTileMode,
459 int verticalTileMode,
460 qreal devicePixelRatio,
461 QRectF *targetRect,
462 QRectF *innerTargetRect,
463 QRectF *innerSourceRect,
464 QRectF *subSourceRect)
465{
466 *innerSourceRect = QRectF(0, 0, 1, 1);
467 *targetRect = QRectF(0, 0, targetSize.width(), targetSize.height());
468 *innerTargetRect = *targetRect;
469
470 if (border) {
471 qreal borderLeft = border->left() * devicePixelRatio;
472 qreal borderRight = border->right() * devicePixelRatio;
473 qreal borderTop = border->top() * devicePixelRatio;
474 qreal borderBottom = border->bottom() * devicePixelRatio;
475 if (borderLeft + borderRight > sourceSize.width() && borderLeft < sourceSize.width())
476 borderRight = sourceSize.width() - borderLeft;
477 if (borderTop + borderBottom > sourceSize.height() && borderTop < sourceSize.height())
478 borderBottom = sourceSize.height() - borderTop;
479 *innerSourceRect = QRectF(QPointF(borderLeft / qreal(sourceSize.width()),
480 borderTop / qreal(sourceSize.height())),
481 QPointF((sourceSize.width() - borderRight) / qreal(sourceSize.width()),
482 (sourceSize.height() - borderBottom) / qreal(sourceSize.height()))),
483 *innerTargetRect = QRectF(border->left(),
484 border->top(),
485 qMax<qreal>(a: 0, b: targetSize.width() - (border->right() + border->left())),
486 qMax<qreal>(a: 0, b: targetSize.height() - (border->bottom() + border->top())));
487 }
488
489 qreal hTiles = 1;
490 qreal vTiles = 1;
491 const QSizeF innerTargetSize = innerTargetRect->size() * devicePixelRatio;
492 if (innerSourceRect->width() <= 0)
493 hTiles = 0;
494 else if (horizontalTileMode != QQuickBorderImage::Stretch) {
495 hTiles = innerTargetSize.width() / qreal(innerSourceRect->width() * sourceSize.width());
496 if (horizontalTileMode == QQuickBorderImage::Round)
497 hTiles = qCeil(v: hTiles);
498 }
499 if (innerSourceRect->height() <= 0)
500 vTiles = 0;
501 else if (verticalTileMode != QQuickBorderImage::Stretch) {
502 vTiles = innerTargetSize.height() / qreal(innerSourceRect->height() * sourceSize.height());
503 if (verticalTileMode == QQuickBorderImage::Round)
504 vTiles = qCeil(v: vTiles);
505 }
506
507 *subSourceRect = QRectF(0, 0, hTiles, vTiles);
508}
509
510
511QSGNode *QQuickBorderImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
512{
513 Q_D(QQuickBorderImage);
514
515 QSGTexture *texture = d->sceneGraphRenderContext()->textureForFactory(factory: d->currentPix->textureFactory(), window: window());
516
517 if (!texture || width() <= 0 || height() <= 0) {
518 delete oldNode;
519 return nullptr;
520 }
521
522 QSGInternalImageNode *node = static_cast<QSGInternalImageNode *>(oldNode);
523
524 bool updatePixmap = d->pixmapChanged;
525 d->pixmapChanged = false;
526 if (!node) {
527 node = d->sceneGraphContext()->createInternalImageNode(renderContext: d->sceneGraphRenderContext());
528 updatePixmap = true;
529 }
530
531 if (updatePixmap)
532 node->setTexture(texture);
533
534 // Don't implicitly create the scalegrid in the rendering thread...
535 QRectF targetRect;
536 QRectF innerTargetRect;
537 QRectF innerSourceRect;
538 QRectF subSourceRect;
539 d->calculateRects(border: d->border,
540 sourceSize: QSize(d->currentPix->width(), d->currentPix->height()), targetSize: QSizeF(width(), height()),
541 horizontalTileMode: d->horizontalTileMode, verticalTileMode: d->verticalTileMode, devicePixelRatio: d->devicePixelRatio,
542 targetRect: &targetRect, innerTargetRect: &innerTargetRect,
543 innerSourceRect: &innerSourceRect, subSourceRect: &subSourceRect);
544
545 node->setTargetRect(targetRect);
546 node->setInnerSourceRect(innerSourceRect);
547 node->setInnerTargetRect(innerTargetRect);
548 node->setSubSourceRect(subSourceRect);
549 node->setMirror(horizontally: d->mirrorHorizontally, vertically: d->mirrorVertically);
550
551 node->setMipmapFiltering(QSGTexture::None);
552 node->setFiltering(d->smooth ? QSGTexture::Linear : QSGTexture::Nearest);
553 if (innerSourceRect == QRectF(0, 0, 1, 1) && (subSourceRect.width() > 1 || subSourceRect.height() > 1)) {
554 node->setHorizontalWrapMode(QSGTexture::Repeat);
555 node->setVerticalWrapMode(QSGTexture::Repeat);
556 } else {
557 node->setHorizontalWrapMode(QSGTexture::ClampToEdge);
558 node->setVerticalWrapMode(QSGTexture::ClampToEdge);
559 }
560 node->setAntialiasing(d->antialiasing);
561 node->update();
562
563 return node;
564}
565
566void QQuickBorderImage::pixmapChange()
567{
568 Q_D(QQuickBorderImage);
569 d->pixmapChanged = true;
570 update();
571}
572
573/*!
574 \qmlproperty int QtQuick::BorderImage::currentFrame
575 \qmlproperty int QtQuick::BorderImage::frameCount
576 \since 5.14
577
578 currentFrame is the frame that is currently visible. The default is \c 0.
579 You can set it to a number between \c 0 and \c {frameCount - 1} to display a
580 different frame, if the image contains multiple frames.
581
582 frameCount is the number of frames in the image. Most images have only one frame.
583*/
584
585/*!
586 \qmlproperty bool QtQuick::BorderImage::retainWhileLoading
587 \since 6.8
588
589 \include qquickimage.cpp qml-image-retainwhileloading
590 */
591
592QT_END_NAMESPACE
593
594#include "moc_qquickborderimage_p.cpp"
595

source code of qtdeclarative/src/quick/items/qquickborderimage.cpp