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 "BookmarksBarWidget.h"
28#include "History.h"
29#include "InspectorWidget.h"
30#include <LibCore/File.h>
31#include <LibGUI/AboutDialog.h>
32#include <LibGUI/Action.h>
33#include <LibGUI/Application.h>
34#include <LibGUI/BoxLayout.h>
35#include <LibGUI/Button.h>
36#include <LibGUI/Menu.h>
37#include <LibGUI/MenuBar.h>
38#include <LibGUI/StatusBar.h>
39#include <LibGUI/TextBox.h>
40#include <LibGUI/ToolBar.h>
41#include <LibGUI/Window.h>
42#include <LibWeb/CSS/StyleResolver.h>
43#include <LibWeb/DOM/Element.h>
44#include <LibWeb/DOMTreeModel.h>
45#include <LibWeb/Dump.h>
46#include <LibWeb/Frame.h>
47#include <LibWeb/HtmlView.h>
48#include <LibWeb/Layout/LayoutBlock.h>
49#include <LibWeb/Layout/LayoutDocument.h>
50#include <LibWeb/Layout/LayoutInline.h>
51#include <LibWeb/Layout/LayoutNode.h>
52#include <LibWeb/Parser/CSSParser.h>
53#include <LibWeb/Parser/HTMLParser.h>
54#include <LibWeb/ResourceLoader.h>
55#include <stdio.h>
56#include <stdlib.h>
57
58static const char* home_url = "file:///home/anon/www/welcome.html";
59static const char* bookmarks_filename = "/home/anon/bookmarks.json";
60
61static String s_title = "";
62
63int main(int argc, char** argv)
64{
65 if (getuid() == 0) {
66 fprintf(stderr, "Refusing to run as root\n");
67 return 1;
68 }
69
70#ifdef __serenity__
71 if (pledge("stdio shared_buffer accept unix cpath rpath wpath fattr", nullptr) < 0) {
72 perror("pledge");
73 return 1;
74 }
75#endif
76
77 GUI::Application app(argc, argv);
78
79 // Connect to the ProtocolServer immediately so we can drop the "unix" pledge.
80 Web::ResourceLoader::the();
81
82#ifdef __serenity__
83 if (pledge("stdio shared_buffer accept cpath rpath wpath", nullptr) < 0) {
84 perror("pledge");
85 return 1;
86 }
87
88 if (unveil("/home", "rwc") < 0) {
89 perror("unveil");
90 return 1;
91 }
92
93 if (unveil("/res", "r") < 0) {
94 perror("unveil");
95 return 1;
96 }
97
98 unveil(nullptr, nullptr);
99#endif
100
101 auto window = GUI::Window::construct();
102 window->set_rect(100, 100, 640, 480);
103
104 auto& widget = window->set_main_widget<GUI::Widget>();
105 widget.set_fill_with_background_color(true);
106 widget.set_layout<GUI::VerticalBoxLayout>();
107 widget.layout()->set_spacing(0);
108
109 bool bookmarksbar_enabled = true;
110
111 auto& toolbar = widget.add<GUI::ToolBar>();
112 auto& bookmarksbar = widget.add<BookmarksBarWidget>(bookmarks_filename, bookmarksbar_enabled);
113 auto& html_widget = widget.add<Web::HtmlView>();
114
115 bookmarksbar.on_bookmark_click = [&](auto&, auto& url) {
116 html_widget.load(url);
117 };
118
119 History<URL> history;
120
121 RefPtr<GUI::Action> go_back_action;
122 RefPtr<GUI::Action> go_forward_action;
123
124 auto update_actions = [&]() {
125 go_back_action->set_enabled(history.can_go_back());
126 go_forward_action->set_enabled(history.can_go_forward());
127 };
128
129 bool should_push_loads_to_history = true;
130
131 go_back_action = GUI::CommonActions::make_go_back_action([&](auto&) {
132 history.go_back();
133 update_actions();
134 TemporaryChange<bool> change(should_push_loads_to_history, false);
135 html_widget.load(history.current());
136 });
137
138 go_forward_action = GUI::CommonActions::make_go_forward_action([&](auto&) {
139 history.go_forward();
140 update_actions();
141 TemporaryChange<bool> change(should_push_loads_to_history, false);
142 html_widget.load(history.current());
143 });
144
145 toolbar.add_action(*go_back_action);
146 toolbar.add_action(*go_forward_action);
147
148 toolbar.add_action(GUI::CommonActions::make_go_home_action([&](auto&) {
149 html_widget.load(home_url);
150 }));
151
152 toolbar.add_action(GUI::CommonActions::make_reload_action([&](auto&) {
153 TemporaryChange<bool> change(should_push_loads_to_history, false);
154 html_widget.reload();
155 }));
156
157 auto& location_box = toolbar.add<GUI::TextBox>();
158
159 location_box.on_return_pressed = [&] {
160 html_widget.load(location_box.text());
161 };
162
163 auto& bookmark_button = toolbar.add<GUI::Button>();
164 bookmark_button.set_button_style(Gfx::ButtonStyle::CoolBar);
165 bookmark_button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/star-black.png"));
166 bookmark_button.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed);
167 bookmark_button.set_preferred_size(22, 22);
168
169 auto update_bookmark_button = [&](const String& url) {
170 if (bookmarksbar.contains_bookmark(url)) {
171 bookmark_button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/star-yellow.png"));
172 } else {
173 bookmark_button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/star-contour.png"));
174 }
175 };
176
177 bookmark_button.on_click = [&] {
178 auto url = html_widget.main_frame().document()->url().to_string();
179 if (bookmarksbar.contains_bookmark(url)) {
180 bookmarksbar.remove_bookmark(url);
181 } else {
182 bookmarksbar.add_bookmark(url, s_title);
183 }
184 update_bookmark_button(url);
185 };
186
187 html_widget.on_load_start = [&](auto& url) {
188 location_box.set_text(url.to_string());
189 if (should_push_loads_to_history)
190 history.push(url);
191 update_actions();
192 update_bookmark_button(url.to_string());
193 };
194
195 html_widget.on_link_click = [&](auto& url) {
196 if (url.starts_with("#")) {
197 html_widget.scroll_to_anchor(url.substring_view(1, url.length() - 1));
198 } else {
199 html_widget.load(html_widget.document()->complete_url(url));
200 }
201 };
202
203 html_widget.on_title_change = [&](auto& title) {
204 s_title = title;
205 window->set_title(String::format("%s - Browser", title.characters()));
206 };
207
208 auto focus_location_box_action = GUI::Action::create("Focus location box", { Mod_Ctrl, Key_L }, [&](auto&) {
209 location_box.select_all();
210 location_box.set_focus(true);
211 });
212
213 auto& statusbar = widget.add<GUI::StatusBar>();
214
215 html_widget.on_link_hover = [&](auto& href) {
216 statusbar.set_text(href);
217 };
218
219 bookmarksbar.on_bookmark_hover = [&](auto&, auto& url) {
220 statusbar.set_text(url);
221 };
222
223 Web::ResourceLoader::the().on_load_counter_change = [&] {
224 if (Web::ResourceLoader::the().pending_loads() == 0) {
225 statusbar.set_text("");
226 return;
227 }
228 statusbar.set_text(String::format("Loading (%d pending resources...)", Web::ResourceLoader::the().pending_loads()));
229 };
230
231 auto menubar = make<GUI::MenuBar>();
232
233 auto& app_menu = menubar->add_menu("Browser");
234 app_menu.add_action(GUI::Action::create("Reload", { Mod_None, Key_F5 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/reload.png"), [&](auto&) {
235 TemporaryChange<bool> change(should_push_loads_to_history, false);
236 html_widget.reload();
237 }));
238 app_menu.add_separator();
239 app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
240 app.quit();
241 }));
242
243 RefPtr<GUI::Window> dom_inspector_window;
244
245 auto& inspect_menu = menubar->add_menu("Inspect");
246 inspect_menu.add_action(GUI::Action::create("View source", { Mod_Ctrl, Key_U }, [&](auto&) {
247 String filename_to_open;
248 char tmp_filename[] = "/tmp/view-source.XXXXXX";
249 ASSERT(html_widget.document());
250 if (html_widget.document()->url().protocol() == "file") {
251 filename_to_open = html_widget.document()->url().path();
252 } else {
253 int fd = mkstemp(tmp_filename);
254 ASSERT(fd >= 0);
255 auto source = html_widget.document()->source();
256 write(fd, source.characters(), source.length());
257 close(fd);
258 filename_to_open = tmp_filename;
259 }
260 if (fork() == 0) {
261 execl("/bin/TextEditor", "TextEditor", filename_to_open.characters(), nullptr);
262 ASSERT_NOT_REACHED();
263 }
264 }));
265 inspect_menu.add_action(GUI::Action::create("Inspect DOM tree", { Mod_None, Key_F12 }, [&](auto&) {
266 if (!dom_inspector_window) {
267 dom_inspector_window = GUI::Window::construct();
268 dom_inspector_window->set_rect(100, 100, 300, 500);
269 dom_inspector_window->set_title("DOM inspector");
270 dom_inspector_window->set_main_widget<InspectorWidget>();
271 }
272 auto* inspector_widget = static_cast<InspectorWidget*>(dom_inspector_window->main_widget());
273 inspector_widget->set_document(html_widget.document());
274 dom_inspector_window->show();
275 dom_inspector_window->move_to_front();
276 }));
277
278 auto& debug_menu = menubar->add_menu("Debug");
279 debug_menu.add_action(GUI::Action::create("Dump DOM tree", [&](auto&) {
280 dump_tree(*html_widget.document());
281 }));
282 debug_menu.add_action(GUI::Action::create("Dump Layout tree", [&](auto&) {
283 dump_tree(*html_widget.document()->layout_node());
284 }));
285 debug_menu.add_action(GUI::Action::create("Dump Style sheets", [&](auto&) {
286 for (auto& sheet : html_widget.document()->stylesheets()) {
287 dump_sheet(sheet);
288 }
289 }));
290 debug_menu.add_separator();
291 auto line_box_borders_action = GUI::Action::create("Line box borders", [&](auto& action) {
292 action.set_checked(!action.is_checked());
293 html_widget.set_should_show_line_box_borders(action.is_checked());
294 html_widget.update();
295 });
296 line_box_borders_action->set_checkable(true);
297 line_box_borders_action->set_checked(false);
298 debug_menu.add_action(line_box_borders_action);
299
300 auto& bookmarks_menu = menubar->add_menu("Bookmarks");
301 auto show_bookmarksbar_action = GUI::Action::create("Show bookmarks bar", [&](auto& action) {
302 action.set_checked(!action.is_checked());
303 bookmarksbar.set_visible(action.is_checked());
304 bookmarksbar.update();
305 });
306 show_bookmarksbar_action->set_checkable(true);
307 show_bookmarksbar_action->set_checked(bookmarksbar_enabled);
308 bookmarks_menu.add_action(show_bookmarksbar_action);
309
310 auto& help_menu = menubar->add_menu("Help");
311 help_menu.add_action(GUI::Action::create("About", [&](const GUI::Action&) {
312 GUI::AboutDialog::show("Browser", Gfx::Bitmap::load_from_file("/res/icons/32x32/filetype-html.png"), window);
313 }));
314
315 app.set_menubar(move(menubar));
316
317 window->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-html.png"));
318
319 window->set_title("Browser");
320 window->show();
321
322 URL url_to_load = home_url;
323
324 if (app.args().size() >= 1) {
325 url_to_load = URL();
326 url_to_load.set_protocol("file");
327 url_to_load.set_path(app.args()[0]);
328 }
329
330 html_widget.load(url_to_load);
331
332 return app.exec();
333}