1 /*
2  * SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
3  *
4  * SPDX-License-Identifier: LGPL-2.1-or-later
5  *
6  */
7 #include "inputwindow.h"
8 #include "fcitxtheme.h"
9 #include <fcitx-gclient/fcitxgclient.h>
10 #include <functional>
11 #include <initializer_list>
12 #include <limits>
13 #include <pango/pangocairo.h>
14 
15 namespace fcitx::gtk {
16 
textLength(GPtrArray * array)17 size_t textLength(GPtrArray *array) {
18     size_t length = 0;
19     for (unsigned int i = 0; i < array->len; i++) {
20         auto *preedit =
21             static_cast<FcitxGPreeditItem *>(g_ptr_array_index(array, i));
22         length += strlen(preedit->string);
23     }
24     return length;
25 }
26 
newPangoLayout(PangoContext * context)27 auto newPangoLayout(PangoContext *context) {
28     GObjectUniquePtr<PangoLayout> ptr(pango_layout_new(context));
29     pango_layout_set_single_paragraph_mode(ptr.get(), false);
30     return ptr;
31 }
32 
prepareLayout(cairo_t * cr,PangoLayout * layout)33 static void prepareLayout(cairo_t *cr, PangoLayout *layout) {
34     const PangoMatrix *matrix;
35 
36     matrix = pango_context_get_matrix(pango_layout_get_context(layout));
37 
38     if (matrix) {
39         cairo_matrix_t cairo_matrix;
40 
41         cairo_matrix_init(&cairo_matrix, matrix->xx, matrix->yx, matrix->xy,
42                           matrix->yy, matrix->x0, matrix->y0);
43 
44         cairo_transform(cr, &cairo_matrix);
45     }
46 }
47 
renderLayout(cairo_t * cr,PangoLayout * layout,int x,int y)48 static void renderLayout(cairo_t *cr, PangoLayout *layout, int x, int y) {
49     auto context = pango_layout_get_context(layout);
50     auto *metrics = pango_context_get_metrics(
51         context, pango_context_get_font_description(context),
52         pango_context_get_language(context));
53     auto ascent = pango_font_metrics_get_ascent(metrics);
54     pango_font_metrics_unref(metrics);
55     auto baseline = pango_layout_get_baseline(layout);
56     auto yOffset = PANGO_PIXELS(ascent - baseline);
57     cairo_save(cr);
58 
59     cairo_move_to(cr, x, y + yOffset);
60     prepareLayout(cr, layout);
61     pango_cairo_show_layout(cr, layout);
62 
63     cairo_restore(cr);
64 }
65 
width() const66 int MultilineLayout::width() const {
67     int width = 0;
68     for (const auto &layout : lines_) {
69         int w, h;
70         pango_layout_get_pixel_size(layout.get(), &w, &h);
71         width = std::max(width, w);
72     }
73     return width;
74 }
75 
render(cairo_t * cr,int x,int y,int lineHeight,bool highlight)76 void MultilineLayout::render(cairo_t *cr, int x, int y, int lineHeight,
77                              bool highlight) {
78     for (size_t i = 0; i < lines_.size(); i++) {
79         if (highlight) {
80             pango_layout_set_attributes(lines_[i].get(),
81                                         highlightAttrLists_[i].get());
82         } else {
83             pango_layout_set_attributes(lines_[i].get(), attrLists_[i].get());
84         }
85         renderLayout(cr, lines_[i].get(), x, y);
86         y += lineHeight;
87     }
88 }
89 
InputWindow(ClassicUIConfig * config,FcitxGClient * client)90 InputWindow::InputWindow(ClassicUIConfig *config, FcitxGClient *client)
91     : config_(config), client_(FCITX_G_CLIENT(g_object_ref(client))) {
92     fontMap_.reset(pango_cairo_font_map_new());
93     context_.reset(pango_font_map_create_context(fontMap_.get()));
94     upperLayout_ = newPangoLayout(context_.get());
95     lowerLayout_ = newPangoLayout(context_.get());
96 
97     auto update_ui_callback =
98         [](FcitxGClient *, GPtrArray *preedit, int cursor_pos, GPtrArray *auxUp,
99            GPtrArray *auxDown, GPtrArray *candidates, int highlight,
100            int layoutHint, gboolean hasPrev, gboolean hasNext,
101            void *user_data) {
102             auto that = static_cast<InputWindow *>(user_data);
103             that->updateUI(preedit, cursor_pos, auxUp, auxDown, candidates,
104                            highlight, layoutHint, hasPrev, hasNext);
105         };
106 
107     auto update_im_callback = [](FcitxGClient *, gchar *, gchar *,
108                                  gchar *langCode, void *user_data) {
109         auto that = static_cast<InputWindow *>(user_data);
110         that->updateLanguage(langCode);
111     };
112 
113     g_signal_connect(client_.get(), "update-client-side-ui",
114                      G_CALLBACK(+update_ui_callback), this);
115 
116     g_signal_connect(client_.get(), "current-im",
117                      G_CALLBACK(+update_im_callback), this);
118 }
119 
~InputWindow()120 InputWindow::~InputWindow() {
121     g_signal_handlers_disconnect_by_data(client_.get(), this);
122 }
123 
insertAttr(PangoAttrList * attrList,FcitxTextFormatFlag format,int start,int end,bool highlight) const124 void InputWindow::insertAttr(PangoAttrList *attrList,
125                              FcitxTextFormatFlag format, int start, int end,
126                              bool highlight) const {
127     if (format & FcitxTextFormatFlag_Underline) {
128         auto *attr = pango_attr_underline_new(PANGO_UNDERLINE_SINGLE);
129         attr->start_index = start;
130         attr->end_index = end;
131         pango_attr_list_insert(attrList, attr);
132     }
133     if (format & FcitxTextFormatFlag_Italic) {
134         auto *attr = pango_attr_style_new(PANGO_STYLE_ITALIC);
135         attr->start_index = start;
136         attr->end_index = end;
137         pango_attr_list_insert(attrList, attr);
138     }
139     if (format & FcitxTextFormatFlag_Strike) {
140         auto *attr = pango_attr_strikethrough_new(true);
141         attr->start_index = start;
142         attr->end_index = end;
143         pango_attr_list_insert(attrList, attr);
144     }
145     if (format & FcitxTextFormatFlag_Bold) {
146         auto *attr = pango_attr_weight_new(PANGO_WEIGHT_BOLD);
147         attr->start_index = start;
148         attr->end_index = end;
149         pango_attr_list_insert(attrList, attr);
150     }
151     GdkRGBA color = (format & FcitxTextFormatFlag_HighLight)
152                         ? config_->theme_.highlightColor
153                         : (highlight ? config_->theme_.highlightCandidateColor
154                                      : config_->theme_.normalColor);
155     const auto scale = std::numeric_limits<uint16_t>::max();
156     auto *attr = pango_attr_foreground_new(
157         color.red * scale, color.green * scale, color.blue * scale);
158     attr->start_index = start;
159     attr->end_index = end;
160     pango_attr_list_insert(attrList, attr);
161 
162     if (color.alpha != 1.0) {
163         auto *alphaAttr = pango_attr_foreground_alpha_new(color.alpha * scale);
164         alphaAttr->start_index = start;
165         alphaAttr->end_index = end;
166         pango_attr_list_insert(attrList, alphaAttr);
167     }
168 
169     auto background = config_->theme_.highlightBackgroundColor;
170     if ((format & FcitxTextFormatFlag_HighLight) && background.alpha > 0) {
171         attr = pango_attr_background_new(background.red * scale,
172                                          background.green * scale,
173                                          background.blue * scale);
174         attr->start_index = start;
175         attr->end_index = end;
176         pango_attr_list_insert(attrList, attr);
177 
178         if (background.alpha != 1.0) {
179             auto *alphaAttr =
180                 pango_attr_background_alpha_new(background.alpha * scale);
181             alphaAttr->start_index = start;
182             alphaAttr->end_index = end;
183             pango_attr_list_insert(attrList, alphaAttr);
184         }
185     }
186 }
187 
appendText(std::string & s,PangoAttrList * attrList,PangoAttrList * highlightAttrList,const GPtrArray * text)188 void InputWindow::appendText(std::string &s, PangoAttrList *attrList,
189                              PangoAttrList *highlightAttrList,
190                              const GPtrArray *text) {
191     for (size_t i = 0, e = text->len; i < e; i++) {
192         auto *item =
193             static_cast<FcitxGPreeditItem *>(g_ptr_array_index(text, i));
194         appendText(s, attrList, highlightAttrList, item->string, item->type);
195     }
196 }
197 
appendText(std::string & s,PangoAttrList * attrList,PangoAttrList * highlightAttrList,const gchar * text,int format)198 void InputWindow::appendText(std::string &s, PangoAttrList *attrList,
199                              PangoAttrList *highlightAttrList,
200                              const gchar *text, int format) {
201     auto start = s.size();
202     s.append(text);
203     auto end = s.size();
204     if (start == end) {
205         return;
206     }
207     const auto formatFlags = static_cast<FcitxTextFormatFlag>(format);
208     insertAttr(attrList, formatFlags, start, end, false);
209     if (highlightAttrList) {
210         insertAttr(highlightAttrList, formatFlags, start, end, true);
211     }
212 }
213 
resizeCandidates(size_t n)214 void InputWindow::resizeCandidates(size_t n) {
215     while (labelLayouts_.size() < n) {
216         labelLayouts_.emplace_back();
217     }
218     while (candidateLayouts_.size() < n) {
219         candidateLayouts_.emplace_back();
220     }
221 
222     nCandidates_ = n;
223 }
setLanguageAttr(size_t size,PangoAttrList * attrList,PangoAttrList * highlightAttrList)224 void InputWindow::setLanguageAttr(size_t size, PangoAttrList *attrList,
225                                   PangoAttrList *highlightAttrList) {
226     if (!config_->useInputMethodLanguageToDisplayText_ || language_.empty()) {
227         return;
228     }
229     if (auto language = pango_language_from_string(language_.c_str())) {
230         if (attrList) {
231             auto attr = pango_attr_language_new(language);
232             attr->start_index = 0;
233             attr->end_index = size;
234             pango_attr_list_insert(attrList, attr);
235         }
236         if (highlightAttrList) {
237             auto attr = pango_attr_language_new(language);
238             attr->start_index = 0;
239             attr->end_index = size;
240             pango_attr_list_insert(highlightAttrList, attr);
241         }
242     }
243 }
244 
setTextToMultilineLayout(MultilineLayout & layout,const gchar * text)245 void InputWindow::setTextToMultilineLayout(MultilineLayout &layout,
246                                            const gchar *text) {
247     gchar **lines = g_strsplit(text, "\n", -1);
248     layout.lines_.clear();
249     layout.attrLists_.clear();
250     layout.highlightAttrLists_.clear();
251 
252     for (int i = 0; lines && lines[i]; i++) {
253         layout.lines_.emplace_back(pango_layout_new(context_.get()));
254         layout.attrLists_.emplace_back();
255         layout.highlightAttrLists_.emplace_back();
256         setTextToLayout(layout.lines_.back().get(), &layout.attrLists_.back(),
257                         &layout.highlightAttrLists_.back(), lines[i]);
258     }
259 }
260 
setTextToLayout(PangoLayout * layout,PangoAttrListUniquePtr * attrList,PangoAttrListUniquePtr * highlightAttrList,std::initializer_list<const GPtrArray * > texts)261 void InputWindow::setTextToLayout(
262     PangoLayout *layout, PangoAttrListUniquePtr *attrList,
263     PangoAttrListUniquePtr *highlightAttrList,
264     std::initializer_list<const GPtrArray *> texts) {
265     auto *newAttrList = pango_attr_list_new();
266     if (attrList) {
267         // PangoAttrList does not have "clear()". So when we set new text,
268         // we need to create a new one and get rid of old one.
269         // We keep a ref to the attrList.
270         attrList->reset(pango_attr_list_ref(newAttrList));
271     }
272     PangoAttrList *newHighlightAttrList = nullptr;
273     if (highlightAttrList) {
274         newHighlightAttrList = pango_attr_list_new();
275         highlightAttrList->reset(newHighlightAttrList);
276     }
277     std::string line;
278     for (const auto &text : texts) {
279         appendText(line, newAttrList, newHighlightAttrList, text);
280     }
281 
282     setLanguageAttr(line.size(), newAttrList, newHighlightAttrList);
283 
284     pango_layout_set_text(layout, line.c_str(), line.size());
285     pango_layout_set_attributes(layout, newAttrList);
286     pango_attr_list_unref(newAttrList);
287 }
288 
setTextToLayout(PangoLayout * layout,PangoAttrListUniquePtr * attrList,PangoAttrListUniquePtr * highlightAttrList,const gchar * text)289 void InputWindow::setTextToLayout(PangoLayout *layout,
290                                   PangoAttrListUniquePtr *attrList,
291                                   PangoAttrListUniquePtr *highlightAttrList,
292                                   const gchar *text) {
293     auto *newAttrList = pango_attr_list_new();
294     if (attrList) {
295         // PangoAttrList does not have "clear()". So when we set new text,
296         // we need to create a new one and get rid of old one.
297         // We keep a ref to the attrList.
298         attrList->reset(pango_attr_list_ref(newAttrList));
299     }
300     PangoAttrList *newHighlightAttrList = nullptr;
301     if (highlightAttrList) {
302         newHighlightAttrList = pango_attr_list_new();
303         highlightAttrList->reset(newHighlightAttrList);
304     }
305     std::string line;
306     appendText(line, newAttrList, newHighlightAttrList, text);
307 
308     pango_layout_set_text(layout, line.c_str(), line.size());
309     pango_layout_set_attributes(layout, newAttrList);
310     pango_attr_list_unref(newAttrList);
311 }
312 
updateUI(GPtrArray * preedit,int cursor_pos,GPtrArray * auxUp,GPtrArray * auxDown,GPtrArray * candidates,int highlight,int layoutHint,bool hasPrev,bool hasNext)313 void InputWindow::updateUI(GPtrArray *preedit, int cursor_pos, GPtrArray *auxUp,
314                            GPtrArray *auxDown, GPtrArray *candidates,
315                            int highlight, int layoutHint, bool hasPrev,
316                            bool hasNext) {
317     // | aux up | preedit
318     // | aux down
319     // | 1 candidate | 2 ...
320     // or
321     // | aux up | preedit
322     // | aux down
323     // | candidate 1
324     // | candidate 2
325     // | candidate 3
326 
327     cursor_ = -1;
328     pango_layout_set_single_paragraph_mode(upperLayout_.get(), true);
329     setTextToLayout(upperLayout_.get(), nullptr, nullptr, {auxUp, preedit});
330     if (cursor_pos >= 0 &&
331         static_cast<size_t>(cursor_pos) <= textLength(preedit)) {
332 
333         cursor_ = cursor_pos + textLength(auxUp);
334     }
335 
336     setTextToLayout(lowerLayout_.get(), nullptr, nullptr, {auxDown});
337 
338     // Count non-placeholder candidates.
339     resizeCandidates(candidates->len);
340 
341     candidateIndex_ = highlight;
342     for (int i = 0, e = candidates->len; i < e; i++) {
343         auto *candidate = static_cast<FcitxGCandidateItem *>(
344             g_ptr_array_index(candidates, i));
345         setTextToMultilineLayout(labelLayouts_[i], candidate->label);
346         setTextToMultilineLayout(candidateLayouts_[i], candidate->candidate);
347     }
348 
349     layoutHint_ = static_cast<FcitxCandidateLayoutHint>(layoutHint);
350     hasPrev_ = hasPrev;
351     hasNext_ = hasNext;
352 
353     visible_ = nCandidates_ ||
354                pango_layout_get_character_count(upperLayout_.get()) ||
355                pango_layout_get_character_count(lowerLayout_.get());
356 
357     update();
358 }
359 
updateLanguage(const char * language)360 void InputWindow::updateLanguage(const char *language) { language_ = language; }
361 
sizeHint()362 std::pair<unsigned int, unsigned int> InputWindow::sizeHint() {
363     auto *fontDesc = pango_font_description_from_string(config_->font_.data());
364     pango_context_set_font_description(context_.get(), fontDesc);
365     if (dpi_ > 0) {
366         pango_cairo_font_map_set_resolution(
367             PANGO_CAIRO_FONT_MAP(fontMap_.get()), dpi_);
368     }
369     pango_cairo_context_set_resolution(context_.get(), dpi_);
370     pango_font_description_free(fontDesc);
371     pango_layout_context_changed(upperLayout_.get());
372     pango_layout_context_changed(lowerLayout_.get());
373     for (size_t i = 0; i < nCandidates_; i++) {
374         labelLayouts_[i].contextChanged();
375         candidateLayouts_[i].contextChanged();
376     }
377     auto *metrics = pango_context_get_metrics(
378         context_.get(), pango_context_get_font_description(context_.get()),
379         pango_context_get_language(context_.get()));
380     auto fontHeight = pango_font_metrics_get_ascent(metrics) +
381                       pango_font_metrics_get_descent(metrics);
382     pango_font_metrics_unref(metrics);
383     fontHeight = PANGO_PIXELS(fontHeight);
384 
385     size_t width = 0;
386     size_t height = 0;
387     auto updateIfLarger = [](size_t &m, size_t n) {
388         if (n > m) {
389             m = n;
390         }
391     };
392     int w, h;
393 
394     const auto &textMargin = config_->theme_.textMargin;
395     auto extraW = textMargin.marginLeft + textMargin.marginRight;
396     auto extraH = textMargin.marginTop + textMargin.marginBottom;
397     if (pango_layout_get_character_count(upperLayout_.get())) {
398         pango_layout_get_pixel_size(upperLayout_.get(), &w, &h);
399         height += fontHeight + extraH;
400         updateIfLarger(width, w + extraW);
401     }
402     if (pango_layout_get_character_count(lowerLayout_.get())) {
403         pango_layout_get_pixel_size(lowerLayout_.get(), &w, &h);
404         height += fontHeight + extraH;
405         updateIfLarger(width, w + extraW);
406     }
407 
408     bool vertical = config_->vertical_;
409     if (layoutHint_ == FcitxCandidateLayoutHint::Vertical) {
410         vertical = true;
411     } else if (layoutHint_ == FcitxCandidateLayoutHint::Horizontal) {
412         vertical = false;
413     }
414 
415     size_t wholeH = 0, wholeW = 0;
416     for (size_t i = 0; i < nCandidates_; i++) {
417         size_t candidateW = 0, candidateH = 0;
418         if (labelLayouts_[i].characterCount()) {
419             candidateW += labelLayouts_[i].width();
420             updateIfLarger(candidateH,
421                            std::max(1, labelLayouts_[i].size()) * fontHeight +
422                                extraH);
423         }
424         if (candidateLayouts_[i].characterCount()) {
425             candidateW += candidateLayouts_[i].width();
426             updateIfLarger(
427                 candidateH,
428                 std::max(1, candidateLayouts_[i].size()) * fontHeight + extraH);
429         }
430         candidateW += extraW;
431 
432         if (vertical) {
433             wholeH += candidateH;
434             updateIfLarger(wholeW, candidateW);
435         } else {
436             wholeW += candidateW;
437             updateIfLarger(wholeH, candidateH);
438         }
439     }
440     updateIfLarger(width, wholeW);
441     candidatesHeight_ = wholeH;
442     height += wholeH;
443     const auto &margin = config_->theme_.contentMargin;
444     width += margin.marginLeft + margin.marginRight;
445     height += margin.marginTop + margin.marginBottom;
446 
447     if (nCandidates_ && (hasPrev_ || hasNext_)) {
448         const auto &prev = config_->theme_.loadAction(config_->theme_.prev);
449         const auto &next = config_->theme_.loadAction(config_->theme_.next);
450         if (prev.valid() && next.valid()) {
451             width += prev.width() + next.width();
452         }
453     }
454 
455     return {width, height};
456 }
457 
paint(cairo_t * cr,unsigned int width,unsigned int height)458 void InputWindow::paint(cairo_t *cr, unsigned int width, unsigned int height) {
459     cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
460     config_->theme_.paint(cr, config_->theme_.background, width, height);
461     const auto &margin = config_->theme_.contentMargin;
462     const auto &textMargin = config_->theme_.textMargin;
463     cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
464     cairo_save(cr);
465 
466     prevRegion_ = cairo_rectangle_int_t{0, 0, 0, 0};
467     nextRegion_ = cairo_rectangle_int_t{0, 0, 0, 0};
468     if (nCandidates_ && (hasPrev_ || hasNext_)) {
469         const auto &prev = config_->theme_.loadAction(config_->theme_.prev);
470         const auto &next = config_->theme_.loadAction(config_->theme_.next);
471         if (prev.valid() && next.valid()) {
472             cairo_save(cr);
473             nextRegion_.x = width - margin.marginRight - next.width();
474             nextRegion_.y = height - margin.marginBottom - next.height();
475             nextRegion_.width = next.width();
476             nextRegion_.height = next.height();
477             cairo_translate(cr, nextRegion_.x, nextRegion_.y);
478             shrink(nextRegion_, config_->theme_.next.clickMargin);
479             double alpha = 1.0;
480             if (!hasNext_) {
481                 alpha = 0.3;
482             } else if (nextHovered_) {
483                 alpha = 0.7;
484             }
485             config_->theme_.paint(cr, config_->theme_.next, alpha);
486             cairo_restore(cr);
487             cairo_save(cr);
488             prevRegion_.x =
489                 width - margin.marginRight - next.width() - prev.width();
490             prevRegion_.y = height - margin.marginBottom - prev.height();
491             prevRegion_.width = prev.width();
492             prevRegion_.height = prev.height();
493             cairo_translate(cr, prevRegion_.x, prevRegion_.y);
494             shrink(prevRegion_, config_->theme_.prev.clickMargin);
495             alpha = 1.0;
496             if (!hasPrev_) {
497                 alpha = 0.3;
498             } else if (prevHovered_) {
499                 alpha = 0.7;
500             }
501             config_->theme_.paint(cr, config_->theme_.prev, alpha);
502             cairo_restore(cr);
503         }
504     }
505 
506     // Move position to the right place.
507     cairo_translate(cr, margin.marginLeft, margin.marginTop);
508 
509     cairo_save(cr);
510     cairoSetSourceColor(cr, config_->theme_.normalColor);
511     // CLASSICUI_DEBUG() << theme.inputPanel->normalColor->toString();
512     auto *metrics = pango_context_get_metrics(
513         context_.get(), pango_context_get_font_description(context_.get()),
514         pango_context_get_language(context_.get()));
515     auto fontHeight = pango_font_metrics_get_ascent(metrics) +
516                       pango_font_metrics_get_descent(metrics);
517     pango_font_metrics_unref(metrics);
518     fontHeight = PANGO_PIXELS(fontHeight);
519 
520     size_t currentHeight = 0;
521     int w, h;
522     auto extraW = textMargin.marginLeft + textMargin.marginRight;
523     auto extraH = textMargin.marginTop + textMargin.marginBottom;
524     if (pango_layout_get_character_count(upperLayout_.get())) {
525         renderLayout(cr, upperLayout_.get(), textMargin.marginLeft,
526                      textMargin.marginTop);
527         pango_layout_get_pixel_size(upperLayout_.get(), &w, &h);
528         PangoRectangle pos;
529         if (cursor_ >= 0) {
530             pango_layout_get_cursor_pos(upperLayout_.get(), cursor_, &pos,
531                                         nullptr);
532 
533             cairo_save(cr);
534             cairo_set_line_width(cr, 2);
535             auto offsetX = pango_units_to_double(pos.x);
536             cairo_move_to(cr, textMargin.marginLeft + offsetX + 1,
537                           textMargin.marginTop);
538             cairo_line_to(cr, textMargin.marginLeft + offsetX + 1,
539                           textMargin.marginTop + fontHeight);
540             cairo_stroke(cr);
541             cairo_restore(cr);
542         }
543         currentHeight += fontHeight + extraH;
544     }
545     if (pango_layout_get_character_count(lowerLayout_.get())) {
546         renderLayout(cr, lowerLayout_.get(), textMargin.marginLeft,
547                      textMargin.marginTop + currentHeight);
548         pango_layout_get_pixel_size(lowerLayout_.get(), &w, nullptr);
549         currentHeight += fontHeight + extraH;
550     }
551 
552     bool vertical = config_->vertical_;
553     if (layoutHint_ == FcitxCandidateLayoutHint::Vertical) {
554         vertical = true;
555     } else if (layoutHint_ == FcitxCandidateLayoutHint::Horizontal) {
556         vertical = false;
557     }
558 
559     candidateRegions_.clear();
560     candidateRegions_.reserve(nCandidates_);
561     size_t wholeW = 0, wholeH = 0;
562 
563     // size of text = textMargin + actual text size.
564     // HighLight = HighLight margin + TEXT.
565     // Click region = HighLight - click
566 
567     for (size_t i = 0; i < nCandidates_; i++) {
568         int x, y;
569         if (vertical) {
570             x = 0;
571             y = currentHeight + wholeH;
572         } else {
573             x = wholeW;
574             y = currentHeight;
575         }
576         x += textMargin.marginLeft;
577         y += textMargin.marginTop;
578         int labelW = 0, labelH = 0, candidateW = 0, candidateH = 0;
579         if (labelLayouts_[i].characterCount()) {
580             labelW = labelLayouts_[i].width();
581             labelH = fontHeight * labelLayouts_[i].size();
582         }
583         if (candidateLayouts_[i].characterCount()) {
584             candidateW = candidateLayouts_[i].width();
585             candidateH = fontHeight * candidateLayouts_[i].size();
586         }
587         int vheight;
588         if (vertical) {
589             vheight = std::max({fontHeight, labelH, candidateH});
590             wholeH += vheight + extraH;
591         } else {
592             vheight = candidatesHeight_ - extraH;
593             wholeW += candidateW + labelW + extraW;
594         }
595         const auto &highlightMargin = config_->theme_.highlight.margin;
596         const auto &clickMargin = config_->theme_.highlight.clickMargin;
597         auto highlightWidth = labelW + candidateW;
598         if (config_->theme_.fullWidthHighlight && vertical) {
599             // Last candidate, fill.
600             highlightWidth = width - margin.marginLeft - margin.marginRight -
601                              textMargin.marginRight - textMargin.marginLeft;
602         }
603         const int highlightIndex = highlight();
604         bool highlight = false;
605         if (highlightIndex >= 0 && i == static_cast<size_t>(highlightIndex)) {
606             cairo_save(cr);
607             cairo_translate(cr, x - highlightMargin.marginLeft,
608                             y - highlightMargin.marginTop);
609             config_->theme_.paint(cr, config_->theme_.highlight,
610                                   highlightWidth + highlightMargin.marginLeft +
611                                       highlightMargin.marginRight,
612                                   vheight + highlightMargin.marginTop +
613                                       highlightMargin.marginBottom);
614             cairo_restore(cr);
615             highlight = true;
616         }
617         cairo_rectangle_int_t candidateRegion;
618         candidateRegion.x = margin.marginLeft + x - highlightMargin.marginLeft +
619                             clickMargin.marginLeft;
620         candidateRegion.y = margin.marginTop + y - highlightMargin.marginTop +
621                             clickMargin.marginTop;
622         candidateRegion.width = highlightWidth + highlightMargin.marginLeft +
623                                 highlightMargin.marginRight -
624                                 clickMargin.marginLeft -
625                                 clickMargin.marginRight;
626         candidateRegion.height =
627             vheight + highlightMargin.marginTop + highlightMargin.marginBottom -
628             clickMargin.marginTop - clickMargin.marginBottom;
629         candidateRegions_.push_back(candidateRegion);
630         if (labelLayouts_[i].characterCount()) {
631             labelLayouts_[i].render(cr, x, y, fontHeight, highlight);
632         }
633         if (candidateLayouts_[i].characterCount()) {
634             candidateLayouts_[i].render(cr, x + labelW, y, fontHeight,
635                                         highlight);
636         }
637     }
638     cairo_restore(cr);
639 }
640 
click(int x,int y)641 void InputWindow::click(int x, int y) {
642     for (size_t idx = 0, e = candidateRegions_.size(); idx < e; idx++) {
643         if (rectContains(candidateRegions_[idx], x, y)) {
644             selectCandidate(idx);
645             return;
646         }
647     }
648     if (hasPrev_ && rectContains(prevRegion_, x, y)) {
649         prev();
650         return;
651     }
652     if (hasNext_ && rectContains(nextRegion_, x, y)) {
653         next();
654         return;
655     }
656 }
657 
wheel(bool up)658 void InputWindow::wheel(bool up) {
659     if (!config_->wheelForPaging_) {
660         return;
661     }
662     if (nCandidates_ == 0) {
663         return;
664     }
665     if (up) {
666         if (hasPrev_) {
667             prev();
668         }
669     } else {
670         if (hasNext_) {
671             next();
672         }
673     }
674 }
675 
highlight() const676 int InputWindow::highlight() const {
677     int highlightIndex = (hoverIndex_ >= 0) ? hoverIndex_ : candidateIndex_;
678     return highlightIndex;
679 }
680 
hover(int x,int y)681 bool InputWindow::hover(int x, int y) {
682     bool needRepaint = false;
683     auto oldHighlight = highlight();
684     hoverIndex_ = -1;
685     for (int idx = 0, e = candidateRegions_.size(); idx < e; idx++) {
686         if (rectContains(candidateRegions_[idx], x, y)) {
687             hoverIndex_ = idx;
688             break;
689         }
690     }
691 
692     needRepaint = needRepaint || oldHighlight != highlight();
693 
694     auto prevHovered = rectContains(prevRegion_, x, y);
695     auto nextHovered = rectContains(nextRegion_, x, y);
696     needRepaint = needRepaint || prevHovered_ != prevHovered;
697     needRepaint = needRepaint || nextHovered_ != nextHovered;
698     prevHovered_ = prevHovered;
699     nextHovered_ = nextHovered;
700     return needRepaint;
701 }
702 
prev()703 void InputWindow::prev() {
704     if (hasPrev_) {
705         fcitx_g_client_prev_page(client_.get());
706     }
707 }
708 
next()709 void InputWindow::next() {
710     if (hasNext_) {
711         fcitx_g_client_next_page(client_.get());
712     }
713 }
714 
selectCandidate(int i)715 void InputWindow::selectCandidate(int i) {
716     fcitx_g_client_select_candidate(client_.get(), i);
717 }
718 
719 } // namespace fcitx::gtk
720