From f508ad5e7583529dc3f941bbd52ec6563601f3d0 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 4 Dec 2023 15:48:17 +0400 Subject: [PATCH] Implement title and in-IV buttons. --- Telegram/Resources/iv_html/page.css | 94 ++++++++++++++++ Telegram/Resources/iv_html/page.js | 95 ++++++++++++++-- Telegram/SourceFiles/iv/iv.style | 100 +++++++++++++++++ Telegram/SourceFiles/iv/iv_controller.cpp | 126 +++++++++++++++++----- Telegram/SourceFiles/iv/iv_controller.h | 10 ++ Telegram/SourceFiles/iv/iv_data.cpp | 3 + Telegram/SourceFiles/iv/iv_data.h | 1 + Telegram/SourceFiles/iv/iv_prepare.cpp | 26 ++++- Telegram/SourceFiles/iv/iv_prepare.h | 1 + Telegram/cmake/td_iv.cmake | 1 + Telegram/cmake/td_ui.cmake | 1 + 11 files changed, 416 insertions(+), 42 deletions(-) create mode 100644 Telegram/SourceFiles/iv/iv.style diff --git a/Telegram/Resources/iv_html/page.css b/Telegram/Resources/iv_html/page.css index 6f27a6e1c4..591fab69b8 100644 --- a/Telegram/Resources/iv_html/page.css +++ b/Telegram/Resources/iv_html/page.css @@ -28,6 +28,100 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover { background-color: var(--td-scroll-bar-bg-over) !important; } +.fixed_button { + position: fixed; + background-color: var(--td-history-to-down-bg); + border: none; + border-radius: 50%; + width: 32px; + height: 32px; + box-shadow: 0 0 4px -2px var(--td-history-to-down-shadow); + cursor: pointer; + outline: none; + z-index: 1000; + overflow: hidden; + user-select: none; + display: flex; + justify-content: center; + align-items: center; +} +.fixed_button:hover { + background-color: var(--td-history-to-down-bg-over); +} +.fixed_button svg { + fill: none; + position: relative; + z-index: 1; +} +.fixed_button .ripple .inner { + position: absolute; + border-radius: 50%; + transform: scale(0); + opacity: 1; + animation: ripple 650ms cubic-bezier(0.22, 1, 0.36, 1) forwards; + background-color: var(--td-history-to-down-bg-ripple); +} +.fixed_button .ripple.hiding { + animation: fadeOut 200ms linear forwards; +} +@keyframes ripple { + to { + transform: scale(2); + } +} +@keyframes fadeOut { + to { + opacity: 0; + } +} +#top_menu svg { + width: 16px; + height: 16px; +} +#top_menu circle { + fill: var(--td-history-to-down-fg); +} +#top_menu:hover circle { + fill: var(--td-history-to-down-fg-over); +} +#top_menu { + top: 10px; + right: 10px; +} +#top_back path, +#bottom_up path { + stroke: var(--td-history-to-down-fg); + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +#top_back:hover path, +#bottom_up:hover path { + stroke: var(--td-history-to-down-fg-over); +} +#top_back { + top: 10px; + left: 10px; + transition: left 200ms linear; +} +#top_back svg { + transform: rotate(90deg); +} +#top_back.hidden { + left: -36px; +} +#bottom_up { + bottom: 10px; + right: 10px; + transition: bottom 200ms linear; +} +#bottom_up svg { + transform: rotate(180deg); +} +#bottom_up.hidden { + bottom: -36px; +} + article { padding-bottom: 12px; overflow: hidden; diff --git a/Telegram/Resources/iv_html/page.js b/Telegram/Resources/iv_html/page.js index b81a556b1e..89aa8fc300 100644 --- a/Telegram/Resources/iv_html/page.js +++ b/Telegram/Resources/iv_html/page.js @@ -25,15 +25,15 @@ var IV = { e.preventDefault(); }, frameKeyDown: function (e) { - let keyW = (e.key === 'w') + const keyW = (e.key === 'w') || (e.code === 'KeyW') || (e.keyCode === 87); - let keyQ = (e.key === 'q') + const keyQ = (e.key === 'q') || (e.code === 'KeyQ') || (e.keyCode === 81); - let keyM = (e.key === 'm') - || (e.code === 'KeyM') - || (e.keyCode === 77); + const keyM = (e.key === 'm') + || (e.code === 'KeyM') + || (e.keyCode === 77); if ((e.metaKey || e.ctrlKey) && (keyW || keyQ || keyM)) { e.preventDefault(); IV.notify({ @@ -49,13 +49,26 @@ var IV = { }); } }, + frameMouseEnter: function (e) { + IV.notify({ event: 'mouseenter' }); + }, + frameMouseUp: function (e) { + IV.notify({ event: 'mouseup' }); + }, + lastScrollTop: 0, + frameScrolled: function (e) { + const now = document.documentElement.scrollTop; + if (now < 100) { + document.getElementById('bottom_up').classList.add('hidden'); + } else if (now > IV.lastScrollTop && now > 200) { + document.getElementById('bottom_up').classList.remove('hidden'); + } + IV.lastScrollTop = now; + }, updateStyles: function (styles) { if (IV.styles !== styles) { - console.log('Setting', styles); IV.styles = styles; document.getElementsByTagName('html')[0].style = styles; - } else { - console.log('Skipping', styles); } }, slideshowSlide: function(el, next) { @@ -72,7 +85,9 @@ var IV = { return false; }, initPreBlocks: function() { - if (!hljs) return; + if (!hljs) { + return; + } var pres = document.getElementsByTagName('pre'); for (var i = 0; i < pres.length; i++) { if (pres[i].hasAttribute('data-language')) { @@ -102,9 +117,71 @@ var IV = { }, false); })(iframes[i]); } + }, + addRipple: function (button, x, y) { + const ripple = document.createElement('span'); + ripple.classList.add('ripple'); + + const inner = document.createElement('span'); + inner.classList.add('inner'); + x -= button.offsetLeft; + y -= button.offsetTop; + + const mx = button.clientWidth - x; + const my = button.clientHeight - y; + const sq1 = x * x + y * y; + const sq2 = mx * mx + y * y; + const sq3 = x * x + my * my; + const sq4 = mx * mx + my * my; + const radius = Math.sqrt(Math.max(sq1, sq2, sq3, sq4)); + + inner.style.width = inner.style.height = `${2 * radius}px`; + inner.style.left = `${x - radius}px`; + inner.style.top = `${y - radius}px`; + inner.classList.add('inner'); + + ripple.addEventListener('animationend', function (e) { + if (e.animationName === 'fadeOut') { + ripple.remove(); + } + }); + + ripple.appendChild(inner); + button.appendChild(ripple); + }, + stopRipples: function (button) { + const ripples = button.getElementsByClassName('ripple'); + for (var i = 0; i < ripples.length; ++i) { + const ripple = ripples[i]; + if (!ripple.classList.contains('hiding')) { + ripple.classList.add('hiding'); + } + } + }, + init: function () { + const buttons = document.getElementsByClassName('fixed_button'); + for (let i = 0; i < buttons.length; ++i) { + const button = buttons[i]; + button.addEventListener('mousedown', function (e) { + IV.addRipple(e.currentTarget, e.clientX, e.clientY); + }); + button.addEventListener('mouseup', function (e) { + IV.stopRipples(e.currentTarget); + }); + button.addEventListener('mouseleave', function (e) { + IV.stopRipples(e.currentTarget); + }); + } + }, + toTop: function () { + document.getElementById('bottom_up').classList.add('hidden'); + window.scrollTo({ top: 0, behavior: 'smooth' }); } }; document.onclick = IV.frameClickHandler; document.onkeydown = IV.frameKeyDown; +document.onmouseenter = IV.frameMouseEnter; +document.onmouseup = IV.frameMouseUp; +document.onscroll = IV.frameScrolled; window.onmessage = IV.postMessageHandler; diff --git a/Telegram/SourceFiles/iv/iv.style b/Telegram/SourceFiles/iv/iv.style new file mode 100644 index 0000000000..8329167917 --- /dev/null +++ b/Telegram/SourceFiles/iv/iv.style @@ -0,0 +1,100 @@ +/* +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 +*/ +using "ui/basic.style"; +using "ui/widgets/widgets.style"; + +ivTitleHeight: 24px; +ivTitleIconShift: point(0px, 0px); +ivTitleButton: IconButton(windowTitleButton) { + height: ivTitleHeight; + iconPosition: ivTitleIconShift; +} +ivTitleButtonClose: IconButton(windowTitleButtonClose) { + height: ivTitleHeight; + iconPosition: ivTitleIconShift; +} + +ivTitleButtonSize: size(windowTitleButtonWidth, ivTitleHeight); +ivTitle: WindowTitle(defaultWindowTitle) { + height: ivTitleHeight; + style: TextStyle(defaultTextStyle) { + font: font(semibold 12px); + } + shadow: false; + minimize: IconButton(ivTitleButton) { + icon: icon { + { ivTitleButtonSize, titleButtonBg }, + { "title_button_minimize", titleButtonFg, ivTitleIconShift }, + }; + iconOver: icon { + { ivTitleButtonSize, titleButtonBgOver }, + { "title_button_minimize", titleButtonFgOver, ivTitleIconShift }, + }; + } + minimizeIconActive: icon { + { ivTitleButtonSize, titleButtonBgActive }, + { "title_button_minimize", titleButtonFgActive, ivTitleIconShift }, + }; + minimizeIconActiveOver: icon { + { ivTitleButtonSize, titleButtonBgActiveOver }, + { "title_button_minimize", titleButtonFgActiveOver, ivTitleIconShift }, + }; + maximize: IconButton(windowTitleButton) { + icon: icon { + { ivTitleButtonSize, titleButtonBg }, + { "title_button_maximize", titleButtonFg, ivTitleIconShift }, + }; + iconOver: icon { + { ivTitleButtonSize, titleButtonBgOver }, + { "title_button_maximize", titleButtonFgOver, ivTitleIconShift }, + }; + } + maximizeIconActive: icon { + { ivTitleButtonSize, titleButtonBgActive }, + { "title_button_maximize", titleButtonFgActive, ivTitleIconShift }, + }; + maximizeIconActiveOver: icon { + { ivTitleButtonSize, titleButtonBgActiveOver }, + { "title_button_maximize", titleButtonFgActiveOver, ivTitleIconShift }, + }; + restoreIcon: icon { + { ivTitleButtonSize, titleButtonBg }, + { "title_button_restore", titleButtonFg, ivTitleIconShift }, + }; + restoreIconOver: icon { + { ivTitleButtonSize, titleButtonBgOver }, + { "title_button_restore", titleButtonFgOver, ivTitleIconShift }, + }; + restoreIconActive: icon { + { ivTitleButtonSize, titleButtonBgActive }, + { "title_button_restore", titleButtonFgActive, ivTitleIconShift }, + }; + restoreIconActiveOver: icon { + { ivTitleButtonSize, titleButtonBgActiveOver }, + { "title_button_restore", titleButtonFgActiveOver, ivTitleIconShift }, + }; + close: IconButton(windowTitleButtonClose) { + icon: icon { + { ivTitleButtonSize, titleButtonCloseBg }, + { "title_button_close", titleButtonCloseFg, ivTitleIconShift }, + }; + iconOver: icon { + { ivTitleButtonSize, titleButtonCloseBgOver }, + { "title_button_close", titleButtonCloseFgOver, ivTitleIconShift }, + }; + } + closeIconActive: icon { + { ivTitleButtonSize, titleButtonCloseBgActive }, + { "title_button_close", titleButtonCloseFgActive, ivTitleIconShift }, + }; + closeIconActiveOver: icon { + { ivTitleButtonSize, titleButtonCloseBgActiveOver }, + { "title_button_close", titleButtonCloseFgActiveOver, ivTitleIconShift }, + }; +} +ivTitleExpandedHeight: 76px; diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp index b2e6e0582a..6a775466d7 100644 --- a/Telegram/SourceFiles/iv/iv_controller.cpp +++ b/Telegram/SourceFiles/iv/iv_controller.cpp @@ -11,14 +11,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/invoke_queued.h" #include "iv/iv_data.h" #include "lang/lang_keys.h" +#include "ui/platform/ui_platform_window_title.h" #include "ui/widgets/rp_window.h" +#include "ui/painter.h" #include "webview/webview_data_stream_memory.h" #include "webview/webview_embed.h" #include "webview/webview_interface.h" #include "styles/palette.h" - -#include "base/call_delayed.h" -#include "ui/effects/animations.h" +#include "styles/style_iv.h" +#include "styles/style_widgets.h" +#include "styles/style_window.h" #include #include @@ -39,12 +41,23 @@ namespace { { "window-bg", &st::windowBg }, { "window-bg-over", &st::windowBgOver }, { "window-bg-ripple", &st::windowBgRipple }, + { "window-bg-active", &st::windowBgActive }, { "window-fg", &st::windowFg }, { "window-sub-text-fg", &st::windowSubTextFg }, { "window-active-text-fg", &st::windowActiveTextFg }, - { "window-bg-active", &st::windowBgActive }, + { "window-shadow-fg", &st::windowShadowFg }, { "box-divider-bg", &st::boxDividerBg }, { "box-divider-fg", &st::boxDividerFg }, + { "menu-icon-fg", &st::menuIconFg }, + { "menu-icon-fg-over", &st::menuIconFgOver }, + { "menu-bg", &st::menuBg }, + { "menu-bg-over", &st::menuBgOver }, + { "history-to-down-fg", &st::historyToDownFg }, + { "history-to-down-fg-over", &st::historyToDownFgOver }, + { "history-to-down-bg", &st::historyToDownBg }, + { "history-to-down-bg-over", &st::historyToDownBgOver }, + { "history-to-down-bg-ripple", &st::historyToDownBgRipple }, + { "history-to-down-shadow", &st::historyToDownShadow }, }; static const auto phrases = base::flat_map>{ { "group-call-join", tr::lng_group_call_join }, @@ -124,52 +137,103 @@ Controller::Controller() Controller::~Controller() { _webview = nullptr; + _title = nullptr; _window = nullptr; } void Controller::show(const QString &dataPath, Prepared page) { createWindow(); + _titleText.setText(st::ivTitle.style, page.title); InvokeQueued(_container, [=, page = std::move(page)]() mutable { showInWindow(dataPath, std::move(page)); }); } +void Controller::updateTitleGeometry() { + _title->setGeometry(0, 0, _window->width(), st::ivTitle.height); +} + +void Controller::paintTitle(Painter &p, QRect clip) { + const auto active = _window->isActiveWindow(); + const auto full = _title->width(); + p.setPen(active ? st::ivTitle.fgActive : st::ivTitle.fg); + const auto available = QRect( + _titleLeftSkip, + 0, + full - _titleLeftSkip - _titleRightSkip, + _title->height()); + const auto use = std::min(available.width(), _titleText.maxWidth()); + const auto center = full + - 2 * std::max(_titleLeftSkip, _titleRightSkip); + const auto left = (use <= center) + ? ((full - use) / 2) + : (use < available.width() && _titleLeftSkip < _titleRightSkip) + ? (available.x() + available.width() - use) + : available.x(); + const auto titleTextHeight = st::ivTitle.style.font->height; + const auto top = (st::ivTitle.height - titleTextHeight) / 2; + _titleText.drawLeftElided(p, left, top, available.width(), full); +} + void Controller::createWindow() { _window = std::make_unique(); + _window->setTitleStyle(st::ivTitle); const auto window = _window.get(); - window->setGeometry({ 200, 200, 600, 800 }); + _title = std::make_unique(window); + _title->setAttribute(Qt::WA_TransparentForMouseEvents); + _title->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto p = Painter(_title.get()); + paintTitle(p, clip); + }, _title->lifetime()); + window->widthValue() | rpl::start_with_next([=] { + updateTitleGeometry(); + }, _title->lifetime()); - const auto skip = window->lifetime().make_state>(0); +#ifdef Q_OS_MAC + _titleLeftSkip = 8 + 12 + 8 + 12 + 8 + 12 + 8; + _titleRightSkip = st::ivTitle.style.font->spacew; +#else // Q_OS_MAC + using namespace Ui::Platform; + TitleControlsLayoutValue( + ) | rpl::start_with_next([=](TitleControls::Layout layout) { + const auto accumulate = [](const auto &list) { + auto result = 0; + for (const auto control : list) { + switch (control) { + case TitleControl::Close: + result += st::ivTitle.close.width; + break; + case TitleControl::Minimize: + result += st::ivTitle.minimize.width; + break; + case TitleControl::Maximize: + result += st::ivTitle.maximize.width; + break; + } + } + return result; + }; + const auto space = st::ivTitle.style.font->spacew; + _titleLeftSkip = accumulate(layout.left) + space; + _titleRightSkip = accumulate(layout.right) + space; + _title->update(); + }, _title->lifetime()); +#endif // Q_OS_MAC + + window->setGeometry({ 200, 200, 600, 800 }); + window->setMinimumSize({ st::windowMinWidth, st::windowMinHeight }); _container = Ui::CreateChild(window->body().get()); rpl::combine( window->body()->sizeValue(), - skip->value() - ) | rpl::start_with_next([=](QSize size, int skip) { - _container->setGeometry(QRect(QPoint(), size).marginsRemoved({ 0, skip, 0, 0 })); + _title->heightValue() + ) | rpl::start_with_next([=](QSize size, int title) { + title -= window->body()->y(); + _container->setGeometry(QRect(QPoint(), size).marginsRemoved( + { 0, title, 0, 0 })); }, _container->lifetime()); - base::call_delayed(5000, window, [=] { - const auto animation = window->lifetime().make_state(); - animation->start([=] { - *skip = animation->value(64); - if (!animation->animating()) { - base::call_delayed(4000, window, [=] { - animation->start([=] { - *skip = animation->value(0); - }, 64, 0, 200, anim::easeOutCirc); - }); - } - }, 0, 64, 200, anim::easeOutCirc); - }); - - window->body()->paintRequest() | rpl::start_with_next([=](QRect clip) { - auto p = QPainter(window->body()); - p.fillRect(clip, st::windowBg); - p.fillRect(clip, QColor(0, 128, 0, 128)); - }, window->body()->lifetime()); - _container->paintRequest() | rpl::start_with_next([=](QRect clip) { QPainter(_container).fillRect(clip, st::windowBg); }, _container->lifetime()); @@ -237,6 +301,10 @@ void Controller::showInWindow(const QString &dataPath, Prepared page) { } else if (key == u"q"_q && modifier == ctrl) { quit(); } + } else if (event == u"mouseenter"_q) { + window->overrideSystemButtonOver({}); + } else if (event == u"mouseup"_q) { + window->overrideSystemButtonDown({}); } }); }); diff --git a/Telegram/SourceFiles/iv/iv_controller.h b/Telegram/SourceFiles/iv/iv_controller.h index 5a081ca32c..1170974e10 100644 --- a/Telegram/SourceFiles/iv/iv_controller.h +++ b/Telegram/SourceFiles/iv/iv_controller.h @@ -8,6 +8,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/invoke_queued.h" +#include "ui/effects/animations.h" +#include "ui/text/text.h" + +class Painter; namespace Webview { struct DataRequest; @@ -49,6 +53,8 @@ public: private: void createWindow(); + void updateTitleGeometry(); + void paintTitle(Painter &p, QRect clip); void showInWindow(const QString &dataPath, Prepared page); void escape(); @@ -56,6 +62,10 @@ private: void quit(); std::unique_ptr _window; + std::unique_ptr _title; + Ui::Text::String _titleText; + int _titleLeftSkip = 0; + int _titleRightSkip = 0; Ui::RpWidget *_container = nullptr; std::unique_ptr _webview; rpl::event_stream _dataRequests; diff --git a/Telegram/SourceFiles/iv/iv_data.cpp b/Telegram/SourceFiles/iv/iv_data.cpp index c34424e27c..962d4fe230 100644 --- a/Telegram/SourceFiles/iv/iv_data.cpp +++ b/Telegram/SourceFiles/iv/iv_data.cpp @@ -45,6 +45,9 @@ Data::Data(const MTPDwebPage &webpage, const MTPPage &page) .webpageDocument = (webpage.vdocument() ? *webpage.vdocument() : std::optional()), + .title = (webpage.vtitle() + ? qs(*webpage.vtitle()) + : qs(webpage.vauthor().value_or_empty())) })) { } diff --git a/Telegram/SourceFiles/iv/iv_data.h b/Telegram/SourceFiles/iv/iv_data.h index 1eec747eff..3c2d049804 100644 --- a/Telegram/SourceFiles/iv/iv_data.h +++ b/Telegram/SourceFiles/iv/iv_data.h @@ -16,6 +16,7 @@ struct Options { }; struct Prepared { + QString title; QByteArray html; std::vector resources; base::flat_map embeds; diff --git a/Telegram/SourceFiles/iv/iv_prepare.cpp b/Telegram/SourceFiles/iv/iv_prepare.cpp index dc93510c7e..a1b8749ef2 100644 --- a/Telegram/SourceFiles/iv/iv_prepare.cpp +++ b/Telegram/SourceFiles/iv/iv_prepare.cpp @@ -172,6 +172,7 @@ Parser::Parser(const Source &source, const Options &options) : _options(options) , _rtl(source.page.data().is_rtl()) { process(source); + _result.title = source.title; _result.html = prepare(page(source.page.data())); } @@ -1003,9 +1004,7 @@ QByteArray Parser::prepare(QByteArray body) { if (_hasEmbeds) { js += "IV.initEmbedBlocks();"; } - if (!js.isEmpty()) { - body += tag("script", js); - } + body += tag("script", js + "IV.init();"); return html(head, body); } @@ -1026,7 +1025,26 @@ QByteArray Parser::html(const QByteArray &head, const QByteArray &body) { )"_q + head + R"( - )"_q + body + R"( + + + + +)"_q + body + R"( + )"_q; } diff --git a/Telegram/SourceFiles/iv/iv_prepare.h b/Telegram/SourceFiles/iv/iv_prepare.h index f5971d6073..06cb0d87cc 100644 --- a/Telegram/SourceFiles/iv/iv_prepare.h +++ b/Telegram/SourceFiles/iv/iv_prepare.h @@ -16,6 +16,7 @@ struct Source { MTPPage page; std::optional webpagePhoto; std::optional webpageDocument; + QString title; }; [[nodiscard]] Prepared Prepare( diff --git a/Telegram/cmake/td_iv.cmake b/Telegram/cmake/td_iv.cmake index c15db5afc8..1ef5a96044 100644 --- a/Telegram/cmake/td_iv.cmake +++ b/Telegram/cmake/td_iv.cmake @@ -38,4 +38,5 @@ PUBLIC PRIVATE desktop-app::lib_webview tdesktop::td_lang + tdesktop::td_ui ) diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 817f3c9aba..7aa713ec0c 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -26,6 +26,7 @@ set(style_files info/boosts/giveaway/giveaway.style info/userpic/info_userpic_builder.style intro/intro.style + iv/iv.style media/player/media_player.style passport/passport.style payments/ui/payments.style