Serenity Operating System
at portability 412 lines 14 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 <LibCore/File.h> 29#include <LibGUI/Application.h> 30#include <LibGUI/Painter.h> 31#include <LibGUI/ScrollBar.h> 32#include <LibGUI/Window.h> 33#include <LibGfx/PNGLoader.h> 34#include <LibHTML/DOM/Element.h> 35#include <LibHTML/DOM/ElementFactory.h> 36#include <LibHTML/DOM/HTMLAnchorElement.h> 37#include <LibHTML/DOM/HTMLImageElement.h> 38#include <LibHTML/DOM/Text.h> 39#include <LibHTML/Dump.h> 40#include <LibHTML/Frame.h> 41#include <LibHTML/HtmlView.h> 42#include <LibHTML/Layout/LayoutDocument.h> 43#include <LibHTML/Layout/LayoutNode.h> 44#include <LibHTML/Parser/HTMLParser.h> 45#include <LibHTML/RenderingContext.h> 46#include <LibHTML/ResourceLoader.h> 47#include <stdio.h> 48 49HtmlView::HtmlView() 50 : m_main_frame(::Frame::create(*this)) 51{ 52 main_frame().on_set_needs_display = [this](auto& content_rect) { 53 if (content_rect.is_empty()) { 54 update(); 55 return; 56 } 57 Gfx::Rect adjusted_rect = content_rect; 58 adjusted_rect.set_location(to_widget_position(content_rect.location())); 59 update(adjusted_rect); 60 }; 61 62 set_should_hide_unnecessary_scrollbars(true); 63 set_background_role(ColorRole::Base); 64} 65 66HtmlView::~HtmlView() 67{ 68} 69 70void HtmlView::set_document(Document* new_document) 71{ 72 RefPtr<Document> old_document = document(); 73 74 if (new_document == old_document) 75 return; 76 77 if (old_document) 78 old_document->on_layout_updated = nullptr; 79 80 main_frame().set_document(new_document); 81 82 if (new_document) { 83 new_document->on_layout_updated = [this] { 84 layout_and_sync_size(); 85 update(); 86 }; 87 } 88 89#ifdef HTML_DEBUG 90 if (document != nullptr) { 91 dbgprintf("\033[33;1mLayout tree before layout:\033[0m\n"); 92 ::dump_tree(*layout_root()); 93 } 94#endif 95 96 layout_and_sync_size(); 97 update(); 98} 99 100void HtmlView::layout_and_sync_size() 101{ 102 if (!document()) 103 return; 104 105 bool had_vertical_scrollbar = vertical_scrollbar().is_visible(); 106 bool had_horizontal_scrollbar = horizontal_scrollbar().is_visible(); 107 108 main_frame().set_size(available_size()); 109 document()->layout(); 110 set_content_size(enclosing_int_rect(layout_root()->rect()).size()); 111 112 // NOTE: If layout caused us to gain or lose scrollbars, we have to lay out again 113 // since the scrollbars now take up some of the available space. 114 if (had_vertical_scrollbar != vertical_scrollbar().is_visible() || had_horizontal_scrollbar != horizontal_scrollbar().is_visible()) { 115 main_frame().set_size(available_size()); 116 document()->layout(); 117 set_content_size(enclosing_int_rect(layout_root()->rect()).size()); 118 } 119 120 main_frame().set_viewport_rect(visible_content_rect()); 121 122#ifdef HTML_DEBUG 123 dbgprintf("\033[33;1mLayout tree after layout:\033[0m\n"); 124 ::dump_tree(*layout_root()); 125#endif 126} 127 128void HtmlView::resize_event(GUI::ResizeEvent& event) 129{ 130 GUI::ScrollableWidget::resize_event(event); 131 layout_and_sync_size(); 132} 133 134void HtmlView::paint_event(GUI::PaintEvent& event) 135{ 136 GUI::Frame::paint_event(event); 137 138 GUI::Painter painter(*this); 139 painter.add_clip_rect(widget_inner_rect()); 140 painter.add_clip_rect(event.rect()); 141 142 if (!layout_root()) { 143 painter.fill_rect(event.rect(), palette().color(background_role())); 144 return; 145 } 146 147 painter.fill_rect(event.rect(), document()->background_color(palette())); 148 149 if (auto background_bitmap = document()->background_image()) { 150 painter.draw_tiled_bitmap(event.rect(), *background_bitmap); 151 } 152 153 painter.translate(frame_thickness(), frame_thickness()); 154 painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value()); 155 156 RenderingContext context(painter, palette()); 157 context.set_should_show_line_box_borders(m_should_show_line_box_borders); 158 context.set_viewport_rect(visible_content_rect()); 159 layout_root()->render(context); 160} 161 162void HtmlView::mousemove_event(GUI::MouseEvent& event) 163{ 164 if (!layout_root()) 165 return GUI::ScrollableWidget::mousemove_event(event); 166 167 bool hovered_node_changed = false; 168 bool is_hovering_link = false; 169 bool was_hovering_link = document()->hovered_node() && document()->hovered_node()->is_link(); 170 auto result = layout_root()->hit_test(to_content_position(event.position())); 171 const HTMLAnchorElement* hovered_link_element = nullptr; 172 if (result.layout_node) { 173 auto* node = result.layout_node->node(); 174 hovered_node_changed = node != document()->hovered_node(); 175 document()->set_hovered_node(const_cast<Node*>(node)); 176 if (node) { 177 hovered_link_element = node->enclosing_link_element(); 178 if (hovered_link_element) { 179#ifdef HTML_DEBUG 180 dbg() << "HtmlView: hovering over a link to " << hovered_link_element->href(); 181#endif 182 is_hovering_link = true; 183 } 184 } 185 if (m_in_mouse_selection) { 186 layout_root()->selection().set_end({ result.layout_node, result.index_in_node }); 187 dump_selection("MouseMove"); 188 update(); 189 } 190 } 191 if (window()) 192 window()->set_override_cursor(is_hovering_link ? GUI::StandardCursor::Hand : GUI::StandardCursor::None); 193 if (hovered_node_changed) { 194 update(); 195 auto* hovered_html_element = document()->hovered_node() ? document()->hovered_node()->enclosing_html_element() : nullptr; 196 if (hovered_html_element && !hovered_html_element->title().is_null()) { 197 auto screen_position = screen_relative_rect().location().translated(event.position()); 198 GUI::Application::the().show_tooltip(hovered_html_element->title(), screen_position.translated(4, 4)); 199 } else { 200 GUI::Application::the().hide_tooltip(); 201 } 202 } 203 if (is_hovering_link != was_hovering_link) { 204 if (on_link_hover) { 205 on_link_hover(hovered_link_element ? document()->complete_url(hovered_link_element->href()).to_string() : String()); 206 } 207 } 208 event.accept(); 209} 210 211void HtmlView::mousedown_event(GUI::MouseEvent& event) 212{ 213 if (!layout_root()) 214 return GUI::ScrollableWidget::mousemove_event(event); 215 216 bool hovered_node_changed = false; 217 auto result = layout_root()->hit_test(to_content_position(event.position())); 218 if (result.layout_node) { 219 auto* node = result.layout_node->node(); 220 hovered_node_changed = node != document()->hovered_node(); 221 document()->set_hovered_node(const_cast<Node*>(node)); 222 if (node) { 223 if (auto* link = node->enclosing_link_element()) { 224 dbg() << "HtmlView: clicking on a link to " << link->href(); 225 if (on_link_click) 226 on_link_click(link->href()); 227 } else { 228 if (event.button() == GUI::MouseButton::Left) { 229 layout_root()->selection().set({ result.layout_node, result.index_in_node }, {}); 230 dump_selection("MouseDown"); 231 m_in_mouse_selection = true; 232 } 233 } 234 } 235 } 236 if (hovered_node_changed) 237 update(); 238 event.accept(); 239} 240 241void HtmlView::mouseup_event(GUI::MouseEvent& event) 242{ 243 if (!layout_root()) 244 return GUI::ScrollableWidget::mouseup_event(event); 245 246 if (event.button() == GUI::MouseButton::Left) { 247 dump_selection("MouseUp"); 248 m_in_mouse_selection = false; 249 } 250} 251 252void HtmlView::keydown_event(GUI::KeyEvent& event) 253{ 254 if (event.modifiers() == 0) { 255 switch (event.key()) { 256 case Key_Home: 257 vertical_scrollbar().set_value(0); 258 break; 259 case Key_End: 260 vertical_scrollbar().set_value(vertical_scrollbar().max()); 261 break; 262 case Key_Down: 263 vertical_scrollbar().set_value(vertical_scrollbar().value() + vertical_scrollbar().step()); 264 break; 265 case Key_Up: 266 vertical_scrollbar().set_value(vertical_scrollbar().value() - vertical_scrollbar().step()); 267 break; 268 case Key_Left: 269 horizontal_scrollbar().set_value(horizontal_scrollbar().value() + horizontal_scrollbar().step()); 270 break; 271 case Key_Right: 272 horizontal_scrollbar().set_value(horizontal_scrollbar().value() - horizontal_scrollbar().step()); 273 break; 274 case Key_PageDown: 275 vertical_scrollbar().set_value(vertical_scrollbar().value() + frame_inner_rect().height()); 276 break; 277 case Key_PageUp: 278 vertical_scrollbar().set_value(vertical_scrollbar().value() - frame_inner_rect().height()); 279 break; 280 } 281 } 282 283 event.accept(); 284} 285 286void HtmlView::reload() 287{ 288 load(main_frame().document()->url()); 289} 290 291static RefPtr<Document> create_image_document(const ByteBuffer& data, const URL& url) 292{ 293 auto document = adopt(*new Document); 294 document->set_url(url); 295 296 auto bitmap = Gfx::load_png_from_memory(data.data(), data.size()); 297 ASSERT(bitmap); 298 299 auto html_element = create_element(document, "html"); 300 document->append_child(html_element); 301 302 auto head_element = create_element(document, "head"); 303 html_element->append_child(head_element); 304 auto title_element = create_element(document, "title"); 305 head_element->append_child(title_element); 306 307 auto basename = FileSystemPath(url.path()).basename(); 308 auto title_text = adopt(*new Text(document, String::format("%s [%dx%d]", basename.characters(), bitmap->width(), bitmap->height()))); 309 title_element->append_child(title_text); 310 311 auto body_element = create_element(document, "body"); 312 html_element->append_child(body_element); 313 314 auto image_element = create_element(document, "img"); 315 image_element->set_attribute("src", url.to_string()); 316 body_element->append_child(image_element); 317 318 return document; 319} 320 321void HtmlView::load(const URL& url) 322{ 323 dbg() << "HtmlView::load: " << url; 324 325 if (window()) 326 window()->set_override_cursor(GUI::StandardCursor::None); 327 328 if (on_load_start) 329 on_load_start(url); 330 331 ResourceLoader::the().load(url, [this, url](auto data) { 332 if (data.is_null()) { 333 dbg() << "Load failed!"; 334 ASSERT_NOT_REACHED(); 335 } 336 337 RefPtr<Document> document; 338 if (url.path().ends_with(".png")) { 339 document = create_image_document(data, url); 340 } else { 341 document = parse_html_document(data, url); 342 } 343 ASSERT(document); 344 set_document(document); 345 if (on_title_change) 346 on_title_change(document->title()); 347 }); 348} 349 350const LayoutDocument* HtmlView::layout_root() const 351{ 352 return document() ? document()->layout_node() : nullptr; 353} 354 355LayoutDocument* HtmlView::layout_root() 356{ 357 if (!document()) 358 return nullptr; 359 return const_cast<LayoutDocument*>(document()->layout_node()); 360} 361 362void HtmlView::scroll_to_anchor(const StringView& name) 363{ 364 if (!document()) 365 return; 366 367 auto* element = document()->get_element_by_id(name); 368 if (!element) { 369 auto candidates = document()->get_elements_by_name(name); 370 for (auto* candidate : candidates) { 371 if (is<HTMLAnchorElement>(*candidate)) { 372 element = to<HTMLAnchorElement>(candidate); 373 break; 374 } 375 } 376 } 377 378 if (!element) { 379 dbg() << "HtmlView::scroll_to_anchor(): Anchor not found: '" << name << "'"; 380 return; 381 } 382 if (!element->layout_node()) { 383 dbg() << "HtmlView::scroll_to_anchor(): Anchor found but without layout node: '" << name << "'"; 384 return; 385 } 386 auto& layout_node = *element->layout_node(); 387 Gfx::FloatRect float_rect { layout_node.box_type_agnostic_position(), { (float)visible_content_rect().width(), (float)visible_content_rect().height() } }; 388 scroll_into_view(enclosing_int_rect(float_rect), true, true); 389 window()->set_override_cursor(GUI::StandardCursor::None); 390} 391 392Document* HtmlView::document() 393{ 394 return main_frame().document(); 395} 396 397const Document* HtmlView::document() const 398{ 399 return main_frame().document(); 400} 401 402void HtmlView::dump_selection(const char* event_name) 403{ 404 dbg() << event_name << " selection start: " 405 << layout_root()->selection().start().layout_node << ":" << layout_root()->selection().start().index_in_node << ", end: " 406 << layout_root()->selection().end().layout_node << ":" << layout_root()->selection().end().index_in_node; 407} 408 409void HtmlView::did_scroll() 410{ 411 main_frame().set_viewport_rect(visible_content_rect()); 412}