Ladybird: Render web content in a separate process :^)

This patch brings over the WebContent process over from SerenityOS
to Ladybird, along with a new WebContentView widget that renders
web content in a separate process.

There's a lot of jank and FIXME material here, notably I had to re-add
manually pumped Core::EventLoop instances on both sides, in order to get
the IPC protocol running. This introduces a lot of latency and we should
work towards replacing those loops with improved abstractions.

The WebContent process is built separately here (not part of Lagom) and
we provide our own main.cpp for it. Like everything, this can be better
architected, it's just a starting point. :^)
This commit is contained in:
Andreas Kling 2022-10-05 15:23:41 +02:00 committed by Andrew Kaster
parent 2451a447f5
commit 26a7ea0e0f
Notes: sideshowbarker 2024-07-17 02:42:03 +09:00
14 changed files with 1313 additions and 795 deletions

View file

@ -9,8 +9,8 @@
#include "BrowserWindow.h"
#include "Settings.h"
#include "SettingsDialog.h"
#include "SimpleWebView.h"
#include "Utilities.h"
#include "WebContentView.h"
#include <AK/TypeCasts.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <QAction>
@ -94,14 +94,7 @@ BrowserWindow::BrowserWindow()
inspect_menu->addAction(view_source_action);
QObject::connect(view_source_action, &QAction::triggered, this, [this] {
if (m_current_tab) {
auto source = m_current_tab->view().source();
auto* text_edit = new QPlainTextEdit(this);
text_edit->setWindowFlags(Qt::Window);
text_edit->setFont(QFontDatabase::systemFont(QFontDatabase::SystemFont::FixedFont));
text_edit->resize(800, 600);
text_edit->setPlainText(source.characters());
text_edit->show();
m_current_tab->view().get_source();
}
});

View file

@ -16,14 +16,14 @@
#pragma once
class SimpleWebView;
class WebContentView;
class BrowserWindow : public QMainWindow {
Q_OBJECT
public:
explicit BrowserWindow();
SimpleWebView& view() const { return m_current_tab->view(); }
WebContentView& view() const { return m_current_tab->view(); }
int tab_index(Tab*);

View file

@ -50,32 +50,26 @@ find_package(Qt6 REQUIRED COMPONENTS Core Widgets Network)
set(SOURCES
BrowserWindow.cpp
ConsoleClient.cpp
ConsoleGlobalObject.cpp
CookieJar.cpp
EventLoopPluginQt.cpp
FontPluginQt.cpp
ImageCodecPluginLadybird.cpp
ModelTranslator.cpp
PageClientLadybird.cpp
RequestManagerQt.cpp
main.cpp
WebContentView.cpp
History.cpp
ModelTranslator.cpp
Settings.cpp
SettingsDialog.cpp
SimpleWebView.cpp
Tab.cpp
TimerQt.cpp
Utilities.cpp
WebSocketClientManagerLadybird.cpp
WebSocketLadybird.cpp
main.cpp
)
qt_add_executable(ladybird ${SOURCES}
MANUAL_FINALIZATION
)
#target_link_libraries(ladybird PRIVATE Qt::Widgets Qt::Network LibWeb LibGUI LibWeb LibWebView LibGL LibSoftGPU LibMain)
target_link_libraries(ladybird PRIVATE Qt::Widgets Qt::Network LibWeb LibWebSocket LibGUI LibWebView LibGL LibSoftGPU LibMain)
target_include_directories(ladybird PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(ladybird PRIVATE ${SERENITY_SOURCE_DIR}/Userland/Services/)
set_target_properties(ladybird PROPERTIES
MACOSX_BUNDLE_GUI_IDENTIFIER org.serenityos.ladybird
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
@ -104,3 +98,5 @@ qt_finalize_executable(ladybird)
if(NOT CMAKE_SKIP_INSTALL_RULES)
include(cmake/InstallRules.cmake)
endif()
add_subdirectory(WebContent)

View file

@ -1,668 +0,0 @@
/*
* Copyright (c) 2022, Dex <dexes.ttp@gmail.com>
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#define AK_DONT_REPLACE_STD
#include "SimpleWebView.h"
#include "ConsoleClient.h"
#include "CookieJar.h"
#include "EventLoopPluginQt.h"
#include "FontPluginQt.h"
#include "ImageCodecPluginLadybird.h"
#include "ModelTranslator.h"
#include "PageClientLadybird.h"
#include "RequestManagerQt.h"
#include "Utilities.h"
#include "WebSocketClientManagerLadybird.h"
#include <AK/Assertions.h>
#include <AK/ByteBuffer.h>
#include <AK/Format.h>
#include <AK/HashTable.h>
#include <AK/LexicalPath.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/StringBuilder.h>
#include <AK/Types.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/EventLoop.h>
#include <LibCore/File.h>
#include <LibCore/IODevice.h>
#include <LibCore/MemoryStream.h>
#include <LibCore/Stream.h>
#include <LibCore/System.h>
#include <LibCore/Timer.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibGfx/PNGWriter.h>
#include <LibGfx/Rect.h>
#include <LibJS/Runtime/ConsoleObject.h>
#include <LibMain/Main.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/Cookie/ParsedCookie.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Dump.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/HTML/Storage.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Layout/InitialContainingBlock.h>
#include <LibWeb/Loader/ContentFilter.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/StackingContext.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWebView/DOMTreeModel.h>
#include <QApplication>
#include <QCursor>
#include <QIcon>
#include <QLineEdit>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPaintEvent>
#include <QPainter>
#include <QScrollBar>
#include <QTextEdit>
#include <QToolTip>
#include <QTreeView>
#include <QVBoxLayout>
String s_serenity_resource_root;
SimpleWebView::SimpleWebView()
{
setMouseTracking(true);
m_page_client = Ladybird::PageClientLadybird::create(*this);
m_page_client->setup_palette(Gfx::load_system_theme(String::formatted("{}/res/themes/Default.ini", s_serenity_resource_root)));
// FIXME: Allow passing these values as arguments
m_page_client->set_viewport_rect({ 0, 0, 800, 600 });
m_inverse_pixel_scaling_ratio = 1.0 / devicePixelRatio();
verticalScrollBar()->setSingleStep(24);
horizontalScrollBar()->setSingleStep(24);
QObject::connect(verticalScrollBar(), &QScrollBar::valueChanged, [this](int) {
update_viewport_rect();
});
QObject::connect(horizontalScrollBar(), &QScrollBar::valueChanged, [this](int) {
update_viewport_rect();
});
}
SimpleWebView::~SimpleWebView()
{
}
void SimpleWebView::reload()
{
auto url = m_page_client->page().top_level_browsing_context().active_document()->url();
m_page_client->load(url);
}
void SimpleWebView::load(String const& url)
{
m_page_client->load(AK::URL(url));
}
unsigned get_button_from_qt_event(QMouseEvent const& event)
{
if (event.button() == Qt::MouseButton::LeftButton)
return 1;
if (event.button() == Qt::MouseButton::RightButton)
return 2;
if (event.button() == Qt::MouseButton::MiddleButton)
return 4;
if (event.button() == Qt::MouseButton::BackButton)
return 8;
if (event.buttons() == Qt::MouseButton::ForwardButton)
return 16;
return 0;
}
unsigned get_buttons_from_qt_event(QMouseEvent const& event)
{
unsigned buttons = 0;
if (event.buttons() & Qt::MouseButton::LeftButton)
buttons |= 1;
if (event.buttons() & Qt::MouseButton::RightButton)
buttons |= 2;
if (event.buttons() & Qt::MouseButton::MiddleButton)
buttons |= 4;
if (event.buttons() & Qt::MouseButton::BackButton)
buttons |= 8;
if (event.buttons() & Qt::MouseButton::ForwardButton)
buttons |= 16;
return buttons;
}
unsigned get_modifiers_from_qt_mouse_event(QMouseEvent const& event)
{
unsigned modifiers = 0;
if (event.modifiers() & Qt::Modifier::ALT)
modifiers |= 1;
if (event.modifiers() & Qt::Modifier::CTRL)
modifiers |= 2;
if (event.modifiers() & Qt::Modifier::SHIFT)
modifiers |= 4;
return modifiers;
}
unsigned get_modifiers_from_qt_keyboard_event(QKeyEvent const& event)
{
auto modifiers = 0;
if (event.modifiers().testFlag(Qt::AltModifier))
modifiers |= KeyModifier::Mod_Alt;
if (event.modifiers().testFlag(Qt::ControlModifier))
modifiers |= KeyModifier::Mod_Ctrl;
if (event.modifiers().testFlag(Qt::MetaModifier))
modifiers |= KeyModifier::Mod_Super;
if (event.modifiers().testFlag(Qt::ShiftModifier))
modifiers |= KeyModifier::Mod_Shift;
if (event.modifiers().testFlag(Qt::AltModifier))
modifiers |= KeyModifier::Mod_AltGr;
return modifiers;
}
KeyCode get_keycode_from_qt_keyboard_event(QKeyEvent const& event)
{
struct Mapping {
constexpr Mapping(Qt::Key q, KeyCode s)
: qt_key(q)
, serenity_key(s)
{
}
Qt::Key qt_key;
KeyCode serenity_key;
};
constexpr Mapping mappings[] = {
{ Qt::Key_0, Key_0 },
{ Qt::Key_1, Key_1 },
{ Qt::Key_2, Key_2 },
{ Qt::Key_3, Key_3 },
{ Qt::Key_4, Key_4 },
{ Qt::Key_5, Key_5 },
{ Qt::Key_6, Key_6 },
{ Qt::Key_7, Key_7 },
{ Qt::Key_8, Key_8 },
{ Qt::Key_9, Key_9 },
{ Qt::Key_A, Key_A },
{ Qt::Key_Alt, Key_Alt },
{ Qt::Key_Ampersand, Key_Ampersand },
{ Qt::Key_Apostrophe, Key_Apostrophe },
{ Qt::Key_AsciiCircum, Key_Circumflex },
{ Qt::Key_AsciiTilde, Key_Tilde },
{ Qt::Key_Asterisk, Key_Asterisk },
{ Qt::Key_At, Key_AtSign },
{ Qt::Key_B, Key_B },
{ Qt::Key_Backslash, Key_Backslash },
{ Qt::Key_Backspace, Key_Backspace },
{ Qt::Key_Bar, Key_Pipe },
{ Qt::Key_BraceLeft, Key_LeftBrace },
{ Qt::Key_BraceRight, Key_RightBrace },
{ Qt::Key_BracketLeft, Key_LeftBracket },
{ Qt::Key_BracketRight, Key_RightBracket },
{ Qt::Key_C, Key_C },
{ Qt::Key_CapsLock, Key_CapsLock },
{ Qt::Key_Colon, Key_Colon },
{ Qt::Key_Comma, Key_Comma },
{ Qt::Key_Control, Key_Control },
{ Qt::Key_D, Key_D },
{ Qt::Key_Delete, Key_Delete },
{ Qt::Key_Dollar, Key_Dollar },
{ Qt::Key_Down, Key_Down },
{ Qt::Key_E, Key_E },
{ Qt::Key_End, Key_End },
{ Qt::Key_Equal, Key_Equal },
{ Qt::Key_Escape, Key_Escape },
{ Qt::Key_exclamdown, Key_ExclamationPoint },
{ Qt::Key_F, Key_F },
{ Qt::Key_F1, Key_F1 },
{ Qt::Key_F10, Key_F10 },
{ Qt::Key_F11, Key_F11 },
{ Qt::Key_F12, Key_F12 },
{ Qt::Key_F2, Key_F2 },
{ Qt::Key_F3, Key_F3 },
{ Qt::Key_F4, Key_F4 },
{ Qt::Key_F5, Key_F5 },
{ Qt::Key_F6, Key_F6 },
{ Qt::Key_F7, Key_F7 },
{ Qt::Key_F8, Key_F8 },
{ Qt::Key_F9, Key_F9 },
{ Qt::Key_G, Key_G },
{ Qt::Key_Greater, Key_GreaterThan },
{ Qt::Key_H, Key_H },
{ Qt::Key_Home, Key_Home },
{ Qt::Key_I, Key_I },
{ Qt::Key_Insert, Key_Insert },
{ Qt::Key_J, Key_J },
{ Qt::Key_K, Key_K },
{ Qt::Key_L, Key_L },
{ Qt::Key_Left, Key_Left },
{ Qt::Key_Less, Key_LessThan },
{ Qt::Key_M, Key_M },
{ Qt::Key_Menu, Key_Menu },
{ Qt::Key_Minus, Key_Minus },
{ Qt::Key_N, Key_N },
{ Qt::Key_NumLock, Key_NumLock },
{ Qt::Key_O, Key_O },
{ Qt::Key_P, Key_P },
{ Qt::Key_PageDown, Key_PageDown },
{ Qt::Key_PageUp, Key_PageUp },
{ Qt::Key_ParenLeft, Key_LeftParen },
{ Qt::Key_ParenRight, Key_RightParen },
{ Qt::Key_Percent, Key_Percent },
{ Qt::Key_Period, Key_Period },
{ Qt::Key_Plus, Key_Plus },
{ Qt::Key_Print, Key_PrintScreen },
{ Qt::Key_Q, Key_Q },
{ Qt::Key_Question, Key_QuestionMark },
{ Qt::Key_QuoteDbl, Key_DoubleQuote },
{ Qt::Key_R, Key_R },
{ Qt::Key_Return, Key_Return },
{ Qt::Key_Right, Key_Right },
{ Qt::Key_S, Key_S },
{ Qt::Key_ScrollLock, Key_ScrollLock },
{ Qt::Key_Semicolon, Key_Semicolon },
{ Qt::Key_Shift, Key_LeftShift },
{ Qt::Key_Slash, Key_Slash },
{ Qt::Key_Space, Key_Space },
{ Qt::Key_Super_L, Key_Super },
{ Qt::Key_SysReq, Key_SysRq },
{ Qt::Key_T, Key_T },
{ Qt::Key_Tab, Key_Tab },
{ Qt::Key_U, Key_U },
{ Qt::Key_Underscore, Key_Underscore },
{ Qt::Key_Up, Key_Up },
{ Qt::Key_V, Key_V },
{ Qt::Key_W, Key_W },
{ Qt::Key_X, Key_X },
{ Qt::Key_Y, Key_Y },
{ Qt::Key_Z, Key_Z },
};
for (auto const& mapping : mappings) {
if (event.key() == mapping.qt_key)
return mapping.serenity_key;
}
return Key_Invalid;
}
void SimpleWebView::mouseMoveEvent(QMouseEvent* event)
{
Gfx::IntPoint position(event->position().x() / m_inverse_pixel_scaling_ratio, event->position().y() / m_inverse_pixel_scaling_ratio);
auto buttons = get_buttons_from_qt_event(*event);
auto modifiers = get_modifiers_from_qt_mouse_event(*event);
m_page_client->page().handle_mousemove(to_content(position), buttons, modifiers);
}
void SimpleWebView::mousePressEvent(QMouseEvent* event)
{
Gfx::IntPoint position(event->position().x() / m_inverse_pixel_scaling_ratio, event->position().y() / m_inverse_pixel_scaling_ratio);
auto button = get_button_from_qt_event(*event);
if (button == 0) {
// We could not convert Qt buttons to something that Lagom can
// recognize - don't even bother propagating this to the web engine
// as it will not handle it anyway, and it will (currently) assert
return;
}
auto modifiers = get_modifiers_from_qt_mouse_event(*event);
m_page_client->page().handle_mousedown(to_content(position), button, modifiers);
}
void SimpleWebView::mouseReleaseEvent(QMouseEvent* event)
{
Gfx::IntPoint position(event->position().x() / m_inverse_pixel_scaling_ratio, event->position().y() / m_inverse_pixel_scaling_ratio);
auto button = get_button_from_qt_event(*event);
if (button == 0) {
// We could not convert Qt buttons to something that Lagom can
// recognize - don't even bother propagating this to the web engine
// as it will not handle it anyway, and it will (currently) assert
return;
}
auto modifiers = get_modifiers_from_qt_mouse_event(*event);
m_page_client->page().handle_mouseup(to_content(position), button, modifiers);
}
void SimpleWebView::keyPressEvent(QKeyEvent* event)
{
switch (event->key()) {
case Qt::Key_Left:
case Qt::Key_Right:
case Qt::Key_Up:
case Qt::Key_Down:
case Qt::Key_PageUp:
case Qt::Key_PageDown:
QAbstractScrollArea::keyPressEvent(event);
break;
default:
break;
}
auto text = event->text();
if (text.isEmpty()) {
return;
}
auto point = event->text()[0].unicode();
auto keycode = get_keycode_from_qt_keyboard_event(*event);
auto modifiers = get_modifiers_from_qt_keyboard_event(*event);
m_page_client->page().handle_keydown(keycode, modifiers, point);
}
void SimpleWebView::keyReleaseEvent(QKeyEvent* event)
{
auto text = event->text();
if (text.isEmpty()) {
return;
}
auto point = event->text()[0].unicode();
auto keycode = get_keycode_from_qt_keyboard_event(*event);
auto modifiers = get_modifiers_from_qt_keyboard_event(*event);
m_page_client->page().handle_keyup(keycode, modifiers, point);
}
Gfx::IntPoint SimpleWebView::to_content(Gfx::IntPoint viewport_position) const
{
return viewport_position.translated(horizontalScrollBar()->value(), verticalScrollBar()->value());
}
Gfx::IntPoint SimpleWebView::to_widget(Gfx::IntPoint content_position) const
{
return content_position.translated(-horizontalScrollBar()->value(), -verticalScrollBar()->value());
}
void SimpleWebView::paintEvent(QPaintEvent* event)
{
QPainter painter(viewport());
painter.setClipRect(event->rect());
painter.scale(m_inverse_pixel_scaling_ratio, m_inverse_pixel_scaling_ratio);
auto output_rect = m_page_client->viewport_rect();
output_rect.set_x(horizontalScrollBar()->value());
output_rect.set_y(verticalScrollBar()->value());
auto output_bitmap = MUST(Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRx8888, output_rect.size()));
m_page_client->paint(output_rect, output_bitmap);
QImage q_image(output_bitmap->scanline_u8(0), output_bitmap->width(), output_bitmap->height(), QImage::Format_RGB32);
painter.drawImage(QPoint(0, 0), q_image);
}
void SimpleWebView::resizeEvent(QResizeEvent* event)
{
QAbstractScrollArea::resizeEvent(event);
update_viewport_rect();
}
void SimpleWebView::update_viewport_rect()
{
auto scaled_width = int(viewport()->size().width() / m_inverse_pixel_scaling_ratio);
auto scaled_height = int(viewport()->size().height() / m_inverse_pixel_scaling_ratio);
Gfx::IntRect rect(horizontalScrollBar()->value(), verticalScrollBar()->value(), scaled_width, scaled_height);
m_page_client->set_viewport_rect(rect);
}
static void platform_init()
{
#ifdef AK_OS_ANDROID
extern void android_platform_init();
android_platform_init();
#else
s_serenity_resource_root = [] {
auto const* source_dir = getenv("SERENITY_SOURCE_DIR");
if (source_dir) {
return String::formatted("{}/Base", source_dir);
}
auto* home = getenv("XDG_CONFIG_HOME") ?: getenv("HOME");
VERIFY(home);
auto home_lagom = String::formatted("{}/.lagom", home);
if (Core::File::is_directory(home_lagom))
return home_lagom;
auto app_dir = akstring_from_qstring(QApplication::applicationDirPath());
return LexicalPath(app_dir).parent().append("share"sv).string();
}();
#endif
}
static ErrorOr<void> load_content_filters()
{
auto file_or_error = Core::Stream::File::open(String::formatted("{}/home/anon/.config/BrowserContentFilters.txt", s_serenity_resource_root), Core::Stream::OpenMode::Read);
if (file_or_error.is_error())
file_or_error = Core::Stream::File::open(String::formatted("{}/res/ladybird/BrowserContentFilters.txt", s_serenity_resource_root), Core::Stream::OpenMode::Read);
if (file_or_error.is_error())
return file_or_error.release_error();
auto file = file_or_error.release_value();
auto ad_filter_list = TRY(Core::Stream::BufferedFile::create(move(file)));
auto buffer = TRY(ByteBuffer::create_uninitialized(4096));
size_t num_filters = 0;
while (TRY(ad_filter_list->can_read_line())) {
auto line = TRY(ad_filter_list->read_line(buffer));
if (!line.is_empty()) {
Web::ContentFilter::the().add_pattern(line);
++num_filters;
}
}
dbgln("Added {} content filters", num_filters);
return {};
}
void initialize_web_engine()
{
platform_init();
Web::Platform::EventLoopPlugin::install(*new Ladybird::EventLoopPluginQt);
Web::Platform::ImageCodecPlugin::install(*new Ladybird::ImageCodecPluginLadybird);
Web::ResourceLoader::initialize(RequestManagerQt::create());
Web::WebSockets::WebSocketClientManager::initialize(Ladybird::WebSocketClientManagerLadybird::create());
Web::FrameLoader::set_default_favicon_path(String::formatted("{}/res/icons/16x16/app-browser.png", s_serenity_resource_root));
Web::Platform::FontPlugin::install(*new Ladybird::FontPluginQt);
Web::FrameLoader::set_error_page_url(String::formatted("file://{}/res/html/error.html", s_serenity_resource_root));
auto maybe_content_filter_error = load_content_filters();
if (maybe_content_filter_error.is_error())
dbgln("Failed to load content filters: {}", maybe_content_filter_error.error());
}
void SimpleWebView::debug_request(String const& request, String const& argument)
{
auto& page = m_page_client->page();
if (request == "dump-dom-tree") {
if (auto* doc = page.top_level_browsing_context().active_document())
Web::dump_tree(*doc);
} else if (request == "dump-layout-tree") {
if (auto* doc = page.top_level_browsing_context().active_document()) {
if (auto* icb = doc->layout_node())
Web::dump_tree(*icb);
}
} else if (request == "dump-stacking-context-tree") {
if (auto* doc = page.top_level_browsing_context().active_document()) {
if (auto* icb = doc->layout_node()) {
if (auto* stacking_context = icb->paint_box()->stacking_context())
stacking_context->dump();
}
}
} else if (request == "dump-style-sheets") {
if (auto* doc = page.top_level_browsing_context().active_document()) {
for (auto& sheet : doc->style_sheets().sheets()) {
Web::dump_sheet(sheet);
}
}
} else if (request == "collect-garbage") {
Web::Bindings::main_thread_vm().heap().collect_garbage(JS::Heap::CollectionType::CollectGarbage, true);
} else if (request == "set-line-box-borders") {
bool state = argument == "on";
m_page_client->set_should_show_line_box_borders(state);
page.top_level_browsing_context().set_needs_display(page.top_level_browsing_context().viewport_rect());
} else if (request == "clear-cache") {
Web::ResourceLoader::the().clear_cache();
} else if (request == "spoof-user-agent") {
Web::ResourceLoader::the().set_user_agent(argument);
} else if (request == "same-origin-policy") {
page.set_same_origin_policy_enabled(argument == "on");
} else if (request == "scripting") {
page.set_is_scripting_enabled(argument == "on");
} else if (request == "dump-local-storage") {
if (auto* doc = page.top_level_browsing_context().active_document())
doc->window().local_storage()->dump();
} else if (request == "dump-cookies"sv) {
m_page_client->dump_cookies();
} else {
dbgln("Unknown debug request: {}", request);
}
}
String SimpleWebView::source() const
{
auto* document = m_page_client->page().top_level_browsing_context().active_document();
if (!document)
return String::empty();
return document->source();
}
void SimpleWebView::run_javascript(String const& js_source) const
{
auto* active_document = const_cast<Web::DOM::Document*>(m_page_client->page().top_level_browsing_context().active_document());
if (!active_document)
return;
// This is partially based on "execute a javascript: URL request" https://html.spec.whatwg.org/multipage/browsing-the-web.html#javascript-protocol
// Let settings be browsingContext's active document's relevant settings object.
auto& settings = active_document->relevant_settings_object();
// Let baseURL be settings's API base URL.
auto base_url = settings.api_base_url();
// Let script be the result of creating a classic script given scriptSource, settings, baseURL, and the default classic script fetch options.
// FIXME: This doesn't pass in "default classic script fetch options"
// FIXME: What should the filename be here?
auto script = Web::HTML::ClassicScript::create("(client connection run_javascript)", js_source, settings, base_url);
// Let evaluationStatus be the result of running the classic script script.
auto evaluation_status = script->run();
if (evaluation_status.is_error())
dbgln("Exception :(");
}
void SimpleWebView::did_output_js_console_message(i32 message_index)
{
m_page_client->m_console_client->send_messages(message_index);
}
void SimpleWebView::did_get_js_console_messages(i32, Vector<String>, Vector<String> messages)
{
ensure_js_console_widget();
for (auto& message : messages) {
m_js_console_output_edit->append(qstring_from_akstring(message).trimmed());
}
}
void SimpleWebView::ensure_js_console_widget()
{
if (!m_js_console_widget) {
m_js_console_widget = new QWidget;
m_js_console_widget->setWindowTitle("JS Console");
auto* layout = new QVBoxLayout(m_js_console_widget);
m_js_console_widget->setLayout(layout);
m_js_console_output_edit = new QTextEdit(this);
m_js_console_output_edit->setReadOnly(true);
m_js_console_input_edit = new QLineEdit(this);
layout->addWidget(m_js_console_output_edit);
layout->addWidget(m_js_console_input_edit);
m_js_console_widget->resize(640, 480);
QObject::connect(m_js_console_input_edit, &QLineEdit::returnPressed, [this] {
auto code = m_js_console_input_edit->text().trimmed();
m_js_console_input_edit->clear();
m_js_console_output_edit->append(QString("> %1").arg(code));
m_page_client->initialize_js_console();
m_page_client->m_console_client->handle_input(akstring_from_qstring(code));
});
}
}
void SimpleWebView::show_js_console()
{
ensure_js_console_widget();
m_js_console_widget->show();
m_js_console_input_edit->setFocus();
}
void SimpleWebView::ensure_inspector_widget()
{
if (m_inspector_widget)
return;
m_inspector_widget = new QWidget;
m_inspector_widget->setWindowTitle("Inspector");
auto* layout = new QVBoxLayout;
m_inspector_widget->setLayout(layout);
auto* tree_view = new QTreeView;
layout->addWidget(tree_view);
auto dom_tree = m_page_client->page().top_level_browsing_context().active_document()->dump_dom_tree_as_json();
auto dom_tree_model = ::WebView::DOMTreeModel::create(dom_tree);
auto* model = new Ladybird::ModelTranslator(dom_tree_model);
tree_view->setModel(model);
tree_view->setHeaderHidden(true);
tree_view->expandToDepth(3);
m_inspector_widget->resize(640, 480);
QObject::connect(tree_view->selectionModel(), &QItemSelectionModel::currentChanged, [this](QModelIndex const& index, QModelIndex const&) {
auto const* json = (JsonObject const*)index.internalPointer();
m_page_client->page().top_level_browsing_context().active_document()->set_inspected_node(Web::DOM::Node::from_id(json->get("id"sv).to_i32()));
});
}
void SimpleWebView::show_inspector()
{
ensure_inspector_widget();
m_inspector_widget->show();
}
void SimpleWebView::set_color_scheme(ColorScheme color_scheme)
{
switch (color_scheme) {
case ColorScheme::Auto:
m_page_client->m_preferred_color_scheme = Web::CSS::PreferredColorScheme::Auto;
break;
case ColorScheme::Light:
m_page_client->m_preferred_color_scheme = Web::CSS::PreferredColorScheme::Light;
break;
case ColorScheme::Dark:
m_page_client->m_preferred_color_scheme = Web::CSS::PreferredColorScheme::Dark;
break;
}
if (auto* document = m_page_client->page().top_level_browsing_context().active_document())
document->invalidate_style();
}
void SimpleWebView::showEvent(QShowEvent* event)
{
QAbstractScrollArea::showEvent(event);
m_page_client->page().top_level_browsing_context().set_system_visibility_state(Web::HTML::VisibilityState::Visible);
}
void SimpleWebView::hideEvent(QHideEvent* event)
{
QAbstractScrollArea::hideEvent(event);
m_page_client->page().top_level_browsing_context().set_system_visibility_state(Web::HTML::VisibilityState::Hidden);
}

View file

@ -1,89 +0,0 @@
/*
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#define AK_DONT_REPLACE_STD
#include <AK/OwnPtr.h>
#include <AK/String.h>
#include <LibGfx/Forward.h>
#include <QAbstractScrollArea>
#include <QPointer>
class QTextEdit;
class QLineEdit;
namespace Ladybird {
class PageClientLadybird;
}
enum class ColorScheme {
Auto,
Light,
Dark,
};
class SimpleWebView final : public QAbstractScrollArea {
Q_OBJECT
public:
SimpleWebView();
virtual ~SimpleWebView() override;
void load(String const& url);
void reload();
virtual void paintEvent(QPaintEvent*) override;
virtual void resizeEvent(QResizeEvent*) override;
virtual void mouseMoveEvent(QMouseEvent*) override;
virtual void mousePressEvent(QMouseEvent*) override;
virtual void mouseReleaseEvent(QMouseEvent*) override;
virtual void keyPressEvent(QKeyEvent* event) override;
virtual void keyReleaseEvent(QKeyEvent* event) override;
virtual void showEvent(QShowEvent*) override;
virtual void hideEvent(QHideEvent*) override;
void debug_request(String const& request, String const& argument);
String source() const;
void run_javascript(String const& js_source) const;
void did_output_js_console_message(i32 message_index);
void did_get_js_console_messages(i32 start_index, Vector<String> message_types, Vector<String> messages);
void show_js_console();
void show_inspector();
Gfx::IntPoint to_content(Gfx::IntPoint) const;
Gfx::IntPoint to_widget(Gfx::IntPoint) const;
void set_color_scheme(ColorScheme);
signals:
void link_hovered(QString, int timeout = 0);
void link_unhovered();
void load_started(const URL&);
void title_changed(QString);
void favicon_changed(QIcon);
private:
void update_viewport_rect();
void ensure_js_console_widget();
void ensure_inspector_widget();
OwnPtr<Ladybird::PageClientLadybird> m_page_client;
qreal m_inverse_pixel_scaling_ratio { 1.0 };
bool m_should_show_line_box_borders { false };
QPointer<QWidget> m_js_console_widget;
QPointer<QWidget> m_inspector_widget;
QTextEdit* m_js_console_output_edit { nullptr };
QLineEdit* m_js_console_input_edit { nullptr };
};

View file

@ -13,6 +13,7 @@
#include <QCoreApplication>
#include <QFont>
#include <QFontMetrics>
#include <QPlainTextEdit>
#include <QPoint>
#include <QResizeEvent>
@ -27,7 +28,7 @@ Tab::Tab(BrowserWindow* window)
m_layout->setSpacing(0);
m_layout->setContentsMargins(0, 0, 0, 0);
m_view = new SimpleWebView;
m_view = new WebContentView;
m_toolbar = new QToolBar(this);
m_location_edit = new QLineEdit(this);
@ -63,30 +64,39 @@ Tab::Tab(BrowserWindow* window)
m_toolbar->addAction(m_home_action);
m_toolbar->addWidget(m_location_edit);
QObject::connect(m_view, &SimpleWebView::link_hovered, [this](QString const& title) {
QObject::connect(m_view, &WebContentView::link_hovered, [this](QString const& title) {
m_hover_label->setText(title);
update_hover_label();
m_hover_label->show();
});
QObject::connect(m_view, &SimpleWebView::link_unhovered, [this] {
QObject::connect(m_view, &WebContentView::link_unhovered, [this] {
m_hover_label->hide();
});
QObject::connect(m_view, &SimpleWebView::load_started, [this](const URL& url) {
QObject::connect(m_view, &WebContentView::load_started, [this](const URL& url) {
m_location_edit->setText(url.to_string().characters());
m_history.push(url, m_title.toUtf8().data());
m_back_action->setEnabled(m_history.can_go_back());
m_forward_action->setEnabled(m_history.can_go_forward());
});
QObject::connect(m_location_edit, &QLineEdit::returnPressed, this, &Tab::location_edit_return_pressed);
QObject::connect(m_view, &SimpleWebView::title_changed, this, &Tab::page_title_changed);
QObject::connect(m_view, &SimpleWebView::favicon_changed, this, &Tab::page_favicon_changed);
QObject::connect(m_view, &WebContentView::title_changed, this, &Tab::page_title_changed);
QObject::connect(m_view, &WebContentView::favicon_changed, this, &Tab::page_favicon_changed);
QObject::connect(m_back_action, &QAction::triggered, this, &Tab::back);
QObject::connect(m_forward_action, &QAction::triggered, this, &Tab::forward);
QObject::connect(m_home_action, &QAction::triggered, this, &Tab::home);
QObject::connect(m_reload_action, &QAction::triggered, this, &Tab::reload);
QObject::connect(focus_location_editor_action, &QAction::triggered, this, &Tab::focus_location_editor);
QObject::connect(m_view, &WebContentView::got_source, this, [this](AK::URL, QString const& source) {
auto* text_edit = new QPlainTextEdit(this);
text_edit->setWindowFlags(Qt::Window);
text_edit->setFont(QFontDatabase::systemFont(QFontDatabase::SystemFont::FixedFont));
text_edit->resize(800, 600);
text_edit->setPlainText(source);
text_edit->show();
});
}
void Tab::focus_location_editor()
@ -99,7 +109,7 @@ void Tab::navigate(QString url)
{
if (!url.startsWith("http://", Qt::CaseInsensitive) && !url.startsWith("https://", Qt::CaseInsensitive) && !url.startsWith("file://", Qt::CaseInsensitive))
url = "http://" + url;
view().load(url.toUtf8().data());
view().load(akstring_from_qstring(url));
}
void Tab::back()

View file

@ -10,7 +10,7 @@
#define AK_DONT_REPLACE_STD
#include "History.h"
#include "SimpleWebView.h"
#include "WebContentView.h"
#include <QBoxLayout>
#include <QLabel>
#include <QLineEdit>
@ -24,7 +24,7 @@ class Tab final : public QWidget {
public:
explicit Tab(BrowserWindow* window);
SimpleWebView& view() { return *m_view; }
WebContentView& view() { return *m_view; }
void navigate(QString);
@ -52,7 +52,7 @@ private:
QBoxLayout* m_layout;
QToolBar* m_toolbar { nullptr };
QLineEdit* m_location_edit { nullptr };
SimpleWebView* m_view { nullptr };
WebContentView* m_view { nullptr };
BrowserWindow* m_window { nullptr };
Browser::History m_history;
QString m_title;

View file

@ -7,6 +7,12 @@
#define AK_DONT_REPLACE_STD
#include "Utilities.h"
#include <AK/LexicalPath.h>
#include <AK/Platform.h>
#include <LibCore/File.h>
#include <QCoreApplication>
String s_serenity_resource_root;
AK::String akstring_from_qstring(QString const& qstring)
{
@ -17,3 +23,25 @@ QString qstring_from_akstring(AK::String const& akstring)
{
return QString::fromUtf8(akstring.characters(), akstring.length());
}
void platform_init()
{
#ifdef AK_OS_ANDROID
extern void android_platform_init();
android_platform_init();
#else
s_serenity_resource_root = [] {
auto const* source_dir = getenv("SERENITY_SOURCE_DIR");
if (source_dir) {
return String::formatted("{}/Base", source_dir);
}
auto* home = getenv("XDG_CONFIG_HOME") ?: getenv("HOME");
VERIFY(home);
auto home_lagom = String::formatted("{}/.lagom", home);
if (Core::File::is_directory(home_lagom))
return home_lagom;
auto app_dir = akstring_from_qstring(QCoreApplication::applicationDirPath());
return LexicalPath(app_dir).parent().append("share"sv).string();
}();
#endif
}

View file

@ -11,3 +11,6 @@
AK::String akstring_from_qstring(QString const&);
QString qstring_from_akstring(AK::String const&);
void platform_init();
extern String s_serenity_resource_root;

View file

@ -0,0 +1,28 @@
set(WEBCONTENT_SOURCE_DIR ${SERENITY_SOURCE_DIR}/Userland/Services/WebContent/)
compile_ipc(${WEBCONTENT_SOURCE_DIR}/WebContentServer.ipc WebContentServerEndpoint.h)
compile_ipc(${WEBCONTENT_SOURCE_DIR}/WebContentClient.ipc WebContentClientEndpoint.h)
set(WEBCONTENT_SOURCES
${WEBCONTENT_SOURCE_DIR}/ConnectionFromClient.cpp
${WEBCONTENT_SOURCE_DIR}/ConsoleGlobalObject.cpp
${WEBCONTENT_SOURCE_DIR}/PageHost.cpp
${WEBCONTENT_SOURCE_DIR}/WebContentConsoleClient.cpp
../EventLoopPluginQt.cpp
../FontPluginQt.cpp
../ImageCodecPluginLadybird.cpp
../RequestManagerQt.cpp
../TimerQt.cpp
../Utilities.cpp
../WebSocketClientManagerLadybird.cpp
../WebSocketLadybird.cpp
WebContentClientEndpoint.h
WebContentServerEndpoint.h
main.cpp
)
qt_add_executable(WebContent ${WEBCONTENT_SOURCES})
target_include_directories(WebContent PRIVATE ${SERENITY_SOURCE_DIR}/Userland/Services/)
target_include_directories(WebContent PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/..)
target_link_libraries(WebContent PRIVATE Qt::Core Qt::Network Qt::Gui LibCore LibIPC LibGfx LibWebView LibWebSocket LibWeb LibMain)

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2020-2022, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#define AK_DONT_REPLACE_STD
#include "../EventLoopPluginQt.h"
#include "../FontPluginQt.h"
#include "../ImageCodecPluginLadybird.h"
#include "../RequestManagerQt.h"
#include "../Utilities.h"
#include "../WebSocketClientManagerLadybird.h"
#include <AK/LexicalPath.h>
#include <LibCore/EventLoop.h>
#include <LibCore/File.h>
#include <LibCore/LocalServer.h>
#include <LibCore/System.h>
#include <LibIPC/SingleServer.h>
#include <LibMain/Main.h>
#include <LibWeb/Loader/ContentFilter.h>
#include <LibWeb/Loader/FrameLoader.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/WebSockets/WebSocket.h>
#include <QGuiApplication>
#include <QSocketNotifier>
#include <QTimer>
#include <WebContent/ConnectionFromClient.h>
static ErrorOr<void> load_content_filters();
extern String s_serenity_resource_root;
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
// NOTE: This is only used for the Core::Socket inside the IPC connection.
// FIXME: Refactor things so we can get rid of this somehow.
Core::EventLoop event_loop;
platform_init();
QGuiApplication app(arguments.argc, arguments.argv);
Web::Platform::EventLoopPlugin::install(*new Ladybird::EventLoopPluginQt);
Web::Platform::ImageCodecPlugin::install(*new Ladybird::ImageCodecPluginLadybird);
Web::ResourceLoader::initialize(RequestManagerQt::create());
Web::WebSockets::WebSocketClientManager::initialize(Ladybird::WebSocketClientManagerLadybird::create());
Web::FrameLoader::set_default_favicon_path(String::formatted("{}/res/icons/16x16/app-browser.png", s_serenity_resource_root));
Web::Platform::FontPlugin::install(*new Ladybird::FontPluginQt);
Web::FrameLoader::set_error_page_url(String::formatted("file://{}/res/html/error.html", s_serenity_resource_root));
auto maybe_content_filter_error = load_content_filters();
if (maybe_content_filter_error.is_error())
dbgln("Failed to load content filters: {}", maybe_content_filter_error.error());
auto client = TRY(IPC::take_over_accepted_client_from_system_server<WebContent::ConnectionFromClient>());
auto* fd_passing_socket_spec = getenv("FD_PASSING_SOCKET");
VERIFY(fd_passing_socket_spec);
auto fd_passing_socket_spec_string = String(fd_passing_socket_spec);
auto maybe_fd_passing_socket = fd_passing_socket_spec_string.to_int();
VERIFY(maybe_fd_passing_socket.has_value());
client->set_fd_passing_socket(TRY(Core::Stream::LocalSocket::adopt_fd(maybe_fd_passing_socket.value())));
QSocketNotifier notifier(client->socket().fd().value(), QSocketNotifier::Type::Read);
QObject::connect(&notifier, &QSocketNotifier::activated, [&] {
client->socket().notifier()->on_ready_to_read();
});
struct DeferredInvokerQt final : IPC::DeferredInvoker {
virtual ~DeferredInvokerQt() = default;
virtual void schedule(Function<void()> callback) override
{
QTimer::singleShot(0, move(callback));
}
};
client->set_deferred_invoker(make<DeferredInvokerQt>());
return app.exec();
}
static ErrorOr<void> load_content_filters()
{
auto file_or_error = Core::Stream::File::open(String::formatted("{}/home/anon/.config/BrowserContentFilters.txt", s_serenity_resource_root), Core::Stream::OpenMode::Read);
if (file_or_error.is_error())
file_or_error = Core::Stream::File::open(String::formatted("{}/res/ladybird/BrowserContentFilters.txt", s_serenity_resource_root), Core::Stream::OpenMode::Read);
if (file_or_error.is_error())
return file_or_error.release_error();
auto file = file_or_error.release_value();
auto ad_filter_list = TRY(Core::Stream::BufferedFile::create(move(file)));
auto buffer = TRY(ByteBuffer::create_uninitialized(4096));
while (TRY(ad_filter_list->can_read_line())) {
auto line = TRY(ad_filter_list->read_line(buffer));
if (!line.is_empty()) {
Web::ContentFilter::the().add_pattern(line);
}
}
return {};
}

911
Ladybird/WebContentView.cpp Normal file
View file

@ -0,0 +1,911 @@
/*
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#define AK_DONT_REPLACE_STD
#include "WebContentView.h"
#include "CookieJar.h"
#include "ModelTranslator.h"
#include "Utilities.h"
#include <AK/Assertions.h>
#include <AK/ByteBuffer.h>
#include <AK/Format.h>
#include <AK/HashTable.h>
#include <AK/LexicalPath.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/StringBuilder.h>
#include <AK/Types.h>
#include <Kernel/API/KeyCode.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/EventLoop.h>
#include <LibCore/File.h>
#include <LibCore/IODevice.h>
#include <LibCore/MemoryStream.h>
#include <LibCore/Stream.h>
#include <LibCore/System.h>
#include <LibCore/Timer.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibGfx/PNGWriter.h>
#include <LibGfx/Painter.h>
#include <LibGfx/Rect.h>
#include <LibJS/Runtime/ConsoleObject.h>
#include <LibMain/Main.h>
#include <LibWeb/Loader/ContentFilter.h>
#include <LibWebView/DOMTreeModel.h>
#include <LibWebView/WebContentClient.h>
#include <QApplication>
#include <QCursor>
#include <QIcon>
#include <QLineEdit>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPaintEvent>
#include <QPainter>
#include <QScrollBar>
#include <QSocketNotifier>
#include <QTextEdit>
#include <QTimer>
#include <QToolTip>
#include <QTreeView>
#include <QVBoxLayout>
WebContentView::WebContentView()
{
setMouseTracking(true);
m_inverse_pixel_scaling_ratio = 1.0 / devicePixelRatio();
verticalScrollBar()->setSingleStep(24);
horizontalScrollBar()->setSingleStep(24);
QObject::connect(verticalScrollBar(), &QScrollBar::valueChanged, [this](int) {
update_viewport_rect();
});
QObject::connect(horizontalScrollBar(), &QScrollBar::valueChanged, [this](int) {
update_viewport_rect();
});
create_client();
}
WebContentView::~WebContentView()
{
}
void WebContentView::reload()
{
load(m_url);
}
void WebContentView::load(AK::URL const& url)
{
m_url = url;
client().async_load_url(url);
}
void WebContentView::load_html(StringView html, AK::URL const& url)
{
m_url = url;
client().async_load_html(html, url);
}
unsigned get_button_from_qt_event(QMouseEvent const& event)
{
if (event.button() == Qt::MouseButton::LeftButton)
return 1;
if (event.button() == Qt::MouseButton::RightButton)
return 2;
if (event.button() == Qt::MouseButton::MiddleButton)
return 4;
if (event.button() == Qt::MouseButton::BackButton)
return 8;
if (event.buttons() == Qt::MouseButton::ForwardButton)
return 16;
return 0;
}
unsigned get_buttons_from_qt_event(QMouseEvent const& event)
{
unsigned buttons = 0;
if (event.buttons() & Qt::MouseButton::LeftButton)
buttons |= 1;
if (event.buttons() & Qt::MouseButton::RightButton)
buttons |= 2;
if (event.buttons() & Qt::MouseButton::MiddleButton)
buttons |= 4;
if (event.buttons() & Qt::MouseButton::BackButton)
buttons |= 8;
if (event.buttons() & Qt::MouseButton::ForwardButton)
buttons |= 16;
return buttons;
}
unsigned get_modifiers_from_qt_mouse_event(QMouseEvent const& event)
{
unsigned modifiers = 0;
if (event.modifiers() & Qt::Modifier::ALT)
modifiers |= 1;
if (event.modifiers() & Qt::Modifier::CTRL)
modifiers |= 2;
if (event.modifiers() & Qt::Modifier::SHIFT)
modifiers |= 4;
return modifiers;
}
unsigned get_modifiers_from_qt_keyboard_event(QKeyEvent const& event)
{
auto modifiers = 0;
if (event.modifiers().testFlag(Qt::AltModifier))
modifiers |= KeyModifier::Mod_Alt;
if (event.modifiers().testFlag(Qt::ControlModifier))
modifiers |= KeyModifier::Mod_Ctrl;
if (event.modifiers().testFlag(Qt::MetaModifier))
modifiers |= KeyModifier::Mod_Super;
if (event.modifiers().testFlag(Qt::ShiftModifier))
modifiers |= KeyModifier::Mod_Shift;
if (event.modifiers().testFlag(Qt::AltModifier))
modifiers |= KeyModifier::Mod_AltGr;
return modifiers;
}
KeyCode get_keycode_from_qt_keyboard_event(QKeyEvent const& event)
{
struct Mapping {
constexpr Mapping(Qt::Key q, KeyCode s)
: qt_key(q)
, serenity_key(s)
{
}
Qt::Key qt_key;
KeyCode serenity_key;
};
constexpr Mapping mappings[] = {
{ Qt::Key_0, Key_0 },
{ Qt::Key_1, Key_1 },
{ Qt::Key_2, Key_2 },
{ Qt::Key_3, Key_3 },
{ Qt::Key_4, Key_4 },
{ Qt::Key_5, Key_5 },
{ Qt::Key_6, Key_6 },
{ Qt::Key_7, Key_7 },
{ Qt::Key_8, Key_8 },
{ Qt::Key_9, Key_9 },
{ Qt::Key_A, Key_A },
{ Qt::Key_Alt, Key_Alt },
{ Qt::Key_Ampersand, Key_Ampersand },
{ Qt::Key_Apostrophe, Key_Apostrophe },
{ Qt::Key_AsciiCircum, Key_Circumflex },
{ Qt::Key_AsciiTilde, Key_Tilde },
{ Qt::Key_Asterisk, Key_Asterisk },
{ Qt::Key_At, Key_AtSign },
{ Qt::Key_B, Key_B },
{ Qt::Key_Backslash, Key_Backslash },
{ Qt::Key_Backspace, Key_Backspace },
{ Qt::Key_Bar, Key_Pipe },
{ Qt::Key_BraceLeft, Key_LeftBrace },
{ Qt::Key_BraceRight, Key_RightBrace },
{ Qt::Key_BracketLeft, Key_LeftBracket },
{ Qt::Key_BracketRight, Key_RightBracket },
{ Qt::Key_C, Key_C },
{ Qt::Key_CapsLock, Key_CapsLock },
{ Qt::Key_Colon, Key_Colon },
{ Qt::Key_Comma, Key_Comma },
{ Qt::Key_Control, Key_Control },
{ Qt::Key_D, Key_D },
{ Qt::Key_Delete, Key_Delete },
{ Qt::Key_Dollar, Key_Dollar },
{ Qt::Key_Down, Key_Down },
{ Qt::Key_E, Key_E },
{ Qt::Key_End, Key_End },
{ Qt::Key_Equal, Key_Equal },
{ Qt::Key_Escape, Key_Escape },
{ Qt::Key_exclamdown, Key_ExclamationPoint },
{ Qt::Key_F, Key_F },
{ Qt::Key_F1, Key_F1 },
{ Qt::Key_F10, Key_F10 },
{ Qt::Key_F11, Key_F11 },
{ Qt::Key_F12, Key_F12 },
{ Qt::Key_F2, Key_F2 },
{ Qt::Key_F3, Key_F3 },
{ Qt::Key_F4, Key_F4 },
{ Qt::Key_F5, Key_F5 },
{ Qt::Key_F6, Key_F6 },
{ Qt::Key_F7, Key_F7 },
{ Qt::Key_F8, Key_F8 },
{ Qt::Key_F9, Key_F9 },
{ Qt::Key_G, Key_G },
{ Qt::Key_Greater, Key_GreaterThan },
{ Qt::Key_H, Key_H },
{ Qt::Key_Home, Key_Home },
{ Qt::Key_I, Key_I },
{ Qt::Key_Insert, Key_Insert },
{ Qt::Key_J, Key_J },
{ Qt::Key_K, Key_K },
{ Qt::Key_L, Key_L },
{ Qt::Key_Left, Key_Left },
{ Qt::Key_Less, Key_LessThan },
{ Qt::Key_M, Key_M },
{ Qt::Key_Menu, Key_Menu },
{ Qt::Key_Minus, Key_Minus },
{ Qt::Key_N, Key_N },
{ Qt::Key_NumLock, Key_NumLock },
{ Qt::Key_O, Key_O },
{ Qt::Key_P, Key_P },
{ Qt::Key_PageDown, Key_PageDown },
{ Qt::Key_PageUp, Key_PageUp },
{ Qt::Key_ParenLeft, Key_LeftParen },
{ Qt::Key_ParenRight, Key_RightParen },
{ Qt::Key_Percent, Key_Percent },
{ Qt::Key_Period, Key_Period },
{ Qt::Key_Plus, Key_Plus },
{ Qt::Key_Print, Key_PrintScreen },
{ Qt::Key_Q, Key_Q },
{ Qt::Key_Question, Key_QuestionMark },
{ Qt::Key_QuoteDbl, Key_DoubleQuote },
{ Qt::Key_R, Key_R },
{ Qt::Key_Return, Key_Return },
{ Qt::Key_Right, Key_Right },
{ Qt::Key_S, Key_S },
{ Qt::Key_ScrollLock, Key_ScrollLock },
{ Qt::Key_Semicolon, Key_Semicolon },
{ Qt::Key_Shift, Key_LeftShift },
{ Qt::Key_Slash, Key_Slash },
{ Qt::Key_Space, Key_Space },
{ Qt::Key_Super_L, Key_Super },
{ Qt::Key_SysReq, Key_SysRq },
{ Qt::Key_T, Key_T },
{ Qt::Key_Tab, Key_Tab },
{ Qt::Key_U, Key_U },
{ Qt::Key_Underscore, Key_Underscore },
{ Qt::Key_Up, Key_Up },
{ Qt::Key_V, Key_V },
{ Qt::Key_W, Key_W },
{ Qt::Key_X, Key_X },
{ Qt::Key_Y, Key_Y },
{ Qt::Key_Z, Key_Z },
};
for (auto const& mapping : mappings) {
if (event.key() == mapping.qt_key)
return mapping.serenity_key;
}
return Key_Invalid;
}
void WebContentView::mouseMoveEvent(QMouseEvent* event)
{
Gfx::IntPoint position(event->position().x() / m_inverse_pixel_scaling_ratio, event->position().y() / m_inverse_pixel_scaling_ratio);
auto buttons = get_buttons_from_qt_event(*event);
auto modifiers = get_modifiers_from_qt_mouse_event(*event);
client().async_mouse_move(to_content(position), 0, buttons, modifiers);
}
void WebContentView::mousePressEvent(QMouseEvent* event)
{
Gfx::IntPoint position(event->position().x() / m_inverse_pixel_scaling_ratio, event->position().y() / m_inverse_pixel_scaling_ratio);
auto button = get_button_from_qt_event(*event);
if (button == 0) {
// We could not convert Qt buttons to something that Lagom can
// recognize - don't even bother propagating this to the web engine
// as it will not handle it anyway, and it will (currently) assert
return;
}
auto modifiers = get_modifiers_from_qt_mouse_event(*event);
auto buttons = get_buttons_from_qt_event(*event);
client().async_mouse_down(to_content(position), button, buttons, modifiers);
}
void WebContentView::mouseReleaseEvent(QMouseEvent* event)
{
Gfx::IntPoint position(event->position().x() / m_inverse_pixel_scaling_ratio, event->position().y() / m_inverse_pixel_scaling_ratio);
auto button = get_button_from_qt_event(*event);
if (button == 0) {
// We could not convert Qt buttons to something that Lagom can
// recognize - don't even bother propagating this to the web engine
// as it will not handle it anyway, and it will (currently) assert
return;
}
auto modifiers = get_modifiers_from_qt_mouse_event(*event);
auto buttons = get_buttons_from_qt_event(*event);
client().async_mouse_up(to_content(position), button, buttons, modifiers);
}
void WebContentView::keyPressEvent(QKeyEvent* event)
{
switch (event->key()) {
case Qt::Key_Left:
case Qt::Key_Right:
case Qt::Key_Up:
case Qt::Key_Down:
case Qt::Key_PageUp:
case Qt::Key_PageDown:
QAbstractScrollArea::keyPressEvent(event);
break;
default:
break;
}
auto text = event->text();
if (text.isEmpty()) {
return;
}
auto point = event->text()[0].unicode();
auto keycode = get_keycode_from_qt_keyboard_event(*event);
auto modifiers = get_modifiers_from_qt_keyboard_event(*event);
client().async_key_down(keycode, modifiers, point);
}
void WebContentView::keyReleaseEvent(QKeyEvent* event)
{
auto text = event->text();
if (text.isEmpty()) {
return;
}
auto point = event->text()[0].unicode();
auto keycode = get_keycode_from_qt_keyboard_event(*event);
auto modifiers = get_modifiers_from_qt_keyboard_event(*event);
client().async_key_up(keycode, modifiers, point);
}
Gfx::IntPoint WebContentView::to_content(Gfx::IntPoint viewport_position) const
{
return viewport_position.translated(horizontalScrollBar()->value(), verticalScrollBar()->value());
}
Gfx::IntPoint WebContentView::to_widget(Gfx::IntPoint content_position) const
{
return content_position.translated(-horizontalScrollBar()->value(), -verticalScrollBar()->value());
}
void WebContentView::paintEvent(QPaintEvent*)
{
QPainter painter(viewport());
painter.scale(m_inverse_pixel_scaling_ratio, m_inverse_pixel_scaling_ratio);
if (auto* bitmap = m_client_state.has_usable_bitmap ? m_client_state.front_bitmap.bitmap.ptr() : m_backup_bitmap.ptr()) {
QImage q_image(bitmap->scanline_u8(0), bitmap->width(), bitmap->height(), QImage::Format_RGB32);
painter.drawImage(QPoint(0, 0), q_image);
return;
}
painter.fillRect(rect(), palette().base());
}
void WebContentView::resizeEvent(QResizeEvent* event)
{
QAbstractScrollArea::resizeEvent(event);
handle_resize();
}
void WebContentView::handle_resize()
{
update_viewport_rect();
if (m_client_state.has_usable_bitmap) {
// NOTE: We keep the outgoing front bitmap as a backup so we have something to paint until we get a new one.
m_backup_bitmap = m_client_state.front_bitmap.bitmap;
}
if (m_client_state.front_bitmap.bitmap)
client().async_remove_backing_store(m_client_state.front_bitmap.id);
if (m_client_state.back_bitmap.bitmap)
client().async_remove_backing_store(m_client_state.back_bitmap.id);
m_client_state.front_bitmap = {};
m_client_state.back_bitmap = {};
m_client_state.has_usable_bitmap = false;
auto available_size = m_viewport_rect.size();
if (available_size.is_empty())
return;
if (auto new_bitmap_or_error = Gfx::Bitmap::try_create_shareable(Gfx::BitmapFormat::BGRx8888, available_size); !new_bitmap_or_error.is_error()) {
m_client_state.front_bitmap.bitmap = new_bitmap_or_error.release_value();
m_client_state.front_bitmap.id = m_client_state.next_bitmap_id++;
client().async_add_backing_store(m_client_state.front_bitmap.id, m_client_state.front_bitmap.bitmap->to_shareable_bitmap());
}
if (auto new_bitmap_or_error = Gfx::Bitmap::try_create_shareable(Gfx::BitmapFormat::BGRx8888, available_size); !new_bitmap_or_error.is_error()) {
m_client_state.back_bitmap.bitmap = new_bitmap_or_error.release_value();
m_client_state.back_bitmap.id = m_client_state.next_bitmap_id++;
client().async_add_backing_store(m_client_state.back_bitmap.id, m_client_state.back_bitmap.bitmap->to_shareable_bitmap());
}
request_repaint();
}
void WebContentView::update_viewport_rect()
{
auto scaled_width = int(viewport()->width() / m_inverse_pixel_scaling_ratio);
auto scaled_height = int(viewport()->height() / m_inverse_pixel_scaling_ratio);
Gfx::IntRect rect(horizontalScrollBar()->value(), verticalScrollBar()->value(), scaled_width, scaled_height);
m_viewport_rect = rect;
client().async_set_viewport_rect(rect);
request_repaint();
}
void WebContentView::debug_request(String const& request, String const& argument)
{
client().async_debug_request(request, argument);
}
void WebContentView::run_javascript(String const& js_source)
{
client().async_run_javascript(js_source);
}
void WebContentView::did_output_js_console_message(i32 message_index)
{
// FIXME
(void)message_index;
}
void WebContentView::did_get_js_console_messages(i32, Vector<String>, Vector<String> messages)
{
ensure_js_console_widget();
for (auto& message : messages) {
m_js_console_output_edit->append(qstring_from_akstring(message).trimmed());
}
}
void WebContentView::ensure_js_console_widget()
{
if (!m_js_console_widget) {
m_js_console_widget = new QWidget;
m_js_console_widget->setWindowTitle("JS Console");
auto* layout = new QVBoxLayout(m_js_console_widget);
m_js_console_widget->setLayout(layout);
m_js_console_output_edit = new QTextEdit(this);
m_js_console_output_edit->setReadOnly(true);
m_js_console_input_edit = new QLineEdit(this);
layout->addWidget(m_js_console_output_edit);
layout->addWidget(m_js_console_input_edit);
m_js_console_widget->resize(640, 480);
QObject::connect(m_js_console_input_edit, &QLineEdit::returnPressed, [this] {
auto code = m_js_console_input_edit->text().trimmed();
client().async_js_console_input(akstring_from_qstring(code));
m_js_console_input_edit->clear();
m_js_console_output_edit->append(QString("> %1").arg(code));
});
}
}
void WebContentView::show_js_console()
{
ensure_js_console_widget();
m_js_console_widget->show();
m_js_console_input_edit->setFocus();
}
void WebContentView::ensure_inspector_widget()
{
if (m_inspector_widget)
return;
#if 0
m_inspector_widget = new QWidget;
m_inspector_widget->setWindowTitle("Inspector");
auto* layout = new QVBoxLayout;
m_inspector_widget->setLayout(layout);
auto* tree_view = new QTreeView;
layout->addWidget(tree_view);
auto dom_tree = m_page_client->page().top_level_browsing_context().active_document()->dump_dom_tree_as_json();
auto dom_tree_model = ::WebView::DOMTreeModel::create(dom_tree);
auto* model = new Ladybird::ModelTranslator(dom_tree_model);
tree_view->setModel(model);
tree_view->setHeaderHidden(true);
tree_view->expandToDepth(3);
m_inspector_widget->resize(640, 480);
QObject::connect(tree_view->selectionModel(), &QItemSelectionModel::currentChanged, [this](QModelIndex const& index, QModelIndex const&) {
auto const* json = (JsonObject const*)index.internalPointer();
m_page_client->page().top_level_browsing_context().active_document()->set_inspected_node(Web::DOM::Node::from_id(json->get("id"sv).to_i32()));
});
#endif
}
void WebContentView::show_inspector()
{
ensure_inspector_widget();
m_inspector_widget->show();
}
void WebContentView::set_color_scheme(ColorScheme color_scheme)
{
switch (color_scheme) {
case ColorScheme::Auto:
client().async_set_preferred_color_scheme(Web::CSS::PreferredColorScheme::Auto);
break;
case ColorScheme::Light:
client().async_set_preferred_color_scheme(Web::CSS::PreferredColorScheme::Light);
break;
case ColorScheme::Dark:
client().async_set_preferred_color_scheme(Web::CSS::PreferredColorScheme::Dark);
break;
}
}
void WebContentView::showEvent(QShowEvent* event)
{
QAbstractScrollArea::showEvent(event);
client().async_set_system_visibility_state(true);
}
void WebContentView::hideEvent(QHideEvent* event)
{
QAbstractScrollArea::hideEvent(event);
client().async_set_system_visibility_state(false);
}
WebContentClient& WebContentView::client()
{
VERIFY(m_client_state.client);
return *m_client_state.client;
}
void WebContentView::create_client()
{
m_client_state = {};
int socket_fds[2] {};
MUST(Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, socket_fds));
int ui_fd = socket_fds[0];
int wc_fd = socket_fds[1];
int fd_passing_socket_fds[2] {};
MUST(Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, fd_passing_socket_fds));
int ui_fd_passing_fd = fd_passing_socket_fds[0];
int wc_fd_passing_fd = fd_passing_socket_fds[1];
auto child_pid = fork();
if (!child_pid) {
auto takeover_string = String::formatted("x:{}", wc_fd);
MUST(Core::System::setenv("SOCKET_TAKEOVER"sv, takeover_string, true));
auto fd_passing_socket_string = String::formatted("{}", wc_fd_passing_fd);
MUST(Core::System::setenv("FD_PASSING_SOCKET"sv, fd_passing_socket_string, true));
auto rc = execlp("./WebContent/WebContent", "WebContent", nullptr);
if (rc < 0)
perror("execlp");
VERIFY_NOT_REACHED();
}
auto socket = MUST(Core::Stream::LocalSocket::adopt_fd(ui_fd));
MUST(socket->set_blocking(true));
auto new_client = MUST(adopt_nonnull_ref_or_enomem(new (nothrow) WebView::WebContentClient(std::move(socket), *this)));
new_client->set_fd_passing_socket(MUST(Core::Stream::LocalSocket::adopt_fd(ui_fd_passing_fd)));
auto* notifier = new QSocketNotifier(new_client->socket().fd().value(), QSocketNotifier::Type::Read);
QObject::connect(notifier, &QSocketNotifier::activated, [new_client = new_client.ptr()] {
if (auto notifier = new_client->socket().notifier())
notifier->on_ready_to_read();
});
struct DeferredInvokerQt final : IPC::DeferredInvoker {
virtual ~DeferredInvokerQt() = default;
virtual void schedule(Function<void()> callback) override
{
QTimer::singleShot(0, std::move(callback));
}
};
new_client->set_deferred_invoker(make<DeferredInvokerQt>());
m_client_state.client = new_client;
m_client_state.client->on_web_content_process_crash = [this] {
QTimer::singleShot(0, [this] {
handle_web_content_process_crash();
});
};
client().async_update_system_theme(Gfx::load_system_theme(String::formatted("{}/res/themes/Default.ini", s_serenity_resource_root)));
client().async_update_system_fonts(Gfx::FontDatabase::default_font_query(), Gfx::FontDatabase::fixed_width_font_query(), Gfx::FontDatabase::window_title_font_query());
// FIXME: Get the screen rect.
// client().async_update_screen_rects(GUI::Desktop::the().rects(), GUI::Desktop::the().main_screen_index());
}
void WebContentView::handle_web_content_process_crash()
{
dbgln("WebContent process crashed!");
create_client();
VERIFY(m_client_state.client);
// Don't keep a stale backup bitmap around.
m_backup_bitmap = nullptr;
handle_resize();
StringBuilder builder;
builder.append("<html><head><title>Crashed: "sv);
builder.append(escape_html_entities(m_url.to_string()));
builder.append("</title></head><body>"sv);
builder.append("<h1>Web page crashed"sv);
if (!m_url.host().is_empty()) {
builder.appendff(" on {}", escape_html_entities(m_url.host()));
}
builder.append("</h1>"sv);
auto escaped_url = escape_html_entities(m_url.to_string());
builder.appendff("The web page <a href=\"{}\">{}</a> has crashed.<br><br>You can reload the page to try again.", escaped_url, escaped_url);
builder.append("</body></html>"sv);
load_html(builder.to_string(), m_url);
}
void WebContentView::notify_server_did_paint(Badge<WebContentClient>, i32 bitmap_id)
{
if (m_client_state.back_bitmap.id == bitmap_id) {
m_client_state.has_usable_bitmap = true;
m_client_state.back_bitmap.pending_paints--;
swap(m_client_state.back_bitmap, m_client_state.front_bitmap);
// We don't need the backup bitmap anymore, so drop it.
m_backup_bitmap = nullptr;
viewport()->update();
if (m_client_state.got_repaint_requests_while_painting) {
m_client_state.got_repaint_requests_while_painting = false;
request_repaint();
}
}
}
void WebContentView::notify_server_did_invalidate_content_rect(Badge<WebContentClient>, [[maybe_unused]] Gfx::IntRect const& content_rect)
{
request_repaint();
}
void WebContentView::notify_server_did_change_selection(Badge<WebContentClient>)
{
request_repaint();
}
void WebContentView::notify_server_did_request_cursor_change(Badge<WebContentClient>, Gfx::StandardCursor cursor)
{
switch (cursor) {
case Gfx::StandardCursor::Hand:
setCursor(Qt::PointingHandCursor);
break;
case Gfx::StandardCursor::IBeam:
setCursor(Qt::IBeamCursor);
break;
case Gfx::StandardCursor::Arrow:
default:
setCursor(Qt::ArrowCursor);
break;
}
}
void WebContentView::notify_server_did_layout(Badge<WebContentClient>, Gfx::IntSize const& content_size)
{
verticalScrollBar()->setMinimum(0);
verticalScrollBar()->setMaximum(content_size.height() - m_viewport_rect.height());
verticalScrollBar()->setPageStep(m_viewport_rect.height());
horizontalScrollBar()->setMinimum(0);
horizontalScrollBar()->setMaximum(content_size.width() - m_viewport_rect.width());
horizontalScrollBar()->setPageStep(m_viewport_rect.width());
}
void WebContentView::notify_server_did_change_title(Badge<WebContentClient>, String const& title)
{
emit title_changed(qstring_from_akstring(title));
}
void WebContentView::notify_server_did_request_scroll(Badge<WebContentClient>, i32 x_delta, i32 y_delta)
{
horizontalScrollBar()->setValue(horizontalScrollBar()->value() + x_delta);
verticalScrollBar()->setValue(verticalScrollBar()->value() + y_delta);
}
void WebContentView::notify_server_did_request_scroll_to(Badge<WebContentClient>, Gfx::IntPoint const& scroll_position)
{
horizontalScrollBar()->setValue(scroll_position.x());
verticalScrollBar()->setValue(scroll_position.y());
}
void WebContentView::notify_server_did_request_scroll_into_view(Badge<WebContentClient>, Gfx::IntRect const& rect)
{
if (m_viewport_rect.contains(rect))
return;
if (rect.top() < m_viewport_rect.top()) {
verticalScrollBar()->setValue(rect.top());
} else if (rect.top() > m_viewport_rect.top() && rect.bottom() > m_viewport_rect.bottom()) {
verticalScrollBar()->setValue(rect.bottom() - m_viewport_rect.height() + 1);
}
}
void WebContentView::notify_server_did_enter_tooltip_area(Badge<WebContentClient>, Gfx::IntPoint const& content_position, String const& tooltip)
{
auto widget_position = to_widget(content_position);
QToolTip::showText(
mapToGlobal(QPoint(widget_position.x(), widget_position.y())),
qstring_from_akstring(tooltip),
this);
}
void WebContentView::notify_server_did_leave_tooltip_area(Badge<WebContentClient>)
{
QToolTip::hideText();
}
void WebContentView::notify_server_did_hover_link(Badge<WebContentClient>, AK::URL const& url)
{
emit link_hovered(qstring_from_akstring(url.to_string()));
}
void WebContentView::notify_server_did_unhover_link(Badge<WebContentClient>)
{
emit link_unhovered();
}
void WebContentView::notify_server_did_click_link(Badge<WebContentClient>, AK::URL const& url, String const& target, unsigned int modifiers)
{
// FIXME
(void)url;
(void)target;
(void)modifiers;
// if (on_link_click)
// on_link_click(url, target, modifiers);
}
void WebContentView::notify_server_did_middle_click_link(Badge<WebContentClient>, AK::URL const& url, String const& target, unsigned int modifiers)
{
(void)url;
(void)target;
(void)modifiers;
}
void WebContentView::notify_server_did_start_loading(Badge<WebContentClient>, AK::URL const& url)
{
emit load_started(url);
}
void WebContentView::notify_server_did_finish_loading(Badge<WebContentClient>, AK::URL const& url)
{
// FIXME
(void)url;
}
void WebContentView::notify_server_did_request_context_menu(Badge<WebContentClient>, Gfx::IntPoint const& content_position)
{
// FIXME
(void)content_position;
}
void WebContentView::notify_server_did_request_link_context_menu(Badge<WebContentClient>, Gfx::IntPoint const& content_position, AK::URL const& url, String const&, unsigned)
{
// FIXME
(void)content_position;
(void)url;
}
void WebContentView::notify_server_did_request_image_context_menu(Badge<WebContentClient>, Gfx::IntPoint const& content_position, AK::URL const& url, String const&, unsigned, Gfx::ShareableBitmap const& bitmap)
{
// FIXME
(void)content_position;
(void)url;
(void)bitmap;
}
void WebContentView::notify_server_did_request_alert(Badge<WebContentClient>, String const& message)
{
QMessageBox::warning(this, "Ladybird", qstring_from_akstring(message));
}
bool WebContentView::notify_server_did_request_confirm(Badge<WebContentClient>, String const& message)
{
auto result = QMessageBox::question(this, "Ladybird", qstring_from_akstring(message),
QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel);
return result == QMessageBox::StandardButton::Ok;
}
String WebContentView::notify_server_did_request_prompt(Badge<WebContentClient>, String const& message, String const& default_)
{
// FIXME
(void)message;
(void)default_;
return String::empty();
}
void WebContentView::get_source()
{
client().async_get_source();
}
void WebContentView::notify_server_did_get_source(AK::URL const& url, String const& source)
{
emit got_source(url, qstring_from_akstring(source));
}
void WebContentView::notify_server_did_get_dom_tree(String const& dom_tree)
{
if (on_get_dom_tree)
on_get_dom_tree(dom_tree);
}
void WebContentView::notify_server_did_get_dom_node_properties(i32 node_id, String const& specified_style, String const& computed_style, String const& custom_properties, String const& node_box_sizing)
{
if (on_get_dom_node_properties)
on_get_dom_node_properties(node_id, specified_style, computed_style, custom_properties, node_box_sizing);
}
void WebContentView::notify_server_did_output_js_console_message(i32 message_index)
{
if (on_js_console_new_message)
on_js_console_new_message(message_index);
}
void WebContentView::notify_server_did_get_js_console_messages(i32 start_index, Vector<String> const& message_types, Vector<String> const& messages)
{
if (on_get_js_console_messages)
on_get_js_console_messages(start_index, message_types, messages);
}
void WebContentView::notify_server_did_change_favicon(Gfx::Bitmap const& bitmap)
{
auto qimage = QImage(bitmap.scanline_u8(0), bitmap.width(), bitmap.height(), QImage::Format_ARGB32);
if (qimage.isNull())
return;
auto qpixmap = QPixmap::fromImage(qimage);
if (qpixmap.isNull())
return;
emit favicon_changed(QIcon(qpixmap));
}
String WebContentView::notify_server_did_request_cookie(Badge<WebContentClient>, AK::URL const& url, Web::Cookie::Source source)
{
if (on_get_cookie)
return on_get_cookie(url, source);
return {};
}
void WebContentView::notify_server_did_set_cookie(Badge<WebContentClient>, AK::URL const& url, Web::Cookie::ParsedCookie const& cookie, Web::Cookie::Source source)
{
if (on_set_cookie)
on_set_cookie(url, cookie, source);
}
void WebContentView::notify_server_did_update_resource_count(i32 count_waiting)
{
// FIXME
(void)count_waiting;
}
void WebContentView::notify_server_did_request_file(Badge<WebContentClient>, String const& path, i32 request_id)
{
auto file = Core::File::open(path, Core::OpenMode::ReadOnly);
if (file.is_error())
client().async_handle_file_return(file.error().code(), {}, request_id);
else
client().async_handle_file_return(0, IPC::File(file.value()->leak_fd()), request_id);
}
void WebContentView::request_repaint()
{
// If this widget was instantiated but not yet added to a window,
// it won't have a back bitmap yet, so we can just skip repaint requests.
if (!m_client_state.back_bitmap.bitmap)
return;
// Don't request a repaint until pending paint requests have finished.
if (m_client_state.back_bitmap.pending_paints) {
m_client_state.got_repaint_requests_while_painting = true;
return;
}
m_client_state.back_bitmap.pending_paints++;
client().async_paint(m_client_state.back_bitmap.bitmap->rect().translated(horizontalScrollBar()->value(), verticalScrollBar()->value()), m_client_state.back_bitmap.id);
}

186
Ladybird/WebContentView.h Normal file
View file

@ -0,0 +1,186 @@
/*
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#define AK_DONT_REPLACE_STD
#include <AK/Function.h>
#include <AK/HashMap.h>
#include <AK/OwnPtr.h>
#include <AK/String.h>
#include <AK/URL.h>
#include <LibGfx/Forward.h>
#include <LibGfx/Rect.h>
#include <LibGfx/StandardCursor.h>
#include <LibWebView/ViewImplementation.h>
#include <LibWeb/Forward.h>
#include <QAbstractScrollArea>
#include <QPointer>
class QTextEdit;
class QLineEdit;
namespace WebView {
class WebContentClient;
}
using WebView::WebContentClient;
enum class ColorScheme {
Auto,
Light,
Dark,
};
class Tab;
class WebContentView final
: public QAbstractScrollArea
, public WebView::ViewImplementation {
Q_OBJECT
public:
WebContentView();
virtual ~WebContentView() override;
void load(AK::URL const&);
void load_html(StringView html, AK::URL const&);
void reload();
Function<void(Gfx::IntPoint const& screen_position)> on_context_menu_request;
Function<void(const AK::URL&, String const& target, unsigned modifiers)> on_link_click;
Function<void(const AK::URL&, Gfx::IntPoint const& screen_position)> on_link_context_menu_request;
Function<void(const AK::URL&, Gfx::IntPoint const& screen_position, Gfx::ShareableBitmap const&)> on_image_context_menu_request;
Function<void(const AK::URL&, String const& target, unsigned modifiers)> on_link_middle_click;
Function<void(const AK::URL&)> on_link_hover;
Function<void(String const&)> on_title_change;
Function<void(const AK::URL&)> on_load_start;
Function<void(const AK::URL&)> on_load_finish;
Function<void(Gfx::Bitmap const&)> on_favicon_change;
Function<void(const AK::URL&)> on_url_drop;
Function<void(Web::DOM::Document*)> on_set_document;
Function<void(const AK::URL&, String const&)> on_get_source;
Function<void(String const&)> on_get_dom_tree;
Function<void(i32 node_id, String const& specified_style, String const& computed_style, String const& custom_properties, String const& node_box_sizing)> on_get_dom_node_properties;
Function<void(i32 message_id)> on_js_console_new_message;
Function<void(i32 start_index, Vector<String> const& message_types, Vector<String> const& messages)> on_get_js_console_messages;
Function<String(const AK::URL& url, Web::Cookie::Source source)> on_get_cookie;
Function<void(const AK::URL& url, Web::Cookie::ParsedCookie const& cookie, Web::Cookie::Source source)> on_set_cookie;
Function<void(i32 count_waiting)> on_resource_status_change;
virtual void paintEvent(QPaintEvent*) override;
virtual void resizeEvent(QResizeEvent*) override;
virtual void mouseMoveEvent(QMouseEvent*) override;
virtual void mousePressEvent(QMouseEvent*) override;
virtual void mouseReleaseEvent(QMouseEvent*) override;
virtual void keyPressEvent(QKeyEvent* event) override;
virtual void keyReleaseEvent(QKeyEvent* event) override;
virtual void showEvent(QShowEvent*) override;
virtual void hideEvent(QHideEvent*) override;
void debug_request(String const& request, String const& argument);
void get_source();
void run_javascript(String const& js_source);
void did_output_js_console_message(i32 message_index);
void did_get_js_console_messages(i32 start_index, Vector<String> message_types, Vector<String> messages);
void show_js_console();
void show_inspector();
Gfx::IntPoint to_content(Gfx::IntPoint) const;
Gfx::IntPoint to_widget(Gfx::IntPoint) const;
void set_color_scheme(ColorScheme);
virtual void notify_server_did_layout(Badge<WebContentClient>, Gfx::IntSize const& content_size) override;
virtual void notify_server_did_paint(Badge<WebContentClient>, i32 bitmap_id) override;
virtual void notify_server_did_invalidate_content_rect(Badge<WebContentClient>, Gfx::IntRect const&) override;
virtual void notify_server_did_change_selection(Badge<WebContentClient>) override;
virtual void notify_server_did_request_cursor_change(Badge<WebContentClient>, Gfx::StandardCursor cursor) override;
virtual void notify_server_did_change_title(Badge<WebContentClient>, String const&) override;
virtual void notify_server_did_request_scroll(Badge<WebContentClient>, i32, i32) override;
virtual void notify_server_did_request_scroll_to(Badge<WebContentClient>, Gfx::IntPoint const&) override;
virtual void notify_server_did_request_scroll_into_view(Badge<WebContentClient>, Gfx::IntRect const&) override;
virtual void notify_server_did_enter_tooltip_area(Badge<WebContentClient>, Gfx::IntPoint const&, String const&) override;
virtual void notify_server_did_leave_tooltip_area(Badge<WebContentClient>) override;
virtual void notify_server_did_hover_link(Badge<WebContentClient>, const AK::URL&) override;
virtual void notify_server_did_unhover_link(Badge<WebContentClient>) override;
virtual void notify_server_did_click_link(Badge<WebContentClient>, const AK::URL&, String const& target, unsigned modifiers) override;
virtual void notify_server_did_middle_click_link(Badge<WebContentClient>, const AK::URL&, String const& target, unsigned modifiers) override;
virtual void notify_server_did_start_loading(Badge<WebContentClient>, const AK::URL&) override;
virtual void notify_server_did_finish_loading(Badge<WebContentClient>, const AK::URL&) override;
virtual void notify_server_did_request_context_menu(Badge<WebContentClient>, Gfx::IntPoint const&) override;
virtual void notify_server_did_request_link_context_menu(Badge<WebContentClient>, Gfx::IntPoint const&, const AK::URL&, String const& target, unsigned modifiers) override;
virtual void notify_server_did_request_image_context_menu(Badge<WebContentClient>, Gfx::IntPoint const&, const AK::URL&, String const& target, unsigned modifiers, Gfx::ShareableBitmap const&) override;
virtual void notify_server_did_request_alert(Badge<WebContentClient>, String const& message) override;
virtual bool notify_server_did_request_confirm(Badge<WebContentClient>, String const& message) override;
virtual String notify_server_did_request_prompt(Badge<WebContentClient>, String const& message, String const& default_) override;
virtual void notify_server_did_get_source(const AK::URL& url, String const& source) override;
virtual void notify_server_did_get_dom_tree(String const& dom_tree) override;
virtual void notify_server_did_get_dom_node_properties(i32 node_id, String const& specified_style, String const& computed_style, String const& custom_properties, String const& node_box_sizing) override;
virtual void notify_server_did_output_js_console_message(i32 message_index) override;
virtual void notify_server_did_get_js_console_messages(i32 start_index, Vector<String> const& message_types, Vector<String> const& messages) override;
virtual void notify_server_did_change_favicon(Gfx::Bitmap const& favicon) override;
virtual String notify_server_did_request_cookie(Badge<WebContentClient>, const AK::URL& url, Web::Cookie::Source source) override;
virtual void notify_server_did_set_cookie(Badge<WebContentClient>, const AK::URL& url, Web::Cookie::ParsedCookie const& cookie, Web::Cookie::Source source) override;
virtual void notify_server_did_update_resource_count(i32 count_waiting) override;
virtual void notify_server_did_request_file(Badge<WebContentClient>, String const& path, i32) override;
signals:
void link_hovered(QString, int timeout = 0);
void link_unhovered();
void load_started(const URL&);
void title_changed(QString);
void favicon_changed(QIcon);
void got_source(URL, QString);
private:
void request_repaint();
void update_viewport_rect();
void handle_resize();
void ensure_js_console_widget();
void ensure_inspector_widget();
qreal m_inverse_pixel_scaling_ratio { 1.0 };
bool m_should_show_line_box_borders { false };
QPointer<QWidget> m_js_console_widget;
QPointer<QWidget> m_inspector_widget;
QTextEdit* m_js_console_output_edit { nullptr };
QLineEdit* m_js_console_input_edit { nullptr };
Gfx::IntRect m_viewport_rect;
void create_client();
WebContentClient& client();
void handle_web_content_process_crash();
AK::URL m_url;
struct SharedBitmap {
i32 id { -1 };
i32 pending_paints { 0 };
RefPtr<Gfx::Bitmap> bitmap;
};
struct ClientState {
RefPtr<WebContentClient> client;
SharedBitmap front_bitmap;
SharedBitmap back_bitmap;
i32 next_bitmap_id { 0 };
bool has_usable_bitmap { false };
bool got_repaint_requests_while_painting { false };
} m_client_state;
RefPtr<Gfx::Bitmap> m_backup_bitmap;
};

View file

@ -6,21 +6,35 @@
#include "BrowserWindow.h"
#include "Settings.h"
#include "SimpleWebView.h"
#include "Utilities.h"
#include "WebContentView.h"
#include <AK/LexicalPath.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/EventLoop.h>
#include <LibCore/File.h>
#include <LibCore/Timer.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibMain/Main.h>
#include <QApplication>
#include <QTimer>
#include <QWidget>
extern void initialize_web_engine();
Browser::Settings* s_settings;
extern String s_serenity_resource_root;
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
QApplication app(arguments.argc, arguments.argv);
platform_init();
initialize_web_engine();
// NOTE: We only instantiate this to ensure that Gfx::FontDatabase has its default queries initialized.
Gfx::FontDatabase::set_default_font_query("Katica 10 400 0");
Gfx::FontDatabase::set_fixed_width_font_query("Csilla 10 400 0");
// NOTE: This is only used for the Core::Socket inside the IPC connections.
// FIXME: Refactor things so we can get rid of this somehow.
Core::EventLoop event_loop;
QApplication app(arguments.argc, arguments.argv);
String url;
Core::ArgsParser args_parser;