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 \instantiates 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 file.open(flags: QIODevice::ReadOnly);
280 setGridScaledImage(QQuickGridScaledImage(&file));
281 } else {
282#if QT_CONFIG(qml_network)
283 d->setProgress(0);
284 d->setStatus(Loading);
285
286 QNetworkRequest req(d->url);
287 d->sciReply = qmlEngine(this)->networkAccessManager()->get(request: req);
288 qmlobject_connect(d->sciReply, QNetworkReply, SIGNAL(finished()),
289 this, QQuickBorderImage, SLOT(sciRequestFinished()));
290#endif
291 }
292 } else {
293 loadPixmap(url: d->url, loadOptions: LoadPixmapOptions(HandleDPR | UseProviderOptions));
294 }
295 }
296}
297
298/*!
299 \qmlpropertygroup QtQuick::BorderImage::border
300 \qmlproperty int QtQuick::BorderImage::border.left
301 \qmlproperty int QtQuick::BorderImage::border.right
302 \qmlproperty int QtQuick::BorderImage::border.top
303 \qmlproperty int QtQuick::BorderImage::border.bottom
304
305 The 4 border lines (2 horizontal and 2 vertical) break the image into 9 sections,
306 as shown below:
307
308 \image declarative-scalegrid.png
309
310 Each border line (left, right, top, and bottom) specifies an offset in pixels
311 from the respective edge of the source image. By default, each border line has
312 a value of 0.
313
314 For example, the following definition sets the bottom line 10 pixels up from
315 the bottom of the image:
316
317 \qml
318 BorderImage {
319 border.bottom: 10
320 // ...
321 }
322 \endqml
323
324 The border lines can also be specified using a
325 \l {BorderImage::source}{.sci file}.
326*/
327
328QQuickScaleGrid *QQuickBorderImage::border()
329{
330 Q_D(QQuickBorderImage);
331 return d->getScaleGrid();
332}
333
334/*!
335 \qmlproperty enumeration QtQuick::BorderImage::horizontalTileMode
336 \qmlproperty enumeration QtQuick::BorderImage::verticalTileMode
337
338 This property describes how to repeat or stretch the middle parts of the border image.
339
340 \value BorderImage.Stretch Scales the image to fit to the available area.
341 \value BorderImage.Repeat Tile the image until there is no more space. May crop the last image.
342 \value BorderImage.Round Like Repeat, but scales the images down to ensure that the last image is not cropped.
343
344 The default tile mode for each property is BorderImage.Stretch.
345*/
346QQuickBorderImage::TileMode QQuickBorderImage::horizontalTileMode() const
347{
348 Q_D(const QQuickBorderImage);
349 return d->horizontalTileMode;
350}
351
352void QQuickBorderImage::setHorizontalTileMode(TileMode t)
353{
354 Q_D(QQuickBorderImage);
355 if (t != d->horizontalTileMode) {
356 d->horizontalTileMode = t;
357 emit horizontalTileModeChanged();
358 update();
359 }
360}
361
362QQuickBorderImage::TileMode QQuickBorderImage::verticalTileMode() const
363{
364 Q_D(const QQuickBorderImage);
365 return d->verticalTileMode;
366}
367
368void QQuickBorderImage::setVerticalTileMode(TileMode t)
369{
370 Q_D(QQuickBorderImage);
371 if (t != d->verticalTileMode) {
372 d->verticalTileMode = t;
373 emit verticalTileModeChanged();
374 update();
375 }
376}
377
378void QQuickBorderImage::setGridScaledImage(const QQuickGridScaledImage& sci)
379{
380 Q_D(QQuickBorderImage);
381 if (!sci.isValid()) {
382 d->setStatus(Error);
383 } else {
384 QQuickScaleGrid *sg = border();
385 sg->setTop(sci.gridTop());
386 sg->setBottom(sci.gridBottom());
387 sg->setLeft(sci.gridLeft());
388 sg->setRight(sci.gridRight());
389 d->horizontalTileMode = sci.horizontalTileRule();
390 d->verticalTileMode = sci.verticalTileRule();
391
392 d->sciurl = d->url.resolved(relative: QUrl(sci.pixmapUrl()));
393 loadPixmap(url: d->sciurl);
394 }
395}
396
397void QQuickBorderImage::requestFinished()
398{
399 Q_D(QQuickBorderImage);
400
401 QSize impsize = d->pix.implicitSize();
402 if (d->pix.isError()) {
403 qmlWarning(me: this) << d->pix.error();
404 d->setStatus(Error);
405 d->setProgress(0);
406 } else {
407 d->setStatus(Ready);
408 d->setProgress(1);
409 }
410
411 setImplicitSize(impsize.width() / d->devicePixelRatio, impsize.height() / d->devicePixelRatio);
412
413 if (sourceSize() != d->oldSourceSize) {
414 d->oldSourceSize = sourceSize();
415 emit sourceSizeChanged();
416 }
417 if (d->frameCount != d->pix.frameCount()) {
418 d->frameCount = d->pix.frameCount();
419 emit frameCountChanged();
420 }
421
422 pixmapChange();
423}
424
425#if QT_CONFIG(qml_network)
426#define BORDERIMAGE_MAX_REDIRECT 16
427
428void QQuickBorderImage::sciRequestFinished()
429{
430 Q_D(QQuickBorderImage);
431
432 d->redirectCount++;
433 if (d->redirectCount < BORDERIMAGE_MAX_REDIRECT) {
434 QVariant redirect = d->sciReply->attribute(code: QNetworkRequest::RedirectionTargetAttribute);
435 if (redirect.isValid()) {
436 QUrl url = d->sciReply->url().resolved(relative: redirect.toUrl());
437 setSource(url);
438 return;
439 }
440 }
441 d->redirectCount=0;
442
443 if (d->sciReply->error() != QNetworkReply::NoError) {
444 d->setStatus(Error);
445 d->sciReply->deleteLater();
446 d->sciReply = nullptr;
447 } else {
448 QQuickGridScaledImage sci(d->sciReply);
449 d->sciReply->deleteLater();
450 d->sciReply = nullptr;
451 setGridScaledImage(sci);
452 }
453}
454#endif // qml_network
455
456void QQuickBorderImage::doUpdate()
457{
458 update();
459}
460
461void QQuickBorderImagePrivate::calculateRects(const QQuickScaleGrid *border,
462 const QSize &sourceSize,
463 const QSizeF &targetSize,
464 int horizontalTileMode,
465 int verticalTileMode,
466 qreal devicePixelRatio,
467 QRectF *targetRect,
468 QRectF *innerTargetRect,
469 QRectF *innerSourceRect,
470 QRectF *subSourceRect)
471{
472 *innerSourceRect = QRectF(0, 0, 1, 1);
473 *targetRect = QRectF(0, 0, targetSize.width(), targetSize.height());
474 *innerTargetRect = *targetRect;
475
476 if (border) {
477 qreal borderLeft = border->left() * devicePixelRatio;
478 qreal borderRight = border->right() * devicePixelRatio;
479 qreal borderTop = border->top() * devicePixelRatio;
480 qreal borderBottom = border->bottom() * devicePixelRatio;
481 if (borderLeft + borderRight > sourceSize.width() && borderLeft < sourceSize.width())
482 borderRight = sourceSize.width() - borderLeft;
483 if (borderTop + borderBottom > sourceSize.height() && borderTop < sourceSize.height())
484 borderBottom = sourceSize.height() - borderTop;
485 *innerSourceRect = QRectF(QPointF(borderLeft / qreal(sourceSize.width()),
486 borderTop / qreal(sourceSize.height())),
487 QPointF((sourceSize.width() - borderRight) / qreal(sourceSize.width()),
488 (sourceSize.height() - borderBottom) / qreal(sourceSize.height()))),
489 *innerTargetRect = QRectF(border->left(),
490 border->top(),
491 qMax<qreal>(a: 0, b: targetSize.width() - (border->right() + border->left())),
492 qMax<qreal>(a: 0, b: targetSize.height() - (border->bottom() + border->top())));
493 }
494
495 qreal hTiles = 1;
496 qreal vTiles = 1;
497 const QSizeF innerTargetSize = innerTargetRect->size() * devicePixelRatio;
498 if (innerSourceRect->width() <= 0)
499 hTiles = 0;
500 else if (horizontalTileMode != QQuickBorderImage::Stretch) {
501 hTiles = innerTargetSize.width() / qreal(innerSourceRect->width() * sourceSize.width());
502 if (horizontalTileMode == QQuickBorderImage::Round)
503 hTiles = qCeil(v: hTiles);
504 }
505 if (innerSourceRect->height() <= 0)
506 vTiles = 0;
507 else if (verticalTileMode != QQuickBorderImage::Stretch) {
508 vTiles = innerTargetSize.height() / qreal(innerSourceRect->height() * sourceSize.height());
509 if (verticalTileMode == QQuickBorderImage::Round)
510 vTiles = qCeil(v: vTiles);
511 }
512
513 *subSourceRect = QRectF(0, 0, hTiles, vTiles);
514}
515
516
517QSGNode *QQuickBorderImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
518{
519 Q_D(QQuickBorderImage);
520
521 QSGTexture *texture = d->sceneGraphRenderContext()->textureForFactory(factory: d->pix.textureFactory(), window: window());
522
523 if (!texture || width() <= 0 || height() <= 0) {
524 delete oldNode;
525 return nullptr;
526 }
527
528 QSGInternalImageNode *node = static_cast<QSGInternalImageNode *>(oldNode);
529
530 bool updatePixmap = d->pixmapChanged;
531 d->pixmapChanged = false;
532 if (!node) {
533 node = d->sceneGraphContext()->createInternalImageNode(renderContext: d->sceneGraphRenderContext());
534 updatePixmap = true;
535 }
536
537 if (updatePixmap)
538 node->setTexture(texture);
539
540 // Don't implicitly create the scalegrid in the rendering thread...
541 QRectF targetRect;
542 QRectF innerTargetRect;
543 QRectF innerSourceRect;
544 QRectF subSourceRect;
545 d->calculateRects(border: d->border,
546 sourceSize: QSize(d->pix.width(), d->pix.height()), targetSize: QSizeF(width(), height()),
547 horizontalTileMode: d->horizontalTileMode, verticalTileMode: d->verticalTileMode, devicePixelRatio: d->devicePixelRatio,
548 targetRect: &targetRect, innerTargetRect: &innerTargetRect,
549 innerSourceRect: &innerSourceRect, subSourceRect: &subSourceRect);
550
551 node->setTargetRect(targetRect);
552 node->setInnerSourceRect(innerSourceRect);
553 node->setInnerTargetRect(innerTargetRect);
554 node->setSubSourceRect(subSourceRect);
555 node->setMirror(horizontally: d->mirrorHorizontally, vertically: d->mirrorVertically);
556
557 node->setMipmapFiltering(QSGTexture::None);
558 node->setFiltering(d->smooth ? QSGTexture::Linear : QSGTexture::Nearest);
559 if (innerSourceRect == QRectF(0, 0, 1, 1) && (subSourceRect.width() > 1 || subSourceRect.height() > 1)) {
560 node->setHorizontalWrapMode(QSGTexture::Repeat);
561 node->setVerticalWrapMode(QSGTexture::Repeat);
562 } else {
563 node->setHorizontalWrapMode(QSGTexture::ClampToEdge);
564 node->setVerticalWrapMode(QSGTexture::ClampToEdge);
565 }
566 node->setAntialiasing(d->antialiasing);
567 node->update();
568
569 return node;
570}
571
572void QQuickBorderImage::pixmapChange()
573{
574 Q_D(QQuickBorderImage);
575 d->pixmapChanged = true;
576 update();
577}
578
579/*!
580 \qmlproperty int QtQuick::BorderImage::currentFrame
581 \qmlproperty int QtQuick::BorderImage::frameCount
582 \since 5.14
583
584 currentFrame is the frame that is currently visible. The default is \c 0.
585 You can set it to a number between \c 0 and \c {frameCount - 1} to display a
586 different frame, if the image contains multiple frames.
587
588 frameCount is the number of frames in the image. Most images have only one frame.
589*/
590
591QT_END_NAMESPACE
592
593#include "moc_qquickborderimage_p.cpp"
594

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