UI/AppKit: Show rich autocomplete suggestion rows

Render AutocompleteSuggestion rows with section headers, favicons,
titles, and secondary text in the child-window popup instead of just
plain strings.

Move the AppKit popup and inline completion onto the shared suggestion
model, and share the base64 PNG decoding helper with the application
menu icon loading path.
This commit is contained in:
Andreas Kling
2026-04-16 09:35:25 +02:00
committed by Andreas Kling
parent 4121b7c0d0
commit 1802a05bc2
Notes: github-actions[bot] 2026-04-16 19:03:01 +00:00
6 changed files with 950 additions and 64 deletions

View File

@@ -8,12 +8,17 @@
#include <AK/String.h>
#include <AK/Vector.h>
#include <LibWebView/Autocomplete.h>
#import <Cocoa/Cocoa.h>
static constexpr auto MAXIMUM_VISIBLE_AUTOCOMPLETE_SUGGESTIONS = 8uz;
@protocol AutocompleteObserver <NSObject>
- (void)onSelectedSuggestion:(String)suggestion;
- (void)onHighlightedSuggestion:(String)suggestion;
- (void)onAutocompleteDidClose;
@end
@@ -22,8 +27,11 @@
- (instancetype)init:(id<AutocompleteObserver>)observer
withToolbarItem:(NSToolbarItem*)toolbar_item;
- (void)showWithSuggestions:(Vector<String>)suggestions;
- (void)showWithSuggestions:(Vector<WebView::AutocompleteSuggestion>)suggestions
selectedRow:(NSInteger)selected_row;
- (void)clearSelection;
- (BOOL)close;
- (BOOL)isVisible;
- (Optional<String>)selectedSuggestion;

View File

@@ -8,9 +8,229 @@
#import <Utilities/Conversions.h>
static NSString* const AUTOCOMPLETE_IDENTIFIER = @"Autocomplete";
static constexpr auto MAX_NUMBER_OF_ROWS = 8uz;
static constexpr CGFloat const POPOVER_PADDING = 6;
static NSString* const AUTOCOMPLETE_SECTION_HEADER_IDENTIFIER = @"AutocompleteSectionHeader";
static constexpr CGFloat const POPOVER_PADDING = 8;
static constexpr CGFloat const MINIMUM_WIDTH = 100;
static constexpr CGFloat const CELL_HORIZONTAL_PADDING = 8;
static constexpr CGFloat const CELL_VERTICAL_PADDING = 10;
static constexpr CGFloat const CELL_ICON_SIZE = 16;
static constexpr CGFloat const CELL_ICON_TEXT_SPACING = 6;
static constexpr CGFloat const CELL_LABEL_VERTICAL_SPACING = 5;
static constexpr CGFloat const SECTION_HEADER_HORIZONTAL_PADDING = 10;
static constexpr CGFloat const SECTION_HEADER_VERTICAL_PADDING = 4;
static NSFont* autocomplete_primary_font();
static NSFont* autocomplete_secondary_font();
static NSFont* autocomplete_section_header_font();
enum class AutocompleteRowKind {
SectionHeader,
Suggestion,
};
struct AutocompleteRowModel {
AutocompleteRowKind kind;
String text;
size_t suggestion_index { 0 };
};
static CGFloat autocomplete_text_field_height(NSFont* font)
{
static CGFloat primary_text_field_height = 0;
static CGFloat secondary_text_field_height = 0;
static CGFloat section_header_text_field_height = 0;
static dispatch_once_t token;
dispatch_once(&token, ^{
auto* text_field = [[NSTextField alloc] initWithFrame:NSZeroRect];
[text_field setBezeled:NO];
[text_field setDrawsBackground:NO];
[text_field setEditable:NO];
[text_field setStringValue:@"Ladybird"];
[text_field setFont:autocomplete_primary_font()];
primary_text_field_height = ceil([text_field fittingSize].height);
[text_field setFont:autocomplete_secondary_font()];
secondary_text_field_height = ceil([text_field fittingSize].height);
[text_field setFont:autocomplete_section_header_font()];
section_header_text_field_height = ceil([text_field fittingSize].height);
});
if (font == autocomplete_secondary_font())
return secondary_text_field_height;
if (font == autocomplete_section_header_font())
return section_header_text_field_height;
return primary_text_field_height;
}
static CGFloat autocomplete_row_height()
{
static CGFloat row_height = 0;
static dispatch_once_t token;
dispatch_once(&token, ^{
auto content_height = max(CELL_ICON_SIZE,
autocomplete_text_field_height(autocomplete_primary_font())
+ CELL_LABEL_VERTICAL_SPACING
+ autocomplete_text_field_height(autocomplete_secondary_font()));
row_height = ceil(content_height + (CELL_VERTICAL_PADDING * 2));
});
return row_height;
}
static CGFloat autocomplete_section_header_height()
{
static CGFloat row_height = 0;
static dispatch_once_t token;
dispatch_once(&token, ^{
row_height = ceil(autocomplete_text_field_height(autocomplete_section_header_font()) + (SECTION_HEADER_VERTICAL_PADDING * 2));
});
return row_height;
}
static NSFont* autocomplete_primary_font()
{
static NSFont* font;
static dispatch_once_t token;
dispatch_once(&token, ^{
font = [NSFont systemFontOfSize:[NSFont systemFontSize] weight:NSFontWeightSemibold];
});
return font;
}
static NSFont* autocomplete_secondary_font()
{
static NSFont* font;
static dispatch_once_t token;
dispatch_once(&token, ^{
font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
});
return font;
}
static NSFont* autocomplete_section_header_font()
{
static NSFont* font;
static dispatch_once_t token;
dispatch_once(&token, ^{
font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize] weight:NSFontWeightSemibold];
});
return font;
}
static NSImage* search_suggestion_icon()
{
static NSImage* image;
static dispatch_once_t token;
dispatch_once(&token, ^{
image = [NSImage imageWithSystemSymbolName:@"magnifyingglass" accessibilityDescription:@""];
[image setSize:NSMakeSize(CELL_ICON_SIZE, CELL_ICON_SIZE)];
});
return image;
}
static NSImage* literal_url_suggestion_icon()
{
static NSImage* image;
static dispatch_once_t token;
dispatch_once(&token, ^{
image = [NSImage imageWithSystemSymbolName:@"globe" accessibilityDescription:@""];
[image setSize:NSMakeSize(CELL_ICON_SIZE, CELL_ICON_SIZE)];
});
return image;
}
@protocol AutocompleteTableViewHoverObserver <NSObject>
- (void)autocompleteTableViewHoveredRowChanged:(NSInteger)row;
@end
@interface AutocompleteRowView : NSTableRowView
@end
@implementation AutocompleteRowView
- (void)drawSelectionInRect:(NSRect)dirtyRect
{
auto selection_rect = NSInsetRect(self.bounds, 2, 3);
auto* selection_path = [NSBezierPath bezierPathWithRoundedRect:selection_rect xRadius:6 yRadius:6];
[[[NSColor controlAccentColor] colorWithAlphaComponent:0.25] setFill];
[selection_path fill];
}
@end
@interface AutocompleteSuggestionView : NSTableCellView
@property (nonatomic, strong) NSImageView* icon_view;
@property (nonatomic, strong) NSTextField* title_text_field;
@property (nonatomic, strong) NSTextField* url_text_field;
@end
@implementation AutocompleteSuggestionView
@end
@interface AutocompleteSectionHeaderView : NSTableCellView
@property (nonatomic, strong) NSTextField* text_field;
@end
@implementation AutocompleteSectionHeaderView
@end
@interface AutocompleteScrollView : NSScrollView
@end
@implementation AutocompleteScrollView
- (void)scrollWheel:(NSEvent*)event
{
}
@end
@interface AutocompleteTableView : NSTableView
@property (nonatomic, weak) id<AutocompleteTableViewHoverObserver> hover_observer;
@property (nonatomic, strong) NSTrackingArea* tracking_area;
@end
@implementation AutocompleteTableView
- (void)updateTrackingAreas
{
if (self.tracking_area != nil)
[self removeTrackingArea:self.tracking_area];
self.tracking_area = [[NSTrackingArea alloc] initWithRect:NSZeroRect
options:NSTrackingActiveAlways | NSTrackingInVisibleRect | NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited
owner:self
userInfo:nil];
[self addTrackingArea:self.tracking_area];
[super updateTrackingAreas];
}
- (void)mouseMoved:(NSEvent*)event
{
[super mouseMoved:event];
auto point = [self convertPoint:event.locationInWindow fromView:nil];
[self.hover_observer autocompleteTableViewHoveredRowChanged:[self rowAtPoint:point]];
}
- (void)mouseExited:(NSEvent*)event
{
[super mouseExited:event];
[self.hover_observer autocompleteTableViewHoveredRowChanged:-1];
}
@end
@interface AutocompleteWindow : NSWindow
@end
@@ -29,9 +249,10 @@ static constexpr CGFloat const MINIMUM_WIDTH = 100;
@end
@interface Autocomplete () <NSTableViewDataSource, NSTableViewDelegate>
@interface Autocomplete () <AutocompleteTableViewHoverObserver, NSTableViewDataSource, NSTableViewDelegate>
{
Vector<String> m_suggestions;
Vector<WebView::AutocompleteSuggestion> m_suggestions;
Vector<AutocompleteRowModel> m_rows;
}
@property (nonatomic, weak) id<AutocompleteObserver> observer;
@@ -41,6 +262,15 @@ static constexpr CGFloat const MINIMUM_WIDTH = 100;
@property (nonatomic, strong) NSView* content_view;
@property (nonatomic, strong) NSScrollView* scroll_view;
@property (nonatomic, strong) NSTableView* table_view;
@property (nonatomic, strong) NSMutableDictionary<NSString*, NSImage*>* suggestion_icons;
- (NSInteger)tableRowForSuggestionIndex:(NSInteger)suggestion_index;
- (BOOL)isSelectableRow:(NSInteger)row;
- (CGFloat)heightOfRowAtIndex:(size_t)row;
- (CGFloat)tableHeightForVisibleSuggestionCount:(size_t)visible_suggestion_count;
- (void)rebuildRows;
- (void)selectRow:(NSInteger)row notifyObserver:(BOOL)notify_observer;
- (NSInteger)stepToSelectableRowFrom:(NSInteger)row direction:(NSInteger)direction;
@end
@@ -56,22 +286,30 @@ static constexpr CGFloat const MINIMUM_WIDTH = 100;
auto* column = [[NSTableColumn alloc] init];
[column setEditable:NO];
self.table_view = [[NSTableView alloc] initWithFrame:NSZeroRect];
self.table_view = [[AutocompleteTableView alloc] initWithFrame:NSZeroRect];
[self.table_view setAction:@selector(selectSuggestion:)];
[self.table_view setBackgroundColor:[NSColor clearColor]];
[self.table_view setHeaderView:nil];
[self.table_view setIntercellSpacing:NSMakeSize(0, 5)];
[self.table_view setIntercellSpacing:NSMakeSize(0, 0)];
[self.table_view setRefusesFirstResponder:YES];
[self.table_view setRowSizeStyle:NSTableViewRowSizeStyleDefault];
[self.table_view setStyle:NSTableViewStyleFullWidth];
[self.table_view setRowSizeStyle:NSTableViewRowSizeStyleCustom];
[self.table_view setRowHeight:autocomplete_row_height()];
[self.table_view setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleRegular];
[self.table_view addTableColumn:column];
[self.table_view setDataSource:self];
[self.table_view setDelegate:self];
[self.table_view setTarget:self];
[(AutocompleteTableView*)self.table_view setHover_observer:self];
self.scroll_view = [[NSScrollView alloc] initWithFrame:NSZeroRect];
self.scroll_view = [[AutocompleteScrollView alloc] initWithFrame:NSZeroRect];
[self.scroll_view setAutohidesScrollers:YES];
[self.scroll_view setBorderType:NSNoBorder];
[self.scroll_view setDrawsBackground:NO];
[self.scroll_view setHasVerticalScroller:YES];
[self.scroll_view setHasHorizontalScroller:NO];
[self.scroll_view setHasVerticalScroller:NO];
[self.scroll_view setHorizontalScrollElasticity:NSScrollElasticityNone];
[self.scroll_view setVerticalScrollElasticity:NSScrollElasticityNone];
[self.scroll_view setDocumentView:self.table_view];
self.content_view = [[NSView alloc] initWithFrame:NSZeroRect];
@@ -79,11 +317,13 @@ static constexpr CGFloat const MINIMUM_WIDTH = 100;
[self.content_view.layer setBackgroundColor:[NSColor windowBackgroundColor].CGColor];
[self.content_view.layer setCornerRadius:8];
[self.content_view addSubview:self.scroll_view];
self.suggestion_icons = [NSMutableDictionary dictionary];
self.popup_window = [[AutocompleteWindow alloc] initWithContentRect:NSZeroRect
styleMask:NSWindowStyleMaskBorderless
backing:NSBackingStoreBuffered
defer:NO];
[self.popup_window setAcceptsMouseMovedEvents:YES];
[self.popup_window setBackgroundColor:[NSColor clearColor]];
[self.popup_window setContentView:self.content_view];
[self.popup_window setHasShadow:YES];
@@ -97,15 +337,36 @@ static constexpr CGFloat const MINIMUM_WIDTH = 100;
#pragma mark - Public methods
- (void)showWithSuggestions:(Vector<String>)suggestions
- (void)showWithSuggestions:(Vector<WebView::AutocompleteSuggestion>)suggestions
selectedRow:(NSInteger)selected_row
{
m_suggestions = move(suggestions);
[self rebuildRows];
[self.suggestion_icons removeAllObjects];
for (auto const& suggestion : m_suggestions) {
if (suggestion.favicon_base64_png.has_value()) {
auto* suggestion_text = Ladybird::string_to_ns_string(suggestion.text);
if (auto* favicon = Ladybird::image_from_base64_png(*suggestion.favicon_base64_png, NSMakeSize(CELL_ICON_SIZE, CELL_ICON_SIZE)); favicon != nil)
[self.suggestion_icons setObject:favicon forKey:suggestion_text];
}
}
[self.table_view reloadData];
if (m_suggestions.is_empty())
if (m_rows.is_empty())
[self close];
else
[self show];
auto table_row = [self tableRowForSuggestionIndex:selected_row];
if (table_row == NSNotFound)
[self clearSelection];
else if (table_row != self.table_view.selectedRow) {
// Refreshing the default row should not behave like an explicit
// highlight, or the location field will re-preview the suggestion.
[self selectRow:table_row notifyObserver:NO];
}
}
- (BOOL)close
@@ -117,19 +378,30 @@ static constexpr CGFloat const MINIMUM_WIDTH = 100;
[parent_window removeChildWindow:self.popup_window];
[self.popup_window orderOut:nil];
[self.observer onAutocompleteDidClose];
return YES;
}
- (BOOL)isVisible
{
return self.popup_window.isVisible;
}
- (void)clearSelection
{
[self.table_view deselectAll:nil];
}
- (Optional<String>)selectedSuggestion
{
if (!self.popup_window.isVisible || self.table_view.numberOfRows == 0)
return {};
auto row = [self.table_view selectedRow];
if (row < 0)
if (![self isSelectableRow:row])
return {};
return m_suggestions[row];
return m_suggestions[m_rows[row].suggestion_index].text;
}
- (BOOL)selectNextSuggestion
@@ -139,11 +411,13 @@ static constexpr CGFloat const MINIMUM_WIDTH = 100;
if (!self.popup_window.isVisible) {
[self show];
[self selectRow:0];
if (auto row = [self stepToSelectableRowFrom:-1 direction:1]; row != NSNotFound)
[self selectRow:row notifyObserver:YES];
return YES;
}
[self selectRow:[self.table_view selectedRow] + 1];
if (auto row = [self stepToSelectableRowFrom:[self.table_view selectedRow] direction:1]; row != NSNotFound)
[self selectRow:row notifyObserver:YES];
return YES;
}
@@ -154,11 +428,13 @@ static constexpr CGFloat const MINIMUM_WIDTH = 100;
if (!self.popup_window.isVisible) {
[self show];
[self selectRow:self.table_view.numberOfRows - 1];
if (auto row = [self stepToSelectableRowFrom:0 direction:-1]; row != NSNotFound)
[self selectRow:row notifyObserver:YES];
return YES;
}
[self selectRow:[self.table_view selectedRow] - 1];
if (auto row = [self stepToSelectableRowFrom:[self.table_view selectedRow] direction:-1]; row != NSNotFound)
[self selectRow:row notifyObserver:YES];
return YES;
}
@@ -170,23 +446,137 @@ static constexpr CGFloat const MINIMUM_WIDTH = 100;
#pragma mark - Private methods
- (void)rebuildRows
{
m_rows.clear();
m_rows.ensure_capacity(m_suggestions.size() * 2);
auto current_section = WebView::AutocompleteSuggestionSection::None;
for (size_t suggestion_index = 0; suggestion_index < m_suggestions.size(); ++suggestion_index) {
auto const& suggestion = m_suggestions[suggestion_index];
if (suggestion.section != WebView::AutocompleteSuggestionSection::None && suggestion.section != current_section) {
current_section = suggestion.section;
m_rows.append({
.kind = AutocompleteRowKind::SectionHeader,
.text = MUST(String::from_utf8(WebView::autocomplete_section_title(current_section))),
});
}
m_rows.append({ .kind = AutocompleteRowKind::Suggestion, .suggestion_index = suggestion_index });
}
}
- (BOOL)isSelectableRow:(NSInteger)row
{
if (row < 0 || row >= static_cast<NSInteger>(m_rows.size()))
return NO;
return m_rows[row].kind == AutocompleteRowKind::Suggestion;
}
- (NSInteger)tableRowForSuggestionIndex:(NSInteger)suggestion_index
{
if (suggestion_index < 0)
return NSNotFound;
for (size_t row = 0; row < m_rows.size(); ++row) {
auto const& row_model = m_rows[row];
if (row_model.kind == AutocompleteRowKind::Suggestion
&& row_model.suggestion_index == static_cast<size_t>(suggestion_index))
return static_cast<NSInteger>(row);
}
return NSNotFound;
}
- (CGFloat)heightOfRowAtIndex:(size_t)row
{
VERIFY(row < m_rows.size());
return m_rows[row].kind == AutocompleteRowKind::SectionHeader
? autocomplete_section_header_height()
: autocomplete_row_height();
}
- (CGFloat)tableHeightForVisibleSuggestionCount:(size_t)visible_suggestion_count
{
if (visible_suggestion_count == 0)
return 0;
CGFloat total_height = 0;
size_t seen_suggestion_count = 0;
for (size_t row = 0; row < m_rows.size(); ++row) {
total_height += [self heightOfRowAtIndex:row];
if (m_rows[row].kind == AutocompleteRowKind::Suggestion) {
++seen_suggestion_count;
if (seen_suggestion_count >= visible_suggestion_count)
break;
}
}
return ceil(total_height);
}
- (NSInteger)stepToSelectableRowFrom:(NSInteger)row direction:(NSInteger)direction
{
if (self.table_view.numberOfRows == 0)
return NSNotFound;
auto candidate = row;
for (NSInteger attempt = 0; attempt < self.table_view.numberOfRows; ++attempt) {
candidate += direction;
if (candidate < 0)
candidate = self.table_view.numberOfRows - 1;
else if (candidate >= self.table_view.numberOfRows)
candidate = 0;
if ([self isSelectableRow:candidate])
return candidate;
}
return NSNotFound;
}
- (void)autocompleteTableViewHoveredRowChanged:(NSInteger)row
{
if (![self isSelectableRow:row])
return;
if (row == self.table_view.selectedRow)
return;
[self selectRow:row notifyObserver:YES];
}
- (void)show
{
auto* toolbar_view = self.toolbar_item.view;
auto* parent_window = [toolbar_view window];
if (parent_window == nil)
return;
auto was_visible = self.popup_window.isVisible;
auto visible_row_count = min(m_suggestions.size(), MAX_NUMBER_OF_ROWS);
auto table_height = (self.table_view.rowHeight + self.table_view.intercellSpacing.height) * visible_row_count;
size_t visible_suggestion_count = 0;
for (auto const& row_model : m_rows) {
if (row_model.kind == AutocompleteRowKind::Suggestion)
++visible_suggestion_count;
}
auto visible_table_height = [self tableHeightForVisibleSuggestionCount:visible_suggestion_count];
auto width = max<CGFloat>(toolbar_view.frame.size.width, MINIMUM_WIDTH);
auto content_size = NSMakeSize(width, table_height + (POPOVER_PADDING * 2));
auto content_size = NSMakeSize(width, visible_table_height + (POPOVER_PADDING * 2));
[self.content_view setFrame:NSMakeRect(0, 0, content_size.width, content_size.height)];
[self.scroll_view setFrame:NSInsetRect(self.content_view.bounds, 0, POPOVER_PADDING)];
[self.scroll_view setHasVerticalScroller:m_suggestions.size() > MAX_NUMBER_OF_ROWS];
[self.table_view deselectAll:nil];
[self.table_view scrollRowToVisible:0];
CGFloat document_width = self.scroll_view.contentSize.width;
[self.table_view setFrame:NSMakeRect(0, 0, document_width, visible_table_height)];
if (auto* column = self.table_view.tableColumns.firstObject)
[column setWidth:document_width];
if (!was_visible)
[self.table_view deselectAll:nil];
[self.table_view scrollRowToVisible:self.table_view.selectedRow >= 0 ? self.table_view.selectedRow : 0];
auto anchor_rect = [toolbar_view convertRect:toolbar_view.bounds toView:nil];
auto popup_rect = [parent_window convertRectToScreen:anchor_rect];
@@ -195,53 +585,172 @@ static constexpr CGFloat const MINIMUM_WIDTH = 100;
[self.popup_window setFrame:popup_rect display:NO];
if (!self.popup_window.isVisible)
if (!was_visible)
[parent_window addChildWindow:self.popup_window ordered:NSWindowAbove];
[self.popup_window orderFront:nil];
}
- (void)selectRow:(NSInteger)row
notifyObserver:(BOOL)notify_observer
{
if (row < 0)
row = self.table_view.numberOfRows - 1;
else if (row >= self.table_view.numberOfRows)
row = 0;
if (![self isSelectableRow:row])
return;
[self.table_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
[self.table_view scrollRowToVisible:[self.table_view selectedRow]];
if (notify_observer) {
if (auto suggestion = [self selectedSuggestion]; suggestion.has_value())
[self.observer onHighlightedSuggestion:suggestion.release_value()];
}
}
#pragma mark - NSTableViewDataSource
- (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView
{
return static_cast<NSInteger>(m_suggestions.size());
return static_cast<NSInteger>(m_rows.size());
}
#pragma mark - NSTableViewDelegate
- (CGFloat)tableView:(NSTableView*)tableView heightOfRow:(NSInteger)row
{
return [self heightOfRowAtIndex:static_cast<size_t>(row)];
}
- (NSTableRowView*)tableView:(NSTableView*)tableView
rowViewForRow:(NSInteger)row
{
return [[AutocompleteRowView alloc] initWithFrame:NSMakeRect(0, 0, NSWidth(tableView.bounds), [self tableView:tableView heightOfRow:row])];
}
- (NSView*)tableView:(NSTableView*)table_view
viewForTableColumn:(NSTableColumn*)table_column
row:(NSInteger)row
{
NSTableCellView* view = [table_view makeViewWithIdentifier:AUTOCOMPLETE_IDENTIFIER owner:self];
auto const& row_model = m_rows[row];
if (row_model.kind == AutocompleteRowKind::SectionHeader) {
AutocompleteSectionHeaderView* view = (AutocompleteSectionHeaderView*)[table_view makeViewWithIdentifier:AUTOCOMPLETE_SECTION_HEADER_IDENTIFIER owner:self];
if (view == nil) {
view = [[AutocompleteSectionHeaderView alloc] initWithFrame:NSZeroRect];
NSTextField* text_field = [[NSTextField alloc] initWithFrame:NSZeroRect];
[text_field setBezeled:NO];
[text_field setDrawsBackground:NO];
[text_field setEditable:NO];
[text_field setFont:autocomplete_section_header_font()];
[text_field setSelectable:NO];
[view addSubview:text_field];
[view setText_field:text_field];
[view setIdentifier:AUTOCOMPLETE_SECTION_HEADER_IDENTIFIER];
}
auto* header_text = Ladybird::string_to_ns_string(row_model.text);
auto header_height = autocomplete_text_field_height(autocomplete_section_header_font());
[view setFrame:NSMakeRect(0, 0, NSWidth(table_view.bounds), [self tableView:table_view heightOfRow:row])];
[view.text_field setStringValue:header_text];
[view.text_field setTextColor:[NSColor tertiaryLabelColor]];
[view.text_field setFrame:NSMakeRect(
SECTION_HEADER_HORIZONTAL_PADDING,
floor((NSHeight(view.bounds) - header_height) / 2.f),
NSWidth(view.bounds) - (SECTION_HEADER_HORIZONTAL_PADDING * 2),
header_height)];
return view;
}
AutocompleteSuggestionView* view = (AutocompleteSuggestionView*)[table_view makeViewWithIdentifier:AUTOCOMPLETE_IDENTIFIER owner:self];
if (view == nil) {
view = [[NSTableCellView alloc] initWithFrame:NSZeroRect];
view = [[AutocompleteSuggestionView alloc] initWithFrame:NSZeroRect];
NSTextField* text_field = [[NSTextField alloc] initWithFrame:NSZeroRect];
[text_field setBezeled:NO];
[text_field setDrawsBackground:NO];
[text_field setEditable:NO];
[text_field setSelectable:NO];
NSImageView* icon_view = [[NSImageView alloc] initWithFrame:NSZeroRect];
[icon_view setImageScaling:NSImageScaleProportionallyDown];
[view addSubview:icon_view];
[view setIcon_view:icon_view];
NSTextField* title_text_field = [[NSTextField alloc] initWithFrame:NSZeroRect];
[title_text_field setBezeled:NO];
[title_text_field setDrawsBackground:NO];
[title_text_field setEditable:NO];
[title_text_field setFont:autocomplete_primary_font()];
[title_text_field setLineBreakMode:NSLineBreakByTruncatingTail];
[title_text_field setSelectable:NO];
[view addSubview:title_text_field];
[view setTitle_text_field:title_text_field];
NSTextField* url_text_field = [[NSTextField alloc] initWithFrame:NSZeroRect];
[url_text_field setBezeled:NO];
[url_text_field setDrawsBackground:NO];
[url_text_field setEditable:NO];
[url_text_field setLineBreakMode:NSLineBreakByTruncatingTail];
[url_text_field setSelectable:NO];
[view addSubview:url_text_field];
[view setUrl_text_field:url_text_field];
[view addSubview:text_field];
[view setTextField:text_field];
[view setIdentifier:AUTOCOMPLETE_IDENTIFIER];
}
[view.textField setStringValue:Ladybird::string_to_ns_string(m_suggestions[row])];
auto const& suggestion = m_suggestions[row_model.suggestion_index];
auto* suggestion_text = Ladybird::string_to_ns_string(suggestion.text);
auto* title_text = suggestion.title.has_value() ? Ladybird::string_to_ns_string(*suggestion.title) : nil;
auto* favicon = [self.suggestion_icons objectForKey:suggestion_text];
auto* icon = suggestion.source == WebView::AutocompleteSuggestionSource::LiteralURL
? literal_url_suggestion_icon()
: suggestion.source == WebView::AutocompleteSuggestionSource::Search ? search_suggestion_icon()
: favicon;
[view setFrame:NSMakeRect(0, 0, NSWidth(table_view.bounds), [self tableView:table_view heightOfRow:row])];
auto primary_text_height = autocomplete_text_field_height(autocomplete_primary_font());
auto secondary_text_height = autocomplete_text_field_height(autocomplete_secondary_font());
CGFloat text_origin_x = CELL_HORIZONTAL_PADDING + CELL_ICON_SIZE + CELL_ICON_TEXT_SPACING;
CGFloat text_width = NSWidth(view.bounds) - text_origin_x - CELL_HORIZONTAL_PADDING;
[view.icon_view setFrame:NSMakeRect(
CELL_HORIZONTAL_PADDING,
floor((NSHeight(view.bounds) - CELL_ICON_SIZE) / 2.f),
CELL_ICON_SIZE,
CELL_ICON_SIZE)];
[view.icon_view setImage:icon];
[view.icon_view setContentTintColor:suggestion.source != WebView::AutocompleteSuggestionSource::History ? [NSColor secondaryLabelColor] : nil];
[view.icon_view setHidden:(icon == nil)];
if (title_text != nil) {
CGFloat text_block_height = primary_text_height + CELL_LABEL_VERTICAL_SPACING + secondary_text_height;
CGFloat text_block_origin_y = floor((NSHeight(view.bounds) - text_block_height) / 2.f);
[view.title_text_field setHidden:NO];
[view.title_text_field setStringValue:title_text];
[view.title_text_field setTextColor:[NSColor textColor]];
[view.title_text_field setFrame:NSMakeRect(
text_origin_x,
text_block_origin_y + secondary_text_height + CELL_LABEL_VERTICAL_SPACING,
text_width,
primary_text_height)];
[view.url_text_field setFont:autocomplete_secondary_font()];
[view.url_text_field setTextColor:[NSColor secondaryLabelColor]];
[view.url_text_field setFrame:NSMakeRect(
text_origin_x,
text_block_origin_y,
text_width,
secondary_text_height)];
} else {
[view.title_text_field setHidden:YES];
[view.url_text_field setFont:[NSFont systemFontOfSize:[NSFont systemFontSize]]];
[view.url_text_field setTextColor:[NSColor textColor]];
[view.url_text_field setFrame:NSMakeRect(
text_origin_x,
floor((NSHeight(view.bounds) - primary_text_height) / 2.f),
text_width,
primary_text_height)];
}
[view.url_text_field setStringValue:suggestion_text];
return view;
}

View File

@@ -4,8 +4,6 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Base64.h>
#import <Interface/Event.h>
#import <Interface/LadybirdWebView.h>
#import <Interface/Menu.h>
@@ -186,25 +184,10 @@ private:
__weak id m_control { nil };
};
static NSImage* image_from_base64_png(StringView favicon_base64_png)
static void initialize_native_icon(WebView::Action& action, id control)
{
static constexpr CGFloat const MENU_ICON_SIZE = 16;
auto decoded = decode_base64(favicon_base64_png);
if (decoded.is_error())
return nil;
auto* data = [NSData dataWithBytes:decoded.value().data()
length:decoded.value().size()];
auto* image = [[NSImage alloc] initWithData:data];
[image setSize:NSMakeSize(MENU_ICON_SIZE, MENU_ICON_SIZE)];
return image;
}
static void initialize_native_icon(WebView::Action& action, id control)
{
switch (action.id()) {
case WebView::ActionID::NavigateBack:
set_control_image(control, @"chevron.left");
@@ -248,7 +231,7 @@ static void initialize_native_icon(WebView::Action& action, id control)
break;
case WebView::ActionID::BookmarkItem:
if (auto icon = action.base64_png_icon(); icon.has_value())
[control setImage:image_from_base64_png(*icon)];
[control setImage:Ladybird::image_from_base64_png(*icon, NSMakeSize(MENU_ICON_SIZE, MENU_ICON_SIZE))];
else
set_control_image(control, @"globe");
break;

View File

@@ -32,6 +32,131 @@ static NSString* const TOOLBAR_BOOKMARK_IDENTIFIER = @"ToolbarBookmarkIdentifier
static NSString* const TOOLBAR_NEW_TAB_IDENTIFIER = @"ToolbarNewTabIdentifier";
static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIdentifier";
static NSString* candidate_by_trimming_root_trailing_slash(NSString* candidate);
static bool query_matches_candidate_exactly(NSString* query, NSString* candidate)
{
auto* trimmed_candidate = candidate_by_trimming_root_trailing_slash(candidate);
return [trimmed_candidate compare:query options:NSCaseInsensitiveSearch] == NSOrderedSame;
}
static NSString* inline_autocomplete_text_for_candidate(NSString* query, NSString* candidate)
{
if (query.length == 0 || candidate.length <= query.length)
return nil;
auto prefix_range = [candidate rangeOfString:query options:NSCaseInsensitiveSearch | NSAnchoredSearch];
if (prefix_range.location == NSNotFound)
return nil;
auto* suffix = [candidate substringFromIndex:query.length];
return [query stringByAppendingString:suffix];
}
static NSString* inline_autocomplete_text_for_suggestion(NSString* query, NSString* suggestion_text)
{
auto* trimmed_suggestion_text = candidate_by_trimming_root_trailing_slash(suggestion_text);
if (auto* direct_match = inline_autocomplete_text_for_candidate(query, trimmed_suggestion_text); direct_match != nil)
return direct_match;
if ([trimmed_suggestion_text hasPrefix:@"www."]) {
auto* stripped_www_suggestion = [trimmed_suggestion_text substringFromIndex:4];
if (auto* stripped_www_match = inline_autocomplete_text_for_candidate(query, stripped_www_suggestion); stripped_www_match != nil)
return stripped_www_match;
}
for (NSString* scheme_prefix in @[ @"https://", @"http://" ]) {
if (![trimmed_suggestion_text hasPrefix:scheme_prefix])
continue;
auto* stripped_suggestion = [trimmed_suggestion_text substringFromIndex:scheme_prefix.length];
if (auto* stripped_match = inline_autocomplete_text_for_candidate(query, stripped_suggestion); stripped_match != nil)
return stripped_match;
if ([stripped_suggestion hasPrefix:@"www."]) {
auto* stripped_www_suggestion = [stripped_suggestion substringFromIndex:4];
if (auto* stripped_www_match = inline_autocomplete_text_for_candidate(query, stripped_www_suggestion); stripped_www_match != nil)
return stripped_www_match;
}
}
return nil;
}
static bool suggestion_matches_query_exactly(NSString* query, NSString* suggestion_text)
{
auto* trimmed_suggestion_text = candidate_by_trimming_root_trailing_slash(suggestion_text);
if (query_matches_candidate_exactly(query, trimmed_suggestion_text))
return true;
if ([trimmed_suggestion_text hasPrefix:@"www."]) {
auto* stripped_www_suggestion = [trimmed_suggestion_text substringFromIndex:4];
if (query_matches_candidate_exactly(query, stripped_www_suggestion))
return true;
}
for (NSString* scheme_prefix in @[ @"https://", @"http://" ]) {
if (![trimmed_suggestion_text hasPrefix:scheme_prefix])
continue;
auto* stripped_suggestion = [trimmed_suggestion_text substringFromIndex:scheme_prefix.length];
if (query_matches_candidate_exactly(query, stripped_suggestion))
return true;
if ([stripped_suggestion hasPrefix:@"www."]) {
auto* stripped_www_suggestion = [stripped_suggestion substringFromIndex:4];
if (query_matches_candidate_exactly(query, stripped_www_suggestion))
return true;
}
}
return false;
}
static NSString* candidate_by_trimming_root_trailing_slash(NSString* candidate)
{
if (![candidate hasSuffix:@"/"])
return candidate;
auto* host_and_path = candidate;
for (NSString* scheme_prefix in @[ @"https://", @"http://" ]) {
if ([host_and_path hasPrefix:scheme_prefix]) {
host_and_path = [host_and_path substringFromIndex:scheme_prefix.length];
break;
}
}
auto first_slash = [host_and_path rangeOfString:@"/"];
if (first_slash.location == NSNotFound || first_slash.location != host_and_path.length - 1)
return candidate;
return [candidate substringToIndex:candidate.length - 1];
}
static bool should_suppress_inline_autocomplete_for_selector(SEL selector)
{
return selector == @selector(deleteBackward:)
|| selector == @selector(deleteBackwardByDecomposingPreviousCharacter:)
|| selector == @selector(deleteForward:)
|| selector == @selector(deleteToBeginningOfLine:)
|| selector == @selector(deleteToEndOfLine:)
|| selector == @selector(deleteWordBackward:)
|| selector == @selector(deleteWordForward:);
}
static NSInteger autocomplete_suggestion_index(NSString* suggestion_text, Vector<WebView::AutocompleteSuggestion> const& suggestions)
{
for (size_t index = 0; index < suggestions.size(); ++index) {
auto* candidate_text = Ladybird::string_to_ns_string(suggestions[index].text);
if ([candidate_text isEqualToString:suggestion_text])
return static_cast<NSInteger>(index);
}
return NSNotFound;
}
@interface LocationSearchField : NSSearchField
- (BOOL)becomeFirstResponder;
@@ -65,6 +190,8 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
u64 m_page_index;
OwnPtr<WebView::Autocomplete> m_autocomplete;
bool m_is_applying_inline_autocomplete;
bool m_should_suppress_inline_autocomplete_on_next_change;
bool m_fullscreen_requested_for_web_content;
bool m_fullscreen_exit_was_ui_initiated;
@@ -88,9 +215,20 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
@property (nonatomic, strong) NSToolbarItem* tab_overview_toolbar_item;
@property (nonatomic, strong) Autocomplete* autocomplete;
@property (nonatomic, copy) NSString* current_inline_autocomplete_suggestion;
@property (nonatomic, copy) NSString* suppressed_inline_autocomplete_query;
@property (nonatomic, assign) NSLayoutConstraint* location_toolbar_item_width;
- (NSString*)currentLocationFieldQuery;
- (BOOL)applyInlineAutocompleteSuggestionText:(NSString*)suggestion_text
forQuery:(NSString*)query;
- (void)applyLocationFieldInlineAutocompleteText:(NSString*)inline_text
forQuery:(NSString*)query;
- (NSInteger)applyInlineAutocomplete:(Vector<WebView::AutocompleteSuggestion> const&)suggestions;
- (void)previewHighlightedSuggestionInLocationField:(String const&)suggestion;
- (void)restoreLocationFieldQuery;
@end
@implementation TabController
@@ -122,6 +260,8 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
[self.toolbar setSizeMode:NSToolbarSizeModeRegular];
m_page_index = 0;
m_is_applying_inline_autocomplete = false;
m_should_suppress_inline_autocomplete_on_next_change = false;
m_fullscreen_requested_for_web_content = false;
m_fullscreen_exit_was_ui_initiated = true;
m_fullscreen_should_restore_tab_bar = false;
@@ -129,13 +269,28 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
self.autocomplete = [[Autocomplete alloc] init:self withToolbarItem:self.location_toolbar_item];
m_autocomplete = make<WebView::Autocomplete>();
m_autocomplete->on_autocomplete_query_complete = [weak_self](auto suggestions) {
m_autocomplete->on_autocomplete_query_complete = [weak_self](auto suggestions, WebView::AutocompleteResultKind result_kind) {
TabController* self = weak_self;
if (self == nil) {
return;
}
[self.autocomplete showWithSuggestions:move(suggestions)];
auto selected_row = [self applyInlineAutocomplete:suggestions];
if (result_kind == WebView::AutocompleteResultKind::Intermediate && [self.autocomplete isVisible]) {
if (auto selected_suggestion = [self.autocomplete selectedSuggestion];
selected_suggestion.has_value()) {
for (auto const& suggestion : suggestions) {
if (suggestion.text == *selected_suggestion)
return;
}
}
[self.autocomplete clearSelection];
return;
}
[self.autocomplete showWithSuggestions:move(suggestions)
selectedRow:selected_row];
};
}
@@ -260,12 +415,194 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
[location_search_field setAttributedStringValue:attributed_url];
}
- (NSString*)currentLocationFieldQuery
{
auto* location_search_field = (LocationSearchField*)[self.location_toolbar_item view];
auto* editor = (NSTextView*)[location_search_field currentEditor];
// Inline autocomplete mutates the field contents in place, so callers
// need a detached copy of the typed prefix for asynchronous comparisons.
if (editor == nil || [self.window firstResponder] != editor)
return [[location_search_field stringValue] copy];
auto* text = [[editor textStorage] string];
auto selected_range = [editor selectedRange];
if (selected_range.location == NSNotFound)
return [text copy];
if (selected_range.length == 0)
return [text copy];
if (NSMaxRange(selected_range) != text.length)
return [text copy];
return [[text substringToIndex:selected_range.location] copy];
}
- (NSInteger)applyInlineAutocomplete:(Vector<WebView::AutocompleteSuggestion> const&)suggestions
{
if (m_is_applying_inline_autocomplete)
return NSNotFound;
auto* location_search_field = (LocationSearchField*)[self.location_toolbar_item view];
auto* editor = (NSTextView*)[location_search_field currentEditor];
if (editor == nil || [self.window firstResponder] != editor || [editor hasMarkedText])
return NSNotFound;
auto* current_text = [[editor textStorage] string];
auto selected_range = [editor selectedRange];
if (selected_range.location == NSNotFound)
return NSNotFound;
auto current_text_length = current_text.length;
NSString* query = nil;
if (selected_range.length == 0) {
if (selected_range.location != current_text_length)
return NSNotFound;
query = current_text;
} else {
if (NSMaxRange(selected_range) != current_text_length)
return NSNotFound;
query = [current_text substringToIndex:selected_range.location];
}
if (suggestions.is_empty())
return NSNotFound;
// Row 0 drives both the visible highlight and (if its text prefix-matches
// the query) the inline completion preview. The user-visible rule is
// "the top row is the default action"; see the Qt implementation in
// UI/Qt/LocationEdit.cpp for a longer discussion.
// A literal URL always wins: no preview, restore the typed text.
if (suggestions.first().source == WebView::AutocompleteSuggestionSource::LiteralURL) {
self.current_inline_autocomplete_suggestion = nil;
if (selected_range.length != 0 || ![current_text isEqualToString:query])
[self restoreLocationFieldQuery];
return 0;
}
// Backspace suppression: the user just deleted into this query, so don't
// re-apply an inline preview — but still honor the "highlight the top
// row" rule.
if (self.suppressed_inline_autocomplete_query != nil && [self.suppressed_inline_autocomplete_query isEqualToString:query]) {
self.current_inline_autocomplete_suggestion = nil;
if (selected_range.length != 0 || ![current_text isEqualToString:query])
[self restoreLocationFieldQuery];
return 0;
}
// Preserve an existing inline preview if its row is still present and
// still extends the typed prefix. This keeps the preview stable while the
// user is still forward-typing into a suggestion.
if (self.current_inline_autocomplete_suggestion != nil) {
auto preserved_row = autocomplete_suggestion_index(self.current_inline_autocomplete_suggestion, suggestions);
if (preserved_row != NSNotFound) {
if (auto* preserved_inline = inline_autocomplete_text_for_suggestion(query, self.current_inline_autocomplete_suggestion); preserved_inline != nil) {
[self applyLocationFieldInlineAutocompleteText:preserved_inline forQuery:query];
return preserved_row;
}
}
}
// Try to inline-preview row 0 specifically.
auto* row_0_text = Ladybird::string_to_ns_string(suggestions.first().text);
if (auto* row_0_inline = inline_autocomplete_text_for_suggestion(query, row_0_text); row_0_inline != nil) {
self.current_inline_autocomplete_suggestion = row_0_text;
[self applyLocationFieldInlineAutocompleteText:row_0_inline forQuery:query];
return 0;
}
// Row 0 does not prefix-match the query: clear any stale inline preview,
// restore the typed text, and still highlight row 0.
self.current_inline_autocomplete_suggestion = nil;
if (selected_range.length != 0 || ![current_text isEqualToString:query])
[self restoreLocationFieldQuery];
return 0;
}
- (BOOL)applyInlineAutocompleteSuggestionText:(NSString*)suggestion_text
forQuery:(NSString*)query
{
if (suggestion_matches_query_exactly(query, suggestion_text)) {
[self restoreLocationFieldQuery];
self.current_inline_autocomplete_suggestion = nil;
return YES;
}
auto* inline_text = inline_autocomplete_text_for_suggestion(query, suggestion_text);
if (inline_text == nil)
return NO;
self.current_inline_autocomplete_suggestion = suggestion_text;
[self applyLocationFieldInlineAutocompleteText:inline_text forQuery:query];
return YES;
}
- (void)applyLocationFieldInlineAutocompleteText:(NSString*)inline_text
forQuery:(NSString*)query
{
auto* location_search_field = (LocationSearchField*)[self.location_toolbar_item view];
auto* editor = (NSTextView*)[location_search_field currentEditor];
if (editor == nil || [self.window firstResponder] != editor || [editor hasMarkedText])
return;
auto* current_text = [[editor textStorage] string];
auto selected_range = [editor selectedRange];
auto completion_range = NSMakeRange(query.length, inline_text.length - query.length);
if ([current_text isEqualToString:inline_text] && NSEqualRanges(selected_range, completion_range))
return;
m_is_applying_inline_autocomplete = true;
[location_search_field setStringValue:inline_text];
[editor setString:inline_text];
[editor setSelectedRange:completion_range];
m_is_applying_inline_autocomplete = false;
}
- (void)previewHighlightedSuggestionInLocationField:(String const&)suggestion
{
auto* query = [self currentLocationFieldQuery];
auto* suggestion_text = Ladybird::string_to_ns_string(suggestion);
[self applyInlineAutocompleteSuggestionText:suggestion_text forQuery:query];
}
- (void)restoreLocationFieldQuery
{
auto* location_search_field = (LocationSearchField*)[self.location_toolbar_item view];
auto* editor = (NSTextView*)[location_search_field currentEditor];
if (editor == nil || [self.window firstResponder] != editor || [editor hasMarkedText])
return;
auto* query = [self currentLocationFieldQuery];
auto* current_text = [[editor textStorage] string];
auto selected_range = [editor selectedRange];
auto query_selection = NSMakeRange(query.length, 0);
if ([current_text isEqualToString:query] && NSEqualRanges(selected_range, query_selection))
return;
m_is_applying_inline_autocomplete = true;
[location_search_field setStringValue:query];
[editor setString:query];
[editor setSelectedRange:query_selection];
m_is_applying_inline_autocomplete = false;
}
- (BOOL)navigateToLocation:(String)location
{
m_autocomplete->cancel_pending_query();
if (auto url = WebView::sanitize_url(location, WebView::Application::settings().search_engine()); url.has_value()) {
[self loadURL:*url];
}
self.current_inline_autocomplete_suggestion = nil;
self.suppressed_inline_autocomplete_query = nil;
m_should_suppress_inline_autocomplete_on_next_change = false;
[self.window makeFirstResponder:nil];
[self.autocomplete close];
@@ -617,10 +954,15 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
textView:(NSTextView*)text_view
doCommandBySelector:(SEL)selector
{
if (should_suppress_inline_autocomplete_for_selector(selector))
m_should_suppress_inline_autocomplete_on_next_change = true;
if (selector == @selector(cancelOperation:)) {
if ([self.autocomplete close])
return YES;
auto const& url = [[[self tab] web_view] view].url();
self.suppressed_inline_autocomplete_query = nil;
m_should_suppress_inline_autocomplete_on_next_change = false;
[self setLocationFieldText:url.serialize()];
[self.window makeFirstResponder:nil];
return YES;
@@ -653,20 +995,49 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
auto* location_search_field = (LocationSearchField*)[self.location_toolbar_item view];
auto url_string = Ladybird::ns_string_to_string([location_search_field stringValue]);
m_autocomplete->cancel_pending_query();
self.current_inline_autocomplete_suggestion = nil;
self.suppressed_inline_autocomplete_query = nil;
m_should_suppress_inline_autocomplete_on_next_change = false;
[self.autocomplete close];
[self setLocationFieldText:url_string];
}
- (void)controlTextDidChange:(NSNotification*)notification
{
auto* location_search_field = (LocationSearchField*)[self.location_toolbar_item view];
if (m_is_applying_inline_autocomplete)
return;
auto url_string = Ladybird::ns_string_to_string([location_search_field stringValue]);
m_autocomplete->query_autocomplete_engine(move(url_string));
auto* query = [self currentLocationFieldQuery];
if (m_should_suppress_inline_autocomplete_on_next_change) {
self.suppressed_inline_autocomplete_query = query;
m_should_suppress_inline_autocomplete_on_next_change = false;
} else if (self.suppressed_inline_autocomplete_query != nil && ![self.suppressed_inline_autocomplete_query isEqualToString:query]) {
self.suppressed_inline_autocomplete_query = nil;
}
if (self.suppressed_inline_autocomplete_query == nil && self.current_inline_autocomplete_suggestion != nil) {
if (![self applyInlineAutocompleteSuggestionText:self.current_inline_autocomplete_suggestion forQuery:query])
self.current_inline_autocomplete_suggestion = nil;
}
auto url_string = Ladybird::ns_string_to_string(query);
m_autocomplete->query_autocomplete_engine(move(url_string), MAXIMUM_VISIBLE_AUTOCOMPLETE_SUGGESTIONS);
}
#pragma mark - AutocompleteObserver
- (void)onHighlightedSuggestion:(String)suggestion
{
[self previewHighlightedSuggestionInLocationField:suggestion];
}
- (void)onAutocompleteDidClose
{
self.current_inline_autocomplete_suggestion = nil;
[self restoreLocationFieldQuery];
}
- (void)onSelectedSuggestion:(String)suggestion
{
[self navigateToLocation:move(suggestion)];

View File

@@ -30,6 +30,7 @@ ByteString ns_string_to_byte_string(NSString*);
ByteString ns_data_to_string(NSData*);
NSData* string_to_ns_data(StringView);
NSImage* image_from_base64_png(StringView, NSSize size);
NSDictionary* deserialize_json_to_dictionary(StringView);

View File

@@ -5,6 +5,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Base64.h>
#include <LibGfx/ImageFormats/PNGWriter.h>
#import <Utilities/Conversions.h>
@@ -56,6 +57,19 @@ NSData* string_to_ns_data(StringView string)
return [NSData dataWithBytes:string.characters_without_null_termination() length:string.length()];
}
NSImage* image_from_base64_png(StringView string, NSSize size)
{
auto decoded = decode_base64(string);
if (decoded.is_error())
return nil;
auto* data = [NSData dataWithBytes:decoded.value().data()
length:decoded.value().size()];
auto* image = [[NSImage alloc] initWithData:data];
[image setSize:size];
return image;
}
NSDictionary* deserialize_json_to_dictionary(StringView json)
{
auto* ns_json = string_to_ns_string(json);