1 /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2 
3 Redistribution and use in source and binary forms, with or without
4 modification, are permitted provided that the following conditions are met:
5 
6 1. Redistributions of source code must retain the above copyright notice, this
7    list of conditions and the following disclaimer.
8 2. Redistributions in binary form must reproduce the above copyright notice,
9    this list of conditions and the following disclaimer in the documentation
10    and/or other materials provided with the distribution.
11 
12 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 
23 #include "lookupwidget.h"
24 
25 #include "app.h"
26 #include "bookmarks.h"
27 #include "command.h"
28 #include "documentwidget.h"
29 #include "feeds.h"
30 #include "gmcerts.h"
31 #include "gmutil.h"
32 #include "history.h"
33 #include "inputwidget.h"
34 #include "listwidget.h"
35 #include "lang.h"
36 #include "lookup.h"
37 #include "util.h"
38 #include "visited.h"
39 
40 #if defined (iPlatformAppleMobile)
41 #   include "../ios.h"
42 #endif
43 
44 #include <the_Foundation/mutex.h>
45 #include <the_Foundation/thread.h>
46 #include <the_Foundation/regexp.h>
47 
48 iDeclareType(LookupJob)
49 
50 struct Impl_LookupJob {
51     iRegExp *term;
52     iTime now;
53     iObjectList *docs;
54     iPtrArray results;
55 };
56 
init_LookupJob(iLookupJob * d)57 static void init_LookupJob(iLookupJob *d) {
58     d->term = NULL;
59     initCurrent_Time(&d->now);
60     d->docs = NULL;
61     init_PtrArray(&d->results);
62 }
63 
deinit_LookupJob(iLookupJob * d)64 static void deinit_LookupJob(iLookupJob *d) {
65     iForEach(PtrArray, i, &d->results) {
66         delete_LookupResult(i.ptr);
67     }
68     deinit_PtrArray(&d->results);
69     iRelease(d->docs);
70     iRelease(d->term);
71 }
72 
73 iDefineTypeConstruction(LookupJob)
74 
75 /*----------------------------------------------------------------------------------------------*/
76 
77 iDeclareType(LookupItem)
78 typedef iListItemClass iLookupItemClass;
79 
80 struct Impl_LookupItem {
81     iListItem listItem;
82     iLookupResult *result;
83     int font;
84     int fg;
85     iString icon;
86     iString text;
87     iString command;
88 };
89 
init_LookupItem(iLookupItem * d,const iLookupResult * res)90 static void init_LookupItem(iLookupItem *d, const iLookupResult *res) {
91     init_ListItem(&d->listItem);
92     d->result = res ? copy_LookupResult(res) : NULL;
93     d->font = uiContent_FontId;
94     d->fg = uiText_ColorId;
95     init_String(&d->icon);
96     if (res && res->icon) {
97         appendChar_String(&d->icon, res->icon);
98     }
99     init_String(&d->text);
100     init_String(&d->command);
101 }
102 
deinit_LookupItem(iLookupItem * d)103 static void deinit_LookupItem(iLookupItem *d) {
104     deinit_String(&d->command);
105     deinit_String(&d->text);
106     deinit_String(&d->icon);
107     delete_LookupResult(d->result);
108 }
109 
draw_LookupItem_(iLookupItem * d,iPaint * p,iRect rect,const iListWidget * list)110 static void draw_LookupItem_(iLookupItem *d, iPaint *p, iRect rect, const iListWidget *list) {
111     const iBool isPressing = isMouseDown_ListWidget(list);
112     const iBool isHover    = isHover_Widget(list) && constHoverItem_ListWidget(list) == d;
113     const iBool isCursor   = d->listItem.isSelected;
114     if (isHover || isCursor) {
115         fillRect_Paint(p,
116                        rect,
117                        isPressing || isCursor ? uiBackgroundPressed_ColorId
118                                               : uiBackgroundFramelessHover_ColorId);
119     }
120     int fg = isHover || isCursor
121                  ? permanent_ColorId | (isPressing || isCursor ? uiTextPressed_ColorId
122                                                                : uiTextFramelessHover_ColorId)
123                  : d->fg;
124     const iInt2 size = measureRange_Text(d->font, range_String(&d->text)).bounds.size;
125     iInt2       pos  = init_I2(left_Rect(rect) + 3 * gap_UI, mid_Rect(rect).y - size.y / 2);
126     if (d->listItem.isSeparator) {
127         pos.y = bottom_Rect(rect) - lineHeight_Text(d->font);
128     }
129     if (!isEmpty_String(&d->icon)) {
130         const iRect iconRect = { init_I2(pos.x, top_Rect(rect)),
131                                  init_I2(gap_UI * 5, height_Rect(rect)) };
132         const iRect iconVis = visualBounds_Text(d->font, range_String(&d->icon));
133         drawRange_Text(d->font,
134                        sub_I2(mid_Rect(iconRect), mid_Rect(iconVis)),
135                        fg,
136                        range_String(&d->icon));
137         pos.x += width_Rect(iconRect) + gap_UI * 3 / 2;
138     }
139     drawRange_Text(d->font, pos, fg, range_String(&d->text));
140 }
141 
142 iBeginDefineSubclass(LookupItem, ListItem)
143     .draw = (iAny *) draw_LookupItem_,
144 iEndDefineSubclass(LookupItem)
145 
146 iDefineObjectConstructionArgs(LookupItem, (const iLookupResult *res), res)
147 
148 /*----------------------------------------------------------------------------------------------*/
149 
150 struct Impl_LookupWidget {
151     iWidget      widget;
152     iListWidget *list;
153     size_t       cursor;
154     iThread *    work;
155     iCondition   jobAvailable; /* wakes up the work thread */
156     iMutex *     mtx;
157     iString      pendingTerm;
158     iObjectList *pendingDocs;
159     iLookupJob * finishedJob;
160 };
161 
scoreMatch_(const iRegExp * pattern,iRangecc text)162 static float scoreMatch_(const iRegExp *pattern, iRangecc text) {
163     float score = 0.0f;
164     iRegExpMatch m;
165     init_RegExpMatch(&m);
166     while (matchRange_RegExp(pattern, text, &m)) {
167         /* Match near the beginning is scored higher. */
168         score += (float) size_Range(&m.range) / ((float) m.range.start + 1);
169     }
170     return score;
171 }
172 
bookmarkRelevance_LookupJob_(const iLookupJob * d,const iBookmark * bm)173 static float bookmarkRelevance_LookupJob_(const iLookupJob *d, const iBookmark *bm) {
174     if (isFolder_Bookmark(bm)) {
175         return 0.0f;
176     }
177     iUrl parts;
178     init_Url(&parts, &bm->url);
179     const float t = scoreMatch_(d->term, range_String(&bm->title));
180     const float h = scoreMatch_(d->term, parts.host);
181     const float p = scoreMatch_(d->term, parts.path);
182     const float g = scoreMatch_(d->term, range_String(&bm->tags));
183     return h + iMax(p, t) + 2 * g; /* extra weight for tags */
184 }
185 
feedEntryRelevance_LookupJob_(const iLookupJob * d,const iFeedEntry * entry)186 static float feedEntryRelevance_LookupJob_(const iLookupJob *d, const iFeedEntry *entry) {
187     iUrl parts;
188     init_Url(&parts, &entry->url);
189     const float t = scoreMatch_(d->term, range_String(&entry->title));
190     const float h = scoreMatch_(d->term, parts.host);
191     const float p = scoreMatch_(d->term, parts.path);
192     const double age = secondsSince_Time(&d->now, &entry->posted) / 3600.0 / 24.0; /* days */
193     return (t * 3 + h + p) / (age + 1); /* extra weight for title, recency */
194 }
195 
identityRelevance_LookupJob_(const iLookupJob * d,const iGmIdentity * identity)196 static float identityRelevance_LookupJob_(const iLookupJob *d, const iGmIdentity *identity) {
197     iString *cn = subject_TlsCertificate(identity->cert);
198     const float c = scoreMatch_(d->term, range_String(cn));
199     const float n = scoreMatch_(d->term, range_String(&identity->notes));
200     delete_String(cn);
201     return c + 2 * n; /* extra weight for notes */
202 }
203 
visitedRelevance_LookupJob_(const iLookupJob * d,const iVisitedUrl * vis)204 static float visitedRelevance_LookupJob_(const iLookupJob *d, const iVisitedUrl *vis) {
205     iUrl parts;
206     init_Url(&parts, &vis->url);
207     const float h = scoreMatch_(d->term, parts.host);
208     const float p = scoreMatch_(d->term, parts.path);
209     const double age = secondsSince_Time(&d->now, &vis->when) / 3600.0 / 24.0; /* days */
210     return iMax(h, p) / (age + 1); /* extra weight for recency */
211 }
212 
matchBookmark_LookupJob_(void * context,const iBookmark * bm)213 static iBool matchBookmark_LookupJob_(void *context, const iBookmark *bm) {
214     return bookmarkRelevance_LookupJob_(context, bm) > 0;
215 }
216 
matchIdentity_LookupJob_(void * context,const iGmIdentity * identity)217 static iBool matchIdentity_LookupJob_(void *context, const iGmIdentity *identity) {
218     return identityRelevance_LookupJob_(context, identity) > 0;
219 }
220 
searchBookmarks_LookupJob_(iLookupJob * d)221 static void searchBookmarks_LookupJob_(iLookupJob *d) {
222     /* Note: Called in a background thread. */
223     /* TODO: Thread safety! What if a bookmark gets deleted while its being accessed here? */
224     iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), NULL, matchBookmark_LookupJob_, d)) {
225         const iBookmark *bm  = i.ptr;
226         iLookupResult *  res = new_LookupResult();
227         res->type            = bookmark_LookupResultType;
228         res->when            = bm->when;
229         res->relevance       = bookmarkRelevance_LookupJob_(d, bm);
230         res->icon            = bm->icon;
231         set_String(&res->label, &bm->title);
232         set_String(&res->url, &bm->url);
233         pushBack_PtrArray(&d->results, res);
234     }
235 }
236 
searchFeeds_LookupJob_(iLookupJob * d)237 static void searchFeeds_LookupJob_(iLookupJob *d) {
238     iConstForEach(PtrArray, i, listEntries_Feeds()) {
239         const iFeedEntry *entry = i.ptr;
240         const iBookmark *bm = get_Bookmarks(bookmarks_App(), entry->bookmarkId);
241         if (!bm) {
242             continue;
243         }
244         const float relevance = feedEntryRelevance_LookupJob_(d, entry);
245         if (relevance > 0) {
246             iLookupResult *res = new_LookupResult();
247             res->type          = feedEntry_LookupResultType;
248             res->when          = entry->posted;
249             res->relevance     = relevance;
250             set_String(&res->url, &entry->url);
251             set_String(&res->meta, &bm->title);
252             set_String(&res->label, &entry->title);
253             res->icon = bm->icon;
254             pushBack_PtrArray(&d->results, res);
255         }
256     }
257 }
258 
searchVisited_LookupJob_(iLookupJob * d)259 static void searchVisited_LookupJob_(iLookupJob *d) {
260     /* Note: Called in a background thread. */
261     /* TODO: Thread safety! Visited URLs may be deleted while being accessed here. */
262     iConstForEach(PtrArray, i, list_Visited(visited_App(), 0)) {
263         const iVisitedUrl *vis = i.ptr;
264         const float relevance = visitedRelevance_LookupJob_(d, vis);
265         if (relevance > 0) {
266             iLookupResult *res = new_LookupResult();
267             res->type = history_LookupResultType;
268             res->relevance = relevance;
269             set_String(&res->label, &vis->url);
270             set_String(&res->url, &vis->url);
271             res->when = vis->when;
272             pushBack_PtrArray(&d->results, res);
273         }
274     }
275 }
276 
searchHistory_LookupJob_(iLookupJob * d)277 static void searchHistory_LookupJob_(iLookupJob *d) {
278     /* Note: Called in a background thread. */
279     size_t index = 0;
280     iForEach(ObjectList, i, d->docs) {
281         iConstForEach(StringArray, j,
282                       searchContents_History(history_DocumentWidget(i.object), d->term)) {
283             const char *match = cstr_String(j.value);
284             const size_t matchLen = argLabel_Command(match, "len");
285             iRangecc text;
286             text.start = strstr(match, " str:") + 5;
287             text.end = text.start + matchLen;
288             const char *url = strstr(text.end, " url:") + 5;
289             iLookupResult *res = new_LookupResult();
290             res->type = content_LookupResultType;
291             res->relevance = ++index; /* most recent comes last */
292             setCStr_String(&res->label, "\"");
293             appendRange_String(&res->label, text);
294             appendCStr_String(&res->label, "\"");
295             setCStr_String(&res->url, url);
296             pushBack_PtrArray(&d->results, res);
297         }
298     }
299 }
300 
searchIdentities_LookupJob_(iLookupJob * d)301 static void searchIdentities_LookupJob_(iLookupJob *d) {
302     /* Note: Called in a background thread. */
303     iConstForEach(PtrArray, i, listIdentities_GmCerts(certs_App(), matchIdentity_LookupJob_, d)) {
304         const iGmIdentity *identity = i.ptr;
305         iLookupResult *res = new_LookupResult();
306         res->type = identity_LookupResultType;
307         res->relevance = identityRelevance_LookupJob_(d, identity);
308         res->icon = 0x1f464; /* identity->icon; */
309         iString *cn = subject_TlsCertificate(identity->cert);
310         set_String(&res->label, cn);
311         delete_String(cn);
312         set_String(&res->meta,
313                    collect_String(
314                        hexEncode_Block(collect_Block(fingerprint_TlsCertificate(identity->cert)))));
315         pushBack_PtrArray(&d->results, res);
316     }
317 }
318 
worker_LookupWidget_(iThread * thread)319 static iThreadResult worker_LookupWidget_(iThread *thread) {
320     iLookupWidget *d = userData_Thread(thread);
321 //    printf("[LookupWidget] worker is running\n"); fflush(stdout);
322     lock_Mutex(d->mtx);
323     for (;;) {
324         wait_Condition(&d->jobAvailable, d->mtx);
325         if (isEmpty_String(&d->pendingTerm)) {
326             break; /* Time to quit. */
327         }
328         iLookupJob *job = new_LookupJob();
329         /* Make a regular expression to search for multiple alternative words. */ {
330             iString *pattern = new_String();
331             iRangecc word = iNullRange;
332             iBool isFirst = iTrue;
333             iString wordStr;
334             init_String(&wordStr);
335             while (nextSplit_Rangecc(range_String(&d->pendingTerm), " ", &word)) {
336                 if (isEmpty_Range(&word)) continue;
337                 if (!isFirst) appendCStr_String(pattern, ".*");
338                 setRange_String(&wordStr, word);
339                 iConstForEach(String, ch, &wordStr) {
340                     /* Escape regular expression characters. */
341                     if (isSyntaxChar_RegExp(ch.value)) {
342                         appendChar_String(pattern, '\\');
343                     }
344                     appendChar_String(pattern, ch.value);
345                 }
346                 isFirst = iFalse;
347             }
348             deinit_String(&wordStr);
349             iAssert(!isEmpty_String(pattern));
350             job->term = new_RegExp(cstr_String(pattern), caseInsensitive_RegExpOption);
351             delete_String(pattern);
352         }
353         const size_t termLen = length_String(&d->pendingTerm); /* characters */
354         clear_String(&d->pendingTerm);
355         job->docs = d->pendingDocs;
356         d->pendingDocs = NULL;
357         unlock_Mutex(d->mtx);
358         /* Do the lookup. */ {
359             searchBookmarks_LookupJob_(job);
360             searchFeeds_LookupJob_(job);
361             searchVisited_LookupJob_(job);
362             if (termLen >= 3) {
363                 searchHistory_LookupJob_(job);
364             }
365             searchIdentities_LookupJob_(job);
366         }
367         /* Submit the result. */
368         lock_Mutex(d->mtx);
369         if (d->finishedJob) {
370             /* Previous results haven't been taken yet. */
371             delete_LookupJob(d->finishedJob);
372         }
373 //        printf("[LookupWidget] worker has %zu results\n", size_PtrArray(&job->results));
374         fflush(stdout);
375         d->finishedJob = job;
376         postCommand_Widget(as_Widget(d), "lookup.ready");
377     }
378     unlock_Mutex(d->mtx);
379 //    printf("[LookupWidget] worker has quit\n"); fflush(stdout);
380     return 0;
381 }
382 
iDefineObjectConstruction(LookupWidget)383 iDefineObjectConstruction(LookupWidget)
384 
385 static void updateMetrics_LookupWidget_(iLookupWidget *d) {
386     setItemHeight_ListWidget(d->list, lineHeight_Text(uiContent_FontId) * 1.333f);
387 }
388 
init_LookupWidget(iLookupWidget * d)389 void init_LookupWidget(iLookupWidget *d) {
390     iWidget *w = as_Widget(d);
391     init_Widget(w);
392     setId_Widget(w, "lookup");
393     setFlags_Widget(w, focusable_WidgetFlag, iTrue);
394 #if defined (iPlatformMobile)
395     setFlags_Widget(w, unhittable_WidgetFlag, iTrue);
396 #endif
397     d->list = addChildFlags_Widget(w, iClob(new_ListWidget()),
398                                    resizeToParentWidth_WidgetFlag |
399                                    resizeToParentHeight_WidgetFlag);
400     d->cursor = iInvalidPos;
401     d->work = new_Thread(worker_LookupWidget_);
402     setUserData_Thread(d->work, d);
403     init_Condition(&d->jobAvailable);
404     d->mtx = new_Mutex();
405     init_String(&d->pendingTerm);
406     d->pendingDocs = NULL;
407     d->finishedJob = NULL;
408     updateMetrics_LookupWidget_(d);
409     start_Thread(d->work);
410 }
411 
deinit_LookupWidget(iLookupWidget * d)412 void deinit_LookupWidget(iLookupWidget *d) {
413     /* Stop the worker. */ {
414         iGuardMutex(d->mtx, {
415             iReleasePtr(&d->pendingDocs);
416             clear_String(&d->pendingTerm);
417             signal_Condition(&d->jobAvailable);
418         });
419         join_Thread(d->work);
420         iRelease(d->work);
421     }
422     delete_LookupJob(d->finishedJob);
423     deinit_String(&d->pendingTerm);
424     delete_Mutex(d->mtx);
425     deinit_Condition(&d->jobAvailable);
426 }
427 
submit_LookupWidget(iLookupWidget * d,const iString * term)428 void submit_LookupWidget(iLookupWidget *d, const iString *term) {
429     iGuardMutex(d->mtx, {
430         set_String(&d->pendingTerm, term);
431         trim_String(&d->pendingTerm);
432         iReleasePtr(&d->pendingDocs);
433         if (!isEmpty_String(&d->pendingTerm)) {
434             d->pendingDocs = listDocuments_App(get_Root()); /* holds reference to all open tabs */
435             signal_Condition(&d->jobAvailable);
436         }
437         else {
438             showCollapsed_Widget(as_Widget(d), iFalse);
439         }
440     });
441 }
442 
draw_LookupWidget_(const iLookupWidget * d)443 static void draw_LookupWidget_(const iLookupWidget *d) {
444     const iWidget *w = constAs_Widget(d);
445     draw_Widget(w);
446     /* Draw a frame. */ {
447         iPaint p;
448         init_Paint(&p);
449         drawRect_Paint(&p,
450                        bounds_Widget(w),
451                        isFocused_Widget(w) ? uiInputFrameFocused_ColorId : uiSeparator_ColorId);
452     }
453 }
454 
cmpPtr_LookupResult_(const void * p1,const void * p2)455 static int cmpPtr_LookupResult_(const void *p1, const void *p2) {
456     const iLookupResult *a = *(const iLookupResult **) p1;
457     const iLookupResult *b = *(const iLookupResult **) p2;
458     if (a->type != b->type) {
459         return iCmp(a->type, b->type);
460     }
461     if (fabsf(a->relevance - b->relevance) < 0.0001f) {
462         return cmpString_String(&a->url, &b->url);
463     }
464     return -iCmp(a->relevance, b->relevance);
465 }
466 
cstr_LookupResultType(enum iLookupResultType d)467 static const char *cstr_LookupResultType(enum iLookupResultType d) {
468     switch (d) {
469         case bookmark_LookupResultType:
470             return "heading.lookup.bookmarks";
471         case feedEntry_LookupResultType:
472             return "heading.lookup.feeds";
473         case history_LookupResultType:
474             return "heading.lookup.history";
475         case content_LookupResultType:
476             return "heading.lookup.pagecontent";
477         case identity_LookupResultType:
478             return "heading.lookup.identities";
479         default:
480             return "heading.lookup.other";
481     }
482 }
483 
presentResults_LookupWidget_(iLookupWidget * d)484 static void presentResults_LookupWidget_(iLookupWidget *d) {
485     iLookupJob *job;
486     iGuardMutex(d->mtx, {
487         job = d->finishedJob;
488         d->finishedJob = NULL;
489     });
490     if (!job) return;
491     clear_ListWidget(d->list);
492     sort_Array(&job->results, cmpPtr_LookupResult_);
493     enum iLookupResultType lastType = none_LookupResultType;
494     const size_t maxPerType = 10; /* TODO: Setting? */
495     size_t perType = 0;
496     iConstForEach(PtrArray, i, &job->results) {
497         const iLookupResult *res = i.ptr;
498         if (lastType != res->type) {
499             /* Heading separator. */
500             iLookupItem *item = new_LookupItem(NULL);
501             item->listItem.isSeparator = iTrue;
502             item->fg = uiHeading_ColorId;
503             item->font = uiLabel_FontId;
504             format_String(&item->text, "%s", cstr_Lang(cstr_LookupResultType(res->type)));
505             addItem_ListWidget(d->list, item);
506             iRelease(item);
507             lastType = res->type;
508             perType = 0;
509         }
510         if (perType > maxPerType) {
511             continue;
512         }
513         if (res->type == identity_LookupResultType) {
514             const iString *docUrl = url_DocumentWidget(document_App());
515             iBlock *finger = hexDecode_Rangecc(range_String(&res->meta));
516             const iGmIdentity *ident = findIdentity_GmCerts(certs_App(), finger);
517             /* Sign in/out. */ {
518                 const iBool isUsed = isUsedOn_GmIdentity(ident, docUrl);
519                 iLookupItem *item  = new_LookupItem(res);
520                 item->fg           = uiText_ColorId;
521                 item->font         = uiContent_FontId;
522                 format_String(&item->text,
523                               "%s \u2014 " uiTextStrong_ColorEscape "%s",
524                               cstr_String(&res->label),
525                               cstr_Lang(isUsed ? "ident.stopuse" : "ident.use"));
526                 format_String(&item->command, "ident.sign%s ident:%s url:%s",
527                               isUsed ? "out arg:0" : "in", cstr_String(&res->meta), cstr_String(docUrl));
528                 addItem_ListWidget(d->list, item);
529                 iRelease(item);
530             }
531             if (isUsed_GmIdentity(ident)) {
532                 iLookupItem *item  = new_LookupItem(res);
533                 item->fg           = uiText_ColorId;
534                 item->font         = uiContent_FontId;
535                 format_String(&item->text,
536                               "%s \u2014 " uiTextStrong_ColorEscape "%s",
537                               cstr_String(&res->label),
538                               cstr_Lang("ident.stopuse.all"));
539                 format_String(&item->command, "ident.signout arg:1 ident:%s", cstr_String(&res->meta));
540                 addItem_ListWidget(d->list, item);
541                 iRelease(item);
542             }
543             delete_Block(finger);
544             continue;
545         }
546         iLookupItem *item = new_LookupItem(res);
547         const char *url = cstr_String(&res->url);
548         if (startsWithCase_String(&res->url, "gemini://")) {
549             url += 9;
550         }
551         switch (res->type) {
552             case bookmark_LookupResultType: {
553                 item->fg = uiTextStrong_ColorId;
554                 item->font = uiContent_FontId;
555                 format_String(&item->text,
556                               "%s %s\u2014 %s",
557                               cstr_String(&res->label),
558                               uiText_ColorEscape,
559                               url);
560                 format_String(&item->command, "open url:%s", cstr_String(&res->url));
561                 break;
562             }
563             case feedEntry_LookupResultType: {
564                 item->fg = uiTextStrong_ColorId;
565                 item->font = uiContent_FontId;
566                 format_String(&item->text,
567                               "%s %s\u2014 %s",
568                               cstr_String(&res->label),
569                               uiText_ColorEscape,
570                               cstr_String(&res->meta));
571                 const iString *cmd = feedEntryOpenCommand_String(&res->url, 0);
572                 if (cmd) {
573                     set_String(&item->command, cmd);
574                 }
575                 break;
576             }
577             case history_LookupResultType: {
578                 item->fg = uiText_ColorId;
579                 item->font = uiContent_FontId;
580                 format_String(&item->text, "%s \u2014 ", url);
581                 append_String(&item->text, collect_String(format_Time(&res->when, "%b %d, %Y")));
582                 format_String(&item->command, "open url:%s", cstr_String(&res->url));
583                 break;
584             }
585             case content_LookupResultType: {
586                 item->fg = uiText_ColorId;
587                 item->font = uiContent_FontId;
588                 format_String(&item->text, "%s \u2014 %s", url, cstr_String(&res->label));
589                 format_String(&item->command, "open url:%s", cstr_String(&res->url));
590                 break;
591             }
592             default:
593                 break;
594         }
595         addItem_ListWidget(d->list, item);
596         iRelease(item);
597         perType++;
598     }
599     delete_LookupJob(job);
600     /* Re-select the item at the cursor. */
601     if (d->cursor != iInvalidPos) {
602         d->cursor = iMin(d->cursor, numItems_ListWidget(d->list) - 1);
603         ((iListItem *) item_ListWidget(d->list, d->cursor))->isSelected = iTrue;
604     }
605     scrollOffset_ListWidget(d->list, 0);
606     updateVisible_ListWidget(d->list);
607     invalidate_ListWidget(d->list);
608     showCollapsed_Widget(as_Widget(d), numItems_ListWidget(d->list) != 0);
609 }
610 
item_LookupWidget_(iLookupWidget * d,size_t index)611 static iLookupItem *item_LookupWidget_(iLookupWidget *d, size_t index) {
612     return item_ListWidget(d->list, index);
613 }
614 
setCursor_LookupWidget_(iLookupWidget * d,size_t index)615 static void setCursor_LookupWidget_(iLookupWidget *d, size_t index) {
616     if (index != d->cursor) {
617         iLookupItem *item = item_LookupWidget_(d, d->cursor);
618         if (item) {
619             item->listItem.isSelected = iFalse;
620             invalidateItem_ListWidget(d->list, d->cursor);
621         }
622         d->cursor = index;
623         if ((item = item_LookupWidget_(d, d->cursor)) != NULL) {
624             item->listItem.isSelected = iTrue;
625             invalidateItem_ListWidget(d->list, d->cursor);
626         }
627         scrollToItem_ListWidget(d->list, d->cursor, 0);
628     }
629 }
630 
moveCursor_LookupWidget_(iLookupWidget * d,int delta)631 static iBool moveCursor_LookupWidget_(iLookupWidget *d, int delta) {
632     const int dir  = iSign(delta);
633     size_t    cur  = d->cursor;
634     size_t    good = cur;
635     while (delta && ((dir < 0 && cur > 0) || (dir > 0 && cur < numItems_ListWidget(d->list) - 1))) {
636         cur += dir;
637         if (!item_LookupWidget_(d, cur)->listItem.isSeparator) {
638             delta -= dir;
639             good = cur;
640         }
641     }
642     setCursor_LookupWidget_(d, good);
643     return delta == 0;
644 }
645 
processEvent_LookupWidget_(iLookupWidget * d,const SDL_Event * ev)646 static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) {
647     iWidget *w = as_Widget(d);
648     const char *cmd = command_UserEvent(ev);
649     if (isCommand_Widget(w, ev, "lookup.ready")) {
650         /* Take the results and present them in the list. */
651         presentResults_LookupWidget_(d);
652         return iTrue;
653     }
654     if (isMetricsChange_UserEvent(ev)) {
655         updateMetrics_LookupWidget_(d);
656     }
657     else if (isResize_UserEvent(ev) || equal_Command(cmd, "keyboard.changed") ||
658              (equal_Command(cmd, "layout.changed") &&
659               equal_Rangecc(range_Command(cmd, "id"), "navbar"))) {
660         /* Position the lookup popup under the URL bar. */ {
661             iRoot *root = w->root;
662             const iRect navBarBounds = bounds_Widget(findChild_Widget(root->widget, "navbar"));
663             iWidget *url = findChild_Widget(root->widget, "url");
664             setFixedSize_Widget(w, init_I2(width_Widget(url),
665                                            (bottom_Rect(rect_Root(root)) - bottom_Rect(navBarBounds)) / 2));
666             setPos_Widget(w, windowToLocal_Widget(w, bottomLeft_Rect(bounds_Widget(url))));
667 #if defined (iPlatformAppleMobile)
668             /* Adjust height based on keyboard size. */ {
669                 w->rect.size.y = visibleSize_Root(root).y - top_Rect(bounds_Widget(w));
670                 if (deviceType_App() == phone_AppDeviceType) {
671                     float l, r;
672                     safeAreaInsets_iOS(&l, NULL, &r, NULL);
673                     w->rect.size.x = size_Root(root).x - l - r;
674                     w->rect.pos.x  = l;
675                     /* TODO: Need to use windowToLocal_Widget? */
676                 }
677             }
678 #endif
679             arrange_Widget(w);
680         }
681         updateVisible_ListWidget(d->list);
682         invalidate_ListWidget(d->list);
683     }
684     if (equal_Command(cmd, "input.ended") && equal_Rangecc(range_Command(cmd, "id"), "url") &&
685         !isFocused_Widget(w)) {
686         showCollapsed_Widget(w, iFalse);
687     }
688     if (isCommand_Widget(w, ev, "focus.lost")) {
689         setCursor_LookupWidget_(d, iInvalidPos);
690     }
691     if (isCommand_Widget(w, ev, "focus.gained")) {
692         if (d->cursor == iInvalidPos) {
693             setCursor_LookupWidget_(d, 1);
694         }
695     }
696     if (isCommand_Widget(w, ev, "list.clicked")) {
697         iInputWidget *url = findWidget_App("url");
698         const iLookupItem *item = constItem_ListWidget(d->list, arg_Command(cmd));
699         if (item && !isEmpty_String(&item->command)) {
700             setText_InputWidget(url, url_DocumentWidget(document_App()));
701             showCollapsed_Widget(w, iFalse);
702             setCursor_LookupWidget_(d, iInvalidPos);
703             postCommandString_Root(get_Root(), &item->command);
704             postCommand_App("focus.set id:"); /* unfocus */
705         }
706         return iTrue;
707     }
708     if (ev->type == SDL_MOUSEMOTION) {
709         if (contains_Widget(w, init_I2(ev->motion.x, ev->motion.y))) {
710             setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_HAND);
711         }
712         return iFalse;
713     }
714     if (ev->type == SDL_KEYDOWN) {
715         const int mods = keyMods_Sym(ev->key.keysym.mod);
716         const int key = ev->key.keysym.sym;
717         if (isFocused_Widget(d)) {
718             iWidget *url = findWidget_App("url");
719             switch (key) {
720                 case SDLK_ESCAPE:
721                     showCollapsed_Widget(w, iFalse);
722                     setCursor_LookupWidget_(d, iInvalidPos);
723                     setFocus_Widget(url);
724                     return iTrue;
725                 case SDLK_UP:
726                     if (!moveCursor_LookupWidget_(d, -1)) {
727                         setCursor_LookupWidget_(d, iInvalidPos);
728                         setFocus_Widget(url);
729                     }
730                     return iTrue;
731                 case SDLK_DOWN:
732                     moveCursor_LookupWidget_(d, +1);
733                     return iTrue;
734                 case SDLK_PAGEUP:
735                     moveCursor_LookupWidget_(d, -visCount_ListWidget(d->list) + 1);
736                     return iTrue;
737                 case SDLK_PAGEDOWN:
738                     moveCursor_LookupWidget_(d, visCount_ListWidget(d->list) - 1);
739                     return iTrue;
740                 case SDLK_HOME:
741                     setCursor_LookupWidget_(d, 1);
742                     return iTrue;
743                 case SDLK_END:
744                     setCursor_LookupWidget_(d, numItems_ListWidget(d->list) - 1);
745                     return iTrue;
746                 case SDLK_KP_ENTER:
747                 case SDLK_SPACE:
748                 case SDLK_RETURN:
749                     postCommand_Widget(w, "list.clicked arg:%zu", d->cursor);
750                     return iTrue;
751             }
752         }
753         /* Focus switching between URL bar and lookup results. */
754         if (isVisible_Widget(w)) {
755             if (((key == SDLK_DOWN && !mods) || key == SDLK_TAB) &&
756                 focus_Widget() == findWidget_App("url") &&
757                 numItems_ListWidget(d->list)) {
758                 setCursor_LookupWidget_(d, 1); /* item 0 is always the first heading */
759                 setFocus_Widget(w);
760                 return iTrue;
761             }
762             else if (key == SDLK_TAB && isFocused_Widget(w)) {
763                 setFocus_Widget(findWidget_App("url"));
764                 return iTrue;
765             }
766         }
767     }
768     return processEvent_Widget(w, ev);
769 }
770 
771 iBeginDefineSubclass(LookupWidget, Widget)
772     .draw         = (iAny *) draw_LookupWidget_,
773     .processEvent = (iAny *) processEvent_LookupWidget_,
774 iEndDefineSubclass(LookupWidget)
775