Everywhere: Move the Ladybird folder to UI

This commit is contained in:
Timothy Flynn 2024-11-09 12:50:33 -05:00 committed by Andreas Kling
commit db47cc41f8
Notes: github-actions[bot] 2024-11-10 11:51:45 +00:00
203 changed files with 266 additions and 244 deletions

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <LibIPC/Forward.h>
#include <LibMain/Main.h>
#include <LibURL/URL.h>
#include <LibWebView/Forward.h>
#import <Cocoa/Cocoa.h>
namespace Ladybird {
class WebViewBridge;
}
@interface Application : NSApplication
- (void)setupWebViewApplication:(Main::Arguments&)arguments
newTabPageURL:(URL::URL)new_tab_page_url;
- (ErrorOr<void>)launchRequestServer;
- (ErrorOr<void>)launchImageDecoder;
- (ErrorOr<NonnullRefPtr<WebView::WebContentClient>>)launchWebContent:(Ladybird::WebViewBridge&)web_view_bridge;
- (ErrorOr<IPC::File>)launchWebWorker;
@end

View file

@ -0,0 +1,156 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <Interface/LadybirdWebViewBridge.h>
#include <LibCore/EventLoop.h>
#include <LibCore/ThreadEventQueue.h>
#include <LibImageDecoderClient/Client.h>
#include <LibRequests/RequestClient.h>
#include <LibWebView/Application.h>
#include <LibWebView/WebContentClient.h>
#include <UI/HelperProcess.h>
#include <UI/Utilities.h>
#include <Utilities/Conversions.h>
#import <Application/Application.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
namespace Ladybird {
class ApplicationBridge : public WebView::Application {
WEB_VIEW_APPLICATION(ApplicationBridge)
private:
virtual Optional<ByteString> ask_user_for_download_folder() const override
{
auto* panel = [NSOpenPanel openPanel];
[panel setAllowsMultipleSelection:NO];
[panel setCanChooseDirectories:YES];
[panel setCanChooseFiles:NO];
[panel setMessage:@"Select download directory"];
if ([panel runModal] != NSModalResponseOK)
return {};
return Ladybird::ns_string_to_byte_string([[panel URL] path]);
}
};
ApplicationBridge::ApplicationBridge(Badge<WebView::Application>, Main::Arguments&)
{
}
}
@interface Application ()
{
OwnPtr<Ladybird::ApplicationBridge> m_application_bridge;
RefPtr<Requests::RequestClient> m_request_server_client;
RefPtr<ImageDecoderClient::Client> m_image_decoder_client;
}
@end
@implementation Application
#pragma mark - Public methods
- (void)setupWebViewApplication:(Main::Arguments&)arguments
newTabPageURL:(URL::URL)new_tab_page_url
{
m_application_bridge = Ladybird::ApplicationBridge::create(arguments, move(new_tab_page_url));
}
- (ErrorOr<void>)launchRequestServer
{
auto request_server_paths = TRY(get_paths_for_helper_process("RequestServer"sv));
m_request_server_client = TRY(launch_request_server_process(request_server_paths, s_ladybird_resource_root));
return {};
}
static ErrorOr<NonnullRefPtr<ImageDecoderClient::Client>> launch_new_image_decoder()
{
auto image_decoder_paths = TRY(get_paths_for_helper_process("ImageDecoder"sv));
return launch_image_decoder_process(image_decoder_paths);
}
- (ErrorOr<void>)launchImageDecoder
{
m_image_decoder_client = TRY(launch_new_image_decoder());
__weak Application* weak_self = self;
m_image_decoder_client->on_death = [weak_self]() {
Application* self = weak_self;
if (self == nil) {
return;
}
m_image_decoder_client = nullptr;
if (auto err = [self launchImageDecoder]; err.is_error()) {
dbgln("Failed to restart image decoder: {}", err.error());
VERIFY_NOT_REACHED();
}
auto num_clients = WebView::WebContentClient::client_count();
auto new_sockets = m_image_decoder_client->send_sync_but_allow_failure<Messages::ImageDecoderServer::ConnectNewClients>(num_clients);
if (!new_sockets || new_sockets->sockets().size() == 0) {
dbgln("Failed to connect {} new clients to ImageDecoder", num_clients);
VERIFY_NOT_REACHED();
}
WebView::WebContentClient::for_each_client([sockets = new_sockets->take_sockets()](WebView::WebContentClient& client) mutable {
client.async_connect_to_image_decoder(sockets.take_last());
return IterationDecision::Continue;
});
};
return {};
}
- (ErrorOr<NonnullRefPtr<WebView::WebContentClient>>)launchWebContent:(Ladybird::WebViewBridge&)web_view_bridge
{
// FIXME: Fail to open the tab, rather than crashing the whole application if this fails
auto request_server_socket = TRY(connect_new_request_server_client(*m_request_server_client));
auto image_decoder_socket = TRY(connect_new_image_decoder_client(*m_image_decoder_client));
auto web_content_paths = TRY(get_paths_for_helper_process("WebContent"sv));
auto web_content = TRY(launch_web_content_process(web_view_bridge, web_content_paths, move(image_decoder_socket), move(request_server_socket)));
return web_content;
}
- (ErrorOr<IPC::File>)launchWebWorker
{
auto web_worker_paths = TRY(get_paths_for_helper_process("WebWorker"sv));
auto worker_client = TRY(launch_web_worker_process(web_worker_paths, *m_request_server_client));
return worker_client->clone_transport();
}
#pragma mark - NSApplication
- (void)terminate:(id)sender
{
Core::EventLoop::current().quit(0);
}
- (void)sendEvent:(NSEvent*)event
{
if ([event type] == NSEventTypeApplicationDefined) {
Core::ThreadEventQueue::current().process();
} else {
[super sendEvent:event];
}
}
@end

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <AK/StringView.h>
#include <LibURL/URL.h>
#include <LibWeb/CSS/PreferredColorScheme.h>
#include <LibWeb/CSS/PreferredContrast.h>
#include <LibWeb/CSS/PreferredMotion.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWebView/Forward.h>
#import <Cocoa/Cocoa.h>
@class Tab;
@class TabController;
@interface ApplicationDelegate : NSObject <NSApplicationDelegate>
- (nullable instancetype)init;
- (nonnull TabController*)createNewTab:(Optional<URL::URL> const&)url
fromTab:(nullable Tab*)tab
activateTab:(Web::HTML::ActivateTab)activate_tab;
- (nonnull TabController*)createNewTab:(StringView)html
url:(URL::URL const&)url
fromTab:(nullable Tab*)tab
activateTab:(Web::HTML::ActivateTab)activate_tab;
- (nonnull TabController*)createChildTab:(Optional<URL::URL> const&)url
fromTab:(nonnull Tab*)tab
activateTab:(Web::HTML::ActivateTab)activate_tab
pageIndex:(u64)page_index;
- (void)setActiveTab:(nonnull Tab*)tab;
- (nullable Tab*)activeTab;
- (void)removeTab:(nonnull TabController*)controller;
- (Web::CSS::PreferredColorScheme)preferredColorScheme;
- (Web::CSS::PreferredContrast)preferredContrast;
- (Web::CSS::PreferredMotion)preferredMotion;
- (WebView::SearchEngine const&)searchEngine;
@end

View file

@ -0,0 +1,802 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWebView/Application.h>
#include <LibWebView/CookieJar.h>
#include <LibWebView/SearchEngine.h>
#import <Application/ApplicationDelegate.h>
#import <Interface/LadybirdWebView.h>
#import <Interface/Tab.h>
#import <Interface/TabController.h>
#import <LibWebView/UserAgent.h>
#if defined(LADYBIRD_USE_SWIFT)
// FIXME: Report this codegen error to Apple
# define StyleMask NSWindowStyleMask
# import <Ladybird-Swift.h>
# undef StyleMask
#else
# import <Interface/TaskManagerController.h>
#endif
#import <Utilities/Conversions.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
@interface ApplicationDelegate () <TaskManagerDelegate>
{
Web::CSS::PreferredColorScheme m_preferred_color_scheme;
Web::CSS::PreferredContrast m_preferred_contrast;
Web::CSS::PreferredMotion m_preferred_motion;
ByteString m_navigator_compatibility_mode;
WebView::SearchEngine m_search_engine;
}
@property (nonatomic, strong) NSMutableArray<TabController*>* managed_tabs;
@property (nonatomic, weak) Tab* active_tab;
@property (nonatomic, strong) TaskManagerController* task_manager_controller;
- (NSMenuItem*)createApplicationMenu;
- (NSMenuItem*)createFileMenu;
- (NSMenuItem*)createEditMenu;
- (NSMenuItem*)createViewMenu;
- (NSMenuItem*)createSettingsMenu;
- (NSMenuItem*)createHistoryMenu;
- (NSMenuItem*)createInspectMenu;
- (NSMenuItem*)createDebugMenu;
- (NSMenuItem*)createWindowMenu;
- (NSMenuItem*)createHelpMenu;
@end
@implementation ApplicationDelegate
- (instancetype)init
{
if (self = [super init]) {
[NSApp setMainMenu:[[NSMenu alloc] init]];
[[NSApp mainMenu] addItem:[self createApplicationMenu]];
[[NSApp mainMenu] addItem:[self createFileMenu]];
[[NSApp mainMenu] addItem:[self createEditMenu]];
[[NSApp mainMenu] addItem:[self createViewMenu]];
[[NSApp mainMenu] addItem:[self createSettingsMenu]];
[[NSApp mainMenu] addItem:[self createHistoryMenu]];
[[NSApp mainMenu] addItem:[self createInspectMenu]];
[[NSApp mainMenu] addItem:[self createDebugMenu]];
[[NSApp mainMenu] addItem:[self createWindowMenu]];
[[NSApp mainMenu] addItem:[self createHelpMenu]];
self.managed_tabs = [[NSMutableArray alloc] init];
m_preferred_color_scheme = Web::CSS::PreferredColorScheme::Auto;
m_preferred_contrast = Web::CSS::PreferredContrast::Auto;
m_preferred_motion = Web::CSS::PreferredMotion::Auto;
m_navigator_compatibility_mode = "chrome";
m_search_engine = WebView::default_search_engine();
// Reduce the tooltip delay, as the default delay feels quite long.
[[NSUserDefaults standardUserDefaults] setObject:@100 forKey:@"NSInitialToolTipDelay"];
}
return self;
}
#pragma mark - Public methods
- (TabController*)createNewTab:(Optional<URL::URL> const&)url
fromTab:(Tab*)tab
activateTab:(Web::HTML::ActivateTab)activate_tab
{
auto* controller = [self createNewTab:activate_tab fromTab:tab];
if (url.has_value()) {
[controller loadURL:*url];
}
return controller;
}
- (nonnull TabController*)createNewTab:(StringView)html
url:(URL::URL const&)url
fromTab:(nullable Tab*)tab
activateTab:(Web::HTML::ActivateTab)activate_tab
{
auto* controller = [self createNewTab:activate_tab fromTab:tab];
[controller loadHTML:html url:url];
return controller;
}
- (nonnull TabController*)createChildTab:(Optional<URL::URL> const&)url
fromTab:(nonnull Tab*)tab
activateTab:(Web::HTML::ActivateTab)activate_tab
pageIndex:(u64)page_index
{
auto* controller = [self createChildTab:activate_tab fromTab:tab pageIndex:page_index];
if (url.has_value()) {
[controller loadURL:*url];
}
return controller;
}
- (void)setActiveTab:(Tab*)tab
{
self.active_tab = tab;
}
- (Tab*)activeTab
{
return self.active_tab;
}
- (void)removeTab:(TabController*)controller
{
[self.managed_tabs removeObject:controller];
if ([self.managed_tabs count] == 0u) {
if (self.task_manager_controller != nil) {
[self.task_manager_controller.window close];
}
}
}
- (Web::CSS::PreferredColorScheme)preferredColorScheme
{
return m_preferred_color_scheme;
}
- (Web::CSS::PreferredContrast)preferredContrast
{
return m_preferred_contrast;
}
- (Web::CSS::PreferredMotion)preferredMotion
{
return m_preferred_motion;
}
- (WebView::SearchEngine const&)searchEngine
{
return m_search_engine;
}
#pragma mark - Private methods
- (void)openAboutVersionPage:(id)sender
{
auto* current_tab = [NSApp keyWindow];
if (![current_tab isKindOfClass:[Tab class]]) {
return;
}
[self createNewTab:URL::URL("about:version"sv)
fromTab:(Tab*)current_tab
activateTab:Web::HTML::ActivateTab::Yes];
}
- (nonnull TabController*)createNewTab:(Web::HTML::ActivateTab)activate_tab
fromTab:(nullable Tab*)tab
{
auto* controller = [[TabController alloc] init];
[self initializeTabController:controller
activateTab:activate_tab
fromTab:tab];
return controller;
}
- (nonnull TabController*)createChildTab:(Web::HTML::ActivateTab)activate_tab
fromTab:(nonnull Tab*)tab
pageIndex:(u64)page_index
{
auto* controller = [[TabController alloc] initAsChild:tab pageIndex:page_index];
[self initializeTabController:controller
activateTab:activate_tab
fromTab:tab];
return controller;
}
- (void)initializeTabController:(TabController*)controller
activateTab:(Web::HTML::ActivateTab)activate_tab
fromTab:(nullable Tab*)tab
{
[controller showWindow:nil];
if (tab) {
[[tab tabGroup] addWindow:controller.window];
// FIXME: Can we create the tabbed window above without it becoming active in the first place?
if (activate_tab == Web::HTML::ActivateTab::No) {
[tab orderFront:nil];
}
}
if (activate_tab == Web::HTML::ActivateTab::Yes) {
[[controller window] orderFrontRegardless];
}
[self.managed_tabs addObject:controller];
[controller onCreateNewTab];
}
- (void)closeCurrentTab:(id)sender
{
auto* current_window = [NSApp keyWindow];
[current_window close];
}
- (void)openTaskManager:(id)sender
{
if (self.task_manager_controller != nil) {
[self.task_manager_controller.window makeKeyAndOrderFront:sender];
return;
}
self.task_manager_controller = [[TaskManagerController alloc] initWithDelegate:self];
[self.task_manager_controller showWindow:nil];
}
- (void)openLocation:(id)sender
{
auto* current_tab = [NSApp keyWindow];
if (![current_tab isKindOfClass:[Tab class]]) {
return;
}
auto* controller = (TabController*)[current_tab windowController];
[controller focusLocationToolbarItem];
}
- (void)setAutoPreferredColorScheme:(id)sender
{
m_preferred_color_scheme = Web::CSS::PreferredColorScheme::Auto;
[self broadcastPreferredColorSchemeUpdate];
}
- (void)setDarkPreferredColorScheme:(id)sender
{
m_preferred_color_scheme = Web::CSS::PreferredColorScheme::Dark;
[self broadcastPreferredColorSchemeUpdate];
}
- (void)setLightPreferredColorScheme:(id)sender
{
m_preferred_color_scheme = Web::CSS::PreferredColorScheme::Light;
[self broadcastPreferredColorSchemeUpdate];
}
- (void)broadcastPreferredColorSchemeUpdate
{
for (TabController* controller in self.managed_tabs) {
auto* tab = (Tab*)[controller window];
[[tab web_view] setPreferredColorScheme:m_preferred_color_scheme];
}
}
- (void)setAutoPreferredContrast:(id)sender
{
m_preferred_contrast = Web::CSS::PreferredContrast::Auto;
[self broadcastPreferredContrastUpdate];
}
- (void)setLessPreferredContrast:(id)sender
{
m_preferred_contrast = Web::CSS::PreferredContrast::Less;
[self broadcastPreferredContrastUpdate];
}
- (void)setMorePreferredContrast:(id)sender
{
m_preferred_contrast = Web::CSS::PreferredContrast::More;
[self broadcastPreferredContrastUpdate];
}
- (void)setNoPreferencePreferredContrast:(id)sender
{
m_preferred_contrast = Web::CSS::PreferredContrast::NoPreference;
[self broadcastPreferredContrastUpdate];
}
- (void)broadcastPreferredContrastUpdate
{
for (TabController* controller in self.managed_tabs) {
auto* tab = (Tab*)[controller window];
[[tab web_view] setPreferredContrast:m_preferred_contrast];
}
}
- (void)setAutoPreferredMotion:(id)sender
{
m_preferred_motion = Web::CSS::PreferredMotion::Auto;
[self broadcastPreferredMotionUpdate];
}
- (void)setNoPreferencePreferredMotion:(id)sender
{
m_preferred_motion = Web::CSS::PreferredMotion::NoPreference;
[self broadcastPreferredMotionUpdate];
}
- (void)setReducePreferredMotion:(id)sender
{
m_preferred_motion = Web::CSS::PreferredMotion::Reduce;
[self broadcastPreferredMotionUpdate];
}
- (void)broadcastPreferredMotionUpdate
{
for (TabController* controller in self.managed_tabs) {
auto* tab = (Tab*)[controller window];
[[tab web_view] setPreferredMotion:m_preferred_motion];
}
}
- (void)setSearchEngine:(id)sender
{
auto* item = (NSMenuItem*)sender;
auto title = Ladybird::ns_string_to_string([item title]);
if (auto search_engine = WebView::find_search_engine_by_name(title); search_engine.has_value())
m_search_engine = search_engine.release_value();
else
m_search_engine = WebView::default_search_engine();
}
- (void)clearHistory:(id)sender
{
for (TabController* controller in self.managed_tabs) {
[controller clearHistory];
}
}
- (void)dumpCookies:(id)sender
{
WebView::Application::cookie_jar().dump_cookies();
}
- (NSMenuItem*)createApplicationMenu
{
auto* menu = [[NSMenuItem alloc] init];
auto* process_name = [[NSProcessInfo processInfo] processName];
auto* submenu = [[NSMenu alloc] initWithTitle:process_name];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:@"About %@", process_name]
action:@selector(openAboutVersionPage:)
keyEquivalent:@""]];
[submenu addItem:[NSMenuItem separatorItem]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:@"Hide %@", process_name]
action:@selector(hide:)
keyEquivalent:@"h"]];
[submenu addItem:[NSMenuItem separatorItem]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:@"Quit %@", process_name]
action:@selector(terminate:)
keyEquivalent:@"q"]];
[menu setSubmenu:submenu];
return menu;
}
- (NSMenuItem*)createFileMenu
{
auto* menu = [[NSMenuItem alloc] init];
auto* submenu = [[NSMenu alloc] initWithTitle:@"File"];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"New Tab"
action:@selector(createNewTab:)
keyEquivalent:@"t"]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Close Tab"
action:@selector(closeCurrentTab:)
keyEquivalent:@"w"]];
[submenu addItem:[NSMenuItem separatorItem]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Location"
action:@selector(openLocation:)
keyEquivalent:@"l"]];
[menu setSubmenu:submenu];
return menu;
}
- (NSMenuItem*)createEditMenu
{
auto* menu = [[NSMenuItem alloc] init];
auto* submenu = [[NSMenu alloc] initWithTitle:@"Edit"];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Undo"
action:@selector(undo:)
keyEquivalent:@"z"]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Redo"
action:@selector(redo:)
keyEquivalent:@"y"]];
[submenu addItem:[NSMenuItem separatorItem]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Cut"
action:@selector(cut:)
keyEquivalent:@"x"]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Copy"
action:@selector(copy:)
keyEquivalent:@"c"]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Paste"
action:@selector(paste:)
keyEquivalent:@"v"]];
[submenu addItem:[NSMenuItem separatorItem]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Select All"
action:@selector(selectAll:)
keyEquivalent:@"a"]];
[submenu addItem:[NSMenuItem separatorItem]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Find..."
action:@selector(find:)
keyEquivalent:@"f"]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Find Next"
action:@selector(findNextMatch:)
keyEquivalent:@"g"]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Find Previous"
action:@selector(findPreviousMatch:)
keyEquivalent:@"G"]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Use Selection for Find"
action:@selector(useSelectionForFind:)
keyEquivalent:@"e"]];
[menu setSubmenu:submenu];
return menu;
}
- (NSMenuItem*)createViewMenu
{
auto* menu = [[NSMenuItem alloc] init];
auto* submenu = [[NSMenu alloc] initWithTitle:@"View"];
auto* color_scheme_menu = [[NSMenu alloc] init];
[color_scheme_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Auto"
action:@selector(setAutoPreferredColorScheme:)
keyEquivalent:@""]];
[color_scheme_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Dark"
action:@selector(setDarkPreferredColorScheme:)
keyEquivalent:@""]];
[color_scheme_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Light"
action:@selector(setLightPreferredColorScheme:)
keyEquivalent:@""]];
auto* color_scheme_menu_item = [[NSMenuItem alloc] initWithTitle:@"Color Scheme"
action:nil
keyEquivalent:@""];
[color_scheme_menu_item setSubmenu:color_scheme_menu];
auto* contrast_menu = [[NSMenu alloc] init];
[contrast_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Auto"
action:@selector(setAutoPreferredContrast:)
keyEquivalent:@""]];
[contrast_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Less"
action:@selector(setLessPreferredContrast:)
keyEquivalent:@""]];
[contrast_menu addItem:[[NSMenuItem alloc] initWithTitle:@"More"
action:@selector(setMorePreferredContrast:)
keyEquivalent:@""]];
[contrast_menu addItem:[[NSMenuItem alloc] initWithTitle:@"No Preference"
action:@selector(setNoPreferencePreferredContrast:)
keyEquivalent:@""]];
auto* contrast_menu_item = [[NSMenuItem alloc] initWithTitle:@"Contrast"
action:nil
keyEquivalent:@""];
[contrast_menu_item setSubmenu:contrast_menu];
auto* motion_menu = [[NSMenu alloc] init];
[motion_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Auto"
action:@selector(setAutoPreferredMotion:)
keyEquivalent:@""]];
[motion_menu addItem:[[NSMenuItem alloc] initWithTitle:@"No Preference"
action:@selector(setNoPreferencePreferredMotion:)
keyEquivalent:@""]];
[motion_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Reduce"
action:@selector(setReducePreferredMotion:)
keyEquivalent:@""]];
auto* motion_menu_item = [[NSMenuItem alloc] initWithTitle:@"Motion"
action:nil
keyEquivalent:@""];
[motion_menu_item setSubmenu:motion_menu];
auto* zoom_menu = [[NSMenu alloc] init];
[zoom_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Zoom In"
action:@selector(zoomIn:)
keyEquivalent:@"+"]];
[zoom_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Zoom Out"
action:@selector(zoomOut:)
keyEquivalent:@"-"]];
[zoom_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Actual Size"
action:@selector(resetZoom:)
keyEquivalent:@"0"]];
auto* zoom_menu_item = [[NSMenuItem alloc] initWithTitle:@"Zoom"
action:nil
keyEquivalent:@""];
[zoom_menu_item setSubmenu:zoom_menu];
[submenu addItem:color_scheme_menu_item];
[submenu addItem:contrast_menu_item];
[submenu addItem:motion_menu_item];
[submenu addItem:zoom_menu_item];
[submenu addItem:[NSMenuItem separatorItem]];
[menu setSubmenu:submenu];
return menu;
}
- (NSMenuItem*)createSettingsMenu
{
auto* menu = [[NSMenuItem alloc] init];
auto* submenu = [[NSMenu alloc] initWithTitle:@"Settings"];
auto* search_engine_menu = [[NSMenu alloc] init];
for (auto const& search_engine : WebView::search_engines()) {
[search_engine_menu addItem:[[NSMenuItem alloc] initWithTitle:Ladybird::string_to_ns_string(search_engine.name)
action:@selector(setSearchEngine:)
keyEquivalent:@""]];
}
auto* search_engine_menu_item = [[NSMenuItem alloc] initWithTitle:@"Search Engine"
action:nil
keyEquivalent:@""];
[search_engine_menu_item setSubmenu:search_engine_menu];
[submenu addItem:search_engine_menu_item];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Enable Autoplay"
action:@selector(toggleAutoplay:)
keyEquivalent:@""]];
[menu setSubmenu:submenu];
return menu;
}
- (NSMenuItem*)createHistoryMenu
{
auto* menu = [[NSMenuItem alloc] init];
auto* submenu = [[NSMenu alloc] initWithTitle:@"History"];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Reload Page"
action:@selector(reload:)
keyEquivalent:@"r"]];
[submenu addItem:[NSMenuItem separatorItem]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Navigate Back"
action:@selector(navigateBack:)
keyEquivalent:@"["]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Navigate Forward"
action:@selector(navigateForward:)
keyEquivalent:@"]"]];
[submenu addItem:[NSMenuItem separatorItem]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Clear History"
action:@selector(clearHistory:)
keyEquivalent:@""]];
[menu setSubmenu:submenu];
return menu;
}
- (NSMenuItem*)createInspectMenu
{
auto* menu = [[NSMenuItem alloc] init];
auto* submenu = [[NSMenu alloc] initWithTitle:@"Inspect"];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"View Source"
action:@selector(viewSource:)
keyEquivalent:@""]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Inspector"
action:@selector(openInspector:)
keyEquivalent:@"I"]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Task Manager"
action:@selector(openTaskManager:)
keyEquivalent:@"M"]];
[menu setSubmenu:submenu];
return menu;
}
- (NSMenuItem*)createDebugMenu
{
auto* menu = [[NSMenuItem alloc] init];
auto* submenu = [[NSMenu alloc] initWithTitle:@"Debug"];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump DOM Tree"
action:@selector(dumpDOMTree:)
keyEquivalent:@""]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump Layout Tree"
action:@selector(dumpLayoutTree:)
keyEquivalent:@""]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump Paint Tree"
action:@selector(dumpPaintTree:)
keyEquivalent:@""]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump Stacking Context Tree"
action:@selector(dumpStackingContextTree:)
keyEquivalent:@""]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump Style Sheets"
action:@selector(dumpStyleSheets:)
keyEquivalent:@""]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump All Resolved Styles"
action:@selector(dumpAllResolvedStyles:)
keyEquivalent:@""]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump History"
action:@selector(dumpHistory:)
keyEquivalent:@""]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump Cookies"
action:@selector(dumpCookies:)
keyEquivalent:@""]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump Local Storage"
action:@selector(dumpLocalStorage:)
keyEquivalent:@""]];
[submenu addItem:[NSMenuItem separatorItem]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Show Line Box Borders"
action:@selector(toggleLineBoxBorders:)
keyEquivalent:@""]];
[submenu addItem:[NSMenuItem separatorItem]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Collect Garbage"
action:@selector(collectGarbage:)
keyEquivalent:@""]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump GC Graph"
action:@selector(dumpGCGraph:)
keyEquivalent:@""]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Clear Cache"
action:@selector(clearCache:)
keyEquivalent:@""]];
[submenu addItem:[NSMenuItem separatorItem]];
auto* spoof_user_agent_menu = [[NSMenu alloc] init];
auto add_user_agent = [spoof_user_agent_menu](ByteString name) {
[spoof_user_agent_menu addItem:[[NSMenuItem alloc] initWithTitle:Ladybird::string_to_ns_string(name)
action:@selector(setUserAgentSpoof:)
keyEquivalent:@""]];
};
add_user_agent("Disabled");
for (auto const& userAgent : WebView::user_agents)
add_user_agent(userAgent.key);
auto* spoof_user_agent_menu_item = [[NSMenuItem alloc] initWithTitle:@"Spoof User Agent"
action:nil
keyEquivalent:@""];
[spoof_user_agent_menu_item setSubmenu:spoof_user_agent_menu];
[submenu addItem:spoof_user_agent_menu_item];
auto* navigator_compatibility_mode_menu = [[NSMenu alloc] init];
auto add_navigator_compatibility_mode = [navigator_compatibility_mode_menu](ByteString name) {
[navigator_compatibility_mode_menu addItem:[[NSMenuItem alloc] initWithTitle:Ladybird::string_to_ns_string(name)
action:@selector(setNavigatorCompatibilityMode:)
keyEquivalent:@""]];
};
add_navigator_compatibility_mode("Chrome");
add_navigator_compatibility_mode("Gecko");
add_navigator_compatibility_mode("WebKit");
auto* navigator_compatibility_mode_menu_item = [[NSMenuItem alloc] initWithTitle:@"Navigator Compatibility Mode"
action:nil
keyEquivalent:@""];
[navigator_compatibility_mode_menu_item setSubmenu:navigator_compatibility_mode_menu];
[submenu addItem:navigator_compatibility_mode_menu_item];
[submenu addItem:[NSMenuItem separatorItem]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Enable Scripting"
action:@selector(toggleScripting:)
keyEquivalent:@""]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Block Pop-ups"
action:@selector(togglePopupBlocking:)
keyEquivalent:@""]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Enable Same-Origin Policy"
action:@selector(toggleSameOriginPolicy:)
keyEquivalent:@""]];
[menu setSubmenu:submenu];
return menu;
}
- (NSMenuItem*)createWindowMenu
{
auto* menu = [[NSMenuItem alloc] init];
auto* submenu = [[NSMenu alloc] initWithTitle:@"Window"];
[NSApp setWindowsMenu:submenu];
[menu setSubmenu:submenu];
return menu;
}
- (NSMenuItem*)createHelpMenu
{
auto* menu = [[NSMenuItem alloc] init];
auto* submenu = [[NSMenu alloc] initWithTitle:@"Help"];
[NSApp setHelpMenu:submenu];
[menu setSubmenu:submenu];
return menu;
}
#pragma mark - NSApplicationDelegate
- (void)applicationDidFinishLaunching:(NSNotification*)notification
{
Tab* tab = nil;
for (auto const& url : WebView::Application::chrome_options().urls) {
auto activate_tab = tab == nil ? Web::HTML::ActivateTab::Yes : Web::HTML::ActivateTab::No;
auto* controller = [self createNewTab:url
fromTab:tab
activateTab:activate_tab];
tab = (Tab*)[controller window];
}
}
- (void)applicationWillTerminate:(NSNotification*)notification
{
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender
{
return YES;
}
- (BOOL)validateMenuItem:(NSMenuItem*)item
{
if ([item action] == @selector(setAutoPreferredColorScheme:)) {
[item setState:(m_preferred_color_scheme == Web::CSS::PreferredColorScheme::Auto) ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(setDarkPreferredColorScheme:)) {
[item setState:(m_preferred_color_scheme == Web::CSS::PreferredColorScheme::Dark) ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(setLightPreferredColorScheme:)) {
[item setState:(m_preferred_color_scheme == Web::CSS::PreferredColorScheme::Light) ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(setAutoPreferredContrast:)) {
[item setState:(m_preferred_contrast == Web::CSS::PreferredContrast::Auto) ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(setLessPreferredContrast:)) {
[item setState:(m_preferred_contrast == Web::CSS::PreferredContrast::Less) ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(setMorePreferredContrast:)) {
[item setState:(m_preferred_contrast == Web::CSS::PreferredContrast::More) ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(setNoPreferencePreferredContrast:)) {
[item setState:(m_preferred_contrast == Web::CSS::PreferredContrast::NoPreference) ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(setAutoPreferredMotion:)) {
[item setState:(m_preferred_motion == Web::CSS::PreferredMotion::Auto) ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(setNoPreferencePreferredMotion:)) {
[item setState:(m_preferred_motion == Web::CSS::PreferredMotion::NoPreference) ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(setReducePreferredMotion:)) {
[item setState:(m_preferred_motion == Web::CSS::PreferredMotion::Reduce) ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(setSearchEngine:)) {
auto title = Ladybird::ns_string_to_string([item title]);
[item setState:(m_search_engine.name == title) ? NSControlStateValueOn : NSControlStateValueOff];
}
return YES;
}
#pragma mark - TaskManagerDelegate
- (void)onTaskManagerClosed
{
self.task_manager_controller = nil;
}
@end

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Function.h>
#include <AK/NonnullOwnPtr.h>
#include <LibCore/EventLoopImplementation.h>
namespace Ladybird {
class CFEventLoopManager final : public Core::EventLoopManager {
public:
virtual NonnullOwnPtr<Core::EventLoopImplementation> make_implementation() override;
virtual intptr_t register_timer(Core::EventReceiver&, int interval_milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible) override;
virtual void unregister_timer(intptr_t timer_id) override;
virtual void register_notifier(Core::Notifier&) override;
virtual void unregister_notifier(Core::Notifier&) override;
virtual void did_post_event() override;
virtual int register_signal(int, Function<void(int)>) override;
virtual void unregister_signal(int) override;
};
class CFEventLoopImplementation final : public Core::EventLoopImplementation {
public:
// FIXME: This currently only manages the main NSApp event loop, as that is all we currently
// interact with. When we need multiple event loops, or an event loop that isn't the
// NSApp loop, we will need to create our own CFRunLoop.
static NonnullOwnPtr<CFEventLoopImplementation> create();
virtual int exec() override;
virtual size_t pump(PumpMode) override;
virtual void quit(int) override;
virtual void wake() override;
virtual void post_event(Core::EventReceiver& receiver, NonnullOwnPtr<Core::Event>&&) override;
// FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them.
virtual void unquit() override { }
virtual bool was_exit_requested() const override { return false; }
virtual void notify_forked_and_in_child() override { }
private:
CFEventLoopImplementation() = default;
int m_exit_code { 0 };
};
}

View file

@ -0,0 +1,414 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Assertions.h>
#include <AK/IDAllocator.h>
#include <AK/Singleton.h>
#include <AK/TemporaryChange.h>
#include <LibCore/Event.h>
#include <LibCore/Notifier.h>
#include <LibCore/ThreadEventQueue.h>
#import <Application/EventLoopImplementation.h>
#import <Cocoa/Cocoa.h>
#import <CoreFoundation/CoreFoundation.h>
#include <sys/event.h>
#include <sys/time.h>
#include <sys/types.h>
namespace Ladybird {
struct ThreadData {
static ThreadData& the()
{
static thread_local ThreadData s_thread_data;
return s_thread_data;
}
Core::Notifier& notifier_by_fd(int fd)
{
for (auto notifier : notifiers) {
if (notifier.key->fd() == fd)
return *notifier.key;
}
// If we didn't have a notifier for the provided FD, it should have been unregistered.
VERIFY_NOT_REACHED();
}
IDAllocator timer_id_allocator;
HashMap<int, CFRunLoopTimerRef> timers;
HashMap<Core::Notifier*, CFRunLoopSourceRef> notifiers;
};
class SignalHandlers : public RefCounted<SignalHandlers> {
AK_MAKE_NONCOPYABLE(SignalHandlers);
AK_MAKE_NONMOVABLE(SignalHandlers);
public:
SignalHandlers(int signal_number, CFFileDescriptorCallBack);
~SignalHandlers();
void dispatch();
int add(Function<void(int)>&& handler);
bool remove(int handler_id);
bool is_empty() const
{
if (m_calling_handlers) {
for (auto const& handler : m_handlers_pending) {
if (handler.value)
return false; // an add is pending
}
}
return m_handlers.is_empty();
}
bool have(int handler_id) const
{
if (m_calling_handlers) {
auto it = m_handlers_pending.find(handler_id);
if (it != m_handlers_pending.end()) {
if (!it->value)
return false; // a deletion is pending
}
}
return m_handlers.contains(handler_id);
}
int m_signal_number;
void (*m_original_handler)(int);
HashMap<int, Function<void(int)>> m_handlers;
HashMap<int, Function<void(int)>> m_handlers_pending;
bool m_calling_handlers { false };
CFRunLoopSourceRef m_source { nullptr };
int m_kevent_fd = { -1 };
};
SignalHandlers::SignalHandlers(int signal_number, CFFileDescriptorCallBack handle_signal)
: m_signal_number(signal_number)
, m_original_handler(signal(signal_number, [](int) {}))
{
m_kevent_fd = kqueue();
if (m_kevent_fd < 0) {
dbgln("Unable to create kqueue to register signal {}: {}", signal_number, strerror(errno));
VERIFY_NOT_REACHED();
}
struct kevent changes = {};
EV_SET(&changes, signal_number, EVFILT_SIGNAL, EV_ADD | EV_RECEIPT, 0, 0, nullptr);
if (auto res = kevent(m_kevent_fd, &changes, 1, &changes, 1, NULL); res < 0) {
dbgln("Unable to register signal {}: {}", signal_number, strerror(errno));
VERIFY_NOT_REACHED();
}
CFFileDescriptorContext context = { 0, this, nullptr, nullptr, nullptr };
CFFileDescriptorRef kq_ref = CFFileDescriptorCreate(kCFAllocatorDefault, m_kevent_fd, FALSE, handle_signal, &context);
m_source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, kq_ref, 0);
CFRunLoopAddSource(CFRunLoopGetMain(), m_source, kCFRunLoopDefaultMode);
CFFileDescriptorEnableCallBacks(kq_ref, kCFFileDescriptorReadCallBack);
CFRelease(kq_ref);
}
SignalHandlers::~SignalHandlers()
{
CFRunLoopRemoveSource(CFRunLoopGetMain(), m_source, kCFRunLoopDefaultMode);
CFRelease(m_source);
(void)::signal(m_signal_number, m_original_handler);
::close(m_kevent_fd);
}
struct SignalHandlersInfo {
HashMap<int, NonnullRefPtr<SignalHandlers>> signal_handlers;
int next_signal_id { 0 };
};
static Singleton<SignalHandlersInfo> s_signals;
static SignalHandlersInfo* signals_info()
{
return s_signals.ptr();
}
void SignalHandlers::dispatch()
{
TemporaryChange change(m_calling_handlers, true);
for (auto& handler : m_handlers)
handler.value(m_signal_number);
if (!m_handlers_pending.is_empty()) {
// Apply pending adds/removes
for (auto& handler : m_handlers_pending) {
if (handler.value) {
auto result = m_handlers.set(handler.key, move(handler.value));
VERIFY(result == AK::HashSetResult::InsertedNewEntry);
} else {
m_handlers.remove(handler.key);
}
}
m_handlers_pending.clear();
}
}
int SignalHandlers::add(Function<void(int)>&& handler)
{
int id = ++signals_info()->next_signal_id; // TODO: worry about wrapping and duplicates?
if (m_calling_handlers)
m_handlers_pending.set(id, move(handler));
else
m_handlers.set(id, move(handler));
return id;
}
bool SignalHandlers::remove(int handler_id)
{
VERIFY(handler_id != 0);
if (m_calling_handlers) {
auto it = m_handlers.find(handler_id);
if (it != m_handlers.end()) {
// Mark pending remove
m_handlers_pending.set(handler_id, {});
return true;
}
it = m_handlers_pending.find(handler_id);
if (it != m_handlers_pending.end()) {
if (!it->value)
return false; // already was marked as deleted
it->value = nullptr;
return true;
}
return false;
}
return m_handlers.remove(handler_id);
}
static void post_application_event()
{
auto* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSMakePoint(0, 0)
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:event atStart:NO];
}
NonnullOwnPtr<Core::EventLoopImplementation> CFEventLoopManager::make_implementation()
{
return CFEventLoopImplementation::create();
}
intptr_t CFEventLoopManager::register_timer(Core::EventReceiver& receiver, int interval_milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible should_fire_when_not_visible)
{
auto& thread_data = ThreadData::the();
auto timer_id = thread_data.timer_id_allocator.allocate();
auto weak_receiver = receiver.make_weak_ptr();
auto interval_seconds = static_cast<double>(interval_milliseconds) / 1000.0;
auto first_fire_time = CFAbsoluteTimeGetCurrent() + interval_seconds;
auto* timer = CFRunLoopTimerCreateWithHandler(
kCFAllocatorDefault, first_fire_time, should_reload ? interval_seconds : 0, 0, 0,
^(CFRunLoopTimerRef) {
auto receiver = weak_receiver.strong_ref();
if (!receiver) {
return;
}
if (should_fire_when_not_visible == Core::TimerShouldFireWhenNotVisible::No) {
if (!receiver->is_visible_for_timer_purposes()) {
return;
}
}
Core::TimerEvent event;
receiver->dispatch_event(event);
});
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);
thread_data.timers.set(timer_id, timer);
return timer_id;
}
void CFEventLoopManager::unregister_timer(intptr_t timer_id)
{
auto& thread_data = ThreadData::the();
thread_data.timer_id_allocator.deallocate(static_cast<int>(timer_id));
auto timer = thread_data.timers.take(static_cast<int>(timer_id));
VERIFY(timer.has_value());
CFRunLoopTimerInvalidate(*timer);
CFRelease(*timer);
}
static void socket_notifier(CFSocketRef socket, CFSocketCallBackType notification_type, CFDataRef, void const*, void*)
{
auto& notifier = ThreadData::the().notifier_by_fd(CFSocketGetNative(socket));
// This socket callback is not quite re-entrant. If Core::Notifier::dispatch_event blocks, e.g.
// to wait upon a Core::Promise, this socket will not receive any more notifications until that
// promise is resolved or rejected. So we mark this socket as able to receive more notifications
// before dispatching the event, which allows it to be triggered again.
CFSocketEnableCallBacks(socket, notification_type);
Core::NotifierActivationEvent event(notifier.fd(), notifier.type());
notifier.dispatch_event(event);
// This manual process of enabling the callbacks also seems to require waking the event loop,
// otherwise it hangs indefinitely in any ongoing pump(PumpMode::WaitForEvents) invocation.
post_application_event();
}
void CFEventLoopManager::register_notifier(Core::Notifier& notifier)
{
auto notification_type = kCFSocketNoCallBack;
switch (notifier.type()) {
case Core::Notifier::Type::Read:
notification_type = kCFSocketReadCallBack;
break;
case Core::Notifier::Type::Write:
notification_type = kCFSocketWriteCallBack;
break;
default:
TODO();
break;
}
CFSocketContext context { .version = 0, .info = nullptr, .retain = nullptr, .release = nullptr, .copyDescription = nullptr };
auto* socket = CFSocketCreateWithNative(kCFAllocatorDefault, notifier.fd(), notification_type, &socket_notifier, &context);
CFOptionFlags sockopt = CFSocketGetSocketFlags(socket);
sockopt &= ~kCFSocketAutomaticallyReenableReadCallBack;
sockopt &= ~kCFSocketCloseOnInvalidate;
CFSocketSetSocketFlags(socket, sockopt);
auto* source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
CFRelease(socket);
ThreadData::the().notifiers.set(&notifier, source);
}
void CFEventLoopManager::unregister_notifier(Core::Notifier& notifier)
{
if (auto source = ThreadData::the().notifiers.take(&notifier); source.has_value()) {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), *source, kCFRunLoopCommonModes);
CFRelease(*source);
}
}
void CFEventLoopManager::did_post_event()
{
post_application_event();
}
static void handle_signal(CFFileDescriptorRef f, CFOptionFlags callback_types, void* info)
{
VERIFY(callback_types & kCFFileDescriptorReadCallBack);
auto* signal_handlers = static_cast<SignalHandlers*>(info);
struct kevent event { };
// returns number of events that have occurred since last call
(void)::kevent(CFFileDescriptorGetNativeDescriptor(f), nullptr, 0, &event, 1, nullptr);
CFFileDescriptorEnableCallBacks(f, kCFFileDescriptorReadCallBack);
signal_handlers->dispatch();
}
int CFEventLoopManager::register_signal(int signal_number, Function<void(int)> handler)
{
VERIFY(signal_number != 0);
auto& info = *signals_info();
auto handlers = info.signal_handlers.find(signal_number);
if (handlers == info.signal_handlers.end()) {
auto signal_handlers = adopt_ref(*new SignalHandlers(signal_number, &handle_signal));
auto handler_id = signal_handlers->add(move(handler));
info.signal_handlers.set(signal_number, move(signal_handlers));
return handler_id;
} else {
return handlers->value->add(move(handler));
}
}
void CFEventLoopManager::unregister_signal(int handler_id)
{
VERIFY(handler_id != 0);
int remove_signal_number = 0;
auto& info = *signals_info();
for (auto& h : info.signal_handlers) {
auto& handlers = *h.value;
if (handlers.remove(handler_id)) {
if (handlers.is_empty())
remove_signal_number = handlers.m_signal_number;
break;
}
}
if (remove_signal_number != 0)
info.signal_handlers.remove(remove_signal_number);
}
NonnullOwnPtr<CFEventLoopImplementation> CFEventLoopImplementation::create()
{
return adopt_own(*new CFEventLoopImplementation);
}
int CFEventLoopImplementation::exec()
{
[NSApp run];
return m_exit_code;
}
size_t CFEventLoopImplementation::pump(PumpMode mode)
{
auto* wait_until = mode == PumpMode::WaitForEvents ? [NSDate distantFuture] : [NSDate distantPast];
auto* event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:wait_until
inMode:NSDefaultRunLoopMode
dequeue:YES];
while (event) {
[NSApp sendEvent:event];
event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:nil
inMode:NSDefaultRunLoopMode
dequeue:YES];
}
return 0;
}
void CFEventLoopImplementation::quit(int exit_code)
{
m_exit_code = exit_code;
[NSApp stop:nil];
}
void CFEventLoopImplementation::wake()
{
CFRunLoopWakeUp(CFRunLoopGetCurrent());
}
void CFEventLoopImplementation::post_event(Core::EventReceiver& receiver, NonnullOwnPtr<Core::Event>&& event)
{
m_thread_event_queue.post_event(receiver, move(event));
if (&m_thread_event_queue != &Core::ThreadEventQueue::current())
wake();
}
}

51
UI/AppKit/CMakeLists.txt Normal file
View file

@ -0,0 +1,51 @@
add_library(ladybird_impl STATIC
${LADYBIRD_SOURCES}
Application/Application.mm
Application/ApplicationDelegate.mm
Application/EventLoopImplementation.mm
Interface/Event.mm
Interface/Inspector.mm
Interface/InspectorController.mm
Interface/LadybirdWebView.mm
Interface/LadybirdWebViewBridge.cpp
Interface/LadybirdWebViewWindow.mm
Interface/Palette.mm
Interface/SearchPanel.mm
Interface/Tab.mm
Interface/TabController.mm
Utilities/Conversions.mm
)
target_include_directories(ladybird_impl PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>)
target_compile_options(ladybird_impl PUBLIC
$<$<COMPILE_LANGUAGE:CXX>:-fobjc-arc>
$<$<COMPILE_LANGUAGE:CXX>:-Wno-deprecated-anon-enum-enum-conversion> # Required for CGImageCreate
)
target_compile_features(ladybird_impl PUBLIC cxx_std_23)
if (ENABLE_SWIFT)
target_sources(ladybird_impl PRIVATE
Interface/TaskManager.swift
Interface/TaskManagerController.swift
)
target_compile_definitions(ladybird_impl PUBLIC LADYBIRD_USE_SWIFT)
set_target_properties(ladybird_impl PROPERTIES Swift_MODULE_NAME "SwiftLadybird")
get_target_property(LADYBIRD_NATIVE_DIRS ladybird_impl INCLUDE_DIRECTORIES)
_swift_generate_cxx_header(ladybird_impl "Ladybird-Swift.h"
SEARCH_PATHS ${LADYBIRD_NATIVE_DIRS}
)
else()
target_sources(ladybird_impl PRIVATE
Interface/TaskManager.mm
Interface/TaskManagerController.mm
)
endif()
add_executable(ladybird MACOSX_BUNDLE
main.mm
)
target_link_libraries(ladybird_impl PUBLIC "-framework Cocoa -framework UniformTypeIdentifiers" LibUnicode)
target_link_libraries(ladybird PRIVATE ladybird_impl)
create_ladybird_bundle(ladybird)

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Vector.h>
#include <LibURL/Forward.h>
#include <LibWeb/Page/InputEvent.h>
#import <Cocoa/Cocoa.h>
namespace Ladybird {
Web::MouseEvent ns_event_to_mouse_event(Web::MouseEvent::Type, NSEvent*, NSView*, Web::UIEvents::MouseButton);
Web::DragEvent ns_event_to_drag_event(Web::DragEvent::Type, id<NSDraggingInfo>, NSView*);
Vector<URL::URL> drag_event_url_list(Web::DragEvent const&);
Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type, NSEvent*);
NSEvent* key_event_to_ns_event(Web::KeyEvent const&);
NSEvent* create_context_menu_mouse_event(NSView*, Gfx::IntPoint);
NSEvent* create_context_menu_mouse_event(NSView*, NSPoint);
}

View file

@ -0,0 +1,329 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/TypeCasts.h>
#include <AK/Utf8View.h>
#include <LibURL/URL.h>
#include <LibWeb/HTML/SelectedFile.h>
#include <LibWeb/UIEvents/KeyCode.h>
#import <Carbon/Carbon.h>
#import <Interface/Event.h>
#import <Utilities/Conversions.h>
namespace Ladybird {
static Web::UIEvents::KeyModifier ns_modifiers_to_key_modifiers(NSEventModifierFlags modifier_flags, Optional<Web::UIEvents::MouseButton&> button = {})
{
unsigned modifiers = Web::UIEvents::KeyModifier::Mod_None;
if ((modifier_flags & NSEventModifierFlagShift) != 0) {
modifiers |= Web::UIEvents::KeyModifier::Mod_Shift;
}
if ((modifier_flags & NSEventModifierFlagControl) != 0) {
if (button == Web::UIEvents::MouseButton::Primary) {
*button = Web::UIEvents::MouseButton::Secondary;
} else {
modifiers |= Web::UIEvents::KeyModifier::Mod_Ctrl;
}
}
if ((modifier_flags & NSEventModifierFlagOption) != 0) {
modifiers |= Web::UIEvents::KeyModifier::Mod_Alt;
}
if ((modifier_flags & NSEventModifierFlagCommand) != 0) {
modifiers |= Web::UIEvents::KeyModifier::Mod_Super;
}
return static_cast<Web::UIEvents::KeyModifier>(modifiers);
}
Web::MouseEvent ns_event_to_mouse_event(Web::MouseEvent::Type type, NSEvent* event, NSView* view, Web::UIEvents::MouseButton button)
{
auto position = [view convertPoint:event.locationInWindow fromView:nil];
auto device_position = ns_point_to_gfx_point(position).to_type<Web::DevicePixels>();
auto screen_position = [NSEvent mouseLocation];
auto device_screen_position = ns_point_to_gfx_point(screen_position).to_type<Web::DevicePixels>();
auto modifiers = ns_modifiers_to_key_modifiers(event.modifierFlags, button);
int wheel_delta_x = 0;
int wheel_delta_y = 0;
if (type == Web::MouseEvent::Type::MouseDown) {
if (event.clickCount % 2 == 0) {
type = Web::MouseEvent::Type::DoubleClick;
}
} else if (type == Web::MouseEvent::Type::MouseWheel) {
CGFloat delta_x = -[event scrollingDeltaX];
CGFloat delta_y = -[event scrollingDeltaY];
if (![event hasPreciseScrollingDeltas]) {
static constexpr CGFloat imprecise_scroll_multiplier = 24;
delta_x *= imprecise_scroll_multiplier;
delta_y *= imprecise_scroll_multiplier;
}
wheel_delta_x = static_cast<int>(delta_x);
wheel_delta_y = static_cast<int>(delta_y);
}
return { type, device_position, device_screen_position, button, button, modifiers, wheel_delta_x, wheel_delta_y, nullptr };
}
struct DragData : public Web::ChromeInputData {
explicit DragData(Vector<URL::URL> urls)
: urls(move(urls))
{
}
Vector<URL::URL> urls;
};
Web::DragEvent ns_event_to_drag_event(Web::DragEvent::Type type, id<NSDraggingInfo> event, NSView* view)
{
auto position = [view convertPoint:event.draggingLocation fromView:nil];
auto device_position = ns_point_to_gfx_point(position).to_type<Web::DevicePixels>();
auto screen_position = [NSEvent mouseLocation];
auto device_screen_position = ns_point_to_gfx_point(screen_position).to_type<Web::DevicePixels>();
auto button = Web::UIEvents::MouseButton::Primary;
auto modifiers = ns_modifiers_to_key_modifiers([NSEvent modifierFlags], button);
Vector<Web::HTML::SelectedFile> files;
OwnPtr<DragData> chrome_data;
auto for_each_file = [&](auto callback) {
NSArray* file_list = [[event draggingPasteboard] readObjectsForClasses:@[ [NSURL class] ]
options:nil];
for (NSURL* file in file_list) {
auto file_path = Ladybird::ns_string_to_byte_string([file path]);
callback(file_path);
}
};
if (type == Web::DragEvent::Type::DragStart) {
for_each_file([&](ByteString const& file_path) {
if (auto file = Web::HTML::SelectedFile::from_file_path(file_path); file.is_error())
warnln("Unable to open file {}: {}", file_path, file.error());
else
files.append(file.release_value());
});
} else if (type == Web::DragEvent::Type::Drop) {
Vector<URL::URL> urls;
for_each_file([&](ByteString const& file_path) {
if (auto url = URL::create_with_url_or_path(file_path); url.is_valid())
urls.append(move(url));
});
chrome_data = make<DragData>(move(urls));
}
return { type, device_position, device_screen_position, button, button, modifiers, move(files), move(chrome_data) };
}
Vector<URL::URL> drag_event_url_list(Web::DragEvent const& event)
{
auto& chrome_data = verify_cast<DragData>(*event.chrome_data);
return move(chrome_data.urls);
}
NSEvent* create_context_menu_mouse_event(NSView* view, Gfx::IntPoint position)
{
return create_context_menu_mouse_event(view, gfx_point_to_ns_point(position));
}
NSEvent* create_context_menu_mouse_event(NSView* view, NSPoint position)
{
return [NSEvent mouseEventWithType:NSEventTypeRightMouseUp
location:[view convertPoint:position fromView:nil]
modifierFlags:0
timestamp:0
windowNumber:[[view window] windowNumber]
context:nil
eventNumber:1
clickCount:1
pressure:1.0];
}
static Web::UIEvents::KeyCode ns_key_code_to_key_code(unsigned short key_code, Web::UIEvents::KeyModifier& modifiers)
{
auto augment_modifiers_and_return = [&](auto key, auto modifier) {
modifiers = static_cast<Web::UIEvents::KeyModifier>(static_cast<unsigned>(modifiers) | modifier);
return key;
};
// clang-format off
switch (key_code) {
case kVK_ANSI_0: return Web::UIEvents::KeyCode::Key_0;
case kVK_ANSI_1: return Web::UIEvents::KeyCode::Key_1;
case kVK_ANSI_2: return Web::UIEvents::KeyCode::Key_2;
case kVK_ANSI_3: return Web::UIEvents::KeyCode::Key_3;
case kVK_ANSI_4: return Web::UIEvents::KeyCode::Key_4;
case kVK_ANSI_5: return Web::UIEvents::KeyCode::Key_5;
case kVK_ANSI_6: return Web::UIEvents::KeyCode::Key_6;
case kVK_ANSI_7: return Web::UIEvents::KeyCode::Key_7;
case kVK_ANSI_8: return Web::UIEvents::KeyCode::Key_8;
case kVK_ANSI_9: return Web::UIEvents::KeyCode::Key_9;
case kVK_ANSI_A: return Web::UIEvents::KeyCode::Key_A;
case kVK_ANSI_B: return Web::UIEvents::KeyCode::Key_B;
case kVK_ANSI_C: return Web::UIEvents::KeyCode::Key_C;
case kVK_ANSI_D: return Web::UIEvents::KeyCode::Key_D;
case kVK_ANSI_E: return Web::UIEvents::KeyCode::Key_E;
case kVK_ANSI_F: return Web::UIEvents::KeyCode::Key_F;
case kVK_ANSI_G: return Web::UIEvents::KeyCode::Key_G;
case kVK_ANSI_H: return Web::UIEvents::KeyCode::Key_H;
case kVK_ANSI_I: return Web::UIEvents::KeyCode::Key_I;
case kVK_ANSI_J: return Web::UIEvents::KeyCode::Key_J;
case kVK_ANSI_K: return Web::UIEvents::KeyCode::Key_K;
case kVK_ANSI_L: return Web::UIEvents::KeyCode::Key_L;
case kVK_ANSI_M: return Web::UIEvents::KeyCode::Key_M;
case kVK_ANSI_N: return Web::UIEvents::KeyCode::Key_N;
case kVK_ANSI_O: return Web::UIEvents::KeyCode::Key_O;
case kVK_ANSI_P: return Web::UIEvents::KeyCode::Key_P;
case kVK_ANSI_Q: return Web::UIEvents::KeyCode::Key_Q;
case kVK_ANSI_R: return Web::UIEvents::KeyCode::Key_R;
case kVK_ANSI_S: return Web::UIEvents::KeyCode::Key_S;
case kVK_ANSI_T: return Web::UIEvents::KeyCode::Key_T;
case kVK_ANSI_U: return Web::UIEvents::KeyCode::Key_U;
case kVK_ANSI_V: return Web::UIEvents::KeyCode::Key_V;
case kVK_ANSI_W: return Web::UIEvents::KeyCode::Key_W;
case kVK_ANSI_X: return Web::UIEvents::KeyCode::Key_X;
case kVK_ANSI_Y: return Web::UIEvents::KeyCode::Key_Y;
case kVK_ANSI_Z: return Web::UIEvents::KeyCode::Key_Z;
case kVK_ANSI_Backslash: return Web::UIEvents::KeyCode::Key_Backslash;
case kVK_ANSI_Comma: return Web::UIEvents::KeyCode::Key_Comma;
case kVK_ANSI_Equal: return Web::UIEvents::KeyCode::Key_Equal;
case kVK_ANSI_Grave: return Web::UIEvents::KeyCode::Key_Backtick;
case kVK_ANSI_Keypad0: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_0, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad1: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_1, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad2: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_2, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad3: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_3, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad4: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_4, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad5: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_5, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad6: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_6, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad7: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_7, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad8: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_8, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad9: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_9, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadClear: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Delete, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadDecimal: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Period, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadDivide: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Slash, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadEnter: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Return, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadEquals: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Equal, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadMinus: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Minus, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadMultiply: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Asterisk, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadPlus: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Plus, Web::UIEvents::KeyModifier::Mod_Keypad);
case kVK_ANSI_LeftBracket: return Web::UIEvents::KeyCode::Key_LeftBracket;
case kVK_ANSI_Minus: return Web::UIEvents::KeyCode::Key_Minus;
case kVK_ANSI_Period: return Web::UIEvents::KeyCode::Key_Period;
case kVK_ANSI_Quote: return Web::UIEvents::KeyCode::Key_Apostrophe;
case kVK_ANSI_RightBracket: return Web::UIEvents::KeyCode::Key_RightBracket;
case kVK_ANSI_Semicolon: return Web::UIEvents::KeyCode::Key_Semicolon;
case kVK_ANSI_Slash: return Web::UIEvents::KeyCode::Key_Slash;
case kVK_CapsLock: return Web::UIEvents::KeyCode::Key_CapsLock;
case kVK_Command: return Web::UIEvents::KeyCode::Key_LeftSuper;
case kVK_Control: return Web::UIEvents::KeyCode::Key_LeftControl;
case kVK_Delete: return Web::UIEvents::KeyCode::Key_Backspace;
case kVK_DownArrow: return Web::UIEvents::KeyCode::Key_Down;
case kVK_End: return Web::UIEvents::KeyCode::Key_End;
case kVK_Escape: return Web::UIEvents::KeyCode::Key_Escape;
case kVK_F1: return Web::UIEvents::KeyCode::Key_F1;
case kVK_F2: return Web::UIEvents::KeyCode::Key_F2;
case kVK_F3: return Web::UIEvents::KeyCode::Key_F3;
case kVK_F4: return Web::UIEvents::KeyCode::Key_F4;
case kVK_F5: return Web::UIEvents::KeyCode::Key_F5;
case kVK_F6: return Web::UIEvents::KeyCode::Key_F6;
case kVK_F7: return Web::UIEvents::KeyCode::Key_F7;
case kVK_F8: return Web::UIEvents::KeyCode::Key_F8;
case kVK_F9: return Web::UIEvents::KeyCode::Key_F9;
case kVK_F10: return Web::UIEvents::KeyCode::Key_F10;
case kVK_F11: return Web::UIEvents::KeyCode::Key_F11;
case kVK_F12: return Web::UIEvents::KeyCode::Key_F12;
case kVK_ForwardDelete: return Web::UIEvents::KeyCode::Key_Delete;
case kVK_Home: return Web::UIEvents::KeyCode::Key_Home;
case kVK_LeftArrow: return Web::UIEvents::KeyCode::Key_Left;
case kVK_Option: return Web::UIEvents::KeyCode::Key_LeftAlt;
case kVK_PageDown: return Web::UIEvents::KeyCode::Key_PageDown;
case kVK_PageUp: return Web::UIEvents::KeyCode::Key_PageUp;
case kVK_Return: return Web::UIEvents::KeyCode::Key_Return;
case kVK_RightArrow: return Web::UIEvents::KeyCode::Key_Right;
case kVK_RightCommand: return Web::UIEvents::KeyCode::Key_RightSuper;
case kVK_RightControl: return Web::UIEvents::KeyCode::Key_RightControl;
case kVK_RightOption: return Web::UIEvents::KeyCode::Key_RightAlt;
case kVK_RightShift: return Web::UIEvents::KeyCode::Key_RightShift;
case kVK_Shift: return Web::UIEvents::KeyCode::Key_LeftShift;
case kVK_Space: return Web::UIEvents::KeyCode::Key_Space;
case kVK_Tab: return Web::UIEvents::KeyCode::Key_Tab;
case kVK_UpArrow: return Web::UIEvents::KeyCode::Key_Up;
default: break;
}
// clang-format on
return Web::UIEvents::KeyCode::Key_Invalid;
}
class KeyData : public Web::ChromeInputData {
public:
explicit KeyData(NSEvent* event)
: m_event(CFBridgingRetain(event))
{
}
virtual ~KeyData() override
{
if (m_event != nullptr) {
CFBridgingRelease(m_event);
}
}
NSEvent* take_event()
{
VERIFY(m_event != nullptr);
CFTypeRef event = exchange(m_event, nullptr);
return CFBridgingRelease(event);
}
private:
CFTypeRef m_event { nullptr };
};
Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type type, NSEvent* event)
{
auto modifiers = ns_modifiers_to_key_modifiers(event.modifierFlags);
auto key_code = ns_key_code_to_key_code(event.keyCode, modifiers);
auto repeat = false;
// FIXME: WebContent should really support multi-code point key events.
u32 code_point = 0;
if (event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp) {
auto const* utf8 = [event.characters UTF8String];
Utf8View utf8_view { StringView { utf8, strlen(utf8) } };
code_point = utf8_view.is_empty() ? 0u : *utf8_view.begin();
repeat = event.isARepeat;
}
// NSEvent assigns PUA code points to to functional keys, e.g. arrow keys. Do not propagate them.
if (code_point >= 0xE000 && code_point <= 0xF8FF)
code_point = 0;
return { type, key_code, modifiers, code_point, repeat, make<KeyData>(event) };
}
NSEvent* key_event_to_ns_event(Web::KeyEvent const& event)
{
auto& chrome_data = verify_cast<KeyData>(*event.chrome_data);
return chrome_data.take_event();
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#import <Cocoa/Cocoa.h>
#import <Interface/LadybirdWebViewWindow.h>
@class LadybirdWebView;
@class Tab;
@interface Inspector : LadybirdWebViewWindow
- (instancetype)init:(Tab*)tab;
- (void)inspect;
- (void)reset;
- (void)selectHoveredElement;
@end

View file

@ -0,0 +1,372 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Cookie/Cookie.h>
#include <LibWebView/Attribute.h>
#include <LibWebView/InspectorClient.h>
#include <LibWebView/ViewImplementation.h>
#import <Interface/Event.h>
#import <Interface/Inspector.h>
#import <Interface/LadybirdWebView.h>
#import <Interface/Tab.h>
#import <Utilities/Conversions.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
static constexpr CGFloat const WINDOW_WIDTH = 875;
static constexpr CGFloat const WINDOW_HEIGHT = 825;
static constexpr NSInteger CONTEXT_MENU_EDIT_NODE_TAG = 1;
static constexpr NSInteger CONTEXT_MENU_REMOVE_ATTRIBUTE_TAG = 2;
static constexpr NSInteger CONTEXT_MENU_COPY_ATTRIBUTE_VALUE_TAG = 3;
static constexpr NSInteger CONTEXT_MENU_DELETE_COOKIE_TAG = 4;
@interface Inspector ()
{
OwnPtr<WebView::InspectorClient> m_inspector_client;
}
@property (nonatomic, strong) Tab* tab;
@property (nonatomic, strong) NSMenu* dom_node_text_context_menu;
@property (nonatomic, strong) NSMenu* dom_node_tag_context_menu;
@property (nonatomic, strong) NSMenu* dom_node_attribute_context_menu;
@property (nonatomic, strong) NSMenu* cookie_context_menu;
@end
@implementation Inspector
@synthesize tab = _tab;
@synthesize dom_node_text_context_menu = _dom_node_text_context_menu;
@synthesize dom_node_tag_context_menu = _dom_node_tag_context_menu;
@synthesize dom_node_attribute_context_menu = _dom_node_attribute_context_menu;
@synthesize cookie_context_menu = _cookie_context_menu;
- (instancetype)init:(Tab*)tab
{
auto tab_rect = [tab frame];
auto position_x = tab_rect.origin.x + (tab_rect.size.width - WINDOW_WIDTH) / 2;
auto position_y = tab_rect.origin.y + (tab_rect.size.height - WINDOW_HEIGHT) / 2;
auto window_rect = NSMakeRect(position_x, position_y, WINDOW_WIDTH, WINDOW_HEIGHT);
if (self = [super initWithWebView:nil windowRect:window_rect]) {
self.tab = tab;
m_inspector_client = make<WebView::InspectorClient>([[tab web_view] view], [[self web_view] view]);
__weak Inspector* weak_self = self;
m_inspector_client->on_requested_dom_node_text_context_menu = [weak_self](auto position) {
Inspector* strong_self = weak_self;
if (strong_self == nil) {
return;
}
auto* event = Ladybird::create_context_menu_mouse_event(strong_self.web_view, position);
[NSMenu popUpContextMenu:strong_self.dom_node_text_context_menu withEvent:event forView:strong_self.web_view];
};
m_inspector_client->on_requested_dom_node_tag_context_menu = [weak_self](auto position, auto const& tag) {
Inspector* strong_self = weak_self;
if (strong_self == nil) {
return;
}
auto edit_node_text = MUST(String::formatted("Edit \"{}\"", tag));
auto* edit_node_menu_item = [strong_self.dom_node_tag_context_menu itemWithTag:CONTEXT_MENU_EDIT_NODE_TAG];
[edit_node_menu_item setTitle:Ladybird::string_to_ns_string(edit_node_text)];
auto* event = Ladybird::create_context_menu_mouse_event(strong_self.web_view, position);
[NSMenu popUpContextMenu:strong_self.dom_node_tag_context_menu withEvent:event forView:strong_self.web_view];
};
m_inspector_client->on_requested_dom_node_attribute_context_menu = [weak_self](auto position, auto const&, auto const& attribute) {
Inspector* strong_self = weak_self;
if (strong_self == nil) {
return;
}
static constexpr size_t MAX_ATTRIBUTE_VALUE_LENGTH = 32;
auto edit_attribute_text = MUST(String::formatted("Edit attribute \"{}\"", attribute.name));
auto remove_attribute_text = MUST(String::formatted("Remove attribute \"{}\"", attribute.name));
auto copy_attribute_value_text = MUST(String::formatted("Copy attribute value \"{:.{}}{}\"",
attribute.value, MAX_ATTRIBUTE_VALUE_LENGTH,
attribute.value.bytes_as_string_view().length() > MAX_ATTRIBUTE_VALUE_LENGTH ? "..."sv : ""sv));
auto* edit_node_menu_item = [strong_self.dom_node_attribute_context_menu itemWithTag:CONTEXT_MENU_EDIT_NODE_TAG];
[edit_node_menu_item setTitle:Ladybird::string_to_ns_string(edit_attribute_text)];
auto* remove_attribute_menu_item = [strong_self.dom_node_attribute_context_menu itemWithTag:CONTEXT_MENU_REMOVE_ATTRIBUTE_TAG];
[remove_attribute_menu_item setTitle:Ladybird::string_to_ns_string(remove_attribute_text)];
auto* copy_attribute_value_menu_item = [strong_self.dom_node_attribute_context_menu itemWithTag:CONTEXT_MENU_COPY_ATTRIBUTE_VALUE_TAG];
[copy_attribute_value_menu_item setTitle:Ladybird::string_to_ns_string(copy_attribute_value_text)];
auto* event = Ladybird::create_context_menu_mouse_event(strong_self.web_view, position);
[NSMenu popUpContextMenu:strong_self.dom_node_attribute_context_menu withEvent:event forView:strong_self.web_view];
};
m_inspector_client->on_requested_cookie_context_menu = [weak_self](auto position, auto const& cookie) {
Inspector* strong_self = weak_self;
if (strong_self == nil) {
return;
}
auto delete_cookie_text = MUST(String::formatted("Delete \"{}\"", cookie.name));
auto* delete_cookie_item = [strong_self.cookie_context_menu itemWithTag:CONTEXT_MENU_DELETE_COOKIE_TAG];
[delete_cookie_item setTitle:Ladybird::string_to_ns_string(delete_cookie_text)];
auto* event = Ladybird::create_context_menu_mouse_event(strong_self.web_view, position);
[NSMenu popUpContextMenu:strong_self.cookie_context_menu withEvent:event forView:strong_self.web_view];
};
[self setContentView:self.web_view];
[self setTitle:@"Inspector"];
[self setIsVisible:YES];
}
return self;
}
- (void)dealloc
{
auto& web_view = [[self.tab web_view] view];
web_view.clear_inspected_dom_node();
}
#pragma mark - Public methods
- (void)inspect
{
m_inspector_client->inspect();
}
- (void)reset
{
m_inspector_client->reset();
}
- (void)selectHoveredElement
{
m_inspector_client->select_hovered_node();
}
#pragma mark - Private methods
- (void)editDOMNode:(id)sender
{
m_inspector_client->context_menu_edit_dom_node();
}
- (void)copyDOMNode:(id)sender
{
m_inspector_client->context_menu_copy_dom_node();
}
- (void)screenshotDOMNode:(id)sender
{
m_inspector_client->context_menu_screenshot_dom_node();
}
- (void)createChildElement:(id)sender
{
m_inspector_client->context_menu_create_child_element();
}
- (void)createChildTextNode:(id)sender
{
m_inspector_client->context_menu_create_child_text_node();
}
- (void)cloneDOMNode:(id)sender
{
m_inspector_client->context_menu_clone_dom_node();
}
- (void)deleteDOMNode:(id)sender
{
m_inspector_client->context_menu_remove_dom_node();
}
- (void)addDOMAttribute:(id)sender
{
m_inspector_client->context_menu_add_dom_node_attribute();
}
- (void)removeDOMAttribute:(id)sender
{
m_inspector_client->context_menu_remove_dom_node_attribute();
}
- (void)copyDOMAttributeValue:(id)sender
{
m_inspector_client->context_menu_copy_dom_node_attribute_value();
}
- (void)deleteCookie:(id)sender
{
m_inspector_client->context_menu_delete_cookie();
}
- (void)deleteAllCookies:(id)sender
{
m_inspector_client->context_menu_delete_all_cookies();
}
#pragma mark - Properties
+ (NSMenuItem*)make_create_child_menu
{
auto* create_child_menu = [[NSMenu alloc] init];
[create_child_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Create child element"
action:@selector(createChildElement:)
keyEquivalent:@""]];
[create_child_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Create child text node"
action:@selector(createChildTextNode:)
keyEquivalent:@""]];
auto* create_child_menu_item = [[NSMenuItem alloc] initWithTitle:@"Create child"
action:nil
keyEquivalent:@""];
[create_child_menu_item setSubmenu:create_child_menu];
return create_child_menu_item;
}
- (NSMenu*)dom_node_text_context_menu
{
if (!_dom_node_text_context_menu) {
_dom_node_text_context_menu = [[NSMenu alloc] initWithTitle:@"DOM Text Context Menu"];
[_dom_node_text_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Edit text"
action:@selector(editDOMNode:)
keyEquivalent:@""]];
[_dom_node_text_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Copy text"
action:@selector(copyDOMNode:)
keyEquivalent:@""]];
[_dom_node_text_context_menu addItem:[NSMenuItem separatorItem]];
[_dom_node_text_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Delete node"
action:@selector(deleteDOMNode:)
keyEquivalent:@""]];
}
return _dom_node_text_context_menu;
}
- (NSMenu*)dom_node_tag_context_menu
{
if (!_dom_node_tag_context_menu) {
_dom_node_tag_context_menu = [[NSMenu alloc] initWithTitle:@"DOM Tag Context Menu"];
auto* edit_node_menu_item = [[NSMenuItem alloc] initWithTitle:@"Edit tag"
action:@selector(editDOMNode:)
keyEquivalent:@""];
[edit_node_menu_item setTag:CONTEXT_MENU_EDIT_NODE_TAG];
[_dom_node_tag_context_menu addItem:edit_node_menu_item];
[_dom_node_tag_context_menu addItem:[NSMenuItem separatorItem]];
[_dom_node_tag_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Add attribute"
action:@selector(addDOMAttribute:)
keyEquivalent:@""]];
[_dom_node_tag_context_menu addItem:[Inspector make_create_child_menu]];
[_dom_node_tag_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Clone node"
action:@selector(cloneDOMNode:)
keyEquivalent:@""]];
[_dom_node_tag_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Delete node"
action:@selector(deleteDOMNode:)
keyEquivalent:@""]];
[_dom_node_tag_context_menu addItem:[NSMenuItem separatorItem]];
[_dom_node_tag_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Copy HTML"
action:@selector(copyDOMNode:)
keyEquivalent:@""]];
[_dom_node_tag_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Take node screenshot"
action:@selector(screenshotDOMNode:)
keyEquivalent:@""]];
}
return _dom_node_tag_context_menu;
}
- (NSMenu*)dom_node_attribute_context_menu
{
if (!_dom_node_attribute_context_menu) {
_dom_node_attribute_context_menu = [[NSMenu alloc] initWithTitle:@"DOM Attribute Context Menu"];
auto* edit_node_menu_item = [[NSMenuItem alloc] initWithTitle:@"Edit attribute"
action:@selector(editDOMNode:)
keyEquivalent:@""];
[edit_node_menu_item setTag:CONTEXT_MENU_EDIT_NODE_TAG];
[_dom_node_attribute_context_menu addItem:edit_node_menu_item];
auto* remove_attribute_menu_item = [[NSMenuItem alloc] initWithTitle:@"Remove attribute"
action:@selector(removeDOMAttribute:)
keyEquivalent:@""];
[remove_attribute_menu_item setTag:CONTEXT_MENU_REMOVE_ATTRIBUTE_TAG];
[_dom_node_attribute_context_menu addItem:remove_attribute_menu_item];
auto* copy_attribute_value_menu_item = [[NSMenuItem alloc] initWithTitle:@"Copy attribute value"
action:@selector(copyDOMAttributeValue:)
keyEquivalent:@""];
[copy_attribute_value_menu_item setTag:CONTEXT_MENU_COPY_ATTRIBUTE_VALUE_TAG];
[_dom_node_attribute_context_menu addItem:copy_attribute_value_menu_item];
[_dom_node_attribute_context_menu addItem:[NSMenuItem separatorItem]];
[_dom_node_attribute_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Add attribute"
action:@selector(addDOMAttribute:)
keyEquivalent:@""]];
[_dom_node_attribute_context_menu addItem:[Inspector make_create_child_menu]];
[_dom_node_attribute_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Clone node"
action:@selector(cloneDOMNode:)
keyEquivalent:@""]];
[_dom_node_attribute_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Delete node"
action:@selector(deleteDOMNode:)
keyEquivalent:@""]];
[_dom_node_attribute_context_menu addItem:[NSMenuItem separatorItem]];
[_dom_node_attribute_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Copy HTML"
action:@selector(copyDOMNode:)
keyEquivalent:@""]];
[_dom_node_attribute_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Take node screenshot"
action:@selector(screenshotDOMNode:)
keyEquivalent:@""]];
}
return _dom_node_attribute_context_menu;
}
- (NSMenu*)cookie_context_menu
{
if (!_cookie_context_menu) {
_cookie_context_menu = [[NSMenu alloc] initWithTitle:@"Cookie Context Menu"];
auto* delete_cookie_item = [[NSMenuItem alloc] initWithTitle:@"Delete cookie"
action:@selector(deleteCookie:)
keyEquivalent:@""];
[delete_cookie_item setTag:CONTEXT_MENU_DELETE_COOKIE_TAG];
[_cookie_context_menu addItem:delete_cookie_item];
[_cookie_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Delete all cookies"
action:@selector(deleteAllCookies:)
keyEquivalent:@""]];
}
return _cookie_context_menu;
}
@end

View file

@ -0,0 +1,17 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#import <Cocoa/Cocoa.h>
@class Tab;
@interface InspectorController : NSWindowController
- (instancetype)init:(Tab*)tab;
@end

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#import <Interface/Inspector.h>
#import <Interface/InspectorController.h>
#import <Interface/LadybirdWebView.h>
#import <Interface/Tab.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
@interface InspectorController () <NSWindowDelegate>
@property (nonatomic, strong) Tab* tab;
@end
@implementation InspectorController
- (instancetype)init:(Tab*)tab
{
if (self = [super init]) {
self.tab = tab;
}
return self;
}
#pragma mark - Private methods
- (Inspector*)inspector
{
return (Inspector*)[self window];
}
#pragma mark - NSWindowController
- (IBAction)showWindow:(id)sender
{
self.window = [[Inspector alloc] init:self.tab];
[self.window setDelegate:self];
[self.window makeKeyAndOrderFront:sender];
}
#pragma mark - NSWindowDelegate
- (void)windowWillClose:(NSNotification*)notification
{
[self.tab onInspectorClosed];
}
- (void)windowDidResize:(NSNotification*)notification
{
[[[self inspector] web_view] handleResize];
}
- (void)windowDidChangeBackingProperties:(NSNotification*)notification
{
[[[self inspector] web_view] handleDevicePixelRatioChange];
}
@end

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#include <LibGfx/Forward.h>
#include <LibURL/Forward.h>
#include <LibWeb/CSS/PreferredColorScheme.h>
#include <LibWeb/CSS/PreferredContrast.h>
#include <LibWeb/CSS/PreferredMotion.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/AudioPlayState.h>
#include <LibWebView/Forward.h>
#import <Cocoa/Cocoa.h>
@protocol LadybirdWebViewObserver <NSObject>
- (String const&)onCreateNewTab:(Optional<URL::URL> const&)url
activateTab:(Web::HTML::ActivateTab)activate_tab;
- (String const&)onCreateNewTab:(StringView)html
url:(URL::URL const&)url
activateTab:(Web::HTML::ActivateTab)activate_tab;
- (String const&)onCreateChildTab:(Optional<URL::URL> const&)url
activateTab:(Web::HTML::ActivateTab)activate_tab
pageIndex:(u64)page_index;
- (void)loadURL:(URL::URL const&)url;
- (void)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)is_redirect;
- (void)onLoadFinish:(URL::URL const&)url;
- (void)onURLChange:(URL::URL const&)url;
- (void)onBackNavigationEnabled:(BOOL)back_enabled
forwardNavigationEnabled:(BOOL)forward_enabled;
- (void)onTitleChange:(ByteString const&)title;
- (void)onFaviconChange:(Gfx::Bitmap const&)bitmap;
- (void)onAudioPlayStateChange:(Web::HTML::AudioPlayState)play_state;
- (void)onFindInPageResult:(size_t)current_match_index
totalMatchCount:(Optional<size_t> const&)total_match_count;
@end
@interface LadybirdWebView : NSView <NSMenuDelegate>
- (instancetype)init:(id<LadybirdWebViewObserver>)observer;
- (instancetype)initAsChild:(id<LadybirdWebViewObserver>)observer
parent:(LadybirdWebView*)parent
pageIndex:(u64)page_index;
- (void)loadURL:(URL::URL const&)url;
- (void)loadHTML:(StringView)html;
- (void)navigateBack;
- (void)navigateForward;
- (void)reload;
- (WebView::ViewImplementation&)view;
- (String const&)handle;
- (void)setWindowPosition:(Gfx::IntPoint)position;
- (void)setWindowSize:(Gfx::IntSize)size;
- (void)handleResize;
- (void)handleDevicePixelRatioChange;
- (void)handleVisibility:(BOOL)is_visible;
- (void)setPreferredColorScheme:(Web::CSS::PreferredColorScheme)color_scheme;
- (void)setPreferredContrast:(Web::CSS::PreferredContrast)contrast;
- (void)setPreferredMotion:(Web::CSS::PreferredMotion)motion;
- (void)findInPage:(NSString*)query
caseSensitivity:(CaseSensitivity)case_sensitivity;
- (void)findInPageNextMatch;
- (void)findInPagePreviousMatch;
- (void)zoomIn;
- (void)zoomOut;
- (void)resetZoom;
- (float)zoomLevel;
- (void)debugRequest:(ByteString const&)request argument:(ByteString const&)argument;
- (void)setEnableAutoplay:(BOOL)enabled;
- (void)viewSource;
@end

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,198 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <Interface/LadybirdWebViewBridge.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibGfx/Rect.h>
#include <LibIPC/File.h>
#include <LibWeb/Crypto/Crypto.h>
#include <LibWebView/Application.h>
#include <LibWebView/UserAgent.h>
#include <UI/HelperProcess.h>
#include <UI/Utilities.h>
#import <Interface/Palette.h>
namespace Ladybird {
template<typename T>
static T scale_for_device(T size, float device_pixel_ratio)
{
return size.template to_type<float>().scaled(device_pixel_ratio).template to_type<int>();
}
ErrorOr<NonnullOwnPtr<WebViewBridge>> WebViewBridge::create(Vector<Web::DevicePixelRect> screen_rects, float device_pixel_ratio, Web::CSS::PreferredColorScheme preferred_color_scheme, Web::CSS::PreferredContrast preferred_contrast, Web::CSS::PreferredMotion preferred_motion)
{
return adopt_nonnull_own_or_enomem(new (nothrow) WebViewBridge(move(screen_rects), device_pixel_ratio, preferred_color_scheme, preferred_contrast, preferred_motion));
}
WebViewBridge::WebViewBridge(Vector<Web::DevicePixelRect> screen_rects, float device_pixel_ratio, Web::CSS::PreferredColorScheme preferred_color_scheme, Web::CSS::PreferredContrast preferred_contrast, Web::CSS::PreferredMotion preferred_motion)
: m_screen_rects(move(screen_rects))
, m_preferred_color_scheme(preferred_color_scheme)
, m_preferred_contrast(preferred_contrast)
, m_preferred_motion(preferred_motion)
{
m_device_pixel_ratio = device_pixel_ratio;
}
WebViewBridge::~WebViewBridge() = default;
void WebViewBridge::set_device_pixel_ratio(float device_pixel_ratio)
{
m_device_pixel_ratio = device_pixel_ratio;
client().async_set_device_pixels_per_css_pixel(m_client_state.page_index, m_device_pixel_ratio * m_zoom_level);
}
void WebViewBridge::set_system_visibility_state(bool is_visible)
{
client().async_set_system_visibility_state(m_client_state.page_index, is_visible);
}
void WebViewBridge::set_viewport_rect(Gfx::IntRect viewport_rect)
{
viewport_rect.set_size(scale_for_device(viewport_rect.size(), m_device_pixel_ratio));
m_viewport_size = viewport_rect.size();
handle_resize();
}
void WebViewBridge::update_palette()
{
auto theme = create_system_palette();
client().async_update_system_theme(m_client_state.page_index, move(theme));
}
void WebViewBridge::set_preferred_color_scheme(Web::CSS::PreferredColorScheme color_scheme)
{
m_preferred_color_scheme = color_scheme;
client().async_set_preferred_color_scheme(m_client_state.page_index, color_scheme);
}
void WebViewBridge::set_preferred_contrast(Web::CSS::PreferredContrast contrast)
{
m_preferred_contrast = contrast;
client().async_set_preferred_contrast(m_client_state.page_index, contrast);
}
void WebViewBridge::set_preferred_motion(Web::CSS::PreferredMotion motion)
{
m_preferred_motion = motion;
client().async_set_preferred_motion(m_client_state.page_index, motion);
}
void WebViewBridge::enqueue_input_event(Web::MouseEvent event)
{
event.position = to_content_position(event.position.to_type<int>()).to_type<Web::DevicePixels>();
event.screen_position = to_content_position(event.screen_position.to_type<int>()).to_type<Web::DevicePixels>();
ViewImplementation::enqueue_input_event(move(event));
}
void WebViewBridge::enqueue_input_event(Web::DragEvent event)
{
event.position = to_content_position(event.position.to_type<int>()).to_type<Web::DevicePixels>();
event.screen_position = to_content_position(event.screen_position.to_type<int>()).to_type<Web::DevicePixels>();
ViewImplementation::enqueue_input_event(move(event));
}
void WebViewBridge::enqueue_input_event(Web::KeyEvent event)
{
ViewImplementation::enqueue_input_event(move(event));
}
void WebViewBridge::set_enable_autoplay(bool enabled)
{
ViewImplementation::set_enable_autoplay(enabled);
}
Optional<WebViewBridge::Paintable> WebViewBridge::paintable()
{
Gfx::Bitmap* bitmap = nullptr;
Gfx::IntSize bitmap_size;
if (m_client_state.has_usable_bitmap) {
bitmap = m_client_state.front_bitmap.bitmap.ptr();
bitmap_size = m_client_state.front_bitmap.last_painted_size.to_type<int>();
} else {
bitmap = m_backup_bitmap.ptr();
bitmap_size = m_backup_bitmap_size.to_type<int>();
}
if (!bitmap)
return {};
return Paintable { *bitmap, bitmap_size };
}
void WebViewBridge::update_zoom()
{
client().async_set_device_pixels_per_css_pixel(m_client_state.page_index, m_device_pixel_ratio * m_zoom_level);
if (on_zoom_level_changed)
on_zoom_level_changed();
}
Web::DevicePixelSize WebViewBridge::viewport_size() const
{
return m_viewport_size.to_type<Web::DevicePixels>();
}
Gfx::IntPoint WebViewBridge::to_content_position(Gfx::IntPoint widget_position) const
{
return scale_for_device(widget_position, m_device_pixel_ratio);
}
Gfx::IntPoint WebViewBridge::to_widget_position(Gfx::IntPoint content_position) const
{
return scale_for_device(content_position, inverse_device_pixel_ratio());
}
void WebViewBridge::initialize_client(CreateNewClient create_new_client)
{
VERIFY(on_request_web_content);
if (create_new_client == CreateNewClient::Yes) {
m_client_state = {};
m_client_state.client = on_request_web_content();
} else {
m_client_state.client->register_view(m_client_state.page_index, *this);
}
m_client_state.client->on_web_content_process_crash = [this] {
Core::deferred_invoke([this] {
handle_web_content_process_crash();
});
};
m_client_state.client_handle = MUST(Web::Crypto::generate_random_uuid());
client().async_set_window_handle(m_client_state.page_index, m_client_state.client_handle);
client().async_set_device_pixels_per_css_pixel(m_client_state.page_index, m_device_pixel_ratio);
client().async_set_preferred_color_scheme(m_client_state.page_index, m_preferred_color_scheme);
update_palette();
if (!m_screen_rects.is_empty()) {
// FIXME: Update the screens again if they ever change.
client().async_update_screen_rects(m_client_state.page_index, m_screen_rects, 0);
}
if (auto const& webdriver_content_ipc_path = WebView::Application::chrome_options().webdriver_content_ipc_path; webdriver_content_ipc_path.has_value()) {
client().async_connect_to_webdriver(m_client_state.page_index, *webdriver_content_ipc_path);
}
if (auto const& user_agent_preset = WebView::Application::web_content_options().user_agent_preset; user_agent_preset.has_value()) {
auto user_agent = *WebView::user_agents.get(*user_agent_preset);
client().async_debug_request(m_client_state.page_index, "spoof-user-agent"sv, user_agent);
}
}
void WebViewBridge::initialize_client_as_child(WebViewBridge const& parent, u64 page_index)
{
m_client_state.client = parent.client();
m_client_state.page_index = page_index;
initialize_client(CreateNewClient::No);
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Vector.h>
#include <LibGfx/Point.h>
#include <LibGfx/Rect.h>
#include <LibGfx/Size.h>
#include <LibGfx/StandardCursor.h>
#include <LibWeb/CSS/PreferredColorScheme.h>
#include <LibWeb/CSS/PreferredContrast.h>
#include <LibWeb/CSS/PreferredMotion.h>
#include <LibWeb/Page/InputEvent.h>
#include <LibWebView/ViewImplementation.h>
namespace Ladybird {
class WebViewBridge final : public WebView::ViewImplementation {
public:
static ErrorOr<NonnullOwnPtr<WebViewBridge>> create(Vector<Web::DevicePixelRect> screen_rects, float device_pixel_ratio, Web::CSS::PreferredColorScheme, Web::CSS::PreferredContrast, Web::CSS::PreferredMotion);
virtual ~WebViewBridge() override;
virtual void initialize_client(CreateNewClient = CreateNewClient::Yes) override;
void initialize_client_as_child(WebViewBridge const& parent, u64 page_index);
float device_pixel_ratio() const { return m_device_pixel_ratio; }
void set_device_pixel_ratio(float device_pixel_ratio);
float inverse_device_pixel_ratio() const { return 1.0f / m_device_pixel_ratio; }
void set_system_visibility_state(bool is_visible);
void set_viewport_rect(Gfx::IntRect);
void update_palette();
void set_preferred_color_scheme(Web::CSS::PreferredColorScheme);
void set_preferred_contrast(Web::CSS::PreferredContrast);
void set_preferred_motion(Web::CSS::PreferredMotion);
void enqueue_input_event(Web::MouseEvent);
void enqueue_input_event(Web::DragEvent);
void enqueue_input_event(Web::KeyEvent);
void set_enable_autoplay(bool enabled);
struct Paintable {
Gfx::Bitmap& bitmap;
Gfx::IntSize bitmap_size;
};
Optional<Paintable> paintable();
Function<NonnullRefPtr<WebView::WebContentClient>()> on_request_web_content;
Function<void()> on_zoom_level_changed;
private:
WebViewBridge(Vector<Web::DevicePixelRect> screen_rects, float device_pixel_ratio, Web::CSS::PreferredColorScheme, Web::CSS::PreferredContrast, Web::CSS::PreferredMotion);
virtual void update_zoom() override;
virtual Web::DevicePixelSize viewport_size() const override;
virtual Gfx::IntPoint to_content_position(Gfx::IntPoint widget_position) const override;
virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const override;
Vector<Web::DevicePixelRect> m_screen_rects;
Gfx::IntSize m_viewport_size;
Web::CSS::PreferredColorScheme m_preferred_color_scheme { Web::CSS::PreferredColorScheme::Auto };
Web::CSS::PreferredContrast m_preferred_contrast { Web::CSS::PreferredContrast::Auto };
Web::CSS::PreferredMotion m_preferred_motion { Web::CSS::PreferredMotion::Auto };
};
}

View file

@ -0,0 +1,20 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#import <Cocoa/Cocoa.h>
@class LadybirdWebView;
@interface LadybirdWebViewWindow : NSWindow
- (instancetype)initWithWebView:(LadybirdWebView*)web_view
windowRect:(NSRect)window_rect;
@property (nonatomic, strong) LadybirdWebView* web_view;
@end

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#import <Interface/LadybirdWebView.h>
#import <Interface/LadybirdWebViewWindow.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
@interface LadybirdWebViewWindow ()
@end
@implementation LadybirdWebViewWindow
- (instancetype)initWithWebView:(LadybirdWebView*)web_view
windowRect:(NSRect)window_rect
{
static constexpr auto style_mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
self = [super initWithContentRect:window_rect
styleMask:style_mask
backing:NSBackingStoreBuffered
defer:NO];
if (self) {
self.web_view = web_view;
if (self.web_view == nil)
self.web_view = [[LadybirdWebView alloc] init:nil];
[self.web_view setClipsToBounds:YES];
}
return self;
}
#pragma mark - NSWindow
- (void)setIsVisible:(BOOL)flag
{
[self.web_view handleVisibility:flag];
[super setIsVisible:flag];
}
- (void)setIsMiniaturized:(BOOL)flag
{
[self.web_view handleVisibility:!flag];
[super setIsMiniaturized:flag];
}
@end

View file

@ -0,0 +1,16 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibCore/AnonymousBuffer.h>
namespace Ladybird {
bool is_using_dark_system_theme();
Core::AnonymousBuffer create_system_palette();
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ByteString.h>
#include <LibCore/Resource.h>
#include <LibGfx/Palette.h>
#include <LibGfx/SystemTheme.h>
#include <UI/Utilities.h>
#import <Cocoa/Cocoa.h>
#import <Interface/Palette.h>
#import <Utilities/Conversions.h>
namespace Ladybird {
bool is_using_dark_system_theme()
{
auto* appearance = [NSApp effectiveAppearance];
auto* matched_appearance = [appearance bestMatchFromAppearancesWithNames:@[
NSAppearanceNameAqua,
NSAppearanceNameDarkAqua,
]];
return [matched_appearance isEqualToString:NSAppearanceNameDarkAqua];
}
Core::AnonymousBuffer create_system_palette()
{
auto is_dark = is_using_dark_system_theme();
auto theme_file = is_dark ? "Dark"sv : "Default"sv;
auto theme_ini = MUST(Core::Resource::load_from_uri(MUST(String::formatted("resource://themes/{}.ini", theme_file))));
auto theme = Gfx::load_system_theme(theme_ini->filesystem_path().to_byte_string()).release_value_but_fixme_should_propagate_errors();
auto palette_impl = Gfx::PaletteImpl::create_with_anonymous_buffer(theme);
auto palette = Gfx::Palette(move(palette_impl));
palette.set_flag(Gfx::FlagRole::IsDark, is_dark);
palette.set_color(Gfx::ColorRole::Accent, ns_color_to_gfx_color([NSColor controlAccentColor]));
// FIXME: There are more system colors we currently don't use (https://developer.apple.com/documentation/appkit/nscolor/3000782-controlaccentcolor?language=objc)
return theme;
}
}

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#import <Cocoa/Cocoa.h>
@interface SearchPanel : NSStackView
- (void)find:(id)selector;
- (void)findNextMatch:(id)selector;
- (void)findPreviousMatch:(id)selector;
- (void)useSelectionForFind:(id)selector;
- (void)onFindInPageResult:(size_t)current_match_index
totalMatchCount:(Optional<size_t> const&)total_match_count;
@end

View file

@ -0,0 +1,225 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <Interface/LadybirdWebViewBridge.h>
#import <Interface/LadybirdWebView.h>
#import <Interface/SearchPanel.h>
#import <Interface/Tab.h>
#import <Utilities/Conversions.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
static constexpr CGFloat const SEARCH_FIELD_HEIGHT = 30;
static constexpr CGFloat const SEARCH_FIELD_WIDTH = 300;
@interface SearchPanel () <NSSearchFieldDelegate>
{
CaseSensitivity m_case_sensitivity;
}
@property (nonatomic, strong) NSSearchField* search_field;
@property (nonatomic, strong) NSButton* search_match_case;
@property (nonatomic, strong) NSTextField* result_label;
@end
@implementation SearchPanel
- (instancetype)init
{
if (self = [super init]) {
self.search_field = [[NSSearchField alloc] init];
[self.search_field setPlaceholderString:@"Search"];
[self.search_field setDelegate:self];
auto* search_previous = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameGoLeftTemplate]
target:self
action:@selector(findPreviousMatch:)];
[search_previous setToolTip:@"Find Previous Match"];
[search_previous setBordered:NO];
auto* search_next = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameGoRightTemplate]
target:self
action:@selector(findNextMatch:)];
[search_next setToolTip:@"Find Next Match"];
[search_next setBordered:NO];
self.search_match_case = [NSButton checkboxWithTitle:@"Match Case"
target:self
action:@selector(find:)];
[self.search_match_case setState:NSControlStateValueOff];
m_case_sensitivity = CaseSensitivity::CaseInsensitive;
self.result_label = [NSTextField labelWithString:@""];
[self.result_label setHidden:YES];
auto* search_done = [NSButton buttonWithTitle:@"Done"
target:self
action:@selector(cancelSearch:)];
[search_done setToolTip:@"Close Search Bar"];
[search_done setBezelStyle:NSBezelStyleAccessoryBarAction];
[self addView:self.search_field inGravity:NSStackViewGravityLeading];
[self addView:search_previous inGravity:NSStackViewGravityLeading];
[self addView:search_next inGravity:NSStackViewGravityLeading];
[self addView:self.search_match_case inGravity:NSStackViewGravityLeading];
[self addView:self.result_label inGravity:NSStackViewGravityLeading];
[self addView:search_done inGravity:NSStackViewGravityTrailing];
[self setOrientation:NSUserInterfaceLayoutOrientationHorizontal];
[self setEdgeInsets:NSEdgeInsets { 0, 8, 0, 8 }];
[[self heightAnchor] constraintEqualToConstant:SEARCH_FIELD_HEIGHT].active = YES;
[[self.search_field widthAnchor] constraintEqualToConstant:SEARCH_FIELD_WIDTH].active = YES;
}
return self;
}
#pragma mark - Public methods
- (void)find:(id)sender
{
[self setHidden:NO];
[self setSearchTextFromPasteBoard];
[self.window makeFirstResponder:self.search_field];
}
- (void)findNextMatch:(id)sender
{
if ([self setSearchTextFromPasteBoard]) {
return;
}
[[[self tab] web_view] findInPageNextMatch];
}
- (void)findPreviousMatch:(id)sender
{
if ([self setSearchTextFromPasteBoard]) {
return;
}
[[[self tab] web_view] findInPagePreviousMatch];
}
- (void)useSelectionForFind:(id)sender
{
auto selected_text = [[[self tab] web_view] view].selected_text();
auto* query = Ladybird::string_to_ns_string(selected_text);
[self setPasteBoardContents:query];
if (![self isHidden]) {
[self.search_field setStringValue:query];
[[[self tab] web_view] findInPage:query caseSensitivity:m_case_sensitivity];
[self.window makeFirstResponder:self.search_field];
}
}
- (void)onFindInPageResult:(size_t)current_match_index
totalMatchCount:(Optional<size_t> const&)total_match_count
{
if (total_match_count.has_value()) {
auto* label_text = *total_match_count > 0
? [NSString stringWithFormat:@"%zu of %zu matches", current_match_index, *total_match_count]
: @"Phrase not found";
auto* label_attributes = @{
NSFontAttributeName : [NSFont boldSystemFontOfSize:12.0f],
};
auto* label_attribute = [[NSAttributedString alloc] initWithString:label_text
attributes:label_attributes];
[self.result_label setAttributedStringValue:label_attribute];
[self.result_label setHidden:NO];
} else {
[self.result_label setHidden:YES];
}
}
#pragma mark - Private methods
- (Tab*)tab
{
return (Tab*)[self window];
}
- (void)setPasteBoardContents:(NSString*)query
{
auto* paste_board = [NSPasteboard pasteboardWithName:NSPasteboardNameFind];
[paste_board clearContents];
[paste_board setString:query forType:NSPasteboardTypeString];
}
- (BOOL)setSearchTextFromPasteBoard
{
auto* paste_board = [NSPasteboard pasteboardWithName:NSPasteboardNameFind];
auto* query = [paste_board stringForType:NSPasteboardTypeString];
if (query) {
auto case_sensitivity = [self.search_match_case state] == NSControlStateValueOff
? CaseSensitivity::CaseInsensitive
: CaseSensitivity::CaseSensitive;
if (case_sensitivity != m_case_sensitivity || ![[self.search_field stringValue] isEqual:query]) {
[self.search_field setStringValue:query];
m_case_sensitivity = case_sensitivity;
[[[self tab] web_view] findInPage:query caseSensitivity:m_case_sensitivity];
return YES;
}
}
return NO;
}
- (void)cancelSearch:(id)sender
{
[self setHidden:YES];
}
#pragma mark - NSSearchFieldDelegate
- (void)controlTextDidChange:(NSNotification*)notification
{
auto* query = [self.search_field stringValue];
[[[self tab] web_view] findInPage:query caseSensitivity:m_case_sensitivity];
[self setPasteBoardContents:query];
}
- (BOOL)control:(NSControl*)control
textView:(NSTextView*)text_view
doCommandBySelector:(SEL)selector
{
if (selector == @selector(insertNewline:)) {
NSEvent* event = [[self tab] currentEvent];
if ((event.modifierFlags & NSEventModifierFlagShift) == 0) {
[self findNextMatch:nil];
} else {
[self findPreviousMatch:nil];
}
return YES;
}
if (selector == @selector(cancelOperation:)) {
[self cancelSearch:nil];
return YES;
}
return NO;
}
@end

27
UI/AppKit/Interface/Tab.h Normal file
View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Types.h>
#import <Cocoa/Cocoa.h>
#import <Interface/LadybirdWebViewWindow.h>
@class LadybirdWebView;
@interface Tab : LadybirdWebViewWindow
- (instancetype)init;
- (instancetype)initAsChild:(Tab*)parent
pageIndex:(u64)page_index;
- (void)tabWillClose;
- (void)openInspector:(id)sender;
- (void)onInspectorClosed;
@end

396
UI/AppKit/Interface/Tab.mm Normal file
View file

@ -0,0 +1,396 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ByteString.h>
#include <AK/String.h>
#include <LibCore/Resource.h>
#include <LibGfx/ImageFormats/PNGWriter.h>
#include <LibGfx/ShareableBitmap.h>
#include <LibURL/URL.h>
#include <LibWebView/ViewImplementation.h>
#include <UI/Utilities.h>
#import <Application/ApplicationDelegate.h>
#import <Interface/Inspector.h>
#import <Interface/InspectorController.h>
#import <Interface/LadybirdWebView.h>
#import <Interface/SearchPanel.h>
#import <Interface/Tab.h>
#import <Interface/TabController.h>
#import <Utilities/Conversions.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
static constexpr CGFloat const WINDOW_WIDTH = 1000;
static constexpr CGFloat const WINDOW_HEIGHT = 800;
@interface Tab () <LadybirdWebViewObserver>
@property (nonatomic, strong) NSString* title;
@property (nonatomic, strong) NSImage* favicon;
@property (nonatomic, strong) SearchPanel* search_panel;
@property (nonatomic, strong) InspectorController* inspector_controller;
@end
@implementation Tab
@dynamic title;
+ (NSImage*)defaultFavicon
{
static NSImage* default_favicon;
static dispatch_once_t token;
dispatch_once(&token, ^{
auto default_favicon_path = MUST(Core::Resource::load_from_uri("resource://icons/48x48/app-browser.png"sv));
auto* ns_default_favicon_path = Ladybird::string_to_ns_string(default_favicon_path->filesystem_path());
default_favicon = [[NSImage alloc] initWithContentsOfFile:ns_default_favicon_path];
});
return default_favicon;
}
- (instancetype)init
{
auto* web_view = [[LadybirdWebView alloc] init:self];
return [self initWithWebView:web_view];
}
- (instancetype)initAsChild:(Tab*)parent
pageIndex:(u64)page_index
{
auto* web_view = [[LadybirdWebView alloc] initAsChild:self parent:[parent web_view] pageIndex:page_index];
return [self initWithWebView:web_view];
}
- (instancetype)initWithWebView:(LadybirdWebView*)web_view
{
auto screen_rect = [[NSScreen mainScreen] frame];
auto position_x = (NSWidth(screen_rect) - WINDOW_WIDTH) / 2;
auto position_y = (NSHeight(screen_rect) - WINDOW_HEIGHT) / 2;
auto window_rect = NSMakeRect(position_x, position_y, WINDOW_WIDTH, WINDOW_HEIGHT);
if (self = [super initWithWebView:web_view windowRect:window_rect]) {
// Remember last window position
self.frameAutosaveName = @"window";
self.favicon = [Tab defaultFavicon];
self.title = @"New Tab";
[self updateTabTitleAndFavicon];
[self setTitleVisibility:NSWindowTitleHidden];
[self setIsVisible:YES];
self.search_panel = [[SearchPanel alloc] init];
[self.search_panel setHidden:YES];
auto* stack_view = [NSStackView stackViewWithViews:@[
self.search_panel,
self.web_view,
]];
[stack_view setOrientation:NSUserInterfaceLayoutOrientationVertical];
[stack_view setSpacing:0];
[self setContentView:stack_view];
[[self.search_panel leadingAnchor] constraintEqualToAnchor:[self.contentView leadingAnchor]].active = YES;
}
return self;
}
#pragma mark - Public methods
- (void)find:(id)sender
{
[self.search_panel find:sender];
}
- (void)findNextMatch:(id)sender
{
[self.search_panel findNextMatch:sender];
}
- (void)findPreviousMatch:(id)sender
{
[self.search_panel findPreviousMatch:sender];
}
- (void)useSelectionForFind:(id)sender
{
[self.search_panel useSelectionForFind:sender];
}
- (void)tabWillClose
{
if (self.inspector_controller != nil) {
[self.inspector_controller.window close];
}
}
- (void)openInspector:(id)sender
{
if (self.inspector_controller != nil) {
[self.inspector_controller.window makeKeyAndOrderFront:sender];
return;
}
self.inspector_controller = [[InspectorController alloc] init:self];
[self.inspector_controller showWindow:nil];
}
- (void)onInspectorClosed
{
self.inspector_controller = nil;
}
- (void)inspectElement:(id)sender
{
[self openInspector:sender];
auto* inspector = (Inspector*)[self.inspector_controller window];
[inspector selectHoveredElement];
}
#pragma mark - Private methods
- (TabController*)tabController
{
return (TabController*)[self windowController];
}
- (void)updateTabTitleAndFavicon
{
static constexpr CGFloat TITLE_FONT_SIZE = 12;
static constexpr CGFloat FAVICON_SIZE = 16;
NSFont* title_font = [NSFont systemFontOfSize:TITLE_FONT_SIZE];
auto* favicon_attachment = [[NSTextAttachment alloc] init];
favicon_attachment.image = self.favicon;
// By default, the image attachment will "automatically adapt to the surrounding font and color
// attributes in attributed strings". Therefore, we specify a clear color here to prevent the
// favicon from having a weird tint.
auto* favicon_attribute = (NSMutableAttributedString*)[NSMutableAttributedString attributedStringWithAttachment:favicon_attachment];
[favicon_attribute addAttribute:NSForegroundColorAttributeName
value:[NSColor clearColor]
range:NSMakeRange(0, [favicon_attribute length])];
// adjust the favicon image to middle center the title text
CGFloat offset_y = (title_font.capHeight - FAVICON_SIZE) / 2.f;
[favicon_attachment setBounds:CGRectMake(0, offset_y, FAVICON_SIZE, FAVICON_SIZE)];
auto* title_attributes = @{
NSForegroundColorAttributeName : [NSColor textColor],
NSFontAttributeName : title_font
};
auto* title_attribute = [[NSAttributedString alloc] initWithString:self.title
attributes:title_attributes];
auto* spacing_attribute = [[NSAttributedString alloc] initWithString:@" "
attributes:title_attributes];
auto* title_and_favicon = [[NSMutableAttributedString alloc] init];
[title_and_favicon appendAttributedString:favicon_attribute];
[title_and_favicon appendAttributedString:spacing_attribute];
[title_and_favicon appendAttributedString:title_attribute];
[[self tab] setAttributedTitle:title_and_favicon];
}
- (void)togglePageMuteState:(id)button
{
auto& view = [[self web_view] view];
view.toggle_page_mute_state();
switch (view.audio_play_state()) {
case Web::HTML::AudioPlayState::Paused:
[[self tab] setAccessoryView:nil];
break;
case Web::HTML::AudioPlayState::Playing:
[button setImage:[self iconForPageMuteState]];
[button setToolTip:[self toolTipForPageMuteState]];
break;
}
}
- (NSImage*)iconForPageMuteState
{
auto& view = [[self web_view] view];
switch (view.page_mute_state()) {
case Web::HTML::MuteState::Muted:
return [NSImage imageNamed:NSImageNameTouchBarAudioOutputVolumeOffTemplate];
case Web::HTML::MuteState::Unmuted:
return [NSImage imageNamed:NSImageNameTouchBarAudioOutputVolumeHighTemplate];
}
VERIFY_NOT_REACHED();
}
- (NSString*)toolTipForPageMuteState
{
auto& view = [[self web_view] view];
switch (view.page_mute_state()) {
case Web::HTML::MuteState::Muted:
return @"Unmute tab";
case Web::HTML::MuteState::Unmuted:
return @"Mute tab";
}
VERIFY_NOT_REACHED();
}
#pragma mark - LadybirdWebViewObserver
- (String const&)onCreateNewTab:(Optional<URL::URL> const&)url
activateTab:(Web::HTML::ActivateTab)activate_tab
{
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
auto* controller = [delegate createNewTab:url
fromTab:self
activateTab:activate_tab];
auto* tab = (Tab*)[controller window];
return [[tab web_view] handle];
}
- (String const&)onCreateNewTab:(StringView)html
url:(URL::URL const&)url
activateTab:(Web::HTML::ActivateTab)activate_tab
{
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
auto* controller = [delegate createNewTab:html
url:url
fromTab:self
activateTab:activate_tab];
auto* tab = (Tab*)[controller window];
return [[tab web_view] handle];
}
- (String const&)onCreateChildTab:(Optional<URL::URL> const&)url
activateTab:(Web::HTML::ActivateTab)activate_tab
pageIndex:(u64)page_index
{
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
auto* controller = [delegate createChildTab:url
fromTab:self
activateTab:activate_tab
pageIndex:page_index];
auto* tab = (Tab*)[controller window];
return [[tab web_view] handle];
}
- (void)loadURL:(URL::URL const&)url
{
[[self tabController] loadURL:url];
}
- (void)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)is_redirect
{
self.title = Ladybird::string_to_ns_string(url.serialize());
self.favicon = [Tab defaultFavicon];
[self updateTabTitleAndFavicon];
[[self tabController] onLoadStart:url isRedirect:is_redirect];
if (self.inspector_controller != nil) {
auto* inspector = (Inspector*)[self.inspector_controller window];
[inspector reset];
}
}
- (void)onLoadFinish:(URL::URL const&)url
{
if (self.inspector_controller != nil) {
auto* inspector = (Inspector*)[self.inspector_controller window];
[inspector inspect];
}
}
- (void)onURLChange:(URL::URL const&)url
{
[[self tabController] onURLChange:url];
}
- (void)onBackNavigationEnabled:(BOOL)back_enabled
forwardNavigationEnabled:(BOOL)forward_enabled
{
[[self tabController] onBackNavigationEnabled:back_enabled
forwardNavigationEnabled:forward_enabled];
}
- (void)onTitleChange:(ByteString const&)title
{
[[self tabController] onTitleChange:title];
self.title = Ladybird::string_to_ns_string(title);
[self updateTabTitleAndFavicon];
}
- (void)onFaviconChange:(Gfx::Bitmap const&)bitmap
{
auto png = Gfx::PNGWriter::encode(bitmap);
if (png.is_error()) {
return;
}
auto* data = [NSData dataWithBytes:png.value().data()
length:png.value().size()];
auto* favicon = [[NSImage alloc] initWithData:data];
[favicon setResizingMode:NSImageResizingModeStretch];
self.favicon = favicon;
[self updateTabTitleAndFavicon];
}
- (void)onAudioPlayStateChange:(Web::HTML::AudioPlayState)play_state
{
auto& view = [[self web_view] view];
switch (play_state) {
case Web::HTML::AudioPlayState::Paused:
if (view.page_mute_state() == Web::HTML::MuteState::Unmuted) {
[[self tab] setAccessoryView:nil];
}
break;
case Web::HTML::AudioPlayState::Playing:
auto* button = [NSButton buttonWithImage:[self iconForPageMuteState]
target:self
action:@selector(togglePageMuteState:)];
[button setToolTip:[self toolTipForPageMuteState]];
[[self tab] setAccessoryView:button];
break;
}
}
- (void)onFindInPageResult:(size_t)current_match_index
totalMatchCount:(Optional<size_t> const&)total_match_count
{
[self.search_panel onFindInPageResult:current_match_index
totalMatchCount:total_match_count];
}
@end

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#include <LibURL/URL.h>
#import <Cocoa/Cocoa.h>
@class Tab;
struct TabSettings {
BOOL should_show_line_box_borders { NO };
BOOL scripting_enabled { YES };
BOOL block_popups { YES };
BOOL autoplay_enabled { NO };
BOOL same_origin_policy_enabled { NO };
ByteString user_agent_name { "Disabled"sv };
ByteString navigator_compatibility_mode { "chrome"sv };
};
@interface TabController : NSWindowController <NSWindowDelegate>
- (instancetype)init;
- (instancetype)initAsChild:(Tab*)parent
pageIndex:(u64)page_index;
- (void)loadURL:(URL::URL const&)url;
- (void)loadHTML:(StringView)html url:(URL::URL const&)url;
- (void)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)isRedirect;
- (void)onURLChange:(URL::URL const&)url;
- (void)onBackNavigationEnabled:(BOOL)back_enabled
forwardNavigationEnabled:(BOOL)forward_enabled;
- (void)onTitleChange:(ByteString const&)title;
- (void)onCreateNewTab;
- (void)navigateBack:(id)sender;
- (void)navigateForward:(id)sender;
- (void)reload:(id)sender;
- (void)clearHistory;
- (void)setPopupBlocking:(BOOL)block_popups;
- (void)setScripting:(BOOL)enabled;
- (void)setAutoplay:(BOOL)enabled;
- (void)debugRequest:(ByteString const&)request argument:(ByteString const&)argument;
- (void)focusLocationToolbarItem;
@end

View file

@ -0,0 +1,725 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Loader/UserAgent.h>
#include <LibWebView/Application.h>
#include <LibWebView/SearchEngine.h>
#include <LibWebView/URL.h>
#include <LibWebView/UserAgent.h>
#import <Application/ApplicationDelegate.h>
#import <Interface/LadybirdWebView.h>
#import <Interface/Tab.h>
#import <Interface/TabController.h>
#import <Utilities/Conversions.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
static NSString* const TOOLBAR_IDENTIFIER = @"Toolbar";
static NSString* const TOOLBAR_NAVIGATE_BACK_IDENTIFIER = @"ToolbarNavigateBackIdentifier";
static NSString* const TOOLBAR_NAVIGATE_FORWARD_IDENTIFIER = @"ToolbarNavigateForwardIdentifier";
static NSString* const TOOLBAR_RELOAD_IDENTIFIER = @"ToolbarReloadIdentifier";
static NSString* const TOOLBAR_LOCATION_IDENTIFIER = @"ToolbarLocationIdentifier";
static NSString* const TOOLBAR_ZOOM_IDENTIFIER = @"ToolbarZoomIdentifier";
static NSString* const TOOLBAR_NEW_TAB_IDENTIFIER = @"ToolbarNewTabIdentifier";
static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIdentifer";
@interface LocationSearchField : NSSearchField
- (BOOL)becomeFirstResponder;
@end
@implementation LocationSearchField
- (BOOL)becomeFirstResponder
{
BOOL result = [super becomeFirstResponder];
if (result)
[self performSelector:@selector(selectText:) withObject:self afterDelay:0];
return result;
}
@end
@interface TabController () <NSToolbarDelegate, NSSearchFieldDelegate>
{
u64 m_page_index;
ByteString m_title;
TabSettings m_settings;
bool m_can_navigate_back;
bool m_can_navigate_forward;
}
@property (nonatomic, strong) Tab* parent;
@property (nonatomic, strong) NSToolbar* toolbar;
@property (nonatomic, strong) NSArray* toolbar_identifiers;
@property (nonatomic, strong) NSToolbarItem* navigate_back_toolbar_item;
@property (nonatomic, strong) NSToolbarItem* navigate_forward_toolbar_item;
@property (nonatomic, strong) NSToolbarItem* reload_toolbar_item;
@property (nonatomic, strong) NSToolbarItem* location_toolbar_item;
@property (nonatomic, strong) NSToolbarItem* zoom_toolbar_item;
@property (nonatomic, strong) NSToolbarItem* new_tab_toolbar_item;
@property (nonatomic, strong) NSToolbarItem* tab_overview_toolbar_item;
@property (nonatomic, assign) NSLayoutConstraint* location_toolbar_item_width;
@end
@implementation TabController
@synthesize toolbar_identifiers = _toolbar_identifiers;
@synthesize navigate_back_toolbar_item = _navigate_back_toolbar_item;
@synthesize navigate_forward_toolbar_item = _navigate_forward_toolbar_item;
@synthesize reload_toolbar_item = _reload_toolbar_item;
@synthesize location_toolbar_item = _location_toolbar_item;
@synthesize zoom_toolbar_item = _zoom_toolbar_item;
@synthesize new_tab_toolbar_item = _new_tab_toolbar_item;
@synthesize tab_overview_toolbar_item = _tab_overview_toolbar_item;
- (instancetype)init
{
if (self = [super init]) {
self.toolbar = [[NSToolbar alloc] initWithIdentifier:TOOLBAR_IDENTIFIER];
[self.toolbar setDelegate:self];
[self.toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
[self.toolbar setAllowsUserCustomization:NO];
[self.toolbar setSizeMode:NSToolbarSizeModeRegular];
m_page_index = 0;
m_settings = {
.scripting_enabled = WebView::Application::chrome_options().disable_scripting == WebView::DisableScripting::Yes ? NO : YES,
.block_popups = WebView::Application::chrome_options().allow_popups == WebView::AllowPopups::Yes ? NO : YES,
.autoplay_enabled = WebView::Application::web_content_options().enable_autoplay == WebView::EnableAutoplay::Yes ? YES : NO,
};
if (auto const& user_agent_preset = WebView::Application::web_content_options().user_agent_preset; user_agent_preset.has_value())
m_settings.user_agent_name = *user_agent_preset;
m_can_navigate_back = false;
m_can_navigate_forward = false;
}
return self;
}
- (instancetype)initAsChild:(Tab*)parent
pageIndex:(u64)page_index
{
if (self = [self init]) {
self.parent = parent;
m_page_index = page_index;
}
return self;
}
#pragma mark - Public methods
- (void)loadURL:(URL::URL const&)url
{
[[self tab].web_view loadURL:url];
}
- (void)loadHTML:(StringView)html url:(URL::URL const&)url
{
[[self tab].web_view loadHTML:html];
}
- (void)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)isRedirect
{
[self setLocationFieldText:url.serialize()];
}
- (void)onURLChange:(URL::URL const&)url
{
[self setLocationFieldText:url.serialize()];
}
- (void)onBackNavigationEnabled:(BOOL)back_enabled
forwardNavigationEnabled:(BOOL)forward_enabled
{
m_can_navigate_back = back_enabled;
m_can_navigate_forward = forward_enabled;
[self updateNavigationButtonStates];
}
- (void)onTitleChange:(ByteString const&)title
{
m_title = title;
}
- (void)onCreateNewTab
{
[self setPopupBlocking:m_settings.block_popups];
[self setScripting:m_settings.scripting_enabled];
[self setAutoplay:m_settings.autoplay_enabled];
}
- (void)zoomIn:(id)sender
{
[[[self tab] web_view] zoomIn];
[self updateZoomButton];
}
- (void)zoomOut:(id)sender
{
[[[self tab] web_view] zoomOut];
[self updateZoomButton];
}
- (void)resetZoom:(id)sender
{
[[[self tab] web_view] resetZoom];
[self updateZoomButton];
}
- (void)navigateBack:(id)sender
{
[[[self tab] web_view] navigateBack];
}
- (void)navigateForward:(id)sender
{
[[[self tab] web_view] navigateForward];
}
- (void)reload:(id)sender
{
[[[self tab] web_view] reload];
}
- (void)clearHistory
{
// FIXME: Reimplement clearing history using WebContent's history.
}
- (void)debugRequest:(ByteString const&)request argument:(ByteString const&)argument
{
[[[self tab] web_view] debugRequest:request argument:argument];
}
- (void)viewSource:(id)sender
{
[[[self tab] web_view] viewSource];
}
- (void)focusLocationToolbarItem
{
[self.window makeFirstResponder:self.location_toolbar_item.view];
}
#pragma mark - Private methods
- (Tab*)tab
{
return (Tab*)[self window];
}
- (void)createNewTab:(id)sender
{
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
self.tab.titlebarAppearsTransparent = NO;
[delegate createNewTab:WebView::Application::chrome_options().new_tab_page_url
fromTab:[self tab]
activateTab:Web::HTML::ActivateTab::Yes];
self.tab.titlebarAppearsTransparent = YES;
}
- (void)setLocationFieldText:(StringView)url
{
NSMutableAttributedString* attributed_url;
auto* dark_attributes = @{
NSForegroundColorAttributeName : [NSColor systemGrayColor],
};
auto* highlight_attributes = @{
NSForegroundColorAttributeName : [NSColor textColor],
};
if (auto url_parts = WebView::break_url_into_parts(url); url_parts.has_value()) {
attributed_url = [[NSMutableAttributedString alloc] init];
auto* attributed_scheme_and_subdomain = [[NSAttributedString alloc]
initWithString:Ladybird::string_to_ns_string(url_parts->scheme_and_subdomain)
attributes:dark_attributes];
auto* attributed_effective_tld_plus_one = [[NSAttributedString alloc]
initWithString:Ladybird::string_to_ns_string(url_parts->effective_tld_plus_one)
attributes:highlight_attributes];
auto* attributed_remainder = [[NSAttributedString alloc]
initWithString:Ladybird::string_to_ns_string(url_parts->remainder)
attributes:dark_attributes];
[attributed_url appendAttributedString:attributed_scheme_and_subdomain];
[attributed_url appendAttributedString:attributed_effective_tld_plus_one];
[attributed_url appendAttributedString:attributed_remainder];
} else {
attributed_url = [[NSMutableAttributedString alloc]
initWithString:Ladybird::string_to_ns_string(url)
attributes:highlight_attributes];
}
auto* location_search_field = (LocationSearchField*)[self.location_toolbar_item view];
[location_search_field setAttributedStringValue:attributed_url];
}
- (void)updateNavigationButtonStates
{
auto* navigate_back_button = (NSButton*)[[self navigate_back_toolbar_item] view];
[navigate_back_button setEnabled:m_can_navigate_back];
auto* navigate_forward_button = (NSButton*)[[self navigate_forward_toolbar_item] view];
[navigate_forward_button setEnabled:m_can_navigate_forward];
}
- (void)showTabOverview:(id)sender
{
self.tab.titlebarAppearsTransparent = NO;
[self.window toggleTabOverview:sender];
self.tab.titlebarAppearsTransparent = YES;
}
- (void)updateZoomButton
{
auto zoom_level = [[[self tab] web_view] zoomLevel];
auto* zoom_level_text = [NSString stringWithFormat:@"%d%%", round_to<int>(zoom_level * 100.0f)];
[self.zoom_toolbar_item setTitle:zoom_level_text];
auto zoom_button_hidden = zoom_level == 1.0 ? YES : NO;
[[self.zoom_toolbar_item view] setHidden:zoom_button_hidden];
}
- (void)dumpDOMTree:(id)sender
{
[self debugRequest:"dump-dom-tree" argument:""];
}
- (void)dumpLayoutTree:(id)sender
{
[self debugRequest:"dump-layout-tree" argument:""];
}
- (void)dumpPaintTree:(id)sender
{
[self debugRequest:"dump-paint-tree" argument:""];
}
- (void)dumpStackingContextTree:(id)sender
{
[self debugRequest:"dump-stacking-context-tree" argument:""];
}
- (void)dumpStyleSheets:(id)sender
{
[self debugRequest:"dump-style-sheets" argument:""];
}
- (void)dumpAllResolvedStyles:(id)sender
{
[self debugRequest:"dump-all-resolved-styles" argument:""];
}
- (void)dumpHistory:(id)sender
{
[self debugRequest:"dump-session-history" argument:""];
}
- (void)dumpLocalStorage:(id)sender
{
[self debugRequest:"dump-local-storage" argument:""];
}
- (void)toggleLineBoxBorders:(id)sender
{
m_settings.should_show_line_box_borders = !m_settings.should_show_line_box_borders;
[self debugRequest:"set-line-box-borders" argument:m_settings.should_show_line_box_borders ? "on" : "off"];
}
- (void)collectGarbage:(id)sender
{
[self debugRequest:"collect-garbage" argument:""];
}
- (void)dumpGCGraph:(id)sender
{
[self debugRequest:"dump-gc-graph" argument:""];
}
- (void)clearCache:(id)sender
{
[self debugRequest:"clear-cache" argument:""];
}
- (void)toggleScripting:(id)sender
{
m_settings.scripting_enabled = !m_settings.scripting_enabled;
[self setScripting:m_settings.scripting_enabled];
}
- (void)setScripting:(BOOL)enabled
{
[self debugRequest:"scripting" argument:enabled ? "on" : "off"];
}
- (void)togglePopupBlocking:(id)sender
{
m_settings.block_popups = !m_settings.block_popups;
[self setPopupBlocking:m_settings.block_popups];
}
- (void)setPopupBlocking:(BOOL)block_popups
{
[self debugRequest:"block-pop-ups" argument:block_popups ? "on" : "off"];
}
- (void)toggleAutoplay:(id)sender
{
m_settings.autoplay_enabled = !m_settings.autoplay_enabled;
[self setAutoplay:m_settings.autoplay_enabled];
}
- (void)setAutoplay:(BOOL)enabled
{
[[[self tab] web_view] setEnableAutoplay:m_settings.autoplay_enabled];
}
- (void)toggleSameOriginPolicy:(id)sender
{
m_settings.same_origin_policy_enabled = !m_settings.same_origin_policy_enabled;
[self debugRequest:"same-origin-policy" argument:m_settings.same_origin_policy_enabled ? "on" : "off"];
}
- (void)setUserAgentSpoof:(NSMenuItem*)sender
{
ByteString const user_agent_name = [[sender title] UTF8String];
ByteString user_agent = "";
if (user_agent_name == "Disabled"sv) {
user_agent = Web::default_user_agent;
} else {
user_agent = WebView::user_agents.get(user_agent_name).value();
}
m_settings.user_agent_name = user_agent_name;
[self debugRequest:"spoof-user-agent" argument:user_agent];
[self debugRequest:"clear-cache" argument:""]; // clear the cache to ensure requests are re-done with the new user agent
}
- (void)setNavigatorCompatibilityMode:(NSMenuItem*)sender
{
ByteString const compatibility_mode = [[[sender title] lowercaseString] UTF8String];
m_settings.navigator_compatibility_mode = compatibility_mode;
[self debugRequest:"navigator-compatibility-mode" argument:compatibility_mode];
}
#pragma mark - Properties
- (NSButton*)create_button:(NSImageName)image
with_action:(nonnull SEL)action
with_tooltip:(NSString*)tooltip
{
auto* button = [NSButton buttonWithImage:[NSImage imageNamed:image]
target:self
action:action];
if (tooltip) {
[button setToolTip:tooltip];
}
[button setBordered:YES];
return button;
}
- (NSToolbarItem*)navigate_back_toolbar_item
{
if (!_navigate_back_toolbar_item) {
auto* button = [self create_button:NSImageNameGoBackTemplate
with_action:@selector(navigateBack:)
with_tooltip:@"Navigate back"];
[button setEnabled:NO];
_navigate_back_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_NAVIGATE_BACK_IDENTIFIER];
[_navigate_back_toolbar_item setView:button];
}
return _navigate_back_toolbar_item;
}
- (NSToolbarItem*)navigate_forward_toolbar_item
{
if (!_navigate_forward_toolbar_item) {
auto* button = [self create_button:NSImageNameGoForwardTemplate
with_action:@selector(navigateForward:)
with_tooltip:@"Navigate forward"];
[button setEnabled:NO];
_navigate_forward_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_NAVIGATE_FORWARD_IDENTIFIER];
[_navigate_forward_toolbar_item setView:button];
}
return _navigate_forward_toolbar_item;
}
- (NSToolbarItem*)reload_toolbar_item
{
if (!_reload_toolbar_item) {
auto* button = [self create_button:NSImageNameRefreshTemplate
with_action:@selector(reload:)
with_tooltip:@"Reload page"];
[button setEnabled:YES];
_reload_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_RELOAD_IDENTIFIER];
[_reload_toolbar_item setView:button];
}
return _reload_toolbar_item;
}
- (NSToolbarItem*)location_toolbar_item
{
if (!_location_toolbar_item) {
auto* location_search_field = [[LocationSearchField alloc] init];
[location_search_field setPlaceholderString:@"Enter web address"];
[location_search_field setTextColor:[NSColor textColor]];
[location_search_field setDelegate:self];
_location_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_LOCATION_IDENTIFIER];
[_location_toolbar_item setView:location_search_field];
}
return _location_toolbar_item;
}
- (NSToolbarItem*)zoom_toolbar_item
{
if (!_zoom_toolbar_item) {
auto* button = [NSButton buttonWithTitle:@"100%"
target:self
action:@selector(resetZoom:)];
[button setToolTip:@"Reset zoom level"];
[button setHidden:YES];
_zoom_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_ZOOM_IDENTIFIER];
[_zoom_toolbar_item setView:button];
}
return _zoom_toolbar_item;
}
- (NSToolbarItem*)new_tab_toolbar_item
{
if (!_new_tab_toolbar_item) {
auto* button = [self create_button:NSImageNameAddTemplate
with_action:@selector(createNewTab:)
with_tooltip:@"New tab"];
_new_tab_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_NEW_TAB_IDENTIFIER];
[_new_tab_toolbar_item setView:button];
}
return _new_tab_toolbar_item;
}
- (NSToolbarItem*)tab_overview_toolbar_item
{
if (!_tab_overview_toolbar_item) {
auto* button = [self create_button:NSImageNameIconViewTemplate
with_action:@selector(showTabOverview:)
with_tooltip:@"Show all tabs"];
_tab_overview_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_TAB_OVERVIEW_IDENTIFIER];
[_tab_overview_toolbar_item setView:button];
}
return _tab_overview_toolbar_item;
}
- (NSArray*)toolbar_identifiers
{
if (!_toolbar_identifiers) {
_toolbar_identifiers = @[
TOOLBAR_NAVIGATE_BACK_IDENTIFIER,
TOOLBAR_NAVIGATE_FORWARD_IDENTIFIER,
NSToolbarFlexibleSpaceItemIdentifier,
TOOLBAR_RELOAD_IDENTIFIER,
TOOLBAR_LOCATION_IDENTIFIER,
TOOLBAR_ZOOM_IDENTIFIER,
NSToolbarFlexibleSpaceItemIdentifier,
TOOLBAR_NEW_TAB_IDENTIFIER,
TOOLBAR_TAB_OVERVIEW_IDENTIFIER,
];
}
return _toolbar_identifiers;
}
#pragma mark - NSWindowController
- (IBAction)showWindow:(id)sender
{
self.window = self.parent
? [[Tab alloc] initAsChild:self.parent pageIndex:m_page_index]
: [[Tab alloc] init];
[self.window setDelegate:self];
[self.window setToolbar:self.toolbar];
[self.window setToolbarStyle:NSWindowToolbarStyleUnified];
[self.window makeKeyAndOrderFront:sender];
[self focusLocationToolbarItem];
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
[delegate setActiveTab:[self tab]];
}
#pragma mark - NSWindowDelegate
- (void)windowDidBecomeMain:(NSNotification*)notification
{
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
[delegate setActiveTab:[self tab]];
}
- (void)windowWillClose:(NSNotification*)notification
{
[[self tab] tabWillClose];
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
[delegate removeTab:self];
}
- (void)windowDidMove:(NSNotification*)notification
{
auto position = Ladybird::ns_point_to_gfx_point([[self tab] frame].origin);
[[[self tab] web_view] setWindowPosition:position];
}
- (void)windowDidResize:(NSNotification*)notification
{
if (self.location_toolbar_item_width != nil) {
self.location_toolbar_item_width.active = NO;
}
auto width = [self window].frame.size.width * 0.6;
self.location_toolbar_item_width = [[[self.location_toolbar_item view] widthAnchor] constraintEqualToConstant:width];
self.location_toolbar_item_width.active = YES;
[[[self tab] web_view] handleResize];
}
- (void)windowDidChangeBackingProperties:(NSNotification*)notification
{
[[[self tab] web_view] handleDevicePixelRatioChange];
}
- (BOOL)validateMenuItem:(NSMenuItem*)item
{
if ([item action] == @selector(toggleLineBoxBorders:)) {
[item setState:m_settings.should_show_line_box_borders ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(toggleScripting:)) {
[item setState:m_settings.scripting_enabled ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(togglePopupBlocking:)) {
[item setState:m_settings.block_popups ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(toggleSameOriginPolicy:)) {
[item setState:m_settings.same_origin_policy_enabled ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(setUserAgentSpoof:)) {
[item setState:(m_settings.user_agent_name == [[item title] UTF8String]) ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(setNavigatorCompatibilityMode:)) {
[item setState:(m_settings.navigator_compatibility_mode == [[[item title] lowercaseString] UTF8String]) ? NSControlStateValueOn : NSControlStateValueOff];
} else if ([item action] == @selector(toggleAutoplay:)) {
[item setState:m_settings.autoplay_enabled ? NSControlStateValueOn : NSControlStateValueOff];
}
return YES;
}
#pragma mark - NSToolbarDelegate
- (NSToolbarItem*)toolbar:(NSToolbar*)toolbar
itemForItemIdentifier:(NSString*)identifier
willBeInsertedIntoToolbar:(BOOL)flag
{
if ([identifier isEqual:TOOLBAR_NAVIGATE_BACK_IDENTIFIER]) {
return self.navigate_back_toolbar_item;
}
if ([identifier isEqual:TOOLBAR_NAVIGATE_FORWARD_IDENTIFIER]) {
return self.navigate_forward_toolbar_item;
}
if ([identifier isEqual:TOOLBAR_RELOAD_IDENTIFIER]) {
return self.reload_toolbar_item;
}
if ([identifier isEqual:TOOLBAR_LOCATION_IDENTIFIER]) {
return self.location_toolbar_item;
}
if ([identifier isEqual:TOOLBAR_ZOOM_IDENTIFIER]) {
return self.zoom_toolbar_item;
}
if ([identifier isEqual:TOOLBAR_NEW_TAB_IDENTIFIER]) {
return self.new_tab_toolbar_item;
}
if ([identifier isEqual:TOOLBAR_TAB_OVERVIEW_IDENTIFIER]) {
return self.tab_overview_toolbar_item;
}
return nil;
}
- (NSArray*)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
{
return self.toolbar_identifiers;
}
- (NSArray*)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
{
return self.toolbar_identifiers;
}
#pragma mark - NSSearchFieldDelegate
- (BOOL)control:(NSControl*)control
textView:(NSTextView*)text_view
doCommandBySelector:(SEL)selector
{
if (selector != @selector(insertNewline:)) {
return NO;
}
auto url_string = Ladybird::ns_string_to_string([[text_view textStorage] string]);
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
if (auto url = WebView::sanitize_url(url_string, [delegate searchEngine].query_url); url.has_value()) {
[self loadURL:*url];
}
[self.window makeFirstResponder:nil];
return YES;
}
- (void)controlTextDidEndEditing:(NSNotification*)notification
{
auto* location_search_field = (LocationSearchField*)[self.location_toolbar_item view];
auto url_string = Ladybird::ns_string_to_string([location_search_field stringValue]);
[self setLocationFieldText:url_string];
}
@end

View file

@ -0,0 +1,18 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#import <Cocoa/Cocoa.h>
#import <Interface/LadybirdWebViewWindow.h>
@class LadybirdWebView;
@interface TaskManager : LadybirdWebViewWindow
- (instancetype)init;
@end

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/String.h>
#include <LibCore/Timer.h>
#include <LibWebView/Application.h>
#import <Interface/LadybirdWebView.h>
#import <Interface/TaskManager.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
static constexpr CGFloat const WINDOW_WIDTH = 600;
static constexpr CGFloat const WINDOW_HEIGHT = 400;
@interface TaskManager ()
{
RefPtr<Core::Timer> m_update_timer;
}
@end
@implementation TaskManager
- (instancetype)init
{
auto tab_rect = [[NSApp keyWindow] frame];
auto position_x = tab_rect.origin.x + (tab_rect.size.width - WINDOW_WIDTH) / 2;
auto position_y = tab_rect.origin.y + (tab_rect.size.height - WINDOW_HEIGHT) / 2;
auto window_rect = NSMakeRect(position_x, position_y, WINDOW_WIDTH, WINDOW_HEIGHT);
if (self = [super initWithWebView:nil windowRect:window_rect]) {
__weak TaskManager* weak_self = self;
m_update_timer = Core::Timer::create_repeating(1000, [weak_self] {
TaskManager* strong_self = weak_self;
if (strong_self == nil) {
return;
}
[strong_self updateStatistics];
});
[self setContentView:self.web_view];
[self setTitle:@"Task Manager"];
[self setIsVisible:YES];
[self updateStatistics];
m_update_timer->start();
}
return self;
}
- (void)updateStatistics
{
WebView::Application::the().update_process_statistics();
[self.web_view loadHTML:WebView::Application::the().generate_process_statistics_html()];
}
@end

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
import Foundation
import Ladybird.WebView
import Ladybird.WebViewApplication
import Ladybird.WebViewWindow
import SwiftUI
public class TaskManager: LadybirdWebViewWindow {
private let WINDOW_WIDTH: CGFloat = 600
private let WINDOW_HEIGHT: CGFloat = 400
private var timer: Timer?
init() {
let tab_rect = NSApplication.shared.keyWindow!.frame
let position_x = tab_rect.origin.x + (tab_rect.size.width - WINDOW_WIDTH) / 2
let position_y = tab_rect.origin.y + (tab_rect.size.height - WINDOW_HEIGHT) / 2
let window_rect = NSMakeRect(position_x, position_y, WINDOW_WIDTH, WINDOW_HEIGHT)
super.init(webView: nil, windowRect: window_rect)
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
if let strong_self = self {
strong_self.updateStatistics()
}
}
self.contentView = self.web_view
self.title = "Task Manager"
self.setIsVisible(true)
self.updateStatistics()
}
func updateStatistics() {
WebView.Application.the().update_process_statistics()
self.web_view.loadHTML(WebView.Application.the().generate_process_statistics_html().__bytes_as_string_viewUnsafe())
}
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#import <Cocoa/Cocoa.h>
@protocol TaskManagerDelegate <NSObject>
- (void)onTaskManagerClosed;
@end
@interface TaskManagerController : NSWindowController
- (instancetype)initWithDelegate:(id<TaskManagerDelegate>)delegate;
@end

View file

@ -0,0 +1,65 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#import <Interface/LadybirdWebView.h>
#import <Interface/TaskManager.h>
#import <Interface/TaskManagerController.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
@interface TaskManagerController () <NSWindowDelegate>
@property (nonatomic, weak) id<TaskManagerDelegate> delegate;
@end
@implementation TaskManagerController
- (instancetype)initWithDelegate:(id<TaskManagerDelegate>)delegate
{
if (self = [super init]) {
self.delegate = delegate;
}
return self;
}
#pragma mark - Private methods
- (TaskManager*)taskManager
{
return (TaskManager*)[self window];
}
#pragma mark - NSWindowController
- (IBAction)showWindow:(id)sender
{
self.window = [[TaskManager alloc] init];
[self.window setDelegate:self];
[self.window makeKeyAndOrderFront:sender];
}
#pragma mark - NSWindowDelegate
- (void)windowWillClose:(NSNotification*)notification
{
[self.delegate onTaskManagerClosed];
}
- (void)windowDidResize:(NSNotification*)notification
{
[[[self taskManager] web_view] handleResize];
}
- (void)windowDidChangeBackingProperties:(NSNotification*)notification
{
[[[self taskManager] web_view] handleDevicePixelRatioChange];
}
@end

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
import Foundation
import SwiftUI
@objc
public protocol TaskManagerDelegate where Self: NSObject {
func onTaskManagerClosed()
}
public class TaskManagerController: NSWindowController, NSWindowDelegate {
private weak var delegate: TaskManagerDelegate?
@objc
public convenience init(delegate: TaskManagerDelegate) {
self.init()
self.delegate = delegate
}
@IBAction public override func showWindow(_ sender: Any?) {
self.window = TaskManager.init()
self.window!.delegate = self
self.window!.makeKeyAndOrderFront(sender)
}
public func windowWillClose(_ sender: Notification) {
self.delegate?.onTaskManagerClosed()
}
public func windowDidResize(_ sender: Notification) {
guard self.window != nil else { return }
if !self.window!.inLiveResize {
self.taskManager().web_view.handleResize()
}
}
public func windowDidChangeBackingProperties(_ sender: Notification) {
self.taskManager().web_view.handleDevicePixelRatioChange()
}
private func taskManager() -> TaskManager {
return self.window as! TaskManager
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteString.h>
#include <AK/String.h>
#include <AK/StringView.h>
#include <LibGfx/Color.h>
#include <LibGfx/Point.h>
#include <LibGfx/Rect.h>
#include <LibGfx/Size.h>
#import <Cocoa/Cocoa.h>
namespace Ladybird {
String ns_string_to_string(NSString*);
ByteString ns_string_to_byte_string(NSString*);
NSString* string_to_ns_string(StringView);
NSData* string_to_ns_data(StringView);
NSDictionary* deserialize_json_to_dictionary(StringView);
Gfx::IntRect ns_rect_to_gfx_rect(NSRect);
NSRect gfx_rect_to_ns_rect(Gfx::IntRect);
Gfx::IntSize ns_size_to_gfx_size(NSSize);
NSSize gfx_size_to_ns_size(Gfx::IntSize);
Gfx::IntPoint ns_point_to_gfx_point(NSPoint);
NSPoint gfx_point_to_ns_point(Gfx::IntPoint);
Gfx::Color ns_color_to_gfx_color(NSColor*);
NSColor* gfx_color_to_ns_color(Gfx::Color);
Gfx::IntPoint compute_origin_relative_to_window(NSWindow*, Gfx::IntPoint);
}

View file

@ -0,0 +1,131 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#import <Utilities/Conversions.h>
namespace Ladybird {
String ns_string_to_string(NSString* string)
{
auto const* utf8 = [string UTF8String];
return MUST(String::from_utf8({ utf8, strlen(utf8) }));
}
ByteString ns_string_to_byte_string(NSString* string)
{
auto const* utf8 = [string UTF8String];
return ByteString(utf8, strlen(utf8));
}
NSString* string_to_ns_string(StringView string)
{
return [[NSString alloc] initWithData:string_to_ns_data(string) encoding:NSUTF8StringEncoding];
}
NSData* string_to_ns_data(StringView string)
{
return [NSData dataWithBytes:string.characters_without_null_termination() length:string.length()];
}
NSDictionary* deserialize_json_to_dictionary(StringView json)
{
auto* ns_json = string_to_ns_string(json);
auto* json_data = [ns_json dataUsingEncoding:NSUTF8StringEncoding];
NSError* error = nil;
NSDictionary* dictionary = [NSJSONSerialization JSONObjectWithData:json_data
options:0
error:&error];
if (!dictionary) {
NSLog(@"Error deserializing DOM tree: %@", error);
}
return dictionary;
}
Gfx::IntRect ns_rect_to_gfx_rect(NSRect rect)
{
return {
static_cast<int>(rect.origin.x),
static_cast<int>(rect.origin.y),
static_cast<int>(rect.size.width),
static_cast<int>(rect.size.height),
};
}
NSRect gfx_rect_to_ns_rect(Gfx::IntRect rect)
{
return NSMakeRect(
static_cast<CGFloat>(rect.x()),
static_cast<CGFloat>(rect.y()),
static_cast<CGFloat>(rect.width()),
static_cast<CGFloat>(rect.height()));
}
Gfx::IntSize ns_size_to_gfx_size(NSSize size)
{
return {
static_cast<int>(size.width),
static_cast<int>(size.height),
};
}
NSSize gfx_size_to_ns_size(Gfx::IntSize size)
{
return NSMakeSize(
static_cast<CGFloat>(size.width()),
static_cast<CGFloat>(size.height()));
}
Gfx::IntPoint ns_point_to_gfx_point(NSPoint point)
{
return {
static_cast<int>(point.x),
static_cast<int>(point.y),
};
}
NSPoint gfx_point_to_ns_point(Gfx::IntPoint point)
{
return NSMakePoint(
static_cast<CGFloat>(point.x()),
static_cast<CGFloat>(point.y()));
}
Gfx::Color ns_color_to_gfx_color(NSColor* color)
{
auto rgb_color = [color colorUsingColorSpace:NSColorSpace.genericRGBColorSpace];
if (rgb_color != nil)
return {
static_cast<u8>([rgb_color redComponent] * 255),
static_cast<u8>([rgb_color greenComponent] * 255),
static_cast<u8>([rgb_color blueComponent] * 255),
static_cast<u8>([rgb_color alphaComponent] * 255)
};
return {};
}
NSColor* gfx_color_to_ns_color(Gfx::Color color)
{
return [NSColor colorWithRed:(color.red() / 255.f)
green:(color.green() / 255.f)
blue:(color.blue() / 255.f)
alpha:(color.alpha() / 255.f)];
}
Gfx::IntPoint compute_origin_relative_to_window(NSWindow* window, Gfx::IntPoint position)
{
// The origin of the NSWindow is its bottom-left corner, relative to the bottom-left of the screen. We must convert
// window positions sent to/from WebContent between this origin and the window's and screen's top-left corners.
auto screen_frame = Ladybird::ns_rect_to_gfx_rect([[window screen] frame]);
auto window_frame = Ladybird::ns_rect_to_gfx_rect([window frame]);
position.set_y(screen_frame.height() - window_frame.height() - position.y());
return position;
}
}

94
UI/AppKit/main.mm Normal file
View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Enumerate.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibMain/Main.h>
#include <LibWebView/Application.h>
#include <LibWebView/ChromeProcess.h>
#include <LibWebView/URL.h>
#include <LibWebView/ViewImplementation.h>
#include <LibWebView/WebContentClient.h>
#include <UI/DefaultSettings.h>
#include <UI/MachPortServer.h>
#include <UI/Utilities.h>
#import <Application/Application.h>
#import <Application/ApplicationDelegate.h>
#import <Application/EventLoopImplementation.h>
#import <Interface/Tab.h>
#import <Interface/TabController.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
static void open_urls_from_client(Vector<URL::URL> const& urls, WebView::NewWindow new_window)
{
ApplicationDelegate* delegate = [NSApp delegate];
Tab* tab = new_window == WebView::NewWindow::Yes ? nil : [delegate activeTab];
for (auto [i, url] : enumerate(urls)) {
auto activate_tab = i == 0 ? Web::HTML::ActivateTab::Yes : Web::HTML::ActivateTab::No;
auto* controller = [delegate createNewTab:url
fromTab:tab
activateTab:activate_tab];
tab = (Tab*)[controller window];
}
}
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
AK::set_rich_debug_enabled(true);
Application* application = [Application sharedApplication];
Core::EventLoopManager::install(*new Ladybird::CFEventLoopManager);
[application setupWebViewApplication:arguments newTabPageURL:Browser::default_new_tab_url];
platform_init();
WebView::ChromeProcess chrome_process;
if (auto const& chrome_options = WebView::Application::chrome_options(); chrome_options.force_new_process == WebView::ForceNewProcess::No) {
auto disposition = TRY(chrome_process.connect(chrome_options.raw_urls, chrome_options.new_window));
if (disposition == WebView::ChromeProcess::ProcessDisposition::ExitProcess) {
outln("Opening in existing process");
return 0;
}
}
chrome_process.on_new_tab = [&](auto const& raw_urls) {
open_urls_from_client(raw_urls, WebView::NewWindow::No);
};
chrome_process.on_new_window = [&](auto const& raw_urls) {
open_urls_from_client(raw_urls, WebView::NewWindow::Yes);
};
auto mach_port_server = make<Ladybird::MachPortServer>();
set_mach_server_name(mach_port_server->server_port_name());
mach_port_server->on_receive_child_mach_port = [&](auto pid, auto port) {
WebView::Application::the().set_process_mach_port(pid, move(port));
};
mach_port_server->on_receive_backing_stores = [](Ladybird::MachPortServer::BackingStoresMessage message) {
if (auto view = WebView::WebContentClient::view_for_pid_and_page_id(message.pid, message.page_id); view.has_value())
view->did_allocate_iosurface_backing_stores(message.front_backing_store_id, move(message.front_backing_store_port), message.back_backing_store_id, move(message.back_backing_store_port));
};
// FIXME: Create an abstraction to re-spawn the RequestServer and re-hook up its client hooks to each tab on crash
TRY([application launchRequestServer]);
TRY([application launchImageDecoder]);
auto* delegate = [[ApplicationDelegate alloc] init];
[NSApp setDelegate:delegate];
return WebView::Application::the().execute();
}

View file

@ -0,0 +1,19 @@
module Ladybird [system] {
requires cplusplus
requires objc_arc
explicit module WebView {
header "Interface/LadybirdWebView.h"
export *
}
explicit module WebViewWindow {
header "Interface/LadybirdWebViewWindow.h"
export *
}
explicit module WebViewApplication {
header "../../Libraries/LibWebView/Application.h"
export *
}
}