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,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;
}