The Battle for Wesnoth  1.19.5+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"
23 #include "gui/dialogs/message.hpp"
24 #include "gui/widgets/settings.hpp"
25 
26 #include "cursor.hpp"
27 #include "desktop/clipboard.hpp"
28 #include "desktop/open.hpp"
29 #include "font/constants.hpp"
30 #include "font/sdl_ttf_compat.hpp"
31 #include "help/help_impl.hpp"
32 #include "gettext.hpp"
33 #include "log.hpp"
34 #include "serialization/markup.hpp"
37 #include "sound.hpp"
38 #include "video.hpp"
39 #include "wml_exception.hpp"
40 
41 #include <functional>
42 #include <string>
43 #include <boost/format.hpp>
44 
45 static lg::log_domain log_rich_label("gui/widget/rich_label");
46 #define DBG_GUI_RL LOG_STREAM(debug, log_rich_label)
47 
48 #define LINK_DEBUG_BORDER false
49 
50 namespace gui2
51 {
52 namespace
53 {
54 using namespace std::string_literals;
55 
56 /** Possible formatting tags, must be the same as those in gui2::text_shape::draw */
57 const std::array format_tags{ "bold"s, "b"s, "italic"s, "i"s, "underline"s, "u"s };
58 }
59 
60 // ------------ WIDGET -----------{
61 
62 REGISTER_WIDGET(rich_label)
63 
64 rich_label::rich_label(const implementation::builder_rich_label& builder)
65  : styled_widget(builder, type())
66  , state_(ENABLED)
67  , can_wrap_(true)
68  , link_aware_(builder.link_aware)
69  , link_color_(font::YELLOW_COLOR)
70  , font_size_(font::SIZE_NORMAL)
71  , can_shrink_(true)
72  , text_alpha_(ALPHA_OPAQUE)
73  , unparsed_text_()
74  , init_w_(builder.width(get_screen_size_variables()))
75  , size_(0, 0)
76  , padding_(5)
77 {
78 }
79 
81  // Set up fake render to calculate text position
82  static wfl::action_function_symbol_table functions;
83  wfl::map_formula_callable variables;
84  variables.add("text", wfl::variant(text_cfg["text"].str()));
85  variables.add("width", wfl::variant(width));
86  variables.add("text_wrap_mode", wfl::variant(PANGO_ELLIPSIZE_NONE));
87  variables.add("fake_draw", wfl::variant(true));
88  gui2::text_shape{text_cfg, functions}.draw(variables);
89  return variables;
90 }
91 
92 point rich_label::get_text_size(config& text_cfg, unsigned width) const {
93  wfl::map_formula_callable variables = setup_text_renderer(text_cfg, width);
94  return {
95  variables.query_value("text_width").as_int(),
96  variables.query_value("text_height").as_int()
97  };
98 }
99 
101  static wfl::action_function_symbol_table functions;
102  wfl::map_formula_callable variables;
103  variables.add("fake_draw", wfl::variant(true));
104  gui2::image_shape{img_cfg, functions}.draw(variables);
105  return {
106  variables.query_value("image_width").as_int(),
107  variables.query_value("image_height").as_int()
108  };
109 }
110 
111 std::pair<size_t, size_t> rich_label::add_text(config& curr_item, std::string text) {
112  auto& attr = curr_item["text"];
113  size_t start = attr.str().size();
114  attr = attr.str() + std::move(text);
115  size_t end = attr.str().size();
116  return { start, end };
117 }
118 
119 void rich_label::add_attribute(config& curr_item, std::string attr_name, size_t start, size_t end, std::string extra_data) {
120  curr_item.add_child("attribute", config{
121  "name" , attr_name,
122  "start" , start,
123  "end" , end == 0 ? curr_item["text"].str().size() : end,
124  "value" , std::move(extra_data)
125  });
126 }
127 
128 std::pair<size_t, size_t> rich_label::add_text_with_attribute(config& curr_item, std::string text, std::string attr_name, std::string extra_data) {
129  const auto [start, end] = add_text(curr_item, std::move(text));
130  add_attribute(curr_item, attr_name, start, end, extra_data);
131  return { start, end };
132 }
133 
134 void rich_label::add_image(config& curr_item, std::string name, std::string align, bool has_prev_image, bool floating) {
135  // TODO: still doesn't cover the case where consecutive inline images have different heights
136  curr_item["name"] = name;
137 
138  if (align.empty()) {
139  align = "left";
140  }
141 
142  if (align == "right") {
143  curr_item["x"] = floating ? "(width - image_width - img_x)" : "(width - image_width - pos_x)";
144  } else if (align == "middle" || align == "center") {
145  // works for single image only
146  curr_item["x"] = floating ? "(img_x + (width - image_width)/2.0)" : "(pos_x + (width - image_width)/2.0)";
147  } else {
148  // left aligned images are default for now
149  curr_item["x"] = floating ? "(img_x)" : "(pos_x)";
150  }
151  curr_item["y"] = (has_prev_image && floating) ? "(img_y + pos_y)" : "(pos_y)";
152  curr_item["h"] = "(image_height)";
153  curr_item["w"] = "(image_width)";
154 
155  std::stringstream actions;
156  actions << "([";
157  if (floating) {
158  if (align == "left") {
159  actions << "set_var('pos_x', image_width + padding)";
160  } else if (align == "right") {
161  actions << "set_var('pos_x', 0)";
162  actions << ",";
163  actions << "set_var('ww', image_width + padding)";
164  }
165 
166  actions << "," << "set_var('img_y', img_y + image_height + padding)";
167  } else {
168  actions << "set_var('pos_x', pos_x + image_width + padding)";
169  // y coordinate is updated later, based on whether a linebreak follows
170  }
171  actions << "])";
172 
173  curr_item["actions"] = actions.str();
174  actions.str("");
175 }
176 
177 void rich_label::add_link(config& curr_item, std::string name, std::string dest, const point& origin, int img_width) {
178  // TODO algorithm needs to be text_alignment independent
179 
180  DBG_GUI_RL << "add_link: " << name << "->" << dest;
181  DBG_GUI_RL << "origin: " << origin;
182  DBG_GUI_RL << "width=" << img_width;
183 
184  point t_start, t_end;
185 
186  setup_text_renderer(curr_item, init_w_ - origin.x - img_width);
187  t_start = origin + get_xy_from_offset(utf8::size(curr_item["text"].str()));
188  DBG_GUI_RL << "link text start:" << t_start;
189 
190  std::string link_text = name.empty() ? dest : name;
191  add_text_with_attribute(curr_item, link_text, "color", link_color_.to_hex_string());
192 
193  setup_text_renderer(curr_item, init_w_ - origin.x - img_width);
194  t_end.x = origin.x + get_xy_from_offset(utf8::size(curr_item["text"].str())).x;
195  DBG_GUI_RL << "link text end:" << t_end;
196 
197  // TODO link after right aligned images
198 
199  // Add link
200  if (t_end.x > t_start.x) {
201  rect link_rect{ t_start, point{t_end.x - t_start.x, font::get_max_height(font_size_) }};
202  links_.emplace_back(link_rect, dest);
203 
204  DBG_GUI_RL << "added link at rect: " << link_rect;
205 
206  } else {
207  //link straddles two lines, break into two rects
208  point t_size(size_.x - t_start.x - (origin.x == 0 ? img_width : 0), t_end.y - t_start.y);
209  point link_start2(origin.x, t_start.y + 1.3*font::get_max_height(font_size_));
210  point t_size2(t_end.x, t_end.y - t_start.y);
211 
212  rect link_rect{ t_start, point{ t_size.x, font::get_max_height(font_size_) } };
213  rect link_rect2{ link_start2, point{ t_size2.x, font::get_max_height(font_size_) } };
214 
215  links_.emplace_back(link_rect, dest);
216  links_.emplace_back(link_rect2, dest);
217 
218  DBG_GUI_RL << "added link at rect 1: " << link_rect;
219  DBG_GUI_RL << "added link at rect 2: " << link_rect2;
220  }
221 }
222 
223 size_t rich_label::get_split_location(std::string_view text, const point& pos) {
224 
225  size_t len = get_offset_from_xy(pos);
226  len = (len > text.size()-1) ? text.size()-1 : len;
227 
228  // break only at word boundary
229  char c;
230  while(!std::isspace(c = text[len])) {
231  len--;
232  if (len == 0) {
233  break;
234  }
235  }
236 
237  return len;
238 }
239 
240 std::vector<std::string> rich_label::split_in_width(const std::string &s, const int font_size, const unsigned width) {
241  std::vector<std::string> res;
242  try {
243  const std::string& first_line = font::pango_word_wrap(s, font_size, width, -1, 1, true);
244  res.push_back(first_line);
245  if(s.size() > first_line.size()) {
246  res.push_back(s.substr(first_line.size()));
247  }
248  } catch (utf8::invalid_utf8_exception&) {
249  throw markup::parse_error (_("corrupted original file"));
250  }
251 
252  return res;
253 }
254 
255 void rich_label::set_topic(const help::topic* topic) {
257  std::tie(text_dom_, size_) = get_parsed_text(topic->text.parsed_text(), point(0,0), init_w_, true);
258 }
259 
260 void rich_label::set_label(const t_string& text) {
262  unparsed_text_ = text;
263  help::topic_text marked_up_text(text);
264  std::tie(text_dom_, size_) = get_parsed_text(marked_up_text.parsed_text(), point(0,0), init_w_, true);
265 }
266 
267 std::pair<config, point> rich_label::get_parsed_text(
268  const config& parsed_text,
269  const point& origin,
270  const unsigned init_width,
271  const bool finalize)
272 {
273  // Initial width
274  DBG_GUI_RL << "Initial width: " << init_width;
275 
276  // Initialization
277  unsigned x = 0;
278  unsigned prev_blk_height = origin.y;
279  unsigned text_height = 0;
280  unsigned h = 0;
281  unsigned w = 0;
282 
283  if (finalize) {
284  links_.clear();
285  }
286 
287  config text_dom;
288  config* curr_item = nullptr;
289  config* remaining_item = nullptr;
290 
291  bool is_text = false;
292  bool is_image = false;
293  bool is_float = false;
294  bool wrap_mode = false;
295  bool new_text_block = false;
296 
297  point img_size;
298  point float_size;
299 
300  DBG_GUI_RL << parsed_text.debug();
301 
302  for(const auto [key, child] : parsed_text.all_children_view()) {
303  if(key == "img") {
304  std::string name = child["src"];
305  std::string align = child["align"];
306  bool is_curr_float = child["float"].to_bool(false);
307 
308  curr_item = &(text_dom.add_child("image"));
309  add_image(*curr_item, name, align, is_image, is_curr_float);
310  const point& curr_img_size = get_image_size(*curr_item);
311 
312  if (is_curr_float) {
313  x = (align == "left") ? float_size.x : 0;
314  float_size.x = curr_img_size.x + padding_;
315  float_size.y += curr_img_size.y;
316  } else {
317  img_size.x += curr_img_size.x + padding_;
318  x = img_size.x;
319  img_size.y = std::max(img_size.y, curr_img_size.y);
320  if (!is_image || (is_image && is_float)) {
321  prev_blk_height += curr_img_size.y;
322  float_size.y -= curr_img_size.y;
323  }
324  }
325 
326  w = std::max(w, x);
327 
328  if(is_curr_float) {
329  wrap_mode = true;
330  }
331 
332  is_image = true;
333  is_float = is_curr_float;
334  is_text = false;
335  new_text_block = true;
336 
337  DBG_GUI_RL << "image: src=" << name << ", size=" << curr_img_size;
338  DBG_GUI_RL << "wrap mode: " << wrap_mode << ", floating: " << is_float;
339 
340  } else if(key == "table") {
341  if (curr_item == nullptr) {
342  curr_item = &(text_dom.add_child("text"));
343  default_text_config(curr_item);
344  new_text_block = false;
345  }
346 
347  // table doesn't support floating images alongside
348  img_size = point(0,0);
349  float_size = point(0,0);
350  x = origin.x;
351  prev_blk_height += text_height;
352  text_height = 0;
353 
354  // init table vars
355  unsigned col_idx = 0;
356  unsigned rows = child.child_count("row");
357  unsigned columns = 1;
358  if (rows > 0) {
359  columns = child.mandatory_child("row").child_count("col");
360  }
361  columns = (columns == 0) ? 1 : columns;
362  unsigned width = child["width"].to_int(init_width);
363  unsigned col_x = 0;
364  unsigned row_y = prev_blk_height;
365  unsigned max_row_height = 0;
366  std::vector<unsigned> col_widths(columns, 0);
367 
368  // start on a new line
369  (*curr_item)["actions"] = boost::str(boost::format("([set_var('pos_x', 0), set_var('pos_y', %d), set_var('tw', width - pos_x - %d)])") % row_y % col_widths[col_idx]);
370 
371  is_text = false;
372  new_text_block = true;
373  is_image = false;
374 
375  DBG_GUI_RL << __LINE__ << "start table : " << "row= " << rows << " col=" << columns << " width=" << width;
376 
377  // optimal col width calculation
378  for(const config& row : child.child_range("row")) {
379  col_x = 0;
380  col_idx = 0;
381 
382  for(const config& col : row.child_range("col")) {
383  config col_cfg;
384  col_cfg.append_children(col);
385 
386  config& col_txt_cfg = col_cfg.add_child("text");
387  col_txt_cfg.append_attributes(col);
388 
389  // attach data
390  auto links = links_;
391  const auto& [table_elem, size] = get_parsed_text(col_cfg, point(col_x, row_y), width/columns);
392  links_ = links;
393  col_widths[col_idx] = std::max(col_widths[col_idx], static_cast<unsigned>(size.x));
394  col_widths[col_idx] = std::min(col_widths[col_idx], width/columns);
395 
396  col_x += width/columns;
397  col_idx++;
398  }
399 
400  row_y += max_row_height + padding_;
401  }
402 
403  // table layouting
404  row_y = prev_blk_height;
405  for(const config& row : child.child_range("row")) {
406  col_x = 0;
407  col_idx = 0;
408  max_row_height = 0;
409 
410  for(const config& col : row.child_range("col")) {
411  config col_cfg;
412  col_cfg.append_children(col);
413 
414  config& col_txt_cfg = col_cfg.add_child("text");
415  col_txt_cfg.append_attributes(col);
416 
417  // attach data
418  auto [table_elem, size] = get_parsed_text(col_cfg, point(col_x, row_y), col_widths[col_idx]);
419  text_dom.append(std::move(table_elem));
420 
421  // column post-processing
422  max_row_height = std::max(max_row_height, static_cast<unsigned>(size.y));
423 
424  col_x += col_widths[col_idx] + 2 * padding_;
425  auto [_, end_cfg] = text_dom.all_children_view().back();
426  end_cfg["actions"] = boost::str(boost::format("([set_var('pos_x', %d), set_var('pos_y', %d), set_var('tw', width - %d - %d)])") % col_x % row_y % col_x % (width/columns));
427 
428  DBG_GUI_RL << "jump to next column";
429 
430  if (!is_image) {
431  new_text_block = true;
432  }
433  is_image = false;
434  col_idx++;
435  }
436 
437  row_y += max_row_height + padding_;
438  auto [_, end_cfg] = text_dom.all_children_view().back();
439  end_cfg["actions"] = boost::str(boost::format("([set_var('pos_x', 0), set_var('pos_y', %d), set_var('tw', width - %d - %d)])") % row_y % col_x % col_widths[columns-1]);
440  DBG_GUI_RL << "row height: " << max_row_height;
441  }
442 
443  prev_blk_height = row_y;
444  text_height = 0;
445 
446  auto [_, end_cfg] = text_dom.all_children_view().back();
447  end_cfg["actions"] = boost::str(boost::format("([set_var('pos_x', 0), set_var('pos_y', %d), set_var('tw', 0)])") % row_y);
448 
449  is_image = false;
450  is_text = false;
451 
452  x = origin.x;
453  col_x = 0;
454  row_y = 0;
455  max_row_height = 0;
456 
457  } else if(key == "break" || key == "br") {
458  if (curr_item == nullptr) {
459  curr_item = &(text_dom.add_child("text"));
460  default_text_config(curr_item);
461  new_text_block = false;
462  }
463 
464  // TODO correct height update
465  if (is_image && !is_float) {
466  prev_blk_height += padding_;
467  (*curr_item)["actions"] = "([set_var('pos_x', 0), set_var('pos_y', pos_y + image_height + padding)])";
468  } else {
469  add_text_with_attribute(*curr_item, "\n");
470  }
471 
472  x = origin.x;
473  is_image = false;
474  img_size = point(0,0);
475 
476  DBG_GUI_RL << "linebreak";
477 
478  if (!is_image) {
479  new_text_block = true;
480  }
481  is_text = false;
482 
483  } else {
484  std::string line = child["text"];
485 
486  if (!finalize && line.empty()) {
487  continue;
488  }
489 
490  config part2_cfg;
491  if (is_image && (!is_float)) {
492  if (!line.empty() && line.at(0) == '\n') {
493  x = origin.x;
494  prev_blk_height += padding_;
495  (*curr_item)["actions"] = "([set_var('pos_x', 0), set_var('pos_y', pos_y + image_height + padding)])";
496  line = line.substr(1);
497  } else if (!line.empty() && line.at(0) != '\n') {
498  std::vector<std::string> parts = split_in_width(line, font_size_, (init_width-x));
499  // First line
500  if (!parts.front().empty()) {
501  line = parts.front();
502  }
503 
504  std::string& part2 = parts.back();
505  if (!part2.empty() && parts.size() > 1) {
506  if (part2[0] == '\n') {
507  part2 = part2.substr(1);
508  }
509 
510  part2_cfg.add_child("text")["text"] = parts.back();
511  part2_cfg = get_parsed_text(part2_cfg, point(0, prev_blk_height), init_width, false).first;
512  remaining_item = &part2_cfg;
513  }
514 
515  if (parts.size() == 1) {
516  prev_blk_height -= img_size.y;
517  }
518  } else {
519  prev_blk_height -= img_size.y;
520  }
521  }
522 
523  if (curr_item == nullptr || new_text_block) {
524  if (curr_item != nullptr) {
525  // table will calculate this by itself, no need to calculate here
526  prev_blk_height += text_height;
527  text_height = 0;
528  }
529 
530  curr_item = &(text_dom.add_child("text"));
531  default_text_config(curr_item);
532  new_text_block = false;
533  }
534 
535  // }---------- TEXT TAGS -----------{
536  int tmp_h = get_text_size(*curr_item, init_width - (x == 0 ? float_size.x : x)).y;
537 
538  if (is_text && key == "text") {
539  add_text_with_attribute(*curr_item, "\n\n");
540  }
541  is_text = false;
542 
543  if(key == "ref") {
544 
545  add_link(*curr_item, line, child["dst"], point(x + origin.x, prev_blk_height), float_size.x);
546  is_image = false;
547 
548  DBG_GUI_RL << "ref: dst=" << child["dst"];
549 
550  } else if(std::find(format_tags.begin(), format_tags.end(), key) != format_tags.end()) {
551 
552  add_text_with_attribute(*curr_item, line, key);
553  config parsed_children = get_parsed_text(child, point(x, prev_blk_height), init_width).first;
554 
555  for (const auto [parsed_key, parsed_cfg] : parsed_children.all_children_view()) {
556  if (parsed_key == "text") {
557  const auto [start, end] = add_text(*curr_item, parsed_cfg["text"]);
558  for (const config& attr : parsed_cfg.child_range("attribute")) {
559  add_attribute(*curr_item, attr["name"], start + attr["start"].to_int(), start + attr["end"].to_int(), attr["value"]);
560  }
561  add_attribute(*curr_item, key, start, end);
562  } else {
563  text_dom.add_child(parsed_key, parsed_cfg);
564  }
565  }
566 
567  is_image = false;
568 
569  DBG_GUI_RL << key << ": text=" << gui2::debug_truncate(line);
570 
571  } else if(key == "header" || key == "h") {
572 
573  const auto [start, end] = add_text(*curr_item, line);
574  add_attribute(*curr_item, "face", start, end, "serif");
575  add_attribute(*curr_item, "color", start, end, font::string_to_color("white").to_hex_string());
576  add_attribute(*curr_item, "size", start, end, std::to_string(font::SIZE_TITLE - 2));
577 
578  is_image = false;
579 
580  DBG_GUI_RL << "h: text=" << line;
581 
582  } else if(key == "character_entity") {
583  line = "&" + child["name"].str() + ";";
584 
585  const auto [start, end] = add_text(*curr_item, line);
586  add_attribute(*curr_item, "face", start, end, "monospace");
587  add_attribute(*curr_item, "color", start, end, font::string_to_color("red").to_hex_string());
588 
589  is_image = false;
590 
591  DBG_GUI_RL << "entity: text=" << line;
592 
593  } else if(key == "span" || key == "format") {
594 
595  const auto [start, end] = add_text(*curr_item, line);
596  DBG_GUI_RL << "span/format: text=" << line;
597  DBG_GUI_RL << "attributes:";
598 
599  for (const auto& [key, value] : child.attribute_range()) {
600  if (key != "text") {
601  add_attribute(*curr_item, key, start, end, value);
602  DBG_GUI_RL << key << "=" << value;
603  }
604  }
605 
606  is_image = false;
607 
608  } else if (key == "text") {
609 
610  DBG_GUI_RL << "text: text=" << gui2::debug_truncate(line) << "...";
611 
612  add_text(*curr_item, line);
613 
614  point text_size = get_text_size(*curr_item, init_width - (x == 0 ? float_size.x : x));
615  text_size.x -= x;
616 
617  is_text = true;
618 
619  if (wrap_mode && (float_size.y > 0) && (text_size.y > float_size.y)) {
620  DBG_GUI_RL << "wrap start";
621 
622  size_t len = get_split_location((*curr_item)["text"].str(), point(init_width - float_size.x, float_size.y * video::get_pixel_scale()));
623  DBG_GUI_RL << "wrap around area: " << float_size;
624 
625  // first part of the text
626  std::string removed_part = (*curr_item)["text"].str().substr(len+1);
627  (*curr_item)["text"] = (*curr_item)["text"].str().substr(0, len);
628  (*curr_item)["maximum_width"] = init_width - float_size.x;
629  (*curr_item)["actions"] = boost::str(boost::format("([set_var('pos_x', 0), set_var('ww', 0), set_var('pos_y', pos_y + text_height + %d)])") % (0.3*font::get_max_height(font_size_)));
630 
631  // Height update
632  int ah = get_text_size(*curr_item, init_width - float_size.x).y;
633  if (tmp_h > ah) {
634  tmp_h = 0;
635  }
636  text_height += ah - tmp_h;
637 
638  prev_blk_height += text_height + 0.3*font::get_max_height(font_size_);
639 
640  DBG_GUI_RL << "wrap: " << prev_blk_height << "," << text_height;
641  text_height = 0;
642 
643  // New text block
644  x = origin.x;
645  wrap_mode = false;
646 
647  // rest of the text
648  curr_item = &(text_dom.add_child("text"));
649  default_text_config(curr_item);
650  tmp_h = get_text_size(*curr_item, init_width).y;
651  add_text_with_attribute(*curr_item, removed_part);
652 
653  } else if ((float_size.y > 0) && (text_size.y < float_size.y)) {
654  //TODO padding?
655  // text height less than floating image's height, don't split
656  DBG_GUI_RL << "no wrap";
657  (*curr_item)["actions"] = "([set_var('pos_y', pos_y + text_height)])";
658  }
659 
660  if (!wrap_mode) {
661  float_size = point(0,0);
662  }
663 
664  is_image = false;
665  }
666 
667  point size = get_text_size(*curr_item, init_width - (x == 0 ? float_size.x : x));
668  int ah = size.y;
669  // update text size and widget height
670  if (tmp_h > ah) {
671  tmp_h = 0;
672  }
673  w = std::max(w, x + static_cast<unsigned>(size.x));
674 
675  text_height += ah - tmp_h;
676 
677  if (remaining_item) {
678  x = origin.x;
679  (*curr_item)["actions"] = "([set_var('pos_x', 0), set_var('pos_y', pos_y + " + std::to_string(img_size.y) + ")])";
680  text_dom.append(*remaining_item);
681  remaining_item = nullptr;
682  curr_item = &text_dom.all_children_view().back().second;
683  }
684  }
685 
686  if (!is_image && !wrap_mode && img_size.y > 0) {
687  img_size = point(0,0);
688  }
689 
690  if (curr_item) {
691  DBG_GUI_RL << "Item:\n" << curr_item->debug();
692  }
693  DBG_GUI_RL << "X: " << x;
694  DBG_GUI_RL << "Prev block height: " << prev_blk_height << " Current text block height: " << text_height;
695  DBG_GUI_RL << "Height: " << h;
696  h = text_height + prev_blk_height;
697  DBG_GUI_RL << "-----------";
698  } // for loop ends
699 
700  if (w == 0) {
701  w = init_width;
702  }
703 
704  if (finalize) {
705  // reset all canvas variables to zero, otherwise they grow infinitely
706  config& break_cfg = text_dom.add_child("text");
707  default_text_config(&break_cfg, " ");
708  break_cfg["actions"] = "([set_var('pos_x', 0), set_var('pos_y', 0), set_var('img_x', 0), set_var('img_y', 0), set_var('ww', 0), set_var('tw', 0)])";
709  DBG_GUI_RL << text_dom.debug();
710  }
711 
712  // DEBUG: draw boxes around links
713  #if LINK_DEBUG_BORDER
714  if (finalize) {
715  for (const auto& entry : links_) {
716  config& link_rect_cfg = text_dom.add_child("rectangle");
717  link_rect_cfg["x"] = entry.first.x;
718  link_rect_cfg["y"] = entry.first.y;
719  link_rect_cfg["w"] = entry.first.w;
720  link_rect_cfg["h"] = entry.first.h;
721  link_rect_cfg["border_thickness"] = 1;
722  link_rect_cfg["border_color"] = "255, 180, 0, 255";
723  }
724  }
725  #endif
726 
727  // TODO float and a mix of floats and images
728  h = std::max(static_cast<unsigned>(img_size.y), h);
729 
730  DBG_GUI_RL << "Width: " << w << " Height: " << h << " Origin: " << origin;
731  return { text_dom, point(w, h - origin.y) };
732 } // function ends
733 
735  if (txt_ptr != nullptr) {
736  (*txt_ptr)["text"] = text;
737  (*txt_ptr)["font_size"] = font_size_;
738  (*txt_ptr)["text_alignment"] = encode_text_alignment(get_text_alignment());
739  (*txt_ptr)["x"] = "(pos_x)";
740  (*txt_ptr)["y"] = "(pos_y)";
741  (*txt_ptr)["w"] = "(text_width)";
742  (*txt_ptr)["h"] = "(text_height)";
743  // tw -> table width, used for wrapping text inside table cols
744  // ww -> wrap width, used for wrapping around floating image
745  // max text width shouldn't go beyond the rich_label's specified width
746  (*txt_ptr)["maximum_width"] = "(width - pos_x - ww - tw)";
747  (*txt_ptr)["actions"] = "([set_var('pos_x', 0), set_var('pos_y', pos_y + text_height)])";
748  }
749 }
750 
752 {
753  for(canvas& tmp : get_canvases()) {
754  tmp.set_variable("pos_x", wfl::variant(0));
755  tmp.set_variable("pos_y", wfl::variant(0));
756  tmp.set_variable("img_x", wfl::variant(0));
757  tmp.set_variable("img_y", wfl::variant(0));
758  tmp.set_variable("width", wfl::variant(init_w_));
759  tmp.set_variable("tw", wfl::variant(0));
760  tmp.set_variable("ww", wfl::variant(0));
761  tmp.set_variable("padding", wfl::variant(padding_));
762  // Disable ellipsization so that text wrapping can work
763  tmp.set_variable("text_wrap_mode", wfl::variant(PANGO_ELLIPSIZE_NONE));
764  tmp.set_cfg(text_dom_, true);
765  tmp.set_variable("text_alpha", wfl::variant(text_alpha_));
766  }
767 }
768 
769 void rich_label::set_text_alpha(unsigned short alpha)
770 {
771  if(alpha != text_alpha_) {
772  text_alpha_ = alpha;
773  update_canvas();
774  queue_redraw();
775  }
776 }
777 
778 void rich_label::set_active(const bool active)
779 {
780  if(get_active() != active) {
781  set_state(active ? ENABLED : DISABLED);
782  }
783 }
784 
785 void rich_label::set_link_aware(bool link_aware)
786 {
787  if(link_aware != link_aware_) {
788  link_aware_ = link_aware;
789  update_canvas();
790  queue_redraw();
791  }
792 }
793 
795 {
796  if(color != link_color_) {
797  link_color_ = color;
798  update_canvas();
799  queue_redraw();
800  }
801 }
802 
804 {
805  if(state != state_) {
806  state_ = state;
807  queue_redraw();
808  }
809 }
810 
811 void rich_label::register_link_callback(std::function<void(std::string)> link_handler)
812 {
813  if(!link_aware_) {
814  return;
815  }
816 
817  connect_signal<event::LEFT_BUTTON_CLICK>(
818  std::bind(&rich_label::signal_handler_left_button_click, this, std::placeholders::_3));
819  connect_signal<event::MOUSE_MOTION>(
820  std::bind(&rich_label::signal_handler_mouse_motion, this, std::placeholders::_3, std::placeholders::_5));
821  connect_signal<event::MOUSE_LEAVE>(
822  std::bind(&rich_label::signal_handler_mouse_leave, this, std::placeholders::_3));
823  link_handler_ = link_handler;
824 }
825 
826 
828 {
829  DBG_GUI_E << "rich_label click";
830 
831  if(!get_link_aware()) {
832  return; // without marking event as "handled"
833  }
834 
835  point mouse = get_mouse_position() - get_origin();
836 
837  DBG_GUI_RL << "(mouse) " << mouse;
838  DBG_GUI_RL << "link count :" << links_.size();
839 
840  for (const auto& entry : links_) {
841  DBG_GUI_RL << "link " << entry.first;
842 
843  if (entry.first.contains(mouse)) {
844  DBG_GUI_RL << "Clicked link! dst = " << entry.second;
846  if (link_handler_) {
847  link_handler_(entry.second);
848  } else {
849  DBG_GUI_RL << "No registered link handler found";
850  }
851 
852  }
853  }
854 
855  handled = true;
856 }
857 
859 {
860  DBG_GUI_E << "rich_label mouse motion";
861 
862  if(!get_link_aware()) {
863  return; // without marking event as "handled"
864  }
865 
866  point mouse = coordinate - get_origin();
867 
868  for (const auto& entry : links_) {
869  if (entry.first.contains(mouse)) {
870  update_mouse_cursor(true);
871  handled = true;
872  return;
873  }
874  }
875 
876  update_mouse_cursor(false);
877 }
878 
880 {
881  DBG_GUI_E << "rich_label mouse leave";
882 
883  if(!get_link_aware()) {
884  return; // without marking event as "handled"
885  }
886 
887  // We left the widget, so just unconditionally reset the cursor
888  update_mouse_cursor(false);
889 
890  handled = true;
891 }
892 
894 {
895  // Someone else may set the mouse cursor for us to something unusual (e.g.
896  // the WAIT cursor) so we ought to mess with that only if it's set to
897  // NORMAL or HYPERLINK.
898 
899  if(enable && cursor::get() == cursor::NORMAL) {
901  } else if(!enable && cursor::get() == cursor::HYPERLINK) {
903  }
904 }
905 
906 // }---------- DEFINITION ---------{
907 
910 {
911  DBG_GUI_P << "Parsing rich_label " << id;
912 
913  load_resolutions<resolution>(cfg);
914 }
915 
917  : resolution_definition(cfg)
918  , link_color(cfg["link_color"].empty() ? font::YELLOW_COLOR : color_t::from_rgba_string(cfg["link_color"].str()))
919  , font_size(cfg["text_font_size"].to_int(font::SIZE_NORMAL))
920 {
921  // Note the order should be the same as the enum state_t is rich_label.hpp.
922  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_enabled", missing_mandatory_wml_tag("rich_label_definition][resolution", "state_enabled")));
923  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_disabled", missing_mandatory_wml_tag("rich_label_definition][resolution", "state_disabled")));
924 }
925 
926 // }---------- BUILDER -----------{
927 
928 namespace implementation
929 {
930 
931 builder_rich_label::builder_rich_label(const config& cfg)
932  : builder_styled_widget(cfg)
933  , text_alignment(decode_text_alignment(cfg["text_alignment"]))
934  , link_aware(cfg["link_aware"].to_bool(true))
935  , width(cfg["width"], 500)
936 {
937 }
938 
939 std::unique_ptr<widget> builder_rich_label::build() const
940 {
941  auto lbl = std::make_unique<rich_label>(*this);
942 
943  const auto conf = lbl->cast_config_to<rich_label_definition>();
944  assert(conf);
945 
946  lbl->set_text_alignment(text_alignment);
947  lbl->set_link_color(conf->link_color);
948  lbl->set_font_size(conf->font_size);
949  lbl->set_label(lbl->get_label());
950 
951  DBG_GUI_G << "Window builder: placed rich_label '" << id << "' with definition '"
952  << definition << "'.";
953 
954  return lbl;
955 }
956 
957 } // namespace implementation
958 
959 // }------------ END --------------
960 
961 } // namespace gui2
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:172
void append(const config &cfg)
Append data from another config object to this one.
Definition: config.cpp:203
auto all_children_view() const
In-order iteration over all children.
Definition: config.hpp:810
child_itors child_range(config_key_type key)
Definition: config.cpp:272
void append_attributes(const config &cfg)
Adds attributes from cfg.
Definition: config.cpp:189
std::string debug() const
Definition: config.cpp:1240
void append_children(const config &cfg)
Adds children from cfg.
Definition: config.cpp:167
config & add_child(config_key_type key)
Definition: config.cpp:440
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:858
state_t state_
Current state of the widget.
Definition: rich_label.hpp:159
void set_state(const state_t state)
Definition: rich_label.cpp:803
size_t get_split_location(std::string_view text, const point &pos)
Definition: rich_label.cpp:223
virtual bool get_active() const override
Gets the active state of the styled_widget.
Definition: rich_label.hpp:68
point get_text_size(config &text_cfg, unsigned width=0) const
size calculation functions
Definition: rich_label.cpp:92
std::function< void(std::string)> link_handler_
Definition: rich_label.hpp:232
virtual void update_canvas() override
Updates the canvas(ses).
Definition: rich_label.cpp:751
std::pair< size_t, size_t > add_text(config &curr_item, std::string text)
Definition: rich_label.cpp:111
void default_text_config(config *txt_ptr, t_string text="")
Create template for text config that can be shown in canvas.
Definition: rich_label.cpp:734
t_string unparsed_text_
The unparsed/raw text.
Definition: rich_label.hpp:201
void signal_handler_mouse_leave(bool &handled)
Mouse leave signal handler: checks if the cursor left a hyperlink.
Definition: rich_label.cpp:879
std::vector< std::pair< rect, std::string > > links_
link variables and functions
Definition: rich_label.hpp:230
void add_link(config &curr_item, std::string name, std::string dest, const point &origin, int img_width)
Definition: rich_label.cpp:177
int font_size_
Base font size.
Definition: rich_label.hpp:185
std::pair< size_t, size_t > add_text_with_attribute(config &curr_item, std::string text, std::string attr_name="", std::string extra_data="")
Definition: rich_label.cpp:128
point get_image_size(config &img_cfg) const
Definition: rich_label.cpp:100
std::vector< std::string > split_in_width(const std::string &s, const int font_size, const unsigned width)
Definition: rich_label.cpp:240
void add_attribute(config &curr_item, std::string attr_name, size_t start=0, size_t end=0, std::string extra_data="")
Definition: rich_label.cpp:119
unsigned short text_alpha_
Definition: rich_label.hpp:189
point get_xy_from_offset(const unsigned offset) const
Definition: rich_label.hpp:239
std::pair< config, point > get_parsed_text(const config &parsed_text, const point &origin, const unsigned init_width, const bool finalize=false)
Definition: rich_label.cpp:267
virtual void set_active(const bool active) override
Sets the styled_widget's state.
Definition: rich_label.cpp:778
const unsigned init_w_
Width and height of the canvas.
Definition: rich_label.hpp:204
state_t
Possible states of the widget.
Definition: rich_label.hpp:146
void register_link_callback(std::function< void(std::string)> link_handler)
Definition: rich_label.cpp:811
void signal_handler_left_button_click(bool &handled)
Left click signal handler: checks if we clicked on a hyperlink.
Definition: rich_label.cpp:827
void set_topic(const help::topic *topic)
Definition: rich_label.cpp:255
int get_offset_from_xy(const point &position) const
Definition: rich_label.hpp:234
bool link_aware_
Whether the rich_label is link aware, rendering links with special formatting and handling click even...
Definition: rich_label.hpp:175
void add_image(config &curr_item, std::string name, std::string align, bool has_prev_image, bool floating)
Definition: rich_label.cpp:134
wfl::map_formula_callable setup_text_renderer(config text_cfg, unsigned width=0) const
Definition: rich_label.cpp:80
void set_link_color(const color_t &color)
Definition: rich_label.cpp:794
unsigned padding_
Padding.
Definition: rich_label.hpp:208
void set_link_aware(bool l)
Definition: rich_label.cpp:785
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 set_text_alpha(unsigned short alpha)
Definition: rich_label.cpp:769
color_t link_color_
What color links will be rendered in.
Definition: rich_label.hpp:180
void set_label(const t_string &text) override
Definition: rich_label.cpp:260
void update_mouse_cursor(bool enable)
Implementation detail for (re)setting the hyperlink cursor.
Definition: rich_label.cpp:893
config text_dom_
structure tree of the marked up text after parsing
Definition: rich_label.hpp:198
PangoAlignment get_text_alignment() const
std::vector< canvas > & get_canvases()
virtual void set_label(const t_string &text)
void queue_redraw()
Indicates that this widget should be redrawn.
Definition: widget.cpp:464
point get_origin() const
Returns the screen origin of the widget.
Definition: widget.cpp:311
The text displayed in a topic.
Definition: help_impl.hpp:85
const config & parsed_text() const
Definition: help_impl.cpp:379
Thrown by operations encountering invalid UTF-8 data.
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
int w
static std::string _(const char *str)
Definition: gettext.hpp:93
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.
Graphical text output.
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:1142
const color_t YELLOW_COLOR
color_t string_to_color(const std::string &cmp_str)
Return the color the string represents.
const int SIZE_TITLE
Definition: constants.cpp:31
std::string pango_word_wrap(const std::string &unwrapped_text, int font_size, int max_width, int max_height, int max_lines, bool)
Uses Pango to word wrap text.
const int SIZE_NORMAL
Definition: constants.cpp:20
std::string sound_button_click
Definition: settings.cpp:48
Generic file dialog.
void get_screen_size_variables(wfl::map_formula_callable &variable)
Gets a formula object with the screen size.
Definition: helper.cpp:125
point get_mouse_position()
Returns the current mouse position.
Definition: helper.cpp:143
std::string_view debug_truncate(std::string_view text)
Returns a truncated version of the text.
Definition: helper.cpp:148
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.
void play_UI_sound(const std::string &files)
Definition: sound.cpp:1077
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
int get_pixel_scale()
Get the current active pixel scale multiplier.
Definition: video.cpp:481
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:46
Transitional API for porting SDL_ttf-based code to Pango.
This file contains the settings handling of the widget library.
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:939
std::string definition
Parameters for the styled_widget.
std::vector< state_definition > state
rich_label_definition(const config &cfg)
Definition: rich_label.cpp:908
A topic contains a title, an id and some text.
Definition: help_impl.hpp:115
topic_text text
Definition: help_impl.hpp:140
Thrown when the help system fails to parse something.
Definition: markup.hpp:175
Holds a 2D point.
Definition: point.hpp:25
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:47
mock_char c
static map_location::direction s
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)
#define h