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