diff --git a/Telegram/Resources/iv_html/page.css b/Telegram/Resources/iv_html/page.css
index 6f27a6e1c..591fab69b 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 b81a556b1..89aa8fc30 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 000000000..832916791
--- /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 b2e6e0582..6a775466d 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 5a081ca32..1170974e1 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 c34424e27..962d4fe23 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 1eec747ef..3c2d04980 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 dc93510c7..a1b8749ef 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"(
+