Serenity Operating System
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}