Implement title and in-IV buttons.

This commit is contained in:
John Preston 2023-12-04 15:48:17 +04:00
parent f9299eee2a
commit f508ad5e75
11 changed files with 416 additions and 42 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 <QtCore/QRegularExpression>
#include <QtCore/QJsonDocument>
@ -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<QByteArray, tr::phrase<>>{
{ "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<Ui::RpWindow>();
_window->setTitleStyle(st::ivTitle);
const auto window = _window.get();
window->setGeometry({ 200, 200, 600, 800 });
_title = std::make_unique<Ui::RpWidget>(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<rpl::variable<int>>(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<Ui::RpWidget>(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<Ui::Animations::Simple>();
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({});
}
});
});

View File

@ -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<Ui::RpWindow> _window;
std::unique_ptr<Ui::RpWidget> _title;
Ui::Text::String _titleText;
int _titleLeftSkip = 0;
int _titleRightSkip = 0;
Ui::RpWidget *_container = nullptr;
std::unique_ptr<Webview::Window> _webview;
rpl::event_stream<Webview::DataRequest> _dataRequests;

View File

@ -45,6 +45,9 @@ Data::Data(const MTPDwebPage &webpage, const MTPPage &page)
.webpageDocument = (webpage.vdocument()
? *webpage.vdocument()
: std::optional<MTPDocument>()),
.title = (webpage.vtitle()
? qs(*webpage.vtitle())
: qs(webpage.vauthor().value_or_empty()))
})) {
}

View File

@ -16,6 +16,7 @@ struct Options {
};
struct Prepared {
QString title;
QByteArray html;
std::vector<QByteArray> resources;
base::flat_map<QByteArray, QByteArray> embeds;

View File

@ -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) {
<link rel="stylesheet" href=")" + resource("iv/page.css") + R"(" />
)"_q + head + R"(
</head>
<body>)"_q + body + R"(</body>
<body>
<button class="fixed_button hidden" id="top_back" onclick="IV.back();">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M17 13L12 18L7 13M12 6L12 17"></path>
</svg>
</button>
<button class="fixed_button" id="top_menu" onclick="IV.menu();">
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="2.5" r="1.6"></circle>
<circle cx="8" cy="8" r="1.6"></circle>
<circle cx="8" cy="13.5" r="1.6"></circle>
</svg>
</button>
<button class="fixed_button hidden" id="bottom_up" onclick="IV.toTop();">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M17 13L12 18L7 13M12 6L12 17"></path>
</svg>
</button>
)"_q + body + R"(
</body>
</html>
)"_q;
}

View File

@ -16,6 +16,7 @@ struct Source {
MTPPage page;
std::optional<MTPPhoto> webpagePhoto;
std::optional<MTPDocument> webpageDocument;
QString title;
};
[[nodiscard]] Prepared Prepare(

View File

@ -38,4 +38,5 @@ PUBLIC
PRIVATE
desktop-app::lib_webview
tdesktop::td_lang
tdesktop::td_ui
)

View File

@ -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