1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-28 04:37:44 +00:00

Ladybird: Implement an AppKit chrome for macOS :^)

This adds an alternative Ladybird chrome for macOS using the AppKit
framework. Just about everything needed for normal web browsing has
been implemented. This includes:

* Tabbed, scrollable navigation
* History navigation (back, forward, reload)
* Keyboard / mouse events
* Favicons
* Context menus
* Cookies
* Dialogs (alert, confirm, prompt)
* WebDriver support

This does not include debugging tools like the JavaScript console and
inspector, nor theme support.

The Qt chrome is still used by default. To use the AppKit chrome, set
the ENABLE_QT CMake option to OFF.
This commit is contained in:
Timothy Flynn 2023-08-20 16:14:31 -04:00 committed by Tim Flynn
parent 66a89bd695
commit 5722d0025b
28 changed files with 3248 additions and 3 deletions

View file

@ -0,0 +1,13 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#import <System/Cocoa.h>
@interface Application : NSApplication
@end

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/EventLoop.h>
#import <Application/Application.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
@interface Application ()
@end
@implementation Application
- (void)terminate:(id)sender
{
Core::EventLoop::current().quit(0);
}
@end

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <AK/StringView.h>
#include <AK/URL.h>
#include <Browser/CookieJar.h>
#include <LibWeb/HTML/ActivateTab.h>
#import <System/Cocoa.h>
@class Tab;
@class TabController;
@interface ApplicationDelegate : NSObject <NSApplicationDelegate>
- (nullable instancetype)init:(Optional<URL>)initial_url
withCookieJar:(Browser::CookieJar)cookie_jar
webdriverContentIPCPath:(StringView)webdriver_content_ipc_path;
- (nonnull TabController*)createNewTab:(Optional<URL> const&)url;
- (nonnull TabController*)createNewTab:(Optional<URL> const&)url
activateTab:(Web::HTML::ActivateTab)activate_tab;
- (void)removeTab:(nonnull TabController*)controller;
- (Browser::CookieJar&)cookieJar;
- (Optional<StringView> const&)webdriverContentIPCPath;
@end

View file

@ -0,0 +1,313 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <BrowserSettings/Defaults.h>
#import <Application/ApplicationDelegate.h>
#import <UI/Tab.h>
#import <UI/TabController.h>
#import <Utilities/URL.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
@interface ApplicationDelegate ()
{
Optional<URL> m_initial_url;
URL m_new_tab_page_url;
// This will always be populated, but we cannot have a non-default constructible instance variable.
Optional<Browser::CookieJar> m_cookie_jar;
Optional<StringView> m_webdriver_content_ipc_path;
}
@property (nonatomic, strong) NSMutableArray<TabController*>* managed_tabs;
- (NSMenuItem*)createApplicationMenu;
- (NSMenuItem*)createFileMenu;
- (NSMenuItem*)createEditMenu;
- (NSMenuItem*)createViewMenu;
- (NSMenuItem*)createHistoryMenu;
- (NSMenuItem*)createDebugMenu;
- (NSMenuItem*)createWindowsMenu;
- (NSMenuItem*)createHelpMenu;
@end
@implementation ApplicationDelegate
- (instancetype)init:(Optional<URL>)initial_url
withCookieJar:(Browser::CookieJar)cookie_jar
webdriverContentIPCPath:(StringView)webdriver_content_ipc_path
{
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 createHistoryMenu]];
[[NSApp mainMenu] addItem:[self createDebugMenu]];
[[NSApp mainMenu] addItem:[self createWindowsMenu]];
[[NSApp mainMenu] addItem:[self createHelpMenu]];
self.managed_tabs = [[NSMutableArray alloc] init];
m_initial_url = move(initial_url);
m_new_tab_page_url = Ladybird::rebase_url_on_serenity_resource_root(Browser::default_new_tab_url);
m_cookie_jar = move(cookie_jar);
if (!webdriver_content_ipc_path.is_empty()) {
m_webdriver_content_ipc_path = webdriver_content_ipc_path;
}
// 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> const&)url
{
return [self createNewTab:url activateTab:Web::HTML::ActivateTab::Yes];
}
- (TabController*)createNewTab:(Optional<URL> const&)url
activateTab:(Web::HTML::ActivateTab)activate_tab
{
// This handle must be acquired before creating the new tab.
auto* current_tab = (Tab*)[NSApp keyWindow];
auto* controller = [[TabController alloc] init:url.value_or(m_new_tab_page_url)];
[controller showWindow:nil];
if (current_tab) {
[[current_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) {
[current_tab orderFront:nil];
}
}
[self.managed_tabs addObject:controller];
return controller;
}
- (void)removeTab:(TabController*)controller
{
[self.managed_tabs removeObject:controller];
}
- (Browser::CookieJar&)cookieJar
{
return *m_cookie_jar;
}
- (Optional<StringView> const&)webdriverContentIPCPath
{
return m_webdriver_content_ipc_path;
}
#pragma mark - Private methods
- (void)closeCurrentTab:(id)sender
{
auto* current_tab = (Tab*)[NSApp keyWindow];
[current_tab close];
}
- (void)openLocation:(id)sender
{
auto* current_tab = (Tab*)[NSApp keyWindow];
auto* controller = (TabController*)[current_tab windowController];
[controller focusLocationToolbarItem];
}
- (void)clearHistory:(id)sender
{
for (TabController* controller in self.managed_tabs) {
[controller clearHistory];
}
}
- (void)dumpCookies:(id)sender
{
m_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(orderFrontStandardAboutPanel:)
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"]];
[menu setSubmenu:submenu];
return menu;
}
- (NSMenuItem*)createViewMenu
{
auto* menu = [[NSMenuItem alloc] init];
auto* submenu = [[NSMenu alloc] initWithTitle:@"View"];
[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*)createDebugMenu
{
auto* menu = [[NSMenuItem alloc] init];
auto* submenu = [[NSMenu alloc] initWithTitle:@"Debug"];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump Cookies"
action:@selector(dumpCookies:)
keyEquivalent:@""]];
[menu setSubmenu:submenu];
return menu;
}
- (NSMenuItem*)createWindowsMenu
{
auto* menu = [[NSMenuItem alloc] init];
auto* submenu = [[NSMenu alloc] initWithTitle:@"Windows"];
[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
{
[self createNewTab:m_initial_url];
}
- (void)applicationWillTerminate:(NSNotification*)notification
{
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender
{
return YES;
}
@end

View file

@ -0,0 +1,56 @@
/*
* 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 int register_timer(Core::EventReceiver&, int interval_milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible) override;
virtual bool unregister_timer(int timer_id) override;
virtual void register_notifier(Core::Notifier&) override;
virtual void unregister_notifier(Core::Notifier&) override;
virtual void did_post_event() override;
// FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them.
virtual int register_signal(int, Function<void(int)>) override { return 0; }
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,214 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Assertions.h>
#include <AK/IDAllocator.h>
#include <LibCore/Event.h>
#include <LibCore/Notifier.h>
#include <LibCore/ThreadEventQueue.h>
#import <Application/EventLoopImplementation.h>
#import <System/Cocoa.h>
#import <System/CoreFoundation.h>
namespace Ladybird {
struct ThreadData {
static ThreadData& the()
{
static thread_local ThreadData s_thread_data;
return s_thread_data;
}
IDAllocator timer_id_allocator;
HashMap<int, CFRunLoopTimerRef> timers;
HashMap<Core::Notifier*, CFRunLoopSourceRef> notifiers;
};
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();
}
int 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(timer_id);
receiver->dispatch_event(event);
});
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);
thread_data.timers.set(timer_id, timer);
return timer_id;
}
bool CFEventLoopManager::unregister_timer(int timer_id)
{
auto& thread_data = ThreadData::the();
thread_data.timer_id_allocator.deallocate(timer_id);
if (auto timer = thread_data.timers.take(timer_id); timer.has_value()) {
CFRunLoopTimerInvalidate(*timer);
return true;
}
return false;
}
static void socket_notifier(CFSocketRef socket, CFSocketCallBackType notification_type, CFDataRef, void const*, void* info)
{
auto& notifier = *reinterpret_cast<Core::Notifier*>(info);
// 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.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 { .info = &notifier };
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, kCFRunLoopDefaultMode);
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, kCFRunLoopDefaultMode);
CFRelease(*source);
}
}
void CFEventLoopManager::did_post_event()
{
post_application_event();
}
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) {
if (event.type == NSEventTypeApplicationDefined) {
m_thread_event_queue.process();
} else {
[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();
}
}