Serenity Operating System
1/*
2 * Copyright (c) 2020-2021, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2021, Max Wipfli <mail@maxwipfli.ch>
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <LibGUI/Event.h>
9#include <LibWeb/DOM/Range.h>
10#include <LibWeb/DOM/Text.h>
11#include <LibWeb/HTML/BrowsingContext.h>
12#include <LibWeb/HTML/Focus.h>
13#include <LibWeb/HTML/HTMLAnchorElement.h>
14#include <LibWeb/HTML/HTMLIFrameElement.h>
15#include <LibWeb/HTML/HTMLImageElement.h>
16#include <LibWeb/Layout/Viewport.h>
17#include <LibWeb/Page/EventHandler.h>
18#include <LibWeb/Page/Page.h>
19#include <LibWeb/Painting/PaintableBox.h>
20#include <LibWeb/UIEvents/EventNames.h>
21#include <LibWeb/UIEvents/KeyboardEvent.h>
22#include <LibWeb/UIEvents/MouseEvent.h>
23#include <LibWeb/UIEvents/WheelEvent.h>
24
25namespace Web {
26
27static JS::GCPtr<DOM::Node> dom_node_for_event_dispatch(Painting::Paintable& paintable)
28{
29 if (auto node = paintable.mouse_event_target())
30 return node;
31 if (auto node = paintable.dom_node())
32 return node;
33 if (auto* layout_parent = paintable.layout_node().parent())
34 return layout_parent->dom_node();
35 return nullptr;
36}
37
38static bool parent_element_for_event_dispatch(Painting::Paintable& paintable, JS::GCPtr<DOM::Node>& node, Layout::Node*& layout_node)
39{
40 layout_node = &paintable.layout_node();
41 while (layout_node && node && !node->is_element() && layout_node->parent()) {
42 layout_node = layout_node->parent();
43 if (layout_node->is_anonymous())
44 continue;
45 node = layout_node->dom_node();
46 }
47 return node && layout_node;
48}
49
50static Gfx::StandardCursor cursor_css_to_gfx(Optional<CSS::Cursor> cursor)
51{
52 if (!cursor.has_value()) {
53 return Gfx::StandardCursor::None;
54 }
55 switch (cursor.value()) {
56 case CSS::Cursor::Crosshair:
57 case CSS::Cursor::Cell:
58 return Gfx::StandardCursor::Crosshair;
59 case CSS::Cursor::Grab:
60 case CSS::Cursor::Grabbing:
61 return Gfx::StandardCursor::Drag;
62 case CSS::Cursor::Pointer:
63 return Gfx::StandardCursor::Hand;
64 case CSS::Cursor::Help:
65 return Gfx::StandardCursor::Help;
66 case CSS::Cursor::None:
67 return Gfx::StandardCursor::Hidden;
68 case CSS::Cursor::Text:
69 case CSS::Cursor::VerticalText:
70 return Gfx::StandardCursor::IBeam;
71 case CSS::Cursor::Move:
72 case CSS::Cursor::AllScroll:
73 return Gfx::StandardCursor::Move;
74 case CSS::Cursor::Progress:
75 case CSS::Cursor::Wait:
76 return Gfx::StandardCursor::Wait;
77 case CSS::Cursor::ColResize:
78 return Gfx::StandardCursor::ResizeColumn;
79 case CSS::Cursor::EResize:
80 case CSS::Cursor::WResize:
81 case CSS::Cursor::EwResize:
82 return Gfx::StandardCursor::ResizeHorizontal;
83 case CSS::Cursor::RowResize:
84 return Gfx::StandardCursor::ResizeRow;
85 case CSS::Cursor::NResize:
86 case CSS::Cursor::SResize:
87 case CSS::Cursor::NsResize:
88 return Gfx::StandardCursor::ResizeVertical;
89 case CSS::Cursor::NeResize:
90 case CSS::Cursor::SwResize:
91 case CSS::Cursor::NeswResize:
92 return Gfx::StandardCursor::ResizeDiagonalBLTR;
93 case CSS::Cursor::NwResize:
94 case CSS::Cursor::SeResize:
95 case CSS::Cursor::NwseResize:
96 return Gfx::StandardCursor::ResizeDiagonalTLBR;
97 case CSS::Cursor::ZoomIn:
98 case CSS::Cursor::ZoomOut:
99 return Gfx::StandardCursor::Zoom;
100 default:
101 return Gfx::StandardCursor::None;
102 }
103}
104
105static CSSPixelPoint compute_mouse_event_offset(CSSPixelPoint position, Layout::Node const& layout_node)
106{
107 auto top_left_of_layout_node = layout_node.box_type_agnostic_position();
108 return {
109 position.x() - top_left_of_layout_node.x(),
110 position.y() - top_left_of_layout_node.y()
111 };
112}
113
114EventHandler::EventHandler(Badge<HTML::BrowsingContext>, HTML::BrowsingContext& browsing_context)
115 : m_browsing_context(browsing_context)
116 , m_edit_event_handler(make<EditEventHandler>(browsing_context))
117{
118}
119
120EventHandler::~EventHandler() = default;
121
122Layout::Viewport const* EventHandler::layout_root() const
123{
124 if (!m_browsing_context.active_document())
125 return nullptr;
126 return m_browsing_context.active_document()->layout_node();
127}
128
129Layout::Viewport* EventHandler::layout_root()
130{
131 if (!m_browsing_context.active_document())
132 return nullptr;
133 return m_browsing_context.active_document()->layout_node();
134}
135
136Painting::PaintableBox* EventHandler::paint_root()
137{
138 if (!m_browsing_context.active_document())
139 return nullptr;
140 return const_cast<Painting::PaintableBox*>(m_browsing_context.active_document()->paint_box());
141}
142
143Painting::PaintableBox const* EventHandler::paint_root() const
144{
145 if (!m_browsing_context.active_document())
146 return nullptr;
147 return const_cast<Painting::PaintableBox*>(m_browsing_context.active_document()->paint_box());
148}
149
150bool EventHandler::handle_mousewheel(CSSPixelPoint position, unsigned button, unsigned buttons, unsigned int modifiers, int wheel_delta_x, int wheel_delta_y)
151{
152 if (m_browsing_context.active_document())
153 m_browsing_context.active_document()->update_layout();
154
155 if (!paint_root())
156 return false;
157
158 if (modifiers & KeyModifier::Mod_Shift)
159 swap(wheel_delta_x, wheel_delta_y);
160
161 bool handled_event = false;
162
163 JS::GCPtr<Painting::Paintable> paintable;
164 if (m_mouse_event_tracking_layout_node) {
165 paintable = m_mouse_event_tracking_layout_node->paintable();
166 } else {
167 if (auto result = paint_root()->hit_test(position, Painting::HitTestType::Exact); result.has_value())
168 paintable = result->paintable;
169 }
170
171 if (paintable) {
172 paintable->handle_mousewheel({}, position, buttons, modifiers, wheel_delta_x, wheel_delta_y);
173
174 auto node = dom_node_for_event_dispatch(*paintable);
175
176 if (node) {
177 // FIXME: Support wheel events in nested browsing contexts.
178 if (is<HTML::HTMLIFrameElement>(*node)) {
179 return false;
180 }
181
182 // Search for the first parent of the hit target that's an element.
183 Layout::Node* layout_node;
184 if (!parent_element_for_event_dispatch(*paintable, node, layout_node))
185 return false;
186
187 auto offset = compute_mouse_event_offset(position, *layout_node);
188 if (node->dispatch_event(UIEvents::WheelEvent::create_from_platform_event(node->realm(), UIEvents::EventNames::wheel, offset.x(), offset.y(), position.x(), position.y(), wheel_delta_x, wheel_delta_y, buttons, button).release_value_but_fixme_should_propagate_errors())) {
189 if (auto* page = m_browsing_context.page()) {
190 page->client().page_did_request_scroll(wheel_delta_x * 20, wheel_delta_y * 20);
191 }
192 }
193
194 handled_event = true;
195 }
196 }
197
198 return handled_event;
199}
200
201bool EventHandler::handle_mouseup(CSSPixelPoint position, unsigned button, unsigned buttons, unsigned modifiers)
202{
203 if (m_browsing_context.active_document())
204 m_browsing_context.active_document()->update_layout();
205
206 if (!paint_root())
207 return false;
208
209 bool handled_event = false;
210
211 JS::GCPtr<Painting::Paintable> paintable;
212 if (m_mouse_event_tracking_layout_node) {
213 paintable = m_mouse_event_tracking_layout_node->paintable();
214 } else {
215 if (auto result = paint_root()->hit_test(position, Painting::HitTestType::Exact); result.has_value())
216 paintable = result->paintable;
217 }
218
219 if (paintable && paintable->wants_mouse_events()) {
220 if (paintable->handle_mouseup({}, position, button, modifiers) == Painting::Paintable::DispatchEventOfSameName::No)
221 return false;
222
223 // Things may have changed as a consequence of Layout::Node::handle_mouseup(). Hit test again.
224 if (!paint_root())
225 return true;
226 if (auto result = paint_root()->hit_test(position, Painting::HitTestType::Exact); result.has_value())
227 paintable = result->paintable;
228 }
229
230 if (paintable) {
231 auto node = dom_node_for_event_dispatch(*paintable);
232
233 if (node) {
234 if (is<HTML::HTMLIFrameElement>(*node)) {
235 if (auto* nested_browsing_context = static_cast<HTML::HTMLIFrameElement&>(*node).nested_browsing_context())
236 return nested_browsing_context->event_handler().handle_mouseup(position.translated(compute_mouse_event_offset({}, paintable->layout_node())), button, buttons, modifiers);
237 return false;
238 }
239
240 // Search for the first parent of the hit target that's an element.
241 // "The click event type MUST be dispatched on the topmost event target indicated by the pointer." (https://www.w3.org/TR/uievents/#event-type-click)
242 // "The topmost event target MUST be the element highest in the rendering order which is capable of being an event target." (https://www.w3.org/TR/uievents/#topmost-event-target)
243 Layout::Node* layout_node;
244 if (!parent_element_for_event_dispatch(*paintable, node, layout_node)) {
245 // FIXME: This is pretty ugly but we need to bail out here.
246 goto after_node_use;
247 }
248
249 auto offset = compute_mouse_event_offset(position, *layout_node);
250 auto client_offset = compute_mouse_event_client_offset(position);
251 auto page_offset = compute_mouse_event_page_offset(client_offset);
252 node->dispatch_event(UIEvents::MouseEvent::create_from_platform_event(node->realm(), UIEvents::EventNames::mouseup, offset, client_offset, page_offset, buttons, button).release_value_but_fixme_should_propagate_errors());
253 handled_event = true;
254
255 bool run_activation_behavior = true;
256 if (node.ptr() == m_mousedown_target && button == GUI::MouseButton::Primary) {
257 run_activation_behavior = node->dispatch_event(UIEvents::MouseEvent::create_from_platform_event(node->realm(), UIEvents::EventNames::click, offset, client_offset, page_offset, button).release_value_but_fixme_should_propagate_errors());
258 }
259
260 if (run_activation_behavior) {
261 // FIXME: This is ad-hoc and incorrect. The reason this exists is
262 // because we are missing browsing context navigation:
263 //
264 // https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate
265 //
266 // Additionally, we currently cannot spawn a new top-level
267 // browsing context for new tab operations, because the new
268 // top-level browsing context would be in another process. To
269 // fix this, there needs to be some way to be able to
270 // communicate with browsing contexts in remote WebContent
271 // processes, and then step 8 of this algorithm needs to be
272 // implemented in BrowsingContext::choose_a_browsing_context:
273 //
274 // https://html.spec.whatwg.org/multipage/browsers.html#the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name
275 if (JS::GCPtr<HTML::HTMLAnchorElement const> link = node->enclosing_link_element()) {
276 JS::NonnullGCPtr<DOM::Document> document = *m_browsing_context.active_document();
277 auto href = link->href();
278 auto url = document->parse_url(href);
279 dbgln("Web::EventHandler: Clicking on a link to {}", url);
280 if (button == GUI::MouseButton::Primary) {
281 if (href.starts_with("javascript:"sv)) {
282 document->run_javascript(href.substring_view(11, href.length() - 11));
283 } else if (!url.fragment().is_null() && url.equals(document->url(), AK::URL::ExcludeFragment::Yes)) {
284 m_browsing_context.scroll_to_anchor(url.fragment());
285 } else {
286 if (m_browsing_context.is_top_level()) {
287 if (auto* page = m_browsing_context.page())
288 page->client().page_did_click_link(url, link->target(), modifiers);
289 }
290 }
291 } else if (button == GUI::MouseButton::Middle) {
292 if (auto* page = m_browsing_context.page())
293 page->client().page_did_middle_click_link(url, link->target(), modifiers);
294 } else if (button == GUI::MouseButton::Secondary) {
295 if (auto* page = m_browsing_context.page())
296 page->client().page_did_request_link_context_menu(m_browsing_context.to_top_level_position(position), url, link->target(), modifiers);
297 }
298 } else if (button == GUI::MouseButton::Secondary) {
299 if (is<HTML::HTMLImageElement>(*node)) {
300 auto& image_element = verify_cast<HTML::HTMLImageElement>(*node);
301 auto image_url = image_element.document().parse_url(image_element.src());
302 if (auto* page = m_browsing_context.page())
303 page->client().page_did_request_image_context_menu(m_browsing_context.to_top_level_position(position), image_url, "", modifiers, image_element.bitmap());
304 } else if (auto* page = m_browsing_context.page()) {
305 page->client().page_did_request_context_menu(m_browsing_context.to_top_level_position(position));
306 }
307 }
308 }
309 }
310 }
311
312after_node_use:
313 if (button == GUI::MouseButton::Primary)
314 m_in_mouse_selection = false;
315 return handled_event;
316}
317
318bool EventHandler::handle_mousedown(CSSPixelPoint position, unsigned button, unsigned buttons, unsigned modifiers)
319{
320 if (m_browsing_context.active_document())
321 m_browsing_context.active_document()->update_layout();
322
323 if (!paint_root())
324 return false;
325
326 JS::NonnullGCPtr<DOM::Document> document = *m_browsing_context.active_document();
327 JS::GCPtr<DOM::Node> node;
328
329 {
330 JS::GCPtr<Painting::Paintable> paintable;
331 if (m_mouse_event_tracking_layout_node) {
332 paintable = m_mouse_event_tracking_layout_node->paintable();
333 } else {
334 auto result = paint_root()->hit_test(position, Painting::HitTestType::Exact);
335 if (!result.has_value())
336 return false;
337 paintable = result->paintable;
338 }
339
340 auto pointer_events = paintable->computed_values().pointer_events();
341 // FIXME: Handle other values for pointer-events.
342 VERIFY(pointer_events != CSS::PointerEvents::None);
343
344 node = dom_node_for_event_dispatch(*paintable);
345 document->set_hovered_node(node);
346
347 if (paintable->wants_mouse_events()) {
348 if (paintable->handle_mousedown({}, position, button, modifiers) == Painting::Paintable::DispatchEventOfSameName::No)
349 return false;
350 }
351
352 if (!node)
353 return false;
354
355 if (is<HTML::HTMLIFrameElement>(*node)) {
356 if (auto* nested_browsing_context = static_cast<HTML::HTMLIFrameElement&>(*node).nested_browsing_context())
357 return nested_browsing_context->event_handler().handle_mousedown(position.translated(compute_mouse_event_offset({}, paintable->layout_node())), button, buttons, modifiers);
358 return false;
359 }
360
361 if (auto* page = m_browsing_context.page())
362 page->set_focused_browsing_context({}, m_browsing_context);
363
364 // Search for the first parent of the hit target that's an element.
365 // "The click event type MUST be dispatched on the topmost event target indicated by the pointer." (https://www.w3.org/TR/uievents/#event-type-click)
366 // "The topmost event target MUST be the element highest in the rendering order which is capable of being an event target." (https://www.w3.org/TR/uievents/#topmost-event-target)
367 Layout::Node* layout_node;
368 if (!parent_element_for_event_dispatch(*paintable, node, layout_node))
369 return false;
370
371 m_mousedown_target = node.ptr();
372 auto offset = compute_mouse_event_offset(position, *layout_node);
373 auto client_offset = compute_mouse_event_client_offset(position);
374 auto page_offset = compute_mouse_event_page_offset(client_offset);
375 node->dispatch_event(UIEvents::MouseEvent::create_from_platform_event(node->realm(), UIEvents::EventNames::mousedown, offset, client_offset, page_offset, buttons, button).release_value_but_fixme_should_propagate_errors());
376 }
377
378 // NOTE: Dispatching an event may have disturbed the world.
379 if (!paint_root() || paint_root() != node->document().paint_box())
380 return true;
381
382 if (button == GUI::MouseButton::Primary) {
383 if (auto result = paint_root()->hit_test(position, Painting::HitTestType::TextCursor); result.has_value()) {
384 auto paintable = result->paintable;
385 if (paintable->dom_node()) {
386 // See if we want to focus something.
387 bool did_focus_something = false;
388 for (auto candidate = node; candidate; candidate = candidate->parent()) {
389 if (candidate->is_focusable()) {
390 // When a user activates a click focusable focusable area, the user agent must run the focusing steps on the focusable area with focus trigger set to "click".
391 // Spec Note: Note that focusing is not an activation behavior, i.e. calling the click() method on an element or dispatching a synthetic click event on it won't cause the element to get focused.
392 HTML::run_focusing_steps(candidate.ptr(), nullptr, "click"sv);
393 did_focus_something = true;
394 break;
395 }
396 }
397
398 // If we didn't focus anything, place the document text cursor at the mouse position.
399 // FIXME: This is all rather strange. Find a better solution.
400 if (!did_focus_something) {
401 m_browsing_context.set_cursor_position(DOM::Position(*paintable->dom_node(), result->index_in_node));
402 if (auto selection = document->get_selection()) {
403 (void)selection->set_base_and_extent(*paintable->dom_node(), result->index_in_node, *paintable->dom_node(), result->index_in_node);
404 }
405 m_in_mouse_selection = true;
406 }
407 }
408 }
409 }
410 return true;
411}
412
413bool EventHandler::handle_mousemove(CSSPixelPoint position, unsigned buttons, unsigned modifiers)
414{
415 if (m_browsing_context.active_document())
416 m_browsing_context.active_document()->update_layout();
417
418 if (!paint_root())
419 return false;
420
421 auto& document = *m_browsing_context.active_document();
422
423 bool hovered_node_changed = false;
424 bool is_hovering_link = false;
425 Gfx::StandardCursor hovered_node_cursor = Gfx::StandardCursor::None;
426
427 JS::GCPtr<Painting::Paintable> paintable;
428 Optional<int> start_index;
429 if (m_mouse_event_tracking_layout_node) {
430 paintable = m_mouse_event_tracking_layout_node->paintable();
431 } else {
432 if (auto result = paint_root()->hit_test(position, Painting::HitTestType::Exact); result.has_value()) {
433 paintable = result->paintable;
434 start_index = result->index_in_node;
435 }
436 }
437
438 const HTML::HTMLAnchorElement* hovered_link_element = nullptr;
439 if (paintable) {
440 if (paintable->wants_mouse_events()) {
441 document.set_hovered_node(paintable->dom_node());
442 if (paintable->handle_mousemove({}, position, buttons, modifiers) == Painting::Paintable::DispatchEventOfSameName::No)
443 return false;
444
445 // FIXME: It feels a bit aggressive to always update the cursor like this.
446 if (auto* page = m_browsing_context.page())
447 page->client().page_did_request_cursor_change(Gfx::StandardCursor::None);
448 }
449
450 auto node = dom_node_for_event_dispatch(*paintable);
451
452 if (node && is<HTML::HTMLIFrameElement>(*node)) {
453 if (auto* nested_browsing_context = static_cast<HTML::HTMLIFrameElement&>(*node).nested_browsing_context())
454 return nested_browsing_context->event_handler().handle_mousemove(position.translated(compute_mouse_event_offset({}, paintable->layout_node())), buttons, modifiers);
455 return false;
456 }
457
458 auto pointer_events = paintable->computed_values().pointer_events();
459 // FIXME: Handle other values for pointer-events.
460 VERIFY(pointer_events != CSS::PointerEvents::None);
461
462 // Search for the first parent of the hit target that's an element.
463 // "The click event type MUST be dispatched on the topmost event target indicated by the pointer." (https://www.w3.org/TR/uievents/#event-type-click)
464 // "The topmost event target MUST be the element highest in the rendering order which is capable of being an event target." (https://www.w3.org/TR/uievents/#topmost-event-target)
465 Layout::Node* layout_node;
466 bool found_parent_element = parent_element_for_event_dispatch(*paintable, node, layout_node);
467 hovered_node_changed = node.ptr() != document.hovered_node();
468 document.set_hovered_node(node);
469 if (found_parent_element) {
470 hovered_link_element = node->enclosing_link_element();
471 if (hovered_link_element)
472 is_hovering_link = true;
473
474 if (node->is_text()) {
475 auto cursor = paintable->computed_values().cursor();
476 if (cursor == CSS::Cursor::Auto)
477 hovered_node_cursor = Gfx::StandardCursor::IBeam;
478 else
479 hovered_node_cursor = cursor_css_to_gfx(cursor);
480 } else if (node->is_element()) {
481 auto cursor = paintable->computed_values().cursor();
482 if (cursor == CSS::Cursor::Auto)
483 hovered_node_cursor = Gfx::StandardCursor::Arrow;
484 else
485 hovered_node_cursor = cursor_css_to_gfx(cursor);
486 }
487
488 auto offset = compute_mouse_event_offset(position, *layout_node);
489 auto client_offset = compute_mouse_event_client_offset(position);
490 auto page_offset = compute_mouse_event_page_offset(client_offset);
491 node->dispatch_event(UIEvents::MouseEvent::create_from_platform_event(node->realm(), UIEvents::EventNames::mousemove, offset, client_offset, page_offset, buttons).release_value_but_fixme_should_propagate_errors());
492 // NOTE: Dispatching an event may have disturbed the world.
493 if (!paint_root() || paint_root() != node->document().paint_box())
494 return true;
495 }
496 if (m_in_mouse_selection) {
497 auto hit = paint_root()->hit_test(position, Painting::HitTestType::TextCursor);
498 if (start_index.has_value() && hit.has_value() && hit->dom_node()) {
499 m_browsing_context.set_cursor_position(DOM::Position(*hit->dom_node(), *start_index));
500 if (auto selection = document.get_selection()) {
501 auto anchor_node = selection->anchor_node();
502 if (anchor_node)
503 (void)selection->set_base_and_extent(*anchor_node, selection->anchor_offset(), *hit->paintable->dom_node(), hit->index_in_node);
504 else
505 (void)selection->set_base_and_extent(*hit->paintable->dom_node(), hit->index_in_node, *hit->paintable->dom_node(), hit->index_in_node);
506 }
507 m_browsing_context.set_needs_display();
508 }
509 if (auto* page = m_browsing_context.page())
510 page->client().page_did_change_selection();
511 }
512 }
513
514 if (auto* page = m_browsing_context.page()) {
515 page->client().page_did_request_cursor_change(hovered_node_cursor);
516
517 if (hovered_node_changed) {
518 JS::GCPtr<HTML::HTMLElement const> hovered_html_element = document.hovered_node() ? document.hovered_node()->enclosing_html_element_with_attribute(HTML::AttributeNames::title) : nullptr;
519 if (hovered_html_element && !hovered_html_element->title().is_null()) {
520 page->client().page_did_enter_tooltip_area(m_browsing_context.to_top_level_position(position), hovered_html_element->title());
521 } else {
522 page->client().page_did_leave_tooltip_area();
523 }
524 if (is_hovering_link)
525 page->client().page_did_hover_link(document.parse_url(hovered_link_element->href()));
526 else
527 page->client().page_did_unhover_link();
528 }
529 }
530 return true;
531}
532
533bool EventHandler::handle_doubleclick(CSSPixelPoint position, unsigned button, unsigned buttons, unsigned modifiers)
534{
535 if (m_browsing_context.active_document())
536 m_browsing_context.active_document()->update_layout();
537
538 if (!paint_root())
539 return false;
540
541 JS::GCPtr<Painting::Paintable> paintable;
542 if (m_mouse_event_tracking_layout_node) {
543 paintable = m_mouse_event_tracking_layout_node->paintable();
544 } else {
545 auto result = paint_root()->hit_test(position, Painting::HitTestType::Exact);
546 if (!result.has_value())
547 return false;
548 paintable = result->paintable;
549 }
550
551 auto pointer_events = paintable->computed_values().pointer_events();
552 // FIXME: Handle other values for pointer-events.
553 if (pointer_events == CSS::PointerEvents::None)
554 return false;
555
556 auto node = dom_node_for_event_dispatch(*paintable);
557
558 if (paintable->wants_mouse_events()) {
559 // FIXME: Handle double clicks.
560 }
561
562 if (!node)
563 return false;
564
565 if (is<HTML::HTMLIFrameElement>(*node)) {
566 if (auto* nested_browsing_context = static_cast<HTML::HTMLIFrameElement&>(*node).nested_browsing_context())
567 return nested_browsing_context->event_handler().handle_doubleclick(position.translated(compute_mouse_event_offset({}, paintable->layout_node())), button, buttons, modifiers);
568 return false;
569 }
570
571 // Search for the first parent of the hit target that's an element.
572 // "The topmost event target MUST be the element highest in the rendering order which is capable of being an event target." (https://www.w3.org/TR/uievents/#topmost-event-target)
573 Layout::Node* layout_node;
574 if (!parent_element_for_event_dispatch(*paintable, node, layout_node))
575 return false;
576
577 auto offset = compute_mouse_event_offset(position, *layout_node);
578 auto client_offset = compute_mouse_event_client_offset(position);
579 auto page_offset = compute_mouse_event_page_offset(client_offset);
580 node->dispatch_event(UIEvents::MouseEvent::create_from_platform_event(node->realm(), UIEvents::EventNames::dblclick, offset, client_offset, page_offset, buttons, button).release_value_but_fixme_should_propagate_errors());
581
582 // NOTE: Dispatching an event may have disturbed the world.
583 if (!paint_root() || paint_root() != node->document().paint_box())
584 return true;
585
586 if (button == GUI::MouseButton::Primary) {
587 if (auto result = paint_root()->hit_test(position, Painting::HitTestType::TextCursor); result.has_value()) {
588 auto hit_paintable = result->paintable;
589 if (!hit_paintable->dom_node())
590 return true;
591
592 auto const& hit_layout_node = hit_paintable->layout_node();
593 if (!hit_layout_node.is_text_node())
594 return true;
595 auto const& text_for_rendering = verify_cast<Layout::TextNode>(hit_layout_node).text_for_rendering();
596
597 int first_word_break_before = [&] {
598 // Start from one before the index position to prevent selecting only spaces between words, caused by the addition below.
599 // This also helps us dealing with cases where index is equal to the string length.
600 for (int i = result->index_in_node - 1; i >= 0; --i) {
601 if (is_ascii_space(text_for_rendering[i])) {
602 // Don't include the space in the selection
603 return i + 1;
604 }
605 }
606 return 0;
607 }();
608
609 int first_word_break_after = [&] {
610 for (size_t i = result->index_in_node; i < text_for_rendering.length(); ++i) {
611 if (is_ascii_space(text_for_rendering[i]))
612 return i;
613 }
614 return text_for_rendering.length();
615 }();
616
617 m_browsing_context.set_cursor_position(DOM::Position(*paintable->dom_node(), first_word_break_after));
618 if (auto selection = node->document().get_selection()) {
619 (void)selection->set_base_and_extent(*paintable->dom_node(), first_word_break_before, *paintable->dom_node(), first_word_break_after);
620 }
621 }
622 }
623
624 return true;
625}
626
627bool EventHandler::focus_next_element()
628{
629 if (!m_browsing_context.active_document())
630 return false;
631 auto* element = m_browsing_context.active_document()->focused_element();
632 if (!element) {
633 element = m_browsing_context.active_document()->first_child_of_type<DOM::Element>();
634 if (element && element->is_focusable()) {
635 m_browsing_context.active_document()->set_focused_element(element);
636 return true;
637 }
638 }
639
640 for (element = element->next_element_in_pre_order(); element && !element->is_focusable(); element = element->next_element_in_pre_order())
641 ;
642
643 m_browsing_context.active_document()->set_focused_element(element);
644 return element;
645}
646
647bool EventHandler::focus_previous_element()
648{
649 if (!m_browsing_context.active_document())
650 return false;
651 auto* element = m_browsing_context.active_document()->focused_element();
652 if (!element) {
653 element = m_browsing_context.active_document()->last_child_of_type<DOM::Element>();
654 if (element && element->is_focusable()) {
655 m_browsing_context.active_document()->set_focused_element(element);
656 return true;
657 }
658 }
659
660 for (element = element->previous_element_in_pre_order(); element && !element->is_focusable(); element = element->previous_element_in_pre_order())
661 ;
662
663 m_browsing_context.active_document()->set_focused_element(element);
664 return element;
665}
666
667constexpr bool should_ignore_keydown_event(u32 code_point)
668{
669 // FIXME: There are probably also keys with non-zero code points that should be filtered out.
670 // FIXME: We should take the modifier keys into consideration somehow. This treats "Ctrl+C" as just "c".
671 return code_point == 0 || code_point == 27;
672}
673
674bool EventHandler::fire_keyboard_event(DeprecatedFlyString const& event_name, HTML::BrowsingContext& browsing_context, KeyCode key, unsigned int modifiers, u32 code_point)
675{
676 JS::NonnullGCPtr<DOM::Document> document = *browsing_context.active_document();
677 if (!document)
678 return false;
679
680 if (JS::GCPtr<DOM::Element> focused_element = document->focused_element()) {
681 if (is<HTML::BrowsingContextContainer>(*focused_element)) {
682 auto& browsing_context_container = verify_cast<HTML::BrowsingContextContainer>(*focused_element);
683 if (browsing_context_container.nested_browsing_context())
684 return fire_keyboard_event(event_name, *browsing_context_container.nested_browsing_context(), key, modifiers, code_point);
685 }
686
687 auto event = UIEvents::KeyboardEvent::create_from_platform_event(document->realm(), event_name, key, modifiers, code_point).release_value_but_fixme_should_propagate_errors();
688 return !focused_element->dispatch_event(event);
689 }
690
691 // FIXME: De-duplicate this. This is just to prevent wasting a KeyboardEvent allocation when recursing into an (i)frame.
692 auto event = UIEvents::KeyboardEvent::create_from_platform_event(document->realm(), event_name, key, modifiers, code_point).release_value_but_fixme_should_propagate_errors();
693
694 if (JS::GCPtr<HTML::HTMLElement> body = document->body())
695 return !body->dispatch_event(event);
696
697 return !document->root().dispatch_event(event);
698}
699
700bool EventHandler::handle_keydown(KeyCode key, unsigned modifiers, u32 code_point)
701{
702 if (!m_browsing_context.active_document())
703 return false;
704
705 JS::NonnullGCPtr<DOM::Document> document = *m_browsing_context.active_document();
706 if (!document->layout_node())
707 return false;
708
709 if (key == KeyCode::Key_Tab) {
710 if (modifiers & KeyModifier::Mod_Shift)
711 return focus_previous_element();
712 return focus_next_element();
713 }
714
715 if (auto selection = document->get_selection()) {
716 auto range = selection->range();
717 if (range && range->start_container()->is_editable()) {
718 selection->remove_all_ranges();
719
720 // FIXME: This doesn't work for some reason?
721 m_browsing_context.set_cursor_position({ *range->start_container(), range->start_offset() });
722
723 if (key == KeyCode::Key_Backspace || key == KeyCode::Key_Delete) {
724 m_edit_event_handler->handle_delete(*range);
725 return true;
726 }
727 if (!should_ignore_keydown_event(code_point)) {
728 m_edit_event_handler->handle_delete(*range);
729 m_edit_event_handler->handle_insert(m_browsing_context.cursor_position(), code_point);
730 m_browsing_context.increment_cursor_position_offset();
731 return true;
732 }
733 }
734 }
735
736 if (m_browsing_context.cursor_position().is_valid() && m_browsing_context.cursor_position().node()->is_editable()) {
737 if (key == KeyCode::Key_Backspace) {
738 if (!m_browsing_context.decrement_cursor_position_offset()) {
739 // FIXME: Move to the previous node and delete the last character there.
740 return true;
741 }
742
743 m_edit_event_handler->handle_delete_character_after(m_browsing_context.cursor_position());
744 return true;
745 }
746 if (key == KeyCode::Key_Delete) {
747 if (m_browsing_context.cursor_position().offset_is_at_end_of_node()) {
748 // FIXME: Move to the next node and delete the first character there.
749 return true;
750 }
751 m_edit_event_handler->handle_delete_character_after(m_browsing_context.cursor_position());
752 return true;
753 }
754 if (key == KeyCode::Key_Right) {
755 if (!m_browsing_context.increment_cursor_position_offset()) {
756 // FIXME: Move to the next node.
757 }
758 return true;
759 }
760 if (key == KeyCode::Key_Left) {
761 if (!m_browsing_context.decrement_cursor_position_offset()) {
762 // FIXME: Move to the previous node.
763 }
764 return true;
765 }
766 if (key == KeyCode::Key_Home) {
767 auto& node = *static_cast<DOM::Text*>(const_cast<DOM::Node*>(m_browsing_context.cursor_position().node()));
768 m_browsing_context.set_cursor_position(DOM::Position { node, 0 });
769 return true;
770 }
771 if (key == KeyCode::Key_End) {
772 auto& node = *static_cast<DOM::Text*>(const_cast<DOM::Node*>(m_browsing_context.cursor_position().node()));
773 m_browsing_context.set_cursor_position(DOM::Position { node, (unsigned)node.data().length() });
774 return true;
775 }
776 if (!should_ignore_keydown_event(code_point)) {
777 m_edit_event_handler->handle_insert(m_browsing_context.cursor_position(), code_point);
778 m_browsing_context.increment_cursor_position_offset();
779 return true;
780 }
781
782 // NOTE: Because modifier keys should be ignored, we need to return true.
783 return true;
784 }
785
786 bool continue_ = fire_keyboard_event(UIEvents::EventNames::keydown, m_browsing_context, key, modifiers, code_point);
787 if (!continue_)
788 return false;
789
790 // FIXME: Work out and implement the difference between this and keydown.
791 return fire_keyboard_event(UIEvents::EventNames::keypress, m_browsing_context, key, modifiers, code_point);
792}
793
794bool EventHandler::handle_keyup(KeyCode key, unsigned modifiers, u32 code_point)
795{
796 return fire_keyboard_event(UIEvents::EventNames::keyup, m_browsing_context, key, modifiers, code_point);
797}
798
799void EventHandler::set_mouse_event_tracking_layout_node(Layout::Node* layout_node)
800{
801 m_mouse_event_tracking_layout_node = layout_node;
802}
803
804CSSPixelPoint EventHandler::compute_mouse_event_client_offset(CSSPixelPoint event_page_position) const
805{
806 // https://w3c.github.io/csswg-drafts/cssom-view/#dom-mouseevent-clientx
807 // The clientX attribute must return the x-coordinate of the position where the event occurred relative to the origin of the viewport.
808
809 auto scroll_offset = m_browsing_context.viewport_scroll_offset();
810 return event_page_position.translated(-scroll_offset);
811}
812
813CSSPixelPoint EventHandler::compute_mouse_event_page_offset(CSSPixelPoint event_client_offset) const
814{
815 // https://w3c.github.io/csswg-drafts/cssom-view/#dom-mouseevent-pagex
816 // FIXME: 1. If the event’s dispatch flag is set, return the horizontal coordinate of the position where the event occurred relative to the origin of the initial containing block and terminate these steps.
817
818 // 2. Let offset be the value of the scrollX attribute of the event’s associated Window object, if there is one, or zero otherwise.
819 auto scroll_offset = m_browsing_context.viewport_scroll_offset();
820
821 // 3. Return the sum of offset and the value of the event’s clientX attribute.
822 return event_client_offset.translated(scroll_offset);
823}
824}