Qt-based title widget ported from tdesktop

This commit is contained in:
Ilya Fedin 2021-02-01 14:22:36 +04:00 committed by John Preston
parent e2633c4b6f
commit 6500dc9610
27 changed files with 1177 additions and 208 deletions

View file

@ -77,6 +77,8 @@ PRIVATE
ui/paint/blobs.h ui/paint/blobs.h
ui/paint/blobs_linear.cpp ui/paint/blobs_linear.cpp
ui/paint/blobs_linear.h ui/paint/blobs_linear.h
ui/platform/linux/ui_linux_wayland_integration.cpp
ui/platform/linux/ui_linux_wayland_integration.h
ui/platform/linux/ui_window_linux.cpp ui/platform/linux/ui_window_linux.cpp
ui/platform/linux/ui_window_linux.h ui/platform/linux/ui_window_linux.h
ui/platform/linux/ui_utility_linux.cpp ui/platform/linux/ui_utility_linux.cpp
@ -95,6 +97,8 @@ PRIVATE
ui/platform/win/ui_window_win.h ui/platform/win/ui_window_win.h
ui/platform/win/ui_utility_win.cpp ui/platform/win/ui_utility_win.cpp
ui/platform/win/ui_utility_win.h ui/platform/win/ui_utility_win.h
ui/platform/ui_platform_window_title.cpp
ui/platform/ui_platform_window_title.h
ui/platform/ui_platform_window.cpp ui/platform/ui_platform_window.cpp
ui/platform/ui_platform_window.h ui/platform/ui_platform_window.h
ui/platform/ui_platform_utility.h ui/platform/ui_platform_utility.h
@ -228,6 +232,11 @@ if (NOT DESKTOP_APP_USE_PACKAGED_FONTS)
nice_target_sources(lib_ui ${src_loc} PRIVATE fonts/fonts.qrc) nice_target_sources(lib_ui ${src_loc} PRIVATE fonts/fonts.qrc)
endif() endif()
if (DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION)
remove_target_sources(Telegram ${src_loc} ui/platform/linux/ui_linux_wayland_integration.cpp)
nice_target_sources(Telegram ${src_loc} PRIVATE ui/platform/linux/ui_linux_wayland_integration_dummy.cpp)
endif()
target_include_directories(lib_ui target_include_directories(lib_ui
PUBLIC PUBLIC
${src_loc} ${src_loc}

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

View file

@ -0,0 +1,46 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/platform/linux/ui_linux_wayland_integration.h"
#include "base/platform/base_platform_info.h"
#include <QtGui/QWindow>
#include <private/qwaylanddisplay_p.h>
#include <private/qwaylandwindow_p.h>
#include <private/qwaylandshellsurface_p.h>
using QtWaylandClient::QWaylandWindow;
namespace Ui {
namespace Platform {
WaylandIntegration::WaylandIntegration() {
}
WaylandIntegration *WaylandIntegration::Instance() {
if (!::Platform::IsWayland()) return nullptr;
static WaylandIntegration instance;
return &instance;
}
bool WaylandIntegration::showWindowMenu(QWindow *window) {
if (const auto waylandWindow = static_cast<QWaylandWindow*>(
window->handle())) {
if (const auto seat = waylandWindow->display()->lastInputDevice()) {
if (const auto shellSurface = waylandWindow->shellSurface()) {
return shellSurface->showWindowMenu(seat);
}
}
}
return false;
}
} // namespace Platform
} // namespace Ui

View file

@ -0,0 +1,25 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class QWindow;
namespace Ui {
namespace Platform {
class WaylandIntegration {
public:
static WaylandIntegration *Instance();
bool showWindowMenu(QWindow *window);
private:
WaylandIntegration();
};
} // namespace Platform
} // namespace Ui

View file

@ -0,0 +1,29 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/platform/linux/ui_linux_wayland_integration.h"
#include "base/platform/base_platform_info.h"
namespace Ui {
namespace Platform {
WaylandIntegration::WaylandIntegration() {
}
WaylandIntegration *WaylandIntegration::Instance() {
if (!::Platform::IsWayland()) return nullptr;
static WaylandIntegration instance;
return &instance;
}
bool WaylandIntegration::showWindowMenu(QWindow *window) {
return false;
}
} // namespace Platform
} // namespace Ui

View file

@ -8,16 +8,201 @@
#include "ui/ui_log.h" #include "ui/ui_log.h"
#include "base/platform/base_platform_info.h" #include "base/platform/base_platform_info.h"
#include "base/platform/linux/base_xcb_utilities_linux.h"
#include "ui/platform/linux/ui_linux_wayland_integration.h"
#include "base/const_string.h"
#include "base/qt_adapters.h" #include "base/qt_adapters.h"
#include "base/flat_set.h" #include "base/flat_set.h"
#include <QtCore/QPoint> #include <QtCore/QPoint>
#include <QtGui/QScreen> #include <QtGui/QScreen>
#include <QtGui/QWindow>
#include <QtWidgets/QApplication> #include <QtWidgets/QApplication>
#include <qpa/qplatformnativeinterface.h> #include <qpa/qplatformnativeinterface.h>
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
#include <QtDBus/QDBusConnection>
#include <QtDBus/QDBusMessage>
#include <QtDBus/QDBusReply>
#include <QtDBus/QDBusVariant>
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
Q_DECLARE_METATYPE(QMargins);
namespace Ui { namespace Ui {
namespace Platform { namespace Platform {
namespace {
constexpr auto kXCBFrameExtentsAtomName = "_GTK_FRAME_EXTENTS"_cs;
constexpr auto kXDGDesktopPortalService = "org.freedesktop.portal.Desktop"_cs;
constexpr auto kXDGDesktopPortalObjectPath = "/org/freedesktop/portal/desktop"_cs;
constexpr auto kSettingsPortalInterface = "org.freedesktop.portal.Settings"_cs;
bool SetXCBFrameExtents(QWindow *window, const QMargins &extents) {
const auto connection = base::Platform::XCB::GetConnectionFromQt();
if (!connection) {
return false;
}
const auto frameExtentsAtom = base::Platform::XCB::GetAtom(
connection,
kXCBFrameExtentsAtomName.utf16());
if (!frameExtentsAtom.has_value()) {
return false;
}
const auto extentsVector = std::vector<uint>{
uint(extents.left()),
uint(extents.right()),
uint(extents.top()),
uint(extents.bottom()),
};
xcb_change_property(
connection,
XCB_PROP_MODE_REPLACE,
window->winId(),
*frameExtentsAtom,
XCB_ATOM_CARDINAL,
32,
extentsVector.size(),
extentsVector.data());
return true;
}
bool UnsetXCBFrameExtents(QWindow *window) {
const auto connection = base::Platform::XCB::GetConnectionFromQt();
if (!connection) {
return false;
}
const auto frameExtentsAtom = base::Platform::XCB::GetAtom(
connection,
kXCBFrameExtentsAtomName.utf16());
if (!frameExtentsAtom.has_value()) {
return false;
}
xcb_delete_property(
connection,
window->winId(),
*frameExtentsAtom);
return true;
}
bool ShowXCBWindowMenu(QWindow *window) {
const auto connection = base::Platform::XCB::GetConnectionFromQt();
if (!connection) {
return false;
}
const auto root = base::Platform::XCB::GetRootWindowFromQt();
if (!root.has_value()) {
return false;
}
const auto showWindowMenuAtom = base::Platform::XCB::GetAtom(
connection,
"_GTK_SHOW_WINDOW_MENU");
if (!showWindowMenuAtom.has_value()) {
return false;
}
const auto globalPos = QCursor::pos();
xcb_client_message_event_t xev;
xev.response_type = XCB_CLIENT_MESSAGE;
xev.type = *showWindowMenuAtom;
xev.sequence = 0;
xev.window = window->winId();
xev.format = 32;
xev.data.data32[0] = 0;
xev.data.data32[1] = globalPos.x();
xev.data.data32[2] = globalPos.y();
xev.data.data32[3] = 0;
xev.data.data32[4] = 0;
xcb_ungrab_pointer(connection, XCB_CURRENT_TIME);
xcb_send_event(
connection,
false,
*root,
XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT
| XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,
reinterpret_cast<const char*>(&xev));
return true;
}
TitleControls::Control GtkKeywordToTitleControl(const QString &keyword) {
if (keyword == qstr("minimize")) {
return TitleControls::Control::Minimize;
} else if (keyword == qstr("maximize")) {
return TitleControls::Control::Maximize;
} else if (keyword == qstr("close")) {
return TitleControls::Control::Close;
}
return TitleControls::Control::Unknown;
}
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
std::optional<TitleControls::Layout> PortalTitleControlsLayout() {
auto message = QDBusMessage::createMethodCall(
kXDGDesktopPortalService.utf16(),
kXDGDesktopPortalObjectPath.utf16(),
kSettingsPortalInterface.utf16(),
"Read");
message.setArguments({
"org.gnome.desktop.wm.preferences",
"button-layout"
});
const QDBusReply<QVariant> reply = QDBusConnection::sessionBus().call(
message);
if (!reply.isValid() || !reply.value().canConvert<QDBusVariant>()) {
return std::nullopt;
}
const auto valueVariant = qvariant_cast<QDBusVariant>(
reply.value()).variant();
if (!valueVariant.canConvert<QString>()) {
return std::nullopt;
}
const auto valueBySides = valueVariant.toString().split(':');
std::vector<TitleControls::Control> controlsLeft;
ranges::transform(
valueBySides[0].split(','),
ranges::back_inserter(controlsLeft),
GtkKeywordToTitleControl);
std::vector<TitleControls::Control> controlsRight;
if (valueBySides.size() > 1) {
ranges::transform(
valueBySides[1].split(','),
ranges::back_inserter(controlsRight),
GtkKeywordToTitleControl);
}
return TitleControls::Layout{
.left = controlsLeft,
.right = controlsRight
};
}
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
} // namespace
bool IsApplicationActive() { bool IsApplicationActive() {
return QApplication::activeWindow() != nullptr; return QApplication::activeWindow() != nullptr;
@ -50,5 +235,71 @@ bool TranslucentWindowsSupported(QPoint globalPosition) {
void IgnoreAllActivation(not_null<QWidget*> widget) { void IgnoreAllActivation(not_null<QWidget*> widget) {
} }
bool WindowExtentsSupported() {
#ifdef DESKTOP_APP_QT_PATCHED
if (::Platform::IsWayland()) {
return true;
}
#endif // DESKTOP_APP_QT_PATCHED
namespace XCB = base::Platform::XCB;
if (!::Platform::IsWayland()
&& XCB::IsSupportedByWM(kXCBFrameExtentsAtomName.utf16())) {
return true;
}
return false;
}
bool SetWindowExtents(QWindow *window, const QMargins &extents) {
if (::Platform::IsWayland()) {
#ifdef DESKTOP_APP_QT_PATCHED
window->setProperty("WaylandCustomMargins", QVariant::fromValue<QMargins>(extents));
return true;
#else // DESKTOP_APP_QT_PATCHED
return false;
#endif // !DESKTOP_APP_QT_PATCHED
} else {
return SetXCBFrameExtents(window, extents);
}
}
bool UnsetWindowExtents(QWindow *window) {
if (::Platform::IsWayland()) {
#ifdef DESKTOP_APP_QT_PATCHED
window->setProperty("WaylandCustomMargins", QVariant());
return true;
#else // DESKTOP_APP_QT_PATCHED
return false;
#endif // !DESKTOP_APP_QT_PATCHED
} else {
return UnsetXCBFrameExtents(window);
}
}
bool ShowWindowMenu(QWindow *window) {
if (const auto integration = WaylandIntegration::Instance()) {
return integration->showWindowMenu(window);
} else {
return ShowXCBWindowMenu(window);
}
}
TitleControls::Layout TitleControlsLayout() {
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
if (const auto portalLayout = PortalTitleControlsLayout()) {
return *portalLayout;
}
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
return TitleControls::Layout{
.right = {
TitleControls::Control::Minimize,
TitleControls::Control::Maximize,
TitleControls::Control::Close,
}
};
}
} // namespace Platform } // namespace Platform
} // namespace Ui } // namespace Ui

View file

@ -6,6 +6,8 @@
// //
#pragma once #pragma once
#include "ui/platform/ui_platform_utility.h"
class QPainter; class QPainter;
class QPaintEvent; class QPaintEvent;

View file

@ -6,6 +6,7 @@
// //
#pragma once #pragma once
#include "ui/platform/ui_platform_utility.h"
#include "base/platform/base_platform_info.h" #include "base/platform/base_platform_info.h"
#include <QtCore/QPoint> #include <QtCore/QPoint>
@ -23,5 +24,21 @@ inline constexpr bool UseMainQueueGeneric() {
return ::Platform::IsMacStoreBuild(); return ::Platform::IsMacStoreBuild();
} }
inline bool WindowExtentsSupported() {
return false;
}
inline bool SetWindowExtents(QWindow *window, const QMargins &extents) {
return false;
}
inline bool UnsetWindowExtents(QWindow *window) {
return false;
}
inline bool ShowWindowMenu(QWindow *window) {
return false;
}
} // namespace Platform } // namespace Platform
} // namespace Ui } // namespace Ui

View file

@ -99,5 +99,15 @@ void DrainMainQueue() {
void IgnoreAllActivation(not_null<QWidget*> widget) { void IgnoreAllActivation(not_null<QWidget*> widget) {
} }
TitleControls::Layout TitleControlsLayout() {
return TitleControls::Layout{
.left = {
TitleControls::Control::Close,
TitleControls::Control::Minimize,
TitleControls::Control::Maximize,
}
};
}
} // namespace Platform } // namespace Platform
} // namespace Ui } // namespace Ui

View file

@ -6,6 +6,8 @@
// //
#pragma once #pragma once
#include "ui/platform/ui_platform_window_title.h"
class QPoint; class QPoint;
class QPainter; class QPainter;
class QPaintEvent; class QPaintEvent;
@ -30,6 +32,12 @@ void IgnoreAllActivation(not_null<QWidget*> widget);
[[nodiscard]] constexpr bool UseMainQueueGeneric(); [[nodiscard]] constexpr bool UseMainQueueGeneric();
void DrainMainQueue(); // Needed only if UseMainQueueGeneric() is false. void DrainMainQueue(); // Needed only if UseMainQueueGeneric() is false.
[[nodiscard]] bool WindowExtentsSupported();
bool SetWindowExtents(QWindow *window, const QMargins &extents);
bool UnsetWindowExtents(QWindow *window);
bool ShowWindowMenu(QWindow *window);
[[nodiscard]] TitleControls::Layout TitleControlsLayout();
} // namespace Platform } // namespace Platform
} // namespace Ui } // namespace Ui

View file

@ -6,21 +6,31 @@
// //
#include "ui/platform/ui_platform_window.h" #include "ui/platform/ui_platform_window.h"
#include "ui/platform/ui_platform_window_title.h"
#include "ui/platform/ui_platform_utility.h"
#include "ui/widgets/window.h" #include "ui/widgets/window.h"
#include "ui/widgets/shadow.h"
#include "ui/painter.h"
#include "styles/style_widgets.h"
#include "styles/style_layers.h"
#include <QtCore/QCoreApplication>
#include <QtGui/QWindow> #include <QtGui/QWindow>
#include <QtGui/QtEvents> #include <QtGui/QtEvents>
namespace Ui { namespace Ui {
namespace Platform { namespace Platform {
namespace {
[[nodiscard]] const style::Shadow &Shadow() {
return st::callShadow;
}
} // namespace
BasicWindowHelper::BasicWindowHelper(not_null<RpWidget*> window) BasicWindowHelper::BasicWindowHelper(not_null<RpWidget*> window)
: _window(window) { : _window(window) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
_window->setWindowFlag(Qt::Window); _window->setWindowFlag(Qt::Window);
#else // Qt >= 5.9
_window->setWindowFlags(_window->windowFlags() | Qt::Window);
#endif // Qt >= 5.9
} }
not_null<RpWidget*> BasicWindowHelper::body() { not_null<RpWidget*> BasicWindowHelper::body() {
@ -100,8 +110,6 @@ void BasicWindowHelper::setupBodyTitleAreaEvents() {
&& (static_cast<QMouseEvent*>(e.get())->button() && (static_cast<QMouseEvent*>(e.get())->button()
== Qt::LeftButton)) { == Qt::LeftButton)) {
_mousePressed = true; _mousePressed = true;
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) || defined DESKTOP_APP_QT_PATCHED
} else if (e->type() == QEvent::MouseMove) { } else if (e->type() == QEvent::MouseMove) {
const auto mouseEvent = static_cast<QMouseEvent*>(e.get()); const auto mouseEvent = static_cast<QMouseEvent*>(e.get());
if (_mousePressed if (_mousePressed
@ -109,7 +117,6 @@ void BasicWindowHelper::setupBodyTitleAreaEvents() {
&& !_window->isFullScreen() && !_window->isFullScreen()
#endif // !Q_OS_WIN #endif // !Q_OS_WIN
&& (hitTest() & WindowTitleHitTestFlag::Move)) { && (hitTest() & WindowTitleHitTestFlag::Move)) {
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
if (_window->isFullScreen()) { if (_window->isFullScreen()) {
// On Windows we just jump out of fullscreen // On Windows we just jump out of fullscreen
@ -121,10 +128,258 @@ void BasicWindowHelper::setupBodyTitleAreaEvents() {
_mousePressed = false; _mousePressed = false;
_window->windowHandle()->startSystemMove(); _window->windowHandle()->startSystemMove();
} }
#endif // Qt >= 5.15 || DESKTOP_APP_QT_PATCHED
} }
}, body()->lifetime()); }, body()->lifetime());
} }
DefaultWindowHelper::DefaultWindowHelper(not_null<RpWidget*> window)
: BasicWindowHelper(window)
, _title(Ui::CreateChild<DefaultTitleWidget>(window.get()))
, _body(Ui::CreateChild<RpWidget>(window.get())) {
init();
}
void DefaultWindowHelper::init() {
window()->setWindowFlag(Qt::FramelessWindowHint);
if (WindowExtentsSupported()) {
window()->setAttribute(Qt::WA_TranslucentBackground);
}
window()->widthValue(
) | rpl::start_with_next([=](int width) {
_title->setGeometry(
resizeArea().left(),
resizeArea().top(),
width - resizeArea().left() - resizeArea().right(),
_title->st()->height);
}, _title->lifetime());
rpl::combine(
window()->sizeValue(),
_title->heightValue()
) | rpl::start_with_next([=](QSize size, int titleHeight) {
const auto sizeWithoutMargins = size
.shrunkBy({ 0, titleHeight, 0, 0 })
.shrunkBy(resizeArea());
const auto topLeft = QPoint(
resizeArea().left(),
resizeArea().top() + titleHeight);
_body->setGeometry(QRect(topLeft, sizeWithoutMargins));
}, _body->lifetime());
window()->paintRequest(
) | rpl::start_with_next([=] {
if (resizeArea().isNull()) {
return;
}
Painter p(window());
if (hasShadow()) {
Ui::Shadow::paint(
p,
QRect(QPoint(), window()->size()).marginsRemoved(resizeArea()),
window()->width(),
Shadow());
} else {
paintBorders(p);
}
}, window()->lifetime());
window()->shownValue(
) | rpl::start_with_next([=](bool shown) {
if (shown) {
updateWindowExtents();
}
}, window()->lifetime());
window()->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) {
const auto mouseEvent = static_cast<QMouseEvent*>(e.get());
const auto currentPoint = mouseEvent->windowPos().toPoint();
const auto edges = edgesFromPos(currentPoint);
if (mouseEvent->button() == Qt::LeftButton && edges) {
window()->windowHandle()->startSystemResize(edges);
}
} else if (e->type() == QEvent::Move
|| e->type() == QEvent::Resize
|| e->type() == QEvent::WindowStateChange) {
updateWindowExtents();
}
}, window()->lifetime());
QCoreApplication::instance()->installEventFilter(this);
}
not_null<RpWidget*> DefaultWindowHelper::body() {
return _body;
}
bool DefaultWindowHelper::hasShadow() const {
const auto center = window()->geometry().center();
return WindowExtentsSupported() && TranslucentWindowsSupported(center);
}
QMargins DefaultWindowHelper::resizeArea() const {
if (window()->isMaximized() || window()->isFullScreen()) {
return QMargins();
}
return Shadow().extend;
}
Qt::Edges DefaultWindowHelper::edgesFromPos(const QPoint &pos) const {
if (pos.x() <= resizeArea().left()) {
if (pos.y() <= resizeArea().top()) {
return Qt::LeftEdge | Qt::TopEdge;
} else if (pos.y() >= (window()->height() - resizeArea().bottom())) {
return Qt::LeftEdge | Qt::BottomEdge;
}
return Qt::LeftEdge;
} else if (pos.x() >= (window()->width() - resizeArea().right())) {
if (pos.y() <= resizeArea().top()) {
return Qt::RightEdge | Qt::TopEdge;
} else if (pos.y() >= (window()->height() - resizeArea().bottom())) {
return Qt::RightEdge | Qt::BottomEdge;
}
return Qt::RightEdge;
} else if (pos.y() <= resizeArea().top()) {
return Qt::TopEdge;
} else if (pos.y() >= (window()->height() - resizeArea().bottom())) {
return Qt::BottomEdge;
} else {
return Qt::Edges();
}
}
bool DefaultWindowHelper::eventFilter(QObject *obj, QEvent *e) {
// doesn't work with RpWidget::events() for some reason
if (e->type() == QEvent::MouseMove
&& obj->isWidgetType()
&& static_cast<QWidget*>(window()) == static_cast<QWidget*>(obj)) {
const auto mouseEvent = static_cast<QMouseEvent*>(e);
const auto currentPoint = mouseEvent->windowPos().toPoint();
const auto edges = edgesFromPos(currentPoint);
if (mouseEvent->buttons() == Qt::NoButton) {
updateCursor(edges);
}
}
return QObject::eventFilter(obj, e);
}
void DefaultWindowHelper::setTitle(const QString &title) {
_title->setText(title);
window()->setWindowTitle(title);
}
void DefaultWindowHelper::setTitleStyle(const style::WindowTitle &st) {
_title->setStyle(st);
_title->setGeometry(
resizeArea().left(),
resizeArea().top(),
window()->width() - resizeArea().left() - resizeArea().right(),
_title->st()->height);
}
void DefaultWindowHelper::setMinimumSize(QSize size) {
const auto sizeWithMargins = size
.grownBy({ 0, _title->height(), 0, 0 })
.grownBy(resizeArea());
window()->setMinimumSize(sizeWithMargins);
}
void DefaultWindowHelper::setFixedSize(QSize size) {
const auto sizeWithMargins = size
.grownBy({ 0, _title->height(), 0, 0 })
.grownBy(resizeArea());
window()->setFixedSize(sizeWithMargins);
_title->setResizeEnabled(false);
}
void DefaultWindowHelper::setGeometry(QRect rect) {
window()->setGeometry(rect
.marginsAdded({ 0, _title->height(), 0, 0 })
.marginsAdded(resizeArea()));
}
void DefaultWindowHelper::paintBorders(QPainter &p) {
const auto titleBackground = window()->isActiveWindow()
? _title->st()->bgActive
: _title->st()->bg;
const auto defaultTitleBackground = window()->isActiveWindow()
? st::defaultWindowTitle.bgActive
: st::defaultWindowTitle.bg;
const auto borderColor = QBrush(titleBackground).isOpaque()
? titleBackground
: defaultTitleBackground;
p.fillRect(
0,
resizeArea().top(),
resizeArea().left(),
window()->height() - resizeArea().top() - resizeArea().bottom(),
borderColor);
p.fillRect(
window()->width() - resizeArea().right(),
resizeArea().top(),
resizeArea().right(),
window()->height() - resizeArea().top() - resizeArea().bottom(),
borderColor);
p.fillRect(
0,
0,
window()->width(),
resizeArea().top(),
borderColor);
p.fillRect(
0,
window()->height() - resizeArea().bottom(),
window()->width(),
resizeArea().bottom(),
borderColor);
}
void DefaultWindowHelper::updateWindowExtents() {
if (hasShadow()) {
Platform::SetWindowExtents(
window()->windowHandle(),
resizeArea());
_extentsSet = true;
} else if (_extentsSet) {
Platform::UnsetWindowExtents(window()->windowHandle());
_extentsSet = false;
}
}
void DefaultWindowHelper::updateCursor(Qt::Edges edges) {
if (((edges & Qt::LeftEdge) && (edges & Qt::TopEdge))
|| ((edges & Qt::RightEdge) && (edges & Qt::BottomEdge))) {
window()->setCursor(QCursor(Qt::SizeFDiagCursor));
} else if (((edges & Qt::LeftEdge) && (edges & Qt::BottomEdge))
|| ((edges & Qt::RightEdge) && (edges & Qt::TopEdge))) {
window()->setCursor(QCursor(Qt::SizeBDiagCursor));
} else if ((edges & Qt::LeftEdge) || (edges & Qt::RightEdge)) {
window()->setCursor(QCursor(Qt::SizeHorCursor));
} else if ((edges & Qt::TopEdge) || (edges & Qt::BottomEdge)) {
window()->setCursor(QCursor(Qt::SizeVerCursor));
} else {
window()->unsetCursor();
}
}
} // namespace Platform } // namespace Platform
} // namespace Ui } // namespace Ui

View file

@ -20,6 +20,8 @@ using WindowTitleHitTestFlags = base::flags<WindowTitleHitTestFlag>;
namespace Platform { namespace Platform {
class DefaultTitleWidget;
class BasicWindowHelper { class BasicWindowHelper {
public: public:
explicit BasicWindowHelper(not_null<RpWidget*> window); explicit BasicWindowHelper(not_null<RpWidget*> window);
@ -57,6 +59,35 @@ private:
}; };
class DefaultWindowHelper final : public QObject, public BasicWindowHelper {
public:
explicit DefaultWindowHelper(not_null<RpWidget*> window);
not_null<RpWidget*> body() override;
void setTitle(const QString &title) override;
void setTitleStyle(const style::WindowTitle &st) override;
void setMinimumSize(QSize size) override;
void setFixedSize(QSize size) override;
void setGeometry(QRect rect) override;
protected:
bool eventFilter(QObject *obj, QEvent *e) override;
private:
void init();
[[nodiscard]] bool hasShadow() const;
[[nodiscard]] QMargins resizeArea() const;
[[nodiscard]] Qt::Edges edgesFromPos(const QPoint &pos) const;
void paintBorders(QPainter &p);
void updateWindowExtents();
void updateCursor(Qt::Edges edges);
const not_null<DefaultTitleWidget*> _title;
const not_null<RpWidget*> _body;
bool _extentsSet = false;
};
[[nodiscard]] std::unique_ptr<BasicWindowHelper> CreateSpecialWindowHelper( [[nodiscard]] std::unique_ptr<BasicWindowHelper> CreateSpecialWindowHelper(
not_null<RpWidget*> window); not_null<RpWidget*> window);
@ -65,7 +96,7 @@ private:
if (auto special = CreateSpecialWindowHelper(window)) { if (auto special = CreateSpecialWindowHelper(window)) {
return special; return special;
} }
return std::make_unique<BasicWindowHelper>(window); return std::make_unique<DefaultWindowHelper>(window);
} }
} // namespace Platform } // namespace Platform

View file

@ -0,0 +1,334 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/platform/ui_platform_window_title.h"
#include "ui/platform/ui_platform_utility.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/shadow.h"
#include "ui/ui_utility.h"
#include "styles/style_widgets.h"
#include "styles/palette.h"
#include "base/algorithm.h"
#include "base/event_filter.h"
#include <QtGui/QPainter>
#include <QtGui/QtEvents>
#include <QtGui/QWindow>
namespace Ui {
namespace Platform {
TitleControls::TitleControls(
not_null<RpWidget*> parent,
const style::WindowTitle &st,
Fn<void(bool maximized)> maximize)
: _st(&st)
, _minimize(parent, _st->minimize)
, _maximizeRestore(parent, _st->maximize)
, _close(parent, _st->close)
, _maximizedState(parent->windowState()
& (Qt::WindowMaximized | Qt::WindowFullScreen))
, _activeState(parent->isActiveWindow()) {
init(std::move(maximize));
_close->paintRequest(
) | rpl::start_with_next([=] {
const auto active = window()->isActiveWindow();
if (_activeState != active) {
_activeState = active;
updateButtonsState();
}
}, _close->lifetime());
}
void TitleControls::setStyle(const style::WindowTitle &st) {
_st = &st;
updateButtonsState();
}
not_null<const style::WindowTitle*> TitleControls::st() const {
return _st;
}
QRect TitleControls::geometry() const {
auto result = QRect();
const auto add = [&](auto &&control) {
if (!control->isHidden()) {
result = result.united(control->geometry());
}
};
add(_minimize);
add(_maximizeRestore);
add(_close);
return result;
}
not_null<RpWidget*> TitleControls::parent() const {
return static_cast<RpWidget*>(_close->parentWidget());
}
not_null<QWidget*> TitleControls::window() const {
return _close->window();
}
void TitleControls::init(Fn<void(bool maximized)> maximize) {
_minimize->setClickedCallback([=] {
window()->setWindowState(
window()->windowState() | Qt::WindowMinimized);
_minimize->clearState();
});
_minimize->setPointerCursor(false);
_maximizeRestore->setClickedCallback([=] {
if (maximize) {
maximize(!_maximizedState);
} else {
window()->setWindowState(_maximizedState
? Qt::WindowNoState
: Qt::WindowMaximized);
}
_maximizeRestore->clearState();
});
_maximizeRestore->setPointerCursor(false);
_close->setClickedCallback([=] {
window()->close();
_close->clearState();
});
_close->setPointerCursor(false);
parent()->widthValue(
) | rpl::start_with_next([=](int width) {
updateControlsPosition();
}, _close->lifetime());
const auto winIdEventFilter = std::make_shared<QObject*>(nullptr);
*winIdEventFilter = base::install_event_filter(
window(),
[=](not_null<QEvent*> e) {
if (!*winIdEventFilter || e->type() != QEvent::WinIdChange) {
return base::EventFilterResult::Continue;
}
QObject::connect(
window()->windowHandle(),
&QWindow::windowStateChanged,
[=](Qt::WindowState state) {
handleWindowStateChanged(state);
});
base::take(*winIdEventFilter)->deleteLater();
return base::EventFilterResult::Continue;
});
_activeState = parent()->isActiveWindow();
updateButtonsState();
}
void TitleControls::setResizeEnabled(bool enabled) {
_resizeEnabled = enabled;
updateControlsPosition();
}
void TitleControls::raise() {
_minimize->raise();
_maximizeRestore->raise();
_close->raise();
}
void TitleControls::updateControlsPosition() {
const auto controlsLayout = TitleControlsLayout();
auto controlsLeft = controlsLayout.left;
auto controlsRight = controlsLayout.right;
if (!_resizeEnabled) {
controlsLeft.erase(
ranges::remove(controlsLeft, Control::Maximize),
end(controlsLeft));
controlsRight.erase(
ranges::remove(controlsRight, Control::Maximize),
end(controlsRight));
}
if (ranges::contains(controlsLeft, Control::Minimize)
|| ranges::contains(controlsRight, Control::Minimize)) {
_minimize->show();
} else {
_minimize->hide();
}
if (ranges::contains(controlsLeft, Control::Maximize)
|| ranges::contains(controlsRight, Control::Maximize)) {
_maximizeRestore->show();
} else {
_maximizeRestore->hide();
}
if (ranges::contains(controlsLeft, Control::Close)
|| ranges::contains(controlsRight, Control::Close)) {
_close->show();
} else {
_close->hide();
}
updateControlsPositionBySide(controlsLeft, false);
updateControlsPositionBySide(controlsRight, true);
}
void TitleControls::updateControlsPositionBySide(
const std::vector<Control> &controls,
bool right) {
const auto preparedControls = right
? (ranges::view::reverse(controls) | ranges::to_vector)
: controls;
auto position = 0;
for (const auto &control : preparedControls) {
switch (control) {
case Control::Minimize:
if (right) {
_minimize->moveToRight(position, 0);
} else {
_minimize->moveToLeft(position, 0);
}
position += _minimize->width();
break;
case Control::Maximize:
if (right) {
_maximizeRestore->moveToRight(position, 0);
} else {
_maximizeRestore->moveToLeft(position, 0);
}
position += _maximizeRestore->width();
break;
case Control::Close:
if (right) {
_close->moveToRight(position, 0);
} else {
_close->moveToLeft(position, 0);
}
position += _close->width();
break;
}
}
}
void TitleControls::handleWindowStateChanged(Qt::WindowState state) {
if (state == Qt::WindowMinimized) {
return;
}
auto maximized = (state == Qt::WindowMaximized)
|| (state == Qt::WindowFullScreen);
if (_maximizedState != maximized) {
_maximizedState = maximized;
updateButtonsState();
}
}
void TitleControls::updateButtonsState() {
const auto minimize = _activeState
? &_st->minimizeIconActive
: &_st->minimize.icon;
const auto minimizeOver = _activeState
? &_st->minimizeIconActiveOver
: &_st->minimize.iconOver;
_minimize->setIconOverride(minimize, minimizeOver);
if (_maximizedState) {
const auto restore = _activeState
? &_st->restoreIconActive
: &_st->restoreIcon;
const auto restoreOver = _activeState
? &_st->restoreIconActiveOver
: &_st->restoreIconOver;
_maximizeRestore->setIconOverride(restore, restoreOver);
} else {
const auto maximize = _activeState
? &_st->maximizeIconActive
: &_st->maximize.icon;
const auto maximizeOver = _activeState
? &_st->maximizeIconActiveOver
: &_st->maximize.iconOver;
_maximizeRestore->setIconOverride(maximize, maximizeOver);
}
const auto close = _activeState
? &_st->closeIconActive
: &_st->close.icon;
const auto closeOver = _activeState
? &_st->closeIconActiveOver
: &_st->close.iconOver;
_close->setIconOverride(close, closeOver);
}
DefaultTitleWidget::DefaultTitleWidget(not_null<RpWidget*> parent)
: RpWidget(parent)
, _controls(this, st::defaultWindowTitle)
, _shadow(this, st::titleShadow) {
setAttribute(Qt::WA_OpaquePaintEvent);
}
not_null<const style::WindowTitle*> DefaultTitleWidget::st() const {
return _controls.st();
}
void DefaultTitleWidget::setText(const QString &text) {
window()->setWindowTitle(text);
}
void DefaultTitleWidget::setStyle(const style::WindowTitle &st) {
_controls.setStyle(st);
update();
}
void DefaultTitleWidget::setResizeEnabled(bool enabled) {
_controls.setResizeEnabled(enabled);
}
void DefaultTitleWidget::paintEvent(QPaintEvent *e) {
const auto active = window()->isActiveWindow();
QPainter(this).fillRect(
e->rect(),
active ? _controls.st()->bgActive : _controls.st()->bg);
}
void DefaultTitleWidget::resizeEvent(QResizeEvent *e) {
_shadow->setGeometry(0, height() - st::lineWidth, width(), st::lineWidth);
}
void DefaultTitleWidget::mousePressEvent(QMouseEvent *e) {
if (e->button() == Qt::LeftButton) {
_mousePressed = true;
} else if (e->button() == Qt::RightButton) {
ShowWindowMenu(window()->windowHandle());
}
}
void DefaultTitleWidget::mouseReleaseEvent(QMouseEvent *e) {
if (e->button() == Qt::LeftButton) {
_mousePressed = false;
}
}
void DefaultTitleWidget::mouseMoveEvent(QMouseEvent *e) {
if (_mousePressed) {
window()->windowHandle()->startSystemMove();
}
}
void DefaultTitleWidget::mouseDoubleClickEvent(QMouseEvent *e) {
const auto state = window()->windowState();
if (state & Qt::WindowMaximized) {
window()->setWindowState(state & ~Qt::WindowMaximized);
} else {
window()->setWindowState(state | Qt::WindowMaximized);
}
}
} // namespace Platform
} // namespace Ui

View file

@ -0,0 +1,100 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "ui/rp_widget.h"
#include "base/object_ptr.h"
#include <QtCore/QRect>
#include <QtCore/QPoint>
namespace style {
struct WindowTitle;
} // namespace style
namespace Ui {
class IconButton;
class PlainShadow;
namespace Platform {
class TitleControls final {
public:
TitleControls(
not_null<RpWidget*> parent,
const style::WindowTitle &st,
Fn<void(bool maximized)> maximize = nullptr);
void setStyle(const style::WindowTitle &st);
[[nodiscard]] not_null<const style::WindowTitle*> st() const;
[[nodiscard]] QRect geometry() const;
void setResizeEnabled(bool enabled);
void raise();
enum class Control {
Unknown,
Minimize,
Maximize,
Close,
};
struct Layout {
std::vector<Control> left;
std::vector<Control> right;
};
private:
[[nodiscard]] not_null<RpWidget*> parent() const;
[[nodiscard]] not_null<QWidget*> window() const;
void init(Fn<void(bool maximized)> maximize);
void updateButtonsState();
void updateControlsPosition();
void updateControlsPositionBySide(
const std::vector<Control> &controls,
bool right);
void handleWindowStateChanged(Qt::WindowState state = Qt::WindowNoState);
not_null<const style::WindowTitle*> _st;
object_ptr<Ui::IconButton> _minimize;
object_ptr<Ui::IconButton> _maximizeRestore;
object_ptr<Ui::IconButton> _close;
bool _maximizedState = false;
bool _activeState = false;
bool _resizeEnabled = true;
};
class DefaultTitleWidget : public RpWidget {
public:
explicit DefaultTitleWidget(not_null<RpWidget*> parent);
[[nodiscard]] not_null<const style::WindowTitle*> st() const;
void setText(const QString &text);
void setStyle(const style::WindowTitle &st);
void setResizeEnabled(bool enabled);
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseDoubleClickEvent(QMouseEvent *e) override;
private:
TitleControls _controls;
object_ptr<Ui::PlainShadow> _shadow;
bool _mousePressed = false;
};
} // namespace Platform
} // namespace Ui

View file

@ -44,5 +44,27 @@ void IgnoreAllActivation(not_null<QWidget*> widget) {
ShowWindow(handle, SW_SHOW); ShowWindow(handle, SW_SHOW);
} }
bool ShowWindowMenu(QWindow *window) {
const auto pos = QCursor::pos();
SendMessage(
HWND(window->winId()),
WM_SYSCOMMAND,
SC_MOUSEMENU,
MAKELPARAM(pos.x(), pos.y()));
return true;
}
TitleControls::Layout TitleControlsLayout() {
return TitleControls::Layout{
.right = {
TitleControls::Control::Minimize,
TitleControls::Control::Maximize,
TitleControls::Control::Close,
}
};
}
} // namespace Platform } // namespace Platform
} // namespace Ui } // namespace Ui

View file

@ -6,6 +6,8 @@
// //
#pragma once #pragma once
#include "ui/platform/ui_platform_utility.h"
#include <QtCore/QPoint> #include <QtCore/QPoint>
class QPainter; class QPainter;
@ -40,5 +42,17 @@ inline constexpr bool UseMainQueueGeneric() {
return true; return true;
} }
inline bool WindowExtentsSupported() {
return false;
}
inline bool SetWindowExtents(QWindow *window, const QMargins &extents) {
return false;
}
inline bool UnsetWindowExtents(QWindow *window) {
return false;
}
} // namespace Platform } // namespace Platform
} // namespace Ui } // namespace Ui

View file

@ -19,170 +19,6 @@
namespace Ui { namespace Ui {
namespace Platform { namespace Platform {
TitleControls::TitleControls(
not_null<RpWidget*> parent,
const style::WindowTitle &st,
Fn<void(bool maximized)> maximize)
: _st(&st)
, _minimize(parent, _st->minimize)
, _maximizeRestore(parent, _st->maximize)
, _close(parent, _st->close)
, _maximizedState(parent->windowState()
& (Qt::WindowMaximized | Qt::WindowFullScreen))
, _activeState(parent->isActiveWindow()) {
init(std::move(maximize));
_close->paintRequest(
) | rpl::start_with_next([=] {
const auto active = window()->isActiveWindow();
if (_activeState != active) {
_activeState = active;
updateButtonsState();
}
}, _close->lifetime());
}
void TitleControls::setStyle(const style::WindowTitle &st) {
_st = &st;
updateButtonsState();
}
not_null<const style::WindowTitle*> TitleControls::st() const {
return _st;
}
QRect TitleControls::geometry() const {
auto result = QRect();
const auto add = [&](auto &&control) {
if (!control->isHidden()) {
result = result.united(control->geometry());
}
};
add(_minimize);
add(_maximizeRestore);
add(_close);
return result;
}
not_null<RpWidget*> TitleControls::parent() const {
return static_cast<RpWidget*>(_close->parentWidget());
}
not_null<QWidget*> TitleControls::window() const {
return _close->window();
}
void TitleControls::init(Fn<void(bool maximized)> maximize) {
_minimize->setClickedCallback([=] {
window()->setWindowState(
window()->windowState() | Qt::WindowMinimized);
_minimize->clearState();
});
_minimize->setPointerCursor(false);
_maximizeRestore->setClickedCallback([=] {
if (maximize) {
maximize(!_maximizedState);
} else {
window()->setWindowState(_maximizedState
? Qt::WindowNoState
: Qt::WindowMaximized);
}
_maximizeRestore->clearState();
});
_maximizeRestore->setPointerCursor(false);
_close->setClickedCallback([=] {
window()->close();
_close->clearState();
});
_close->setPointerCursor(false);
parent()->widthValue(
) | rpl::start_with_next([=](int width) {
updateControlsPosition();
}, _close->lifetime());
window()->createWinId();
QObject::connect(
window()->windowHandle(),
&QWindow::windowStateChanged,
[=](Qt::WindowState state) { handleWindowStateChanged(state); });
_activeState = parent()->isActiveWindow();
updateButtonsState();
}
void TitleControls::setResizeEnabled(bool enabled) {
_resizeEnabled = enabled;
updateControlsVisibility();
}
void TitleControls::raise() {
_minimize->raise();
_maximizeRestore->raise();
_close->raise();
}
void TitleControls::updateControlsPosition() {
auto right = 0;
_close->moveToRight(right, 0); right += _close->width();
_maximizeRestore->moveToRight(right, 0);
if (_resizeEnabled) {
right += _maximizeRestore->width();
}
_minimize->moveToRight(right, 0);
}
void TitleControls::updateControlsVisibility() {
_maximizeRestore->setVisible(_resizeEnabled);
updateControlsPosition();
}
void TitleControls::handleWindowStateChanged(Qt::WindowState state) {
if (state == Qt::WindowMinimized) {
return;
}
auto maximized = (state == Qt::WindowMaximized)
|| (state == Qt::WindowFullScreen);
if (_maximizedState != maximized) {
_maximizedState = maximized;
updateButtonsState();
}
}
void TitleControls::updateButtonsState() {
const auto minimize = _activeState
? &_st->minimizeIconActive
: &_st->minimize.icon;
const auto minimizeOver = _activeState
? &_st->minimizeIconActiveOver
: &_st->minimize.iconOver;
_minimize->setIconOverride(minimize, minimizeOver);
if (_maximizedState) {
const auto restore = _activeState
? &_st->restoreIconActive
: &_st->restoreIcon;
const auto restoreOver = _activeState
? &_st->restoreIconActiveOver
: &_st->restoreIconOver;
_maximizeRestore->setIconOverride(restore, restoreOver);
} else {
const auto maximize = _activeState
? &_st->maximizeIconActive
: &_st->maximize.icon;
const auto maximizeOver = _activeState
? &_st->maximizeIconActiveOver
: &_st->maximize.iconOver;
_maximizeRestore->setIconOverride(maximize, maximizeOver);
}
const auto close = _activeState
? &_st->closeIconActive
: &_st->close.icon;
const auto closeOver = _activeState
? &_st->closeIconActiveOver
: &_st->close.iconOver;
_close->setIconOverride(close, closeOver);
}
TitleWidget::TitleWidget(not_null<RpWidget*> parent) TitleWidget::TitleWidget(not_null<RpWidget*> parent)
: RpWidget(parent) : RpWidget(parent)
, _controls(this, st::defaultWindowTitle) , _controls(this, st::defaultWindowTitle)

View file

@ -6,6 +6,7 @@
// //
#pragma once #pragma once
#include "ui/platform/ui_platform_window_title.h"
#include "ui/rp_widget.h" #include "ui/rp_widget.h"
#include "base/object_ptr.h" #include "base/object_ptr.h"
@ -38,41 +39,6 @@ enum class HitTestResult {
TopLeft, TopLeft,
}; };
class TitleControls final {
public:
TitleControls(
not_null<RpWidget*> parent,
const style::WindowTitle &st,
Fn<void(bool maximized)> maximize = nullptr);
void setStyle(const style::WindowTitle &st);
[[nodiscard]] not_null<const style::WindowTitle*> st() const;
[[nodiscard]] QRect geometry() const;
void setResizeEnabled(bool enabled);
void raise();
private:
[[nodiscard]] not_null<RpWidget*> parent() const;
[[nodiscard]] not_null<QWidget*> window() const;
void init(Fn<void(bool maximized)> maximize);
void updateControlsVisibility();
void updateButtonsState();
void updateControlsPosition();
void handleWindowStateChanged(Qt::WindowState state = Qt::WindowNoState);
not_null<const style::WindowTitle*> _st;
object_ptr<Ui::IconButton> _minimize;
object_ptr<Ui::IconButton> _maximizeRestore;
object_ptr<Ui::IconButton> _close;
bool _maximizedState = false;
bool _activeState = false;
bool _resizeEnabled = true;
};
class TitleWidget : public RpWidget { class TitleWidget : public RpWidget {
public: public:
explicit TitleWidget(not_null<RpWidget*> parent); explicit TitleWidget(not_null<RpWidget*> parent);

View file

@ -1582,3 +1582,17 @@ defaultWindowTitle: WindowTitle {
windowShadow: icon {{ "window_shadow", windowShadowFg }}; windowShadow: icon {{ "window_shadow", windowShadowFg }};
windowShadowShift: 1px; windowShadowShift: 1px;
callRadius: 6px;
callShadow: Shadow {
left: icon {{ "calls/call_shadow_left", windowShadowFg }};
topLeft: icon {{ "calls/call_shadow_top_left", windowShadowFg }};
top: icon {{ "calls/call_shadow_top", windowShadowFg }};
topRight: icon {{ "calls/call_shadow_top_left-flip_horizontal", windowShadowFg }};
right: icon {{ "calls/call_shadow_left-flip_horizontal", windowShadowFg }};
bottomRight: icon {{ "calls/call_shadow_top_left-flip_vertical-flip_horizontal", windowShadowFg }};
bottom: icon {{ "calls/call_shadow_top-flip_vertical", windowShadowFg }};
bottomLeft: icon {{ "calls/call_shadow_top_left-flip_vertical", windowShadowFg }};
extend: margins(9px, 8px, 9px, 10px);
fallback: windowShadowFgFallback;
}