The Battle for Wesnoth  1.19.3+dev
rich_label.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2024
3  by Subhraman Sarkar (babaissarkar) <suvrax@gmail.com>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #define GETTEXT_DOMAIN "wesnoth-lib"
17 
19 
20 #include "gui/core/log.hpp"
21 
24 #include "gui/dialogs/message.hpp"
25 
26 #include "cursor.hpp"
27 #include "desktop/clipboard.hpp"
28 #include "desktop/open.hpp"
29 #include "help/help_impl.hpp"
30 #include "gettext.hpp"
31 #include "log.hpp"
34 #include "wml_exception.hpp"
35 
36 #include <functional>
37 #include <string>
38 #include <boost/format.hpp>
39 
40 static lg::log_domain log_rich_label("gui/widget/rich_label");
41 #define DBG_GUI_RL LOG_STREAM(debug, log_rich_label)
42 
43 namespace gui2
44 {
45 
46 // ------------ WIDGET -----------{
47 
48 REGISTER_WIDGET(rich_label)
49 
50 rich_label::rich_label(const implementation::builder_rich_label& builder)
51  : styled_widget(builder, type())
52  , state_(ENABLED)
53  , can_wrap_(true)
54  , link_aware_(true)
55  , link_color_(font::YELLOW_COLOR)
56  , can_shrink_(true)
57  , text_alpha_(ALPHA_OPAQUE)
58  , unparsed_text_()
59  , w_(0)
60  , h_(0)
61  , x_(0)
62  , padding_(5)
63  , txt_height_(0)
64  , prev_blk_height_(0)
65 {
66  connect_signal<event::LEFT_BUTTON_CLICK>(
67  std::bind(&rich_label::signal_handler_left_button_click, this, std::placeholders::_3));
68  connect_signal<event::MOUSE_MOTION>(
69  std::bind(&rich_label::signal_handler_mouse_motion, this, std::placeholders::_3, std::placeholders::_5));
70  connect_signal<event::MOUSE_LEAVE>(
71  std::bind(&rich_label::signal_handler_mouse_leave, this, std::placeholders::_3));
72 }
73 
75  // Set up fake render to calculate text position
77  wfl::map_formula_callable variables;
78  variables.add("text", wfl::variant(text_cfg["text"].str()));
79  variables.add("width", wfl::variant(width > 0 ? width : w_));
80  variables.add("text_wrap_mode", wfl::variant(PANGO_ELLIPSIZE_NONE));
81  variables.add("fake_draw", wfl::variant(true));
82  tshape_ = std::make_unique<gui2::text_shape>(text_cfg, functions);
83  tshape_->draw(variables);
84  return variables;
85 }
86 
87 point rich_label::get_text_size(config text_cfg, unsigned width) {
88  wfl::map_formula_callable variables = setup_text_renderer(text_cfg, width);
89  return point(variables.query_value("text_width").as_int(), variables.query_value("text_height").as_int());
90 }
91 
94  wfl::map_formula_callable variables;
95  variables.add("fake_draw", wfl::variant(true));
96  ishape_ = std::make_unique<gui2::image_shape>(img_cfg, functions);
97  ishape_->draw(variables);
98  return point(variables.query_value("image_width").as_int(), variables.query_value("image_height").as_int());
99 }
100 
101 void rich_label::add_text_with_attribute(config& curr_item, std::string text, std::string attr_name, std::string extra_data) {
102  size_t start = curr_item["text"].str().size();
103 
104  curr_item["text"] = curr_item["text"].str() + text;
105 
106  if (!attr_name.empty()) {
107  append_if_not_empty(&curr_item["attr_name"], ",");
108  curr_item["attr_name"] = curr_item["attr_name"].str() + attr_name;
109 
110  append_if_not_empty(&curr_item["attr_start"], ",");
111  curr_item["attr_start"] = curr_item["attr_start"].str() + std::to_string(start);
112 
113  append_if_not_empty(&curr_item["attr_end"], ",");
114  curr_item["attr_end"] = curr_item["attr_end"].str() + std::to_string(curr_item["text"].str().size());
115 
116  if (!extra_data.empty()) {
117  append_if_not_empty(&curr_item["attr_data"], ",");
118  curr_item["attr_data"] = curr_item["attr_data"].str() + extra_data;
119  }
120  }
121 }
122 
123 void rich_label::add_text_with_attributes(config& curr_item, std::string text, std::vector<std::string> attr_names, std::vector<std::string> extra_data) {
124 
125  size_t start = curr_item["text"].str().size();
126  curr_item["text"] = curr_item["text"].str() + text;
127 
128  if (!attr_names.empty()) {
129  append_if_not_empty(&curr_item["attr_name"], ",");
130  curr_item["attr_name"] = curr_item["attr_name"].str() + utils::join(attr_names);
131 
132  for (size_t i = 0; i < attr_names.size(); i++) {
133  append_if_not_empty(&curr_item["attr_start"], ",");
134  curr_item["attr_start"] = curr_item["attr_start"].str() + std::to_string(start);
135  append_if_not_empty(&curr_item["attr_end"], ",");
136  curr_item["attr_end"] = curr_item["attr_end"].str() + std::to_string(curr_item["text"].str().size());
137  }
138 
139  if (!extra_data.empty()) {
140  append_if_not_empty(&curr_item["attr_data"], ",");
141  curr_item["attr_data"] = curr_item["attr_data"].str() + utils::join(extra_data);
142  }
143  }
144 }
145 
146 void rich_label::add_image(config& curr_item, std::string name, std::string align, bool floating, point& img_size) {
147  curr_item["name"] = name;
148 
149  if (align.empty()) {
150  align = "left";
151  }
152 
153  if (align == "right") {
154  curr_item["x"] = floating ? "(width - image_width - img_x)" : "(width - image_width - pos_x)";
155  } else if (align == "middle" || align == "center") {
156  // works for single image only
157  curr_item["x"] = floating ? "(img_x + (width - image_width)/2.0)" : "(pos_x + (width - image_width)/2.0)";
158  } else {
159  // left aligned images are default for now
160  curr_item["x"] = floating ? "(img_x)" : "(pos_x)";
161  }
162  curr_item["y"] = floating ? "(img_y + pos_y)" : "(pos_y)";
163  curr_item["h"] = "(image_height)";
164  curr_item["w"] = "(image_width)";
165 
166  // Sizing
167  if (floating) {
168  img_size.x = get_image_size(curr_item).x;
169  img_size.y += get_image_size(curr_item).y;
170  } else {
171  img_size.x += get_image_size(curr_item).x + padding_;
172  img_size.y = get_image_size(curr_item).y;
173  }
174 
175  std::stringstream actions;
176  actions << "([";
177  if (floating) {
178 
179  if (align == "left") {
180  x_ = img_size.x + padding_;
181  actions << "set_var('pos_x', image_width + padding)";
182  } else if (align == "right") {
183  x_ = 0;
184  actions << "set_var('pos_x', 0)";
185  actions << ",";
186  actions << "set_var('ww', image_width)";
187  }
188 
189  img_size.y += padding_;
190  actions << "," << "set_var('img_y', img_y + image_height + padding)";
191 
192  } else {
193  x_ = img_size.x;
194  actions << "set_var('pos_x', pos_x + image_width + padding)";
195  }
196  actions << "])";
197 
198  curr_item["actions"] = actions.str();
199  actions.str("");
200 }
201 
202 void rich_label::add_link(config& curr_item, std::string name, std::string dest, int img_width) {
203  // TODO algorithm needs to be text_alignment independent
204 
205  DBG_GUI_RL << "add_link, x=" << x_ << " width=" << img_width;
206 
207  setup_text_renderer(curr_item, w_ - x_ - img_width);
208  point t_start = get_xy_from_offset(utf8::size(curr_item["text"].str()));
209 
210  DBG_GUI_RL << "link text start:" << t_start;
211 
212  std::string link_text = name.empty() ? dest : name;
213  add_text_with_attribute(curr_item, link_text, "color", link_color_.to_hex_string().substr(1));
214 
215  setup_text_renderer(curr_item, w_ - x_ - img_width);
216  point t_end = get_xy_from_offset(utf8::size(curr_item["text"].str()));
217  DBG_GUI_RL << "link text end:" << t_end;
218 
219  point link_start(x_ + t_start.x, prev_blk_height_ + t_start.y);
221 
222  // TODO link after right aligned images
223 
224  // Add link
225  if (t_end.x > t_start.x) {
226  point link_size = t_end - t_start;
227  rect link_rect = {
228  link_start.x,
229  link_start.y,
230  link_size.x,
231  link_size.y,
232  };
233  links_.push_back(std::pair(link_rect, dest));
234 
235  DBG_GUI_RL << "added link at rect: " << link_rect;
236 
237  } else {
238  //link straddles two lines, break into two rects
239  point t_size(w_ - link_start.x - (x_ == 0 ? img_width : 0), t_end.y - t_start.y);
240  point link_start2(x_, link_start.y + font::get_max_height(font::SIZE_NORMAL));
241  point t_size2(t_end.x, t_end.y - t_start.y);
242 
243  rect link_rect = {
244  link_start.x,
245  link_start.y,
246  t_size.x,
247  t_size.y,
248  };
249 
250  rect link_rect2 = {
251  link_start2.x,
252  link_start2.y,
253  t_size2.x,
254  t_size2.y,
255  };
256 
257  links_.push_back(std::pair(link_rect, dest));
258  links_.push_back(std::pair(link_rect2, dest));
259 
260  DBG_GUI_RL << "added link at rect 1: " << link_rect;
261  DBG_GUI_RL << "added link at rect 2: " << link_rect2;
262  }
263 }
264 
265 size_t rich_label::get_split_location(std::string text, int img_height) {
266  point wrap_position = get_column_line(point(w_, img_height));
267 
268  size_t len = 0;
269  for (int i = 0; i < wrap_position.y; i++) {
270  len += utf8::size(font::get_text_renderer().get_lines()[i]);
271  }
272  len += wrap_position.x;
273 
274  // break only at word boundary
275  char c;
276  while((c = text.at(len)) != ' ') {
277  len--;
278  }
279 
280  return len;
281 }
282 
284 {
285  // Initialization
286  w_ = (w_ == 0) ? styled_widget::calculate_best_size().x : w_;
287  DBG_GUI_RL << "Width: " << w_;
288  h_ = 0;
289  unparsed_text_ = text;
290  text_dom_.clear();
291  links_.clear();
292  help::topic_text marked_up_text(text);
293  std::vector<std::string> parsed_text = marked_up_text.parsed_text();
294 
295  config* curr_item = nullptr;
297 
298  bool is_image = false;
299  bool floating = false;
300  bool new_text_block = false;
301  bool needs_size_update = true;
302  bool in_table = false;
303  point img_size;
304  unsigned col_width = 0;
305  unsigned max_col_height = 0;
306  prev_blk_height_ = 0;
307  txt_height_ = 0;
308 
309  for (size_t i = 0; i < parsed_text.size(); i++) {
310  bool last_entry = (i == parsed_text.size() - 1);
311  std::string line = parsed_text.at(i);
312 
313  if (!line.empty() && line.at(0) == '[') {
314  config cfg;
315  ::read(cfg, line);
316 
317  if ((child = cfg.optional_child("img"))) {
318 
319  std::string name = child["src"];
320  floating = child["float"].to_bool();
321  std::string align = child["align"];
322 
323  curr_item = &(text_dom_.add_child("image"));
324  add_image(*curr_item, name, align, floating, img_size);
325 
326  is_image = true;
327  new_text_block = true;
328 
329  DBG_GUI_RL << "image: src=" << name << ", size=" << get_image_size(*curr_item);
330 
331  } else {
332 
333  if (is_image && (!floating)) {
334  x_ = 0;
335  (*curr_item)["actions"] = "([set_var('pos_x', 0), set_var('pos_y', pos_y + image_height + padding)])";
336  }
337 
338  if (curr_item == nullptr || new_text_block) {
340  txt_height_ = 0;
341 
342  curr_item = &(text_dom_.add_child("text"));
343  default_text_config(curr_item);
344  new_text_block = false;
345  }
346 
347  // }---------- TEXT TAGS -----------{
348  int tmp_h = get_text_size(*curr_item, w_ - x_).y;
349 
350  if ((child = cfg.optional_child("ref"))) {
351 
352  add_link(*curr_item, child["text"], child["dst"], img_size.x);
353  is_image = false;
354 
355  DBG_GUI_RL << "ref: dst=" << child["dst"];
356 
357  } else if ((child = cfg.optional_child("bold")) || (child = cfg.optional_child("b"))) {
358 
359  add_text_with_attribute(*curr_item, child["text"], "bold");
360  is_image = false;
361 
362  DBG_GUI_RL << "bold: text=" << child["text"];
363 
364  } else if ((child = cfg.optional_child("italic")) || (child = cfg.optional_child("i"))) {
365 
366  add_text_with_attribute(*curr_item, child["text"], "italic");
367  is_image = false;
368 
369  DBG_GUI_RL << "italic: text=" << child["text"];
370 
371  } else if ((child = cfg.optional_child("underline")) || (child = cfg.optional_child("u"))) {
372 
373  add_text_with_attribute(*curr_item, child["text"], "underline");
374  is_image = false;
375 
376  DBG_GUI_RL << "u: text=" << child["text"];
377 
378  } else if ((child = cfg.optional_child("header")) || (child = cfg.optional_child("h"))) {
379 
380  // Header starts in a new line
381 
382  append_if_not_empty(&((*curr_item)["text"]), "\n");
383  append_if_not_empty(&((*curr_item)["attr_name"]), ",");
384  append_if_not_empty(&((*curr_item)["attr_start"]), ",");
385  append_if_not_empty(&((*curr_item)["attr_end"]), ",");
386  append_if_not_empty(&((*curr_item)["attr_data"]), ",");
387 
388  std::stringstream header_text;
389  header_text << child["text"].str() + "\n";
390  std::vector<std::string> attrs = {"color", "size"};
391  std::vector<std::string> attr_data;
392  attr_data.push_back(font::TITLE_COLOR.to_hex_string().substr(1));
393  attr_data.push_back(std::to_string(font::SIZE_TITLE));
394 
395  add_text_with_attributes((*curr_item), header_text.str(), attrs, attr_data);
396 
397  is_image = false;
398 
399  DBG_GUI_RL << "h: text=" << child["text"];
400 
401  } else if ((child = cfg.optional_child("span")) || (child = cfg.optional_child("format"))) {
402 
403  std::vector<std::string> attrs;
404  std::vector<std::string> attr_data;
405 
406  DBG_GUI_RL << "span/format: text=" << child["text"];
407  DBG_GUI_RL << "attributes:";
408 
409  for (const auto& attr : child.value().attribute_range()) {
410  if (attr.first != "text") {
411  attrs.push_back(attr.first);
412  attr_data.push_back(attr.second);
413  DBG_GUI_RL << attr.first << "=" << attr.second;
414  }
415  }
416 
417  add_text_with_attributes((*curr_item), child["text"], attrs, attr_data);
418  is_image = false;
419 
420  // }---------- TABLE TAGS -----------{
421  } else if ((child = cfg.optional_child("table"))) {
422 
423  in_table = true;
424 
425  // setup column width
426  unsigned columns = child["col"].to_int();
427  unsigned width = child["width"].to_int();
428  width = width > 0 ? width : w_;
429  col_width = width/columns;
430 
431  // start on a new line
432  (*curr_item)["actions"] = boost::str(boost::format("([set_var('pos_x', 0), set_var('pos_y', pos_y + if(ih > text_height, ih, text_height)), set_var('tw', width - pos_x - %d), set_var('ih', 0)])") % col_width);
433  x_ = 0;
434  prev_blk_height_ += std::max(img_size.y, get_text_size(*curr_item, w_ - img_size.x).y);
435  txt_height_ = 0;
436 
437  new_text_block = true;
438 
439  DBG_GUI_RL << "start table : " << "col=" << columns;
440  DBG_GUI_RL << "col_width : " << col_width;
441 
442  } else if (cfg.optional_child("jump")) {
443 
444  if (col_width > 0) {
445 
446  max_col_height = std::max(max_col_height, txt_height_);
447  max_col_height = std::max(max_col_height, static_cast<unsigned>(img_size.y));
448  txt_height_ = 0;
449  x_ += col_width;
450 
451  DBG_GUI_RL << "jump to next column";
452 
453  (*curr_item)["actions"] = boost::str(boost::format("([set_var('pos_x', pos_x + %d), set_var('tw', width - pos_x - %d)])") % col_width % col_width);
454 
455  if (!is_image) {
456  new_text_block = true;
457  }
458  }
459 
460  } else if (cfg.optional_child("break") || cfg.optional_child("br")) {
461 
462  if (in_table) {
463 
464  max_col_height = std::max(max_col_height, txt_height_);
465  max_col_height = std::max(max_col_height, static_cast<unsigned>(img_size.y));
466 
467  //linebreak
468  x_ = 0;
469  prev_blk_height_ += max_col_height;
470  max_col_height = 0;
471  txt_height_ = 0;
472 
473  (*curr_item)["actions"] = boost::str(boost::format("([set_var('pos_x', 0), set_var('pos_y', pos_y + %d + %d), set_var('tw', width - pos_x - %d)])") % max_col_height % padding_ % col_width);
474 
475  }
476 
477  DBG_GUI_RL << "linebreak: " << (in_table ? max_col_height : w_);
478 
479  if (!is_image) {
480  new_text_block = true;
481  }
482 
483  } else if (cfg.optional_child("endtable")) {
484 
485  DBG_GUI_RL << "end table: " << max_col_height;
486  max_col_height = std::max(max_col_height, txt_height_);
487  max_col_height = std::max(max_col_height, static_cast<unsigned>(img_size.y));
488  (*curr_item)["actions"] = boost::str(boost::format("([set_var('pos_x', 0), set_var('pos_y', pos_y + %d), set_var('tw', 0)])") % max_col_height);
489 
490  //linebreak and reset col_width
491  col_width = 0;
492  x_ = 0;
493  prev_blk_height_ += max_col_height;
494  max_col_height = 0;
495  txt_height_ = 0;
496 
497  if (!last_entry) {
498  new_text_block = true;
499  }
500 
501  in_table = false;
502  }
503 
504  if (needs_size_update) {
505  int ah = get_text_size(*curr_item, w_ - x_).y;
506  // update text size and widget height
507  if (tmp_h > ah) {
508  tmp_h = 0;
509  }
510 
511  txt_height_ += ah - tmp_h;
512  }
513  }
514 
515  } else if (!line.empty()) {
516  DBG_GUI_RL << "text: text=" << line.substr(1, 20) << "...";
517 
518  // Start the text in a new paragraph if a newline follows after an image
519  if (is_image && (!floating)) {
520  if (line.at(0) == '\n') {
521  x_ = 0;
522  (*curr_item)["actions"] = "([set_var('pos_x', 0), set_var('pos_y', pos_y + image_height + padding)])";
523  line = line.substr(1, line.size());
524  needs_size_update = true;
525  } else {
526  needs_size_update = false;
527  }
528  }
529 
530  if (curr_item == nullptr || new_text_block) {
532  txt_height_ = 0;
533 
534  curr_item = &(text_dom_.add_child("text"));
535  default_text_config(curr_item);
536  new_text_block = false;
537  }
538 
539  (*curr_item)["font_size"] = font::SIZE_NORMAL;
540 
541  int tmp_h = get_text_size(*curr_item, w_ - x_).y;
542 
543  (*curr_item)["text"] = (*curr_item)["text"].str() + line;
544 
545  point text_size;
546  text_size.x = get_text_size(*curr_item, w_ - (x_ == 0 ? img_size.x : x_)).x - x_;
547  text_size.y = get_text_size(*curr_item, w_ - (x_ == 0 ? img_size.x : x_)).y;
548 
549  if ( floating && (img_size.y > 0) && (text_size.y > img_size.y) ) {
550  DBG_GUI_RL << "wrap start";
551 
552  size_t len = get_split_location((*curr_item)["text"].str(), img_size.y);
553  t_string* removed_part = new t_string((*curr_item)["text"].str().substr(len+1));
554  (*curr_item)["text"] = (*curr_item)["text"].str().substr(0, len);
555  (*curr_item)["actions"] = "([set_var('pos_x', 0), set_var('ww', 0), set_var('pos_y', pos_y + text_height)])";
556 
557  // New text block
558  x_ = 0;
559  prev_blk_height_ += img_size.y + padding_;
560  // TODO excess line gets added, so that needs to be compensated
561  txt_height_ = 0;
562  img_size = point(0,0);
563  floating = false;
564 
565  curr_item = &(text_dom_.add_child("text"));
566  default_text_config(curr_item);
567 
568  add_text_with_attribute(*curr_item, *removed_part);
569 
570  } else if ((img_size.y > 0) && (text_size.y < img_size.y)) {
571  DBG_GUI_RL << "no wrap";
572  if (is_image) {
573  (*curr_item)["actions"] = "([set_var('pos_y', pos_y + image_height)])";
574  } else {
575  (*curr_item)["actions"] = "([set_var('pos_y', pos_y + text_height)])";
576  }
577  }
578 
579  int ah = get_text_size(*curr_item, w_ - x_).y;
580  // update text size and widget height
581  if (tmp_h > ah) {
582  tmp_h = 0;
583  }
584 
585  txt_height_ += ah - tmp_h;
586 
587  is_image = false;
588  }
589 
590  // Height Update
591  if (!is_image && !floating && img_size.y > 0) {
592  if (needs_size_update) {
593  prev_blk_height_ += img_size.y;
594  }
595  img_size = point(0,0);
596  }
597 
598 
599  DBG_GUI_RL << "Item :" << curr_item->debug();
600  DBG_GUI_RL << "X: " << x_;
601  DBG_GUI_RL << "Prev block height: " << prev_blk_height_ << " Current text block height: " << txt_height_;
602  DBG_GUI_RL << "Height: " << h_;
604 
605  // reset all variables to zero, otherwise they grow infinitely
606  if (last_entry) {
607  if (static_cast<unsigned>(img_size.y) > h_) {
608  h_ = img_size.y;
609  }
611 
612  config& break_cfg = text_dom_.add_child("text");
613  default_text_config(&break_cfg);
614  break_cfg["text"] = " ";
615  break_cfg["actions"] = "([set_var('pos_x', 0), set_var('pos_y', 0), set_var('img_x', 0), set_var('img_y', 0)])";
616  }
617 
618  DBG_GUI_RL << "-----------";
619 
620  } // for loop ends
621 
622 } // function ends
623 
625  if (txt_ptr != nullptr) {
626  (*txt_ptr)["text"] = text;
627  (*txt_ptr)["font_size"] = font::SIZE_NORMAL;
628  (*txt_ptr)["text_alignment"] = encode_text_alignment(get_text_alignment());
629  (*txt_ptr)["x"] = "(pos_x)";
630  (*txt_ptr)["y"] = "(pos_y)";
631  (*txt_ptr)["w"] = "(text_width)";
632  (*txt_ptr)["h"] = "(text_height)";
633  // tw -> table width, used for wrapping text inside table cols
634  // ww -> wrap width, used for wrapping around floating image
635  (*txt_ptr)["maximum_width"] = "(width - pos_x - ww - tw)";
636  (*txt_ptr)["actions"] = "([set_var('pos_y', pos_y+text_height)])";
637  }
638 }
639 
641 {
642  for(canvas& tmp : get_canvases()) {
643  tmp.set_variable("pos_x", wfl::variant(0));
644  tmp.set_variable("pos_y", wfl::variant(0));
645  tmp.set_variable("img_x", wfl::variant(0));
646  tmp.set_variable("img_y", wfl::variant(0));
647  tmp.set_variable("tw", wfl::variant(0));
648  tmp.set_variable("ww", wfl::variant(0));
649  tmp.set_variable("padding", wfl::variant(padding_));
650  // Disable ellipsization so that text wrapping can work
651  tmp.set_variable("text_wrap_mode", wfl::variant(PANGO_ELLIPSIZE_NONE));
652  tmp.set_cfg(text_dom_, true);
653  tmp.set_variable("text_alpha", wfl::variant(text_alpha_));
654  }
655 }
656 
657 void rich_label::set_text_alpha(unsigned short alpha)
658 {
659  if(alpha != text_alpha_) {
660  text_alpha_ = alpha;
661  update_canvas();
662  queue_redraw();
663  }
664 }
665 
666 void rich_label::set_active(const bool active)
667 {
668  if(get_active() != active) {
669  set_state(active ? ENABLED : DISABLED);
670  }
671 }
672 
673 void rich_label::set_link_aware(bool link_aware)
674 {
675  if(link_aware != link_aware_) {
676  link_aware_ = link_aware;
677  update_canvas();
678  queue_redraw();
679  }
680 }
681 
683 {
684  if(color != link_color_) {
685  link_color_ = color;
686  update_canvas();
687  queue_redraw();
688  }
689 }
690 
692 {
693  if(state != state_) {
694  state_ = state;
695  queue_redraw();
696  }
697 }
698 
700 {
701  DBG_GUI_E << "rich_label click";
702 
703  if(!get_link_aware()) {
704  return; // without marking event as "handled"
705  }
706 
707  point mouse = get_mouse_position();
708 
709  mouse.x -= get_x();
710  mouse.y -= get_y();
711 
712  DBG_GUI_RL << "(mouse)" << mouse.x << "," << mouse.y;
713  DBG_GUI_RL << "link count :" << links_.size();
714 
715  for (const auto& entry : links_) {
716  DBG_GUI_RL << "link [" << entry.first.x << "," << entry.first.y << ","
717  << entry.first.x + entry.first.w << "," << entry.first.y + entry.first.h << "]";
718 
719  if (entry.first.contains(mouse)) {
720  DBG_GUI_RL << "Clicked link! dst = " << entry.second;
721  if (link_handler_) {
722  link_handler_(entry.second);
723  } else {
724  DBG_GUI_RL << "No registered link handler found";
725  }
726 
727  }
728  }
729 
730  handled = true;
731 }
732 
734 {
735  DBG_GUI_E << "rich_label mouse motion";
736 
737  if(!get_link_aware()) {
738  return; // without marking event as "handled"
739  }
740 
741  point mouse = coordinate;
742 
743  mouse.x -= get_x();
744  mouse.y -= get_y();
745 
746  for (const auto& entry : links_) {
747  if (entry.first.contains(mouse)) {
748  update_mouse_cursor(true);
749  handled = true;
750  return;
751  }
752  }
753 
754  update_mouse_cursor(false);
755 }
756 
758 {
759  DBG_GUI_E << "rich_label mouse leave";
760 
761  if(!get_link_aware()) {
762  return; // without marking event as "handled"
763  }
764 
765  // We left the widget, so just unconditionally reset the cursor
766  update_mouse_cursor(false);
767 
768  handled = true;
769 }
770 
772 {
773  // Someone else may set the mouse cursor for us to something unusual (e.g.
774  // the WAIT cursor) so we ought to mess with that only if it's set to
775  // NORMAL or HYPERLINK.
776 
777  if(enable && cursor::get() == cursor::NORMAL) {
779  } else if(!enable && cursor::get() == cursor::HYPERLINK) {
781  }
782 }
783 
784 // }---------- DEFINITION ---------{
785 
788 {
789  DBG_GUI_P << "Parsing rich_label " << id;
790 
791  load_resolutions<resolution>(cfg);
792 }
793 
795  : resolution_definition(cfg)
796  , link_color(cfg["link_color"].empty() ? font::YELLOW_COLOR : color_t::from_rgba_string(cfg["link_color"].str()))
797 {
798  // Note the order should be the same as the enum state_t is rich_label.hpp.
799  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_enabled", missing_mandatory_wml_tag("rich_label_definition][resolution", "state_enabled")));
800  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_disabled", missing_mandatory_wml_tag("rich_label_definition][resolution", "state_disabled")));
801 }
802 
803 // }---------- BUILDER -----------{
804 
805 namespace implementation
806 {
807 
808 builder_rich_label::builder_rich_label(const config& cfg)
809  : builder_styled_widget(cfg)
810  , text_alignment(decode_text_alignment(cfg["text_alignment"]))
811  , link_aware(cfg["link_aware"].to_bool(true))
812  , width(cfg["width"].to_int(500))
813 {
814 }
815 
816 std::unique_ptr<widget> builder_rich_label::build() const
817 {
818  auto lbl = std::make_unique<rich_label>(*this);
819 
820  const auto conf = lbl->cast_config_to<rich_label_definition>();
821  assert(conf);
822 
823  lbl->set_text_alignment(text_alignment);
824  lbl->set_link_aware(link_aware);
825  lbl->set_link_color(conf->link_color);
826  lbl->set_width(width);
827  lbl->set_label(lbl->get_label());
828 
829  DBG_GUI_G << "Window builder: placed rich_label '" << id << "' with definition '"
830  << definition << "'.";
831 
832  return lbl;
833 }
834 
835 } // namespace implementation
836 
837 // }------------ END --------------
838 
839 } // namespace gui2
std::size_t w_
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:159
std::string debug() const
Definition: config.cpp:1244
void clear()
Definition: config.cpp:831
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:385
config & add_child(config_key_type key)
Definition: config.cpp:441
A simple canvas which can be drawn upon.
Definition: canvas.hpp:45
A rich_label takes marked up text and shows it correctly formatted and wrapped but no scrollbars are ...
Definition: rich_label.hpp:40
void signal_handler_mouse_motion(bool &handled, const point &coordinate)
Mouse motion signal handler: checks if the cursor is on a hyperlink.
Definition: rich_label.cpp:733
void append_if_not_empty(config_attribute_value *key, std::string suffix)
Definition: rich_label.hpp:212
state_t state_
Current state of the widget.
Definition: rich_label.hpp:149
void set_state(const state_t state)
Definition: rich_label.cpp:691
void add_text_with_attribute(config &curr_item, std::string text, std::string attr_name="", std::string extra_data="")
Definition: rich_label.cpp:101
size_t get_split_location(std::string text, int img_height)
Definition: rich_label.cpp:265
virtual bool get_active() const override
Gets the active state of the styled_widget.
Definition: rich_label.hpp:68
std::function< void(std::string)> link_handler_
Definition: rich_label.hpp:229
wfl::map_formula_callable setup_text_renderer(config text_cfg, unsigned width=0)
Definition: rich_label.cpp:74
virtual void update_canvas() override
Updates the canvas(ses).
Definition: rich_label.cpp:640
unsigned w_
Width and height of the canvas.
Definition: rich_label.hpp:193
void add_link(config &curr_item, std::string name, std::string dest, int img_width)
Definition: rich_label.cpp:202
void default_text_config(config *txt_ptr, t_string text="")
template for canvas text config
Definition: rich_label.cpp:624
t_string unparsed_text_
The unparsed/raw text.
Definition: rich_label.hpp:186
void signal_handler_mouse_leave(bool &handled)
Mouse leave signal handler: checks if the cursor left a hyperlink.
Definition: rich_label.cpp:757
std::vector< std::pair< rect, std::string > > links_
link variables and functions
Definition: rich_label.hpp:227
std::unique_ptr< image_shape > ishape_
Definition: rich_label.hpp:190
unsigned short text_alpha_
Definition: rich_label.hpp:174
point get_image_size(config img_cfg)
Definition: rich_label.cpp:92
point get_xy_from_offset(const unsigned offset) const
Definition: rich_label.hpp:236
virtual void set_active(const bool active) override
Sets the styled_widget's state.
Definition: rich_label.cpp:666
void add_image(config &curr_item, std::string name, std::string align, bool floating, point &img_size)
Definition: rich_label.cpp:146
point get_text_size(config text_cfg, unsigned width=0)
size calculation functions
Definition: rich_label.cpp:87
point get_column_line(const point &position) const
Definition: rich_label.hpp:231
unsigned prev_blk_height_
Height of all previous blocks, combined.
Definition: rich_label.hpp:202
state_t
Possible states of the widget.
Definition: rich_label.hpp:136
void signal_handler_left_button_click(bool &handled)
Left click signal handler: checks if we clicked on a hyperlink.
Definition: rich_label.cpp:699
unsigned txt_height_
Height of current text block.
Definition: rich_label.hpp:199
bool link_aware_
Whether the rich_label is link aware, rendering links with special formatting and handling click even...
Definition: rich_label.hpp:165
void set_link_color(const color_t &color)
Definition: rich_label.cpp:682
std::unique_ptr< text_shape > tshape_
shapes used for size calculation
Definition: rich_label.hpp:189
unsigned padding_
Padding.
Definition: rich_label.hpp:196
void set_link_aware(bool l)
Definition: rich_label.cpp:673
virtual bool get_link_aware() const override
Returns whether the label should be link_aware, in in rendering and in searching for links with get_l...
Definition: rich_label.hpp:56
void add_text_with_attributes(config &curr_item, std::string text, std::vector< std::string > attr_names, std::vector< std::string > extra_data)
Definition: rich_label.cpp:123
void set_text_alpha(unsigned short alpha)
Definition: rich_label.cpp:657
color_t link_color_
What color links will be rendered in.
Definition: rich_label.hpp:170
void set_label(const t_string &text) override
Definition: rich_label.cpp:283
void update_mouse_cursor(bool enable)
Implementation detail for (re)setting the hyperlink cursor.
Definition: rich_label.cpp:771
config text_dom_
structure tree of the marked up text after parsing
Definition: rich_label.hpp:183
PangoAlignment get_text_alignment() const
std::vector< canvas > & get_canvases()
virtual point calculate_best_size() const override
See widget::calculate_best_size.
void queue_redraw()
Indicates that this widget should be redrawn.
Definition: widget.cpp:454
int get_x() const
Definition: widget.cpp:316
int get_y() const
Definition: widget.cpp:321
The text displayed in a topic.
Definition: help_impl.hpp:83
const std::vector< std::string > & parsed_text() const
Definition: help_impl.cpp:386
variant query_value(const std::string &key) const
Definition: callable.hpp:50
map_formula_callable & add(const std::string &key, const variant &value)
Definition: callable.hpp:253
int as_int() const
Definition: variant.cpp:291
constexpr uint8_t ALPHA_OPAQUE
Definition: color.hpp:45
std::size_t i
Definition: function.cpp:965
Define the common log macros for the gui toolkit.
#define DBG_GUI_G
Definition: log.hpp:41
#define DBG_GUI_P
Definition: log.hpp:66
#define DBG_GUI_E
Definition: log.hpp:35
Standard logging facilities (interface).
CURSOR_TYPE get()
Definition: cursor.cpp:216
@ NORMAL
Definition: cursor.hpp:28
@ HYPERLINK
Definition: cursor.hpp:28
void set(CURSOR_TYPE type)
Use the default parameter to reset cursors.
Definition: cursor.cpp:176
void point(int x, int y)
Draw a single point.
Definition: draw.cpp:202
void line(int from_x, int from_y, int to_x, int to_y)
Draw a line.
Definition: draw.cpp:180
EXIT_STATUS start(bool clear_id, const std::string &filename, bool take_screenshot, const std::string &screenshot_filename)
Main interface for launching the editor from the title screen.
Collection of helper functions relating to Pango formatting.
int get_max_height(unsigned size, font::family_class fclass, pango_text::FONT_STYLE style)
Returns the maximum glyph height of a font, in pixels.
Definition: text.cpp:1124
const color_t YELLOW_COLOR
pango_text & get_text_renderer()
Returns a reference to a static pango_text object.
Definition: text.cpp:1118
constexpr float get_line_spacing_factor()
Definition: text.hpp:557
const color_t TITLE_COLOR
const int SIZE_TITLE
Definition: constants.cpp:31
const int SIZE_NORMAL
Definition: constants.cpp:20
Generic file dialog.
point get_mouse_position()
Returns the current mouse position.
Definition: helper.cpp:143
PangoAlignment decode_text_alignment(const std::string &alignment)
Converts a text alignment string to a text alignment.
Definition: helper.cpp:89
std::string encode_text_alignment(const PangoAlignment alignment)
Converts a text alignment to its string representation.
Definition: helper.cpp:104
Contains the implementation details for lexical_cast and shouldn't be used directly.
map_location coordinate
Contains an x and y coordinate used for starting positions in maps.
std::size_t size(const std::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:85
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
Desktop environment interaction functions.
#define REGISTER_WIDGET(id)
Wrapper for REGISTER_WIDGET3.
static lg::log_domain log_rich_label("gui/widget/rich_label")
#define DBG_GUI_RL
Definition: rich_label.cpp:41
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:623
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:59
std::string to_hex_string() const
Returns the stored color in rrggbb hex format.
Definition: color.cpp:88
virtual std::unique_ptr< widget > build() const override
Definition: rich_label.cpp:816
std::string definition
Parameters for the styled_widget.
std::vector< state_definition > state
rich_label_definition(const config &cfg)
Definition: rich_label.cpp:786
Holds a 2D point.
Definition: point.hpp:25
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:47
mock_char c
std::string missing_mandatory_wml_tag(const std::string &section, const std::string &tag)
Returns a standard message for a missing wml child (tag).
Add a special kind of assert to validate whether the input from WML doesn't contain any problems that...
#define VALIDATE_WML_CHILD(cfg, key, message)