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
Notes: sideshowbarker 2024-07-17 01:53:23 +09:00
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();
}
}

View File

@ -0,0 +1,13 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "Detail/Header.h"
#import <Carbon/Carbon.h>
#include "Detail/Footer.h"

View File

@ -0,0 +1,13 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "Detail/Header.h"
#import <Cocoa/Cocoa.h>
#include "Detail/Footer.h"

View File

@ -0,0 +1,13 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "Detail/Header.h"
#import <CoreFoundation/CoreFoundation.h>
#include "Detail/Footer.h"

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#if !defined(MACOS_UGLY_WORKAROUND)
# error Footer.h was included before Header.h.
#endif
#undef Duration
#undef FixedPoint
#undef MACOS_UGLY_WORKAROUND

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#if defined(MACOS_UGLY_WORKAROUND)
# error Header.h was included again before Footer.h.
#endif
#define Duration DurationMacOS
#define FixedPoint FixedPointMacOS
#define MACOS_UGLY_WORKAROUND

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
// FIXME: These should not be included outside of Serenity.
#include <Kernel/API/KeyCode.h>
#include <LibGUI/Event.h>
#import <System/Cocoa.h>
namespace Ladybird {
struct MouseEvent {
Gfx::IntPoint position {};
GUI::MouseButton button { GUI::MouseButton::Primary };
KeyModifier modifiers { KeyModifier::Mod_None };
};
MouseEvent ns_event_to_mouse_event(NSEvent*, NSView*, GUI::MouseButton);
NSEvent* create_context_menu_mouse_event(NSView*, Gfx::IntPoint);
NSEvent* create_context_menu_mouse_event(NSView*, NSPoint);
struct KeyEvent {
KeyCode key_code { KeyCode::Key_Invalid };
KeyModifier modifiers { KeyModifier::Mod_None };
u32 code_point { 0 };
};
KeyEvent ns_event_to_key_event(NSEvent*);
}

195
Ladybird/AppKit/UI/Event.mm Normal file
View File

@ -0,0 +1,195 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Utf8View.h>
#import <System/Carbon.h>
#import <UI/Event.h>
#import <Utilities/Conversions.h>
namespace Ladybird {
static KeyModifier ns_modifiers_to_key_modifiers(NSEventModifierFlags modifier_flags, Optional<GUI::MouseButton&> button = {})
{
unsigned modifiers = KeyModifier::Mod_None;
if ((modifier_flags & NSEventModifierFlagShift) != 0) {
modifiers |= KeyModifier::Mod_Shift;
}
if ((modifier_flags & NSEventModifierFlagControl) != 0) {
if (button == GUI::MouseButton::Primary) {
*button = GUI::MouseButton::Secondary;
} else {
modifiers |= KeyModifier::Mod_Ctrl;
}
}
if ((modifier_flags & NSEventModifierFlagOption) != 0) {
modifiers |= KeyModifier::Mod_Alt;
}
if ((modifier_flags & NSEventModifierFlagCommand) != 0) {
modifiers |= KeyModifier::Mod_Super;
}
return static_cast<KeyModifier>(modifiers);
}
MouseEvent ns_event_to_mouse_event(NSEvent* event, NSView* view, GUI::MouseButton button)
{
auto position = [view convertPoint:event.locationInWindow fromView:nil];
auto modifiers = ns_modifiers_to_key_modifiers(event.modifierFlags, button);
return { ns_point_to_gfx_point(position), button, modifiers };
}
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 KeyCode ns_key_code_to_key_code(unsigned short key_code, KeyModifier& modifiers)
{
auto augment_modifiers_and_return = [&](auto key, auto modifier) {
modifiers = static_cast<KeyModifier>(static_cast<unsigned>(modifiers) | modifier);
return key;
};
// clang-format off
switch (key_code) {
case kVK_ANSI_0: return KeyCode::Key_0;
case kVK_ANSI_1: return KeyCode::Key_1;
case kVK_ANSI_2: return KeyCode::Key_2;
case kVK_ANSI_3: return KeyCode::Key_3;
case kVK_ANSI_4: return KeyCode::Key_4;
case kVK_ANSI_5: return KeyCode::Key_5;
case kVK_ANSI_6: return KeyCode::Key_6;
case kVK_ANSI_7: return KeyCode::Key_7;
case kVK_ANSI_8: return KeyCode::Key_8;
case kVK_ANSI_9: return KeyCode::Key_9;
case kVK_ANSI_A: return KeyCode::Key_A;
case kVK_ANSI_B: return KeyCode::Key_B;
case kVK_ANSI_C: return KeyCode::Key_C;
case kVK_ANSI_D: return KeyCode::Key_D;
case kVK_ANSI_E: return KeyCode::Key_E;
case kVK_ANSI_F: return KeyCode::Key_F;
case kVK_ANSI_G: return KeyCode::Key_G;
case kVK_ANSI_H: return KeyCode::Key_H;
case kVK_ANSI_I: return KeyCode::Key_I;
case kVK_ANSI_J: return KeyCode::Key_J;
case kVK_ANSI_K: return KeyCode::Key_K;
case kVK_ANSI_L: return KeyCode::Key_L;
case kVK_ANSI_M: return KeyCode::Key_M;
case kVK_ANSI_N: return KeyCode::Key_N;
case kVK_ANSI_O: return KeyCode::Key_O;
case kVK_ANSI_P: return KeyCode::Key_P;
case kVK_ANSI_Q: return KeyCode::Key_Q;
case kVK_ANSI_R: return KeyCode::Key_R;
case kVK_ANSI_S: return KeyCode::Key_S;
case kVK_ANSI_T: return KeyCode::Key_T;
case kVK_ANSI_U: return KeyCode::Key_U;
case kVK_ANSI_V: return KeyCode::Key_V;
case kVK_ANSI_W: return KeyCode::Key_W;
case kVK_ANSI_X: return KeyCode::Key_X;
case kVK_ANSI_Y: return KeyCode::Key_Y;
case kVK_ANSI_Z: return KeyCode::Key_Z;
case kVK_ANSI_Backslash: return KeyCode::Key_Backslash;
case kVK_ANSI_Comma: return KeyCode::Key_Comma;
case kVK_ANSI_Equal: return KeyCode::Key_Equal;
case kVK_ANSI_Grave: return KeyCode::Key_Backtick;
case kVK_ANSI_Keypad0: return augment_modifiers_and_return(KeyCode::Key_0, KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad1: return augment_modifiers_and_return(KeyCode::Key_1, KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad2: return augment_modifiers_and_return(KeyCode::Key_2, KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad3: return augment_modifiers_and_return(KeyCode::Key_3, KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad4: return augment_modifiers_and_return(KeyCode::Key_4, KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad5: return augment_modifiers_and_return(KeyCode::Key_5, KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad6: return augment_modifiers_and_return(KeyCode::Key_6, KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad7: return augment_modifiers_and_return(KeyCode::Key_7, KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad8: return augment_modifiers_and_return(KeyCode::Key_8, KeyModifier::Mod_Keypad);
case kVK_ANSI_Keypad9: return augment_modifiers_and_return(KeyCode::Key_9, KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadClear: return augment_modifiers_and_return(KeyCode::Key_Delete, KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadDecimal: return augment_modifiers_and_return(KeyCode::Key_Period, KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadDivide: return augment_modifiers_and_return(KeyCode::Key_Slash, KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadEnter: return augment_modifiers_and_return(KeyCode::Key_Return, KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadEquals: return augment_modifiers_and_return(KeyCode::Key_Equal, KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadMinus: return augment_modifiers_and_return(KeyCode::Key_Minus, KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadMultiply: return augment_modifiers_and_return(KeyCode::Key_Asterisk, KeyModifier::Mod_Keypad);
case kVK_ANSI_KeypadPlus: return augment_modifiers_and_return(KeyCode::Key_Plus, KeyModifier::Mod_Keypad);
case kVK_ANSI_LeftBracket: return KeyCode::Key_LeftBracket;
case kVK_ANSI_Minus: return KeyCode::Key_Minus;
case kVK_ANSI_Period: return KeyCode::Key_Period;
case kVK_ANSI_Quote: return KeyCode::Key_Apostrophe;
case kVK_ANSI_RightBracket: return KeyCode::Key_RightBracket;
case kVK_ANSI_Semicolon: return KeyCode::Key_Semicolon;
case kVK_ANSI_Slash: return KeyCode::Key_Slash;
case kVK_CapsLock: return KeyCode::Key_CapsLock;
case kVK_Command: return KeyCode::Key_Super;
case kVK_Control: return KeyCode::Key_Control;
case kVK_Delete: return KeyCode::Key_Backspace;
case kVK_DownArrow: return KeyCode::Key_Down;
case kVK_End: return KeyCode::Key_End;
case kVK_Escape: return KeyCode::Key_Escape;
case kVK_F1: return KeyCode::Key_F1;
case kVK_F2: return KeyCode::Key_F2;
case kVK_F3: return KeyCode::Key_F3;
case kVK_F4: return KeyCode::Key_F4;
case kVK_F5: return KeyCode::Key_F5;
case kVK_F6: return KeyCode::Key_F6;
case kVK_F7: return KeyCode::Key_F7;
case kVK_F8: return KeyCode::Key_F8;
case kVK_F9: return KeyCode::Key_F9;
case kVK_F10: return KeyCode::Key_F10;
case kVK_F11: return KeyCode::Key_F11;
case kVK_F12: return KeyCode::Key_F12;
case kVK_ForwardDelete: return KeyCode::Key_Delete;
case kVK_Home: return KeyCode::Key_Home;
case kVK_LeftArrow: return KeyCode::Key_Left;
case kVK_Option: return KeyCode::Key_Alt;
case kVK_PageDown: return KeyCode::Key_PageDown;
case kVK_PageUp: return KeyCode::Key_PageUp;
case kVK_Return: return KeyCode::Key_Return;
case kVK_RightArrow: return KeyCode::Key_Right;
case kVK_RightCommand: return KeyCode::Key_Super; // FIXME: We do not distinguish left-vs-right.
case kVK_RightControl: return KeyCode::Key_Control; // FIXME: We do not distinguish left-vs-right.
case kVK_RightOption: return KeyCode::Key_Alt; // FIXME: We do not distinguish left-vs-right.
case kVK_RightShift: return KeyCode::Key_RightShift;
case kVK_Shift: return KeyCode::Key_Shift;
case kVK_Space: return KeyCode::Key_Space;
case kVK_Tab: return KeyCode::Key_Tab;
case kVK_UpArrow: return KeyCode::Key_Up;
default: break;
}
// clang-format on
return KeyCode::Key_Invalid;
}
KeyEvent ns_event_to_key_event(NSEvent* event)
{
auto modifiers = ns_modifiers_to_key_modifiers(event.modifierFlags);
auto key_code = ns_key_code_to_key_code(event.keyCode, modifiers);
auto const* utf8 = [event.characters UTF8String];
Utf8View utf8_view { StringView { utf8, strlen(utf8) } };
// FIXME: WebContent should really support multi-code point key events.
auto code_point = utf8_view.is_empty() ? 0u : *utf8_view.begin();
return { key_code, modifiers, code_point };
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#import <System/Cocoa.h>
@interface LadybirdWebView : NSClipView
- (void)load:(URL const&)url;
- (void)handleResize;
- (void)handleScroll;
- (void)handleVisibility:(BOOL)is_visible;
@end

View File

@ -0,0 +1,949 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Optional.h>
#include <AK/TemporaryChange.h>
#include <AK/URL.h>
#include <LibGfx/ImageFormats/PNGWriter.h>
#include <LibGfx/ShareableBitmap.h>
#include <UI/LadybirdWebViewBridge.h>
#import <Application/ApplicationDelegate.h>
#import <UI/Event.h>
#import <UI/LadybirdWebView.h>
#import <UI/Tab.h>
#import <UI/TabController.h>
#import <Utilities/Conversions.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
static constexpr NSInteger CONTEXT_MENU_PLAY_PAUSE_TAG = 1;
static constexpr NSInteger CONTEXT_MENU_MUTE_UNMUTE_TAG = 2;
static constexpr NSInteger CONTEXT_MENU_CONTROLS_TAG = 3;
static constexpr NSInteger CONTEXT_MENU_LOOP_TAG = 4;
// Calls to [NSCursor hide] and [NSCursor unhide] must be balanced. We use this struct to ensure
// we only call [NSCursor hide] once and to ensure that we do call [NSCursor unhide].
// https://developer.apple.com/documentation/appkit/nscursor#1651301
struct HideCursor {
HideCursor()
{
[NSCursor hide];
}
~HideCursor()
{
[NSCursor unhide];
}
};
@interface LadybirdWebView ()
{
OwnPtr<Ladybird::WebViewBridge> m_web_view_bridge;
URL m_context_menu_url;
Gfx::ShareableBitmap m_context_menu_bitmap;
Optional<HideCursor> m_hidden_cursor;
}
@property (nonatomic, strong) NSMenu* page_context_menu;
@property (nonatomic, strong) NSMenu* link_context_menu;
@property (nonatomic, strong) NSMenu* image_context_menu;
@property (nonatomic, strong) NSMenu* video_context_menu;
@property (nonatomic, strong) NSTextField* status_label;
@property (nonatomic, strong) NSAlert* dialog;
@end
@implementation LadybirdWebView
@synthesize page_context_menu = _page_context_menu;
@synthesize link_context_menu = _link_context_menu;
@synthesize image_context_menu = _image_context_menu;
@synthesize video_context_menu = _video_context_menu;
@synthesize status_label = _status_label;
- (instancetype)init
{
if (self = [super init]) {
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
auto* screens = [NSScreen screens];
Vector<Gfx::IntRect> screen_rects;
screen_rects.ensure_capacity([screens count]);
for (id screen in screens) {
auto screen_rect = Ladybird::ns_rect_to_gfx_rect([screen frame]);
screen_rects.unchecked_append(screen_rect);
}
auto device_pixel_ratio = [[NSScreen mainScreen] backingScaleFactor];
m_web_view_bridge = MUST(Ladybird::WebViewBridge::create(move(screen_rects), device_pixel_ratio, [delegate webdriverContentIPCPath]));
[self setWebViewCallbacks];
auto* area = [[NSTrackingArea alloc] initWithRect:[self bounds]
options:NSTrackingActiveInKeyWindow | NSTrackingInVisibleRect | NSTrackingMouseMoved
owner:self
userInfo:nil];
[self addTrackingArea:area];
}
return self;
}
#pragma mark - Public methods
- (void)load:(URL const&)url
{
m_web_view_bridge->load(url);
}
- (void)handleResize
{
[self updateViewportRect:Ladybird::WebViewBridge::ForResize::Yes];
[self updateStatusLabelPosition];
}
- (void)handleScroll
{
[self updateViewportRect:Ladybird::WebViewBridge::ForResize::No];
[self updateStatusLabelPosition];
}
- (void)handleVisibility:(BOOL)is_visible
{
m_web_view_bridge->set_system_visibility_state(is_visible);
}
#pragma mark - Private methods
- (void)updateViewportRect:(Ladybird::WebViewBridge::ForResize)for_resize
{
auto content_rect = [self frame];
auto document_rect = [[self documentView] frame];
auto device_pixel_ratio = m_web_view_bridge->device_pixel_ratio();
auto position = [&](auto content_size, auto document_size, auto scroll) {
return max(0, (document_size - content_size) * device_pixel_ratio * scroll);
};
auto horizontal_scroll = [[[self scrollView] horizontalScroller] floatValue];
auto vertical_scroll = [[[self scrollView] verticalScroller] floatValue];
auto ns_viewport_rect = NSMakeRect(
position(content_rect.size.width, document_rect.size.width, horizontal_scroll),
position(content_rect.size.height, document_rect.size.height, vertical_scroll),
content_rect.size.width,
content_rect.size.height);
auto viewport_rect = Ladybird::ns_rect_to_gfx_rect(ns_viewport_rect);
m_web_view_bridge->set_viewport_rect(viewport_rect, for_resize);
}
- (void)updateStatusLabelPosition
{
static constexpr CGFloat LABEL_INSET = 10;
if (_status_label == nil || [[self status_label] isHidden]) {
return;
}
auto visible_rect = [self visibleRect];
auto status_label_rect = [self.status_label frame];
auto position = NSMakePoint(LABEL_INSET, visible_rect.origin.y + visible_rect.size.height - status_label_rect.size.height - LABEL_INSET);
[self.status_label setFrameOrigin:position];
}
- (void)setWebViewCallbacks
{
m_web_view_bridge->on_layout = [self](auto content_size) {
auto ns_content_size = Ladybird::gfx_size_to_ns_size(content_size);
[[self documentView] setFrameSize:ns_content_size];
};
m_web_view_bridge->on_ready_to_paint = [self]() {
[self setNeedsDisplay:YES];
};
m_web_view_bridge->on_new_tab = [](auto activate_tab) {
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
auto* controller = [delegate createNewTab:"about:blank"sv activateTab:activate_tab];
auto* tab = (Tab*)[controller window];
auto* web_view = [tab web_view];
return web_view->m_web_view_bridge->handle();
};
m_web_view_bridge->on_activate_tab = [self]() {
[[self tab] orderFront:nil];
};
m_web_view_bridge->on_close = [self]() {
[[self tab] close];
};
m_web_view_bridge->on_load_start = [self](auto const& url, bool is_redirect) {
[[self tabController] onLoadStart:url isRedirect:is_redirect];
[[self tab] onLoadStart:url];
if (_status_label != nil) {
[self.status_label setHidden:YES];
}
};
m_web_view_bridge->on_title_change = [self](auto const& title) {
[[self tabController] onTitleChange:title];
auto* ns_title = Ladybird::string_to_ns_string(title);
[[self tab] onTitleChange:ns_title];
};
m_web_view_bridge->on_favicon_change = [self](auto const& bitmap) {
static constexpr size_t FAVICON_SIZE = 16;
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];
[favicon setSize:NSMakeSize(FAVICON_SIZE, FAVICON_SIZE)];
[[self tab] onFaviconChange:favicon];
};
m_web_view_bridge->on_scroll = [self](auto position) {
[self scrollToPoint:Ladybird::gfx_point_to_ns_point(position)];
[[self scrollView] reflectScrolledClipView:self];
[self updateViewportRect:Ladybird::WebViewBridge::ForResize::No];
};
m_web_view_bridge->on_cursor_change = [self](auto cursor) {
if (cursor == Gfx::StandardCursor::Hidden) {
if (!m_hidden_cursor.has_value()) {
m_hidden_cursor.emplace();
}
return;
}
m_hidden_cursor.clear();
switch (cursor) {
case Gfx::StandardCursor::Arrow:
[[NSCursor arrowCursor] set];
break;
case Gfx::StandardCursor::Crosshair:
[[NSCursor crosshairCursor] set];
break;
case Gfx::StandardCursor::IBeam:
[[NSCursor IBeamCursor] set];
break;
case Gfx::StandardCursor::ResizeHorizontal:
[[NSCursor resizeLeftRightCursor] set];
break;
case Gfx::StandardCursor::ResizeVertical:
[[NSCursor resizeUpDownCursor] set];
break;
case Gfx::StandardCursor::ResizeDiagonalTLBR:
// FIXME: AppKit does not have a corresponding cursor, so we should make one.
[[NSCursor arrowCursor] set];
break;
case Gfx::StandardCursor::ResizeDiagonalBLTR:
// FIXME: AppKit does not have a corresponding cursor, so we should make one.
[[NSCursor arrowCursor] set];
break;
case Gfx::StandardCursor::ResizeColumn:
// FIXME: AppKit does not have a corresponding cursor, so we should make one.
[[NSCursor arrowCursor] set];
break;
case Gfx::StandardCursor::ResizeRow:
// FIXME: AppKit does not have a corresponding cursor, so we should make one.
[[NSCursor arrowCursor] set];
break;
case Gfx::StandardCursor::Hand:
[[NSCursor pointingHandCursor] set];
break;
case Gfx::StandardCursor::Help:
// FIXME: AppKit does not have a corresponding cursor, so we should make one.
[[NSCursor arrowCursor] set];
break;
case Gfx::StandardCursor::Drag:
[[NSCursor closedHandCursor] set];
break;
case Gfx::StandardCursor::DragCopy:
[[NSCursor dragCopyCursor] set];
break;
case Gfx::StandardCursor::Move:
// FIXME: AppKit does not have a corresponding cursor, so we should make one.
[[NSCursor dragCopyCursor] set];
break;
case Gfx::StandardCursor::Wait:
// FIXME: AppKit does not have a corresponding cursor, so we should make one.
[[NSCursor arrowCursor] set];
break;
case Gfx::StandardCursor::Disallowed:
// FIXME: AppKit does not have a corresponding cursor, so we should make one.
[[NSCursor arrowCursor] set];
break;
case Gfx::StandardCursor::Eyedropper:
// FIXME: AppKit does not have a corresponding cursor, so we should make one.
[[NSCursor arrowCursor] set];
break;
case Gfx::StandardCursor::Zoom:
// FIXME: AppKit does not have a corresponding cursor, so we should make one.
[[NSCursor arrowCursor] set];
break;
default:
break;
}
};
m_web_view_bridge->on_navigate_back = [self]() {
[[self tabController] navigateBack:nil];
};
m_web_view_bridge->on_navigate_forward = [self]() {
[[self tabController] navigateForward:nil];
};
m_web_view_bridge->on_refresh = [self]() {
[[self tabController] reload:nil];
};
m_web_view_bridge->on_tooltip_entered = [self](auto const& tooltip) {
self.toolTip = Ladybird::string_to_ns_string(tooltip);
};
m_web_view_bridge->on_tooltip_left = [self]() {
self.toolTip = nil;
};
m_web_view_bridge->on_link_hover = [self](auto const& url) {
auto* url_string = Ladybird::string_to_ns_string(url.serialize());
[self.status_label setStringValue:url_string];
[self.status_label sizeToFit];
[self.status_label setHidden:NO];
[self updateStatusLabelPosition];
};
m_web_view_bridge->on_link_unhover = [self]() {
[self.status_label setHidden:YES];
};
m_web_view_bridge->on_link_click = [self](auto const& url, auto const& target, unsigned modifiers) {
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
if (modifiers == Mod_Super) {
[delegate createNewTab:url activateTab:Web::HTML::ActivateTab::No];
} else if (target == "_blank"sv) {
[delegate createNewTab:url activateTab:Web::HTML::ActivateTab::Yes];
} else {
[[self tabController] load:url];
}
};
m_web_view_bridge->on_link_middle_click = [](auto url, auto, unsigned) {
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
[delegate createNewTab:url activateTab:Web::HTML::ActivateTab::No];
};
m_web_view_bridge->on_context_menu_request = [self](auto position) {
auto* event = Ladybird::create_context_menu_mouse_event(self, position);
[NSMenu popUpContextMenu:self.page_context_menu withEvent:event forView:self];
};
m_web_view_bridge->on_link_context_menu_request = [self](auto const& url, auto position) {
TemporaryChange change_url { m_context_menu_url, url };
auto* event = Ladybird::create_context_menu_mouse_event(self, position);
[NSMenu popUpContextMenu:self.link_context_menu withEvent:event forView:self];
};
m_web_view_bridge->on_image_context_menu_request = [self](auto const& url, auto position, auto const& bitmap) {
TemporaryChange change_url { m_context_menu_url, url };
TemporaryChange change_bitmap { m_context_menu_bitmap, bitmap };
auto* event = Ladybird::create_context_menu_mouse_event(self, position);
[NSMenu popUpContextMenu:self.image_context_menu withEvent:event forView:self];
};
m_web_view_bridge->on_media_context_menu_request = [self](auto position, auto const& menu) {
if (!menu.is_video) {
NSLog(@"TODO: Implement audio context menu once audio elements are supported");
return;
}
TemporaryChange change_url { m_context_menu_url, menu.media_url };
auto* play_pause_menu_item = [self.video_context_menu itemWithTag:CONTEXT_MENU_PLAY_PAUSE_TAG];
auto* mute_unmute_menu_item = [self.video_context_menu itemWithTag:CONTEXT_MENU_MUTE_UNMUTE_TAG];
auto* controls_menu_item = [self.video_context_menu itemWithTag:CONTEXT_MENU_CONTROLS_TAG];
auto* loop_menu_item = [self.video_context_menu itemWithTag:CONTEXT_MENU_LOOP_TAG];
if (menu.is_playing) {
[play_pause_menu_item setTitle:@"Pause"];
} else {
[play_pause_menu_item setTitle:@"Play"];
}
if (menu.is_muted) {
[mute_unmute_menu_item setTitle:@"Unmute"];
} else {
[mute_unmute_menu_item setTitle:@"Mute"];
}
auto controls_state = menu.has_user_agent_controls ? NSControlStateValueOn : NSControlStateValueOff;
[controls_menu_item setState:controls_state];
auto loop_state = menu.is_looping ? NSControlStateValueOn : NSControlStateValueOff;
[loop_menu_item setState:loop_state];
auto* event = Ladybird::create_context_menu_mouse_event(self, position);
[NSMenu popUpContextMenu:self.video_context_menu withEvent:event forView:self];
};
m_web_view_bridge->on_alert = [self](auto const& message) {
auto* ns_message = Ladybird::string_to_ns_string(message);
self.dialog = [[NSAlert alloc] init];
[self.dialog setMessageText:ns_message];
[self.dialog beginSheetModalForWindow:[self window]
completionHandler:^(NSModalResponse) {
m_web_view_bridge->alert_closed();
self.dialog = nil;
}];
};
m_web_view_bridge->on_confirm = [self](auto const& message) {
auto* ns_message = Ladybird::string_to_ns_string(message);
self.dialog = [[NSAlert alloc] init];
[[self.dialog addButtonWithTitle:@"OK"] setTag:NSModalResponseOK];
[[self.dialog addButtonWithTitle:@"Cancel"] setTag:NSModalResponseCancel];
[self.dialog setMessageText:ns_message];
[self.dialog beginSheetModalForWindow:[self window]
completionHandler:^(NSModalResponse response) {
m_web_view_bridge->confirm_closed(response == NSModalResponseOK);
self.dialog = nil;
}];
};
m_web_view_bridge->on_prompt = [self](auto const& message, auto const& default_) {
auto* ns_message = Ladybird::string_to_ns_string(message);
auto* ns_default = Ladybird::string_to_ns_string(default_);
auto* input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 24)];
[input setStringValue:ns_default];
self.dialog = [[NSAlert alloc] init];
[[self.dialog addButtonWithTitle:@"OK"] setTag:NSModalResponseOK];
[[self.dialog addButtonWithTitle:@"Cancel"] setTag:NSModalResponseCancel];
[self.dialog setMessageText:ns_message];
[self.dialog setAccessoryView:input];
self.dialog.window.initialFirstResponder = input;
[self.dialog beginSheetModalForWindow:[self window]
completionHandler:^(NSModalResponse response) {
Optional<String> text;
if (response == NSModalResponseOK) {
text = Ladybird::ns_string_to_string([input stringValue]);
}
m_web_view_bridge->prompt_closed(move(text));
self.dialog = nil;
}];
};
m_web_view_bridge->on_prompt_text_changed = [self](auto const& message) {
if (self.dialog == nil || [self.dialog accessoryView] == nil) {
return;
}
auto* ns_message = Ladybird::string_to_ns_string(message);
auto* input = (NSTextField*)[self.dialog accessoryView];
[input setStringValue:ns_message];
};
m_web_view_bridge->on_dialog_accepted = [self]() {
if (self.dialog == nil) {
return;
}
[[self window] endSheet:[[self dialog] window]
returnCode:NSModalResponseOK];
};
m_web_view_bridge->on_dialog_dismissed = [self]() {
if (self.dialog == nil) {
return;
}
[[self window] endSheet:[[self dialog] window]
returnCode:NSModalResponseCancel];
};
m_web_view_bridge->on_get_all_cookies = [](auto const& url) {
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
return [delegate cookieJar].get_all_cookies(url);
};
m_web_view_bridge->on_get_named_cookie = [](auto const& url, auto const& name) {
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
return [delegate cookieJar].get_named_cookie(url, name);
};
m_web_view_bridge->on_get_cookie = [](auto const& url, auto source) -> DeprecatedString {
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
return [delegate cookieJar].get_cookie(url, source);
};
m_web_view_bridge->on_set_cookie = [](auto const& url, auto const& cookie, auto source) {
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
[delegate cookieJar].set_cookie(url, cookie, source);
};
m_web_view_bridge->on_update_cookie = [](auto const& cookie) {
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
[delegate cookieJar].update_cookie(cookie);
};
m_web_view_bridge->on_restore_window = [self]() {
[[self window] setIsMiniaturized:NO];
[[self window] orderFront:nil];
};
m_web_view_bridge->on_reposition_window = [self](auto const& position) {
auto frame = [[self window] frame];
frame.origin = Ladybird::gfx_point_to_ns_point(position);
[[self window] setFrame:frame display:YES];
return Ladybird::ns_point_to_gfx_point([[self window] frame].origin);
};
m_web_view_bridge->on_resize_window = [self](auto const& size) {
auto frame = [[self window] frame];
frame.size = Ladybird::gfx_size_to_ns_size(size);
[[self window] setFrame:frame display:YES];
return Ladybird::ns_size_to_gfx_size([[self window] frame].size);
};
m_web_view_bridge->on_maximize_window = [self]() {
auto frame = [[NSScreen mainScreen] frame];
[[self window] setFrame:frame display:YES];
return Ladybird::ns_rect_to_gfx_rect([[self window] frame]);
};
m_web_view_bridge->on_minimize_window = [self]() {
[[self window] setIsMiniaturized:YES];
return Ladybird::ns_rect_to_gfx_rect([[self window] frame]);
};
m_web_view_bridge->on_fullscreen_window = [self]() {
if (([[self window] styleMask] & NSWindowStyleMaskFullScreen) == 0) {
[[self window] toggleFullScreen:nil];
}
return Ladybird::ns_rect_to_gfx_rect([[self window] frame]);
};
}
- (Tab*)tab
{
return (Tab*)[self window];
}
- (TabController*)tabController
{
return (TabController*)[[self tab] windowController];
}
- (NSScrollView*)scrollView
{
return (NSScrollView*)[self superview];
}
static void copy_text_to_clipboard(StringView text)
{
auto* string = Ladybird::string_to_ns_string(text);
auto* pasteBoard = [NSPasteboard generalPasteboard];
[pasteBoard clearContents];
[pasteBoard setString:string forType:NSPasteboardTypeString];
}
- (void)copy:(id)sender
{
copy_text_to_clipboard(m_web_view_bridge->selected_text());
}
- (void)selectAll:(id)sender
{
m_web_view_bridge->select_all();
}
- (void)takeVisibleScreenshot:(id)sender
{
auto result = m_web_view_bridge->take_screenshot(WebView::ViewImplementation::ScreenshotType::Visible);
(void)result; // FIXME: Display an error if this failed.
}
- (void)takeFullScreenshot:(id)sender
{
auto result = m_web_view_bridge->take_screenshot(WebView::ViewImplementation::ScreenshotType::Full);
(void)result; // FIXME: Display an error if this failed.
}
- (void)openLink:(id)sender
{
m_web_view_bridge->on_link_click(m_context_menu_url, {}, 0);
}
- (void)openLinkInNewTab:(id)sender
{
m_web_view_bridge->on_link_middle_click(m_context_menu_url, {}, 0);
}
- (void)copyLink:(id)sender
{
copy_text_to_clipboard(m_context_menu_url.serialize());
}
- (void)copyImage:(id)sender
{
auto* bitmap = m_context_menu_bitmap.bitmap();
if (bitmap == nullptr) {
return;
}
auto png = Gfx::PNGWriter::encode(*bitmap);
if (png.is_error()) {
return;
}
auto* data = [NSData dataWithBytes:png.value().data() length:png.value().size()];
auto* pasteBoard = [NSPasteboard generalPasteboard];
[pasteBoard clearContents];
[pasteBoard setData:data forType:NSPasteboardTypePNG];
}
- (void)toggleMediaPlayState:(id)sender
{
m_web_view_bridge->toggle_media_play_state();
}
- (void)toggleMediaMuteState:(id)sender
{
m_web_view_bridge->toggle_media_mute_state();
}
- (void)toggleMediaControlsState:(id)sender
{
m_web_view_bridge->toggle_media_controls_state();
}
- (void)toggleMediaLoopState:(id)sender
{
m_web_view_bridge->toggle_media_loop_state();
}
#pragma mark - Properties
- (NSMenu*)page_context_menu
{
if (!_page_context_menu) {
_page_context_menu = [[NSMenu alloc] initWithTitle:@"Page Context Menu"];
[_page_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Go Back"
action:@selector(navigateBack:)
keyEquivalent:@""]];
[_page_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Go Forward"
action:@selector(navigateForward:)
keyEquivalent:@""]];
[_page_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Reload"
action:@selector(reload:)
keyEquivalent:@""]];
[_page_context_menu addItem:[NSMenuItem separatorItem]];
[_page_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Copy"
action:@selector(copy:)
keyEquivalent:@""]];
[_page_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Select All"
action:@selector(selectAll:)
keyEquivalent:@""]];
[_page_context_menu addItem:[NSMenuItem separatorItem]];
[_page_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Take Visible Screenshot"
action:@selector(takeVisibleScreenshot:)
keyEquivalent:@""]];
[_page_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Take Full Screenshot"
action:@selector(takeFullScreenshot:)
keyEquivalent:@""]];
}
return _page_context_menu;
}
- (NSMenu*)link_context_menu
{
if (!_link_context_menu) {
_link_context_menu = [[NSMenu alloc] initWithTitle:@"Link Context Menu"];
[_link_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Open"
action:@selector(openLink:)
keyEquivalent:@""]];
[_link_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Open in New Tab"
action:@selector(openLinkInNewTab:)
keyEquivalent:@""]];
[_link_context_menu addItem:[NSMenuItem separatorItem]];
[_link_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Copy URL"
action:@selector(copyLink:)
keyEquivalent:@""]];
}
return _link_context_menu;
}
- (NSMenu*)image_context_menu
{
if (!_image_context_menu) {
_image_context_menu = [[NSMenu alloc] initWithTitle:@"Image Context Menu"];
[_image_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Image"
action:@selector(openLink:)
keyEquivalent:@""]];
[_image_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Image in New Tab"
action:@selector(openLinkInNewTab:)
keyEquivalent:@""]];
[_image_context_menu addItem:[NSMenuItem separatorItem]];
[_image_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Copy Image"
action:@selector(copyImage:)
keyEquivalent:@""]];
[_image_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Copy Image URL"
action:@selector(copyLink:)
keyEquivalent:@""]];
}
return _image_context_menu;
}
- (NSMenu*)video_context_menu
{
if (!_video_context_menu) {
_video_context_menu = [[NSMenu alloc] initWithTitle:@"Video Context Menu"];
auto* play_pause_menu_item = [[NSMenuItem alloc] initWithTitle:@"Play"
action:@selector(toggleMediaPlayState:)
keyEquivalent:@""];
[play_pause_menu_item setTag:CONTEXT_MENU_PLAY_PAUSE_TAG];
auto* mute_unmute_menu_item = [[NSMenuItem alloc] initWithTitle:@"Mute"
action:@selector(toggleMediaMuteState:)
keyEquivalent:@""];
[mute_unmute_menu_item setTag:CONTEXT_MENU_MUTE_UNMUTE_TAG];
auto* controls_menu_item = [[NSMenuItem alloc] initWithTitle:@"Controls"
action:@selector(toggleMediaControlsState:)
keyEquivalent:@""];
[controls_menu_item setTag:CONTEXT_MENU_CONTROLS_TAG];
auto* loop_menu_item = [[NSMenuItem alloc] initWithTitle:@"Loop"
action:@selector(toggleMediaLoopState:)
keyEquivalent:@""];
[loop_menu_item setTag:CONTEXT_MENU_LOOP_TAG];
[_video_context_menu addItem:play_pause_menu_item];
[_video_context_menu addItem:mute_unmute_menu_item];
[_video_context_menu addItem:controls_menu_item];
[_video_context_menu addItem:loop_menu_item];
[_video_context_menu addItem:[NSMenuItem separatorItem]];
[_video_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Video"
action:@selector(openLink:)
keyEquivalent:@""]];
[_video_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Video in New Tab"
action:@selector(openLinkInNewTab:)
keyEquivalent:@""]];
[_video_context_menu addItem:[NSMenuItem separatorItem]];
[_video_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Copy Video URL"
action:@selector(copyLink:)
keyEquivalent:@""]];
}
return _video_context_menu;
}
- (NSTextField*)status_label
{
if (!_status_label) {
_status_label = [NSTextField labelWithString:@""];
[_status_label setDrawsBackground:YES];
[_status_label setBordered:YES];
[_status_label setHidden:YES];
[self addSubview:_status_label];
}
return _status_label;
}
#pragma mark - NSView
- (void)drawRect:(NSRect)rect
{
auto paintable = m_web_view_bridge->paintable();
if (!paintable.has_value()) {
[super drawRect:rect];
return;
}
auto [bitmap, bitmap_size] = *paintable;
VERIFY(bitmap.format() == Gfx::BitmapFormat::BGRA8888);
static constexpr size_t BITS_PER_COMPONENT = 8;
static constexpr size_t BITS_PER_PIXEL = 32;
static constexpr size_t COMPONENTS_PER_PIXEL = 4;
auto* context = [[NSGraphicsContext currentContext] CGContext];
CGContextSaveGState(context);
auto device_pixel_ratio = m_web_view_bridge->device_pixel_ratio();
auto inverse_device_pixel_ratio = m_web_view_bridge->inverse_device_pixel_ratio();
CGContextScaleCTM(context, inverse_device_pixel_ratio, inverse_device_pixel_ratio);
auto* provider = CGDataProviderCreateWithData(nil, bitmap.scanline_u8(0), bitmap.size_in_bytes(), nil);
auto image_rect = CGRectMake(rect.origin.x * device_pixel_ratio, rect.origin.y * device_pixel_ratio, bitmap_size.width(), bitmap_size.height());
// Ideally, this would be NSBitmapImageRep, but the equivalent factory initWithBitmapDataPlanes: does
// not seem to actually respect endianness. We need NSBitmapFormatThirtyTwoBitLittleEndian, but the
// resulting image is always big endian. CGImageCreate actually does respect the endianness.
auto* bitmap_image = CGImageCreate(
bitmap_size.width(),
bitmap_size.height(),
BITS_PER_COMPONENT,
BITS_PER_PIXEL,
COMPONENTS_PER_PIXEL * bitmap.width(),
CGColorSpaceCreateDeviceRGB(),
kCGBitmapByteOrder32Little | kCGImageAlphaFirst,
provider,
nil,
NO,
kCGRenderingIntentDefault);
auto* image = [[NSImage alloc] initWithCGImage:bitmap_image size:NSZeroSize];
[image drawInRect:image_rect];
CGContextRestoreGState(context);
CGImageRelease(bitmap_image);
[super drawRect:rect];
}
- (void)viewDidEndLiveResize
{
[super viewDidEndLiveResize];
[self handleResize];
}
- (BOOL)isFlipped
{
// The origin of a NSScrollView is the lower-left corner, with the y-axis extending upwards. Instead,
// we want the origin to be the top-left corner, with the y-axis extending downward.
return YES;
}
- (void)mouseMoved:(NSEvent*)event
{
auto [position, button, modifiers] = Ladybird::ns_event_to_mouse_event(event, self, GUI::MouseButton::None);
m_web_view_bridge->mouse_move_event(position, button, modifiers);
}
- (void)mouseDown:(NSEvent*)event
{
[[self window] makeFirstResponder:self];
auto [position, button, modifiers] = Ladybird::ns_event_to_mouse_event(event, self, GUI::MouseButton::Primary);
if (event.clickCount % 2 == 0) {
m_web_view_bridge->mouse_double_click_event(position, button, modifiers);
} else {
m_web_view_bridge->mouse_down_event(position, button, modifiers);
}
}
- (void)mouseUp:(NSEvent*)event
{
auto [position, button, modifiers] = Ladybird::ns_event_to_mouse_event(event, self, GUI::MouseButton::Primary);
m_web_view_bridge->mouse_up_event(position, button, modifiers);
}
- (void)mouseDragged:(NSEvent*)event
{
auto [position, button, modifiers] = Ladybird::ns_event_to_mouse_event(event, self, GUI::MouseButton::Primary);
m_web_view_bridge->mouse_move_event(position, button, modifiers);
}
- (void)rightMouseDown:(NSEvent*)event
{
[[self window] makeFirstResponder:self];
auto [position, button, modifiers] = Ladybird::ns_event_to_mouse_event(event, self, GUI::MouseButton::Secondary);
if (event.clickCount % 2 == 0) {
m_web_view_bridge->mouse_double_click_event(position, button, modifiers);
} else {
m_web_view_bridge->mouse_down_event(position, button, modifiers);
}
}
- (void)rightMouseUp:(NSEvent*)event
{
auto [position, button, modifiers] = Ladybird::ns_event_to_mouse_event(event, self, GUI::MouseButton::Secondary);
m_web_view_bridge->mouse_up_event(position, button, modifiers);
}
- (void)rightMouseDragged:(NSEvent*)event
{
auto [position, button, modifiers] = Ladybird::ns_event_to_mouse_event(event, self, GUI::MouseButton::Secondary);
m_web_view_bridge->mouse_move_event(position, button, modifiers);
}
- (void)keyDown:(NSEvent*)event
{
auto [key_code, modifiers, code_point] = Ladybird::ns_event_to_key_event(event);
m_web_view_bridge->key_down_event(key_code, modifiers, code_point);
}
- (void)keyUp:(NSEvent*)event
{
auto [key_code, modifiers, code_point] = Ladybird::ns_event_to_key_event(event);
m_web_view_bridge->key_up_event(key_code, modifiers, code_point);
}
@end

View File

@ -0,0 +1,325 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <Ladybird/HelperProcess.h>
#include <Ladybird/Types.h>
#include <Ladybird/Utilities.h>
#include <LibCore/File.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibGfx/Rect.h>
#include <LibIPC/File.h>
#include <LibWeb/Crypto/Crypto.h>
#include <UI/LadybirdWebViewBridge.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<Gfx::IntRect> screen_rects, float device_pixel_ratio, Optional<StringView> webdriver_content_ipc_path)
{
return adopt_nonnull_own_or_enomem(new (nothrow) WebViewBridge(move(screen_rects), device_pixel_ratio, move(webdriver_content_ipc_path)));
}
WebViewBridge::WebViewBridge(Vector<Gfx::IntRect> screen_rects, float device_pixel_ratio, Optional<StringView> webdriver_content_ipc_path)
: m_screen_rects(move(screen_rects))
, m_webdriver_content_ipc_path(move(webdriver_content_ipc_path))
{
m_device_pixel_ratio = device_pixel_ratio;
m_inverse_device_pixel_ratio = 1.0 / device_pixel_ratio;
create_client(WebView::EnableCallgrindProfiling::No);
}
WebViewBridge::~WebViewBridge() = default;
void WebViewBridge::set_system_visibility_state(bool is_visible)
{
client().async_set_system_visibility_state(is_visible);
}
void WebViewBridge::set_viewport_rect(Gfx::IntRect viewport_rect, ForResize for_resize)
{
viewport_rect.set_size(scale_for_device(viewport_rect.size(), m_device_pixel_ratio));
m_viewport_rect = viewport_rect;
client().async_set_viewport_rect(m_viewport_rect);
request_repaint();
if (for_resize == ForResize::Yes) {
handle_resize();
}
}
void WebViewBridge::mouse_down_event(Gfx::IntPoint position, GUI::MouseButton button, KeyModifier modifiers)
{
client().async_mouse_down(to_content_position(position), to_underlying(button), to_underlying(button), modifiers);
}
void WebViewBridge::mouse_up_event(Gfx::IntPoint position, GUI::MouseButton button, KeyModifier modifiers)
{
client().async_mouse_up(to_content_position(position), to_underlying(button), to_underlying(button), modifiers);
}
void WebViewBridge::mouse_move_event(Gfx::IntPoint position, GUI::MouseButton button, KeyModifier modifiers)
{
client().async_mouse_move(to_content_position(position), 0, to_underlying(button), modifiers);
}
void WebViewBridge::mouse_double_click_event(Gfx::IntPoint position, GUI::MouseButton button, KeyModifier modifiers)
{
client().async_doubleclick(to_content_position(position), button, to_underlying(button), modifiers);
}
void WebViewBridge::key_down_event(KeyCode key_code, KeyModifier modifiers, u32 code_point)
{
client().async_key_down(key_code, modifiers, code_point);
}
void WebViewBridge::key_up_event(KeyCode key_code, KeyModifier modifiers, u32 code_point)
{
client().async_key_up(key_code, modifiers, code_point);
}
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;
} else {
bitmap = m_backup_bitmap.ptr();
bitmap_size = m_backup_bitmap_size;
}
if (!bitmap)
return {};
return Paintable { *bitmap, bitmap_size };
}
void WebViewBridge::notify_server_did_layout(Badge<WebView::WebContentClient>, Gfx::IntSize content_size)
{
if (on_layout) {
content_size = scale_for_device(content_size, m_inverse_device_pixel_ratio);
on_layout(content_size);
}
}
void WebViewBridge::notify_server_did_paint(Badge<WebView::WebContentClient>, i32 bitmap_id, Gfx::IntSize size)
{
if (m_client_state.back_bitmap.id == bitmap_id) {
m_client_state.has_usable_bitmap = true;
m_client_state.back_bitmap.pending_paints--;
m_client_state.back_bitmap.last_painted_size = size;
swap(m_client_state.back_bitmap, m_client_state.front_bitmap);
// We don't need the backup bitmap anymore, so drop it.
m_backup_bitmap = nullptr;
if (on_ready_to_paint)
on_ready_to_paint();
if (m_client_state.got_repaint_requests_while_painting) {
m_client_state.got_repaint_requests_while_painting = false;
request_repaint();
}
}
}
void WebViewBridge::notify_server_did_invalidate_content_rect(Badge<WebView::WebContentClient>, Gfx::IntRect const&)
{
request_repaint();
}
void WebViewBridge::notify_server_did_change_selection(Badge<WebView::WebContentClient>)
{
request_repaint();
}
void WebViewBridge::notify_server_did_request_cursor_change(Badge<WebView::WebContentClient>, Gfx::StandardCursor cursor)
{
if (on_cursor_change)
on_cursor_change(cursor);
}
void WebViewBridge::notify_server_did_request_scroll(Badge<WebView::WebContentClient>, i32 x_delta, i32 y_delta)
{
// FIXME: This currently isn't reached because we do not yet propagate mouse wheel events to WebContent.
// When that is implemented, make sure our mutations to the viewport position here are correct.
auto position = m_viewport_rect.location();
position.set_x(position.x() + x_delta);
position.set_y(position.y() + y_delta);
if (on_scroll)
on_scroll(position);
}
void WebViewBridge::notify_server_did_request_scroll_to(Badge<WebView::WebContentClient>, Gfx::IntPoint position)
{
if (on_scroll)
on_scroll(position);
}
void WebViewBridge::notify_server_did_request_scroll_into_view(Badge<WebView::WebContentClient>, Gfx::IntRect const& rect)
{
if (m_viewport_rect.contains(rect))
return;
auto position = m_viewport_rect.location();
if (rect.top() < m_viewport_rect.top()) {
position.set_y(rect.top());
} else if (rect.top() > m_viewport_rect.top() && rect.bottom() > m_viewport_rect.bottom()) {
position.set_y(rect.bottom() - m_viewport_rect.height());
} else {
return;
}
if (on_scroll)
on_scroll(position);
}
void WebViewBridge::notify_server_did_enter_tooltip_area(Badge<WebView::WebContentClient>, Gfx::IntPoint, DeprecatedString const& tooltip)
{
if (on_tooltip_entered)
on_tooltip_entered(tooltip);
}
void WebViewBridge::notify_server_did_leave_tooltip_area(Badge<WebView::WebContentClient>)
{
if (on_tooltip_left)
on_tooltip_left();
}
void WebViewBridge::notify_server_did_request_alert(Badge<WebView::WebContentClient>, String const& message)
{
if (on_alert)
on_alert(message);
}
void WebViewBridge::alert_closed()
{
client().async_alert_closed();
}
void WebViewBridge::notify_server_did_request_confirm(Badge<WebView::WebContentClient>, String const& message)
{
if (on_confirm)
on_confirm(message);
}
void WebViewBridge::confirm_closed(bool accepted)
{
client().async_confirm_closed(accepted);
}
void WebViewBridge::notify_server_did_request_prompt(Badge<WebView::WebContentClient>, String const& message, String const& default_)
{
if (on_prompt)
on_prompt(message, default_);
}
void WebViewBridge::prompt_closed(Optional<String> response)
{
client().async_prompt_closed(move(response));
}
void WebViewBridge::notify_server_did_request_set_prompt_text(Badge<WebView::WebContentClient>, String const& message)
{
if (on_prompt_text_changed)
on_prompt_text_changed(message);
}
void WebViewBridge::notify_server_did_request_accept_dialog(Badge<WebView::WebContentClient>)
{
if (on_dialog_accepted)
on_dialog_accepted();
}
void WebViewBridge::notify_server_did_request_dismiss_dialog(Badge<WebView::WebContentClient>)
{
if (on_dialog_dismissed)
on_dialog_dismissed();
}
void WebViewBridge::notify_server_did_request_file(Badge<WebView::WebContentClient>, DeprecatedString const& path, i32 request_id)
{
auto file = Core::File::open(path, Core::File::OpenMode::Read);
if (file.is_error())
client().async_handle_file_return(file.error().code(), {}, request_id);
else
client().async_handle_file_return(0, IPC::File(*file.value()), request_id);
}
void WebViewBridge::notify_server_did_finish_handling_input_event(bool)
{
}
void WebViewBridge::update_zoom()
{
}
Gfx::IntRect WebViewBridge::viewport_rect() const
{
return m_viewport_rect;
}
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, m_inverse_device_pixel_ratio);
}
void WebViewBridge::create_client(WebView::EnableCallgrindProfiling enable_callgrind_profiling)
{
m_client_state = {};
auto candidate_web_content_paths = MUST(get_paths_for_helper_process("WebContent"sv));
auto new_client = MUST(launch_web_content_process(*this, candidate_web_content_paths, enable_callgrind_profiling, WebView::IsLayoutTestMode::No, Ladybird::UseLagomNetworking::Yes));
m_client_state.client = new_client;
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.client_handle);
client().async_set_device_pixels_per_css_pixel(m_device_pixel_ratio);
client().async_update_system_fonts(Gfx::FontDatabase::default_font_query(), Gfx::FontDatabase::fixed_width_font_query(), Gfx::FontDatabase::window_title_font_query());
update_palette();
if (!m_screen_rects.is_empty()) {
// FIXME: Update the screens again if they ever change.
client().async_update_screen_rects(m_screen_rects, 0);
}
if (m_webdriver_content_ipc_path.has_value()) {
client().async_connect_to_webdriver(*m_webdriver_content_ipc_path);
}
}
void WebViewBridge::update_palette()
{
auto theme = MUST(Gfx::load_system_theme(DeprecatedString::formatted("{}/res/themes/Default.ini", s_serenity_resource_root)));
auto palette_impl = Gfx::PaletteImpl::create_with_anonymous_buffer(theme);
auto palette = Gfx::Palette(move(palette_impl));
client().async_update_system_theme(move(theme));
}
}

View File

@ -0,0 +1,114 @@
/*
* Copyright (c) 2023, 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 <LibWebView/ViewImplementation.h>
// FIXME: These should not be included outside of Serenity.
#include <Kernel/API/KeyCode.h>
#include <LibGUI/Event.h>
namespace Ladybird {
class WebViewBridge final : public WebView::ViewImplementation {
public:
static ErrorOr<NonnullOwnPtr<WebViewBridge>> create(Vector<Gfx::IntRect> screen_rects, float device_pixel_ratio, Optional<StringView> webdriver_content_ipc_path);
virtual ~WebViewBridge() override;
float device_pixel_ratio() const { return m_device_pixel_ratio; }
float inverse_device_pixel_ratio() const { return m_inverse_device_pixel_ratio; }
void set_system_visibility_state(bool is_visible);
enum class ForResize {
Yes,
No,
};
void set_viewport_rect(Gfx::IntRect, ForResize = ForResize::No);
void mouse_down_event(Gfx::IntPoint, GUI::MouseButton, KeyModifier);
void mouse_up_event(Gfx::IntPoint, GUI::MouseButton, KeyModifier);
void mouse_move_event(Gfx::IntPoint, GUI::MouseButton, KeyModifier);
void mouse_double_click_event(Gfx::IntPoint, GUI::MouseButton, KeyModifier);
void key_down_event(KeyCode, KeyModifier, u32);
void key_up_event(KeyCode, KeyModifier, u32);
struct Paintable {
Gfx::Bitmap& bitmap;
Gfx::IntSize bitmap_size;
};
Optional<Paintable> paintable();
Function<void(Gfx::IntSize)> on_layout;
Function<void()> on_ready_to_paint;
Function<void(Gfx::IntPoint)> on_scroll;
Function<void(Gfx::StandardCursor)> on_cursor_change;
Function<void(DeprecatedString const&)> on_tooltip_entered;
Function<void()> on_tooltip_left;
Function<void(String const&)> on_alert;
void alert_closed();
Function<void(String const&)> on_confirm;
void confirm_closed(bool);
Function<void(String const&, String const&)> on_prompt;
Function<void(String const&)> on_prompt_text_changed;
void prompt_closed(Optional<String>);
Function<void()> on_dialog_accepted;
Function<void()> on_dialog_dismissed;
private:
WebViewBridge(Vector<Gfx::IntRect> screen_rects, float device_pixel_ratio, Optional<StringView> webdriver_content_ipc_path);
virtual void notify_server_did_layout(Badge<WebView::WebContentClient>, Gfx::IntSize content_size) override;
virtual void notify_server_did_paint(Badge<WebView::WebContentClient>, i32 bitmap_id, Gfx::IntSize) override;
virtual void notify_server_did_invalidate_content_rect(Badge<WebView::WebContentClient>, Gfx::IntRect const&) override;
virtual void notify_server_did_change_selection(Badge<WebView::WebContentClient>) override;
virtual void notify_server_did_request_cursor_change(Badge<WebView::WebContentClient>, Gfx::StandardCursor cursor) override;
virtual void notify_server_did_request_scroll(Badge<WebView::WebContentClient>, i32, i32) override;
virtual void notify_server_did_request_scroll_to(Badge<WebView::WebContentClient>, Gfx::IntPoint) override;
virtual void notify_server_did_request_scroll_into_view(Badge<WebView::WebContentClient>, Gfx::IntRect const&) override;
virtual void notify_server_did_enter_tooltip_area(Badge<WebView::WebContentClient>, Gfx::IntPoint, DeprecatedString const&) override;
virtual void notify_server_did_leave_tooltip_area(Badge<WebView::WebContentClient>) override;
virtual void notify_server_did_request_alert(Badge<WebView::WebContentClient>, String const& message) override;
virtual void notify_server_did_request_confirm(Badge<WebView::WebContentClient>, String const& message) override;
virtual void notify_server_did_request_prompt(Badge<WebView::WebContentClient>, String const& message, String const& default_) override;
virtual void notify_server_did_request_set_prompt_text(Badge<WebView::WebContentClient>, String const& message) override;
virtual void notify_server_did_request_accept_dialog(Badge<WebView::WebContentClient>) override;
virtual void notify_server_did_request_dismiss_dialog(Badge<WebView::WebContentClient>) override;
virtual void notify_server_did_request_file(Badge<WebView::WebContentClient>, DeprecatedString const& path, i32) override;
virtual void notify_server_did_finish_handling_input_event(bool event_was_accepted) override;
virtual void update_zoom() override;
virtual Gfx::IntRect viewport_rect() 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;
virtual void create_client(WebView::EnableCallgrindProfiling) override;
void update_palette();
Vector<Gfx::IntRect> m_screen_rects;
Gfx::IntRect m_viewport_rect;
float m_inverse_device_pixel_ratio { 1.0 };
Optional<StringView> m_webdriver_content_ipc_path;
};
}

23
Ladybird/AppKit/UI/Tab.h Normal file
View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/URL.h>
#import <System/Cocoa.h>
@class LadybirdWebView;
@interface Tab : NSWindow
- (void)onLoadStart:(URL const&)url;
- (void)onTitleChange:(NSString*)title;
- (void)onFaviconChange:(NSImage*)favicon;
@property (nonatomic, strong) LadybirdWebView* web_view;
@end

170
Ladybird/AppKit/UI/Tab.mm Normal file
View File

@ -0,0 +1,170 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#import <UI/LadybirdWebView.h>
#import <UI/Tab.h>
#import <UI/TabController.h>
#import <Utilities/Conversions.h>
#include <Ladybird/Utilities.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 ()
@property (nonatomic, strong) NSString* title;
@property (nonatomic, strong) NSImage* favicon;
@end
@implementation Tab
@dynamic title;
+ (NSImage*)defaultFavicon
{
static NSImage* default_favicon;
static dispatch_once_t token;
dispatch_once(&token, ^{
auto default_favicon_path = MUST(String::formatted("{}/res/icons/16x16/app-browser.png", s_serenity_resource_root));
auto* ns_default_favicon_path = Ladybird::string_to_ns_string(default_favicon_path);
default_favicon = [[NSImage alloc] initWithContentsOfFile:ns_default_favicon_path];
});
return default_favicon;
}
- (instancetype)init
{
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);
auto style_mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
self = [super initWithContentRect:window_rect
styleMask:style_mask
backing:NSBackingStoreBuffered
defer:NO];
if (self) {
self.web_view = [[LadybirdWebView alloc] init];
[self.web_view setPostsBoundsChangedNotifications:YES];
self.favicon = [Tab defaultFavicon];
self.title = @"New Tab";
[self updateTabTitleAndFavicon];
[self setTitleVisibility:NSWindowTitleHidden];
[self setIsVisible:YES];
auto* scroll_view = [[NSScrollView alloc] initWithFrame:[self frame]];
[scroll_view setHasVerticalScroller:YES];
[scroll_view setHasHorizontalScroller:YES];
[scroll_view setLineScroll:24];
[scroll_view setContentView:self.web_view];
[scroll_view setDocumentView:[[NSView alloc] init]];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(onContentScroll:)
name:NSViewBoundsDidChangeNotification
object:[scroll_view contentView]];
[self setContentView:scroll_view];
}
return self;
}
#pragma mark - Public methods
- (void)onLoadStart:(URL const&)url
{
self.title = Ladybird::string_to_ns_string(url.serialize());
self.favicon = [Tab defaultFavicon];
[self updateTabTitleAndFavicon];
}
- (void)onTitleChange:(NSString*)title
{
self.title = title;
[self updateTabTitleAndFavicon];
}
- (void)onFaviconChange:(NSImage*)favicon
{
self.favicon = favicon;
[self updateTabTitleAndFavicon];
}
#pragma mark - Private methods
- (void)updateTabTitleAndFavicon
{
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])];
// By default, the text attachment will be aligned to the bottom of the string. We have to manually
// try to center it vertically.
// FIXME: Figure out a way to programmatically arrive at a good NSBaselineOffsetAttributeName. Using
// half the distance between the font's line height and the height of the favicon produces a
// value that results in the title being aligned too low still.
auto* title_attributes = @{
NSForegroundColorAttributeName : [NSColor textColor],
NSBaselineOffsetAttributeName : @3
};
auto* title_attribute = [[NSAttributedString alloc] initWithString:self.title
attributes:title_attributes];
auto* spacing_attribute = [[NSAttributedString alloc] initWithString:@" "];
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)onContentScroll:(NSNotification*)notification
{
[[self web_view] handleScroll];
}
#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,30 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#include <AK/URL.h>
#import <System/Cocoa.h>
@interface TabController : NSWindowController <NSWindowDelegate>
- (instancetype)init:(URL)url;
- (void)load:(URL const&)url;
- (void)onLoadStart:(URL const&)url isRedirect:(BOOL)isRedirect;
- (void)onTitleChange:(DeprecatedString const&)title;
- (void)navigateBack:(id)sender;
- (void)navigateForward:(id)sender;
- (void)reload:(id)sender;
- (void)clearHistory;
- (void)focusLocationToolbarItem;
@end

View File

@ -0,0 +1,395 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <Browser/History.h>
#import <Application/ApplicationDelegate.h>
#import <UI/LadybirdWebView.h>
#import <UI/Tab.h>
#import <UI/TabController.h>
#import <Utilities/Conversions.h>
#import <Utilities/URL.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_NEW_TAB_IDENTIFIER = @"ToolbarNewTabIdentifier";
static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIdentifer";
enum class IsHistoryNavigation {
Yes,
No,
};
@interface TabController () <NSToolbarDelegate, NSSearchFieldDelegate>
{
URL m_url;
DeprecatedString m_title;
Browser::History m_history;
IsHistoryNavigation m_is_history_navigation;
}
@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* 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 new_tab_toolbar_item = _new_tab_toolbar_item;
@synthesize tab_overview_toolbar_item = _tab_overview_toolbar_item;
- (instancetype)init:(URL)url
{
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_url = move(url);
m_is_history_navigation = IsHistoryNavigation::No;
}
return self;
}
#pragma mark - Public methods
- (void)load:(URL const&)url
{
[[self tab].web_view load:url];
}
- (void)onLoadStart:(URL const&)url isRedirect:(BOOL)isRedirect
{
if (isRedirect) {
m_history.replace_current(url, m_title);
}
auto* url_string = Ladybird::string_to_ns_string(url.serialize());
auto* location_search_field = (NSSearchField*)[self.location_toolbar_item view];
[location_search_field setStringValue:url_string];
if (m_is_history_navigation == IsHistoryNavigation::Yes) {
m_is_history_navigation = IsHistoryNavigation::No;
} else {
m_history.push(url, m_title);
}
[self updateNavigationButtonStates];
}
- (void)onTitleChange:(DeprecatedString const&)title
{
m_title = title;
m_history.update_title(m_title);
}
- (void)navigateBack:(id)sender
{
if (!m_history.can_go_back()) {
return;
}
m_is_history_navigation = IsHistoryNavigation::Yes;
m_history.go_back();
auto url = m_history.current().url;
[self load:url];
}
- (void)navigateForward:(id)sender
{
if (!m_history.can_go_forward()) {
return;
}
m_is_history_navigation = IsHistoryNavigation::Yes;
m_history.go_forward();
auto url = m_history.current().url;
[self load:url];
}
- (void)reload:(id)sender
{
m_is_history_navigation = IsHistoryNavigation::Yes;
auto url = m_history.current().url;
[self load:url];
}
- (void)clearHistory
{
m_history.clear();
[self updateNavigationButtonStates];
}
- (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];
[delegate createNewTab:OptionalNone {}];
}
- (void)updateNavigationButtonStates
{
auto* navigate_back_button = (NSButton*)[[self navigate_back_toolbar_item] view];
[navigate_back_button setEnabled:m_history.can_go_back()];
auto* navigate_forward_button = (NSButton*)[[self navigate_forward_toolbar_item] view];
[navigate_forward_button setEnabled:m_history.can_go_forward()];
}
- (void)showTabOverview:(id)sender
{
[self.window toggleTabOverview:sender];
}
#pragma mark - Properties
- (NSButton*)create_button:(NSImageName)image
with_action:(nonnull SEL)action
{
auto* button = [NSButton buttonWithImage:[NSImage imageNamed:image]
target:self
action:action];
[button setBordered:NO];
return button;
}
- (NSToolbarItem*)navigate_back_toolbar_item
{
if (!_navigate_back_toolbar_item) {
auto* button = [self create_button:NSImageNameGoBackTemplate
with_action:@selector(navigateBack:)];
[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:)];
[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:)];
_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 = [[NSSearchField 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*)new_tab_toolbar_item
{
if (!_new_tab_toolbar_item) {
auto* button = [self create_button:NSImageNameAddTemplate
with_action:@selector(createNewTab:)];
_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:)];
_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,
NSToolbarFlexibleSpaceItemIdentifier,
TOOLBAR_NEW_TAB_IDENTIFIER,
TOOLBAR_TAB_OVERVIEW_IDENTIFIER,
];
}
return _toolbar_identifiers;
}
#pragma mark - NSWindowController
- (IBAction)showWindow:(id)sender
{
self.window = [[Tab alloc] init];
[self load:m_url];
[self.window setDelegate:self];
[self.window setToolbar:self.toolbar];
[self.window setToolbarStyle:NSWindowToolbarStyleUnified];
[self.window makeKeyAndOrderFront:sender];
[self focusLocationToolbarItem];
}
#pragma mark - NSWindowDelegate
- (void)windowWillClose:(NSNotification*)notification
{
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
[delegate removeTab:self];
}
- (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;
if (![[self window] inLiveResize]) {
[[[self tab] web_view] handleResize];
}
}
#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_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 = [[text_view textStorage] string];
auto url = Ladybird::sanitize_url(url_string);
[self load:url];
[self.window makeFirstResponder:nil];
return YES;
}
@end

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <AK/StringView.h>
#include <LibGfx/Point.h>
#include <LibGfx/Rect.h>
#include <LibGfx/Size.h>
#import <System/Cocoa.h>
namespace Ladybird {
String ns_string_to_string(NSString*);
NSString* string_to_ns_string(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);
}

View File

@ -0,0 +1,72 @@
/*
* 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) }));
}
NSString* string_to_ns_string(StringView string)
{
auto* data = [NSData dataWithBytes:string.characters_without_null_termination() length:string.length()];
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
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()));
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/StringView.h>
#include <AK/URL.h>
#import <System/Cocoa.h>
namespace Ladybird {
URL sanitize_url(NSString*);
URL sanitize_url(StringView);
URL rebase_url_on_serenity_resource_root(StringView);
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/DeprecatedString.h>
#include <AK/String.h>
#include <AK/Vector.h>
#include <Ladybird/Utilities.h>
#include <LibFileSystem/FileSystem.h>
#import <Utilities/URL.h>
namespace Ladybird {
URL sanitize_url(StringView url_string)
{
if (url_string.starts_with('/') || FileSystem::exists(url_string))
return MUST(String::formatted("file://{}", MUST(FileSystem::real_path(url_string))));
URL url { url_string };
if (!url.is_valid())
url = MUST(String::formatted("https://{}", url_string));
return url;
}
URL sanitize_url(NSString* url_string)
{
auto const* utf8 = [url_string UTF8String];
return sanitize_url({ utf8, strlen(utf8) });
}
URL rebase_url_on_serenity_resource_root(StringView url_string)
{
URL url { url_string };
Vector<DeprecatedString> paths;
for (auto segment : s_serenity_resource_root.split('/'))
paths.append(move(segment));
for (size_t i = 0; i < url.path_segment_count(); ++i)
paths.append(url.path_segment_at_index(i));
url.set_paths(move(paths));
return url;
}
}

67
Ladybird/AppKit/main.mm Normal file
View File

@ -0,0 +1,67 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <Browser/CookieJar.h>
#include <Browser/Database.h>
#include <Ladybird/Utilities.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/EventLoop.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibMain/Main.h>
#import <Application/Application.h>
#import <Application/ApplicationDelegate.h>
#import <Application/EventLoopImplementation.h>
#import <UI/Tab.h>
#import <UI/TabController.h>
#import <Utilities/URL.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
[Application sharedApplication];
Core::EventLoopManager::install(*new Ladybird::CFEventLoopManager);
Core::EventLoop event_loop;
platform_init();
// NOTE: We only instantiate this to ensure that Gfx::FontDatabase has its default queries initialized.
Gfx::FontDatabase::set_default_font_query("Katica 10 400 0");
Gfx::FontDatabase::set_fixed_width_font_query("Csilla 10 400 0");
StringView url;
StringView webdriver_content_ipc_path;
Core::ArgsParser args_parser;
args_parser.set_general_help("The Ladybird web browser");
args_parser.add_positional_argument(url, "URL to open", "url", Core::ArgsParser::Required::No);
args_parser.add_option(webdriver_content_ipc_path, "Path to WebDriver IPC for WebContent", "webdriver-content-path", 0, "path");
args_parser.parse(arguments);
auto sql_server_paths = TRY(get_paths_for_helper_process("SQLServer"sv));
auto sql_client = TRY(SQL::SQLClient::launch_server_and_create_client(move(sql_server_paths)));
auto database = TRY(Browser::Database::create(move(sql_client)));
auto cookie_jar = TRY(Browser::CookieJar::create(*database));
Optional<URL> initial_url;
if (auto parsed_url = Ladybird::sanitize_url(url); parsed_url.is_valid()) {
initial_url = move(parsed_url);
}
auto* delegate = [[ApplicationDelegate alloc] init:move(initial_url)
withCookieJar:move(cookie_jar)
webdriverContentIPCPath:webdriver_content_ipc_path];
[NSApp setDelegate:delegate];
[NSApp activateIgnoringOtherApps:YES];
return event_loop.exec();
}

View File

@ -81,6 +81,8 @@ if (ENABLE_QT)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Network Multimedia)
elseif (APPLE)
find_library(COCOA_LIBRARY Cocoa)
endif()
set(BROWSER_SOURCE_DIR ${SERENITY_SOURCE_DIR}/Userland/Applications/Browser/)
@ -124,11 +126,33 @@ if (ENABLE_QT)
Qt/main.cpp
)
target_link_libraries(ladybird PRIVATE Qt::Core Qt::Gui Qt::Network Qt::Widgets)
elseif (APPLE)
add_executable(ladybird MACOSX_BUNDLE
${SOURCES}
AppKit/main.mm
AppKit/Application/Application.mm
AppKit/Application/ApplicationDelegate.mm
AppKit/Application/EventLoopImplementation.mm
AppKit/UI/Event.mm
AppKit/UI/LadybirdWebView.mm
AppKit/UI/LadybirdWebViewBridge.cpp
AppKit/UI/Tab.mm
AppKit/UI/TabController.mm
AppKit/Utilities/Conversions.mm
AppKit/Utilities/URL.mm
)
target_include_directories(ladybird PRIVATE AppKit)
target_link_libraries(ladybird PRIVATE ${COCOA_LIBRARY})
target_compile_options(ladybird PRIVATE
-fobjc-arc
-Wno-deprecated-anon-enum-enum-conversion # Required for CGImageCreate
)
else()
# TODO: Check for other GUI frameworks here when we move them in-tree
# For now, we can export a static library of common files for chromes to link to
add_library(ladybird STATIC ${SOURCES})
endif()
target_sources(ladybird PUBLIC FILE_SET browser TYPE HEADERS
BASE_DIRS ${SERENITY_SOURCE_DIR}/Userland/Applications
FILES ${BROWSER_HEADERS}
@ -205,9 +229,7 @@ function(create_ladybird_bundle target_name)
endif()
endfunction()
if (ENABLE_QT)
create_ladybird_bundle(ladybird)
endif()
create_ladybird_bundle(ladybird)
if(NOT CMAKE_SKIP_INSTALL_RULES)
include(cmake/InstallRules.cmake)

View File

@ -34,6 +34,8 @@ LICENSE_HEADER_CHECK_EXCLUDES = {
PRAGMA_ONCE_STRING = '#pragma once'
PRAGMA_ONCE_CHECK_EXCLUDES = {
'Userland/Libraries/LibC/assert.h',
'Ladybird/AppKit/System/Detail/Header.h',
'Ladybird/AppKit/System/Detail/Footer.h',
}
# We make sure that there's a blank line before and after pragma once