@@ -108,24 +108,22 @@ public record Entry {
108108 /// Callback for when this entry is selected.
109109 public required Action OnUse ;
110110 /// Whether the entry can be selected.
111- public bool Disabled = false ;
111+ public bool Disabled ;
112112
113113 /// Index used to determine "usage frequency" of entry
114114 public int FrequentlyUsedIndex = - 1 ;
115115
116116 /// Unique identifier for the category of the entry
117- private string ? storageKey ;
118117 public string ? StorageKey {
119- get => storageKey ;
120- set => storageKey = value ? . Replace ( '.' , '#' ) ;
118+ get ;
119+ init => field = value ? . Replace ( '.' , '#' ) ;
121120 }
122121
123122 /// Unique identifier inside the current category
124- private string ? storageName ;
125123 [ AllowNull ]
126124 public string StorageName {
127- get => storageName ?? DisplayText . Replace ( '.' , '#' ) ;
128- set => storageName = value ? . Replace ( '.' , '#' ) ;
125+ get => field ?? DisplayText . Replace ( '.' , '#' ) ;
126+ init => field = value ? . Replace ( '.' , '#' ) ;
129127 }
130128
131129 /// Active data slot, used for storing persistant data
@@ -176,8 +174,7 @@ public ContentDrawable(PopupMenu menu) {
176174 public override void Draw ( SKSurface surface ) {
177175 var canvas = surface . Canvas ;
178176
179- var visibleElements = menu . VisibleEntries . ToArray ( ) ;
180- if ( visibleElements . Length == 0 ) {
177+ if ( menu . VisibleEntries . Length == 0 ) {
181178 return ;
182179 }
183180
@@ -186,18 +183,17 @@ public override void Draw(SKSurface surface) {
186183 canvas . DrawRect ( backgroundRect , Settings . Instance . Theme . PopupMenuBgPaint ) ;
187184
188185 var font = FontManager . SKPopupFont ;
189- int maxDisplayLen = visibleElements . Select ( entry => entry . DisplayText . Length ) . Aggregate ( Math . Max ) ;
186+ int maxDisplayLen = menu . VisibleEntries
187+ . Select ( pair => pair . Entry . DisplayText . Length )
188+ . Aggregate ( Math . Max ) ;
190189
191190 float width = menu . ContentWidth - Settings . Instance . Theme . PopupMenuBorderPadding * 2.0f ;
192191 float height = menu . EntryHeight ;
193192 int iconWidth = menu . IconWidth ;
194193
195- const int rowCullOverhead = 3 ;
196- int minRow = Math . Max ( 0 , ( int ) ( menu . ScrollPosition . Y / height ) - rowCullOverhead ) ;
197- int maxRow = Math . Min ( menu . shownEntries . Length - 1 , ( int ) ( ( menu . ScrollPosition . Y + menu . ClientSize . Height ) / height ) + rowCullOverhead ) ;
198-
199- for ( int row = minRow ; row <= maxRow ; row ++ ) {
200- var entry = menu . shownEntries [ row ] ;
194+ for ( int idx = 0 ; idx < menu . VisibleEntries . Length ; idx ++ ) {
195+ int row = menu . VisibleEntriesMinRow + idx ;
196+ var ( entry , indices ) = menu . VisibleEntries [ idx ] ;
201197
202198 const float normalIconScale = 0.75f ;
203199 const float hoverIconScale = 0.9f ;
@@ -276,13 +272,52 @@ public override void Draw(SKSurface surface) {
276272 Settings . Instance . Theme . PopupMenuSelectedPaint ) ;
277273 }
278274
279- canvas . DrawText ( entry . DisplayText ,
280- x : Settings . Instance . Theme . PopupMenuBorderPadding + Settings . Instance . Theme . PopupMenuEntryHorizontalPadding + menu . IconWidth ,
281- y : Settings . Instance . Theme . PopupMenuBorderPadding + row * height + Settings . Instance . Theme . PopupMenuEntryVerticalPadding + Settings . Instance . Theme . PopupMenuEntrySpacing / 2.0f + font . Offset ( ) ,
282- font , entry . Disabled ? Settings . Instance . Theme . PopupMenuFgDisabledPaint : Settings . Instance . Theme . PopupMenuFgPaint ) ;
275+ float textX = Settings . Instance . Theme . PopupMenuBorderPadding + Settings . Instance . Theme . PopupMenuEntryHorizontalPadding + menu . IconWidth ;
276+ float textY = Settings . Instance . Theme . PopupMenuBorderPadding + row * height + Settings . Instance . Theme . PopupMenuEntryVerticalPadding + Settings . Instance . Theme . PopupMenuEntrySpacing / 2.0f + font . Offset ( ) ;
277+
278+ // Highlight fuzzy-matched letter
279+ var boldFont = FontManager . SKPopupFontBold ;
280+ var boldMetrics = boldFont . Metrics ;
281+ float boldUnderlineOffset = boldMetrics . UnderlinePosition ?? boldFont . LineHeight ( ) / 10.0f ;
282+
283+ var strokePaint = new SKPaint ( ) ;
284+ strokePaint . Style = SKPaintStyle . Stroke ;
285+ strokePaint . Color = Settings . Instance . Theme . PopupMenuFgPaint . Color ;
286+ strokePaint . StrokeWidth = boldMetrics . UnderlineThickness ?? 1.0f ;
287+ strokePaint . StrokeCap = SKStrokeCap . Round ;
288+
289+ int highlightIdx = 0 ;
290+ for ( int currLetter = 0 ; currLetter < entry . DisplayText . Length ; ) {
291+ int nextLetter = highlightIdx < indices . Count ? indices [ highlightIdx ] : entry . DisplayText . Length ;
292+ highlightIdx ++ ;
293+
294+ var regularText = entry . DisplayText . AsSpan ( ) [ currLetter ..nextLetter ] ;
295+ canvas . DrawText ( regularText ,
296+ x : textX + currLetter * font . CharWidth ( ) ,
297+ y : textY ,
298+ font , entry . Disabled ? Settings . Instance . Theme . PopupMenuFgDisabledPaint : Settings . Instance . Theme . PopupMenuFgPaint ) ;
299+
300+ if ( nextLetter == entry . DisplayText . Length ) {
301+ break ;
302+ }
303+
304+ float boldX = textX + nextLetter * font . CharWidth ( ) ;
305+
306+ var boldText = entry . DisplayText . AsSpan ( ) [ nextLetter ..( nextLetter + 1 ) ] ;
307+ canvas . DrawText ( boldText ,
308+ x : boldX ,
309+ y : textY ,
310+ boldFont , entry . Disabled ? Settings . Instance . Theme . PopupMenuFgDisabledPaint : Settings . Instance . Theme . PopupMenuFgPaint ) ;
311+
312+ float underlineY = textY + boldUnderlineOffset ;
313+ canvas . DrawLine ( boldX , underlineY , boldX + font . CharWidth ( ) , underlineY , strokePaint ) ;
314+
315+ currLetter = nextLetter + 1 ;
316+ }
317+
283318 canvas . DrawText ( entry . ExtraText ,
284- x : Settings . Instance . Theme . PopupMenuBorderPadding + Settings . Instance . Theme . PopupMenuEntryHorizontalPadding + menu . IconWidth + font . CharWidth ( ) * ( maxDisplayLen + DisplayExtraPadding ) ,
285- y : Settings . Instance . Theme . PopupMenuBorderPadding + row * height + Settings . Instance . Theme . PopupMenuEntryVerticalPadding + Settings . Instance . Theme . PopupMenuEntrySpacing / 2.0f + font . Offset ( ) ,
319+ x : textX + ( maxDisplayLen + DisplayExtraPadding ) * font . CharWidth ( ) ,
320+ y : textY ,
286321 font , Settings . Instance . Theme . PopupMenuFgExtraPaint ) ;
287322 }
288323 }
@@ -460,14 +495,33 @@ public int RecommendedWidth {
460495 private Entry [ ] shownEntries = [ ] ;
461496 private readonly ContentDrawable drawable ;
462497
463- private int TopVisibleEntry => ( int ) MathF . Floor ( ScrollPosition . Y / ( float ) EntryHeight ) ;
464- private int BottomVisibleEntry => ( int ) MathF . Ceiling ( ( ScrollPosition . Y + ClientSize . Height ) / ( float ) EntryHeight ) ;
465- private IEnumerable < Entry > VisibleEntries {
498+ private ( ( Entry Entry , List < int > FuzzyIndices ) [ ] ? Entries , int MinRow , int MaxRow ) visibleEntryData ;
499+
500+ private int VisibleEntriesMinRow => Math . Max ( visibleEntryData . MinRow , 0 ) ;
501+ private int VisibleEntriesMaxRow => Math . Min ( visibleEntryData . MaxRow , shownEntries . Length - 1 ) ;
502+ private ( Entry Entry , List < int > FuzzyIndices ) [ ] VisibleEntries {
466503 get {
467- int top = TopVisibleEntry ;
468- int bottom = BottomVisibleEntry ;
504+ const int rowCullOverhead = 3 ;
505+ float height = EntryHeight ;
506+ int minRow = Math . Max ( 0 , ( int ) ( ScrollPosition . Y / height ) - rowCullOverhead ) ;
507+ int maxRow = Math . Min ( shownEntries . Length - 1 , ( int ) ( ( ScrollPosition . Y + ClientSize . Height ) / height ) + rowCullOverhead ) ;
508+
509+ if ( visibleEntryData . Entries == null || visibleEntryData . MinRow != minRow || visibleEntryData . MaxRow != maxRow ) {
510+ // Recalculate visible entries
511+ visibleEntryData . MinRow = minRow ;
512+ visibleEntryData . MaxRow = maxRow ;
513+ visibleEntryData . Entries = shownEntries
514+ . Skip ( minRow )
515+ . Take ( maxRow - minRow + 1 )
516+ . Select ( entry => {
517+ var indices = new List < int > ( ) ;
518+ matcher . GetIndices ( entry . SearchText . AsSpan ( ) , filter . AsSpan ( ) , indices ) ;
519+ return ( entry , indices ) ;
520+ } )
521+ . ToArray ( ) ;
522+ }
469523
470- return shownEntries . Skip ( top ) . Take ( Math . Min ( bottom - top + 1 , shownEntries . Length - top ) ) ;
524+ return visibleEntryData . Entries ;
471525 }
472526 }
473527
@@ -522,7 +576,7 @@ public void Recalc() {
522576 return 1 ;
523577 }
524578 if ( lhsFavourite && rhsFavourite ) {
525- return lhsScore - rhsScore ;
579+ return rhsScore - lhsScore ;
526580 }
527581
528582 bool lhsFrequent = lhs . Entry . FrequentlyUsedIndex is >= 0 and < FrequentlyUsedCategorySize ;
@@ -534,7 +588,7 @@ public void Recalc() {
534588 return 1 ;
535589 }
536590 if ( lhsFrequent && rhsFrequent ) {
537- return ( lhs . Entry . FrequentlyUsedIndex * 10 + lhsScore ) - ( rhs . Entry . FrequentlyUsedIndex * 10 + rhsScore ) ;
591+ return ( rhsScore - lhsScore ) + ( rhs . Entry . FrequentlyUsedIndex - lhs . Entry . FrequentlyUsedIndex ) * 5 ;
538592 }
539593
540594 bool lhsSuggestion = lhs . Entry . Suggestion ;
@@ -546,10 +600,10 @@ public void Recalc() {
546600 return 1 ;
547601 }
548602 if ( lhsSuggestion && rhsSuggestion ) {
549- return lhsScore - rhsScore ;
603+ return rhsScore - lhsScore ;
550604 }
551605
552- return lhsScore - rhsScore ;
606+ return rhsScore - lhsScore ;
553607 } ) )
554608 . Select ( pair => pair . Entry )
555609 . ToArray ( ) ;
@@ -564,6 +618,9 @@ public void Recalc() {
564618 } ] ;
565619 }
566620
621+ // Clear cached entries
622+ visibleEntryData . Entries = null ;
623+
567624 selectedEntry = Math . Clamp ( selectedEntry , 0 , shownEntries . Length - 1 ) ;
568625
569626 // Calculate content bounds. Calculate height first to account for scroll bar
0 commit comments