diff --git a/CMakeLists.txt b/CMakeLists.txt index 770ba1a..b48b8f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,8 @@ PRIVATE ui/paint/blobs.h ui/paint/blobs_linear.cpp 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.h ui/platform/linux/ui_utility_linux.cpp @@ -95,6 +97,8 @@ PRIVATE ui/platform/win/ui_window_win.h ui/platform/win/ui_utility_win.cpp 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.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) 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 PUBLIC ${src_loc} diff --git a/icons/calls/call_shadow_left.png b/icons/calls/call_shadow_left.png new file mode 100644 index 0000000..74864ad Binary files /dev/null and b/icons/calls/call_shadow_left.png differ diff --git a/icons/calls/call_shadow_left@2x.png b/icons/calls/call_shadow_left@2x.png new file mode 100644 index 0000000..6a0e6e4 Binary files /dev/null and b/icons/calls/call_shadow_left@2x.png differ diff --git a/icons/calls/call_shadow_left@3x.png b/icons/calls/call_shadow_left@3x.png new file mode 100644 index 0000000..30257eb Binary files /dev/null and b/icons/calls/call_shadow_left@3x.png differ diff --git a/icons/calls/call_shadow_top.png b/icons/calls/call_shadow_top.png new file mode 100644 index 0000000..653e0af Binary files /dev/null and b/icons/calls/call_shadow_top.png differ diff --git a/icons/calls/call_shadow_top@2x.png b/icons/calls/call_shadow_top@2x.png new file mode 100644 index 0000000..47c672c Binary files /dev/null and b/icons/calls/call_shadow_top@2x.png differ diff --git a/icons/calls/call_shadow_top@3x.png b/icons/calls/call_shadow_top@3x.png new file mode 100644 index 0000000..350db04 Binary files /dev/null and b/icons/calls/call_shadow_top@3x.png differ diff --git a/icons/calls/call_shadow_top_left.png b/icons/calls/call_shadow_top_left.png new file mode 100644 index 0000000..baba493 Binary files /dev/null and b/icons/calls/call_shadow_top_left.png differ diff --git a/icons/calls/call_shadow_top_left@2x.png b/icons/calls/call_shadow_top_left@2x.png new file mode 100644 index 0000000..0d9672f Binary files /dev/null and b/icons/calls/call_shadow_top_left@2x.png differ diff --git a/icons/calls/call_shadow_top_left@3x.png b/icons/calls/call_shadow_top_left@3x.png new file mode 100644 index 0000000..260bb26 Binary files /dev/null and b/icons/calls/call_shadow_top_left@3x.png differ diff --git a/ui/platform/linux/ui_linux_wayland_integration.cpp b/ui/platform/linux/ui_linux_wayland_integration.cpp new file mode 100644 index 0000000..7cb7dd1 --- /dev/null +++ b/ui/platform/linux/ui_linux_wayland_integration.cpp @@ -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 + +#include +#include +#include + +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( + 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 diff --git a/ui/platform/linux/ui_linux_wayland_integration.h b/ui/platform/linux/ui_linux_wayland_integration.h new file mode 100644 index 0000000..bbc2331 --- /dev/null +++ b/ui/platform/linux/ui_linux_wayland_integration.h @@ -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 diff --git a/ui/platform/linux/ui_linux_wayland_integration_dummy.cpp b/ui/platform/linux/ui_linux_wayland_integration_dummy.cpp new file mode 100644 index 0000000..11abd62 --- /dev/null +++ b/ui/platform/linux/ui_linux_wayland_integration_dummy.cpp @@ -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 diff --git a/ui/platform/linux/ui_utility_linux.cpp b/ui/platform/linux/ui_utility_linux.cpp index 01566c5..8e9d80e 100644 --- a/ui/platform/linux/ui_utility_linux.cpp +++ b/ui/platform/linux/ui_utility_linux.cpp @@ -8,16 +8,201 @@ #include "ui/ui_log.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/flat_set.h" #include #include +#include #include #include +#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION +#include +#include +#include +#include +#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION + +Q_DECLARE_METATYPE(QMargins); + namespace Ui { 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(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(&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 PortalTitleControlsLayout() { + auto message = QDBusMessage::createMethodCall( + kXDGDesktopPortalService.utf16(), + kXDGDesktopPortalObjectPath.utf16(), + kSettingsPortalInterface.utf16(), + "Read"); + + message.setArguments({ + "org.gnome.desktop.wm.preferences", + "button-layout" + }); + + const QDBusReply reply = QDBusConnection::sessionBus().call( + message); + + if (!reply.isValid() || !reply.value().canConvert()) { + return std::nullopt; + } + + const auto valueVariant = qvariant_cast( + reply.value()).variant(); + + if (!valueVariant.canConvert()) { + return std::nullopt; + } + + const auto valueBySides = valueVariant.toString().split(':'); + + std::vector controlsLeft; + ranges::transform( + valueBySides[0].split(','), + ranges::back_inserter(controlsLeft), + GtkKeywordToTitleControl); + + std::vector 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() { return QApplication::activeWindow() != nullptr; @@ -50,5 +235,71 @@ bool TranslucentWindowsSupported(QPoint globalPosition) { void IgnoreAllActivation(not_null 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(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 Ui diff --git a/ui/platform/linux/ui_utility_linux.h b/ui/platform/linux/ui_utility_linux.h index c9081fc..c81a814 100644 --- a/ui/platform/linux/ui_utility_linux.h +++ b/ui/platform/linux/ui_utility_linux.h @@ -6,6 +6,8 @@ // #pragma once +#include "ui/platform/ui_platform_utility.h" + class QPainter; class QPaintEvent; diff --git a/ui/platform/mac/ui_utility_mac.h b/ui/platform/mac/ui_utility_mac.h index 7d4bffe..97f3c11 100644 --- a/ui/platform/mac/ui_utility_mac.h +++ b/ui/platform/mac/ui_utility_mac.h @@ -6,6 +6,7 @@ // #pragma once +#include "ui/platform/ui_platform_utility.h" #include "base/platform/base_platform_info.h" #include @@ -23,5 +24,21 @@ inline constexpr bool UseMainQueueGeneric() { 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 Ui diff --git a/ui/platform/mac/ui_utility_mac.mm b/ui/platform/mac/ui_utility_mac.mm index 46aeb11..fd8b492 100644 --- a/ui/platform/mac/ui_utility_mac.mm +++ b/ui/platform/mac/ui_utility_mac.mm @@ -99,5 +99,15 @@ void DrainMainQueue() { void IgnoreAllActivation(not_null widget) { } +TitleControls::Layout TitleControlsLayout() { + return TitleControls::Layout{ + .left = { + TitleControls::Control::Close, + TitleControls::Control::Minimize, + TitleControls::Control::Maximize, + } + }; +} + } // namespace Platform } // namespace Ui diff --git a/ui/platform/ui_platform_utility.h b/ui/platform/ui_platform_utility.h index a8eaedd..e8629a1 100644 --- a/ui/platform/ui_platform_utility.h +++ b/ui/platform/ui_platform_utility.h @@ -6,6 +6,8 @@ // #pragma once +#include "ui/platform/ui_platform_window_title.h" + class QPoint; class QPainter; class QPaintEvent; @@ -30,6 +32,12 @@ void IgnoreAllActivation(not_null widget); [[nodiscard]] constexpr bool UseMainQueueGeneric(); 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 Ui diff --git a/ui/platform/ui_platform_window.cpp b/ui/platform/ui_platform_window.cpp index c1b88eb..d6a6e18 100644 --- a/ui/platform/ui_platform_window.cpp +++ b/ui/platform/ui_platform_window.cpp @@ -6,21 +6,31 @@ // #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/shadow.h" +#include "ui/painter.h" +#include "styles/style_widgets.h" +#include "styles/style_layers.h" +#include #include #include namespace Ui { namespace Platform { +namespace { + +[[nodiscard]] const style::Shadow &Shadow() { + return st::callShadow; +} + +} // namespace BasicWindowHelper::BasicWindowHelper(not_null window) : _window(window) { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)) _window->setWindowFlag(Qt::Window); -#else // Qt >= 5.9 - _window->setWindowFlags(_window->windowFlags() | Qt::Window); -#endif // Qt >= 5.9 } not_null BasicWindowHelper::body() { @@ -100,8 +110,6 @@ void BasicWindowHelper::setupBodyTitleAreaEvents() { && (static_cast(e.get())->button() == Qt::LeftButton)) { _mousePressed = true; - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) || defined DESKTOP_APP_QT_PATCHED } else if (e->type() == QEvent::MouseMove) { const auto mouseEvent = static_cast(e.get()); if (_mousePressed @@ -109,7 +117,6 @@ void BasicWindowHelper::setupBodyTitleAreaEvents() { && !_window->isFullScreen() #endif // !Q_OS_WIN && (hitTest() & WindowTitleHitTestFlag::Move)) { - #ifdef Q_OS_WIN if (_window->isFullScreen()) { // On Windows we just jump out of fullscreen @@ -121,10 +128,258 @@ void BasicWindowHelper::setupBodyTitleAreaEvents() { _mousePressed = false; _window->windowHandle()->startSystemMove(); } -#endif // Qt >= 5.15 || DESKTOP_APP_QT_PATCHED } }, body()->lifetime()); } +DefaultWindowHelper::DefaultWindowHelper(not_null window) +: BasicWindowHelper(window) +, _title(Ui::CreateChild(window.get())) +, _body(Ui::CreateChild(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 e) { + if (e->type() == QEvent::MouseButtonPress) { + const auto mouseEvent = static_cast(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 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(window()) == static_cast(obj)) { + const auto mouseEvent = static_cast(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 Ui diff --git a/ui/platform/ui_platform_window.h b/ui/platform/ui_platform_window.h index 0844545..46e4e9c 100644 --- a/ui/platform/ui_platform_window.h +++ b/ui/platform/ui_platform_window.h @@ -20,6 +20,8 @@ using WindowTitleHitTestFlags = base::flags; namespace Platform { +class DefaultTitleWidget; + class BasicWindowHelper { public: explicit BasicWindowHelper(not_null window); @@ -57,6 +59,35 @@ private: }; +class DefaultWindowHelper final : public QObject, public BasicWindowHelper { +public: + explicit DefaultWindowHelper(not_null window); + + not_null 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 _title; + const not_null _body; + bool _extentsSet = false; + +}; + [[nodiscard]] std::unique_ptr CreateSpecialWindowHelper( not_null window); @@ -65,7 +96,7 @@ private: if (auto special = CreateSpecialWindowHelper(window)) { return special; } - return std::make_unique(window); + return std::make_unique(window); } } // namespace Platform diff --git a/ui/platform/ui_platform_window_title.cpp b/ui/platform/ui_platform_window_title.cpp new file mode 100644 index 0000000..e8de516 --- /dev/null +++ b/ui/platform/ui_platform_window_title.cpp @@ -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 +#include +#include + +namespace Ui { +namespace Platform { + +TitleControls::TitleControls( + not_null parent, + const style::WindowTitle &st, + Fn 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 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 TitleControls::parent() const { + return static_cast(_close->parentWidget()); +} + +not_null TitleControls::window() const { + return _close->window(); +} + +void TitleControls::init(Fn 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(nullptr); + *winIdEventFilter = base::install_event_filter( + window(), + [=](not_null 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 &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 parent) +: RpWidget(parent) +, _controls(this, st::defaultWindowTitle) +, _shadow(this, st::titleShadow) { + setAttribute(Qt::WA_OpaquePaintEvent); +} + +not_null 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 diff --git a/ui/platform/ui_platform_window_title.h b/ui/platform/ui_platform_window_title.h new file mode 100644 index 0000000..b4f14d8 --- /dev/null +++ b/ui/platform/ui_platform_window_title.h @@ -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 +#include + +namespace style { +struct WindowTitle; +} // namespace style + +namespace Ui { + +class IconButton; +class PlainShadow; + +namespace Platform { + +class TitleControls final { +public: + TitleControls( + not_null parent, + const style::WindowTitle &st, + Fn maximize = nullptr); + + void setStyle(const style::WindowTitle &st); + [[nodiscard]] not_null st() const; + [[nodiscard]] QRect geometry() const; + void setResizeEnabled(bool enabled); + void raise(); + + enum class Control { + Unknown, + Minimize, + Maximize, + Close, + }; + + struct Layout { + std::vector left; + std::vector right; + }; + +private: + [[nodiscard]] not_null parent() const; + [[nodiscard]] not_null window() const; + + void init(Fn maximize); + void updateButtonsState(); + void updateControlsPosition(); + void updateControlsPositionBySide( + const std::vector &controls, + bool right); + void handleWindowStateChanged(Qt::WindowState state = Qt::WindowNoState); + + not_null _st; + + object_ptr _minimize; + object_ptr _maximizeRestore; + object_ptr _close; + + bool _maximizedState = false; + bool _activeState = false; + bool _resizeEnabled = true; + +}; + +class DefaultTitleWidget : public RpWidget { +public: + explicit DefaultTitleWidget(not_null parent); + + [[nodiscard]] not_null 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 _shadow; + bool _mousePressed = false; + +}; + +} // namespace Platform +} // namespace Ui diff --git a/ui/platform/win/ui_utility_win.cpp b/ui/platform/win/ui_utility_win.cpp index 6af98ea..935564e 100644 --- a/ui/platform/win/ui_utility_win.cpp +++ b/ui/platform/win/ui_utility_win.cpp @@ -44,5 +44,27 @@ void IgnoreAllActivation(not_null widget) { 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 Ui diff --git a/ui/platform/win/ui_utility_win.h b/ui/platform/win/ui_utility_win.h index a7a1a14..d836b49 100644 --- a/ui/platform/win/ui_utility_win.h +++ b/ui/platform/win/ui_utility_win.h @@ -6,6 +6,8 @@ // #pragma once +#include "ui/platform/ui_platform_utility.h" + #include class QPainter; @@ -40,5 +42,17 @@ inline constexpr bool UseMainQueueGeneric() { 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 Ui diff --git a/ui/platform/win/ui_window_title_win.cpp b/ui/platform/win/ui_window_title_win.cpp index c28cf16..ba578f9 100644 --- a/ui/platform/win/ui_window_title_win.cpp +++ b/ui/platform/win/ui_window_title_win.cpp @@ -19,170 +19,6 @@ namespace Ui { namespace Platform { -TitleControls::TitleControls( - not_null parent, - const style::WindowTitle &st, - Fn 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 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 TitleControls::parent() const { - return static_cast(_close->parentWidget()); -} - -not_null TitleControls::window() const { - return _close->window(); -} - -void TitleControls::init(Fn 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 parent) : RpWidget(parent) , _controls(this, st::defaultWindowTitle) diff --git a/ui/platform/win/ui_window_title_win.h b/ui/platform/win/ui_window_title_win.h index 6515065..bc5fed6 100644 --- a/ui/platform/win/ui_window_title_win.h +++ b/ui/platform/win/ui_window_title_win.h @@ -6,6 +6,7 @@ // #pragma once +#include "ui/platform/ui_platform_window_title.h" #include "ui/rp_widget.h" #include "base/object_ptr.h" @@ -38,41 +39,6 @@ enum class HitTestResult { TopLeft, }; -class TitleControls final { -public: - TitleControls( - not_null parent, - const style::WindowTitle &st, - Fn maximize = nullptr); - - void setStyle(const style::WindowTitle &st); - [[nodiscard]] not_null st() const; - [[nodiscard]] QRect geometry() const; - void setResizeEnabled(bool enabled); - void raise(); - -private: - [[nodiscard]] not_null parent() const; - [[nodiscard]] not_null window() const; - - void init(Fn maximize); - void updateControlsVisibility(); - void updateButtonsState(); - void updateControlsPosition(); - void handleWindowStateChanged(Qt::WindowState state = Qt::WindowNoState); - - not_null _st; - - object_ptr _minimize; - object_ptr _maximizeRestore; - object_ptr _close; - - bool _maximizedState = false; - bool _activeState = false; - bool _resizeEnabled = true; - -}; - class TitleWidget : public RpWidget { public: explicit TitleWidget(not_null parent); diff --git a/ui/widgets/widgets.style b/ui/widgets/widgets.style index 6da3006..ec57cd1 100644 --- a/ui/widgets/widgets.style +++ b/ui/widgets/widgets.style @@ -1582,3 +1582,17 @@ defaultWindowTitle: WindowTitle { windowShadow: icon {{ "window_shadow", windowShadowFg }}; 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; +}