aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorEike Ziller <eike.ziller@qt.io>2021-01-14 09:27:26 +0100
committerEike Ziller <eike.ziller@qt.io>2021-01-19 12:33:49 +0100
commit6c5ef81184d0310fea1da1f2fd188fee6cdcd005 (patch)
treea3b72db74411b62c9efde9e175c90bc4c6969106 /src
Initial import from Qt Creator
Diffstat (limited to 'src')
m---------src/3rdparty/litehtml0
-rw-r--r--src/CMakeLists.txt119
-rw-r--r--src/container_qpainter.cpp1360
-rw-r--r--src/container_qpainter.h124
-rw-r--r--src/container_qpainter_p.h163
-rw-r--r--src/qlitehtml.pri164
-rw-r--r--src/qlitehtml.qbs216
-rw-r--r--src/qlitehtml_global.h36
-rw-r--r--src/qlitehtmlwidget.cpp654
-rw-r--r--src/qlitehtmlwidget.h95
10 files changed, 2931 insertions, 0 deletions
diff --git a/src/3rdparty/litehtml b/src/3rdparty/litehtml
new file mode 160000
+Subproject db7f59d5886fd50f84d48720c79dc2e6152efa8
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..22f51eb
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,119 @@
+# set the following variables before adding as subdirectory into a project
+# QLITEHTML_BIN_PATH - relative install path for result DLLs
+# QLITEHTML_LIBRARY_PATH - relative install path for result dynamic libraries
+# QLITEHTML_EXPORT - export name for qlitehtml
+# QLITEHTML_DEVEL_COMPONENT - component name for development installation
+# QLITEHTML_DEVEL_EXCLUDE_FROM_ALL - if development component should not be installed by default
+# QLITEHTML_HEADER_PATH - relative install path for development headers
+set(QLITEHTML_VERSION ${PROJECT_VERSION})
+set(QLITEHTML_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
+set(QLITEHTML_VERSION_COMPAT ${QLITEHTML_VERSION} CACHE STRING "qlitehtml compat version number.")
+
+find_package(litehtml QUIET)
+if(NOT TARGET litehtml AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/litehtml/CMakeLists.txt)
+ set(ORIG_FPIC ${CMAKE_POSITION_INDEPENDENT_CODE})
+ if (WIN32)
+ set(LITEHTML_UTF8 ON CACHE BOOL "")
+ endif()
+ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+
+ add_subdirectory(3rdparty/litehtml EXCLUDE_FROM_ALL)
+
+ set(CMAKE_POSITION_INDEPENDENT_CODE "${ORIG_FPIC}")
+ # force optimized litehtml even in debug
+ if (CMAKE_BUILD_TYPE STREQUAL "Debug")
+ # except for windows
+ if (NOT WIN32)
+ target_compile_options(gumbo PRIVATE -O2)
+ target_compile_options(litehtml PRIVATE -O2)
+ endif()
+ endif()
+endif()
+
+# TODO error if litehtml was not found?
+
+find_package(Qt5 COMPONENTS Widgets REQUIRED)
+
+if(TARGET litehtml)
+ set(PUBLIC_HEADERS
+ container_qpainter.h
+ container_qpainter_p.h
+ qlitehtml_global.h
+ qlitehtmlwidget.h)
+ add_library(qlitehtml SHARED ${PUBLIC_HEADERS} container_qpainter.cpp qlitehtmlwidget.cpp)
+
+ target_include_directories(qlitehtml PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>)
+ target_link_libraries(qlitehtml PUBLIC Qt5::Widgets PRIVATE litehtml)
+ target_compile_definitions(qlitehtml PRIVATE
+ QLITEHTML_LIBRARY
+ QT_NO_JAVA_STYLE_ITERATORS
+ QT_NO_CAST_TO_ASCII QT_RESTRICTED_CAST_FROM_ASCII
+ QT_USE_QSTRINGBUILDER)
+ if (WIN32)
+ target_compile_definitions(qlitehtml PRIVATE UNICODE _UNICODE _CRT_SECURE_NO_WARNINGS)
+ if (NOT BUILD_WITH_PCH)
+ # Windows 8 0x0602
+ target_compile_definitions(qlitehtml PRIVATE WINVER=0x0602 _WIN32_WINNT=0x0602
+ WIN32_LEAN_AND_MEAN)
+ endif()
+ endif()
+
+ set_target_properties(qlitehtml PROPERTIES
+ SOURCES_DIR "${CMAKE_CURRENT_SOURCE_DIR}"
+ VERSION "${QLITEHTML_VERSION}"
+ SOVERSION "${QLITEHTML_VERSION_MAJOR}"
+ MACHO_CURRENT_VERSION "${QLITEHTML_VERSION}"
+ MACHO_COMPATIBILITY_VERSION "${QLITEHTML_VERSION_COMPAT}"
+ CXX_EXTENSIONS OFF
+ CXX_VISIBILITY_PRESET hidden
+ VISIBILITY_INLINES_HIDDEN ON
+ POSITION_INDEPENDENT_CODE ON)
+
+ if(WIN32)
+ set_target_properties(qlitehtml PROPERTIES
+ SUFFIX "${QLITEHTML_VERSION_MAJOR}${CMAKE_SHARED_LIBRARY_SUFFIX}"
+ PREFIX "")
+ endif()
+
+ if(DEFINED QLITEHTML_BIN_PATH AND DEFINED QLITEHTML_LIBRARY_PATH)
+ install(TARGETS qlitehtml
+ RUNTIME
+ DESTINATION "${QLITEHTML_BIN_PATH}"
+ OPTIONAL
+ LIBRARY
+ DESTINATION "${QLITEHTML_LIBRARY_PATH}"
+ NAMELINK_SKIP
+ OPTIONAL)
+ endif()
+
+ if(DEFINED QLITEHTML_EXPORT)
+ install(TARGETS qlitehtml EXPORT ${QLITEHTML_EXPORT})
+ add_library(${QLITEHTML_EXPORT}::qlitehtml ALIAS qlitehtml)
+ endif()
+
+ if(DEFINED QLITEHTML_DEVEL_COMPONENT)
+ set(_EXCLUDE)
+ if(QLITEHTML_DEVEL_EXCLUDE_FROM_ALL)
+ set(_EXCLUDE EXCLUDE_FROM_ALL)
+ endif()
+ if(DEFINED QLITEHTML_LIBRARY_PATH)
+ install(TARGETS qlitehtml
+ OBJECTS
+ DESTINATION "${QLITEHTML_LIBRARY_PATH}"
+ COMPONENT ${QLITEHTML_DEVEL_COMPONENT} ${_EXCLUDE}
+ OPTIONAL
+ ARCHIVE
+ DESTINATION "${QLITEHTML_LIBRARY_PATH}"
+ COMPONENT ${QLITEHTML_DEVEL_COMPONENT} ${_EXCLUDE}
+ OPTIONAL)
+ endif()
+ if(DEFINED QLITEHTML_HEADER_PATH)
+ install(
+ FILES ${PUBLIC_HEADERS}
+ DESTINATION ${QLITEHTML_HEADER_PATH}
+ COMPONENT ${QLITEHTML_DEVEL_COMPONENT} ${_EXCLUDE})
+ target_include_directories(qlitehtml PUBLIC $<INSTALL_INTERFACE:${QLITEHTML_HEADER_PATH}>)
+ endif()
+ endif()
+
+endif()
diff --git a/src/container_qpainter.cpp b/src/container_qpainter.cpp
new file mode 100644
index 0000000..269b0de
--- /dev/null
+++ b/src/container_qpainter.cpp
@@ -0,0 +1,1360 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of QLiteHtml.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "container_qpainter.h"
+#include "container_qpainter_p.h"
+
+#include <QClipboard>
+#include <QCursor>
+#include <QDebug>
+#include <QDir>
+#include <QFont>
+#include <QFontDatabase>
+#include <QFontMetrics>
+#include <QGuiApplication>
+#include <QLoggingCategory>
+#include <QPainter>
+#include <QPalette>
+#include <QRegularExpression>
+#include <QScreen>
+#include <QTextLayout>
+#include <QUrl>
+
+#include <algorithm>
+#include <set>
+
+const int kDragDistance = 5;
+
+using Font = QFont;
+using Context = QPainter;
+
+namespace {
+static Q_LOGGING_CATEGORY(log, "qlitehtml", QtCriticalMsg)
+}
+
+static QFont toQFont(litehtml::uint_ptr hFont)
+{
+ return *reinterpret_cast<Font *>(hFont);
+}
+
+static QPainter *toQPainter(litehtml::uint_ptr hdc)
+{
+ return reinterpret_cast<Context *>(hdc);
+}
+
+static QRect toQRect(litehtml::position position)
+{
+ return {position.x, position.y, position.width, position.height};
+}
+
+static litehtml::elements_vector path(const litehtml::element::ptr &element)
+{
+ litehtml::elements_vector result;
+ litehtml::element::ptr current = element;
+ while (current) {
+ result.push_back(current);
+ current = current->parent();
+ }
+ std::reverse(std::begin(result), std::end(result));
+ return result;
+}
+
+static std::pair<litehtml::element::ptr, size_t> getCommonParent(const litehtml::elements_vector &a,
+ const litehtml::elements_vector &b)
+{
+ litehtml::element::ptr parent;
+ const size_t minSize = std::min(a.size(), b.size());
+ for (size_t i = 0; i < minSize; ++i) {
+ if (a.at(i) != b.at(i))
+ return {parent, i};
+ parent = a.at(i);
+ }
+ return {parent, minSize};
+}
+
+static std::pair<Selection::Element, Selection::Element> getStartAndEnd(const Selection::Element &a,
+ const Selection::Element &b)
+{
+ if (a.element == b.element) {
+ if (a.index <= b.index)
+ return {a, b};
+ return {b, a};
+ }
+ const litehtml::elements_vector aPath = path(a.element);
+ const litehtml::elements_vector bPath = path(b.element);
+ litehtml::element::ptr commonParent;
+ size_t firstDifferentIndex;
+ std::tie(commonParent, firstDifferentIndex) = getCommonParent(aPath, bPath);
+ if (!commonParent) {
+ qWarning() << "internal error: litehtml elements do not have common parent";
+ return {a, b};
+ }
+ if (commonParent == a.element)
+ return {a, a}; // 'a' already contains 'b'
+ if (commonParent == b.element)
+ return {b, b};
+ // find out if a or b is first in the child sub-trees of commonParent
+ const litehtml::element::ptr aBranch = aPath.at(firstDifferentIndex);
+ const litehtml::element::ptr bBranch = bPath.at(firstDifferentIndex);
+ for (int i = 0; i < int(commonParent->get_children_count()); ++i) {
+ const litehtml::element::ptr child = commonParent->get_child(i);
+ if (child == aBranch)
+ return {a, b};
+ if (child == bBranch)
+ return {b, a};
+ }
+ qWarning() << "internal error: failed to find out order of litehtml elements";
+ return {a, b};
+}
+
+static int findChild(const litehtml::element::ptr &child, const litehtml::element::ptr &parent)
+{
+ for (int i = 0; i < int(parent->get_children_count()); ++i)
+ if (parent->get_child(i) == child)
+ return i;
+ return -1;
+}
+
+// 1) stops right away if element == stop, otherwise stops whenever stop element is encountered
+// 2) moves down the first children from element until there is none anymore
+static litehtml::element::ptr firstLeaf(const litehtml::element::ptr &element,
+ const litehtml::element::ptr &stop)
+{
+ if (element == stop)
+ return element;
+ litehtml::element::ptr current = element;
+ while (current != stop && current->get_children_count() > 0)
+ current = current->get_child(0);
+ return current;
+}
+
+// 1) stops right away if element == stop, otherwise stops whenever stop element is encountered
+// 2) starts at next sibling (up the hierarchy chain) if possible, otherwise root
+// 3) returns first leaf of the element found in 2
+static litehtml::element::ptr nextLeaf(const litehtml::element::ptr &element,
+ const litehtml::element::ptr &stop)
+{
+ if (element == stop)
+ return element;
+ litehtml::element::ptr current = element;
+ if (current->have_parent()) {
+ // find next sibling
+ const litehtml::element::ptr parent = current->parent();
+ const int childIndex = findChild(current, parent);
+ if (childIndex < 0) {
+ qWarning() << "internal error: filed to find litehtml child element in parent";
+ return stop;
+ }
+ if (childIndex + 1 >= int(parent->get_children_count())) // no sibling, move up
+ return nextLeaf(parent, stop);
+ current = parent->get_child(childIndex + 1);
+ }
+ return firstLeaf(current, stop);
+}
+
+static Selection::Element selectionDetails(const litehtml::element::ptr &element,
+ const QString &text,
+ const QPoint &pos)
+{
+ // shortcut, which _might_ not really be correct
+ if (element->get_children_count() > 0)
+ return {element, -1, -1}; // everything selected
+ const QFont &font = toQFont(element->get_font());
+ const QFontMetrics fm(font);
+ int previous = 0;
+ for (int i = 0; i < text.size(); ++i) {
+ const int width = fm.size(0, text.left(i + 1)).width();
+ if ((width + previous) / 2 >= pos.x())
+ return {element, i, previous};
+ previous = width;
+ }
+ return {element, int(text.size()), previous};
+}
+
+static Selection::Element deepest_child_at_point(const litehtml::document::ptr &document,
+ const QPoint &pos,
+ const QPoint &viewportPos,
+ Selection::Mode mode)
+{
+ if (!document)
+ return {};
+
+ // the following does not find the "smallest" element, it often consists of children
+ // with individual words as text...
+ const litehtml::element::ptr element = document->root()->get_element_by_point(pos.x(),
+ pos.y(),
+ viewportPos.x(),
+ viewportPos.y());
+ // ...so try to find a better match
+ const std::function<Selection::Element(litehtml::element::ptr, QRect)> recursion =
+ [&recursion, pos, mode](const litehtml::element::ptr &element,
+ const QRect &placement) -> Selection::Element {
+ if (!element)
+ return {};
+ Selection::Element result;
+ for (int i = 0; i < int(element->get_children_count()); ++i) {
+ const litehtml::element::ptr child = element->get_child(i);
+ result = recursion(child,
+ toQRect(child->get_position()).translated(placement.topLeft()));
+ if (result.element)
+ return result;
+ }
+ if (placement.contains(pos)) {
+ litehtml::tstring text;
+ element->get_text(text);
+ if (!text.empty()) {
+ return mode == Selection::Mode::Free
+ ? selectionDetails(element,
+ QString::fromStdString(text),
+ pos - placement.topLeft())
+ : Selection::Element({element, -1, -1});
+ }
+ }
+ return {};
+ };
+ return recursion(element, element ? toQRect(element->get_placement()) : QRect());
+}
+
+// CSS: 400 == normal, 700 == bold.
+// Qt5: 50 == normal, 75 == bold
+// Qt6: == CSS
+static QFont::Weight cssWeightToQtWeight(int cssWeight)
+{
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ return QFont::Weight(cssWeight);
+#else
+ if (cssWeight <= 400)
+ return QFont::Weight(cssWeight * 50 / 400);
+ if (cssWeight >= 700)
+ return QFont::Weight(75 + (cssWeight - 700) * 25 / 300);
+ return QFont::Weight(50 + (cssWeight - 400) * 25 / 300);
+#endif
+}
+
+static QFont::Style toQFontStyle(litehtml::font_style style)
+{
+ switch (style) {
+ case litehtml::fontStyleNormal:
+ return QFont::StyleNormal;
+ case litehtml::fontStyleItalic:
+ return QFont::StyleItalic;
+ }
+ // should not happen
+ qWarning(log) << "Unknown litehtml font style:" << style;
+ return QFont::StyleNormal;
+}
+
+static QColor toQColor(const litehtml::web_color &color)
+{
+ return {color.red, color.green, color.blue, color.alpha};
+}
+
+static Qt::PenStyle borderPenStyle(litehtml::border_style style)
+{
+ switch (style) {
+ case litehtml::border_style_dotted:
+ return Qt::DotLine;
+ case litehtml::border_style_dashed:
+ return Qt::DashLine;
+ case litehtml::border_style_solid:
+ return Qt::SolidLine;
+ default:
+ qWarning(log) << "Unsupported border style:" << style;
+ }
+ return Qt::SolidLine;
+}
+
+static QPen borderPen(const litehtml::border &border)
+{
+ return {toQColor(border.color), qreal(border.width), borderPenStyle(border.style)};
+}
+
+static QCursor toQCursor(const QString &c)
+{
+ if (c == "alias")
+ return {Qt::PointingHandCursor}; // ???
+ if (c == "all-scroll")
+ return {Qt::SizeAllCursor};
+ if (c == "auto")
+ return {Qt::ArrowCursor}; // ???
+ if (c == "cell")
+ return {Qt::UpArrowCursor};
+ if (c == "context-menu")
+ return {Qt::ArrowCursor}; // ???
+ if (c == "col-resize")
+ return {Qt::SplitHCursor};
+ if (c == "copy")
+ return {Qt::DragCopyCursor};
+ if (c == "crosshair")
+ return {Qt::CrossCursor};
+ if (c == "default")
+ return {Qt::ArrowCursor};
+ if (c == "e-resize")
+ return {Qt::SizeHorCursor}; // ???
+ if (c == "ew-resize")
+ return {Qt::SizeHorCursor};
+ if (c == "grab")
+ return {Qt::OpenHandCursor};
+ if (c == "grabbing")
+ return {Qt::ClosedHandCursor};
+ if (c == "help")
+ return {Qt::WhatsThisCursor};
+ if (c == "move")
+ return {Qt::SizeAllCursor};
+ if (c == "n-resize")
+ return {Qt::SizeVerCursor}; // ???
+ if (c == "ne-resize")
+ return {Qt::SizeBDiagCursor}; // ???
+ if (c == "nesw-resize")
+ return {Qt::SizeBDiagCursor};
+ if (c == "ns-resize")
+ return {Qt::SizeVerCursor};
+ if (c == "nw-resize")
+ return {Qt::SizeFDiagCursor}; // ???
+ if (c == "nwse-resize")
+ return {Qt::SizeFDiagCursor};
+ if (c == "no-drop")
+ return {Qt::ForbiddenCursor};
+ if (c == "none")
+ return {Qt::BlankCursor};
+ if (c == "not-allowed")
+ return {Qt::ForbiddenCursor};
+ if (c == "pointer")
+ return {Qt::PointingHandCursor};
+ if (c == "progress")
+ return {Qt::BusyCursor};
+ if (c == "row-resize")
+ return {Qt::SplitVCursor};
+ if (c == "s-resize")
+ return {Qt::SizeVerCursor}; // ???
+ if (c == "se-resize")
+ return {Qt::SizeFDiagCursor}; // ???
+ if (c == "sw-resize")
+ return {Qt::SizeBDiagCursor}; // ???
+ if (c == "text")
+ return {Qt::IBeamCursor};
+ if (c == "url")
+ return {Qt::ArrowCursor}; // ???
+ if (c == "w-resize")
+ return {Qt::SizeHorCursor}; // ???
+ if (c == "wait")
+ return {Qt::BusyCursor};
+ if (c == "zoom-in")
+ return {Qt::ArrowCursor}; // ???
+ qWarning(log) << QString("unknown cursor property \"%1\"").arg(c).toUtf8().constData();
+ return {Qt::ArrowCursor};
+}
+
+bool Selection::isValid() const
+{
+ return !selection.isEmpty();
+}
+
+void Selection::update()
+{
+ const auto addElement = [this](const Selection::Element &element,
+ const Selection::Element &end = {}) {
+ litehtml::tstring elemText;
+ element.element->get_text(elemText);
+ const QString textStr = QString::fromStdString(elemText);
+ if (!textStr.isEmpty()) {
+ QRect rect = toQRect(element.element->get_placement()).adjusted(-1, -1, 1, 1);
+ if (element.index < 0) { // fully selected
+ text += textStr;
+ } else if (end.element) { // select from element "to end"
+ if (element.element == end.element) {
+ // end.index is guaranteed to be >= element.index by caller, same for x
+ text += textStr.mid(element.index, end.index - element.index);
+ const int left = rect.left();
+ rect.setLeft(left + element.x);
+ rect.setRight(left + end.x);
+ } else {
+ text += textStr.mid(element.index);
+ rect.setLeft(rect.left() + element.x);
+ }
+ } else { // select from start of element
+ text += textStr.left(element.index);
+ rect.setRight(rect.left() + element.x);
+ }
+ selection.append(rect);
+ }
+ };
+
+ if (startElem.element && endElem.element) {
+ // Edge cases:
+ // start and end elements could be reversed or children of each other
+ Selection::Element start;
+ Selection::Element end;
+ std::tie(start, end) = getStartAndEnd(startElem, endElem);
+
+ selection.clear();
+ text.clear();
+
+ // Treats start element as a leaf even if it isn't, because it already contains all its
+ // children
+ addElement(start, end);
+ if (start.element != end.element) {
+ litehtml::element::ptr current = start.element;
+ do {
+ current = nextLeaf(current, end.element);
+ if (current == end.element)
+ addElement(end);
+ else
+ addElement({current, -1, -1});
+ } while (current != end.element);
+ }
+ } else {
+ selection = {};
+ text.clear();
+ }
+ QClipboard *cb = QGuiApplication::clipboard();
+ if (cb->supportsSelection())
+ cb->setText(text, QClipboard::Selection);
+}
+
+QRect Selection::boundingRect() const
+{
+ QRect rect;
+ for (const QRect &r : selection)
+ rect = rect.united(r);
+ return rect;
+}
+
+DocumentContainer::DocumentContainer()
+ : d(new DocumentContainerPrivate)
+{}
+
+DocumentContainer::~DocumentContainer() = default;
+
+litehtml::uint_ptr DocumentContainerPrivate::create_font(const litehtml::tchar_t *faceName,
+ int size,
+ int weight,
+ litehtml::font_style italic,
+ unsigned int decoration,
+ litehtml::font_metrics *fm)
+{
+ const QStringList splitNames = QString::fromUtf8(faceName).split(',', Qt::SkipEmptyParts);
+ QStringList familyNames;
+ std::transform(splitNames.cbegin(),
+ splitNames.cend(),
+ std::back_inserter(familyNames),
+ [this](const QString &s) {
+ // clean whitespace and quotes
+ QString name = s.trimmed();
+ if (name.startsWith('\"'))
+ name = name.mid(1);
+ if (name.endsWith('\"'))
+ name.chop(1);
+ const QString lowerName = name.toLower();
+ if (lowerName == "serif")
+ return serifFont();
+ if (lowerName == "sans-serif")
+ return sansSerifFont();
+ if (lowerName == "monospace")
+ return monospaceFont();
+ return name;
+ });
+ auto font = new QFont();
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0))
+ font->setFamilies(familyNames);
+#else
+ struct CompareCaseinsensitive
+ {
+ bool operator()(const QString &a, const QString &b) const
+ {
+ return a.compare(b, Qt::CaseInsensitive) < 0;
+ }
+ };
+ static const QStringList known = QFontDatabase().families();
+ static const std::set<QString, CompareCaseinsensitive> knownFamilies(known.cbegin(),
+ known.cend());
+ font->setFamily(familyNames.last());
+ for (const QString &name : qAsConst(familyNames)) {
+ const auto found = knownFamilies.find(name);
+ if (found != knownFamilies.end()) {
+ font->setFamily(*found);
+ break;
+ }
+ }
+#endif
+ font->setPixelSize(size);
+ font->setWeight(cssWeightToQtWeight(weight));
+ font->setStyle(toQFontStyle(italic));
+ if (decoration == litehtml::font_decoration_underline)
+ font->setUnderline(true);
+ if (decoration == litehtml::font_decoration_overline)
+ font->setOverline(true);
+ if (decoration == litehtml::font_decoration_linethrough)
+ font->setStrikeOut(true);
+ if (fm) {
+ const QFontMetrics metrics(*font);
+ fm->height = metrics.height();
+ fm->ascent = metrics.ascent();
+ fm->descent = metrics.descent();
+ fm->x_height = metrics.xHeight();
+ fm->draw_spaces = true;
+ }
+ return reinterpret_cast<litehtml::uint_ptr>(font);
+}
+
+void DocumentContainerPrivate::delete_font(litehtml::uint_ptr hFont)
+{
+ auto font = reinterpret_cast<Font *>(hFont);
+ delete font;
+}
+
+int DocumentContainerPrivate::text_width(const litehtml::tchar_t *text, litehtml::uint_ptr hFont)
+{
+ const QFontMetrics fm(toQFont(hFont));
+ return fm.horizontalAdvance(QString::fromUtf8(text));
+}
+
+void DocumentContainerPrivate::draw_text(litehtml::uint_ptr hdc,
+ const litehtml::tchar_t *text,
+ litehtml::uint_ptr hFont,
+ litehtml::web_color color,
+ const litehtml::position &pos)
+{
+ auto painter = toQPainter(hdc);
+ painter->setFont(toQFont(hFont));
+ painter->setPen(toQColor(color));
+ painter->drawText(toQRect(pos), 0, QString::fromUtf8(text));
+}
+
+int DocumentContainerPrivate::pt_to_px(int pt)
+{
+ // magic factor of 11/12 to account for differences to webengine/webkit
+ return m_paintDevice->physicalDpiY() * pt * 11 / m_paintDevice->logicalDpiY() / 12;
+}
+
+int DocumentContainerPrivate::get_default_font_size() const
+{
+ return m_defaultFont.pointSize();
+}
+
+const litehtml::tchar_t *DocumentContainerPrivate::get_default_font_name() const
+{
+ return m_defaultFontFamilyName.constData();
+}
+
+void DocumentContainerPrivate::draw_list_marker(litehtml::uint_ptr hdc,
+ const litehtml::list_marker &marker)
+{
+ auto painter = toQPainter(hdc);
+ if (marker.image.empty()) {
+ if (marker.marker_type == litehtml::list_style_type_square) {
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(toQColor(marker.color));
+ painter->drawRect(toQRect(marker.pos));
+ } else if (marker.marker_type == litehtml::list_style_type_disc) {
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(toQColor(marker.color));
+ painter->drawEllipse(toQRect(marker.pos));
+ } else if (marker.marker_type == litehtml::list_style_type_circle) {
+ painter->setPen(toQColor(marker.color));
+ painter->setBrush(Qt::NoBrush);
+ painter->drawEllipse(toQRect(marker.pos));
+ } else {
+ // TODO we do not get information about index and font for e.g. decimal / roman
+ // at least draw a bullet
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(toQColor(marker.color));
+ painter->drawEllipse(toQRect(marker.pos));
+ qWarning(log) << "list marker of type" << marker.marker_type << "not supported";
+ }
+ } else {
+ const QPixmap pixmap = getPixmap(QString::fromStdString(marker.image),
+ QString::fromStdString(marker.baseurl));
+ painter->drawPixmap(toQRect(marker.pos), pixmap);
+ }
+}
+
+void DocumentContainerPrivate::load_image(const litehtml::tchar_t *src,
+ const litehtml::tchar_t *baseurl,
+ bool redraw_on_ready)
+{
+ const auto qtSrc = QString::fromUtf8(src);
+ const auto qtBaseUrl = QString::fromUtf8(baseurl);
+ Q_UNUSED(redraw_on_ready)
+ qDebug(log) << "load_image:" << QString("src = \"%1\";").arg(qtSrc).toUtf8().constData()
+ << QString("base = \"%1\"").arg(qtBaseUrl).toUtf8().constData();
+ const QUrl url = resolveUrl(qtSrc, qtBaseUrl);
+ if (m_pixmaps.contains(url))
+ return;
+
+ QPixmap pixmap;
+ pixmap.loadFromData(m_dataCallback(url));
+ m_pixmaps.insert(url, pixmap);
+}
+
+void DocumentContainerPrivate::get_image_size(const litehtml::tchar_t *src,
+ const litehtml::tchar_t *baseurl,
+ litehtml::size &sz)
+{
+ const auto qtSrc = QString::fromUtf8(src);
+ const auto qtBaseUrl = QString::fromUtf8(baseurl);
+ if (qtSrc.isEmpty()) // for some reason that happens
+ return;
+ qDebug(log) << "get_image_size:" << QString("src = \"%1\";").arg(qtSrc).toUtf8().constData()
+ << QString("base = \"%1\"").arg(qtBaseUrl).toUtf8().constData();
+ const QPixmap pm = getPixmap(qtSrc, qtBaseUrl);
+ sz.width = pm.width();
+ sz.height = pm.height();
+}
+
+void DocumentContainerPrivate::drawSelection(QPainter *painter, const QRect &clip) const
+{
+ painter->save();
+ painter->setClipRect(clip, Qt::IntersectClip);
+ for (const QRect &r : m_selection.selection) {
+ const QRect clientRect = r.translated(-m_scrollPosition);
+ const QPalette palette = m_paletteCallback();
+ painter->fillRect(clientRect, palette.brush(QPalette::Highlight));
+ }
+ painter->restore();
+}
+
+static QString tagName(const litehtml::element::ptr &e)
+{
+ litehtml::element::ptr current = e;
+ while (current && std::strlen(current->get_tagName()) == 0)
+ current = current->parent();
+ return current ? QString::fromUtf8(current->get_tagName()) : QString();
+}
+
+void DocumentContainerPrivate::buildIndex()
+{
+ m_index.elementToIndex.clear();
+ m_index.indexToElement.clear();
+ m_index.text.clear();
+
+ int index = 0;
+ bool inBody = false;
+ litehtml::element::ptr current = firstLeaf(m_document->root(), nullptr);
+ while (current != m_document->root()) {
+ m_index.elementToIndex.insert({current, index});
+ if (!inBody)
+ inBody = tagName(current).toLower() == "body";
+ if (inBody && current->is_visible()) {
+ litehtml::tstring text;
+ current->get_text(text);
+ if (!text.empty()) {
+ m_index.indexToElement.push_back({index, current});
+ const QString str = QString::fromStdString(text);
+ m_index.text += str;
+ index += str.size();
+ }
+ }
+ current = nextLeaf(current, m_document->root());
+ }
+}
+
+void DocumentContainerPrivate::draw_background(litehtml::uint_ptr hdc,
+ const litehtml::background_paint &bg)
+{
+ auto painter = toQPainter(hdc);
+ if (bg.is_root) {
+ // TODO ?
+ return;
+ }
+ painter->save();
+ painter->setClipRect(toQRect(bg.clip_box));
+ const QRegion horizontalMiddle(
+ QRect(bg.border_box.x,
+ bg.border_box.y + bg.border_radius.top_left_y,
+ bg.border_box.width,
+ bg.border_box.height - bg.border_radius.top_left_y - bg.border_radius.bottom_left_y));
+ const QRegion horizontalTop(
+ QRect(bg.border_box.x + bg.border_radius.top_left_x,
+ bg.border_box.y,
+ bg.border_box.width - bg.border_radius.top_left_x - bg.border_radius.top_right_x,
+ bg.border_radius.top_left_y));
+ const QRegion horizontalBottom(QRect(bg.border_box.x + bg.border_radius.bottom_left_x,
+ bg.border_box.bottom() - bg.border_radius.bottom_left_y,
+ bg.border_box.width - bg.border_radius.bottom_left_x
+ - bg.border_radius.bottom_right_x,
+ bg.border_radius.bottom_left_y));
+ const QRegion topLeft(QRect(bg.border_box.left(),
+ bg.border_box.top(),
+ 2 * bg.border_radius.top_left_x,
+ 2 * bg.border_radius.top_left_y),
+ QRegion::Ellipse);
+ const QRegion topRight(QRect(bg.border_box.right() - 2 * bg.border_radius.top_right_x,
+ bg.border_box.top(),
+ 2 * bg.border_radius.top_right_x,
+ 2 * bg.border_radius.top_right_y),
+ QRegion::Ellipse);
+ const QRegion bottomLeft(QRect(bg.border_box.left(),
+ bg.border_box.bottom() - 2 * bg.border_radius.bottom_left_y,
+ 2 * bg.border_radius.bottom_left_x,
+ 2 * bg.border_radius.bottom_left_y),
+ QRegion::Ellipse);
+ const QRegion bottomRight(QRect(bg.border_box.right() - 2 * bg.border_radius.bottom_right_x,
+ bg.border_box.bottom() - 2 * bg.border_radius.bottom_right_y,
+ 2 * bg.border_radius.bottom_right_x,
+ 2 * bg.border_radius.bottom_right_y),
+ QRegion::Ellipse);
+ const QRegion clipRegion = horizontalMiddle.united(horizontalTop)
+ .united(horizontalBottom)
+ .united(topLeft)
+ .united(topRight)
+ .united(bottomLeft)
+ .united(bottomRight);
+ painter->setClipRegion(clipRegion, Qt::IntersectClip);
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(toQColor(bg.color));
+ painter->drawRect(bg.border_box.x, bg.border_box.y, bg.border_box.width, bg.border_box.height);
+ drawSelection(painter, toQRect(bg.border_box));
+ if (!bg.image.empty()) {
+ const QPixmap pixmap = getPixmap(QString::fromStdString(bg.image),
+ QString::fromStdString(bg.baseurl));
+ if (bg.repeat == litehtml::background_repeat_no_repeat) {
+ painter->drawPixmap(QRect(bg.position_x,
+ bg.position_y,
+ bg.image_size.width,
+ bg.image_size.height),
+ pixmap);
+ } else if (bg.repeat == litehtml::background_repeat_repeat_x) {
+ if (bg.image_size.width > 0) {
+ int x = bg.border_box.left();
+ while (x <= bg.border_box.right()) {
+ painter->drawPixmap(QRect(x,
+ bg.border_box.top(),
+ bg.image_size.width,
+ bg.image_size.height),
+ pixmap);
+ x += bg.image_size.width;
+ }
+ }
+ } else {
+ qWarning(log) << "unsupported background repeat" << bg.repeat;
+ }
+ }
+ painter->restore();
+}
+
+void DocumentContainerPrivate::draw_borders(litehtml::uint_ptr hdc,
+ const litehtml::borders &borders,
+ const litehtml::position &draw_pos,
+ bool root)
+{
+ Q_UNUSED(root)
+ // TODO: special border styles
+ auto painter = toQPainter(hdc);
+ if (borders.top.style != litehtml::border_style_none
+ && borders.top.style != litehtml::border_style_hidden) {
+ painter->setPen(borderPen(borders.top));
+ painter->drawLine(draw_pos.left() + borders.radius.top_left_x,
+ draw_pos.top(),
+ draw_pos.right() - borders.radius.top_right_x,
+ draw_pos.top());
+ painter->drawArc(draw_pos.left(),
+ draw_pos.top(),
+ 2 * borders.radius.top_left_x,
+ 2 * borders.radius.top_left_y,
+ 90 * 16,
+ 90 * 16);
+ painter->drawArc(draw_pos.right() - 2 * borders.radius.top_right_x,
+ draw_pos.top(),
+ 2 * borders.radius.top_right_x,
+ 2 * borders.radius.top_right_y,
+ 0,
+ 90 * 16);
+ }
+ if (borders.bottom.style != litehtml::border_style_none
+ && borders.bottom.style != litehtml::border_style_hidden) {
+ painter->setPen(borderPen(borders.bottom));
+ painter->drawLine(draw_pos.left() + borders.radius.bottom_left_x,
+ draw_pos.bottom(),
+ draw_pos.right() - borders.radius.bottom_right_x,
+ draw_pos.bottom());
+ painter->drawArc(draw_pos.left(),
+ draw_pos.bottom() - 2 * borders.radius.bottom_left_y,
+ 2 * borders.radius.bottom_left_x,
+ 2 * borders.radius.bottom_left_y,
+ 180 * 16,
+ 90 * 16);
+ painter->drawArc(draw_pos.right() - 2 * borders.radius.bottom_right_x,
+ draw_pos.bottom() - 2 * borders.radius.bottom_right_y,
+ 2 * borders.radius.bottom_right_x,
+ 2 * borders.radius.bottom_right_y,
+ 270 * 16,
+ 90 * 16);
+ }
+ if (borders.left.style != litehtml::border_style_none
+ && borders.left.style != litehtml::border_style_hidden) {
+ painter->setPen(borderPen(borders.left));
+ painter->drawLine(draw_pos.left(),
+ draw_pos.top() + borders.radius.top_left_y,
+ draw_pos.left(),
+ draw_pos.bottom() - borders.radius.bottom_left_y);
+ }
+ if (borders.right.style != litehtml::border_style_none
+ && borders.right.style != litehtml::border_style_hidden) {
+ painter->setPen(borderPen(borders.right));
+ painter->drawLine(draw_pos.right(),
+ draw_pos.top() + borders.radius.top_right_y,
+ draw_pos.right(),
+ draw_pos.bottom() - borders.radius.bottom_right_y);
+ }
+}
+
+void DocumentContainerPrivate::set_caption(const litehtml::tchar_t *caption)
+{
+ m_caption = QString::fromUtf8(caption);
+}
+
+void DocumentContainerPrivate::set_base_url(const litehtml::tchar_t *base_url)
+{
+ m_baseUrl = QString::fromUtf8(base_url);
+}
+
+void DocumentContainerPrivate::link(const std::shared_ptr<litehtml::document> &doc,
+ const litehtml::element::ptr &el)
+{
+ // TODO
+ qDebug(log) << "link";
+ Q_UNUSED(doc)
+ Q_UNUSED(el)
+}
+
+void DocumentContainerPrivate::on_anchor_click(const litehtml::tchar_t *url,
+ const litehtml::element::ptr &el)
+{
+ Q_UNUSED(el)
+ if (!m_blockLinks)
+ m_linkCallback(resolveUrl(QString::fromUtf8(url), m_baseUrl));
+}
+
+void DocumentContainerPrivate::set_cursor(const litehtml::tchar_t *cursor)
+{
+ m_cursorCallback(toQCursor(QString::fromUtf8(cursor)));
+}
+
+void DocumentContainerPrivate::transform_text(litehtml::tstring &text, litehtml::text_transform tt)
+{
+ // TODO
+ qDebug(log) << "transform_text";
+ Q_UNUSED(text)
+ Q_UNUSED(tt)
+}
+
+void DocumentContainerPrivate::import_css(litehtml::tstring &text,
+ const litehtml::tstring &url,
+ litehtml::tstring &baseurl)
+{
+ const QUrl actualUrl = resolveUrl(QString::fromStdString(url), QString::fromStdString(baseurl));
+ const QString urlString = actualUrl.toString(QUrl::None);
+ const int lastSlash = urlString.lastIndexOf('/');
+ baseurl = urlString.left(lastSlash).toStdString();
+ text = QString::fromUtf8(m_dataCallback(actualUrl)).toStdString();
+}
+
+void DocumentContainerPrivate::set_clip(const litehtml::position &pos,
+ const litehtml::border_radiuses &bdr_radius,
+ bool valid_x,
+ bool valid_y)
+{
+ // TODO
+ qDebug(log) << "set_clip";
+ Q_UNUSED(pos)
+ Q_UNUSED(bdr_radius)
+ Q_UNUSED(valid_x)
+ Q_UNUSED(valid_y)
+}
+
+void DocumentContainerPrivate::del_clip()
+{
+ // TODO
+ qDebug(log) << "del_clip";
+}
+
+void DocumentContainerPrivate::get_client_rect(litehtml::position &client) const
+{
+ client = {m_clientRect.x(), m_clientRect.y(), m_clientRect.width(), m_clientRect.height()};
+}
+
+std::shared_ptr<litehtml::element> DocumentContainerPrivate::create_element(
+ const litehtml::tchar_t *tag_name,
+ const litehtml::string_map &attributes,
+ const std::shared_ptr<litehtml::document> &doc)
+{
+ // TODO
+ qDebug(log) << "create_element" << QString::fromUtf8(tag_name);
+ Q_UNUSED(attributes)
+ Q_UNUSED(doc)
+ return {};
+}
+
+void DocumentContainerPrivate::get_media_features(litehtml::media_features &media) const
+{
+ media.type = litehtml::media_type_screen;
+ // TODO
+ qDebug(log) << "get_media_features";
+}
+
+void DocumentContainerPrivate::get_language(litehtml::tstring &language, litehtml::tstring &culture) const
+{
+ // TODO
+ qDebug(log) << "get_language";
+ Q_UNUSED(language)
+ Q_UNUSED(culture)
+}
+
+void DocumentContainer::setPaintDevice(QPaintDevice *paintDevice)
+{
+ d->m_paintDevice = paintDevice;
+}
+
+void DocumentContainer::setScrollPosition(const QPoint &pos)
+{
+ d->m_scrollPosition = pos;
+}
+
+void DocumentContainer::setDocument(const QByteArray &data, DocumentContainerContext *context)
+{
+ d->m_pixmaps.clear();
+ d->m_selection = {};
+ d->m_document = litehtml::document::createFromUTF8(data.constData(), d.get(), &context->d->context);
+ d->buildIndex();
+}
+
+bool DocumentContainer::hasDocument() const
+{
+ return d->m_document.get();
+}
+
+void DocumentContainer::setBaseUrl(const QString &url)
+{
+ d->set_base_url(url.toUtf8().constData());
+}
+
+void DocumentContainer::render(int width, int height)
+{
+ d->m_clientRect = {0, 0, width, height};
+ if (!d->m_document)
+ return;
+ d->m_document->render(width);
+ d->m_selection.update();
+}
+
+void DocumentContainer::draw(QPainter *painter, const QRect &clip)
+{
+ d->drawSelection(painter, clip);
+ const QPoint pos = -d->m_scrollPosition;
+ const litehtml::position clipRect = {clip.x(), clip.y(), clip.width(), clip.height()};
+ d->m_document->draw(reinterpret_cast<litehtml::uint_ptr>(painter), pos.x(), pos.y(), &clipRect);
+}
+
+int DocumentContainer::documentWidth() const
+{
+ return d->m_document->width();
+}
+
+int DocumentContainer::documentHeight() const
+{
+ return d->m_document->height();
+}
+
+int DocumentContainer::anchorY(const QString &anchorName) const
+{
+ litehtml::element::ptr element = d->m_document->root()->select_one(
+ QString("#%1").arg(anchorName).toStdString());
+ if (!element) {
+ element = d->m_document->root()->select_one(QString("[name=%1]").arg(anchorName).toStdString());
+ }
+ if (element)
+ return element->get_placement().y;
+ return -1;
+}
+
+QVector<QRect> DocumentContainer::mousePressEvent(const QPoint &documentPos,
+ const QPoint &viewportPos,
+ Qt::MouseButton button)
+{
+ if (!d->m_document || button != Qt::LeftButton)
+ return {};
+ QVector<QRect> redrawRects;
+ // selection
+ if (d->m_selection.isValid())
+ redrawRects.append(d->m_selection.boundingRect());
+ d->m_selection = {};
+ d->m_selection.selectionStartDocumentPos = documentPos;
+ d->m_selection.startElem = deepest_child_at_point(d->m_document,
+ documentPos,
+ viewportPos,
+ d->m_selection.mode);
+ // post to litehtml
+ litehtml::position::vector redrawBoxes;
+ if (d->m_document->on_lbutton_down(
+ documentPos.x(), documentPos.y(), viewportPos.x(), viewportPos.y(), redrawBoxes)) {
+ for (const litehtml::position &box : redrawBoxes)
+ redrawRects.append(toQRect(box));
+ }
+ return redrawRects;
+}
+
+QVector<QRect> DocumentContainer::mouseMoveEvent(const QPoint &documentPos,
+ const QPoint &viewportPos)
+{
+ if (!d->m_document)
+ return {};
+ QVector<QRect> redrawRects;
+ // selection
+ if (d->m_selection.isSelecting
+ || (!d->m_selection.selectionStartDocumentPos.isNull()
+ && (d->m_selection.selectionStartDocumentPos - documentPos).manhattanLength() >= kDragDistance
+ && d->m_selection.startElem.element)) {
+ const Selection::Element element = deepest_child_at_point(d->m_document,
+ documentPos,
+ viewportPos,
+ d->m_selection.mode);
+ if (element.element) {
+ redrawRects.append(
+ d->m_selection.boundingRect() /*.adjusted(-1, -1, +1, +1)*/); // redraw old selection area
+ d->m_selection.endElem = element;
+ d->m_selection.update();
+ redrawRects.append(d->m_selection.boundingRect());
+ }
+ d->m_selection.isSelecting = true;
+ }
+ litehtml::position::vector redrawBoxes;
+ if (d->m_document->on_mouse_over(
+ documentPos.x(), documentPos.y(), viewportPos.x(), viewportPos.y(), redrawBoxes)) {
+ for (const litehtml::position &box : redrawBoxes)
+ redrawRects.append(toQRect(box));
+ }
+ return redrawRects;
+}
+
+QVector<QRect> DocumentContainer::mouseReleaseEvent(const QPoint &documentPos,
+ const QPoint &viewportPos,
+ Qt::MouseButton button)
+{
+ if (!d->m_document || button != Qt::LeftButton)
+ return {};
+ QVector<QRect> redrawRects;
+ // selection
+ d->m_selection.isSelecting = false;
+ d->m_selection.selectionStartDocumentPos = {};
+ if (d->m_selection.isValid())
+ d->m_blockLinks = true;
+ else
+ d->m_selection = {};
+ litehtml::position::vector redrawBoxes;
+ if (d->m_document->on_lbutton_up(
+ documentPos.x(), documentPos.y(), viewportPos.x(), viewportPos.y(), redrawBoxes)) {
+ for (const litehtml::position &box : redrawBoxes)
+ redrawRects.append(toQRect(box));
+ }
+ d->m_blockLinks = false;
+ return redrawRects;
+}
+
+QVector<QRect> DocumentContainer::mouseDoubleClickEvent(const QPoint &documentPos,
+ const QPoint &viewportPos,
+ Qt::MouseButton button)
+{
+ if (!d->m_document || button != Qt::LeftButton)
+ return {};
+ QVector<QRect> redrawRects;
+ d->m_selection = {};
+ d->m_selection.mode = Selection::Mode::Word;
+ const Selection::Element element = deepest_child_at_point(d->m_document,
+ documentPos,
+ viewportPos,
+ d->m_selection.mode);
+ if (element.element) {
+ d->m_selection.startElem = element;
+ d->m_selection.endElem = d->m_selection.startElem;
+ d->m_selection.isSelecting = true;
+ d->m_selection.update();
+ if (d->m_selection.isValid())
+ redrawRects.append(d->m_selection.boundingRect());
+ } else {
+ if (d->m_selection.isValid())
+ redrawRects.append(d->m_selection.boundingRect());
+ d->m_selection = {};
+ }
+ return redrawRects;
+}
+
+QVector<QRect> DocumentContainer::leaveEvent()
+{
+ if (!d->m_document)
+ return {};
+ litehtml::position::vector redrawBoxes;
+ if (d->m_document->on_mouse_leave(redrawBoxes)) {
+ QVector<QRect> redrawRects;
+ for (const litehtml::position &box : redrawBoxes)
+ redrawRects.append(toQRect(box));
+ return redrawRects;
+ }
+ return {};
+}
+
+QUrl DocumentContainer::linkAt(const QPoint &documentPos, const QPoint &viewportPos)
+{
+ if (!d->m_document)
+ return {};
+ const litehtml::element::ptr element = d->m_document->root()->get_element_by_point(
+ documentPos.x(), documentPos.y(), viewportPos.x(), viewportPos.y());
+ const char *href = element->get_attr("href");
+ if (href)
+ return d->resolveUrl(QString::fromUtf8(href), d->m_baseUrl);
+ return {};
+}
+
+QString DocumentContainer::caption() const
+{
+ return d->m_caption;
+}
+
+QString DocumentContainer::selectedText() const
+{
+ return d->m_selection.text;
+}
+
+void DocumentContainer::findText(const QString &text,
+ QTextDocument::FindFlags flags,
+ bool incremental,
+ bool *wrapped,
+ bool *success,
+ QVector<QRect> *oldSelection,
+ QVector<QRect> *newSelection)
+{
+ if (success)
+ *success = false;
+ if (oldSelection)
+ oldSelection->clear();
+ if (newSelection)
+ newSelection->clear();
+ if (!d->m_document)
+ return;
+ const bool backward = flags & QTextDocument::FindBackward;
+ int startIndex = backward ? -1 : 0;
+ if (d->m_selection.startElem.element && d->m_selection.endElem.element) { // selection
+ // poor-man's incremental search starts at beginning of selection,
+ // non-incremental at end (forward search) or beginning (backward search)
+ Selection::Element start;
+ Selection::Element end;
+ std::tie(start, end) = getStartAndEnd(d->m_selection.startElem, d->m_selection.endElem);
+ Selection::Element searchStart;
+ if (incremental || backward) {
+ if (start.index < 0) // fully selected
+ searchStart = {firstLeaf(start.element, nullptr), 0, -1};
+ else
+ searchStart = start;
+ } else {
+ if (end.index < 0) // fully selected
+ searchStart = {nextLeaf(end.element, nullptr), 0, -1};
+ else
+ searchStart = end;
+ }
+ const auto findInIndex = d->m_index.elementToIndex.find(searchStart.element);
+ if (findInIndex == std::end(d->m_index.elementToIndex)) {
+ qWarning() << "internal error: cannot find litehmtl element in index";
+ return;
+ }
+ startIndex = findInIndex->second + searchStart.index;
+ if (backward)
+ --startIndex;
+ }
+
+ const auto fillXPos = [](const Selection::Element &e) {
+ litehtml::tstring ttext;
+ e.element->get_text(ttext);
+ const QString text = QString::fromStdString(ttext);
+ const QFont &font = toQFont(e.element->get_font());
+ const QFontMetrics fm(font);
+ return Selection::Element{e.element, e.index, fm.size(0, text.left(e.index)).width()};
+ };
+
+ QString term = QRegularExpression::escape(text);
+ if (flags & QTextDocument::FindWholeWords)
+ term = QString("\\b%1\\b").arg(term);
+ const QRegularExpression::PatternOptions patternOptions
+ = (flags & QTextDocument::FindCaseSensitively) ? QRegularExpression::NoPatternOption
+ : QRegularExpression::CaseInsensitiveOption;
+ const QRegularExpression expression(term, patternOptions);
+
+ int foundIndex = backward ? d->m_index.text.lastIndexOf(expression, startIndex)
+ : d->m_index.text.indexOf(expression, startIndex);
+ if (foundIndex < 0) { // wrap
+ foundIndex = backward ? d->m_index.text.lastIndexOf(expression)
+ : d->m_index.text.indexOf(expression);
+ if (wrapped && foundIndex >= 0)
+ *wrapped = true;
+ }
+ if (foundIndex >= 0) {
+ const Index::Entry startEntry = d->m_index.findElement(foundIndex);
+ const Index::Entry endEntry = d->m_index.findElement(foundIndex + text.size());
+ if (!startEntry.second || !endEntry.second) {
+ qWarning() << "internal error: search ended up with nullptr elements";
+ return;
+ }
+ if (oldSelection)
+ *oldSelection = d->m_selection.selection;
+ d->m_selection = {};
+ d->m_selection.startElem = fillXPos({startEntry.second, foundIndex - startEntry.first, -1});
+ d->m_selection.endElem = fillXPos(
+ {endEntry.second, int(foundIndex + text.size() - endEntry.first), -1});
+ d->m_selection.update();
+ if (newSelection)
+ *newSelection = d->m_selection.selection;
+ if (success)
+ *success = true;
+ return;
+ }
+ return;
+}
+
+void DocumentContainer::setDefaultFont(const QFont &font)
+{
+ d->m_defaultFont = font;
+ d->m_defaultFontFamilyName = d->m_defaultFont.family().toUtf8();
+}
+
+QFont DocumentContainer::defaultFont() const
+{
+ return d->m_defaultFont;
+}
+
+void DocumentContainer::setDataCallback(const DocumentContainer::DataCallback &callback)
+{
+ d->m_dataCallback = callback;
+}
+
+void DocumentContainer::setCursorCallback(const DocumentContainer::CursorCallback &callback)
+{
+ d->m_cursorCallback = callback;
+}
+
+void DocumentContainer::setLinkCallback(const DocumentContainer::LinkCallback &callback)
+{
+ d->m_linkCallback = callback;
+}
+
+void DocumentContainer::setPaletteCallback(const DocumentContainer::PaletteCallback &callback)
+{
+ d->m_paletteCallback = callback;
+}
+
+static litehtml::element::ptr elementForY(int y, const litehtml::document::ptr &document)
+{
+ if (!document)
+ return {};
+
+ const std::function<litehtml::element::ptr(int, litehtml::element::ptr)> recursion =
+ [&recursion](int y, const litehtml::element::ptr &element) {
+ litehtml::element::ptr result;
+ const int subY = y - element->get_position().y;
+ if (subY <= 0)
+ return element;
+ for (int i = 0; i < int(element->get_children_count()); ++i) {
+ const litehtml::element::ptr child = element->get_child(i);
+ result = recursion(subY, child);
+ if (result)
+ return result;
+ }
+ return result;
+ };
+
+ return recursion(y, document->root());
+}
+
+int DocumentContainer::withFixedElementPosition(int y, const std::function<void()> &action)
+{
+ const litehtml::element::ptr element = elementForY(y, d->m_document);
+ action();
+ if (element)
+ return element->get_placement().y;
+ return -1;
+}
+
+QPixmap DocumentContainerPrivate::getPixmap(const QString &imageUrl, const QString &baseUrl)
+{
+ const QString actualBaseurl = baseUrl.isEmpty() ? m_baseUrl : baseUrl;
+ const QUrl url = resolveUrl(imageUrl, baseUrl);
+ if (!m_pixmaps.contains(url)) {
+ qWarning(log) << "draw_background: pixmap not loaded for" << url;
+ return {};
+ }
+ return m_pixmaps.value(url);
+}
+
+QString DocumentContainerPrivate::serifFont() const
+{
+ // TODO make configurable
+ return {"Times New Roman"};
+}
+
+QString DocumentContainerPrivate::sansSerifFont() const
+{
+ // TODO make configurable
+ return {"Arial"};
+}
+
+QString DocumentContainerPrivate::monospaceFont() const
+{
+ // TODO make configurable
+ return {"Courier"};
+}
+
+QUrl DocumentContainerPrivate::resolveUrl(const QString &url, const QString &baseUrl) const
+{
+ const QUrl qurl(url);
+ if (qurl.isRelative() && !qurl.path(QUrl::FullyEncoded).isEmpty()) {
+ const QString actualBaseurl = baseUrl.isEmpty() ? m_baseUrl : baseUrl;
+ QUrl resolvedUrl(actualBaseurl + '/' + url);
+ resolvedUrl.setPath(QDir::cleanPath(resolvedUrl.path(QUrl::FullyEncoded)));
+ return resolvedUrl;
+ }
+ return qurl;
+}
+
+Index::Entry Index::findElement(int index) const
+{
+ const auto upper = std::upper_bound(std::begin(indexToElement),
+ std::end(indexToElement),
+ Entry{index, {}},
+ [](const Entry &a, const Entry &b) {
+ return a.first < b.first;
+ });
+ if (upper == std::begin(indexToElement)) // should not happen for index >= 0
+ return {-1, {}};
+ return *(upper - 1);
+}
+
+DocumentContainerContext::DocumentContainerContext()
+ : d(new DocumentContainerContextPrivate)
+{}
+
+DocumentContainerContext::~DocumentContainerContext() = default;
+
+void DocumentContainerContext::setMasterStyleSheet(const QString &css)
+{
+ d->context.load_master_stylesheet(css.toUtf8().constData());
+}
diff --git a/src/container_qpainter.h b/src/container_qpainter.h
new file mode 100644
index 0000000..66b10c2
--- /dev/null
+++ b/src/container_qpainter.h
@@ -0,0 +1,124 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of QLiteHtml.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include "qlitehtml_global.h"
+
+#include <QByteArray>
+#include <QPaintDevice>
+#include <QPainter>
+#include <QPoint>
+#include <QRect>
+#include <QString>
+#include <QTextDocument>
+#include <QUrl>
+#include <QVector>
+
+#include <functional>
+#include <memory>
+
+class DocumentContainerPrivate;
+class DocumentContainerContextPrivate;
+
+class QLITEHTML_EXPORT DocumentContainerContext
+{
+public:
+ DocumentContainerContext();
+ ~DocumentContainerContext();
+
+ void setMasterStyleSheet(const QString &css);
+
+private:
+ std::unique_ptr<DocumentContainerContextPrivate> d;
+
+ friend class DocumentContainer;
+ friend class DocumentContainerPrivate;
+};
+
+class QLITEHTML_EXPORT DocumentContainer
+{
+public:
+ DocumentContainer();
+ virtual ~DocumentContainer();
+
+public: // outside API
+ void setPaintDevice(QPaintDevice *paintDevice);
+ void setDocument(const QByteArray &data, DocumentContainerContext *context);
+ bool hasDocument() const;
+ void setBaseUrl(const QString &url);
+ void setScrollPosition(const QPoint &pos);
+ void render(int width, int height);
+ void draw(QPainter *painter, const QRect &clip);
+ int documentWidth() const;
+ int documentHeight() const;
+ int anchorY(const QString &anchorName) const;
+
+ // these return areas to redraw in document space
+ QVector<QRect> mousePressEvent(const QPoint &documentPos,
+ const QPoint &viewportPos,
+ Qt::MouseButton button);
+ QVector<QRect> mouseMoveEvent(const QPoint &documentPos, const QPoint &viewportPos);
+ QVector<QRect> mouseReleaseEvent(const QPoint &documentPos,
+ const QPoint &viewportPos,
+ Qt::MouseButton button);
+ QVector<QRect> mouseDoubleClickEvent(const QPoint &documentPos,
+ const QPoint &viewportPos,
+ Qt::MouseButton button);
+ QVector<QRect> leaveEvent();
+
+ QUrl linkAt(const QPoint &documentPos, const QPoint &viewportPos);
+
+ QString caption() const;
+ QString selectedText() const;
+
+ void findText(const QString &text,
+ QTextDocument::FindFlags flags,
+ bool incremental,
+ bool *wrapped,
+ bool *success,
+ QVector<QRect> *oldSelection,
+ QVector<QRect> *newSelection);
+
+ void setDefaultFont(const QFont &font);
+ QFont defaultFont() const;
+
+ using DataCallback = std::function<QByteArray(QUrl)>;
+ void setDataCallback(const DataCallback &callback);
+
+ using CursorCallback = std::function<void(QCursor)>;
+ void setCursorCallback(const CursorCallback &callback);
+
+ using LinkCallback = std::function<void(QUrl)>;
+ void setLinkCallback(const LinkCallback &callback);
+
+ using PaletteCallback = std::function<QPalette()>;
+ void setPaletteCallback(const PaletteCallback &callback);
+
+ int withFixedElementPosition(int y, const std::function<void()> &action);
+
+private:
+ std::unique_ptr<DocumentContainerPrivate> d;
+};
diff --git a/src/container_qpainter_p.h b/src/container_qpainter_p.h
new file mode 100644
index 0000000..0391147
--- /dev/null
+++ b/src/container_qpainter_p.h
@@ -0,0 +1,163 @@
+/****************************************************************************
+**
+** Copyright (C) 2020 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of QLiteHtml.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include "container_qpainter.h"
+
+#include <litehtml.h>
+
+#include <QPaintDevice>
+#include <QPixmap>
+#include <QPoint>
+#include <QRect>
+#include <QString>
+#include <QVector>
+
+#include <unordered_map>
+
+class Selection
+{
+public:
+ struct Element
+ {
+ litehtml::element::ptr element;
+ int index = -1;
+ int x = -1;
+ };
+
+ enum class Mode { Free, Word };
+
+ bool isValid() const;
+
+ void update();
+ QRect boundingRect() const;
+
+ Element startElem;
+ Element endElem;
+ QVector<QRect> selection;
+ QString text;
+
+ QPoint selectionStartDocumentPos;
+ Mode mode = Mode::Free;
+ bool isSelecting = false;
+};
+
+struct Index
+{
+ QString text;
+ // only contains leaf elements
+ std::unordered_map<litehtml::element::ptr, int> elementToIndex;
+
+ using Entry = std::pair<int, litehtml::element::ptr>;
+ std::vector<Entry> indexToElement;
+
+ Entry findElement(int index) const;
+};
+
+class DocumentContainerPrivate : public litehtml::document_container
+{
+public: // document_container API
+ litehtml::uint_ptr create_font(const litehtml::tchar_t *faceName,
+ int size,
+ int weight,
+ litehtml::font_style italic,
+ unsigned int decoration,
+ litehtml::font_metrics *fm) override;
+ void delete_font(litehtml::uint_ptr hFont) override;
+ int text_width(const litehtml::tchar_t *text, litehtml::uint_ptr hFont) override;
+ void draw_text(litehtml::uint_ptr hdc,
+ const litehtml::tchar_t *text,
+ litehtml::uint_ptr hFont,
+ litehtml::web_color color,
+ const litehtml::position &pos) override;
+ int pt_to_px(int pt) override;
+ int get_default_font_size() const override;
+ const litehtml::tchar_t *get_default_font_name() const override;
+ void draw_list_marker(litehtml::uint_ptr hdc, const litehtml::list_marker &marker) override;
+ void load_image(const litehtml::tchar_t *src,
+ const litehtml::tchar_t *baseurl,
+ bool redraw_on_ready) override;
+ void get_image_size(const litehtml::tchar_t *src,
+ const litehtml::tchar_t *baseurl,
+ litehtml::size &sz) override;
+ void draw_background(litehtml::uint_ptr hdc, const litehtml::background_paint &bg) override;
+ void draw_borders(litehtml::uint_ptr hdc,
+ const litehtml::borders &borders,
+ const litehtml::position &draw_pos,
+ bool root) override;
+ void set_caption(const litehtml::tchar_t *caption) override;
+ void set_base_url(const litehtml::tchar_t *base_url) override;
+ void link(const std::shared_ptr<litehtml::document> &doc,
+ const litehtml::element::ptr &el) override;
+ void on_anchor_click(const litehtml::tchar_t *url, const litehtml::element::ptr &el) override;
+ void set_cursor(const litehtml::tchar_t *cursor) override;
+ void transform_text(litehtml::tstring &text, litehtml::text_transform tt) override;
+ void import_css(litehtml::tstring &text,
+ const litehtml::tstring &url,
+ litehtml::tstring &baseurl) override;
+ void set_clip(const litehtml::position &pos,
+ const litehtml::border_radiuses &bdr_radius,
+ bool valid_x,
+ bool valid_y) override;
+ void del_clip() override;
+ void get_client_rect(litehtml::position &client) const override;
+ std::shared_ptr<litehtml::element> create_element(const litehtml::tchar_t *tag_name,
+ const litehtml::string_map &attributes,
+ const std::shared_ptr<litehtml::document> &doc) override;
+ void get_media_features(litehtml::media_features &media) const override;
+ void get_language(litehtml::tstring &language, litehtml::tstring &culture) const override;
+
+ QPixmap getPixmap(const QString &imageUrl, const QString &baseUrl);
+ QString serifFont() const;
+ QString sansSerifFont() const;
+ QString monospaceFont() const;
+ QUrl resolveUrl(const QString &url, const QString &baseUrl) const;
+ void drawSelection(QPainter *painter, const QRect &clip) const;
+ void buildIndex();
+
+ QPaintDevice *m_paintDevice = nullptr;
+ litehtml::document::ptr m_document;
+ Index m_index;
+ QString m_baseUrl;
+ QRect m_clientRect;
+ QPoint m_scrollPosition;
+ QString m_caption;
+ QFont m_defaultFont = QFont(sansSerifFont(), 16);
+ QByteArray m_defaultFontFamilyName = m_defaultFont.family().toUtf8();
+ QHash<QUrl, QPixmap> m_pixmaps;
+ Selection m_selection;
+ DocumentContainer::DataCallback m_dataCallback;
+ DocumentContainer::CursorCallback m_cursorCallback;
+ DocumentContainer::LinkCallback m_linkCallback;
+ DocumentContainer::PaletteCallback m_paletteCallback;
+ bool m_blockLinks = false;
+};
+
+class DocumentContainerContextPrivate
+{
+public:
+ litehtml::context context;
+};
diff --git a/src/qlitehtml.pri b/src/qlitehtml.pri
new file mode 100644
index 0000000..a4b6918
--- /dev/null
+++ b/src/qlitehtml.pri
@@ -0,0 +1,164 @@
+exists($$PWD/3rdparty/litehtml/CMakeLists.txt) {
+ LH_SRC = $$PWD/3rdparty/litehtml
+ LH_HDR = $$LH_SRC/include/litehtml
+ GB_SRC = $$PWD/3rdparty/litehtml/src/gumbo
+ GB_HDR = $$GB_SRC/include/gumbo
+
+ # gumbo
+ SOURCES += \
+ $$GB_SRC/attribute.c \
+ $$GB_SRC/char_ref.c \
+ $$GB_SRC/error.c \
+ $$GB_SRC/parser.c \
+ $$GB_SRC/string_buffer.c \
+ $$GB_SRC/string_piece.c \
+ $$GB_SRC/tag.c \
+ $$GB_SRC/tokenizer.c \
+ $$GB_SRC/utf8.c \
+ $$GB_SRC/util.c \
+ $$GB_SRC/vector.c
+
+ HEADERS += \
+ $$GB_SRC/include//gumbo.h \
+ $$GB_HDR/attribute.h \
+ $$GB_HDR/char_ref.h \
+ $$GB_HDR/error.h \
+ $$GB_HDR/insertion_mode.h \
+ $$GB_HDR/parser.h \
+ $$GB_HDR/string_buffer.h \
+ $$GB_HDR/string_piece.h \
+ $$GB_HDR/tag_enum.h \
+ $$GB_HDR/tag_gperf.h \
+ $$GB_HDR/tag_sizes.h \
+ $$GB_HDR/tag_strings.h \
+ $$GB_HDR/token_type.h \
+ $$GB_HDR/tokenizer.h \
+ $$GB_HDR/tokenizer_states.h \
+ $$GB_HDR/utf8.h \
+ $$GB_HDR/util.h \
+ $$GB_HDR/vector.h
+
+ INCLUDEPATH *= $$GB_SRC/include $$GB_HDR
+
+ win32 {
+ HEADERS += \
+ $$GB_SRC/visualc/include/strings.h
+ INCLUDEPATH *= $$GB_SRC/visualc/include
+ }
+
+ # litehtml
+ SOURCES += \
+ $$LH_SRC/src/background.cpp \
+ $$LH_SRC/src/box.cpp \
+ $$LH_SRC/src/context.cpp \
+ $$LH_SRC/src/css_length.cpp \
+ $$LH_SRC/src/css_selector.cpp \
+ $$LH_SRC/src/document.cpp \
+ $$LH_SRC/src/el_anchor.cpp \
+ $$LH_SRC/src/el_base.cpp \
+ $$LH_SRC/src/el_before_after.cpp \
+ $$LH_SRC/src/el_body.cpp \
+ $$LH_SRC/src/el_break.cpp \
+ $$LH_SRC/src/el_cdata.cpp \
+ $$LH_SRC/src/el_comment.cpp \
+ $$LH_SRC/src/el_div.cpp \
+ $$LH_SRC/src/element.cpp \
+ $$LH_SRC/src/el_font.cpp \
+ $$LH_SRC/src/el_image.cpp \
+ $$LH_SRC/src/el_li.cpp \
+ $$LH_SRC/src/el_link.cpp \
+ $$LH_SRC/src/el_para.cpp \
+ $$LH_SRC/src/el_script.cpp \
+ $$LH_SRC/src/el_space.cpp \
+ $$LH_SRC/src/el_style.cpp \
+ $$LH_SRC/src/el_table.cpp \
+ $$LH_SRC/src/el_td.cpp \
+ $$LH_SRC/src/el_text.cpp \
+ $$LH_SRC/src/el_title.cpp \
+ $$LH_SRC/src/el_tr.cpp \
+ $$LH_SRC/src/html.cpp \
+ $$LH_SRC/src/html_tag.cpp \
+ $$LH_SRC/src/iterators.cpp \
+ $$LH_SRC/src/media_query.cpp \
+ $$LH_SRC/src/num_cvt.cpp \
+ $$LH_SRC/src/style.cpp \
+ $$LH_SRC/src/stylesheet.cpp \
+ $$LH_SRC/src/table.cpp \
+ $$LH_SRC/src/utf8_strings.cpp \
+ $$LH_SRC/src/web_color.cpp
+
+ HEADERS += \
+ $$LH_SRC/include/litehtml.h \
+ $$LH_HDR/attributes.h \
+ $$LH_HDR/background.h \
+ $$LH_HDR/borders.h \
+ $$LH_HDR/box.h \
+ $$LH_HDR/context.h \
+ $$LH_HDR/css_length.h \
+ $$LH_HDR/css_margins.h \
+ $$LH_HDR/css_offsets.h \
+ $$LH_HDR/css_position.h \
+ $$LH_HDR/css_selector.h \
+ $$LH_HDR/document.h \
+ $$LH_HDR/el_anchor.h \
+ $$LH_HDR/el_base.h \
+ $$LH_HDR/el_before_after.h \
+ $$LH_HDR/el_body.h \
+ $$LH_HDR/el_break.h \
+ $$LH_HDR/el_cdata.h \
+ $$LH_HDR/el_comment.h \
+ $$LH_HDR/el_div.h \
+ $$LH_HDR/el_font.h \
+ $$LH_HDR/el_image.h \
+ $$LH_HDR/el_li.h \
+ $$LH_HDR/el_link.h \
+ $$LH_HDR/el_para.h \
+ $$LH_HDR/el_script.h \
+ $$LH_HDR/el_space.h \
+ $$LH_HDR/el_style.h \
+ $$LH_HDR/el_table.h \
+ $$LH_HDR/el_td.h \
+ $$LH_HDR/el_text.h \
+ $$LH_HDR/el_title.h \
+ $$LH_HDR/el_tr.h \
+ $$LH_HDR/element.h \
+ $$LH_HDR/html.h \
+ $$LH_HDR/html_tag.h \
+ $$LH_HDR/iterators.h \
+ $$LH_HDR/media_query.h \
+ $$LH_HDR/num_cvt.h \
+ $$LH_HDR/os_types.h \
+ $$LH_HDR/style.h \
+ $$LH_HDR/stylesheet.h \
+ $$LH_HDR/table.h \
+ $$LH_HDR/types.h \
+ $$LH_HDR/utf8_strings.h \
+ $$LH_HDR/web_color.h
+
+ INCLUDEPATH *= $$LH_SRC/include $$LH_HDR
+
+ # litehtml without optimization is not fun
+ QMAKE_CFLAGS_DEBUG += -O2
+ QMAKE_CXXFLAGS_DEBUG += -O2
+} else {
+ INCLUDEPATH *= $$LITEHTML_INSTALL_DIR/include $$LITEHTML_INSTALL_DIR/include/litehtml
+ LITEHTML_LIB_DIR = $$LITEHTML_INSTALL_DIR/lib
+ LIBS += -L$$LITEHTML_LIB_DIR -llitehtml -lgumbo
+
+ win32: PRE_TARGETDEPS += $$LITEHTML_LIB_DIR/litehtml.lib $$LITEHTML_LIB_DIR/gumbo.lib
+ else:unix: PRE_TARGETDEPS += $$LITEHTML_LIB_DIR/liblitehtml.a $$LITEHTML_LIB_DIR/libgumbo.a
+}
+
+HEADERS += \
+ $$PWD/container_qpainter.h \
+ $$PWD/container_qpainter_p.h \
+ $$PWD/qlitehtmlwidget.h
+
+SOURCES += \
+ $$PWD/container_qpainter.cpp \
+ $$PWD/qlitehtmlwidget.cpp
+
+INCLUDEPATH *= $$PWD
+win32: DEFINES += LITEHTML_UTF8
+
+DEFINES *= QLITEHTML_STATIC_LIBRARY
diff --git a/src/qlitehtml.qbs b/src/qlitehtml.qbs
new file mode 100644
index 0000000..1e06f77
--- /dev/null
+++ b/src/qlitehtml.qbs
@@ -0,0 +1,216 @@
+import qbs.File
+import qbs.FileInfo
+
+Product {
+ type: buildLib ? ["staticlibrary"] : undefined
+
+ Depends { name: "cpp" }
+ Depends { name: "qtc" }
+
+ property bool useExternalLib: qtc.litehtmlInstallDir
+ property bool buildLib: !useExternalLib && File.exists(path + "/3rdparty/litehtml/CMakeLists.txt")
+ condition: useExternalLib || buildLib
+
+ property string gumboSrcDir: path + "/3rdparty/litehtml/src/gumbo"
+ property string gumboHeaderDir: gumboSrcDir + "/include/gumbo"
+ property string litehtmlHeaderDir: path + "/3rdparty/litehtml/include/litehtml"
+ property string mainHeaderDir: litehtmlHeaderDir + '/..'
+ property stringList sharedDefines: {
+ var defines = ["QLITEHTML_STATIC_LIBRARY"];
+ if (qbs.targetOS.contains("windows"))
+ defines.push("LITEHTML_UTF8");
+ return defines;
+ }
+
+ cpp.defines: sharedDefines
+ cpp.includePaths: {
+ var paths = [gumboHeaderDir, gumboHeaderDir + '/..', litehtmlHeaderDir, mainHeaderDir];
+ if (qbs.targetOS.contains("windows"))
+ paths.push(gumboSrcDir + "/visualc/include");
+ return paths;
+ }
+ cpp.optimization: "fast"
+ cpp.warningLevel: "none"
+ cpp.cxxLanguageVersion: "c++14"
+
+ Export {
+ Depends { name: "cpp" }
+ Group {
+ name: "litehtml/Qt glue"
+ cpp.warningLevel: "none"
+ files: [
+ "container_qpainter.cpp",
+ "container_qpainter.h",
+ "container_qpainter_p.h",
+ "qlitehtmlwidget.cpp",
+ "qlitehtmlwidget.h",
+ ]
+ }
+
+ Properties {
+ condition: product.useExternalLib
+ cpp.dynamicLibraries: ["litehtml", "gumbo"]
+ cpp.includePaths: [
+ FileInfo.joinPaths(qtc.litehtmlInstallDir, "include"),
+ FileInfo.joinPaths(qtc.litehtmlInstallDir, "include", "litehtml"),
+ ]
+ cpp.libraryPaths: FileInfo.joinPaths(qtc.litehtmlInstallDir, "lib")
+ }
+ Properties {
+ condition: product.buildLib
+ cpp.defines: product.sharedDefines
+ cpp.includePaths: [product.mainHeaderDir, path]
+ }
+ }
+
+ Group {
+ condition: buildLib
+ name: "gumbo sources"
+ prefix: gumboSrcDir + '/'
+ files: [
+ "attribute.c",
+ "char_ref.c",
+ "error.c",
+ "parser.c",
+ "string_buffer.c",
+ "string_piece.c",
+ "tag.c",
+ "tokenizer.c",
+ "utf8.c",
+ "util.c",
+ "vector.c",
+ ]
+ }
+
+ Group {
+ condition: buildLib
+ name: "gumbo headers"
+ prefix: gumboHeaderDir + '/'
+ files: [
+ "../gumbo.h",
+ "attribute.h",
+ "char_ref.h",
+ "error.h",
+ "insertion_mode.h",
+ "parser.h",
+ "string_buffer.h",
+ "string_piece.h",
+ "tag_enum.h",
+ "tag_gperf.h",
+ "tag_sizes.h",
+ "tag_strings.h",
+ "token_type.h",
+ "tokenizer.h",
+ "tokenizer_states.h",
+ "utf8.h",
+ "util.h",
+ "vector.h",
+ ]
+
+ Group {
+ name: "gumbo Windows headers"
+ condition: qbs.targetOS.contains("windows")
+ files: "../../visualc/include/strings.h"
+ }
+ }
+
+ Group {
+ condition: buildLib
+ name: "litehtml sources"
+ prefix: litehtmlHeaderDir + "/../../src/"
+ files: [
+ "background.cpp",
+ "box.cpp",
+ "context.cpp",
+ "css_length.cpp",
+ "css_selector.cpp",
+ "document.cpp",
+ "el_anchor.cpp",
+ "el_base.cpp",
+ "el_before_after.cpp",
+ "el_body.cpp",
+ "el_break.cpp",
+ "el_cdata.cpp",
+ "el_comment.cpp",
+ "el_div.cpp",
+ "element.cpp",
+ "el_font.cpp",
+ "el_image.cpp",
+ "el_li.cpp",
+ "el_link.cpp",
+ "el_para.cpp",
+ "el_script.cpp",
+ "el_space.cpp",
+ "el_style.cpp",
+ "el_table.cpp",
+ "el_td.cpp",
+ "el_text.cpp",
+ "el_title.cpp",
+ "el_tr.cpp",
+ "html.cpp",
+ "html_tag.cpp",
+ "iterators.cpp",
+ "media_query.cpp",
+ "num_cvt.cpp",
+ "style.cpp",
+ "stylesheet.cpp",
+ "table.cpp",
+ "utf8_strings.cpp",
+ "web_color.cpp",
+ ]
+ }
+
+ Group {
+ condition: buildLib
+ name: "litehtml headers"
+ prefix: litehtmlHeaderDir + '/'
+ files: [
+ "../litehtml.h",
+ "attributes.h",
+ "background.h",
+ "borders.h",
+ "box.h",
+ "context.h",
+ "css_length.h",
+ "css_margins.h",
+ "css_offsets.h",
+ "css_position.h",
+ "css_selector.h",
+ "document.h",
+ "el_anchor.h",
+ "el_base.h",
+ "el_before_after.h",
+ "el_body.h",
+ "el_break.h",
+ "el_cdata.h",
+ "el_comment.h",
+ "el_div.h",
+ "el_font.h",
+ "el_image.h",
+ "el_li.h",
+ "el_link.h",
+ "el_para.h",
+ "el_script.h",
+ "el_space.h",
+ "el_style.h",
+ "el_table.h",
+ "el_td.h",
+ "el_text.h",
+ "el_title.h",
+ "el_tr.h",
+ "element.h",
+ "html.h",
+ "html_tag.h",
+ "iterators.h",
+ "media_query.h",
+ "num_cvt.h",
+ "os_types.h",
+ "style.h",
+ "stylesheet.h",
+ "table.h",
+ "types.h",
+ "utf8_strings.h",
+ "web_color.h",
+ ]
+ }
+}
diff --git a/src/qlitehtml_global.h b/src/qlitehtml_global.h
new file mode 100644
index 0000000..5ff23c3
--- /dev/null
+++ b/src/qlitehtml_global.h
@@ -0,0 +1,36 @@
+/****************************************************************************
+**
+** Copyright (C) 2020 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of QLiteHtml
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include <qglobal.h>
+
+#if defined(QLITEHTML_LIBRARY)
+# define QLITEHTML_EXPORT Q_DECL_EXPORT
+#elif defined(QLITEHTML_STATIC_LIBRARY) // Abuse single files for manual tests
+# define QLITEHTML_EXPORT
+#else
+# define QLITEHTML_EXPORT Q_DECL_IMPORT
+#endif
diff --git a/src/qlitehtmlwidget.cpp b/src/qlitehtmlwidget.cpp
new file mode 100644
index 0000000..8110417
--- /dev/null
+++ b/src/qlitehtmlwidget.cpp
@@ -0,0 +1,654 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of QLiteHtml.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "qlitehtmlwidget.h"
+
+#include "container_qpainter.h"
+
+#include <QDebug>
+#include <QPaintEvent>
+#include <QPainter>
+#include <QScrollBar>
+#include <QStyle>
+#include <QTimer>
+
+const int kScrollBarStep = 40;
+
+// TODO copied from litehtml/include/master.css
+const char mastercss[] = R"RAW(
+html {
+ display: block;
+height:100%;
+width:100%;
+position: relative;
+}
+
+head {
+ display: none
+}
+
+meta {
+ display: none
+}
+
+title {
+ display: none
+}
+
+link {
+ display: none
+}
+
+style {
+ display: none
+}
+
+script {
+ display: none
+}
+
+body {
+display:block;
+ margin:8px;
+ height:100%;
+width:100%;
+}
+
+p {
+display:block;
+ margin-top:1em;
+ margin-bottom:1em;
+}
+
+b, strong {
+display:inline;
+ font-weight:bold;
+}
+
+i, em {
+display:inline;
+ font-style:italic;
+}
+
+center
+{
+ text-align:center;
+display:block;
+}
+
+a:link
+{
+ text-decoration: underline;
+color: #00f;
+cursor: pointer;
+}
+
+h1, h2, h3, h4, h5, h6, div {
+display:block;
+}
+
+h1 {
+ font-weight:bold;
+ margin-top:0.67em;
+ margin-bottom:0.67em;
+ font-size: 2em;
+}
+
+h2 {
+ font-weight:bold;
+ margin-top:0.83em;
+ margin-bottom:0.83em;
+ font-size: 1.5em;
+}
+
+h3 {
+ font-weight:bold;
+ margin-top:1em;
+ margin-bottom:1em;
+ font-size:1.17em;
+}
+
+h4 {
+ font-weight:bold;
+ margin-top:1.33em;
+ margin-bottom:1.33em
+}
+
+h5 {
+ font-weight:bold;
+ margin-top:1.67em;
+ margin-bottom:1.67em;
+ font-size:.83em;
+}
+
+h6 {
+ font-weight:bold;
+ margin-top:2.33em;
+ margin-bottom:2.33em;
+ font-size:.67em;
+}
+
+br {
+display:inline-block;
+}
+
+br[clear="all"]
+{
+clear:both;
+}
+
+br[clear="left"]
+{
+clear:left;
+}
+
+br[clear="right"]
+{
+clear:right;
+}
+
+span {
+ display:inline
+}
+
+img {
+display: inline-block;
+}
+
+img[align="right"]
+{
+ float: right;
+}
+
+img[align="left"]
+{
+ float: left;
+}
+
+hr {
+display: block;
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+ margin-left: auto;
+ margin-right: auto;
+ border-style: inset;
+ border-width: 1px
+}
+
+
+/***************** TABLES ********************/
+
+table {
+display: table;
+ border-collapse: separate;
+ border-spacing: 2px;
+ border-top-color:gray;
+ border-left-color:gray;
+ border-bottom-color:black;
+ border-right-color:black;
+}
+
+tbody, tfoot, thead {
+display:table-row-group;
+ vertical-align:middle;
+}
+
+tr {
+display: table-row;
+ vertical-align: inherit;
+ border-color: inherit;
+}
+
+td, th {
+display: table-cell;
+ vertical-align: inherit;
+ border-width:1px;
+padding:1px;
+}
+
+th {
+ font-weight: bold;
+}
+
+table[border] {
+ border-style:solid;
+}
+
+table[border|=0] {
+ border-style:none;
+}
+
+table[border] td, table[border] th {
+ border-style:solid;
+ border-top-color:black;
+ border-left-color:black;
+ border-bottom-color:gray;
+ border-right-color:gray;
+}
+
+table[border|=0] td, table[border|=0] th {
+ border-style:none;
+}
+
+caption {
+display: table-caption;
+}
+
+td[nowrap], th[nowrap] {
+ white-space:nowrap;
+}
+
+tt, code, kbd, samp {
+ font-family: monospace
+}
+pre, xmp, plaintext, listing {
+display: block;
+ font-family: monospace;
+ white-space: pre;
+margin: 1em 0
+}
+
+/***************** LISTS ********************/
+
+ul, menu, dir {
+display: block;
+ list-style-type: disc;
+ margin-top: 1em;
+ margin-bottom: 1em;
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 40px
+}
+
+ol {
+display: block;
+ list-style-type: decimal;
+ margin-top: 1em;
+ margin-bottom: 1em;
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 40px
+}
+
+li {
+display: list-item;
+}
+
+ul ul, ol ul {
+ list-style-type: circle;
+}
+
+ol ol ul, ol ul ul, ul ol ul, ul ul ul {
+ list-style-type: square;
+}
+
+dd {
+display: block;
+ margin-left: 40px;
+}
+
+dl {
+display: block;
+ margin-top: 1em;
+ margin-bottom: 1em;
+ margin-left: 0;
+ margin-right: 0;
+}
+
+dt {
+display: block;
+}
+
+ol ul, ul ol, ul ul, ol ol {
+ margin-top: 0;
+ margin-bottom: 0
+}
+
+blockquote {
+display: block;
+ margin-top: 1em;
+ margin-bottom: 1em;
+ margin-left: 40px;
+ margin-left: 40px;
+}
+
+/*********** FORM ELEMENTS ************/
+
+form {
+display: block;
+ margin-top: 0em;
+}
+
+option {
+display: none;
+}
+
+input, textarea, keygen, select, button, isindex {
+margin: 0em;
+color: initial;
+ line-height: normal;
+ text-transform: none;
+ text-indent: 0;
+ text-shadow: none;
+display: inline-block;
+}
+input[type="hidden"] {
+display: none;
+}
+
+
+article, aside, footer, header, hgroup, nav, section
+{
+display: block;
+}
+)RAW";
+
+class QLiteHtmlWidgetPrivate
+{
+public:
+ QString html;
+ DocumentContainerContext context;
+ QUrl url;
+ DocumentContainer documentContainer;
+ qreal zoomFactor = 1;
+};
+
+QLiteHtmlWidget::QLiteHtmlWidget(QWidget *parent)
+ : QAbstractScrollArea(parent)
+ , d(new QLiteHtmlWidgetPrivate)
+{
+ setMouseTracking(true);
+ horizontalScrollBar()->setSingleStep(kScrollBarStep);
+ verticalScrollBar()->setSingleStep(kScrollBarStep);
+
+ d->documentContainer.setCursorCallback([this](const QCursor &c) { viewport()->setCursor(c); });
+ d->documentContainer.setPaletteCallback([this] { return palette(); });
+ d->documentContainer.setLinkCallback([this](const QUrl &url) {
+ QUrl fullUrl = url;
+ if (url.isRelative() && url.path(QUrl::FullyEncoded).isEmpty()) { // fragment/anchor only
+ fullUrl = d->url;
+ fullUrl.setFragment(url.fragment(QUrl::FullyEncoded));
+ }
+ // delay because document may not be changed directly during this callback
+ QTimer::singleShot(0, this, [this, fullUrl] { emit linkClicked(fullUrl); });
+ });
+
+ // TODO adapt mastercss to palette (default text & background color)
+ d->context.setMasterStyleSheet(mastercss);
+}
+
+QLiteHtmlWidget::~QLiteHtmlWidget()
+{
+ delete d;
+}
+
+void QLiteHtmlWidget::setUrl(const QUrl &url)
+{
+ d->url = url;
+ QUrl urlWithoutAnchor = url;
+ urlWithoutAnchor.setFragment({});
+ const QString urlString = urlWithoutAnchor.toString(QUrl::None);
+ const int lastSlash = urlString.lastIndexOf('/');
+ const QString baseUrl = lastSlash >= 0 ? urlString.left(lastSlash) : urlString;
+ d->documentContainer.setBaseUrl(baseUrl);
+}
+
+QUrl QLiteHtmlWidget::url() const
+{
+ return d->url;
+}
+
+void QLiteHtmlWidget::setHtml(const QString &content)
+{
+ d->html = content;
+ d->documentContainer.setPaintDevice(viewport());
+ d->documentContainer.setDocument(content.toUtf8(), &d->context);
+ verticalScrollBar()->setValue(0);
+ horizontalScrollBar()->setValue(0);
+ render();
+}
+
+QString QLiteHtmlWidget::html() const
+{
+ return d->html;
+}
+
+QString QLiteHtmlWidget::title() const
+{
+ return d->documentContainer.caption();
+}
+
+void QLiteHtmlWidget::setZoomFactor(qreal scale)
+{
+ Q_ASSERT(scale != 0);
+ d->zoomFactor = scale;
+ withFixedTextPosition([this] { render(); });
+}
+
+qreal QLiteHtmlWidget::zoomFactor() const
+{
+ return d->zoomFactor;
+}
+
+bool QLiteHtmlWidget::findText(const QString &text,
+ QTextDocument::FindFlags flags,
+ bool incremental,
+ bool *wrapped)
+{
+ bool success = false;
+ QVector<QRect> oldSelection;
+ QVector<QRect> newSelection;
+ d->documentContainer
+ .findText(text, flags, incremental, wrapped, &success, &oldSelection, &newSelection);
+ // scroll to search result position and/or redraw as necessary
+ QRect newSelectionCombined;
+ for (const QRect &r : newSelection)
+ newSelectionCombined = newSelectionCombined.united(r);
+ QScrollBar *vBar = verticalScrollBar();
+ const int top = newSelectionCombined.top();
+ const int bottom = newSelectionCombined.bottom() - toVirtual(viewport()->size()).height();
+ if (success && top < vBar->value() && vBar->minimum() <= top) {
+ vBar->setValue(top);
+ } else if (success && vBar->value() < bottom && bottom <= vBar->maximum()) {
+ vBar->setValue(bottom);
+ } else {
+ viewport()->update(fromVirtual(newSelectionCombined.translated(-scrollPosition())));
+ for (const QRect &r : oldSelection)
+ viewport()->update(fromVirtual(r.translated(-scrollPosition())));
+ }
+ return success;
+}
+
+void QLiteHtmlWidget::setDefaultFont(const QFont &font)
+{
+ d->documentContainer.setDefaultFont(font);
+ render();
+}
+
+QFont QLiteHtmlWidget::defaultFont() const
+{
+ return d->documentContainer.defaultFont();
+}
+
+void QLiteHtmlWidget::scrollToAnchor(const QString &name)
+{
+ if (!d->documentContainer.hasDocument())
+ return;
+ horizontalScrollBar()->setValue(0);
+ if (name.isEmpty()) {
+ verticalScrollBar()->setValue(0);
+ return;
+ }
+ const int y = d->documentContainer.anchorY(name);
+ if (y >= 0)
+ verticalScrollBar()->setValue(std::min(y, verticalScrollBar()->maximum()));
+}
+
+void QLiteHtmlWidget::setResourceHandler(const QLiteHtmlWidget::ResourceHandler &handler)
+{
+ d->documentContainer.setDataCallback(handler);
+}
+
+QString QLiteHtmlWidget::selectedText() const
+{
+ return d->documentContainer.selectedText();
+}
+
+void QLiteHtmlWidget::paintEvent(QPaintEvent *event)
+{
+ if (!d->documentContainer.hasDocument())
+ return;
+ d->documentContainer.setScrollPosition(scrollPosition());
+ QPainter p(viewport());
+ p.setWorldTransform(QTransform().scale(d->zoomFactor, d->zoomFactor));
+ p.setRenderHint(QPainter::SmoothPixmapTransform, true);
+ p.setRenderHint(QPainter::Antialiasing, true);
+ d->documentContainer.draw(&p, toVirtual(event->rect()));
+}
+
+void QLiteHtmlWidget::resizeEvent(QResizeEvent *event)
+{
+ withFixedTextPosition([this, event] {
+ QAbstractScrollArea::resizeEvent(event);
+ render();
+ });
+}
+
+void QLiteHtmlWidget::mouseMoveEvent(QMouseEvent *event)
+{
+ QPoint viewportPos;
+ QPoint pos;
+ htmlPos(event->pos(), &viewportPos, &pos);
+ for (const QRect &r : d->documentContainer.mouseMoveEvent(pos, viewportPos))
+ viewport()->update(fromVirtual(r.translated(-scrollPosition())));
+}
+
+void QLiteHtmlWidget::mousePressEvent(QMouseEvent *event)
+{
+ QPoint viewportPos;
+ QPoint pos;
+ htmlPos(event->pos(), &viewportPos, &pos);
+ for (const QRect &r : d->documentContainer.mousePressEvent(pos, viewportPos, event->button()))
+ viewport()->update(fromVirtual(r.translated(-scrollPosition())));
+}
+
+void QLiteHtmlWidget::mouseReleaseEvent(QMouseEvent *event)
+{
+ QPoint viewportPos;
+ QPoint pos;
+ htmlPos(event->pos(), &viewportPos, &pos);
+ for (const QRect &r : d->documentContainer.mouseReleaseEvent(pos, viewportPos, event->button()))
+ viewport()->update(fromVirtual(r.translated(-scrollPosition())));
+}
+
+void QLiteHtmlWidget::mouseDoubleClickEvent(QMouseEvent *event)
+{
+ QPoint viewportPos;
+ QPoint pos;
+ htmlPos(event->pos(), &viewportPos, &pos);
+ for (const QRect &r :
+ d->documentContainer.mouseDoubleClickEvent(pos, viewportPos, event->button())) {
+ viewport()->update(fromVirtual(r.translated(-scrollPosition())));
+ }
+}
+
+void QLiteHtmlWidget::leaveEvent(QEvent *event)
+{
+ Q_UNUSED(event)
+ for (const QRect &r : d->documentContainer.leaveEvent())
+ viewport()->update(fromVirtual(r.translated(-scrollPosition())));
+}
+
+void QLiteHtmlWidget::contextMenuEvent(QContextMenuEvent *event)
+{
+ QPoint viewportPos;
+ QPoint pos;
+ htmlPos(event->pos(), &viewportPos, &pos);
+ emit contextMenuRequested(event->pos(), d->documentContainer.linkAt(pos, viewportPos));
+}
+
+void QLiteHtmlWidget::withFixedTextPosition(const std::function<void()> &action)
+{
+ // remember element to which to scroll after re-rendering
+ QPoint viewportPos;
+ QPoint pos;
+ htmlPos({}, &viewportPos, &pos); // top-left
+ const int y = d->documentContainer.withFixedElementPosition(pos.y(), action);
+ if (y >= 0)
+ verticalScrollBar()->setValue(std::min(y, verticalScrollBar()->maximum()));
+}
+
+void QLiteHtmlWidget::render()
+{
+ if (!d->documentContainer.hasDocument())
+ return;
+ const int fullWidth = width() / d->zoomFactor;
+ const QSize vViewportSize = toVirtual(viewport()->size());
+ const int scrollbarWidth = style()->pixelMetric(QStyle::PM_ScrollBarExtent, nullptr, this);
+ const int w = fullWidth - scrollbarWidth - 2;
+ d->documentContainer.render(w, vViewportSize.height());
+ // scroll bars reflect virtual/scaled size of html document
+ horizontalScrollBar()->setPageStep(vViewportSize.width());
+ horizontalScrollBar()->setRange(0, std::max(0, d->documentContainer.documentWidth() - w));
+ verticalScrollBar()->setPageStep(vViewportSize.height());
+ verticalScrollBar()
+ ->setRange(0, std::max(0, d->documentContainer.documentHeight() - vViewportSize.height()));
+ viewport()->update();
+}
+
+QPoint QLiteHtmlWidget::scrollPosition() const
+{
+ return {horizontalScrollBar()->value(), verticalScrollBar()->value()};
+}
+
+void QLiteHtmlWidget::htmlPos(const QPoint &pos, QPoint *viewportPos, QPoint *htmlPos) const
+{
+ *viewportPos = toVirtual(viewport()->mapFromParent(pos));
+ *htmlPos = *viewportPos + scrollPosition();
+}
+
+QPoint QLiteHtmlWidget::toVirtual(const QPoint &p) const
+{
+ return {int(p.x() / d->zoomFactor), int(p.y() / d->zoomFactor)};
+}
+
+QSize QLiteHtmlWidget::toVirtual(const QSize &s) const
+{
+ return {int(s.width() / d->zoomFactor), int(s.height() / d->zoomFactor)};
+}
+
+QRect QLiteHtmlWidget::toVirtual(const QRect &r) const
+{
+ return {toVirtual(r.topLeft()), toVirtual(r.size())};
+}
+
+QRect QLiteHtmlWidget::fromVirtual(const QRect &r) const
+{
+ const QPoint tl{int(r.x() * d->zoomFactor), int(r.y() * d->zoomFactor)};
+ // round size up, and add one since the topleft point was rounded down
+ const QSize s{int(r.width() * d->zoomFactor + 0.5) + 1,
+ int(r.height() * d->zoomFactor + 0.5) + 1};
+ return {tl, s};
+}
diff --git a/src/qlitehtmlwidget.h b/src/qlitehtmlwidget.h
new file mode 100644
index 0000000..3722dce
--- /dev/null
+++ b/src/qlitehtmlwidget.h
@@ -0,0 +1,95 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of QLiteHtml.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include "qlitehtml_global.h"
+
+#include <QAbstractScrollArea>
+#include <QTextDocument>
+
+#include <functional>
+
+class QLiteHtmlWidgetPrivate;
+
+class QLITEHTML_EXPORT QLiteHtmlWidget : public QAbstractScrollArea
+{
+ Q_OBJECT
+public:
+ explicit QLiteHtmlWidget(QWidget *parent = nullptr);
+ ~QLiteHtmlWidget() override;
+
+ // declaring the getters Q_INVOKABLE to make them Squish-testable
+ void setUrl(const QUrl &url);
+ Q_INVOKABLE QUrl url() const;
+ void setHtml(const QString &content);
+ Q_INVOKABLE QString html() const;
+ Q_INVOKABLE QString title() const;
+
+ void setZoomFactor(qreal scale);
+ qreal zoomFactor() const;
+
+ bool findText(const QString &text,
+ QTextDocument::FindFlags flags,
+ bool incremental,
+ bool *wrapped = nullptr);
+
+ void setDefaultFont(const QFont &font);
+ QFont defaultFont() const;
+
+ void scrollToAnchor(const QString &name);
+
+ using ResourceHandler = std::function<QByteArray(QUrl)>;
+ void setResourceHandler(const ResourceHandler &handler);
+
+ // declaring this Q_INVOKABLE to make it Squish-testable
+ Q_INVOKABLE QString selectedText() const;
+
+signals:
+ void linkClicked(const QUrl &url);
+ void contextMenuRequested(const QPoint &pos, const QUrl &url);
+
+protected:
+ void paintEvent(QPaintEvent *event) override;
+ void resizeEvent(QResizeEvent *event) override;
+ void mouseMoveEvent(QMouseEvent *event) override;
+ void mousePressEvent(QMouseEvent *event) override;
+ void mouseReleaseEvent(QMouseEvent *event) override;
+ void mouseDoubleClickEvent(QMouseEvent *event) override;
+ void leaveEvent(QEvent *event) override;
+ void contextMenuEvent(QContextMenuEvent *event) override;
+
+private:
+ void withFixedTextPosition(const std::function<void()> &action);
+ void render();
+ QPoint scrollPosition() const;
+ void htmlPos(const QPoint &pos, QPoint *viewportPos, QPoint *htmlPos) const;
+ QPoint toVirtual(const QPoint &p) const;
+ QSize toVirtual(const QSize &s) const;
+ QRect toVirtual(const QRect &r) const;
+ QRect fromVirtual(const QRect &r) const;
+
+ QLiteHtmlWidgetPrivate *d;
+};