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