diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index ac10ad7245b..bae5ccb6963 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -32,6 +32,7 @@ class CanvasRenderingContext2D; class Document; class Element; class Event; +class EventHandler; class EventListener; class EventTarget; class Frame; diff --git a/Libraries/LibWeb/Frame/EventHandler.cpp b/Libraries/LibWeb/Frame/EventHandler.cpp new file mode 100644 index 00000000000..c4d69ce7efc --- /dev/null +++ b/Libraries/LibWeb/Frame/EventHandler.cpp @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2020, Andreas Kling + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web { + +static Gfx::Point compute_mouse_event_offset(const Gfx::Point& position, const LayoutNode& layout_node) +{ + auto top_left_of_layout_node = layout_node.box_type_agnostic_position(); + return { + position.x() - static_cast(top_left_of_layout_node.x()), + position.y() - static_cast(top_left_of_layout_node.y()) + }; +} + +EventHandler::EventHandler(Badge, Frame& frame) + : m_frame(frame) +{ +} + +EventHandler::~EventHandler() +{ +} + +const LayoutDocument* EventHandler::layout_root() const +{ + if (!m_frame.document()) + return nullptr; + return static_cast(m_frame.document()->layout_node()); +} + +LayoutDocument* EventHandler::layout_root() +{ + if (!m_frame.document()) + return nullptr; + return const_cast(m_frame.document()->layout_node()); +} + +bool EventHandler::handle_mouseup(const Gfx::Point& position, unsigned button, unsigned modifiers) +{ + auto* layout_root_ptr = this->layout_root(); + if (!layout_root_ptr) + return false; + auto& layout_root = *layout_root_ptr; + bool handled_event = false; + + auto result = layout_root.hit_test(position); + if (result.layout_node && result.layout_node->node()) { + RefPtr node = result.layout_node->node(); + if (is(*node)) { + if (auto* subframe = to(*node).hosted_frame()) + return subframe->event_handler().handle_mouseup(position.translated(compute_mouse_event_offset({}, *result.layout_node)), button, modifiers); + return false; + } + auto offset = compute_mouse_event_offset(position, *result.layout_node); + node->dispatch_event(MouseEvent::create("mouseup", offset.x(), offset.y())); + handled_event = true; + } + + if (button == GUI::MouseButton::Left) { + dump_selection("MouseUp"); + m_in_mouse_selection = false; + } + return handled_event; +} + +bool EventHandler::handle_mousedown(const Gfx::Point& position, unsigned button, unsigned modifiers) +{ + auto* layout_root_ptr = this->layout_root(); + if (!layout_root_ptr) + return false; + auto& layout_root = *layout_root_ptr; + auto& document = *m_frame.document(); + auto& page_view = *m_frame.page_view(); + + auto result = layout_root.hit_test(position); + if (!result.layout_node) + return false; + + RefPtr node = result.layout_node->node(); + document.set_hovered_node(node); + if (!node) + return false; + + if (is(*node)) { + if (auto* subframe = to(*node).hosted_frame()) + return subframe->event_handler().handle_mousedown(position.translated(compute_mouse_event_offset({}, *result.layout_node)), button, modifiers); + return false; + } + + auto offset = compute_mouse_event_offset(position, *result.layout_node); + node->dispatch_event(MouseEvent::create("mousedown", offset.x(), offset.y())); + if (RefPtr link = node->enclosing_link_element()) { + dbg() << "Web::EventHandler: Clicking on a link to " << link->href(); + + if (button == GUI::MouseButton::Left) { + auto href = link->href(); + if (href.starts_with("javascript:")) { + document.run_javascript(href.substring_view(11, href.length() - 11)); + } else { + page_view.notify_link_click({}, m_frame, link->href(), link->target(), modifiers); + } + } else if (button == GUI::MouseButton::Right) { + page_view.notify_link_context_menu_request({}, m_frame, position, link->href(), link->target(), modifiers); + } else if (button == GUI::MouseButton::Middle) { + page_view.notify_link_middle_click({}, m_frame, link->href(), link->target(), modifiers); + } + } else { + if (button == GUI::MouseButton::Left) { + layout_root.selection().set({ result.layout_node, result.index_in_node }, {}); + dump_selection("MouseDown"); + m_in_mouse_selection = true; + } + } + return true; +} + +bool EventHandler::handle_mousemove(const Gfx::Point& position, unsigned buttons, unsigned modifiers) +{ + auto* layout_root_ptr = this->layout_root(); + if (!layout_root_ptr) + return false; + auto& layout_root = *layout_root_ptr; + auto& document = *m_frame.document(); + auto& page_view = *m_frame.page_view(); + + bool hovered_node_changed = false; + bool is_hovering_link = false; + bool was_hovering_link = document.hovered_node() && document.hovered_node()->is_link(); + auto result = layout_root.hit_test(position); + const HTMLAnchorElement* hovered_link_element = nullptr; + if (result.layout_node) { + RefPtr node = result.layout_node->node(); + + if (node && is(*node)) { + if (auto* subframe = to(*node).hosted_frame()) + return subframe->event_handler().handle_mousemove(position.translated(compute_mouse_event_offset({}, *result.layout_node)), buttons, modifiers); + return false; + } + + hovered_node_changed = node != document.hovered_node(); + document.set_hovered_node(node); + if (node) { + hovered_link_element = node->enclosing_link_element(); + if (hovered_link_element) { +#ifdef HTML_DEBUG + dbg() << "PageView: hovering over a link to " << hovered_link_element->href(); +#endif + is_hovering_link = true; + } + auto offset = compute_mouse_event_offset(position, *result.layout_node); + node->dispatch_event(MouseEvent::create("mousemove", offset.x(), offset.y())); + } + if (m_in_mouse_selection) { + layout_root.selection().set_end({ result.layout_node, result.index_in_node }); + dump_selection("MouseMove"); + page_view.update(); + } + } + if (page_view.window()) + page_view.window()->set_override_cursor(is_hovering_link ? GUI::StandardCursor::Hand : GUI::StandardCursor::None); + if (hovered_node_changed) { + page_view.update(); + RefPtr hovered_html_element = document.hovered_node() ? document.hovered_node()->enclosing_html_element() : nullptr; + if (hovered_html_element && !hovered_html_element->title().is_null()) { + page_view.notify_tooltip_area_enter({}, m_frame, position, hovered_html_element->title()); + } else { + page_view.notify_tooltip_area_leave({}, m_frame); + } + } + if (is_hovering_link != was_hovering_link) { + page_view.notify_link_hover({}, m_frame, hovered_link_element ? document.complete_url(hovered_link_element->href()).to_string() : String()); + } + return true; +} + +void EventHandler::dump_selection(const char* event_name) const +{ + UNUSED_PARAM(event_name); +#ifdef SELECTION_DEBUG + dbg() << event_name << " selection start: " + << layout_root()->selection().start().layout_node << ":" << layout_root()->selection().start().index_in_node << ", end: " + << layout_root()->selection().end().layout_node << ":" << layout_root()->selection().end().index_in_node; +#endif +} + +} diff --git a/Libraries/LibWeb/Frame/EventHandler.h b/Libraries/LibWeb/Frame/EventHandler.h new file mode 100644 index 00000000000..21f893617c9 --- /dev/null +++ b/Libraries/LibWeb/Frame/EventHandler.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020, Andreas Kling + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include +#include +#include + +namespace Web { + +class Frame; + +class EventHandler { +public: + explicit EventHandler(Badge, Frame&); + ~EventHandler(); + + bool handle_mouseup(const Gfx::Point&, unsigned button, unsigned modifiers); + bool handle_mousedown(const Gfx::Point&, unsigned button, unsigned modifiers); + bool handle_mousemove(const Gfx::Point&, unsigned buttons, unsigned modifiers); + +private: + LayoutDocument* layout_root(); + const LayoutDocument* layout_root() const; + + void dump_selection(const char* event_name) const; + + Frame& m_frame; + + bool m_in_mouse_selection { false }; +}; + +} diff --git a/Libraries/LibWeb/Frame/Frame.cpp b/Libraries/LibWeb/Frame/Frame.cpp index 221edad879d..ec73c378544 100644 --- a/Libraries/LibWeb/Frame/Frame.cpp +++ b/Libraries/LibWeb/Frame/Frame.cpp @@ -35,6 +35,7 @@ namespace Web { Frame::Frame(Element& host_element, Frame& main_frame) : m_main_frame(main_frame) , m_loader(*this) + , m_event_handler({}, *this) , m_host_element(host_element.make_weak_ptr()) { } @@ -42,6 +43,7 @@ Frame::Frame(Element& host_element, Frame& main_frame) Frame::Frame(PageView& page_view) : m_main_frame(*this) , m_loader(*this) + , m_event_handler({}, *this) , m_page_view(page_view.make_weak_ptr()) { } diff --git a/Libraries/LibWeb/Frame/Frame.h b/Libraries/LibWeb/Frame/Frame.h index 3a84cdfd6ac..90ed25fb5ac 100644 --- a/Libraries/LibWeb/Frame/Frame.h +++ b/Libraries/LibWeb/Frame/Frame.h @@ -33,14 +33,12 @@ #include #include #include +#include #include #include namespace Web { -class Document; -class PageView; - class Frame : public TreeNode { public: static NonnullRefPtr create_subframe(Element& host_element, Frame& main_frame) { return adopt(*new Frame(host_element, main_frame)); } @@ -76,6 +74,9 @@ public: FrameLoader& loader() { return m_loader; } const FrameLoader& loader() const { return m_loader; } + EventHandler& event_handler() { return m_event_handler; } + const EventHandler& event_handler() const { return m_event_handler; } + void scroll_to_anchor(const String&); Frame& main_frame() { return m_main_frame; } @@ -91,6 +92,7 @@ private: Frame& m_main_frame; FrameLoader m_loader; + EventHandler m_event_handler; WeakPtr m_host_element; WeakPtr m_page_view; diff --git a/Libraries/LibWeb/PageView.cpp b/Libraries/LibWeb/PageView.cpp index 2271b9f2909..4ed333bf01d 100644 --- a/Libraries/LibWeb/PageView.cpp +++ b/Libraries/LibWeb/PageView.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -153,118 +154,29 @@ void PageView::paint_event(GUI::PaintEvent& event) void PageView::mousemove_event(GUI::MouseEvent& event) { - if (!layout_root()) - return GUI::ScrollableWidget::mousemove_event(event); - - bool hovered_node_changed = false; - bool is_hovering_link = false; - bool was_hovering_link = document()->hovered_node() && document()->hovered_node()->is_link(); - auto result = layout_root()->hit_test(to_content_position(event.position())); - const HTMLAnchorElement* hovered_link_element = nullptr; - if (result.layout_node) { - RefPtr node = result.layout_node->node(); - hovered_node_changed = node != document()->hovered_node(); - document()->set_hovered_node(node); - if (node) { - hovered_link_element = node->enclosing_link_element(); - if (hovered_link_element) { -#ifdef HTML_DEBUG - dbg() << "PageView: hovering over a link to " << hovered_link_element->href(); -#endif - is_hovering_link = true; - } - auto offset = compute_mouse_event_offset(event.position(), *result.layout_node); - node->dispatch_event(MouseEvent::create("mousemove", offset.x(), offset.y())); - } - if (m_in_mouse_selection) { - layout_root()->selection().set_end({ result.layout_node, result.index_in_node }); - dump_selection("MouseMove"); - update(); - } + if (main_frame().event_handler().handle_mousemove(to_content_position(event.position()), event.buttons(), event.modifiers())) { + event.accept(); + return; } - if (window()) - window()->set_override_cursor(is_hovering_link ? GUI::StandardCursor::Hand : GUI::StandardCursor::None); - if (hovered_node_changed) { - update(); - RefPtr hovered_html_element = document()->hovered_node() ? document()->hovered_node()->enclosing_html_element() : nullptr; - if (hovered_html_element && !hovered_html_element->title().is_null()) { - auto screen_position = screen_relative_rect().location().translated(event.position()); - GUI::Application::the().show_tooltip(hovered_html_element->title(), screen_position.translated(4, 4)); - } else { - GUI::Application::the().hide_tooltip(); - } - } - if (is_hovering_link != was_hovering_link) { - if (on_link_hover) { - on_link_hover(hovered_link_element ? document()->complete_url(hovered_link_element->href()).to_string() : String()); - } - } - event.accept(); + GUI::ScrollableWidget::mousemove_event(event); } void PageView::mousedown_event(GUI::MouseEvent& event) { - if (!layout_root()) - return GUI::ScrollableWidget::mousemove_event(event); - - bool hovered_node_changed = false; - auto result = layout_root()->hit_test(to_content_position(event.position())); - if (result.layout_node) { - RefPtr node = result.layout_node->node(); - hovered_node_changed = node != document()->hovered_node(); - document()->set_hovered_node(node); - if (node) { - auto offset = compute_mouse_event_offset(event.position(), *result.layout_node); - node->dispatch_event(MouseEvent::create("mousedown", offset.x(), offset.y())); - if (RefPtr link = node->enclosing_link_element()) { - dbg() << "PageView: clicking on a link to " << link->href(); - - if (event.button() == GUI::MouseButton::Left) { - if (link->href().starts_with("javascript:")) { - run_javascript_url(link->href()); - } else { - if (on_link_click) - on_link_click(link->href(), link->target(), event.modifiers()); - } - } else if (event.button() == GUI::MouseButton::Right) { - if (on_link_context_menu_request) - on_link_context_menu_request(link->href(), event.position().translated(screen_relative_rect().location())); - } else if (event.button() == GUI::MouseButton::Middle) { - if (on_link_middle_click) - on_link_middle_click(link->href()); - } - } else { - if (event.button() == GUI::MouseButton::Left) { - if (layout_root()) - layout_root()->selection().set({ result.layout_node, result.index_in_node }, {}); - dump_selection("MouseDown"); - m_in_mouse_selection = true; - } - } - } + if (main_frame().event_handler().handle_mousedown(to_content_position(event.position()), event.button(), event.modifiers())) { + event.accept(); + return; } - if (hovered_node_changed) - update(); - event.accept(); + GUI::ScrollableWidget::mousedown_event(event); } void PageView::mouseup_event(GUI::MouseEvent& event) { - if (!layout_root()) - return GUI::ScrollableWidget::mouseup_event(event); - - auto result = layout_root()->hit_test(to_content_position(event.position())); - if (result.layout_node) { - if (RefPtr node = result.layout_node->node()) { - auto offset = compute_mouse_event_offset(event.position(), *result.layout_node); - node->dispatch_event(MouseEvent::create("mouseup", offset.x(), offset.y())); - } - } - - if (event.button() == GUI::MouseButton::Left) { - dump_selection("MouseUp"); - m_in_mouse_selection = false; + if (main_frame().event_handler().handle_mouseup(to_content_position(event.position()), event.button(), event.modifiers())) { + event.accept(); + return; } + GUI::ScrollableWidget::mouseup_event(event); } void PageView::keydown_event(GUI::KeyEvent& event) @@ -383,44 +295,12 @@ void PageView::set_document(Document* document) main_frame().set_document(document); } -void PageView::dump_selection(const char* event_name) -{ - UNUSED_PARAM(event_name); -#ifdef SELECTION_DEBUG - dbg() << event_name << " selection start: " - << layout_root()->selection().start().layout_node << ":" << layout_root()->selection().start().index_in_node << ", end: " - << layout_root()->selection().end().layout_node << ":" << layout_root()->selection().end().index_in_node; -#endif -} - void PageView::did_scroll() { main_frame().set_viewport_rect(viewport_rect_in_content_coordinates()); main_frame().did_scroll({}); } -Gfx::Point PageView::compute_mouse_event_offset(const Gfx::Point& event_position, const LayoutNode& layout_node) const -{ - auto content_event_position = to_content_position(event_position); - auto top_left_of_layout_node = layout_node.box_type_agnostic_position(); - - return { - content_event_position.x() - static_cast(top_left_of_layout_node.x()), - content_event_position.y() - static_cast(top_left_of_layout_node.y()) - }; -} - -void PageView::run_javascript_url(const String& url) -{ - ASSERT(url.starts_with("javascript:")); - if (!document()) - return; - - auto source = url.substring_view(11, url.length() - 11); - dbg() << "running js from url: _" << source << "_"; - document()->run_javascript(source); -} - void PageView::drop_event(GUI::DropEvent& event) { if (event.mime_data().has_urls()) { @@ -432,4 +312,48 @@ void PageView::drop_event(GUI::DropEvent& event) ScrollableWidget::drop_event(event); } +void PageView::notify_link_click(Badge, Web::Frame&, const String& href, const String& target, unsigned modifiers) +{ + if (on_link_click) + on_link_click(href, target, modifiers); +} + +void PageView::notify_link_middle_click(Badge, Web::Frame&, const String& href, const String&, unsigned) +{ + if (on_link_middle_click) + on_link_middle_click(href); +} + +Gfx::Point PageView::to_screen_position(const Web::Frame& frame, const Gfx::Point& frame_position) const +{ + Gfx::Point offset; + for (auto* f = &frame; f; f = f->parent()) { + auto f_position = f->host_element()->layout_node()->box_type_agnostic_position().to_int_point(); + offset.move_by(f_position); + } + return screen_relative_rect().location().translated(offset).translated(frame_position); +} + +void PageView::notify_link_context_menu_request(Badge, Web::Frame& frame, const Gfx::Point& content_position, const String& href, const String&, unsigned) +{ + if (on_link_context_menu_request) + on_link_context_menu_request(href, to_screen_position(frame, content_position)); +} + +void PageView::notify_link_hover(Badge, Web::Frame&, const String& href) +{ + if (on_link_hover) + on_link_hover(href); +} + +void PageView::notify_tooltip_area_enter(Badge, Web::Frame& frame, const Gfx::Point& content_position, const String& title) +{ + GUI::Application::the().show_tooltip(title, to_screen_position(frame, content_position)); +} + +void PageView::notify_tooltip_area_leave(Badge, Web::Frame&) +{ + GUI::Application::the().hide_tooltip(); +} + } diff --git a/Libraries/LibWeb/PageView.h b/Libraries/LibWeb/PageView.h index d2fa30fdb2b..0bd808c9880 100644 --- a/Libraries/LibWeb/PageView.h +++ b/Libraries/LibWeb/PageView.h @@ -29,13 +29,13 @@ #include #include #include +#include namespace Web { -class Frame; - class PageView : public GUI::ScrollableWidget { - C_OBJECT(PageView) + C_OBJECT(PageView); + public: virtual ~PageView() override; @@ -75,6 +75,13 @@ public: virtual bool accepts_focus() const override { return true; } + void notify_link_click(Badge, Web::Frame&, const String& href, const String& target, unsigned modifiers); + void notify_link_middle_click(Badge, Web::Frame&, const String& href, const String& target, unsigned modifiers); + void notify_link_context_menu_request(Badge, Web::Frame&, const Gfx::Point& content_position, const String& href, const String& target, unsigned modifiers); + void notify_link_hover(Badge, Web::Frame&, const String& href); + void notify_tooltip_area_enter(Badge, Web::Frame&, const Gfx::Point& content_position, const String& title); + void notify_tooltip_area_leave(Badge, Web::Frame&); + protected: PageView(); @@ -89,17 +96,13 @@ protected: private: virtual void did_scroll() override; - RefPtr create_document_from_mime_type(const ByteBuffer& data, const URL& url, const String& mime_type, const String& encoding); + Gfx::Point to_screen_position(const Web::Frame&, const Gfx::Point&) const; - void run_javascript_url(const String& url); void layout_and_sync_size(); - void dump_selection(const char* event_name); - Gfx::Point compute_mouse_event_offset(const Gfx::Point&, const LayoutNode&) const; RefPtr m_main_frame; bool m_should_show_line_box_borders { false }; - bool m_in_mouse_selection { false }; }; }