/* 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 "platform/mac/overlay_widget_mac.h" #include "base/object_ptr.h" #include "ui/platform/ui_platform_window_title.h" #include "ui/widgets/buttons.h" #include "ui/widgets/rp_window.h" #include "styles/style_media_view.h" #include #include namespace Platform { namespace { using namespace Media::View; } // namespace struct MacOverlayWidgetHelper::Data { const not_null window; const Fn maximize; object_ptr buttonClose = { nullptr }; object_ptr buttonMinimize = { nullptr }; object_ptr buttonMaximize = { nullptr }; rpl::event_stream<> activations; rpl::variable masterOpacity = 1.; rpl::variable maximized = false; rpl::event_stream<> clearStateRequests; bool anyOver = false; NSWindow * __weak native = nil; rpl::variable topNotchSkip; }; MacOverlayWidgetHelper::MacOverlayWidgetHelper( not_null window, Fn maximize) : _data(std::make_unique(Data{ .window = window, .maximize = std::move(maximize), })) { _data->buttonClose = create(window, Control::Close); _data->buttonMinimize = create(window, Control::Minimize); _data->buttonMaximize = create(window, Control::Maximize); } MacOverlayWidgetHelper::~MacOverlayWidgetHelper() = default; void MacOverlayWidgetHelper::activate(Control control) { const auto fullscreen = (_data->window->windowHandle()->flags() & Qt::FramelessWindowHint); switch (control) { case Control::Close: _data->window->close(); return; case Control::Minimize: [_data->native miniaturize:_data->native]; return; case Control::Maximize: _data->maximize(!fullscreen); return; } } void MacOverlayWidgetHelper::beforeShow(bool fullscreen) { _data->window->setAttribute(Qt::WA_MacAlwaysShowToolWindow, !fullscreen); _data->window->windowHandle()->setFlag(Qt::FramelessWindowHint, fullscreen); updateStyles(fullscreen); clearState(); } void MacOverlayWidgetHelper::afterShow(bool fullscreen) { updateStyles(fullscreen); refreshButtons(fullscreen); _data->window->activateWindow(); } void MacOverlayWidgetHelper::resolveNative() { if (const auto handle = _data->window->winId()) { _data->native = [reinterpret_cast(handle) window]; } } void MacOverlayWidgetHelper::updateStyles(bool fullscreen) { _data->maximized = fullscreen; resolveNative(); if (!_data->native) { return; } const auto window = _data->native; const auto level = !fullscreen ? NSNormalWindowLevel : NSPopUpMenuWindowLevel; [window setLevel:level]; [window setHidesOnDeactivate:!_data->window->testAttribute(Qt::WA_MacAlwaysShowToolWindow)]; [window setTitleVisibility:NSWindowTitleHidden]; [window setTitlebarAppearsTransparent:YES]; [window setStyleMask:[window styleMask] | NSWindowStyleMaskFullSizeContentView]; if (@available(macOS 12.0, *)) { _data->topNotchSkip = [[window screen] safeAreaInsets].top; } } void MacOverlayWidgetHelper::refreshButtons(bool fullscreen) { Expects(_data->native != nullptr); const auto window = _data->native; const auto process = [&](NSWindowButton type) { if (const auto button = [window standardWindowButton:type]) { [button setHidden:YES]; } }; process(NSWindowCloseButton); process(NSWindowMiniaturizeButton); process(NSWindowZoomButton); _data->buttonClose->moveToLeft(0, 0); _data->buttonClose->raise(); _data->buttonClose->show(); _data->buttonMinimize->moveToLeft(_data->buttonClose->width(), 0); _data->buttonMinimize->raise(); _data->buttonMinimize->show(); _data->buttonMaximize->moveToLeft(_data->buttonClose->width() + _data->buttonMinimize->width(), 0); _data->buttonMaximize->raise(); _data->buttonMaximize->show(); } void MacOverlayWidgetHelper::notifyFileDialogShown(bool shown) { resolveNative(); if (_data->native && !_data->window->isHidden()) { const auto level = [_data->native level]; if (level != NSNormalWindowLevel) { const auto level = shown ? NSModalPanelWindowLevel : NSPopUpMenuWindowLevel; [_data->native setLevel:level]; } } } void MacOverlayWidgetHelper::minimize(not_null window) { resolveNative(); if (_data->native) { [_data->native miniaturize:_data->native]; } } void MacOverlayWidgetHelper::clearState() { _data->clearStateRequests.fire({}); } void MacOverlayWidgetHelper::setControlsOpacity(float64 opacity) { _data->masterOpacity = opacity; } rpl::producer MacOverlayWidgetHelper::controlsSideRightValue() { return rpl::single(false); } rpl::producer MacOverlayWidgetHelper::topNotchSkipValue() { return _data->topNotchSkip.value(); } object_ptr MacOverlayWidgetHelper::create( not_null parent, Control control) { auto result = object_ptr(parent); const auto raw = result.data(); raw->setClickedCallback([=] { activate(control); }); struct State { Ui::Animations::Simple animation; float64 progress = -1.; QImage frame; bool maximized = false; bool anyOver = false; bool over = false; }; const auto state = raw->lifetime().make_state(); rpl::merge( _data->masterOpacity.changes() | rpl::to_empty, _data->maximized.changes() | rpl::to_empty ) | rpl::start_with_next([=] { raw->update(); }, raw->lifetime()); _data->clearStateRequests.events( ) | rpl::start_with_next([=] { raw->clearState(); raw->update(); state->over = raw->isOver(); _data->anyOver = false; state->animation.stop(); }, raw->lifetime()); struct Info { const style::icon *icon = nullptr; style::margins padding; }; const auto info = [&]() -> Info { switch (control) { case Control::Minimize: return { &st::mediaviewTitleMinimizeMac, st::mediaviewTitleMinimizeMacPadding }; case Control::Maximize: return { &st::mediaviewTitleMaximizeMac, st::mediaviewTitleMaximizeMacPadding }; case Control::Close: return { &st::mediaviewTitleCloseMac, st::mediaviewTitleCloseMacPadding }; } Unexpected("Value in DefaultOverlayWidgetHelper::Buttons::create."); }(); const auto icon = info.icon; raw->resize(QRect(QPoint(), icon->size()).marginsAdded(info.padding).size()); state->frame = QImage( icon->size() * style::DevicePixelRatio(), QImage::Format_ARGB32_Premultiplied); state->frame.setDevicePixelRatio(style::DevicePixelRatio()); const auto updateOver = [=] { const auto over = raw->isOver(); if (state->over == over) { return; } state->over = over; const auto anyOver = over || _data->buttonClose->isOver() || _data->buttonMinimize->isOver() || _data->buttonMaximize->isOver(); if (_data->anyOver != anyOver) { _data->anyOver = anyOver; _data->buttonClose->update(); _data->buttonMinimize->update(); _data->buttonMaximize->update(); } state->animation.start( [=] { raw->update(); }, state->over ? 0. : 1., state->over ? 1. : 0., st::mediaviewFadeDuration); }; const auto prepareFrame = [=] { const auto progress = state->animation.value(state->over ? 1. : 0.); const auto maximized = _data->maximized.current(); const auto anyOver = _data->anyOver; if (state->progress == progress && state->maximized == maximized && state->anyOver == anyOver) { return; } state->progress = progress; state->maximized = maximized; state->anyOver = anyOver; auto current = icon; if (control == Control::Maximize) { current = maximized ? &st::mediaviewTitleRestoreMac : icon; } state->frame.fill(Qt::transparent); auto q = QPainter(&state->frame); const auto normal = maximized ? kMaximizedIconOpacity : kNormalIconOpacity; q.setOpacity(progress + (1 - progress) * normal); st::mediaviewTitleButtonMac.paint(q, 0, 0, raw->width()); if (anyOver) { q.setOpacity(1.); current->paint(q, 0, 0, raw->width()); } q.end(); }; raw->paintRequest( ) | rpl::start_with_next([=, padding = info.padding] { updateOver(); prepareFrame(); auto p = QPainter(raw); p.setOpacity(_data->masterOpacity.current()); p.drawImage(padding.left(), padding.top(), state->frame); }, raw->lifetime()); return result; } std::unique_ptr CreateOverlayWidgetHelper( not_null window, Fn maximize) { return std::make_unique( window, std::move(maximize)); } } // namespace Platform