Serenity Operating System
at hosted 496 lines 17 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, this 9 * list of conditions and the following disclaimer. 10 * 11 * 2. Redistributions in binary form must reproduce the above copyright notice, 12 * this list of conditions and the following disclaimer in the documentation 13 * and/or other materials provided with the distribution. 14 * 15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 */ 26 27#include <AK/FileSystemPath.h> 28#include <AK/URL.h> 29#include <LibCore/File.h> 30#include <LibGUI/Application.h> 31#include <LibGUI/Painter.h> 32#include <LibGUI/ScrollBar.h> 33#include <LibGUI/Window.h> 34#include <LibGfx/PNGLoader.h> 35#include <LibJS/Runtime/Value.h> 36#include <LibWeb/DOM/Element.h> 37#include <LibWeb/DOM/ElementFactory.h> 38#include <LibWeb/DOM/HTMLAnchorElement.h> 39#include <LibWeb/DOM/HTMLImageElement.h> 40#include <LibWeb/DOM/MouseEvent.h> 41#include <LibWeb/DOM/Text.h> 42#include <LibWeb/Dump.h> 43#include <LibWeb/Frame.h> 44#include <LibWeb/HtmlView.h> 45#include <LibWeb/Layout/LayoutDocument.h> 46#include <LibWeb/Layout/LayoutNode.h> 47#include <LibWeb/Parser/HTMLParser.h> 48#include <LibWeb/RenderingContext.h> 49#include <LibWeb/ResourceLoader.h> 50#include <stdio.h> 51 52//#define SELECTION_DEBUG 53 54namespace Web { 55 56HtmlView::HtmlView() 57 : m_main_frame(Web::Frame::create(*this)) 58{ 59 main_frame().on_set_needs_display = [this](auto& content_rect) { 60 if (content_rect.is_empty()) { 61 update(); 62 return; 63 } 64 Gfx::Rect adjusted_rect = content_rect; 65 adjusted_rect.set_location(to_widget_position(content_rect.location())); 66 update(adjusted_rect); 67 }; 68 69 set_should_hide_unnecessary_scrollbars(true); 70 set_background_role(ColorRole::Base); 71} 72 73HtmlView::~HtmlView() 74{ 75} 76 77void HtmlView::set_document(Document* new_document) 78{ 79 RefPtr<Document> old_document = document(); 80 81 if (new_document == old_document) 82 return; 83 84 if (old_document) 85 old_document->on_layout_updated = nullptr; 86 87 main_frame().set_document(new_document); 88 89 if (new_document) { 90 new_document->on_layout_updated = [this] { 91 layout_and_sync_size(); 92 update(); 93 }; 94 } 95 96#ifdef HTML_DEBUG 97 if (document != nullptr) { 98 dbgprintf("\033[33;1mLayout tree before layout:\033[0m\n"); 99 ::dump_tree(*layout_root()); 100 } 101#endif 102 103 layout_and_sync_size(); 104 update(); 105} 106 107void HtmlView::layout_and_sync_size() 108{ 109 if (!document()) 110 return; 111 112 bool had_vertical_scrollbar = vertical_scrollbar().is_visible(); 113 bool had_horizontal_scrollbar = horizontal_scrollbar().is_visible(); 114 115 main_frame().set_size(available_size()); 116 document()->layout(); 117 set_content_size(enclosing_int_rect(layout_root()->rect()).size()); 118 119 // NOTE: If layout caused us to gain or lose scrollbars, we have to lay out again 120 // since the scrollbars now take up some of the available space. 121 if (had_vertical_scrollbar != vertical_scrollbar().is_visible() || had_horizontal_scrollbar != horizontal_scrollbar().is_visible()) { 122 main_frame().set_size(available_size()); 123 document()->layout(); 124 set_content_size(enclosing_int_rect(layout_root()->rect()).size()); 125 } 126 127 main_frame().set_viewport_rect(visible_content_rect()); 128 129#ifdef HTML_DEBUG 130 dbgprintf("\033[33;1mLayout tree after layout:\033[0m\n"); 131 ::dump_tree(*layout_root()); 132#endif 133} 134 135void HtmlView::resize_event(GUI::ResizeEvent& event) 136{ 137 GUI::ScrollableWidget::resize_event(event); 138 layout_and_sync_size(); 139} 140 141void HtmlView::paint_event(GUI::PaintEvent& event) 142{ 143 GUI::Frame::paint_event(event); 144 145 GUI::Painter painter(*this); 146 painter.add_clip_rect(widget_inner_rect()); 147 painter.add_clip_rect(event.rect()); 148 149 if (!layout_root()) { 150 painter.fill_rect(event.rect(), palette().color(background_role())); 151 return; 152 } 153 154 painter.fill_rect(event.rect(), document()->background_color(palette())); 155 156 if (auto background_bitmap = document()->background_image()) { 157 painter.draw_tiled_bitmap(event.rect(), *background_bitmap); 158 } 159 160 painter.translate(frame_thickness(), frame_thickness()); 161 painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value()); 162 163 RenderingContext context(painter, palette()); 164 context.set_should_show_line_box_borders(m_should_show_line_box_borders); 165 context.set_viewport_rect(visible_content_rect()); 166 layout_root()->render(context); 167} 168 169void HtmlView::mousemove_event(GUI::MouseEvent& event) 170{ 171 if (!layout_root()) 172 return GUI::ScrollableWidget::mousemove_event(event); 173 174 bool hovered_node_changed = false; 175 bool is_hovering_link = false; 176 bool was_hovering_link = document()->hovered_node() && document()->hovered_node()->is_link(); 177 auto result = layout_root()->hit_test(to_content_position(event.position())); 178 const HTMLAnchorElement* hovered_link_element = nullptr; 179 if (result.layout_node) { 180 RefPtr<Node> node = result.layout_node->node(); 181 hovered_node_changed = node != document()->hovered_node(); 182 document()->set_hovered_node(node); 183 if (node) { 184 hovered_link_element = node->enclosing_link_element(); 185 if (hovered_link_element) { 186#ifdef HTML_DEBUG 187 dbg() << "HtmlView: hovering over a link to " << hovered_link_element->href(); 188#endif 189 is_hovering_link = true; 190 } 191 auto offset = compute_mouse_event_offset(event.position(), *result.layout_node); 192 node->dispatch_event(MouseEvent::create("mousemove", offset.x(), offset.y())); 193 } 194 if (m_in_mouse_selection) { 195 layout_root()->selection().set_end({ result.layout_node, result.index_in_node }); 196 dump_selection("MouseMove"); 197 update(); 198 } 199 } 200 if (window()) 201 window()->set_override_cursor(is_hovering_link ? GUI::StandardCursor::Hand : GUI::StandardCursor::None); 202 if (hovered_node_changed) { 203 update(); 204 RefPtr<HTMLElement> hovered_html_element = document()->hovered_node() ? document()->hovered_node()->enclosing_html_element() : nullptr; 205 if (hovered_html_element && !hovered_html_element->title().is_null()) { 206 auto screen_position = screen_relative_rect().location().translated(event.position()); 207 GUI::Application::the().show_tooltip(hovered_html_element->title(), screen_position.translated(4, 4)); 208 } else { 209 GUI::Application::the().hide_tooltip(); 210 } 211 } 212 if (is_hovering_link != was_hovering_link) { 213 if (on_link_hover) { 214 on_link_hover(hovered_link_element ? document()->complete_url(hovered_link_element->href()).to_string() : String()); 215 } 216 } 217 event.accept(); 218} 219 220void HtmlView::mousedown_event(GUI::MouseEvent& event) 221{ 222 if (!layout_root()) 223 return GUI::ScrollableWidget::mousemove_event(event); 224 225 bool hovered_node_changed = false; 226 auto result = layout_root()->hit_test(to_content_position(event.position())); 227 if (result.layout_node) { 228 RefPtr<Node> node = result.layout_node->node(); 229 hovered_node_changed = node != document()->hovered_node(); 230 document()->set_hovered_node(node); 231 if (node) { 232 auto offset = compute_mouse_event_offset(event.position(), *result.layout_node); 233 node->dispatch_event(MouseEvent::create("mousedown", offset.x(), offset.y())); 234 if (RefPtr<HTMLAnchorElement> link = node->enclosing_link_element()) { 235 dbg() << "HtmlView: clicking on a link to " << link->href(); 236 237 if (link->href().starts_with("javascript:")) { 238 run_javascript_url(link->href()); 239 } else { 240 if (on_link_click) 241 on_link_click(link->href()); 242 } 243 } else { 244 if (event.button() == GUI::MouseButton::Left) { 245 if (layout_root()) 246 layout_root()->selection().set({ result.layout_node, result.index_in_node }, {}); 247 dump_selection("MouseDown"); 248 m_in_mouse_selection = true; 249 } 250 } 251 } 252 } 253 if (hovered_node_changed) 254 update(); 255 event.accept(); 256} 257 258void HtmlView::mouseup_event(GUI::MouseEvent& event) 259{ 260 if (!layout_root()) 261 return GUI::ScrollableWidget::mouseup_event(event); 262 263 auto result = layout_root()->hit_test(to_content_position(event.position())); 264 if (result.layout_node) { 265 if (RefPtr<Node> node = result.layout_node->node()) { 266 auto offset = compute_mouse_event_offset(event.position(), *result.layout_node); 267 node->dispatch_event(MouseEvent::create("mouseup", offset.x(), offset.y())); 268 } 269 } 270 271 if (event.button() == GUI::MouseButton::Left) { 272 dump_selection("MouseUp"); 273 m_in_mouse_selection = false; 274 } 275} 276 277void HtmlView::keydown_event(GUI::KeyEvent& event) 278{ 279 if (event.modifiers() == 0) { 280 switch (event.key()) { 281 case Key_Home: 282 vertical_scrollbar().set_value(0); 283 break; 284 case Key_End: 285 vertical_scrollbar().set_value(vertical_scrollbar().max()); 286 break; 287 case Key_Down: 288 vertical_scrollbar().set_value(vertical_scrollbar().value() + vertical_scrollbar().step()); 289 break; 290 case Key_Up: 291 vertical_scrollbar().set_value(vertical_scrollbar().value() - vertical_scrollbar().step()); 292 break; 293 case Key_Left: 294 horizontal_scrollbar().set_value(horizontal_scrollbar().value() + horizontal_scrollbar().step()); 295 break; 296 case Key_Right: 297 horizontal_scrollbar().set_value(horizontal_scrollbar().value() - horizontal_scrollbar().step()); 298 break; 299 case Key_PageDown: 300 vertical_scrollbar().set_value(vertical_scrollbar().value() + frame_inner_rect().height()); 301 break; 302 case Key_PageUp: 303 vertical_scrollbar().set_value(vertical_scrollbar().value() - frame_inner_rect().height()); 304 break; 305 } 306 } 307 308 event.accept(); 309} 310 311void HtmlView::reload() 312{ 313 load(main_frame().document()->url()); 314} 315 316static RefPtr<Document> create_image_document(const ByteBuffer& data, const URL& url) 317{ 318 auto document = adopt(*new Document(url)); 319 320 auto bitmap = Gfx::load_png_from_memory(data.data(), data.size()); 321 ASSERT(bitmap); 322 323 auto html_element = create_element(document, "html"); 324 document->append_child(html_element); 325 326 auto head_element = create_element(document, "head"); 327 html_element->append_child(head_element); 328 auto title_element = create_element(document, "title"); 329 head_element->append_child(title_element); 330 331 auto basename = FileSystemPath(url.path()).basename(); 332 auto title_text = adopt(*new Text(document, String::format("%s [%dx%d]", basename.characters(), bitmap->width(), bitmap->height()))); 333 title_element->append_child(title_text); 334 335 auto body_element = create_element(document, "body"); 336 html_element->append_child(body_element); 337 338 auto image_element = create_element(document, "img"); 339 image_element->set_attribute("src", url.to_string()); 340 body_element->append_child(image_element); 341 342 return document; 343} 344 345void HtmlView::load(const URL& url) 346{ 347 dbg() << "HtmlView::load: " << url.to_string(); 348 349 if (!url.is_valid()) { 350 load_error_page(url, "Invalid URL"); 351 return; 352 } 353 354 if (window()) 355 window()->set_override_cursor(GUI::StandardCursor::None); 356 357 if (on_load_start) 358 on_load_start(url); 359 360 ResourceLoader::the().load( 361 url, 362 [this, url](auto data) { 363 if (data.is_null()) { 364 load_error_page(url, "No data"); 365 return; 366 } 367 368 RefPtr<Document> document; 369 if (url.path().ends_with(".png")) { 370 document = create_image_document(data, url); 371 } else { 372 document = parse_html_document(data, url); 373 } 374 ASSERT(document); 375 set_document(document); 376 if (on_title_change) 377 on_title_change(document->title()); 378 }, 379 [this, url](auto error) { 380 load_error_page(url, error); 381 }); 382} 383 384void HtmlView::load_error_page(const URL& failed_url, const String& error) 385{ 386 auto error_page_url = "file:///res/html/error.html"; 387 ResourceLoader::the().load( 388 error_page_url, 389 [this, failed_url, error](auto data) { 390 ASSERT(!data.is_null()); 391 auto html = String::format( 392 String::copy(data).characters(), 393 escape_html_entities(failed_url.to_string()).characters(), 394 escape_html_entities(error).characters()); 395 auto document = parse_html_document(html, failed_url); 396 ASSERT(document); 397 set_document(document); 398 if (on_title_change) 399 on_title_change(document->title()); 400 }, 401 [](auto error) { 402 dbg() << "Failed to load error page: " << error; 403 ASSERT_NOT_REACHED(); 404 }); 405} 406 407const LayoutDocument* HtmlView::layout_root() const 408{ 409 return document() ? document()->layout_node() : nullptr; 410} 411 412LayoutDocument* HtmlView::layout_root() 413{ 414 if (!document()) 415 return nullptr; 416 return const_cast<LayoutDocument*>(document()->layout_node()); 417} 418 419void HtmlView::scroll_to_anchor(const StringView& name) 420{ 421 if (!document()) 422 return; 423 424 const auto* element = document()->get_element_by_id(name); 425 if (!element) { 426 auto candidates = document()->get_elements_by_name(name); 427 for (auto* candidate : candidates) { 428 if (is<HTMLAnchorElement>(*candidate)) { 429 element = to<HTMLAnchorElement>(candidate); 430 break; 431 } 432 } 433 } 434 435 if (!element) { 436 dbg() << "HtmlView::scroll_to_anchor(): Anchor not found: '" << name << "'"; 437 return; 438 } 439 if (!element->layout_node()) { 440 dbg() << "HtmlView::scroll_to_anchor(): Anchor found but without layout node: '" << name << "'"; 441 return; 442 } 443 auto& layout_node = *element->layout_node(); 444 Gfx::FloatRect float_rect { layout_node.box_type_agnostic_position(), { (float)visible_content_rect().width(), (float)visible_content_rect().height() } }; 445 scroll_into_view(enclosing_int_rect(float_rect), true, true); 446 window()->set_override_cursor(GUI::StandardCursor::None); 447} 448 449Document* HtmlView::document() 450{ 451 return main_frame().document(); 452} 453 454const Document* HtmlView::document() const 455{ 456 return main_frame().document(); 457} 458 459void HtmlView::dump_selection(const char* event_name) 460{ 461 UNUSED_PARAM(event_name); 462#ifdef SELECTION_DEBUG 463 dbg() << event_name << " selection start: " 464 << layout_root()->selection().start().layout_node << ":" << layout_root()->selection().start().index_in_node << ", end: " 465 << layout_root()->selection().end().layout_node << ":" << layout_root()->selection().end().index_in_node; 466#endif 467} 468 469void HtmlView::did_scroll() 470{ 471 main_frame().set_viewport_rect(visible_content_rect()); 472} 473 474Gfx::Point HtmlView::compute_mouse_event_offset(const Gfx::Point& event_position, const LayoutNode& layout_node) const 475{ 476 auto content_event_position = to_content_position(event_position); 477 auto top_left_of_layout_node = layout_node.box_type_agnostic_position(); 478 479 return { 480 content_event_position.x() - static_cast<int>(top_left_of_layout_node.x()), 481 content_event_position.y() - static_cast<int>(top_left_of_layout_node.y()) 482 }; 483} 484 485void HtmlView::run_javascript_url(const String& url) 486{ 487 ASSERT(url.starts_with("javascript:")); 488 if (!document()) 489 return; 490 491 auto source = url.substring_view(11, url.length() - 11); 492 dbg() << "running js from url: _" << source << "_"; 493 document()->run_javascript(source); 494} 495 496}