1// Copyright (C) 2024 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 <QtQuick/private/qquicksafearea_p.h>
5
6#include <QtQuick/private/qquickanchors_p_p.h>
7#include <QtQuick/private/qquickitem_p.h>
8#include <QtQuick/private/qquickflickable_p.h>
9#include <QtQuick/qquickwindow.h>
10#include <QtQuick/qquickitem.h>
11
12QT_BEGIN_NAMESPACE
13
14Q_STATIC_LOGGING_CATEGORY(lcSafeArea, "qt.quick.safearea", QtWarningMsg)
15
16/*!
17 \qmltype SafeArea
18 \nativetype QQuickSafeArea
19 \inqmlmodule QtQuick
20 \ingroup qtquick-visual
21 \since 6.9
22 \brief Provides access to the safe area properties of the item or window.
23
24 The SafeArea attached type provides information about the areas of
25 an Item or Window where content may risk being overlapped by other
26 UI elements, such as system title bars or status bars.
27
28 This information can be used to lay out children of an item within
29 the safe area of the item, while still allowing a background color
30 or effect to span the entire item.
31
32 \table
33 \row
34 \li \snippet qml/safearea/basic.qml 0
35 \li \inlineimage safearea-ios.webp
36 \endtable
37
38 The SafeArea margins are relative to the item they attach to. If an
39 ancestor item has laid out its children within the safe area margins,
40 any descendant item with its own SafeArea attached will report zero
41 margins, unless \l{Additional margins}{additional margins} have been
42 added.
43
44 \note An item should not be positioned based on \e{its own} safe area,
45 as that would result in a binding loop.
46
47 \section2 Additional margins
48
49 Sometimes an item's layout involves child items that overlap each other,
50 for example in a window with a semi transparent header, where the rest
51 of the window content flows underneath the header.
52
53 In this scenario, the item may reflect the header's position and size
54 to the child items via the additionalMargins property.
55
56 The additional margins will be added to any margins that the
57 item already picks up from its parent hierarchy (including system
58 margins, such as title bars or status bars), and child items will
59 reflect the combined margins accordingly.
60
61 \table
62 \row
63 \li \snippet qml/safearea/additional.qml 0
64 \li \br \inlineimage safearea-ios-header.webp
65 \endtable
66
67 In the example above, the header item is positioned at the top of
68 the window, which may potentially overlap with existing safe area
69 margins coming from the window. To account for this we only add
70 additional margins for the part of the header that extends beyond
71 the window's safe area margins.
72
73 \note In this example the header item does not overlap the child item,
74 as the goal is to show how the items are positioned and resized in
75 response to safe area margin changes.
76
77 \section2 Controls
78
79 Applying safe area margins to a Control is straightforward,
80 as Control already offers properties to add padding to the
81 control's content item.
82
83 \snippet qml/safearea/controls.qml 0
84 */
85
86QQuickSafeArea *QQuickSafeArea::qmlAttachedProperties(QObject *attachee)
87{
88 auto *item = qobject_cast<QQuickItem*>(o: attachee);
89 if (!item) {
90 if (auto *window = qobject_cast<QQuickWindow*>(object: attachee))
91 item = window->contentItem();
92 }
93 if (!item) {
94 if (auto *safeAreaAttachable = qobject_cast<QQuickSafeAreaAttachable*>(object: attachee))
95 item = safeAreaAttachable->safeAreaAttachmentItem();
96 }
97 if (!item) {
98 qmlWarning(me: attachee) << "SafeArea can not be attached to this type";
99 return nullptr;
100 }
101
102 // We may already have created a safe area for Window, and are now
103 // requesting one for Window.contentItem (or the other way around).
104 // As both map to the same safe area item, we need to check first
105 // if we already have created one for this item.
106 if (auto *safeArea = item->findChild<QQuickSafeArea*>(options: Qt::FindDirectChildrenOnly))
107 return safeArea;
108
109 return new QQuickSafeArea(item);
110}
111
112QQuickSafeArea::QQuickSafeArea(QQuickItem *item)
113 : QObject(item)
114{
115 qCInfo(lcSafeArea) << "Creating" << this;
116
117 connect(sender: item, signal: &QQuickItem::windowChanged,
118 context: this, slot: &QQuickSafeArea::windowChanged);
119
120 item->setFlag(flag: QQuickItem::ItemObservesViewport);
121 QQuickItemPrivate::get(item)->addItemChangeListener(
122 listener: this, types: QQuickItemPrivate::Matrix);
123
124 updateSafeArea();
125}
126
127QQuickSafeArea::~QQuickSafeArea()
128{
129 qCInfo(lcSafeArea) << "Destroying" << this;
130
131 const auto listenedItems = m_listenedItems;
132 for (const auto &item : listenedItems) {
133 if (!item)
134 continue;
135 auto *itemPrivate = QQuickItemPrivate::get(item);
136 itemPrivate->removeItemChangeListener(this,
137 types: QQuickItemPrivate::Matrix);
138 itemPrivate->removeItemChangeListener(this,
139 types: QQuickItemPrivate::Geometry);
140 }
141}
142
143/*!
144 \qmlpropertygroup QtQuick::SafeArea::margins
145 \qmlproperty real QtQuick::SafeArea::margins.top
146 \qmlproperty real QtQuick::SafeArea::margins.left
147 \qmlproperty real QtQuick::SafeArea::margins.right
148 \qmlproperty real QtQuick::SafeArea::margins.bottom
149 \readonly
150
151 This property holds the safe area margins, relative
152 to the attached item.
153
154 \sa additionalMargins
155 */
156QMarginsF QQuickSafeArea::margins() const
157{
158 return m_safeAreaMargins;
159}
160
161/*!
162 \qmlpropertygroup QtQuick::SafeArea::additionalMargins
163 \qmlproperty real QtQuick::SafeArea::additionalMargins.top
164 \qmlproperty real QtQuick::SafeArea::additionalMargins.left
165 \qmlproperty real QtQuick::SafeArea::additionalMargins.right
166 \qmlproperty real QtQuick::SafeArea::additionalMargins.bottom
167
168 This property holds the additional safe area margins for the item.
169
170 The additional safe area margins can not be negative, and will be
171 automatically clamped to 0.
172
173 The resulting safe area margins of the item are the sum of the inherited
174 margins (for example from title bars or status bar) and the additional
175 margins applied to the item.
176
177 \sa margins
178 */
179
180void QQuickSafeArea::setAdditionalMargins(const QMarginsF &additionalMargins)
181{
182 // Additional margins should never be negative
183 auto newMargins = additionalMargins | QMarginsF();
184
185 if (newMargins == m_additionalMargins)
186 return;
187
188 m_additionalMargins = newMargins;
189
190 emit additionalMarginsChanged();
191
192 auto *attachedItem = qobject_cast<QQuickItem*>(o: parent());
193 updateSafeAreasRecursively(fromItem: attachedItem);
194}
195
196QMarginsF QQuickSafeArea::additionalMargins() const
197{
198 return m_additionalMargins;
199}
200
201/*
202 Maps the safe area \a margins from \a fromItem to \a toItem
203*/
204static QMarginsF toLocalMargins(const QMarginsF &margins, QQuickItem *fromItem, QQuickItem *toItem)
205{
206 if (margins.isNull())
207 return margins;
208
209 const auto localMarginRect = fromItem->mapRectToItem(item: toItem,
210 rect: QRectF(margins.left(), margins.top(),
211 fromItem->width() - margins.left() - margins.right(),
212 fromItem->height() - margins.top() - margins.bottom()));
213
214 // Only return a mapped margin if there was an original margin
215 return QMarginsF(
216 margins.left() > 0 ? localMarginRect.left() : 0,
217 margins.top() > 0 ? localMarginRect.top() : 0,
218 margins.right() > 0 ? toItem->width() - localMarginRect.right() : 0,
219 margins.bottom() > 0 ? toItem->height() - localMarginRect.bottom() : 0
220 ) | QMarginsF();
221}
222
223void QQuickSafeArea::updateSafeArea()
224{
225 qCDebug(lcSafeArea) << "✨ Updating" << this;
226
227 auto *attachedItem = qobject_cast<QQuickItem*>(o: parent());
228 if (!QQuickItemPrivate::get(item: attachedItem)->componentComplete) {
229 qCDebug(lcSafeArea) << attachedItem << "is not complete. Deferring";
230 return;
231 }
232
233 QMarginsF inheritedMargins;
234 auto *parentItem = attachedItem->parentItem();
235 while (parentItem) {
236 if (qobject_cast<QQuickFlickable*>(object: parentItem)) {
237 // Stop propagation of safe areas when we hit a Flickable,
238 // as items within the content item that account for safe
239 // area margins will continuously update when the content
240 // item is moved, which is not necessarily what the user
241 // expects.
242 qCDebug(lcSafeArea) << "Stopping safe area margin propagation on" << parentItem;
243 break;
244 }
245
246
247 // We attach the safe area to the relevant item for an attachee
248 // such as QQuickWindow or QQuickPopup, so we can't go via
249 // qmlAttachedPropertiesObject to find the safe area for an
250 // item, as the attached object cache is based on the original
251 // attachee.
252 if (auto *safeArea = parentItem->findChild<QQuickSafeArea*>(options: Qt::FindDirectChildrenOnly)) {
253 inheritedMargins = safeArea->margins();
254 break;
255 }
256
257 parentItem = parentItem->parentItem();
258 }
259
260 const auto *window = attachedItem->window();
261 if (!parentItem && window) {
262 // We didn't find a parent item with a safe area,
263 // so inherit the margins from the window.
264 parentItem = window->contentItem();
265 inheritedMargins = window->safeAreaMargins();
266 }
267
268 auto inheritedMarginsMapped = toLocalMargins(margins: inheritedMargins, fromItem: parentItem, toItem: attachedItem);
269
270 // Make sure margins are never negative
271 const QMarginsF newMargins = QMarginsF() | (inheritedMarginsMapped + additionalMargins());
272
273 if (newMargins != m_safeAreaMargins) {
274 qCDebug(lcSafeArea) << "Margins changed from" << m_safeAreaMargins
275 << "to" << newMargins
276 << "based on inherited" << inheritedMargins
277 << "mapped to local" << inheritedMarginsMapped
278 << "and additional" << additionalMargins();
279
280 m_safeAreaMargins = newMargins;
281
282 if (emittingMarginsUpdate) {
283 // We are already in the process of emitting an update for this
284 // safe area, which resulted in the safe area margins changing.
285 // This can be a binding loop if the margins do not stabilize,
286 // which we'll detect when we return from the root emit below.
287 qCDebug(lcSafeArea) << "Already emitting update for" << this;
288 return;
289 }
290
291 QScopedValueRollback blocker(emittingMarginsUpdate, true);
292 emit marginsChanged();
293
294 if (m_safeAreaMargins != newMargins) {
295 qCDebug(lcSafeArea) << "⚠️ Possible binding loop for" << this
296 << newMargins << "changed to" << m_safeAreaMargins;
297
298 QScopedValueRollback blocker(detectedPossibleBindingLoop, true);
299
300 for (int i = 0; i < 5; ++i) {
301 auto marginsBeforeEmit = m_safeAreaMargins;
302 emit marginsChanged();
303 if (m_safeAreaMargins == marginsBeforeEmit) {
304 qCDebug(lcSafeArea) << "✅ Margins stabilized for" << this;
305 return;
306 }
307
308 qCDebug(lcSafeArea) << qPrintable(QStringLiteral("‼️").repeated(i + 1))
309 << marginsBeforeEmit << "changed to" << m_safeAreaMargins;
310 }
311
312 qmlWarning(me: attachedItem) << "Safe area binding loop detected";
313 }
314 }
315}
316
317void QQuickSafeArea::windowChanged()
318{
319 updateSafeArea();
320}
321
322void QQuickSafeArea::itemTransformChanged(QQuickItem *item, QQuickItem *transformedItem)
323{
324 Q_ASSERT(item == parent());
325
326 auto *transformedItemPrivate = QQuickItemPrivate::get(item: transformedItem);
327 qCDebug(lcSafeArea) << "📏 Transform changed for" << transformedItem
328 << "with dirty state" << transformedItemPrivate->dirtyToString();
329
330 if (qobject_cast<QQuickFlickable*>(object: transformedItem->parentItem())) {
331 qCDebug(lcSafeArea) << "Ignoring transform change for Flickable content item";
332 return;
333 }
334
335 // The order of transform and geometry change callbacks may not be in paint order,
336 // so to ensure we update the safe areas in paint order we find the item closest
337 // to the transformed item with a safe area, and let that safe area trigger the
338 // update recursively in paint order.
339 if (transformedItem != item) {
340 for (auto *parent = item->parentItem(); parent; parent = parent->parentItem()) {
341 if (parent->findChild<QQuickSafeArea*>(options: Qt::FindDirectChildrenOnly))
342 item = parent;
343
344 if (parent == transformedItem)
345 break;
346 }
347 }
348
349 if (item != parent()) {
350 qCDebug(lcSafeArea) << "Found" << item << "closer to transformed item than" << this;
351 return;
352 }
353
354 // The dirtying of position and size will be followed by a geometry change,
355 // which via anchors or event listeners may result in an ancestor invalidating
356 // its transform, which might invalidate the margins we're about to compute.
357 // Instead of processing the margin change now, possibly resulting in a flip-
358 // flop of the margins, we wait for the geometry notification, where the item
359 // hierarchy has already reacted to the geometry change of the transformed item.
360 // This accounts for anchors, and items that listen to geometry changes, but not
361 // property bindings, as those are emitted after notifying listeners (us) about
362 // the geometry change.
363 auto dirtyAttributes = transformedItemPrivate->dirtyAttributes;
364 if (dirtyAttributes & (QQuickItemPrivate::Position | QQuickItemPrivate::Size)) {
365 qCDebug(lcSafeArea) << "Deferring update of" << this << "until geometry change";
366 transformedItemPrivate->addItemChangeListener(
367 listener: this, types: QQuickItemPrivate::Geometry);
368 return;
369 }
370
371 updateSafeAreasRecursively(fromItem: item);
372}
373
374void QQuickSafeArea::itemGeometryChanged(QQuickItem *item, QQuickGeometryChange change, const QRectF &oldGeometry)
375{
376 Q_UNUSED(change);
377 Q_UNUSED(oldGeometry);
378
379 auto *itemPrivate = QQuickItemPrivate::get(item);
380 itemPrivate->removeItemChangeListener(this, types: QQuickItemPrivate::Geometry);
381
382 qCDebug(lcSafeArea) << "📐 Geometry changed for" << item << "from" << oldGeometry
383 << "to" << QRectF(item->position(), item->size());
384
385 updateSafeAreasRecursively(fromItem: item);
386}
387
388void QQuickSafeArea::updateSafeAreasRecursively(QQuickItem *item)
389{
390 Q_ASSERT(item);
391
392 if (auto *safeArea = item->findChild<QQuickSafeArea*>(options: Qt::FindDirectChildrenOnly))
393 safeArea->updateSafeArea();
394
395 auto *itemPrivate = QQuickItemPrivate::get(item);
396 const auto paintOrderChildItems = itemPrivate->paintOrderChildItems();
397 for (auto *child : paintOrderChildItems)
398 updateSafeAreasRecursively(item: child);
399}
400
401void QQuickSafeArea::addSourceItem(QQuickItem *item)
402{
403 m_listenedItems << item;
404}
405
406void QQuickSafeArea::removeSourceItem(QQuickItem *item)
407{
408 m_listenedItems.removeAll(t: item);
409}
410
411#ifndef QT_NO_DEBUG_STREAM
412QDebug operator<<(QDebug debug, const QQuickSafeArea *safeArea)
413{
414 QDebugStateSaver saver(debug);
415 debug.nospace();
416
417 if (!safeArea) {
418 debug << "QQuickSafeArea(nullptr)";
419 return debug;
420 }
421
422 debug << safeArea->metaObject()->className() << '(' << static_cast<const void *>(safeArea);
423
424 debug << ", attachedItem=" << safeArea->parent();
425 debug << ", safeAreaMargins=" << safeArea->m_safeAreaMargins;
426 debug << ", additionalMargins=" << safeArea->additionalMargins();
427
428 debug << ')';
429 return debug;
430}
431#endif // QT_NO_DEBUG_STREAM
432
433QQuickSafeAreaAttachable::~QQuickSafeAreaAttachable() = default;
434
435QT_END_NAMESPACE
436
437#include "moc_qquicksafearea_p.cpp"
438

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