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