diff --git a/gyp/sources.txt b/gyp/sources.txt index 3c8a8db..4ecd79a 100644 --- a/gyp/sources.txt +++ b/gyp/sources.txt @@ -28,13 +28,20 @@ <(src_loc)/ui/layers/layer_manager.h <(src_loc)/ui/layers/layer_widget.cpp <(src_loc)/ui/layers/layer_widget.h +<(src_loc)/ui/platform/linux/ui_utility_linux.cpp +<(src_loc)/ui/platform/linux/ui_utility_linux.h +<(src_loc)/ui/platform/mac/ui_utility_mac.h +<(src_loc)/ui/platform/mac/ui_utility_mac.mm +<(src_loc)/ui/platform/win/ui_window_shadow_win.cpp +<(src_loc)/ui/platform/win/ui_window_shadow_win.h +<(src_loc)/ui/platform/win/ui_window_title_win.cpp +<(src_loc)/ui/platform/win/ui_window_title_win.h +<(src_loc)/ui/platform/win/ui_window_win.cpp +<(src_loc)/ui/platform/win/ui_window_win.h +<(src_loc)/ui/platform/win/ui_utility_win.cpp +<(src_loc)/ui/platform/win/ui_utility_win.h +<(src_loc)/ui/platform/ui_platform_window.h <(src_loc)/ui/platform/ui_platform_utility.h -<(src_loc)/ui/platform/linux/ui_platform_utility_linux.cpp -<(src_loc)/ui/platform/linux/ui_platform_utility_linux.h -<(src_loc)/ui/platform/mac/ui_platform_utility_mac.h -<(src_loc)/ui/platform/mac/ui_platform_utility_mac.mm -<(src_loc)/ui/platform/win/ui_platform_utility_win.cpp -<(src_loc)/ui/platform/win/ui_platform_utility_win.h <(src_loc)/ui/style/style_core.cpp <(src_loc)/ui/style/style_core.h <(src_loc)/ui/style/style_core_color.cpp @@ -88,6 +95,8 @@ <(src_loc)/ui/widgets/shadow.h <(src_loc)/ui/widgets/tooltip.cpp <(src_loc)/ui/widgets/tooltip.h +<(src_loc)/ui/widgets/window.cpp +<(src_loc)/ui/widgets/window.h <(src_loc)/ui/wrap/fade_wrap.cpp <(src_loc)/ui/wrap/fade_wrap.h <(src_loc)/ui/wrap/padding_wrap.cpp diff --git a/icons/title_button_close.png b/icons/title_button_close.png new file mode 100644 index 0000000..22cfe52 Binary files /dev/null and b/icons/title_button_close.png differ diff --git a/icons/title_button_close@2x.png b/icons/title_button_close@2x.png new file mode 100644 index 0000000..e43b4d8 Binary files /dev/null and b/icons/title_button_close@2x.png differ diff --git a/icons/title_button_close@3x.png b/icons/title_button_close@3x.png new file mode 100644 index 0000000..18c8496 Binary files /dev/null and b/icons/title_button_close@3x.png differ diff --git a/icons/title_button_maximize.png b/icons/title_button_maximize.png new file mode 100644 index 0000000..2759b3f Binary files /dev/null and b/icons/title_button_maximize.png differ diff --git a/icons/title_button_maximize@2x.png b/icons/title_button_maximize@2x.png new file mode 100644 index 0000000..00daaae Binary files /dev/null and b/icons/title_button_maximize@2x.png differ diff --git a/icons/title_button_maximize@3x.png b/icons/title_button_maximize@3x.png new file mode 100644 index 0000000..4d8073f Binary files /dev/null and b/icons/title_button_maximize@3x.png differ diff --git a/icons/title_button_minimize.png b/icons/title_button_minimize.png new file mode 100644 index 0000000..85934dd Binary files /dev/null and b/icons/title_button_minimize.png differ diff --git a/icons/title_button_minimize@2x.png b/icons/title_button_minimize@2x.png new file mode 100644 index 0000000..d70a497 Binary files /dev/null and b/icons/title_button_minimize@2x.png differ diff --git a/icons/title_button_minimize@3x.png b/icons/title_button_minimize@3x.png new file mode 100644 index 0000000..93e36c2 Binary files /dev/null and b/icons/title_button_minimize@3x.png differ diff --git a/icons/title_button_restore.png b/icons/title_button_restore.png new file mode 100644 index 0000000..c9f276b Binary files /dev/null and b/icons/title_button_restore.png differ diff --git a/icons/title_button_restore@2x.png b/icons/title_button_restore@2x.png new file mode 100644 index 0000000..63c3e1b Binary files /dev/null and b/icons/title_button_restore@2x.png differ diff --git a/icons/title_button_restore@3x.png b/icons/title_button_restore@3x.png new file mode 100644 index 0000000..44b9323 Binary files /dev/null and b/icons/title_button_restore@3x.png differ diff --git a/icons/window_shadow.png b/icons/window_shadow.png new file mode 100644 index 0000000..88f5ee9 Binary files /dev/null and b/icons/window_shadow.png differ diff --git a/icons/window_shadow@2x.png b/icons/window_shadow@2x.png new file mode 100644 index 0000000..d2e968f Binary files /dev/null and b/icons/window_shadow@2x.png differ diff --git a/icons/window_shadow@3x.png b/icons/window_shadow@3x.png new file mode 100644 index 0000000..6864b17 Binary files /dev/null and b/icons/window_shadow@3x.png differ diff --git a/ui/layers/layer_widget.cpp b/ui/layers/layer_widget.cpp index e6bb7dd..6a05357 100644 --- a/ui/layers/layer_widget.cpp +++ b/ui/layers/layer_widget.cpp @@ -697,8 +697,8 @@ void LayerStackWidget::showSpecialLayer( } bool LayerStackWidget::showSectionInternal( - not_null memento, - const Window::SectionShow ¶ms) { + not_null<::Window::SectionMemento*> memento, + const ::Window::SectionShow ¶ms) { if (_specialLayer) { return _specialLayer->showSectionInternal(memento, params); } diff --git a/ui/layers/layer_widget.h b/ui/layers/layer_widget.h index 6fa7116..02f6b1c 100644 --- a/ui/layers/layer_widget.h +++ b/ui/layers/layer_widget.h @@ -57,8 +57,8 @@ public: return false; } virtual bool showSectionInternal( - not_null memento, - const Window::SectionShow ¶ms) { + not_null<::Window::SectionMemento*> memento, + const ::Window::SectionShow ¶ms) { return false; } virtual bool closeByOutsideClick() const { @@ -118,8 +118,8 @@ public: void removeBodyCache(); bool showSectionInternal( - not_null memento, - const Window::SectionShow ¶ms); + not_null<::Window::SectionMemento*> memento, + const ::Window::SectionShow ¶ms); bool layerShown() const; diff --git a/ui/platform/linux/ui_platform_utility_linux.cpp b/ui/platform/linux/ui_utility_linux.cpp similarity index 96% rename from ui/platform/linux/ui_platform_utility_linux.cpp rename to ui/platform/linux/ui_utility_linux.cpp index 6d86f53..0bd8f94 100644 --- a/ui/platform/linux/ui_platform_utility_linux.cpp +++ b/ui/platform/linux/ui_utility_linux.cpp @@ -4,7 +4,7 @@ // For license and copyright information please follow this link: // https://github.com/desktop-app/legal/blob/master/LEGAL // -#include "ui/platform/linux/ui_platform_utility_linux.h" +#include "ui/platform/linux/ui_utility_linux.h" #include "base/flat_set.h" #include "ui/ui_log.h" diff --git a/ui/platform/linux/ui_platform_utility_linux.h b/ui/platform/linux/ui_utility_linux.h similarity index 100% rename from ui/platform/linux/ui_platform_utility_linux.h rename to ui/platform/linux/ui_utility_linux.h diff --git a/ui/platform/mac/ui_platform_utility_mac.h b/ui/platform/mac/ui_utility_mac.h similarity index 100% rename from ui/platform/mac/ui_platform_utility_mac.h rename to ui/platform/mac/ui_utility_mac.h diff --git a/ui/platform/mac/ui_platform_utility_mac.mm b/ui/platform/mac/ui_utility_mac.mm similarity index 98% rename from ui/platform/mac/ui_platform_utility_mac.mm rename to ui/platform/mac/ui_utility_mac.mm index e4492c6..e0f06bf 100644 --- a/ui/platform/mac/ui_platform_utility_mac.mm +++ b/ui/platform/mac/ui_utility_mac.mm @@ -4,7 +4,7 @@ // For license and copyright information please follow this link: // https://github.com/desktop-app/legal/blob/master/LEGAL // -#include "ui/platform/mac/ui_platform_utility_mac.h" +#include "ui/platform/mac/ui_platform_mac.h" #include "ui/integration.h" diff --git a/ui/platform/ui_platform_utility.h b/ui/platform/ui_platform_utility.h index 83fcba7..d253c91 100644 --- a/ui/platform/ui_platform_utility.h +++ b/ui/platform/ui_platform_utility.h @@ -35,9 +35,9 @@ void DrainMainQueue(); // Needed only if UseMainQueueGeneric() is false. // Platform dependent implementations. #ifdef Q_OS_MAC -#include "ui/platform/mac/ui_platform_utility_mac.h" +#include "ui/platform/mac/ui_utility_mac.h" #elif defined Q_OS_LINUX // Q_OS_MAC -#include "ui/platform/linux/ui_platform_utility_linux.h" +#include "ui/platform/linux/ui_utility_linux.h" #elif defined Q_OS_WINRT || defined Q_OS_WIN // Q_OS_MAC || Q_OS_LINUX -#include "ui/platform/win/ui_platform_utility_win.h" +#include "ui/platform/win/ui_utility_win.h" #endif // Q_OS_MAC || Q_OS_LINUX || Q_OS_WINRT || Q_OS_WIN diff --git a/ui/platform/ui_platform_window.h b/ui/platform/ui_platform_window.h new file mode 100644 index 0000000..3069163 --- /dev/null +++ b/ui/platform/ui_platform_window.h @@ -0,0 +1,28 @@ +// 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 + +namespace Ui { + +class RpWidget; + +namespace Platform { + +class BasicWindowHelper { +public: + [[nodiscard]] virtual not_null body() = 0; + virtual void setTitle(const QString &title) = 0; + virtual void setSizeMin(QSize size) = 0; + virtual ~BasicWindowHelper() = default; + +}; + +[[nodiscard]] std::unique_ptr CreateWindowHelper( + not_null window); + +} // namespace Platform +} // namespace Ui diff --git a/ui/platform/win/ui_platform_utility_win.cpp b/ui/platform/win/ui_utility_win.cpp similarity index 94% rename from ui/platform/win/ui_platform_utility_win.cpp rename to ui/platform/win/ui_utility_win.cpp index b281cf2..f6fd431 100644 --- a/ui/platform/win/ui_platform_utility_win.cpp +++ b/ui/platform/win/ui_utility_win.cpp @@ -4,7 +4,7 @@ // For license and copyright information please follow this link: // https://github.com/desktop-app/legal/blob/master/LEGAL // -#include "ui/platform/win/ui_platform_utility_win.h" +#include "ui/platform/win/ui_utility_win.h" #include diff --git a/ui/platform/win/ui_platform_utility_win.h b/ui/platform/win/ui_utility_win.h similarity index 100% rename from ui/platform/win/ui_platform_utility_win.h rename to ui/platform/win/ui_utility_win.h diff --git a/ui/platform/win/ui_window_shadow_win.cpp b/ui/platform/win/ui_window_shadow_win.cpp new file mode 100644 index 0000000..478c780 --- /dev/null +++ b/ui/platform/win/ui_window_shadow_win.cpp @@ -0,0 +1,612 @@ +// 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/win/ui_window_shadow_win.h" + +#include "ui/rp_widget.h" +#include "ui/platform/win/ui_window_win.h" +#include "styles/style_widgets.h" + +#include +#include +#include + +#include + +namespace Ui { +namespace Platform { +namespace { + +base::flat_map> ShadowByHandle; + +} // namespace + +WindowShadow::WindowShadow(not_null window, QColor color) +: _window(window) +, _handle(GetWindowHandle(window)) { + window->hide(); + init(color); +} + +WindowShadow::~WindowShadow() { + destroy(); +} + +void WindowShadow::setColor(QColor value) { + _r = value.red(); + _g = value.green(); + _b = value.blue(); + if (!working()) { + return; + } + + auto brush = getBrush(_alphas[0]); + for (auto i = 0; i != 4; ++i) { + auto graphics = Gdiplus::Graphics(_contexts[i]); + graphics.SetCompositingMode(Gdiplus::CompositingModeSourceCopy); + if ((i % 2) && _h || !(i % 2) && _w) { + const auto width = (i % 2) ? _size : _w; + const auto height = (i % 2) ? _h : _size; + graphics.FillRectangle(&brush, 0, 0, width, height); + } + } + initCorners(); + + _x = _y = _w = _h = 0; + update(Change::Moved | Change::Resized); +} + +bool WindowShadow::working() const { + return (_handle != nullptr) && (_handles[0] != nullptr); +} + +void WindowShadow::destroy() { + for (int i = 0; i < 4; ++i) { + if (_contexts[i]) { + DeleteDC(_contexts[i]); + _contexts[i] = nullptr; + } + if (_bitmaps[i]) { + DeleteObject(_bitmaps[i]); + _bitmaps[i] = nullptr; + } + if (_handles[i]) { + ShadowByHandle.remove(_handles[i]); + DestroyWindow(_handles[i]); + _handles[i] = nullptr; + } + } + if (_screenContext) { + ReleaseDC(nullptr, _screenContext); + _screenContext = nullptr; + } +} + +void WindowShadow::init(QColor color) { + if (!_handle) { + return; + } + + initBlend(); + + _fullsize = st::windowShadow.width(); + _shift = st::windowShadowShift; + auto cornersImage = QImage( + QSize(_fullsize, _fullsize), + QImage::Format_ARGB32_Premultiplied); + cornersImage.fill(QColor(0, 0, 0)); + { + QPainter p(&cornersImage); + p.setCompositionMode(QPainter::CompositionMode_Source); + st::windowShadow.paint(p, 0, 0, _fullsize, QColor(255, 255, 255)); + } + if (style::RightToLeft()) { + cornersImage = cornersImage.mirrored(true, false); + } + const auto pixels = cornersImage.bits(); + const auto pixel = [&](int x, int y) { + if (x < 0 || y < 0) { + return 0; + } + const auto data = pixels + + (cornersImage.bytesPerLine() * y) + + (sizeof(uint32) * x); + return int(data[0]); + }; + + _metaSize = _fullsize + 2 * _shift; + _alphas.reserve(_metaSize); + _colors.reserve(_metaSize * _metaSize); + for (auto j = 0; j != _metaSize; ++j) { + for (auto i = 0; i != _metaSize; ++i) { + const auto value = pixel(i - 2 * _shift, j - 2 * _shift); + _colors.push_back(uchar(std::max(value, 1))); + } + } + auto previous = uchar(0); + for (auto i = 0; i != _metaSize; ++i) { + const auto alpha = _colors[(_metaSize - 1) * _metaSize + i]; + if (alpha < previous) { + break; + } + _alphas.push_back(alpha); + previous = alpha; + } + _size = _alphas.size() - 2 * _shift; + + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + ULONG_PTR gdiplusToken; + Gdiplus::Status gdiRes = Gdiplus::GdiplusStartup( + &gdiplusToken, + &gdiplusStartupInput, + NULL); + + if (gdiRes != Gdiplus::Ok) { + return; + } + + _screenContext = GetDC(nullptr); + if (!_screenContext) { + return; + } + + const auto avail = QApplication::desktop()->availableGeometry(); + _widthMax = std::max(avail.width(), 1); + _heightMax = std::max(avail.height(), 1); + + const auto instance = (HINSTANCE)GetModuleHandle(nullptr); + for (auto i = 0; i != 4; ++i) { + const auto className = "WindowShadow" + QString::number(i); + const auto wcharClassName = className.toStdWString(); + auto wc = WNDCLASSEX(); + + wc.cbSize = sizeof(wc); + wc.style = 0; + wc.lpfnWndProc = WindowCallback; + wc.cbClsExtra = 0; + wc.cbWndExtra = 0; + wc.hInstance = instance; + wc.hIcon = 0; + wc.hCursor = 0; + wc.hbrBackground = 0; + wc.lpszMenuName = NULL; + wc.lpszClassName = wcharClassName.c_str(); + wc.hIconSm = 0; + if (!RegisterClassEx(&wc)) { + return; + } + + _handles[i] = CreateWindowEx( + WS_EX_LAYERED | WS_EX_TOOLWINDOW, + wcharClassName.c_str(), + 0, + WS_POPUP, + 0, + 0, + 0, + 0, + 0, + 0, + instance, + 0); + if (!_handles[i]) { + destroy(); + return; + } + ShadowByHandle.emplace(_handles[i], this); + SetWindowLong(_handles[i], GWL_HWNDPARENT, (LONG)_handle); + + _contexts[i] = CreateCompatibleDC(_screenContext); + if (!_contexts[i]) { + destroy(); + return; + } + + const auto width = (i % 2) ? _size : _widthMax; + const auto height = (i % 2) ? _heightMax : _size; + _bitmaps[i] = CreateCompatibleBitmap(_screenContext, width, height); + if (!_bitmaps[i]) { + return; + } + + SelectObject(_contexts[i], _bitmaps[i]); + } + setColor(color); +} + +void WindowShadow::initCorners(Directions directions) { + const auto hor = (directions & Direction::Horizontal); + const auto ver = (directions & Direction::Vertical); + auto graphics0 = Gdiplus::Graphics(_contexts[0]); + auto graphics1 = Gdiplus::Graphics(_contexts[1]); + auto graphics2 = Gdiplus::Graphics(_contexts[2]); + auto graphics3 = Gdiplus::Graphics(_contexts[3]); + graphics0.SetCompositingMode(Gdiplus::CompositingModeSourceCopy); + graphics1.SetCompositingMode(Gdiplus::CompositingModeSourceCopy); + graphics2.SetCompositingMode(Gdiplus::CompositingModeSourceCopy); + graphics3.SetCompositingMode(Gdiplus::CompositingModeSourceCopy); + + auto brush = getBrush(_alphas[0]); + if (hor) { + graphics0.FillRectangle(&brush, 0, 0, _fullsize - (_size - _shift), 2 * _shift); + } + + if (ver) { + graphics1.FillRectangle(&brush, 0, 0, _size, 2 * _shift); + graphics3.FillRectangle(&brush, 0, 0, _size, 2 * _shift); + graphics1.FillRectangle(&brush, _size - _shift, 2 * _shift, _shift, _fullsize); + graphics3.FillRectangle(&brush, 0, 2 * _shift, _shift, _fullsize); + } + + if (hor) { + for (int j = 2 * _shift; j < _size; ++j) { + for (int k = 0; k < _fullsize - (_size - _shift); ++k) { + brush.SetColor(getColor(_colors[j * _metaSize + k + (_size + _shift)])); + graphics0.FillRectangle(&brush, k, j, 1, 1); + graphics2.FillRectangle(&brush, k, _size - (j - 2 * _shift) - 1, 1, 1); + } + } + for (int j = _size; j < _size + 2 * _shift; ++j) { + for (int k = 0; k < _fullsize - (_size - _shift); ++k) { + brush.SetColor(getColor(_colors[j * _metaSize + k + (_size + _shift)])); + graphics2.FillRectangle(&brush, k, _size - (j - 2 * _shift) - 1, 1, 1); + } + } + } + if (ver) { + for (int j = 2 * _shift; j < _fullsize + 2 * _shift; ++j) { + for (int k = _shift; k < _size; ++k) { + brush.SetColor(getColor(_colors[j * _metaSize + (k + _shift)])); + graphics1.FillRectangle(&brush, _size - k - 1, j, 1, 1); + graphics3.FillRectangle(&brush, k, j, 1, 1); + } + } + } +} + +void WindowShadow::verCorners(int h, Gdiplus::Graphics *pgraphics1, Gdiplus::Graphics *pgraphics3) { + auto brush = getBrush(_alphas[0]); + pgraphics1->FillRectangle(&brush, _size - _shift, h - _fullsize, _shift, _fullsize); + pgraphics3->FillRectangle(&brush, 0, h - _fullsize, _shift, _fullsize); + for (int j = 0; j < _fullsize; ++j) { + for (int k = _shift; k < _size; ++k) { + brush.SetColor(getColor(_colors[(j + 2 * _shift) * _metaSize + k + _shift])); + pgraphics1->FillRectangle(&brush, _size - k - 1, h - j - 1, 1, 1); + pgraphics3->FillRectangle(&brush, k, h - j - 1, 1, 1); + } + } +} + +void WindowShadow::horCorners(int w, Gdiplus::Graphics *pgraphics0, Gdiplus::Graphics *pgraphics2) { + auto brush = getBrush(_alphas[0]); + pgraphics0->FillRectangle(&brush, w - 2 * _size - (_fullsize - (_size - _shift)), 0, _fullsize - (_size - _shift), 2 * _shift); + for (int j = 2 * _shift; j < _size; ++j) { + for (int k = 0; k < _fullsize - (_size - _shift); ++k) { + brush.SetColor(getColor(_colors[j * _metaSize + k + (_size + _shift)])); + pgraphics0->FillRectangle(&brush, w - 2 * _size - k - 1, j, 1, 1); + pgraphics2->FillRectangle(&brush, w - 2 * _size - k - 1, _size - (j - 2 * _shift) - 1, 1, 1); + } + } + for (int j = _size; j < _size + 2 * _shift; ++j) { + for (int k = 0; k < _fullsize - (_size - _shift); ++k) { + brush.SetColor(getColor(_colors[j * _metaSize + k + (_size + _shift)])); + pgraphics2->FillRectangle(&brush, w - 2 * _size - k - 1, _size - (j - 2 * _shift) - 1, 1, 1); + } + } +} + +Gdiplus::Color WindowShadow::getColor(uchar alpha) const { + return Gdiplus::Color(BYTE(alpha), _r, _g, _b); +} + +Gdiplus::SolidBrush WindowShadow::getBrush(uchar alpha) const { + return Gdiplus::SolidBrush(getColor(alpha)); +} + +Gdiplus::Pen WindowShadow::getPen(uchar alpha) const { + return Gdiplus::Pen(getColor(alpha)); +} + +void WindowShadow::update(Changes changes, WINDOWPOS *pos) { + if (!working()) { + return; + } + + if (changes == Changes(Change::Activate)) { + for (int i = 0; i < 4; ++i) { + SetWindowPos(_handles[i], _handle, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + } + return; + } + + if (changes & Change::Hidden) { + if (!_hidden) { + for (int i = 0; i < 4; ++i) { + _hidden = true; + ShowWindow(_handles[i], SW_HIDE); + } + } + return; + } + if (_window->isHidden()) { + return; + } + + int x = _x, y = _y, w = _w, h = _h; + if (pos && (!(pos->flags & SWP_NOMOVE) || !(pos->flags & SWP_NOSIZE) || !(pos->flags & SWP_NOREPOSITION))) { + if (!(pos->flags & SWP_NOMOVE)) { + x = pos->x - _size; + y = pos->y - _size; + } else if (pos->flags & SWP_NOSIZE) { + for (int i = 0; i < 4; ++i) { + SetWindowPos(_handles[i], _handle, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + } + return; + } + if (!(pos->flags & SWP_NOSIZE)) { + w = pos->cx + 2 * _size; + h = pos->cy + 2 * _size; + } + } else { + RECT r; + GetWindowRect(_handle, &r); + x = r.left - _size; + y = r.top - _size; + w = r.right + _size - x; + h = r.bottom + _size - y; + } + if (h < 2 * _fullsize + 2 * _shift) { + h = 2 * _fullsize + 2 * _shift; + } + if (w < 2 * (_fullsize + _shift)) { + w = 2 * (_fullsize + _shift); + } + + if (w != _w) { + int from = (_w > 2 * (_fullsize + _shift)) ? (_w - _size - _fullsize - _shift) : (_fullsize - (_size - _shift)); + int to = w - _size - _fullsize - _shift; + if (w > _widthMax) { + from = _fullsize - (_size - _shift); + do { + _widthMax *= 2; + } while (w > _widthMax); + for (int i = 0; i < 4; i += 2) { + DeleteObject(_bitmaps[i]); + _bitmaps[i] = CreateCompatibleBitmap(_screenContext, _widthMax, _size); + SelectObject(_contexts[i], _bitmaps[i]); + } + initCorners(Direction::Horizontal); + } + Gdiplus::Graphics graphics0(_contexts[0]); + Gdiplus::Graphics graphics2(_contexts[2]); + graphics0.SetCompositingMode(Gdiplus::CompositingModeSourceCopy); + graphics2.SetCompositingMode(Gdiplus::CompositingModeSourceCopy); + auto brush = getBrush(_alphas[0]); + if (to > from) { + graphics0.FillRectangle(&brush, from, 0, to - from, 2 * _shift); + for (int i = 2 * _shift; i < _size; ++i) { + auto pen = getPen(_alphas[i]); + graphics0.DrawLine(&pen, from, i, to, i); + graphics2.DrawLine(&pen, from, _size - (i - 2 * _shift) - 1, to, _size - (i - 2 * _shift) - 1); + } + for (int i = _size; i < _size + 2 * _shift; ++i) { + auto pen = getPen(_alphas[i]); + graphics2.DrawLine(&pen, from, _size - (i - 2 * _shift) - 1, to, _size - (i - 2 * _shift) - 1); + } + } + if (_w > w) { + graphics0.FillRectangle(&brush, w - _size - _fullsize - _shift, 0, _fullsize - (_size - _shift), _size); + graphics2.FillRectangle(&brush, w - _size - _fullsize - _shift, 0, _fullsize - (_size - _shift), _size); + } + horCorners(w, &graphics0, &graphics2); + POINT p0 = { x + _size, y }, p2 = { x + _size, y + h - _size }, f = { 0, 0 }; + SIZE s = { w - 2 * _size, _size }; + updateWindow(0, &p0, &s); + updateWindow(2, &p2, &s); + } else if (x != _x || y != _y) { + POINT p0 = { x + _size, y }, p2 = { x + _size, y + h - _size }; + updateWindow(0, &p0); + updateWindow(2, &p2); + } else if (h != _h) { + POINT p2 = { x + _size, y + h - _size }; + updateWindow(2, &p2); + } + + if (h != _h) { + int from = (_h > 2 * _fullsize + 2 * _shift) ? (_h - _fullsize) : (_fullsize + 2 * _shift); + int to = h - _fullsize; + if (h > _heightMax) { + from = (_fullsize + 2 * _shift); + do { + _heightMax *= 2; + } while (h > _heightMax); + for (int i = 1; i < 4; i += 2) { + DeleteObject(_bitmaps[i]); + _bitmaps[i] = CreateCompatibleBitmap(_contexts[i], _size, _heightMax); + SelectObject(_contexts[i], _bitmaps[i]); + } + initCorners(Direction::Vertical); + } + Gdiplus::Graphics graphics1(_contexts[1]), graphics3(_contexts[3]); + graphics1.SetCompositingMode(Gdiplus::CompositingModeSourceCopy); + graphics3.SetCompositingMode(Gdiplus::CompositingModeSourceCopy); + + auto brush = getBrush(_alphas[0]); + if (to > from) { + graphics1.FillRectangle(&brush, _size - _shift, from, _shift, to - from); + graphics3.FillRectangle(&brush, 0, from, _shift, to - from); + for (int i = 2 * _shift; i < _size + _shift; ++i) { + auto pen = getPen(_alphas[i]); + graphics1.DrawLine(&pen, _size + _shift - i - 1, from, _size + _shift - i - 1, to); + graphics3.DrawLine(&pen, i - _shift, from, i - _shift, to); + } + } + if (_h > h) { + graphics1.FillRectangle(&brush, 0, h - _fullsize, _size, _fullsize); + graphics3.FillRectangle(&brush, 0, h - _fullsize, _size, _fullsize); + } + verCorners(h, &graphics1, &graphics3); + + POINT p1 = { x + w - _size, y }, p3 = { x, y }, f = { 0, 0 }; + SIZE s = { _size, h }; + updateWindow(1, &p1, &s); + updateWindow(3, &p3, &s); + } else if (x != _x || y != _y) { + POINT p1 = { x + w - _size, y }, p3 = { x, y }; + updateWindow(1, &p1); + updateWindow(3, &p3); + } else if (w != _w) { + POINT p1 = { x + w - _size, y }; + updateWindow(1, &p1); + } + _x = x; + _y = y; + _w = w; + _h = h; + + if (_hidden && (changes & Change::Shown)) { + for (int i = 0; i < 4; ++i) { + SetWindowPos( + _handles[i], + _handle, + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW | SWP_NOACTIVATE); + } + _hidden = false; + } +} + +void WindowShadow::initBlend() { + _blend.AlphaFormat = AC_SRC_ALPHA; + _blend.SourceConstantAlpha = 255; + _blend.BlendFlags = 0; + _blend.BlendOp = AC_SRC_OVER; +} + +void WindowShadow::updateWindow(int i, POINT *p, SIZE *s) { + static POINT f = { 0, 0 }; + if (s) { + UpdateLayeredWindow( + _handles[i], + (s ? _screenContext : nullptr), + p, + s, + (s ? _contexts[i] : nullptr), + (s ? (&f) : nullptr), + _noKeyColor, + &_blend, + ULW_ALPHA); + } else { + SetWindowPos( + _handles[i], + 0, + p->x, + p->y, + 0, + 0, + SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER); + } +} + +LRESULT CALLBACK WindowShadow::WindowCallback( + HWND hwnd, + UINT msg, + WPARAM wParam, + LPARAM lParam) { + const auto i = ShadowByHandle.find(hwnd); + return (i != end(ShadowByHandle)) + ? i->second->windowCallback(hwnd, msg, wParam, lParam) + : DefWindowProc(hwnd, msg, wParam, lParam); +} + +LRESULT WindowShadow::windowCallback( + HWND hwnd, + UINT msg, + WPARAM wParam, + LPARAM lParam) { + if (!working()) { + return DefWindowProc(hwnd, msg, wParam, lParam); + } + + switch (msg) { + case WM_CLOSE: + _window->close(); + return 0; + + case WM_NCHITTEST: { + const auto xPos = GET_X_LPARAM(lParam); + const auto yPos = GET_Y_LPARAM(lParam); + if (hwnd == _handles[0]) { + return HTTOP; + } else if (hwnd == _handles[1]) { + return (yPos < _y + _size) + ? HTTOPRIGHT + : (yPos >= _y + _h - _size) + ? HTBOTTOMRIGHT + : HTRIGHT; + } else if (hwnd == _handles[2]) { + return HTBOTTOM; + } else if (hwnd == _handles[3]) { + return (yPos < _y + _size) + ? HTTOPLEFT + : (yPos >= _y + _h - _size) + ? HTBOTTOMLEFT + : HTLEFT; + } else { + Unexpected("Handle in WindowShadow::windowCallback."); + } + } break; + + case WM_NCACTIVATE: + return DefWindowProc(hwnd, msg, wParam, lParam); + + case WM_NCLBUTTONDOWN: + case WM_NCLBUTTONUP: + case WM_NCLBUTTONDBLCLK: + case WM_NCMBUTTONDOWN: + case WM_NCMBUTTONUP: + case WM_NCMBUTTONDBLCLK: + case WM_NCRBUTTONDOWN: + case WM_NCRBUTTONUP: + case WM_NCRBUTTONDBLCLK: + case WM_NCXBUTTONDOWN: + case WM_NCXBUTTONUP: + case WM_NCXBUTTONDBLCLK: + case WM_NCMOUSEHOVER: + case WM_NCMOUSELEAVE: + case WM_NCMOUSEMOVE: + case WM_NCPOINTERUPDATE: + case WM_NCPOINTERDOWN: + case WM_NCPOINTERUP: + if (msg == WM_NCLBUTTONDOWN) { + ::SetForegroundWindow(_handle); + } + return SendMessage(_handle, msg, wParam, lParam); + case WM_ACTIVATE: + if (wParam == WA_ACTIVE) { + if ((HWND)lParam != _handle) { + ::SetForegroundWindow(hwnd); + ::SetWindowPos( + _handle, + hwnd, + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE); + } + } + return DefWindowProc(hwnd, msg, wParam, lParam); + default: + return DefWindowProc(hwnd, msg, wParam, lParam); + } +} + +} // namespace Platform +} // namespace Ui diff --git a/ui/platform/win/ui_window_shadow_win.h b/ui/platform/win/ui_window_shadow_win.h new file mode 100644 index 0000000..8d6f581 --- /dev/null +++ b/ui/platform/win/ui_window_shadow_win.h @@ -0,0 +1,110 @@ +// 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 "base/flags.h" + +#include + +#define min(a, b) ((a) < (b) ? (a) : (b)) +#define max(a, b) ((a) < (b) ? (b) : (a)) +#include +#undef min +#undef max + +class QColor; + +namespace Ui { + +class RpWidget; + +namespace Platform { + +class WindowShadow final { +public: + WindowShadow(not_null window, QColor color); + ~WindowShadow(); + + enum class Change { + Moved = (1 << 0), + Resized = (1 << 1), + Activate = (1 << 2), + Deactivate = (1 << 3), + Hidden = (1 << 4), + Shown = (1 << 5), + }; + friend inline constexpr bool is_flag_type(Change) { return true; }; + using Changes = base::flags; + + void setColor(QColor color); + void update(Changes changes, WINDOWPOS *pos = nullptr); + void updateWindow(int i, POINT *p, SIZE *s = nullptr); + +private: + enum class Direction { + Horizontal = (1 << 0), + Vertical = (1 << 1), + All = Horizontal | Vertical, + }; + friend inline constexpr bool is_flag_type(Direction) { return true; }; + using Directions = base::flags; + + static LRESULT CALLBACK WindowCallback( + HWND hwnd, + UINT msg, + WPARAM wParam, + LPARAM lParam); + + LRESULT windowCallback( + HWND hwnd, + UINT msg, + WPARAM wParam, + LPARAM lParam); + [[nodiscard]] bool working() const; + void destroy(); + void init(QColor color); + void initBlend(); + void initCorners(Directions directions = Direction::All); + void verCorners(int h, Gdiplus::Graphics *pgraphics1, Gdiplus::Graphics *pgraphics3); + void horCorners(int w, Gdiplus::Graphics *pgraphics0, Gdiplus::Graphics *pgraphics2); + [[nodiscard]] Gdiplus::Color getColor(uchar alpha) const; + [[nodiscard]] Gdiplus::SolidBrush getBrush(uchar alpha) const; + [[nodiscard]] Gdiplus::Pen getPen(uchar alpha) const; + + const not_null _window; + const HWND _handle; + + int _x = 0; + int _y = 0; + int _w = 0; + int _h = 0; + int _metaSize = 0; + int _fullsize = 0; + int _size = 0; + int _shift = 0; + std::vector _alphas; + std::vector _colors; + + bool _hidden = true; + + HWND _handles[4] = { nullptr }; + HDC _contexts[4] = { nullptr }; + HBITMAP _bitmaps[4] = { nullptr }; + HDC _screenContext = nullptr; + int _widthMax = 0; + int _heightMax = 0; + BLENDFUNCTION _blend; + + BYTE _r = 0; + BYTE _g = 0; + BYTE _b = 0; + COLORREF _noKeyColor = RGB(255, 255, 255); + +}; + +} // namespace Platform +} // namespace Ui diff --git a/ui/platform/win/ui_window_title_win.cpp b/ui/platform/win/ui_window_title_win.cpp new file mode 100644 index 0000000..af86ee9 --- /dev/null +++ b/ui/platform/win/ui_window_title_win.cpp @@ -0,0 +1,160 @@ +// 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/win/ui_window_title_win.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 +#include +#include + +namespace Ui { +namespace Platform { + +TitleWidget::TitleWidget(not_null parent) +: RpWidget(parent) +, _minimize(this, st::titleButtonMinimize) +, _maximizeRestore(this, st::titleButtonMaximize) +, _close(this, st::titleButtonClose) +, _shadow(this, st::titleShadow) +, _maximizedState(parent->windowState() & Qt::WindowMaximized) +, _activeState(parent->isActiveWindow()) { + init(); +} + +void TitleWidget::setText(const QString &text) { + window()->setWindowTitle(text); +} + +not_null TitleWidget::window() const { + return static_cast(parentWidget()); +} + +void TitleWidget::init() { + _minimize->setClickedCallback([=] { + window()->setWindowState(Qt::WindowMinimized); + _minimize->clearState(); + }); + _minimize->setPointerCursor(false); + _maximizeRestore->setClickedCallback([=] { + window()->setWindowState(_maximizedState + ? Qt::WindowNoState + : Qt::WindowMaximized); + _maximizeRestore->clearState(); + }); + _maximizeRestore->setPointerCursor(false); + _close->setClickedCallback([=] { + window()->close(); + _close->clearState(); + }); + _close->setPointerCursor(false); + + setAttribute(Qt::WA_OpaquePaintEvent); + + window()->widthValue( + ) | rpl::start_with_next([=](int width) { + setGeometry(0, 0, width, st::titleHeight); + }, lifetime()); + + window()->createWinId(); + connect( + window()->windowHandle(), + &QWindow::windowStateChanged, + [=](Qt::WindowState state) { handleWindowStateChanged(state); }); + _activeState = isActiveWindow(); + updateButtonsState(); +} + +void TitleWidget::paintEvent(QPaintEvent *e) { + const auto active = isActiveWindow(); + if (_activeState != active) { + _activeState = active; + updateButtonsState(); + } + QPainter(this).fillRect(e->rect(), active ? st::titleBgActive : st::titleBg); +} + +void TitleWidget::updateControlsPosition() { + auto right = 0; + _close->moveToRight(right, 0); right += _close->width(); + _maximizeRestore->moveToRight(right, 0); right += _maximizeRestore->width(); + _minimize->moveToRight(right, 0); +} + +void TitleWidget::resizeEvent(QResizeEvent *e) { + updateControlsPosition(); + _shadow->setGeometry(0, height() - st::lineWidth, width(), st::lineWidth); +} + +void TitleWidget::updateControlsVisibility() { + updateControlsPosition(); + update(); +} + +void TitleWidget::handleWindowStateChanged(Qt::WindowState state) { + if (state == Qt::WindowMinimized) return; + + auto maximized = (state == Qt::WindowMaximized); + if (_maximizedState != maximized) { + _maximizedState = maximized; + updateButtonsState(); + } +} + +void TitleWidget::updateButtonsState() { + const auto minimize = _activeState + ? &st::titleButtonMinimizeIconActive + : nullptr; + const auto minimizeOver = _activeState + ? &st::titleButtonMinimizeIconActiveOver + : nullptr; + _minimize->setIconOverride(minimize, minimizeOver); + if (_maximizedState) { + const auto restore = _activeState + ? &st::titleButtonRestoreIconActive + : &st::titleButtonRestoreIcon; + const auto restoreOver = _activeState + ? &st::titleButtonRestoreIconActiveOver + : &st::titleButtonRestoreIconOver; + _maximizeRestore->setIconOverride(restore, restoreOver); + } else { + const auto maximize = _activeState + ? &st::titleButtonMaximizeIconActive + : nullptr; + const auto maximizeOver = _activeState + ? &st::titleButtonMaximizeIconActiveOver + : nullptr; + _maximizeRestore->setIconOverride(maximize, maximizeOver); + } + const auto close = _activeState + ? &st::titleButtonCloseIconActive + : nullptr; + const auto closeOver = _activeState + ? &st::titleButtonCloseIconActiveOver + : nullptr; + _close->setIconOverride(close, closeOver); +} + +HitTestResult TitleWidget::hitTest(QPoint point) const { + if (false + || (_minimize->geometry().contains(point)) + || (_maximizeRestore->geometry().contains(point)) + || (_close->geometry().contains(point)) + ) { + return HitTestResult::SysButton; + } else if (rect().contains(point)) { + return HitTestResult::Caption; + } + return HitTestResult::None; +} + +} // namespace Platform +} // namespace Ui diff --git a/ui/platform/win/ui_window_title_win.h b/ui/platform/win/ui_window_title_win.h new file mode 100644 index 0000000..144a007 --- /dev/null +++ b/ui/platform/win/ui_window_title_win.h @@ -0,0 +1,68 @@ +// 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 Ui { + +class IconButton; +class PlainShadow; + +namespace Platform { + +enum class HitTestResult { + None = 0, + Client, + SysButton, + Caption, + Top, + TopRight, + Right, + BottomRight, + Bottom, + BottomLeft, + Left, + TopLeft, +}; + +class TitleWidget : public RpWidget { +public: + explicit TitleWidget(not_null parent); + + void setText(const QString &text); + HitTestResult hitTest(QPoint point) const; + +protected: + void paintEvent(QPaintEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + +private: + not_null window() const; + + void init(); + void handleWindowStateChanged(Qt::WindowState state = Qt::WindowNoState); + void updateControlsVisibility(); + void updateButtonsState(); + void updateControlsPosition(); + + object_ptr _minimize; + object_ptr _maximizeRestore; + object_ptr _close; + object_ptr _shadow; + + bool _maximizedState = false; + bool _activeState = false; + +}; + +} // namespace Platform +} // namespace Ui diff --git a/ui/platform/win/ui_window_win.cpp b/ui/platform/win/ui_window_win.cpp new file mode 100644 index 0000000..134ecb3 --- /dev/null +++ b/ui/platform/win/ui_window_win.cpp @@ -0,0 +1,466 @@ +// 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/win/ui_window_win.h" + +#include "ui/inactive_press.h" +#include "ui/platform/win/ui_window_title_win.h" +#include "base/platform/base_platform_info.h" +#include "styles/palette.h" + +#include +#include +#include +#include +#include + +#include +#include + +Q_DECLARE_METATYPE(QMargins); + +namespace Ui { +namespace Platform { +namespace { + +bool IsCompositionEnabled() { + auto result = BOOL(FALSE); + const auto success = (DwmIsCompositionEnabled(&result) == S_OK); + return success && result; +} + +} // namespace + +class WindowHelper::NativeFilter final : public QAbstractNativeEventFilter { +public: + void registerWindow(HWND handle, not_null helper); + void unregisterWindow(HWND handle); + + bool nativeEventFilter( + const QByteArray &eventType, + void *message, + long *result) override; + +private: + base::flat_map> _windowByHandle; + +}; + +void WindowHelper::NativeFilter::registerWindow( + HWND handle, + not_null helper) { + _windowByHandle.emplace(handle, helper); +} + +void WindowHelper::NativeFilter::unregisterWindow(HWND handle) { + _windowByHandle.remove(handle); +} + +bool WindowHelper::NativeFilter::nativeEventFilter( + const QByteArray &eventType, + void *message, + long *result) { + const auto msg = static_cast(message); + const auto i = _windowByHandle.find(msg->hwnd); + return (i != end(_windowByHandle)) + ? i->second->handleNativeEvent( + msg->message, + msg->wParam, + msg->lParam, + static_cast(result)) + : false; +} + +WindowHelper::WindowHelper(not_null window) +: _window(window) +, _handle(GetWindowHandle(_window)) +, _title(Ui::CreateChild(_window.get())) +, _body(Ui::CreateChild(_window.get())) +, _shadow(_window, st::windowShadowFg->c) { + Expects(_handle != nullptr); + + GetNativeFilter()->registerWindow(_handle, this); + init(); +} + +WindowHelper::~WindowHelper() { + GetNativeFilter()->unregisterWindow(_handle); +} + +not_null WindowHelper::body() { + return _body; +} + +void WindowHelper::setTitle(const QString &title) { + _title->setText(title); + _window->setWindowTitle(title); +} + +void WindowHelper::setSizeMin(QSize size) { + _window->setMinimumSize(size.width(), _title->height() + size.height()); +} + +void WindowHelper::init() { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _shadow.setColor(st::windowShadowFg->c); + }, _window->lifetime()); + + rpl::combine( + _window->sizeValue(), + _title->heightValue() + ) | rpl::start_with_next([=](QSize size, int titleHeight) { + _body->setGeometry( + 0, + titleHeight, + size.width(), + size.height() - titleHeight); + }, _body->lifetime()); + + updateMargins(); + + if (!::Platform::IsWindows8OrGreater()) { + SetWindowTheme(_handle, L" ", L" "); + QApplication::setStyle(QStyleFactory::create("Windows")); + } + + _menu = GetSystemMenu(_handle, FALSE); + updateSystemMenu(); + Ui::Connect( + _window->windowHandle(), + &QWindow::windowStateChanged, + [=](Qt::WindowState state) { updateSystemMenu(state); }); +} + +bool WindowHelper::handleNativeEvent( + UINT msg, + WPARAM wParam, + LPARAM lParam, + LRESULT *result) { + switch (msg) { + + case WM_ACTIVATE: { + if (LOWORD(wParam) == WA_CLICKACTIVE) { + Ui::MarkInactivePress(_window, true); + } + if (LOWORD(wParam) != WA_INACTIVE) { + _shadow.update(WindowShadow::Change::Activate); + } else { + _shadow.update(WindowShadow::Change::Deactivate); + } + _window->update(); + } return false; + + case WM_NCPAINT: { + if (::Platform::IsWindows8OrGreater()) { + return false; + } + if (result) *result = 0; + } return true; + + case WM_NCCALCSIZE: { + WINDOWPLACEMENT wp; + wp.length = sizeof(WINDOWPLACEMENT); + if (GetWindowPlacement(_handle, &wp) + && (wp.showCmd == SW_SHOWMAXIMIZED)) { + const auto params = (LPNCCALCSIZE_PARAMS)lParam; + const auto r = (wParam == TRUE) + ? ¶ms->rgrc[0] + : (LPRECT)lParam; + const auto hMonitor = MonitorFromPoint( + { (r->left + r->right) / 2, (r->top + r->bottom) / 2 }, + MONITOR_DEFAULTTONEAREST); + if (hMonitor) { + MONITORINFO mi; + mi.cbSize = sizeof(mi); + if (GetMonitorInfo(hMonitor, &mi)) { + *r = mi.rcWork; + } + } + } + if (result) *result = 0; + return true; + } + + case WM_NCACTIVATE: { + if (IsCompositionEnabled()) { + const auto res = DefWindowProc(_handle, msg, wParam, -1); + if (result) *result = res; + } else { + // Thanks https://github.com/melak47/BorderlessWindow + if (result) *result = 1; + } + } return true; + + case WM_WINDOWPOSCHANGING: + case WM_WINDOWPOSCHANGED: { + WINDOWPLACEMENT wp; + wp.length = sizeof(WINDOWPLACEMENT); + if (GetWindowPlacement(_handle, &wp) + && (wp.showCmd == SW_SHOWMAXIMIZED + || wp.showCmd == SW_SHOWMINIMIZED)) { + _shadow.update(WindowShadow::Change::Hidden); + } else { + _shadow.update( + WindowShadow::Change::Moved | WindowShadow::Change::Resized, + (WINDOWPOS*)lParam); + } + } return false; + + case WM_SIZE: { + if (wParam == SIZE_MAXIMIZED + || wParam == SIZE_RESTORED + || wParam == SIZE_MINIMIZED) { + if (wParam != SIZE_RESTORED + || _window->windowState() != Qt::WindowNoState) { + Qt::WindowState state = Qt::WindowNoState; + if (wParam == SIZE_MAXIMIZED) { + state = Qt::WindowMaximized; + } else if (wParam == SIZE_MINIMIZED) { + state = Qt::WindowMinimized; + } + emit _window->windowHandle()->windowStateChanged(state); + } + updateMargins(); + const auto changes = (wParam == SIZE_MINIMIZED + || wParam == SIZE_MAXIMIZED) + ? WindowShadow::Change::Hidden + : (WindowShadow::Change::Resized + | WindowShadow::Change::Shown); + _shadow.update(changes); + } + } return false; + + case WM_SHOWWINDOW: { + const auto style = GetWindowLong(_handle, GWL_STYLE); + const auto changes = WindowShadow::Change::Resized + | ((wParam && !(style & (WS_MAXIMIZE | WS_MINIMIZE))) + ? WindowShadow::Change::Shown + : WindowShadow::Change::Hidden); + _shadow.update(changes); + } return false; + + case WM_MOVE: { + _shadow.update(WindowShadow::Change::Moved); + } return false; + + case WM_NCHITTEST: { + if (!result) { + return false; + } + + const auto p = MAKEPOINTS(lParam); + auto r = RECT(); + GetWindowRect(_handle, &r); + const auto mapped = QPoint( + p.x - r.left + _marginsDelta.left(), + p.y - r.top + _marginsDelta.top()); + if (!_window->rect().contains(mapped)) { + *result = HTTRANSPARENT; + } else if (!_title->geometry().contains(mapped)) { + *result = HTCLIENT; + } else switch (_title->hitTest(_title->pos() + mapped)) { + case HitTestResult::Client: + case HitTestResult::SysButton: *result = HTCLIENT; break; + case HitTestResult::Caption: *result = HTCAPTION; break; + case HitTestResult::Top: *result = HTTOP; break; + case HitTestResult::TopRight: *result = HTTOPRIGHT; break; + case HitTestResult::Right: *result = HTRIGHT; break; + case HitTestResult::BottomRight: *result = HTBOTTOMRIGHT; break; + case HitTestResult::Bottom: *result = HTBOTTOM; break; + case HitTestResult::BottomLeft: *result = HTBOTTOMLEFT; break; + case HitTestResult::Left: *result = HTLEFT; break; + case HitTestResult::TopLeft: *result = HTTOPLEFT; break; + case HitTestResult::None: + default: *result = HTTRANSPARENT; break; + }; + } return true; + + case WM_NCRBUTTONUP: { + SendMessage(_handle, WM_SYSCOMMAND, SC_MOUSEMENU, lParam); + } return true; + + case WM_SYSCOMMAND: { + if (wParam == SC_MOUSEMENU) { + POINTS p = MAKEPOINTS(lParam); + updateSystemMenu(_window->windowHandle()->windowState()); + TrackPopupMenu( + _menu, + TPM_LEFTALIGN | TPM_TOPALIGN | TPM_LEFTBUTTON, + p.x, + p.y, + 0, + _handle, + 0); + } + } return false; + + case WM_COMMAND: { + if (HIWORD(wParam)) { + return false; + } + const auto command = LOWORD(wParam); + switch (command) { + case SC_CLOSE: _window->close(); return true; + case SC_MINIMIZE: + _window->setWindowState(Qt::WindowMinimized); + return true; + case SC_MAXIMIZE: + _window->setWindowState(Qt::WindowMaximized); + return true; + case SC_RESTORE: + _window->setWindowState(Qt::WindowNoState); + return true; + } + } return true; + + } + return false; +} + +void WindowHelper::updateMargins() { + if (_updatingMargins) return; + + _updatingMargins = true; + const auto guard = gsl::finally([&] { _updatingMargins = false; }); + + RECT r, a; + + GetClientRect(_handle, &r); + a = r; + + const auto style = GetWindowLong(_handle, GWL_STYLE); + const auto styleEx = GetWindowLong(_handle, GWL_EXSTYLE); + AdjustWindowRectEx(&a, style, false, styleEx); + auto margins = QMargins( + a.left - r.left, + a.top - r.top, + r.right - a.right, + r.bottom - a.bottom); + if (style & WS_MAXIMIZE) { + RECT w, m; + GetWindowRect(_handle , &w); + m = w; + + HMONITOR hMonitor = MonitorFromRect(&w, MONITOR_DEFAULTTONEAREST); + if (hMonitor) { + MONITORINFO mi; + mi.cbSize = sizeof(mi); + GetMonitorInfo(hMonitor, &mi); + m = mi.rcWork; + } + + _marginsDelta = QMargins( + w.left - m.left, + w.top - m.top, + m.right - w.right, + m.bottom - w.bottom); + + margins.setLeft(margins.left() - _marginsDelta.left()); + margins.setRight(margins.right() - _marginsDelta.right()); + margins.setBottom(margins.bottom() - _marginsDelta.bottom()); + margins.setTop(margins.top() - _marginsDelta.top()); + } else if (!_marginsDelta.isNull()) { + RECT w; + GetWindowRect(_handle, &w); + SetWindowPos( + _handle, + 0, + 0, + 0, + w.right - w.left - _marginsDelta.left() - _marginsDelta.right(), + w.bottom - w.top - _marginsDelta.top() - _marginsDelta.bottom(), + (SWP_NOMOVE + | SWP_NOSENDCHANGING + | SWP_NOZORDER + | SWP_NOACTIVATE + | SWP_NOREPOSITION)); + _marginsDelta = QMargins(); + } + + if (const auto native = QGuiApplication::platformNativeInterface()) { + native->setWindowProperty( + _window->windowHandle()->handle(), + "WindowsCustomMargins", + QVariant::fromValue(margins)); + } +} + +void WindowHelper::updateSystemMenu() { + updateSystemMenu(_window->windowHandle()->windowState()); +} + +void WindowHelper::updateSystemMenu(Qt::WindowState state) { + if (!_menu) { + return; + } + + const auto menuToDisable = (state == Qt::WindowMaximized) + ? SC_MAXIMIZE + : (state == Qt::WindowMinimized) + ? SC_MINIMIZE + : SC_RESTORE; + const auto itemCount = GetMenuItemCount(_menu); + for (int i = 0; i < itemCount; ++i) { + MENUITEMINFO itemInfo = { 0 }; + itemInfo.cbSize = sizeof(itemInfo); + itemInfo.fMask = MIIM_TYPE | MIIM_STATE | MIIM_ID; + if (!GetMenuItemInfo(_menu, i, TRUE, &itemInfo)) { + break; + } + if (itemInfo.fType & MFT_SEPARATOR) { + continue; + } else if (!itemInfo.wID || itemInfo.wID == SC_CLOSE) { + continue; + } + UINT fOldState = itemInfo.fState; + UINT fState = itemInfo.fState & ~(MFS_DISABLED | MFS_DEFAULT); + if (itemInfo.wID == menuToDisable + || (itemInfo.wID != SC_MINIMIZE + && itemInfo.wID != SC_MAXIMIZE + && itemInfo.wID != SC_RESTORE)) { + fState |= MFS_DISABLED; + } + itemInfo.fMask = MIIM_STATE; + itemInfo.fState = fState; + if (!SetMenuItemInfo(_menu, i, TRUE, &itemInfo)) { + break; + } + } +} + +not_null WindowHelper::GetNativeFilter() { + Expects(QCoreApplication::instance() != nullptr); + + static const auto GlobalFilter = [&] { + const auto application = QCoreApplication::instance(); + const auto filter = Ui::CreateChild(application); + application->installNativeEventFilter(filter); + return filter; + }(); + return GlobalFilter; +} + +HWND GetWindowHandle(not_null widget) { + widget->window()->createWinId(); + + const auto window = widget->window()->windowHandle(); + const auto native = QGuiApplication::platformNativeInterface(); + Assert(window != nullptr); + Assert(native != nullptr); + + return static_cast(native->nativeResourceForWindow( + QByteArrayLiteral("handle"), + window)); +} + +std::unique_ptr CreateWindowHelper( + not_null window) { + return std::make_unique(window); +} + +} // namespace Platform +} // namespace Ui diff --git a/ui/platform/win/ui_window_win.h b/ui/platform/win/ui_window_win.h new file mode 100644 index 0000000..d51298b --- /dev/null +++ b/ui/platform/win/ui_window_win.h @@ -0,0 +1,58 @@ +// 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/platform/ui_platform_window.h" +#include "ui/platform/win/ui_window_shadow_win.h" + +#include + +namespace Ui { +namespace Platform { + +class TitleWidget; + +class WindowHelper final : public BasicWindowHelper { +public: + explicit WindowHelper(not_null window); + ~WindowHelper(); + + not_null body() override; + void setTitle(const QString &title) override; + void setSizeMin(QSize size) override; + +private: + class NativeFilter; + friend class NativeFilter; + + void init(); + void updateMargins(); + void updateSystemMenu(); + void updateSystemMenu(Qt::WindowState state); + [[nodiscard]] bool handleNativeEvent( + UINT msg, + WPARAM wParam, + LPARAM lParam, + LRESULT *result); + + static not_null GetNativeFilter(); + + const not_null _window; + const HWND _handle = nullptr; + const not_null _title; + const not_null _body; + WindowShadow _shadow; + bool _updatingMargins = false; + QMargins _marginsDelta; + HMENU _menu = nullptr; + +}; + +[[nodiscard]] HWND GetWindowHandle(not_null widget); + +} // namespace Platform +} // namespace Ui diff --git a/ui/widgets/widgets.style b/ui/widgets/widgets.style index a84f07e..087b52d 100644 --- a/ui/widgets/widgets.style +++ b/ui/widgets/widgets.style @@ -1161,3 +1161,86 @@ backButton: IconButton(defaultIconButton) { color: windowBgOver; } } + +// Windows specific title + +titleHeight: 21px; +titleButtonMinimize: IconButton { + width: 24px; + height: 21px; + + icon: icon { + { size(24px, 21px), titleButtonBg }, + { "title_button_minimize", titleButtonFg, point(4px, 4px) }, + }; + iconOver: icon { + { size(24px, 21px), titleButtonBgOver }, + { "title_button_minimize", titleButtonFgOver, point(4px, 4px) }, + }; + iconPosition: point(0px, 0px); +} +titleButtonMinimizeIconActive: icon { + { size(24px, 21px), titleButtonBgActive }, + { "title_button_minimize", titleButtonFgActive, point(4px, 4px) }, +}; +titleButtonMinimizeIconActiveOver: icon { + { size(24px, 21px), titleButtonBgActiveOver }, + { "title_button_minimize", titleButtonFgActiveOver, point(4px, 4px) }, +}; +titleButtonMaximize: IconButton(titleButtonMinimize) { + icon: icon { + { size(24px, 21px), titleButtonBg }, + { "title_button_maximize", titleButtonFg, point(4px, 4px) }, + }; + iconOver: icon { + { size(24px, 21px), titleButtonBgOver }, + { "title_button_maximize", titleButtonFgOver, point(4px, 4px) }, + }; +} +titleButtonMaximizeIconActive: icon { + { size(24px, 21px), titleButtonBgActive }, + { "title_button_maximize", titleButtonFgActive, point(4px, 4px) }, +}; +titleButtonMaximizeIconActiveOver: icon { + { size(24px, 21px), titleButtonBgActiveOver }, + { "title_button_maximize", titleButtonFgActiveOver, point(4px, 4px) }, +}; +titleButtonRestoreIcon: icon { + { size(24px, 21px), titleButtonBg }, + { "title_button_restore", titleButtonFg, point(4px, 4px) }, +}; +titleButtonRestoreIconOver: icon { + { size(24px, 21px), titleButtonBgOver }, + { "title_button_restore", titleButtonFgOver, point(4px, 4px) }, +}; +titleButtonRestoreIconActive: icon { + { size(24px, 21px), titleButtonBgActive }, + { "title_button_restore", titleButtonFgActive, point(4px, 4px) }, +}; +titleButtonRestoreIconActiveOver: icon { + { size(24px, 21px), titleButtonBgActiveOver }, + { "title_button_restore", titleButtonFgActiveOver, point(4px, 4px) }, +}; +titleButtonClose: IconButton(titleButtonMinimize) { + width: 25px; + + icon: icon { + { size(25px, 21px), titleButtonCloseBg }, + { "title_button_close", titleButtonCloseFg, point(5px, 4px) }, + }; + iconOver: icon { + { size(25px, 21px), titleButtonCloseBgOver }, + { "title_button_close", titleButtonCloseFgOver, point(5px, 4px) }, + }; +} +titleButtonCloseIconActive: icon { + { size(25px, 21px), titleButtonCloseBgActive }, + { "title_button_close", titleButtonCloseFgActive, point(5px, 4px) }, +}; +titleButtonCloseIconActiveOver: icon { + { size(25px, 21px), titleButtonCloseBgActiveOver }, + { "title_button_close", titleButtonCloseFgActiveOver, point(5px, 4px) }, +}; + +windowShadow: icon {{ "window_shadow", windowShadowFg }}; +windowShadowShift: 1px; diff --git a/ui/widgets/window.cpp b/ui/widgets/window.cpp new file mode 100644 index 0000000..6edd99c --- /dev/null +++ b/ui/widgets/window.cpp @@ -0,0 +1,45 @@ +// 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/widgets/window.h" + +#include "ui/platform/ui_platform_window.h" + +namespace Ui { + +Window::Window(QWidget *parent) +: RpWidget(parent) +, _helper(Platform::CreateWindowHelper(this)) { + hide(); +} + +Window::~Window() = default; + +not_null Window::body() { + return _helper ? _helper->body() : this; +} + +not_null Window::body() const { + return _helper ? _helper->body().get() : this; +} + +void Window::setTitle(const QString &title) { + if (_helper) { + _helper->setTitle(title); + } else { + setWindowTitle(title); + } +} + +void Window::setSizeMin(QSize size) { + if (_helper) { + _helper->setSizeMin(size); + } else { + setMinimumSize(size); + } +} + +} // namespace Ui diff --git a/ui/widgets/window.h b/ui/widgets/window.h new file mode 100644 index 0000000..3e180eb --- /dev/null +++ b/ui/widgets/window.h @@ -0,0 +1,32 @@ +// 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" + +namespace Ui { +namespace Platform { +class BasicWindowHelper; +} // namespace Platform + +class Window : public RpWidget { +public: + explicit Window(QWidget *parent = nullptr); + ~Window(); + + [[nodiscard]] not_null body(); + [[nodiscard]] not_null body() const; + + void setTitle(const QString &title); + void setSizeMin(QSize size); + +private: + const std::unique_ptr _helper; + +}; + +} // namespace Ui