diff --git a/Base/res/ladybird/about-pages/bookmarks.html b/Base/res/ladybird/about-pages/bookmarks.html
new file mode 100644
index 00000000000..b620d3adb16
--- /dev/null
+++ b/Base/res/ladybird/about-pages/bookmarks.html
@@ -0,0 +1,468 @@
+
+
+
+ Bookmark Manager
+
+
+
+
+
+
+
+
+
+
+ Bookmarks
+
+
+
+
+
+
+
diff --git a/Libraries/LibURL/URL.h b/Libraries/LibURL/URL.h
index f80e65851b4..62ba35ffbc4 100644
--- a/Libraries/LibURL/URL.h
+++ b/Libraries/LibURL/URL.h
@@ -219,13 +219,14 @@ inline URL about_srcdoc() { return URL::about("srcdoc"_string); }
inline URL about_error() { return URL::about("error"_string); }
inline URL about_newtab() { return URL::about("newtab"_string); }
+inline URL about_bookmarks() { return URL::about("bookmarks"_string); }
inline URL about_processes() { return URL::about("processes"_string); }
inline URL about_settings() { return URL::about("settings"_string); }
inline URL about_version() { return URL::about("version"_string); }
inline bool is_webui_url(URL const& url)
{
- return first_is_one_of(url, about_processes(), about_settings());
+ return first_is_one_of(url, about_bookmarks(), about_processes(), about_settings());
}
}
diff --git a/Libraries/LibWebView/Application.cpp b/Libraries/LibWebView/Application.cpp
index 0d5158801b2..b1b6aea51f0 100644
--- a/Libraries/LibWebView/Application.cpp
+++ b/Libraries/LibWebView/Application.cpp
@@ -934,6 +934,10 @@ void Application::initialize_actions()
m_motion_menu->items().first().get>()->set_checked(true);
m_bookmarks_menu = Menu::create("Bookmarks"sv);
+ m_bookmarks_menu->add_action(Action::create("Manage Bookmarks"sv, ActionID::ManageBookmarks, [this]() {
+ open_url_in_new_tab(URL::about_bookmarks(), Web::HTML::ActivateTab::Yes);
+ }));
+ m_bookmarks_menu->add_separator();
m_toggle_bookmark_action = Action::create("Toggle Bookmark"sv, ActionID::ToggleBookmark, [this]() {
auto view = active_web_view();
diff --git a/Libraries/LibWebView/Application.h b/Libraries/LibWebView/Application.h
index 67282293a69..c2188bd2fec 100644
--- a/Libraries/LibWebView/Application.h
+++ b/Libraries/LibWebView/Application.h
@@ -67,6 +67,8 @@ public:
void bookmarks_changed(Badge);
void show_bookmarks_bar_changed(Badge);
+ virtual void show_bookmark_context_menu(Gfx::IntPoint, Optional, [[maybe_unused]] Optional target_folder_id) { }
+
static CookieJar& cookie_jar() { return *the().m_cookie_jar; }
static StorageJar& storage_jar() { return *the().m_storage_jar; }
diff --git a/Libraries/LibWebView/CMakeLists.txt b/Libraries/LibWebView/CMakeLists.txt
index 10c14c0e53a..c419e46b56e 100644
--- a/Libraries/LibWebView/CMakeLists.txt
+++ b/Libraries/LibWebView/CMakeLists.txt
@@ -29,6 +29,7 @@ set(SOURCES
ViewImplementation.cpp
WebContentClient.cpp
WebUI.cpp
+ WebUI/BookmarksUI.cpp
WebUI/ProcessesUI.cpp
WebUI/SettingsUI.cpp
)
diff --git a/Libraries/LibWebView/Forward.h b/Libraries/LibWebView/Forward.h
index 42afdc83fb3..af64f7b7b16 100644
--- a/Libraries/LibWebView/Forward.h
+++ b/Libraries/LibWebView/Forward.h
@@ -27,6 +27,7 @@ class WebUI;
struct Attribute;
struct AutocompleteEngine;
+struct BookmarkItem;
struct BrowserOptions;
struct ConsoleOutput;
struct CookieStorageKey;
diff --git a/Libraries/LibWebView/Menu.h b/Libraries/LibWebView/Menu.h
index d19e2df71bf..376bb7fa065 100644
--- a/Libraries/LibWebView/Menu.h
+++ b/Libraries/LibWebView/Menu.h
@@ -37,6 +37,7 @@ enum class ActionID {
TakeVisibleScreenshot,
TakeFullScreenshot,
+ ManageBookmarks,
ToggleBookmark,
ToggleBookmarkViaToolbar,
ToggleBookmarksBar,
diff --git a/Libraries/LibWebView/WebUI.cpp b/Libraries/LibWebView/WebUI.cpp
index d016f75787b..21125d25444 100644
--- a/Libraries/LibWebView/WebUI.cpp
+++ b/Libraries/LibWebView/WebUI.cpp
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
#include
#include
@@ -29,7 +30,9 @@ ErrorOr> WebUI::create(WebContentClient& client, String host)
{
RefPtr web_ui;
- if (host == "processes"sv)
+ if (host == "bookmarks"sv)
+ web_ui = TRY(create_web_ui(client, move(host)));
+ else if (host == "processes"sv)
web_ui = TRY(create_web_ui(client, move(host)));
else if (host == "settings"sv)
web_ui = TRY(create_web_ui(client, move(host)));
diff --git a/Libraries/LibWebView/WebUI/BookmarksUI.cpp b/Libraries/LibWebView/WebUI/BookmarksUI.cpp
new file mode 100644
index 00000000000..b740f730cbf
--- /dev/null
+++ b/Libraries/LibWebView/WebUI/BookmarksUI.cpp
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2026, Tim Flynn
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include
+#include
+#include
+
+namespace WebView {
+
+void BookmarksUI::register_interfaces()
+{
+ register_interface("loadBookmarks"sv, [this](auto const&) {
+ load_bookmarks();
+ });
+ register_interface("moveItem"sv, [this](auto const& data) {
+ move_item(data);
+ });
+ register_interface("showContextMenu"sv, [this](auto const& data) {
+ show_context_menu(data);
+ });
+}
+
+void BookmarksUI::bookmarks_changed()
+{
+ load_bookmarks();
+}
+
+void BookmarksUI::load_bookmarks()
+{
+ async_send_message("loadBookmarks"sv, Application::bookmark_store().serialize_items());
+}
+
+void BookmarksUI::move_item(JsonValue const& data)
+{
+ if (!data.is_object())
+ return;
+ auto const& object = data.as_object();
+
+ auto id = object.get_string("id"sv);
+ auto index = object.get_integer("index"sv);
+ if (!id.has_value() || !index.has_value())
+ return;
+
+ auto target_folder_id = object.get_string("targetFolderId"sv);
+ Application::bookmark_store().move_item(*id, target_folder_id, *index);
+}
+
+void BookmarksUI::show_context_menu(JsonValue const& data)
+{
+ if (!data.is_object())
+ return;
+ auto const& object = data.as_object();
+
+ auto client_x = object.get_integer("clientX"sv);
+ auto client_y = object.get_integer("clientY"sv);
+ if (!client_x.has_value() || !client_y.has_value())
+ return;
+
+ if (auto id = object.get_string("id"sv); id.has_value()) {
+ auto item = Application::bookmark_store().find_item_by_id(*id);
+ auto target_folder_id = object.get_string("targetFolderId"sv);
+
+ Application::the().show_bookmark_context_menu({ *client_x, *client_y }, item, target_folder_id);
+ } else {
+ Application::the().show_bookmark_context_menu({ *client_x, *client_y }, {}, {});
+ }
+}
+
+}
diff --git a/Libraries/LibWebView/WebUI/BookmarksUI.h b/Libraries/LibWebView/WebUI/BookmarksUI.h
new file mode 100644
index 00000000000..6ddf13e1504
--- /dev/null
+++ b/Libraries/LibWebView/WebUI/BookmarksUI.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2026, Tim Flynn
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include
+#include
+
+namespace WebView {
+
+class BookmarksUI
+ : public WebUI
+ , public BookmarkStoreObserver {
+ WEB_UI(BookmarksUI);
+
+private:
+ virtual void register_interfaces() override;
+ virtual void bookmarks_changed() override;
+
+ void load_bookmarks();
+ void move_item(JsonValue const&);
+ void show_context_menu(JsonValue const&);
+};
+
+}
diff --git a/UI/AppKit/Application/Application.h b/UI/AppKit/Application/Application.h
index 50bb3d87919..a41966db635 100644
--- a/UI/AppKit/Application/Application.h
+++ b/UI/AppKit/Application/Application.h
@@ -33,6 +33,7 @@ private:
virtual void rebuild_bookmarks_menu() const override;
virtual void update_bookmarks_bar_display(bool) const override;
+ virtual void show_bookmark_context_menu(Gfx::IntPoint, Optional, Optional target_folder_id) override;
virtual Optional bookmark_item_id_for_context_menu() const override;
virtual NonnullRefPtr display_add_bookmark_dialog() const override;
virtual NonnullRefPtr display_edit_bookmark_dialog(WebView::BookmarkItem::Bookmark const& current_bookmark) const override;
diff --git a/UI/AppKit/Application/Application.mm b/UI/AppKit/Application/Application.mm
index bb2d55a476d..30759182cc2 100644
--- a/UI/AppKit/Application/Application.mm
+++ b/UI/AppKit/Application/Application.mm
@@ -166,6 +166,18 @@ void Application::update_bookmarks_bar_display(bool show_bookmarks_bar) const
[delegate updateBookmarksBarDisplay:show_bookmarks_bar];
}
+void Application::show_bookmark_context_menu(Gfx::IntPoint content_position, Optional item, Optional target_folder_id)
+{
+ ApplicationDelegate* delegate = [NSApp delegate];
+
+ if (auto* tab = [delegate activeTab]) {
+ [[tab bookmarksBar] showContextMenu:content_position
+ view:[tab web_view]
+ bookmarkItem:item
+ targetFolderID:target_folder_id];
+ }
+}
+
Optional Application::bookmark_item_id_for_context_menu() const
{
ApplicationDelegate* delegate = [NSApp delegate];
diff --git a/UI/AppKit/Interface/BookmarksBar.h b/UI/AppKit/Interface/BookmarksBar.h
index cb052494e57..9b6b0de52b8 100644
--- a/UI/AppKit/Interface/BookmarksBar.h
+++ b/UI/AppKit/Interface/BookmarksBar.h
@@ -6,6 +6,10 @@
#pragma once
+#include
+#include
+#include
+
#import
@class BookmarkFolderPopover;
@@ -20,6 +24,10 @@
- (void)bookmarkFolderDidClose:(BookmarkFolderPopover*)folder;
- (void)showContextMenu:(id)control event:(NSEvent*)event;
+- (void)showContextMenu:(Gfx::IntPoint)content_position
+ view:(NSView*)view
+ bookmarkItem:(Optional)item
+ targetFolderID:(Optional)target_folder_id;
@property (nonatomic, strong, readonly) NSString* selected_bookmark_menu_item_id;
@property (nonatomic, strong, readonly) NSString* selected_bookmark_menu_target_folder_id;
diff --git a/UI/AppKit/Interface/BookmarksBar.mm b/UI/AppKit/Interface/BookmarksBar.mm
index 599cc1d9147..d235ecce8ff 100644
--- a/UI/AppKit/Interface/BookmarksBar.mm
+++ b/UI/AppKit/Interface/BookmarksBar.mm
@@ -5,10 +5,12 @@
*/
#include
+#include
#include
#import
#import
+#import
#import
#import
@@ -287,6 +289,31 @@ static Optional find_bookmark_folder_by_id(WebView::Menu& menu,
[NSMenu popUpContextMenu:self.bookmark_folder_context_menu withEvent:event forView:control];
}
+- (void)showContextMenu:(Gfx::IntPoint)content_position
+ view:(NSView*)view
+ bookmarkItem:(Optional)item
+ targetFolderID:(Optional)target_folder_id
+{
+ auto* event = Ladybird::create_context_menu_mouse_event(view, content_position);
+
+ if (item.has_value()) {
+ self.selected_bookmark_menu_item_id = Ladybird::string_to_ns_string(item->id);
+ self.selected_bookmark_menu_target_folder_id = target_folder_id.has_value()
+ ? Ladybird::string_to_ns_string(*target_folder_id)
+ : nil;
+
+ if (item->is_bookmark())
+ [NSMenu popUpContextMenu:self.bookmark_context_menu withEvent:event forView:view];
+ else if (item->is_folder())
+ [NSMenu popUpContextMenu:self.bookmark_folder_context_menu withEvent:event forView:view];
+ } else {
+ self.selected_bookmark_menu_item_id = @"";
+ self.selected_bookmark_menu_target_folder_id = nil;
+
+ [NSMenu popUpContextMenu:self.bookmarks_bar_context_menu withEvent:event forView:view];
+ }
+}
+
- (void)showContextMenuForEvent:(NSEvent*)event
{
if (auto* button = [self bookmarkButtonForEvent:event]) {
diff --git a/UI/AppKit/Interface/Menu.mm b/UI/AppKit/Interface/Menu.mm
index ff55bd4ca65..07b8d0c20b1 100644
--- a/UI/AppKit/Interface/Menu.mm
+++ b/UI/AppKit/Interface/Menu.mm
@@ -236,6 +236,9 @@ static void initialize_native_icon(WebView::Action& action, id control)
set_control_image(control, @"magnifyingglass");
break;
+ case WebView::ActionID::ManageBookmarks:
+ set_control_image(control, @"bookmark");
+ break;
case WebView::ActionID::ToggleBookmark:
[control setKeyEquivalent:@"d"];
break;
diff --git a/UI/Qt/Application.cpp b/UI/Qt/Application.cpp
index 5581f06220e..b4e2f8cb773 100644
--- a/UI/Qt/Application.cpp
+++ b/UI/Qt/Application.cpp
@@ -10,6 +10,7 @@
#include
#include
#include
+#include
#include
#include
@@ -248,6 +249,14 @@ void Application::update_bookmarks_bar_display(bool show_bookmarks_bar) const
}
}
+void Application::show_bookmark_context_menu(Gfx::IntPoint content_position, Optional item, Optional target_folder_id)
+{
+ if (auto* active_tab = this->active_tab()) {
+ auto position = active_tab->view().mapToGlobal(QPoint { content_position.x(), content_position.y() });
+ active_tab->bookmarks_bar().show_context_menu(position, item, target_folder_id);
+ }
+}
+
Optional Application::bookmark_item_id_for_context_menu() const
{
if (auto* active_tab = this->active_tab()) {
diff --git a/UI/Qt/Application.h b/UI/Qt/Application.h
index c0f5546c4e4..174b2a3a74a 100644
--- a/UI/Qt/Application.h
+++ b/UI/Qt/Application.h
@@ -49,6 +49,7 @@ private:
virtual void rebuild_bookmarks_menu() const override;
virtual void update_bookmarks_bar_display(bool) const override;
+ virtual void show_bookmark_context_menu(Gfx::IntPoint, Optional, Optional target_folder_id) override;
virtual Optional bookmark_item_id_for_context_menu() const override;
virtual NonnullRefPtr display_add_bookmark_dialog() const override;
virtual NonnullRefPtr display_edit_bookmark_dialog(WebView::BookmarkItem::Bookmark const& current_bookmark) const override;
diff --git a/UI/Qt/BookmarksBar.cpp b/UI/Qt/BookmarksBar.cpp
index 04e2aa0adeb..e320536ffbd 100644
--- a/UI/Qt/BookmarksBar.cpp
+++ b/UI/Qt/BookmarksBar.cpp
@@ -5,6 +5,7 @@
*/
#include
+#include
#include
#include
#include
@@ -95,6 +96,24 @@ void BookmarksBar::rebuild()
}
}
+void BookmarksBar::show_context_menu(QPoint position, Optional item, Optional target_folder_id)
+{
+ if (item.has_value()) {
+ m_selected_bookmark_menu_item_id = item->id;
+ m_selected_bookmark_menu_target_folder_id = target_folder_id.copy();
+
+ if (item->is_bookmark())
+ bookmark_context_menu().exec(position);
+ else if (item->is_folder())
+ bookmark_folder_context_menu().exec(position);
+ } else {
+ m_selected_bookmark_menu_item_id = {};
+ m_selected_bookmark_menu_target_folder_id = {};
+
+ bookmarks_bar_context_menu().exec(position);
+ }
+}
+
bool BookmarksBar::eventFilter(QObject* object, QEvent* event)
{
if (event->type() == QEvent::MouseButtonPress) {
diff --git a/UI/Qt/BookmarksBar.h b/UI/Qt/BookmarksBar.h
index a491c535fd4..8f2ca6acb56 100644
--- a/UI/Qt/BookmarksBar.h
+++ b/UI/Qt/BookmarksBar.h
@@ -8,6 +8,7 @@
#include
#include
+#include
#include
@@ -24,6 +25,8 @@ public:
String const& selected_bookmark_menu_item_id() const { return m_selected_bookmark_menu_item_id; }
Optional const& selected_bookmark_menu_target_folder_id() const { return m_selected_bookmark_menu_target_folder_id; }
+ void show_context_menu(QPoint, Optional, Optional target_folder_id);
+
private:
virtual bool eventFilter(QObject* object, QEvent* event) override;
diff --git a/UI/cmake/ResourceFiles.cmake b/UI/cmake/ResourceFiles.cmake
index 8d9fac359a3..ed020f13135 100644
--- a/UI/cmake/ResourceFiles.cmake
+++ b/UI/cmake/ResourceFiles.cmake
@@ -70,6 +70,7 @@ list(TRANSFORM INTERNAL_RESOURCES PREPEND "${LADYBIRD_SOURCE_DIR}/Base/res/ladyb
set(ABOUT_PAGES
about.html
+ bookmarks.html
newtab.html
processes.html
settings.html