We currently use LibWebView's Application as the entry point to learn about the bookmarks bar being shown/hidden, and propagate that through virtual methods. At the time this was added, AppKit's Tab window and Qt's BrowserWindow did not have a settings observer. They do now, so let's skip a couple of middle-men. For AppKit, we change the settings observer to just (weakly) store the Tab instance so that we don't have to add callback functions for each setting.
482 lines
16 KiB
Text
482 lines
16 KiB
Text
/*
|
|
* Copyright (c) 2023-2026, Tim Flynn <trflynn89@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/Function.h>
|
|
#include <AK/OwnPtr.h>
|
|
#include <AK/String.h>
|
|
#include <LibCore/Resource.h>
|
|
#include <LibURL/URL.h>
|
|
#include <LibWebView/Application.h>
|
|
#include <LibWebView/Settings.h>
|
|
#include <LibWebView/ViewImplementation.h>
|
|
#include <LibWebView/WebContentClient.h>
|
|
|
|
#import <Application/ApplicationDelegate.h>
|
|
#import <Interface/BookmarksBar.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;
|
|
static constexpr CGFloat const TAB_ICON_SIZE = 16;
|
|
static constexpr NSUInteger const TAB_LOADING_SPINNER_SEGMENT_COUNT = 12;
|
|
|
|
class TabSettingsObserver final : public WebView::SettingsObserver {
|
|
public:
|
|
explicit TabSettingsObserver(Tab* tab)
|
|
: m_tab(tab)
|
|
{
|
|
}
|
|
|
|
private:
|
|
// These are forward-declared so that they may access non-public Tab methods.
|
|
virtual void show_bookmarks_bar_changed() override;
|
|
virtual void config_variable_changed(WebView::ConfigVariableID variable) override;
|
|
|
|
__weak Tab* m_tab { nil };
|
|
};
|
|
|
|
static NSImage* tab_loading_spinner_icon(NSUInteger frame)
|
|
{
|
|
auto* image = [NSImage imageWithSize:NSMakeSize(TAB_ICON_SIZE, TAB_ICON_SIZE)
|
|
flipped:NO
|
|
drawingHandler:^BOOL(NSRect) {
|
|
auto* context = [NSGraphicsContext currentContext].CGContext;
|
|
auto* color = [NSColor labelColor];
|
|
static constexpr CGFloat radians_per_segment = 2.0 * 3.14159265358979323846 / TAB_LOADING_SPINNER_SEGMENT_COUNT;
|
|
|
|
CGContextSaveGState(context);
|
|
CGContextTranslateCTM(context, TAB_ICON_SIZE / 2.0, TAB_ICON_SIZE / 2.0);
|
|
|
|
for (NSUInteger segment = 0; segment < TAB_LOADING_SPINNER_SEGMENT_COUNT; ++segment) {
|
|
auto alpha = static_cast<CGFloat>(((segment + frame % TAB_LOADING_SPINNER_SEGMENT_COUNT) % TAB_LOADING_SPINNER_SEGMENT_COUNT) + 1) / TAB_LOADING_SPINNER_SEGMENT_COUNT;
|
|
auto* segment_color = [color colorWithAlphaComponent:alpha];
|
|
|
|
CGContextSaveGState(context);
|
|
CGContextRotateCTM(context, static_cast<CGFloat>(segment) * radians_per_segment);
|
|
CGContextSetStrokeColorWithColor(context, segment_color.CGColor);
|
|
CGContextSetLineWidth(context, 2);
|
|
CGContextSetLineCap(context, kCGLineCapRound);
|
|
CGContextMoveToPoint(context, 0, -4);
|
|
CGContextAddLineToPoint(context, 0, -7);
|
|
CGContextStrokePath(context);
|
|
CGContextRestoreGState(context);
|
|
}
|
|
|
|
CGContextRestoreGState(context);
|
|
return YES;
|
|
}];
|
|
return image;
|
|
}
|
|
|
|
@interface Tab () <LadybirdWebViewObserver>
|
|
{
|
|
BOOL m_loading;
|
|
NSUInteger m_loading_spinner_frame;
|
|
__strong NSTimer* m_loading_spinner_timer;
|
|
OwnPtr<TabSettingsObserver> m_settings_observer;
|
|
}
|
|
|
|
@property (nonatomic, strong) NSString* title;
|
|
@property (nonatomic, strong) NSImage* favicon;
|
|
|
|
@property (nonatomic, strong) NSTitlebarAccessoryViewController* bookmarks_bar_controller;
|
|
@property (nonatomic, strong) SearchPanel* search_panel;
|
|
|
|
@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];
|
|
|
|
m_settings_observer = make<TabSettingsObserver>(self);
|
|
|
|
auto* bookmarks_bar = [[BookmarksBar alloc] init];
|
|
self.bookmarks_bar_controller = [[NSTitlebarAccessoryViewController alloc] init];
|
|
[self.bookmarks_bar_controller setView:bookmarks_bar];
|
|
[self.bookmarks_bar_controller setLayoutAttribute:NSLayoutAttributeBottom];
|
|
[self updateBookmarksBarDisplay:WebView::Application::settings().show_bookmarks_bar()];
|
|
[self addTitlebarAccessoryViewController:self.bookmarks_bar_controller];
|
|
|
|
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;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[m_loading_spinner_timer invalidate];
|
|
}
|
|
|
|
#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];
|
|
}
|
|
|
|
#pragma mark - Private methods
|
|
|
|
- (TabController*)tabController
|
|
{
|
|
return (TabController*)[self windowController];
|
|
}
|
|
|
|
- (NSImage*)tabIcon
|
|
{
|
|
if (m_loading)
|
|
return tab_loading_spinner_icon(m_loading_spinner_frame);
|
|
return self.favicon;
|
|
}
|
|
|
|
- (NSString*)displayTitle
|
|
{
|
|
if (!WebView::Application::settings().config_variable_as_bool(WebView::ConfigVariableID::ShowWebContentProcessIDInTabTitle))
|
|
return self.title;
|
|
|
|
auto title = MUST(String::formatted("{} [{}]", Ladybird::ns_string_to_string(self.title), [[self web_view] view].client().pid()));
|
|
return Ladybird::string_to_ns_string(title);
|
|
}
|
|
|
|
- (void)updateLoadingSpinner
|
|
{
|
|
if (!m_loading)
|
|
return;
|
|
|
|
m_loading_spinner_frame = (m_loading_spinner_frame + 1) % TAB_LOADING_SPINNER_SEGMENT_COUNT;
|
|
[self updateTabTitleAndFavicon];
|
|
}
|
|
|
|
- (void)setTabLoading:(BOOL)loading
|
|
{
|
|
if (m_loading == loading)
|
|
return;
|
|
|
|
m_loading = loading;
|
|
m_loading_spinner_frame = 0;
|
|
|
|
if (m_loading) {
|
|
__weak Tab* weak_self = self;
|
|
m_loading_spinner_timer = [NSTimer timerWithTimeInterval:0.08
|
|
repeats:YES
|
|
block:^(NSTimer*) {
|
|
Tab* strong_self = weak_self;
|
|
if (strong_self == nil)
|
|
return;
|
|
[strong_self updateLoadingSpinner];
|
|
}];
|
|
[[NSRunLoop mainRunLoop] addTimer:m_loading_spinner_timer forMode:NSRunLoopCommonModes];
|
|
} else {
|
|
[m_loading_spinner_timer invalidate];
|
|
m_loading_spinner_timer = nil;
|
|
}
|
|
|
|
[self updateTabTitleAndFavicon];
|
|
}
|
|
|
|
- (void)updateTabTitleAndFavicon
|
|
{
|
|
static constexpr CGFloat TITLE_FONT_SIZE = 12;
|
|
|
|
NSFont* title_font = [NSFont systemFontOfSize:TITLE_FONT_SIZE];
|
|
|
|
auto* favicon_attachment = [[NSTextAttachment alloc] init];
|
|
favicon_attachment.image = [self tabIcon];
|
|
|
|
// 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 - TAB_ICON_SIZE) / 2.f;
|
|
[favicon_attachment setBounds:CGRectMake(0, offset_y, TAB_ICON_SIZE, TAB_ICON_SIZE)];
|
|
|
|
auto* title_attributes = @{
|
|
NSForegroundColorAttributeName : [NSColor textColor],
|
|
NSFontAttributeName : title_font
|
|
};
|
|
|
|
auto* display_title = [self displayTitle];
|
|
auto* title_attribute = [[NSAttributedString alloc] initWithString:display_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];
|
|
if ([[self tab] respondsToSelector:@selector(setToolTip:)])
|
|
[(id)[self tab] setToolTip:display_title];
|
|
}
|
|
|
|
- (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&)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)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)is_redirect
|
|
{
|
|
self.title = Ladybird::string_to_ns_string(url.serialize());
|
|
self.favicon = [Tab defaultFavicon];
|
|
[self setTabLoading:YES];
|
|
[self updateTabTitleAndFavicon];
|
|
|
|
[[self tabController] onFaviconChange:nil];
|
|
[[self tabController] onLoadStart:url isRedirect:is_redirect];
|
|
}
|
|
|
|
- (void)onLoadFinish:(URL::URL const&)url
|
|
{
|
|
[self setTabLoading:NO];
|
|
[[self tabController] onLoadFinish:url];
|
|
}
|
|
|
|
- (void)onURLChange:(URL::URL const&)url
|
|
{
|
|
[[self tabController] onURLChange:url];
|
|
}
|
|
|
|
- (void)onTitleChange:(Utf16String const&)title
|
|
{
|
|
self.title = Ladybird::utf16_string_to_ns_string(title);
|
|
[self updateTabTitleAndFavicon];
|
|
}
|
|
|
|
- (void)onFaviconChange:(Gfx::Bitmap const&)bitmap
|
|
{
|
|
auto* favicon = Ladybird::gfx_bitmap_to_ns_image(bitmap);
|
|
[favicon setResizingMode:NSImageResizingModeStretch];
|
|
self.favicon = favicon;
|
|
[self updateTabTitleAndFavicon];
|
|
[[self tabController] onFaviconChange:favicon];
|
|
}
|
|
|
|
- (BookmarksBar*)bookmarksBar
|
|
{
|
|
return (BookmarksBar*)[self.bookmarks_bar_controller view];
|
|
}
|
|
|
|
- (void)rebuildBookmarksBar
|
|
{
|
|
[[self bookmarksBar] rebuild];
|
|
}
|
|
|
|
- (void)updateBookmarksBarDisplay:(bool)show_bookmarks_bar
|
|
{
|
|
[self.bookmarks_bar_controller setHidden:!show_bookmarks_bar];
|
|
}
|
|
|
|
- (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)onEnterFullscreenWindow
|
|
{
|
|
[[self tabController] onEnterFullscreenWindow];
|
|
}
|
|
|
|
- (void)onExitFullscreenWindow
|
|
{
|
|
[[self tabController] onExitFullscreenWindow];
|
|
}
|
|
|
|
- (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
|
|
|
|
void TabSettingsObserver::show_bookmarks_bar_changed()
|
|
{
|
|
[m_tab updateBookmarksBarDisplay:WebView::Application::settings().show_bookmarks_bar()];
|
|
}
|
|
|
|
void TabSettingsObserver::config_variable_changed(WebView::ConfigVariableID variable)
|
|
{
|
|
if (variable == WebView::ConfigVariableID::ShowWebContentProcessIDInTabTitle)
|
|
[m_tab updateTabTitleAndFavicon];
|
|
}
|