Skip to content

Commit b169a98

Browse files
trflynn89kalenikaliaksandr
authored andcommittedMar 22, 2025
LibWeb+LibWebView+WebContent: Introduce a basic about:settings page
This adds a basic settings page to manage persistent Ladybird settings. As a first pass, this exposes settings for the new tab page URL and the default search engine. The way the search engine option works is that once search is enabled, the user must choose their default search engine; we do not apply any default automatically. Search remains disabled until this is done. There are a couple of improvements that we should make here: * Settings changes are not broadcasted to all open about:settings pages. So if two instances are open, and the user changes the search engine in one instance, the other instance will have a stale UI. * Adding an IPC per setting is going to get annoying. It would be nice if we can come up with a smaller set of IPCs to send only the relevant changed settings.
1 parent e084a86 commit b169a98

16 files changed

+475
-2
lines changed
 
+254
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>Settings</title>
5+
<link rel="stylesheet" type="text/css" href="resource://ladybird/ladybird.css" />
6+
<style>
7+
@media (prefers-color-scheme: light) {
8+
:root {
9+
--card-background-color: #f9f9f9;
10+
--card-header-background-color: #f0f2f5;
11+
12+
--input-background-color: white;
13+
14+
--border-color: #dcdde1;
15+
}
16+
}
17+
18+
@media (prefers-color-scheme: dark) {
19+
:root {
20+
--card-background-color: #2c2c2c;
21+
--card-header-background-color: #252525;
22+
23+
--border-color: #3d3d3d;
24+
}
25+
}
26+
27+
* {
28+
box-sizing: border-box;
29+
}
30+
31+
body {
32+
max-width: 800px;
33+
34+
margin: 0 auto;
35+
padding: 20px;
36+
}
37+
38+
header {
39+
display: flex;
40+
align-items: center;
41+
42+
margin-bottom: 30px;
43+
}
44+
45+
header h1 {
46+
font-size: 20px;
47+
}
48+
49+
header img {
50+
height: 48px;
51+
margin-right: 10px;
52+
float: left;
53+
}
54+
55+
.card {
56+
background-color: var(--card-background-color);
57+
58+
border-radius: 8px;
59+
margin-bottom: 20px;
60+
61+
overflow: hidden;
62+
}
63+
64+
.card-header {
65+
background-color: var(--card-header-background-color);
66+
67+
border-bottom: 1px solid var(--border-color);
68+
padding: 15px 20px;
69+
}
70+
71+
.card-body {
72+
padding: 20px;
73+
}
74+
75+
.card-group {
76+
margin-bottom: 20px;
77+
}
78+
79+
.card-group:last-child {
80+
margin-bottom: 0;
81+
}
82+
83+
label {
84+
display: block;
85+
margin-bottom: 8px;
86+
87+
font-size: 14px;
88+
}
89+
90+
input[type="url"],
91+
select {
92+
background-color: var(--input-background-color);
93+
94+
width: 100%;
95+
border: 1px solid var(--border-color);
96+
}
97+
98+
input[type="url"].success {
99+
border: 1px solid green;
100+
}
101+
102+
input[type="url"].error {
103+
border: 1px solid red;
104+
}
105+
106+
.button-container {
107+
display: flex;
108+
justify-content: flex-end;
109+
110+
margin-top: 20px;
111+
gap: 10px;
112+
}
113+
</style>
114+
</head>
115+
<body>
116+
<header>
117+
<picture>
118+
<source
119+
srcset="resource://icons/128x128/app-browser.png"
120+
media="(prefers-color-scheme: dark)"
121+
/>
122+
<img src="resource://icons/128x128/app-browser-dark.png" />
123+
</picture>
124+
<h1>Ladybird Settings</h1>
125+
</header>
126+
127+
<div class="card">
128+
<div class="card-header">General</div>
129+
<div class="card-body">
130+
<div class="card-group">
131+
<label for="new-tab-page-url">New Tab Page URL</label>
132+
<input id="new-tab-page-url" type="url" placeholder="about:newtab" />
133+
</div>
134+
</div>
135+
</div>
136+
137+
<div class="card">
138+
<div class="card-header">Search</div>
139+
<div class="card-body">
140+
<div class="card-group">
141+
<div class="toggle-container">
142+
<label for="search-enabled">Enable Search</label>
143+
<label class="toggle">
144+
<input id="search-enabled" type="checkbox" />
145+
<span class="toggle-button"></span>
146+
</label>
147+
</div>
148+
</div>
149+
<div id="search-engine-list" class="card-group" style="display: none">
150+
<label for="search-engine">Default Search Engine</label>
151+
<select id="search-engine">
152+
<option value="">Please Select a Search Engine</option>
153+
<hr />
154+
</select>
155+
</div>
156+
</div>
157+
</div>
158+
159+
<div class="button-container">
160+
<button id="restore-defaults" class="primary-button">Restore Defaults</button>
161+
</div>
162+
163+
<script>
164+
const newTabPageURL = document.querySelector("#new-tab-page-url");
165+
const searchEngineList = document.querySelector("#search-engine-list");
166+
const searchEnabled = document.querySelector("#search-enabled");
167+
const searchEngine = document.querySelector("#search-engine");
168+
const restoreDefaults = document.querySelector("#restore-defaults");
169+
170+
settings.settings = {};
171+
172+
const renderSettings = () => {
173+
newTabPageURL.classList.remove("error");
174+
newTabPageURL.value = settings.settings.newTabPageURL;
175+
176+
const searchEngineName = settings.settings.searchEngine?.name;
177+
178+
if (searchEngineName) {
179+
searchEnabled.checked = true;
180+
searchEngine.value = searchEngineName;
181+
} else {
182+
searchEnabled.checked = false;
183+
}
184+
185+
renderSearchEngine();
186+
};
187+
188+
const renderSearchEngine = () => {
189+
searchEngineList.style.display = searchEnabled.checked ? "block" : "none";
190+
191+
if (searchEnabled.checked && searchEngine.selectedIndex !== 0) {
192+
searchEngine.item(0).disabled = true;
193+
} else if (!searchEnabled.checked) {
194+
searchEngine.item(0).disabled = false;
195+
searchEngine.selectedIndex = 0;
196+
}
197+
};
198+
199+
const saveSearchEngine = () => {
200+
if (searchEnabled.checked && searchEngine.selectedIndex !== 0) {
201+
settings.setSearchEngine(searchEngine.value);
202+
} else if (!searchEnabled.checked) {
203+
settings.setSearchEngine(null);
204+
}
205+
206+
renderSearchEngine();
207+
};
208+
209+
newTabPageURL.addEventListener("change", () => {
210+
newTabPageURL.classList.remove("success");
211+
newTabPageURL.classList.remove("error");
212+
213+
if (!newTabPageURL.checkValidity()) {
214+
newTabPageURL.classList.add("error");
215+
return;
216+
}
217+
218+
settings.setNewTabPageURL(newTabPageURL.value);
219+
newTabPageURL.classList.add("success");
220+
221+
setTimeout(() => {
222+
newTabPageURL.classList.remove("success");
223+
}, 1000);
224+
});
225+
226+
searchEnabled.addEventListener("change", saveSearchEngine);
227+
searchEngine.addEventListener("change", saveSearchEngine);
228+
229+
restoreDefaults.addEventListener("click", () => {
230+
settings.restoreDefaultSettings();
231+
});
232+
233+
settings.loadSettings = settings => {
234+
window.settings.settings = JSON.parse(settings);
235+
renderSettings();
236+
};
237+
238+
settings.loadSearchEngines = engines => {
239+
for (const engine of JSON.parse(engines)) {
240+
const option = document.createElement("option");
241+
option.text = engine;
242+
option.value = engine;
243+
244+
searchEngine.add(option);
245+
}
246+
};
247+
248+
document.addEventListener("DOMContentLoaded", () => {
249+
settings.loadAvailableSearchEngines();
250+
settings.loadCurrentSettings();
251+
});
252+
</script>
253+
</body>
254+
</html>

‎Libraries/LibWeb/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,7 @@ set(SOURCES
564564
Internals/Internals.cpp
565565
Internals/InternalsBase.cpp
566566
Internals/Processes.cpp
567+
Internals/Settings.cpp
567568
IntersectionObserver/IntersectionObserver.cpp
568569
IntersectionObserver/IntersectionObserverEntry.cpp
569570
Layout/AudioBox.cpp

‎Libraries/LibWeb/HTML/Window.cpp

+4-2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
#include <LibWeb/Infra/CharacterTypes.h>
6262
#include <LibWeb/Internals/Internals.h>
6363
#include <LibWeb/Internals/Processes.h>
64+
#include <LibWeb/Internals/Settings.h>
6465
#include <LibWeb/Layout/Viewport.h>
6566
#include <LibWeb/Page/Page.h>
6667
#include <LibWeb/Painting/PaintableBox.h>
@@ -738,9 +739,10 @@ WebIDL::ExceptionOr<void> Window::initialize_web_interfaces(Badge<WindowEnvironm
738739
if (url.scheme() == "about"sv && url.paths().size() == 1) {
739740
auto const& path = url.paths().first();
740741

741-
if (path == "processes"sv) {
742+
if (path == "processes"sv)
742743
define_direct_property("processes", realm.create<Internals::Processes>(realm), JS::default_attributes);
743-
}
744+
else if (path == "settings"sv)
745+
define_direct_property("settings", realm.create<Internals::Settings>(realm), JS::default_attributes);
744746
}
745747

746748
return {};
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#include <LibURL/Parser.h>
8+
#include <LibURL/URL.h>
9+
#include <LibWeb/Bindings/Intrinsics.h>
10+
#include <LibWeb/Bindings/SettingsPrototype.h>
11+
#include <LibWeb/Internals/Settings.h>
12+
#include <LibWeb/Page/Page.h>
13+
14+
namespace Web::Internals {
15+
16+
GC_DEFINE_ALLOCATOR(Settings);
17+
18+
Settings::Settings(JS::Realm& realm)
19+
: InternalsBase(realm)
20+
{
21+
}
22+
23+
Settings::~Settings() = default;
24+
25+
void Settings::initialize(JS::Realm& realm)
26+
{
27+
Base::initialize(realm);
28+
WEB_SET_PROTOTYPE_FOR_INTERFACE(Settings);
29+
}
30+
31+
void Settings::load_current_settings()
32+
{
33+
page().client().request_current_settings();
34+
}
35+
36+
void Settings::restore_default_settings()
37+
{
38+
page().client().restore_default_settings();
39+
}
40+
41+
void Settings::set_new_tab_page_url(String const& new_tab_page_url)
42+
{
43+
if (auto parsed_new_tab_page_url = URL::Parser::basic_parse(new_tab_page_url); parsed_new_tab_page_url.has_value())
44+
page().client().set_new_tab_page_url(*parsed_new_tab_page_url);
45+
}
46+
47+
void Settings::load_available_search_engines()
48+
{
49+
page().client().request_available_search_engines();
50+
}
51+
52+
void Settings::set_search_engine(Optional<String> const& search_engine)
53+
{
54+
page().client().set_search_engine(search_engine);
55+
}
56+
57+
}

‎Libraries/LibWeb/Internals/Settings.h

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#pragma once
8+
9+
#include <LibWeb/Internals/InternalsBase.h>
10+
11+
namespace Web::Internals {
12+
13+
class Settings final : public InternalsBase {
14+
WEB_PLATFORM_OBJECT(Settings, InternalsBase);
15+
GC_DECLARE_ALLOCATOR(Settings);
16+
17+
public:
18+
virtual ~Settings() override;
19+
20+
void load_current_settings();
21+
void restore_default_settings();
22+
23+
void set_new_tab_page_url(String const& new_tab_page_url);
24+
25+
void load_available_search_engines();
26+
void set_search_engine(Optional<String> const& search_engine);
27+
28+
private:
29+
explicit Settings(JS::Realm&);
30+
31+
virtual void initialize(JS::Realm&) override;
32+
};
33+
34+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[Exposed=Nobody]
2+
interface Settings {
3+
undefined loadCurrentSettings();
4+
undefined restoreDefaultSettings();
5+
6+
undefined setNewTabPageURL(USVString newTabPageURL);
7+
8+
undefined loadAvailableSearchEngines();
9+
undefined setSearchEngine(DOMString? search_engine);
10+
};

‎Libraries/LibWeb/Page/Page.h

+6
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,12 @@ class PageClient : public JS::Cell {
402402

403403
virtual void update_process_statistics() { }
404404

405+
virtual void request_current_settings() { }
406+
virtual void restore_default_settings() { }
407+
virtual void set_new_tab_page_url(URL::URL const&) { }
408+
virtual void request_available_search_engines() { }
409+
virtual void set_search_engine(Optional<String> const&) { }
410+
405411
virtual bool is_ready_to_paint() const = 0;
406412

407413
virtual DisplayListPlayerType display_list_player_type() const = 0;

‎Libraries/LibWeb/idl_files.cmake

+1
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ libweb_js_bindings(IndexedDB/IDBVersionChangeEvent)
267267
libweb_js_bindings(Internals/InternalAnimationTimeline)
268268
libweb_js_bindings(Internals/Internals)
269269
libweb_js_bindings(Internals/Processes)
270+
libweb_js_bindings(Internals/Settings)
270271
libweb_js_bindings(IntersectionObserver/IntersectionObserver)
271272
libweb_js_bindings(IntersectionObserver/IntersectionObserverEntry)
272273
libweb_js_bindings(MathML/MathMLElement)

‎Libraries/LibWebView/Application.cpp

+30
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
#include <AK/Debug.h>
8+
#include <AK/JsonArraySerializer.h>
89
#include <LibCore/ArgsParser.h>
910
#include <LibCore/Environment.h>
1011
#include <LibCore/StandardPaths.h>
@@ -336,6 +337,35 @@ void Application::send_updated_process_statistics_to_view(ViewImplementation& vi
336337
view.run_javascript(MUST(builder.to_string()));
337338
}
338339

340+
void Application::send_current_settings_to_view(ViewImplementation& view)
341+
{
342+
auto settings = m_settings.serialize_json();
343+
344+
StringBuilder builder;
345+
builder.append("settings.loadSettings(\""sv);
346+
builder.append_escaped_for_json(settings);
347+
builder.append("\");"sv);
348+
349+
view.run_javascript(MUST(builder.to_string()));
350+
}
351+
352+
void Application::send_available_search_engines_to_view(ViewImplementation& view)
353+
{
354+
StringBuilder engines;
355+
356+
auto serializer = MUST(JsonArraySerializer<>::try_create(engines));
357+
for (auto const& engine : search_engines())
358+
MUST(serializer.add(engine.name));
359+
MUST(serializer.finish());
360+
361+
StringBuilder builder;
362+
builder.append("settings.loadSearchEngines(\""sv);
363+
builder.append_escaped_for_json(engines.string_view());
364+
builder.append("\");"sv);
365+
366+
view.run_javascript(MUST(builder.to_string()));
367+
}
368+
339369
void Application::process_did_exit(Process&& process)
340370
{
341371
if (m_in_shutdown)

‎Libraries/LibWebView/Application.h

+3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ class Application : public DevTools::DevToolsDelegate {
6161

6262
void send_updated_process_statistics_to_view(ViewImplementation&);
6363

64+
void send_current_settings_to_view(ViewImplementation&);
65+
void send_available_search_engines_to_view(ViewImplementation&);
66+
6467
ErrorOr<LexicalPath> path_for_downloaded_file(StringView file) const;
6568

6669
enum class DevtoolsState {

‎Libraries/LibWebView/WebContentClient.cpp

+33
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,39 @@ void WebContentClient::update_process_statistics(u64 page_id)
670670
WebView::Application::the().send_updated_process_statistics_to_view(*view);
671671
}
672672

673+
void WebContentClient::request_current_settings(u64 page_id)
674+
{
675+
if (auto view = view_for_page_id(page_id); view.has_value())
676+
WebView::Application::the().send_current_settings_to_view(*view);
677+
}
678+
679+
void WebContentClient::restore_default_settings(u64 page_id)
680+
{
681+
WebView::Application::settings().restore_defaults();
682+
request_current_settings(page_id);
683+
}
684+
685+
void WebContentClient::set_new_tab_page_url(u64 page_id, URL::URL new_tab_page_url)
686+
{
687+
WebView::Application::settings().set_new_tab_page_url(move(new_tab_page_url));
688+
request_current_settings(page_id);
689+
}
690+
691+
void WebContentClient::request_available_search_engines(u64 page_id)
692+
{
693+
if (auto view = view_for_page_id(page_id); view.has_value())
694+
WebView::Application::the().send_available_search_engines_to_view(*view);
695+
}
696+
697+
void WebContentClient::set_search_engine(u64 page_id, Optional<String> search_engine)
698+
{
699+
WebView::Application::settings().set_search_engine(search_engine.map([](auto const& search_engine) {
700+
return search_engine.bytes_as_string_view();
701+
}));
702+
703+
request_current_settings(page_id);
704+
}
705+
673706
Optional<ViewImplementation&> WebContentClient::view_for_page_id(u64 page_id, SourceLocation location)
674707
{
675708
// Don't bother logging anything for the spare WebContent process. It will only receive a load notification for about:blank.

‎Libraries/LibWebView/WebContentClient.h

+5
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ class WebContentClient final
130130
virtual void did_allocate_backing_stores(u64 page_id, i32 front_bitmap_id, Gfx::ShareableBitmap, i32 back_bitmap_id, Gfx::ShareableBitmap) override;
131131
virtual Messages::WebContentClient::RequestWorkerAgentResponse request_worker_agent(u64 page_id) override;
132132
virtual void update_process_statistics(u64 page_id) override;
133+
virtual void request_current_settings(u64 page_id) override;
134+
virtual void restore_default_settings(u64 page_id) override;
135+
virtual void set_new_tab_page_url(u64 page_id, URL::URL new_tab_page_url) override;
136+
virtual void request_available_search_engines(u64 page_id) override;
137+
virtual void set_search_engine(u64 page_id, Optional<String> search_engine) override;
133138

134139
Optional<ViewImplementation&> view_for_page_id(u64, SourceLocation = SourceLocation::current());
135140

‎Services/WebContent/PageClient.cpp

+25
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,31 @@ void PageClient::update_process_statistics()
704704
client().async_update_process_statistics(m_id);
705705
}
706706

707+
void PageClient::request_current_settings()
708+
{
709+
client().async_request_current_settings(m_id);
710+
}
711+
712+
void PageClient::restore_default_settings()
713+
{
714+
client().async_restore_default_settings(m_id);
715+
}
716+
717+
void PageClient::set_new_tab_page_url(URL::URL const& new_tab_page_url)
718+
{
719+
client().async_set_new_tab_page_url(m_id, new_tab_page_url);
720+
}
721+
722+
void PageClient::request_available_search_engines()
723+
{
724+
client().async_request_available_search_engines(m_id);
725+
}
726+
727+
void PageClient::set_search_engine(Optional<String> const& search_engine)
728+
{
729+
client().async_set_search_engine(m_id, search_engine);
730+
}
731+
707732
ErrorOr<void> PageClient::connect_to_webdriver(ByteString const& webdriver_ipc_path)
708733
{
709734
VERIFY(!m_webdriver);

‎Services/WebContent/PageClient.h

+5
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ class PageClient final : public Web::PageClient {
175175
virtual IPC::File request_worker_agent() override;
176176
virtual void page_did_mutate_dom(FlyString const& type, Web::DOM::Node const& target, Web::DOM::NodeList& added_nodes, Web::DOM::NodeList& removed_nodes, GC::Ptr<Web::DOM::Node> previous_sibling, GC::Ptr<Web::DOM::Node> next_sibling, Optional<String> const& attribute_name) override;
177177
virtual void update_process_statistics() override;
178+
virtual void request_current_settings() override;
179+
virtual void restore_default_settings() override;
180+
virtual void set_new_tab_page_url(URL::URL const&) override;
181+
virtual void request_available_search_engines() override;
182+
virtual void set_search_engine(Optional<String> const&) override;
178183

179184
Web::Layout::Viewport* layout_root();
180185
void setup_palette();

‎Services/WebContent/WebContentClient.ipc

+6
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,10 @@ endpoint WebContentClient
111111
request_worker_agent(u64 page_id) => (IPC::File socket) // FIXME: Add required attributes to select a SharedWorker Agent
112112

113113
update_process_statistics(u64 page_id) =|
114+
115+
request_current_settings(u64 page_id) =|
116+
restore_default_settings(u64 page_id) =|
117+
set_new_tab_page_url(u64 page_id, URL::URL new_tab_page_url) =|
118+
request_available_search_engines(u64 page_id) =|
119+
set_search_engine(u64 page_id, Optional<String> search_engine) =|
114120
}

‎UI/cmake/ResourceFiles.cmake

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ set(ABOUT_PAGES
7070
about.html
7171
newtab.html
7272
processes.html
73+
settings.html
7374
)
7475
list(TRANSFORM ABOUT_PAGES PREPEND "${LADYBIRD_SOURCE_DIR}/Base/res/ladybird/about-pages/")
7576

0 commit comments

Comments
 (0)
Please sign in to comment.