Files
ladybird/Ladybird/AppKit/UI/Tab.mm
Timothy Flynn 53d5c7084e UI/AppKit: Extract bare minimum WebView functionality into its own class
When a window containing a WebView becomes visibile, we have to inform
WebContent. This was only implemented for the Tab class (not Inspector
or Task Manaager).

This patch adds LadybirdWebViewWindow to contain the bare minimum needed
to render a LadybirdWebView. All windows containing a WebView inherit
from this class, to ensure their WebContent processes know they became
visible.
2024-10-30 08:51:14 +01:00

397 lines
11 KiB
Plaintext

/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ByteString.h>
#include <AK/String.h>
#include <Ladybird/Utilities.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 <UI/Inspector.h>
#import <UI/InspectorController.h>
#import <UI/LadybirdWebView.h>
#import <UI/SearchPanel.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 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.enclosingScrollView,
]];
[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