Serenity Operating System
at master 824 lines 38 kB view raw
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}