diff --git a/Ladybird/AppKit/Application/ApplicationDelegate.mm b/Ladybird/AppKit/Application/ApplicationDelegate.mm index d025e15e7c4..ccc5786b0e3 100644 --- a/Ladybird/AppKit/Application/ApplicationDelegate.mm +++ b/Ladybird/AppKit/Application/ApplicationDelegate.mm @@ -347,6 +347,9 @@ [submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Console" action:@selector(openConsole:) keyEquivalent:@"J"]]; + [submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Inspector" + action:@selector(openInspector:) + keyEquivalent:@"I"]]; [menu setSubmenu:submenu]; return menu; diff --git a/Ladybird/AppKit/UI/Inspector.h b/Ladybird/AppKit/UI/Inspector.h new file mode 100644 index 00000000000..5cca8b58620 --- /dev/null +++ b/Ladybird/AppKit/UI/Inspector.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#import + +@class LadybirdWebView; +@class Tab; + +@interface Inspector : NSWindow + +- (instancetype)init:(Tab*)tab; + +- (void)inspect; +- (void)reset; + +@end diff --git a/Ladybird/AppKit/UI/Inspector.mm b/Ladybird/AppKit/UI/Inspector.mm new file mode 100644 index 00000000000..caae1119c91 --- /dev/null +++ b/Ladybird/AppKit/UI/Inspector.mm @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +#import +#import +#import +#import +#import + +#if !__has_feature(objc_arc) +# error "This project requires ARC" +#endif + +static constexpr CGFloat const WINDOW_WIDTH = 600; +static constexpr CGFloat const WINDOW_HEIGHT = 800; + +@interface Inspector () + +@property (nonatomic, strong) Tab* tab; + +@property (nonatomic, strong) NSOutlineView* dom_tree_outline_view; +@property (nonatomic, strong) NSDictionary* dom_tree; + +@end + +@implementation Inspector + +@synthesize tab = _tab; + +- (instancetype)init:(Tab*)tab +{ + auto tab_rect = [tab frame]; + auto position_x = tab_rect.origin.x + (tab_rect.size.width - WINDOW_WIDTH) / 2; + auto position_y = tab_rect.origin.y + (tab_rect.size.height - 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.tab = tab; + + auto* split_view = [[NSSplitView alloc] initWithFrame:[self frame]]; + [split_view setDividerStyle:NSSplitViewDividerStylePaneSplitter]; + + auto* top_tab_view = [[NSTabView alloc] init]; + [split_view addSubview:top_tab_view]; + + [self initializeDOMTreeTab:top_tab_view]; + [self reset]; + + auto& web_view = [[self.tab web_view] view]; + __weak Inspector* weak_self = self; + + web_view.on_received_dom_tree = [weak_self](auto const& dom_tree) { + Inspector* strong_self = weak_self; + if (strong_self == nil) { + return; + } + + strong_self.dom_tree = Ladybird::deserialize_json_to_dictionary(dom_tree); + + if (strong_self.dom_tree) { + [strong_self.dom_tree_outline_view reloadItem:nil reloadChildren:YES]; + [strong_self.dom_tree_outline_view sizeToFit]; + } else { + strong_self.dom_tree = @{}; + } + }; + + [self setContentView:split_view]; + [self setTitle:@"Inspector"]; + [self setIsVisible:YES]; + + auto split_view_height = [split_view frame].size.height; + [split_view setPosition:(split_view_height * 0.6f) ofDividerAtIndex:0]; + } + + return self; +} + +- (void)dealloc +{ + auto& web_view = [[self.tab web_view] view]; + web_view.on_received_dom_tree = nullptr; +} + +#pragma mark - Public methods + +- (void)inspect +{ + auto& web_view = [[self.tab web_view] view]; + web_view.inspect_dom_tree(); +} + +- (void)reset +{ + self.dom_tree = @{}; + [self.dom_tree_outline_view reloadItem:nil reloadChildren:YES]; + [self.dom_tree_outline_view sizeToFit]; +} + +#pragma mark - Private methods + +- (void)initializeDOMTreeTab:(NSTabView*)tab_view +{ + auto* tab = [[NSTabViewItem alloc] initWithIdentifier:@"DOM Tree"]; + [tab setLabel:@"DOM"]; + + auto* scroll_view = [[NSScrollView alloc] init]; + [scroll_view setHasVerticalScroller:YES]; + [scroll_view setHasHorizontalScroller:YES]; + [scroll_view setLineScroll:24]; + [tab setView:scroll_view]; + + self.dom_tree_outline_view = [[NSOutlineView alloc] initWithFrame:[tab_view frame]]; + [self.dom_tree_outline_view setDoubleAction:@selector(onTreeDoubleClick:)]; + [self.dom_tree_outline_view setDataSource:self]; + [self.dom_tree_outline_view setDelegate:self]; + [self.dom_tree_outline_view setHeaderView:nil]; + [scroll_view setDocumentView:self.dom_tree_outline_view]; + + auto* column = [[NSTableColumn alloc] initWithIdentifier:@"DOM Tree"]; + [self.dom_tree_outline_view addTableColumn:column]; + + [tab_view addTabViewItem:tab]; +} + +- (void)onTreeDoubleClick:(id)sender +{ + NSOutlineView* outline_view = sender; + id item = [outline_view itemAtRow:[outline_view clickedRow]]; + + if ([outline_view isItemExpanded:item]) { + [outline_view collapseItem:item]; + } else { + [outline_view expandItem:item]; + } +} + +#pragma mark - NSOutlineViewDataSource + +- (id)outlineView:(NSOutlineView*)view + child:(NSInteger)index + ofItem:(id)item +{ + if (item == nil) { + item = self.dom_tree; + } + + NSArray* children = [item objectForKey:@"children"]; + return [children objectAtIndex:index]; +} + +- (NSInteger)outlineView:(NSOutlineView*)view + numberOfChildrenOfItem:(id)item +{ + if (item == nil) { + item = self.dom_tree; + } + + NSArray* children = [item objectForKey:@"children"]; + return static_cast(children.count); +} + +- (BOOL)outlineView:(NSOutlineView*)view + isItemExpandable:(id)item +{ + NSArray* children = [item objectForKey:@"children"]; + return children.count != 0; +} + +- (NSView*)outlineView:(NSOutlineView*)outline_view + viewForTableColumn:(NSTableColumn*)table_column + item:(id)item +{ + auto* font = [NSFont monospacedSystemFontOfSize:12.0 weight:NSFontWeightRegular]; + auto* bold_font = [NSFont monospacedSystemFontOfSize:12.0 weight:NSFontWeightBold]; + + auto attributed_text = [&](NSString* text, NSColor* color = nil, BOOL bold = false) { + auto* attributes = [[NSMutableDictionary alloc] initWithDictionary:@{ + NSFontAttributeName : bold ? bold_font : font, + }]; + + if (color != nil) { + [attributes setObject:color forKey:NSForegroundColorAttributeName]; + } + + return [[NSMutableAttributedString alloc] initWithString:text attributes:attributes]; + }; + + NSString* type = [item objectForKey:@"type"]; + NSMutableAttributedString* text = nil; + + if ([type isEqualToString:@"text"]) { + text = attributed_text([[item objectForKey:@"text"] stringByCollapsingConsecutiveWhitespace]); + } else if ([type isEqualToString:@"comment"]) { + auto* comment = [NSString stringWithFormat:@"", [item objectForKey:@"data"]]; + text = attributed_text(comment, [NSColor systemGreenColor]); + } else if ([type isEqualToString:@"shadow-root"]) { + auto* shadow = [NSString stringWithFormat:@"%@ (%@)", [item objectForKey:@"name"], [item objectForKey:@"mode"]]; + text = attributed_text(shadow, [NSColor systemGrayColor]); + } else if ([type isEqualToString:@"element"]) { + text = attributed_text(@"<"); + + auto* element = attributed_text( + [[item objectForKey:@"name"] lowercaseString], + [NSColor systemPinkColor], + YES); + [text appendAttributedString:element]; + + NSDictionary* attributes = [item objectForKey:@"attributes"]; + + [attributes enumerateKeysAndObjectsUsingBlock:^(id name, id value, BOOL*) { + [text appendAttributedString:attributed_text(@" ")]; + + name = attributed_text(name, [NSColor systemOrangeColor]); + [text appendAttributedString:name]; + + [text appendAttributedString:attributed_text(@"=")]; + + value = [NSString stringWithFormat:@"\"%@\"", [value stringByCollapsingConsecutiveWhitespace]]; + value = attributed_text(value, [NSColor systemCyanColor]); + [text appendAttributedString:value]; + }]; + + [text appendAttributedString:attributed_text(@">")]; + } else { + text = attributed_text([item objectForKey:@"name"], [NSColor systemGrayColor]); + } + + auto* view = [NSTextField labelWithAttributedString:text]; + view.identifier = [NSString stringWithFormat:@"%@", [item objectForKey:@"id"]]; + + return view; +} + +#pragma mark - NSOutlineViewDelegate + +- (BOOL)outlineView:(NSOutlineView*)outline_view + shouldEditTableColumn:(NSTableColumn*)table_column + item:(id)item +{ + return NO; +} + +@end diff --git a/Ladybird/AppKit/UI/InspectorController.h b/Ladybird/AppKit/UI/InspectorController.h new file mode 100644 index 00000000000..099e05974de --- /dev/null +++ b/Ladybird/AppKit/UI/InspectorController.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#import + +@class Tab; + +@interface InspectorController : NSWindowController + +- (instancetype)init:(Tab*)tab; + +@end diff --git a/Ladybird/AppKit/UI/InspectorController.mm b/Ladybird/AppKit/UI/InspectorController.mm new file mode 100644 index 00000000000..9520c7ab63e --- /dev/null +++ b/Ladybird/AppKit/UI/InspectorController.mm @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#import +#import +#import +#import + +#if !__has_feature(objc_arc) +# error "This project requires ARC" +#endif + +@interface InspectorController () + +@property (nonatomic, strong) Tab* tab; + +@end + +@implementation InspectorController + +- (instancetype)init:(Tab*)tab +{ + if (self = [super init]) { + self.tab = tab; + } + + return self; +} + +#pragma mark - Private methods + +- (Inspector*)inspector +{ + return (Inspector*)[self window]; +} + +#pragma mark - NSWindowController + +- (IBAction)showWindow:(id)sender +{ + self.window = [[Inspector alloc] init:self.tab]; + [self.window setDelegate:self]; + [self.window makeKeyAndOrderFront:sender]; + + [[self inspector] inspect]; +} + +#pragma mark - NSWindowDelegate + +- (void)windowWillClose:(NSNotification*)notification +{ + [self.tab onInspectorClosed]; +} + +@end diff --git a/Ladybird/AppKit/UI/LadybirdWebView.h b/Ladybird/AppKit/UI/LadybirdWebView.h index da7661be44a..f839ee70126 100644 --- a/Ladybird/AppKit/UI/LadybirdWebView.h +++ b/Ladybird/AppKit/UI/LadybirdWebView.h @@ -25,6 +25,7 @@ - (void)loadURL:(URL const&)url; - (void)onLoadStart:(URL const&)url isRedirect:(BOOL)is_redirect; +- (void)onLoadFinish:(URL const&)url; - (void)onTitleChange:(DeprecatedString const&)title; - (void)onFaviconChange:(Gfx::Bitmap const&)bitmap; diff --git a/Ladybird/AppKit/UI/LadybirdWebView.mm b/Ladybird/AppKit/UI/LadybirdWebView.mm index 303490c531c..bba79d03e46 100644 --- a/Ladybird/AppKit/UI/LadybirdWebView.mm +++ b/Ladybird/AppKit/UI/LadybirdWebView.mm @@ -227,6 +227,10 @@ struct HideCursor { } }; + m_web_view_bridge->on_load_finish = [self](auto const& url) { + [self.observer onLoadFinish:url]; + }; + m_web_view_bridge->on_title_change = [self](auto const& title) { [self.observer onTitleChange:title]; }; diff --git a/Ladybird/AppKit/UI/Tab.h b/Ladybird/AppKit/UI/Tab.h index d60e85892b8..559a5b84cd2 100644 --- a/Ladybird/AppKit/UI/Tab.h +++ b/Ladybird/AppKit/UI/Tab.h @@ -17,6 +17,9 @@ - (void)openConsole:(id)sender; - (void)onConsoleClosed; +- (void)openInspector:(id)sender; +- (void)onInspectorClosed; + @property (nonatomic, strong) LadybirdWebView* web_view; @end diff --git a/Ladybird/AppKit/UI/Tab.mm b/Ladybird/AppKit/UI/Tab.mm index 53b9abb4b0d..61fabf617cf 100644 --- a/Ladybird/AppKit/UI/Tab.mm +++ b/Ladybird/AppKit/UI/Tab.mm @@ -14,6 +14,8 @@ #import #import #import +#import +#import #import #import #import @@ -32,6 +34,7 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800; @property (nonatomic, strong) NSImage* favicon; @property (nonatomic, strong) ConsoleController* console_controller; +@property (nonatomic, strong) InspectorController* inspector_controller; @end @@ -109,6 +112,9 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800; if (self.console_controller != nil) { [self.console_controller.window close]; } + if (self.inspector_controller != nil) { + [self.inspector_controller.window close]; + } } - (void)openConsole:(id)sender @@ -127,6 +133,22 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800; self.console_controller = nil; } +- (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; +} + #pragma mark - Private methods - (TabController*)tabController @@ -222,6 +244,18 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800; auto* console = (Console*)[self.console_controller window]; [console reset]; } + if (self.inspector_controller != nil) { + auto* inspector = (Inspector*)[self.inspector_controller window]; + [inspector reset]; + } +} + +- (void)onLoadFinish:(URL const&)url +{ + if (self.inspector_controller != nil) { + auto* inspector = (Inspector*)[self.inspector_controller window]; + [inspector inspect]; + } } - (void)onTitleChange:(DeprecatedString const&)title diff --git a/Ladybird/CMakeLists.txt b/Ladybird/CMakeLists.txt index f2b781eb421..71b4a26e4e5 100644 --- a/Ladybird/CMakeLists.txt +++ b/Ladybird/CMakeLists.txt @@ -136,6 +136,8 @@ elseif (APPLE) AppKit/UI/Console.mm AppKit/UI/ConsoleController.mm AppKit/UI/Event.mm + AppKit/UI/Inspector.mm + AppKit/UI/InspectorController.mm AppKit/UI/LadybirdWebView.mm AppKit/UI/LadybirdWebViewBridge.cpp AppKit/UI/Palette.mm