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