The Battle for Wesnoth  1.19.21+dev
rich_label.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2024 - 2025
3  by Subhraman Sarkar (babaissarkar) <sbmskmm@protonmail.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 "gettext.hpp"
21 #include "gui/core/log.hpp"
24 #include "gui/widgets/settings.hpp"
25 
26 #include "cursor.hpp"
27 #include "font/constants.hpp"
28 #include "font/sdl_ttf_compat.hpp"
29 #include "log.hpp"
30 #include "serialization/markup.hpp"
33 #include "sound.hpp"
34 #include "video.hpp"
35 #include "wml_exception.hpp"
36 
37 #include <boost/format.hpp>
38 #include <boost/multi_array.hpp>
39 #include <functional>
40 #include <numeric>
41 #include <string>
42 #include <utility>
43 
44 static lg::log_domain log_rich_label("gui/widget/rich_label");
45 #define DBG_GUI_RL LOG_STREAM(debug, log_rich_label)
46 
47 // Enable this to draw borders around links.
48 // Useful for debugging misplaced links.
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  , predef_colors_()
72  , font_size_(font::SIZE_NORMAL)
73  , can_shrink_(true)
74  , text_alpha_(ALPHA_OPAQUE)
75  , init_w_(0)
76  , size_(0, 0)
77  , padding_(builder.padding)
78 {
79  const auto conf = cast_config_to<rich_label_definition>();
80  assert(conf);
81  text_color_enabled_ = conf->text_color_enabled;
82  text_color_disabled_ = conf->text_color_disabled;
83  font_family_ = conf->font_family;
84  font_size_ = conf->font_size;
85  font_style_ = conf->font_style;
86  link_color_ = conf->link_color;
87  predef_colors_.insert(conf->colors.begin(), conf->colors.end());
88  set_text_alignment(builder.text_alignment);
89  set_label(builder.label_string);
90 }
91 
92 color_t rich_label::get_color(const std::string& color)
93 {
94  const auto iter = predef_colors_.find(color);
95  return (iter != predef_colors_.end()) ? iter->second : font::string_to_color(color);
96 }
97 
99 {
100  // Set up fake render to calculate text position
101  static wfl::action_function_symbol_table functions;
102  wfl::map_formula_callable variables;
103  variables.add("text", wfl::variant(text_cfg["text"]));
104  variables.add("width", wfl::variant(width));
105  variables.add("text_wrap_mode", wfl::variant(PANGO_ELLIPSIZE_NONE));
106  variables.add("fake_draw", wfl::variant(true));
107  gui2::text_shape{text_cfg, functions}.draw(variables);
108  return variables;
109 }
110 
111 point rich_label::get_text_size(config& text_cfg, unsigned width) const
112 {
113  wfl::map_formula_callable variables = setup_text_renderer(text_cfg, width);
114  return {
115  variables.query_value("text_width").as_int(),
116  variables.query_value("text_height").as_int()
117  };
118 }
119 
121 {
122  static wfl::action_function_symbol_table functions;
123  wfl::map_formula_callable variables;
124  variables.add("fake_draw", wfl::variant(true));
125  gui2::image_shape{img_cfg, functions}.draw(variables);
126  return {
127  variables.query_value("image_width").as_int(),
128  variables.query_value("image_height").as_int()
129  };
130 }
131 
132 std::pair<std::size_t, std::size_t> rich_label::add_text(config& curr_item, const std::string& text)
133 {
134  auto& attr = curr_item["text"];
135  std::size_t start = attr.str().size();
136  attr = attr.str() + text;
137  std::size_t end = attr.str().size();
138  return { start, end };
139 }
140 
142  config& curr_item,
143  const std::string& attr_name,
144  const std::string& extra_data,
145  std::size_t start,
146  std::size_t end)
147 {
148  if (start == end && start != 0) {
149  return;
150  }
151 
152  config& cfg = curr_item.add_child("attribute");
153  cfg["name"] = attr_name;
154  // No need to set any keys that's aren't given
155  if (start != 0) {
156  cfg["start"] = start;
157  }
158  if (end != 0) {
159  cfg["end"] = end;
160  }
161  if (!extra_data.empty()) {
162  cfg["value"] = extra_data;
163  }
164 }
165 
166 std::pair<std::size_t, std::size_t> rich_label::add_text_with_attribute(
167  config& curr_item,
168  const std::string& text,
169  const std::string& attr_name,
170  const std::string& extra_data)
171 {
172  const auto [start, end] = add_text(curr_item, text);
173  add_attribute(curr_item, attr_name, extra_data, start, end);
174  return { start, end };
175 }
176 
178  config& curr_item,
179  const std::string& name,
180  const std::string& dest,
181  const point& origin,
182  int img_width)
183 {
184  // TODO algorithm needs to be text_alignment independent
185 
186  DBG_GUI_RL << "add_link: " << name << "->" << dest;
187  DBG_GUI_RL << "origin: " << origin;
188  DBG_GUI_RL << "width: " << img_width;
189 
190  point t_start, t_end;
191 
192  setup_text_renderer(curr_item, init_w_ - origin.x - img_width);
193  t_start = origin + get_xy_from_offset(utf8::size(curr_item["text"].str()));
194  DBG_GUI_RL << "link text start:" << t_start;
195 
196  std::string link_text = name.empty() ? dest : name;
197  add_text_with_attribute(curr_item, link_text, "color", link_color_.to_hex_string());
198 
199  setup_text_renderer(curr_item, init_w_ - origin.x - img_width);
200  t_end.x = origin.x + get_xy_from_offset(utf8::size(curr_item["text"].str())).x;
201  DBG_GUI_RL << "link text end:" << t_end;
202 
203  // TODO link after right aligned images
204 
205  // Add link
206  if(t_end.x > t_start.x) {
207  rect link_rect{ t_start, point{t_end.x - t_start.x, font::get_max_height(font_size_) }};
208  links_.emplace_back(link_rect, dest);
209 
210  DBG_GUI_RL << "added link at rect: " << link_rect;
211 
212  } else {
213  //link straddles two lines, break into two rects
214  int text_height = font::get_max_height(font_size_);
215 
216  point t_size(size_.x - t_start.x - (origin.x == 0 ? img_width : 0), t_end.y - t_start.y);
217  point link_start2(origin.x, t_start.y + font::get_line_spacing_factor() * text_height);
218  point t_size2(t_end.x, t_end.y - t_start.y);
219 
220  rect link_rect{ t_start, point{ t_size.x, text_height } };
221  rect link_rect2{ link_start2, point{ t_size2.x, text_height } };
222 
223  links_.emplace_back(link_rect, dest);
224  links_.emplace_back(link_rect2, dest);
225 
226  DBG_GUI_RL << "added link at rect 1: " << link_rect;
227  DBG_GUI_RL << "added link at rect 2: " << link_rect2;
228  }
229 }
230 
231 std::size_t rich_label::get_split_location(std::string_view text, const point& pos)
232 {
233  int offset = get_offset_from_xy(pos);
234  if(offset <= 0) {
235  return 0;
236  }
237 
238  std::size_t len = static_cast<size_t>(offset);
239  if (len >= text.size() - 1) {
240  return text.size() - 1;
241  }
242 
243  // break only at word boundary
244  char c;
245  while(!std::isspace(c = text[len])) {
246  len--;
247  if(len == 0) {
248  break;
249  }
250  }
251 
252  return len;
253 }
254 
256 {
257  dom_ = dom;
258  std::tie(shapes_, size_) = get_parsed_text(dom, point(0,0), init_w_, true);
259  update_canvas();
260  queue_redraw();
261 }
262 
264 {
266 }
267 
268 void rich_label::request_reduce_width(const unsigned maximum_width)
269 {
270  if(maximum_width != static_cast<unsigned>(size_.x)) {
271  init_w_ = maximum_width;
272  set_dom(dom_);
273  }
275 }
276 
278 {
279  return size_;
280 }
281 
282 void rich_label::place(const point& origin, const point& size)
283 {
284  if(size.x != size_.x) {
285  init_w_ = size.x;
286  set_dom(dom_);
287  }
288  styled_widget::place(origin, size);
289 }
290 
291 std::pair<config, point> rich_label::get_parsed_text(
292  const config& parsed_text,
293  const point& origin,
294  const unsigned init_width,
295  const bool finalize)
296 {
297  // Initial width
298  DBG_GUI_RL << "Initial width: " << init_width;
299 
300  // Initialization
301  unsigned x = 0;
302  unsigned prev_blk_height = origin.y;
303  unsigned text_height = 0;
304  unsigned h = 0;
305  unsigned w = 0;
306 
307  if(finalize) {
308  links_.clear();
309  }
310 
311  config text_dom;
312  config* curr_item = nullptr;
313 
314  bool is_text = false;
315  bool is_image = false;
316  bool wrap_mode = false;
317  bool new_text_block = false;
318 
319  point pos(origin);
320  point float_pos, float_size;
321  point img_size;
322 
323  DBG_GUI_RL << parsed_text.debug();
324 
325  for(const auto [orig_key, child] : parsed_text.all_children_view()) {
326 
327  const std::string key = (orig_key == "img" && !child["float"].to_bool(false)) ? "inline_image" : orig_key;
328 
329  DBG_GUI_RL << "\n Trying to layout tag: " << key;
330 
331  if(key == "img") {
332  prev_blk_height += text_height;
333  text_height = 0;
334 
335  const std::string& align = child["align"].str("left");
336 
337  curr_item = &(text_dom.add_child("image"));
338  (*curr_item)["name"] = child["src"];
339  (*curr_item)["x"] = 0;
340  (*curr_item)["y"] = 0;
341  (*curr_item)["w"] = "(image_width)";
342  (*curr_item)["h"] = "(image_height)";
343 
344  const point& curr_img_size = get_image_size(*curr_item);
345 
346  if (align == "right") {
347  float_pos.x = init_width - curr_img_size.x;
348  } else if (align == "middle" || align == "center") {
349  // works for single image only
350  float_pos.x = float_size.x + (init_width - curr_img_size.x)/2;
351  }
352 
353  if (is_image) {
354  float_pos.y += float_size.y;
355  }
356 
357  (*curr_item)["x"] = float_pos.x;
358  (*curr_item)["y"] = pos.y + float_pos.y;
359 
360  float_size.x = curr_img_size.x + padding_;
361  float_size.y += curr_img_size.y + padding_;
362 
363  x = ((align == "left") ? float_size.x : 0);
364  pos.x += ((align == "left") ? float_size.x : 0);
365 
366  wrap_mode = true;
367 
368  w = std::max(w, x);
369 
370  is_image = true;
371  is_text = false;
372  new_text_block = true;
373 
374  DBG_GUI_RL << key << ": src=" << child["src"] << ", size=" << img_size;
375  DBG_GUI_RL << "wrap turned on.";
376  } else if(key == "clear") {
377  // Moves the text below the preceding floating image and turns off wrapping
378  wrap_mode = false;
379  prev_blk_height += float_size.y;
380  pos.y += float_size.y;
381  float_size = point(0, 0);
382  pos.x = origin.x;
383 
384  DBG_GUI_RL << key;
385  DBG_GUI_RL << "wrap turned off";
386  } else if(key == "table") {
387  if(curr_item == nullptr) {
388  curr_item = &(text_dom.add_child("text"));
389  default_text_config(curr_item, pos, init_width);
390  new_text_block = false;
391  }
392 
393  // table doesn't support floating images alongside
394  img_size = point(0,0);
395  float_size = point(0,0);
396  x = origin.x;
397  prev_blk_height += text_height + padding_;
398  text_height = 0;
399  pos = point(origin.x, prev_blk_height + padding_);
400 
401  // init table vars
402  unsigned col_idx = 0, row_idx = 0;
403  unsigned rows = child.child_count("row");
404  unsigned columns = 1;
405  if(rows > 0) {
406  columns = child.mandatory_child("row").child_count("col");
407  }
408  columns = (columns == 0) ? 1 : columns;
409  int init_cell_width;
410  if(child["width"] == "fill") {
411  init_cell_width = init_width/columns;
412  } else {
413  init_cell_width = child["width"].to_int(init_width)/columns;
414  }
415  std::vector<int> col_widths(columns, 0);
416  std::vector<int> row_heights(rows, 0);
417 
418  is_text = false;
419  new_text_block = true;
420  is_image = false;
421 
422  DBG_GUI_RL << "start table: " << "row=" << rows << " col=" << columns
423  << " width=" << init_cell_width*columns;
424 
425  const auto get_padding = [this](const config::attribute_value& val) {
426  if(val.blank()) {
427  return std::array{ padding_, padding_ };
428  } else {
429  auto paddings = utils::split(val.str(), ' ');
430  if(paddings.size() == 1) {
431  return std::array{ std::stoi(paddings[0]), std::stoi(paddings[0]) };
432  } else {
433  return std::array{ std::stoi(paddings[0]), std::stoi(paddings[1]) };
434  }
435  }
436  };
437 
438  std::array<int, 2> row_paddings;
439  boost::multi_array<point, 2> cell_sizes(boost::extents[rows][columns]);
440 
441  // optimal col width calculation
442  for(const config& row : child.child_range("row")) {
443  pos.x = origin.x;
444  col_idx = 0;
445 
446  // order: top padding|bottom padding
447  row_paddings = get_padding(row["padding"]);
448 
449  pos.y += row_paddings[0];
450  for(const config& col : row.child_range("col")) {
451  DBG_GUI_RL << "table cell origin (pre-layout): " << pos.x << ", " << pos.y;
452  config col_cfg;
453  col_cfg.append_children(col);
454  config& col_txt_cfg = col_cfg.add_child("text");
455  col_txt_cfg.append_attributes(col);
456 
457  // order: left padding|right padding
458  std::array<int, 2> col_paddings = get_padding(col["padding"]);
459  int cell_width = init_cell_width - col_paddings[0] - col_paddings[1];
460 
461  pos.x += col_paddings[0];
462  // attach data
463  auto links = links_;
464  cell_sizes[row_idx][col_idx] = get_parsed_text(col_cfg, pos, init_cell_width).second;
465  links_ = links;
466 
467  // column post-processing
468  row_heights[row_idx] = std::max(row_heights[row_idx], cell_sizes[row_idx][col_idx].y);
469  if(!child["width"].empty()) {
470  col_widths[col_idx] = cell_width;
471  }
472  col_widths[col_idx] = std::max(col_widths[col_idx], cell_sizes[row_idx][col_idx].x);
473  if(child["width"].empty()) {
474  col_widths[col_idx] = std::min(col_widths[col_idx], cell_width);
475  }
476 
477  DBG_GUI_RL << "table row " << row_idx << " height: " << row_heights[row_idx]
478  << "col " << col_idx << " width: " << col_widths[col_idx];
479 
480  pos.x += cell_width;
481  pos.x += col_paddings[1];
482  col_idx++;
483  }
484 
485  pos.y += row_heights[row_idx] + row_paddings[1];
486  row_idx++;
487  }
488 
489  // table layouting
490  row_idx = 0;
491  pos = point(origin.x, prev_blk_height);
492  for(const config& row : child.child_range("row")) {
493  pos.x = origin.x;
494  col_idx = 0;
495 
496  if(!row["bgcolor"].blank()) {
497  config bg_base;
498  config& bgbox = bg_base.add_child("rectangle");
499  bgbox["x"] = origin.x;
500  bgbox["y"] = pos.y;
501  bgbox["w"] = std::accumulate(col_widths.begin(), col_widths.end(), 0) + 2*(row_paddings[0] + row_paddings[1])*columns;
502  bgbox["h"] = row_paddings[0] + row_heights[row_idx] + row_paddings[1];
503  bgbox["fill_color"] = get_color(row["bgcolor"].str()).to_rgba_string();
504  text_dom.append(std::move(bg_base));
505  }
506 
507  row_paddings = get_padding(row["padding"]);
508  pos.y += row_paddings[0];
509 
510  for(const config& col : row.child_range("col")) {
511  DBG_GUI_RL << "table row " << row_idx << " height: " << row_heights[row_idx]
512  << "col " << col_idx << " width: " << col_widths[col_idx];
513  DBG_GUI_RL << "cell origin: " << pos;
514 
515  config col_cfg;
516  col_cfg.append_children(col);
517  config& col_txt_cfg = col_cfg.add_child("text");
518  col_txt_cfg.append_attributes(col);
519 
520  // order: left padding|right padding
521  std::array<int, 2> col_paddings = get_padding(col["padding"]);
522 
523  pos.x += col_paddings[0];
524 
525  const std::string& valign = row["valign"].str("center");
526  const std::string& halign = col["halign"].str("left");
527 
528  // set position according to alignment keys
529  point text_pos(pos);
530  if (valign == "center" || valign == "middle") {
531  text_pos.y += (row_heights[row_idx] - cell_sizes[row_idx][col_idx].y)/2;
532  } else if (valign == "bottom") {
533  text_pos.y += row_heights[row_idx] - cell_sizes[row_idx][col_idx].y;
534  }
535  if (halign == "center" || halign == "middle") {
536  text_pos.x += (col_widths[col_idx] - cell_sizes[row_idx][col_idx].x)/2;
537  } else if (halign == "right") {
538  text_pos.x += col_widths[col_idx] - cell_sizes[row_idx][col_idx].x;
539  }
540 
541  // attach data
542  auto [table_elem, size] = get_parsed_text(col_cfg, text_pos, col_widths[col_idx]);
543  text_dom.append(std::move(table_elem));
544  pos.x += col_widths[col_idx];
545  pos.x += col_paddings[1];
546 
547  auto [_, end_cfg] = text_dom.all_children_view().back();
548  end_cfg["maximum_width"] = col_widths[col_idx];
549 
550  DBG_GUI_RL << "jump to next column";
551 
552  if(!is_image) {
553  new_text_block = true;
554  }
555  is_image = false;
556  col_idx++;
557  }
558 
559  pos.y += row_heights[row_idx];
560  pos.y += row_paddings[1];
561  DBG_GUI_RL << "row height: " << row_heights[row_idx];
562  row_idx++;
563  }
564 
565  w = std::max(w, static_cast<unsigned>(pos.x));
566  prev_blk_height = pos.y;
567  text_height = 0;
568  pos.x = origin.x;
569 
570  is_image = false;
571  is_text = false;
572 
573  x = origin.x;
574 
575  } else {
576  std::string line = child["text"];
577 
578  if (!finalize && (line.empty() && key == "text")) {
579  continue;
580  }
581 
582  if (curr_item == nullptr || new_text_block) {
583  curr_item = &(text_dom.add_child("text"));
584  default_text_config(curr_item, pos, init_width - pos.x - float_size.x);
585  new_text_block = false;
586  }
587 
588  // }---------- TEXT TAGS -----------{
589  int tmp_h = get_text_size(*curr_item, init_width - (x == 0 ? float_size.x : x)).y;
590 
591  if(is_text && key == "text") {
592  add_text_with_attribute(*curr_item, "\n\n");
593  }
594 
595  is_text = false;
596  is_image = false;
597 
598  if (key == "inline_image") {
599 
600  // Inline image is rendered as a custom text glyph (pango shape attribute)
601  // FIXME: If linebreak (\n) is followed by an inline image
602  // the text size is calculated wrongly as being decreased.
603  // Workaround: append a zero width space always in front of the image.
604  add_text(*curr_item, "\u200b");
605  add_text_with_attribute(*curr_item, "\ufffc", "image", child["src"]);
606 
607  DBG_GUI_RL << key << ": src=" << child["src"];
608 
609  } else if(key == "ref") {
610 
611  add_link(*curr_item, line, child["dst"], point(x + origin.x, prev_blk_height), float_size.x);
612 
613  DBG_GUI_RL << key << ": dst=" << child["dst"];
614 
615  } else if(utils::contains(format_tags, key)) {
616  // TODO only the formatting tags here support nesting
617 
618  add_text_with_attribute(*curr_item, line, key);
619 
620  // Calculate the location of the nested children
621  setup_text_renderer(*curr_item, init_w_ - origin.x - float_size.x);
622  point child_origin = origin + get_xy_from_offset(utf8::size((*curr_item)["text"].str()));
623  child_origin.y += prev_blk_height;
624 
625  config parsed_children = get_parsed_text(child, child_origin, init_width).first;
626 
627  for(const auto [parsed_key, parsed_cfg] : parsed_children.all_children_view()) {
628  if(parsed_key == "text") {
629  const auto [start, end] = add_text(*curr_item, parsed_cfg["text"]);
630  for (const config& attr : parsed_cfg.child_range("attribute")) {
631  add_attribute(*curr_item, attr["name"], attr["value"], start + attr["start"].to_int(), start + attr["end"].to_int());
632  }
633  add_attribute(*curr_item, key, "", start, end);
634  } else {
635  text_dom.add_child(parsed_key, parsed_cfg);
636  }
637  }
638 
639  DBG_GUI_RL << key << ": text=" << gui2::debug_truncate(line);
640 
641  } else if(key == "header" || key == "h") {
642 
643  const auto [start, end] = add_text(*curr_item, line);
644  add_attribute(*curr_item, "weight", "heavy", start, end);
645  add_attribute(*curr_item, "color", "white", start, end);
646  add_attribute(*curr_item, "size", std::to_string(font::SIZE_TITLE - 2), start, end);
647 
648  DBG_GUI_RL << key << ": text=" << line;
649 
650  } else if(key == "character_entity") {
651 
652  line = "&" + child["name"].str() + ";";
653 
654  const auto [start, end] = add_text(*curr_item, line);
655  add_attribute(*curr_item, "face", "monospace", start, end);
656  add_attribute(*curr_item, "color", "red", start, end);
657 
658  DBG_GUI_RL << key << ": text=" << line;
659 
660  } else if(key == "span" || key == "format") {
661 
662  const auto [start, end] = add_text(*curr_item, line);
663  DBG_GUI_RL << "span/format: text=" << line;
664  DBG_GUI_RL << "attributes:";
665 
666  for (const auto& [key, value] : child.attribute_range()) {
667  if (key != "text") {
668  add_attribute(*curr_item, key, value, start, end);
669  DBG_GUI_RL << key << "=" << value;
670  }
671  }
672 
673  } else if (key == "text") {
674 
675  DBG_GUI_RL << "text: text=" << gui2::debug_truncate(line) << "...";
676 
677  add_text(*curr_item, line);
678 
679  point text_size = get_text_size(*curr_item, init_width - (x == 0 ? float_size.x : x));
680 
681  is_text = true;
682 
683  // Text wrapping around floating images
684  if(wrap_mode && (float_size.y > 0) && (text_size.y > float_size.y)) {
685  DBG_GUI_RL << "wrap start";
686 
687  const std::string full_text = (*curr_item)["text"].str();
688 
689  std::size_t len = get_split_location(full_text, point(init_width - float_size.x, float_size.y * video::get_pixel_scale()));
690 
691  DBG_GUI_RL << "wrap around area: " << float_size;
692 
693  if(len > 0) {
694  // first part of the text
695  (*curr_item)["text"] = full_text.substr(0, len);
696  }
697 
698  (*curr_item)["maximum_width"] = init_width - float_size.x;
699  float_size = point(0,0);
700 
701  // Height update
702  int ah = get_text_size(*curr_item, init_width - float_size.x).y;
703  if(tmp_h > ah) {
704  tmp_h = 0;
705  }
706  text_height += ah - tmp_h;
707  prev_blk_height += text_height;
708  pos = point(origin.x, prev_blk_height);
709 
710  DBG_GUI_RL << "wrap: " << prev_blk_height << "," << text_height;
711  text_height = 0;
712 
713  // New text block
714  x = origin.x;
715  wrap_mode = false;
716 
717  if (len > 0) {
718  // layout rest of the text
719  curr_item = &(text_dom.add_child("text"));
720  default_text_config(curr_item, pos, init_width - pos.x);
721  tmp_h = get_text_size(*curr_item, init_width).y;
722  // get_split_location always splits at word bounds,
723  // so substr(len) will include a space. we skip that.
724  add_text_with_attribute(*curr_item, full_text.substr(len+1));
725  }
726  } else if((float_size.y > 0) && (text_size.y < float_size.y)) {
727  //TODO padding?
728  // text height less than floating image's height, don't split
729  DBG_GUI_RL << "no wrap";
730  pos.y += text_size.y;
731  }
732 
733  if(!wrap_mode) {
734  float_size = point(0,0);
735  }
736  }
737 
738  point size = get_text_size(*curr_item, init_width - (x == 0 ? float_size.x : x));
739  // update text size and widget height
740  if(tmp_h > size.y) {
741  tmp_h = 0;
742  }
743  w = std::max(w, x + static_cast<unsigned>(size.x));
744 
745  text_height += size.y - tmp_h;
746  pos.y += size.y - tmp_h;
747  }
748 
749  if(!is_image && !wrap_mode && img_size.y > 0) {
750  img_size = point(0,0);
751  }
752 
753  if(curr_item) {
754  DBG_GUI_RL << "Item:\n" << curr_item->debug();
755  }
756  DBG_GUI_RL << "X: " << x;
757  DBG_GUI_RL << "Prev block height: " << prev_blk_height << " Current text block height: " << text_height;
758  DBG_GUI_RL << "Height: " << h;
759  h = text_height + prev_blk_height;
760  DBG_GUI_RL << "-----------";
761  } // for loop ends
762 
763  if(w == 0) {
764  w = init_width;
765  }
766 
767  // DEBUG: draw boxes around links
768  #if LINK_DEBUG_BORDER
769  if(finalize) {
770  for(const auto& entry : links_) {
771  config& link_rect_cfg = text_dom.add_child("rectangle");
772  link_rect_cfg["x"] = entry.first.x;
773  link_rect_cfg["y"] = entry.first.y;
774  link_rect_cfg["w"] = entry.first.w;
775  link_rect_cfg["h"] = entry.first.h;
776  link_rect_cfg["border_thickness"] = 1;
777  link_rect_cfg["border_color"] = "255, 180, 0, 255";
778  }
779  }
780  #endif
781 
782  // TODO float and a mix of floats and images and tables
783  h = std::max(static_cast<unsigned>(img_size.y), h);
784 
785  DBG_GUI_RL << "[\n" << text_dom.debug() << "]\n";
786 
787  DBG_GUI_RL << "Width: " << w << " Height: " << h << " Origin: " << origin;
788  return { text_dom, point(w, h - origin.y) };
789 } // function ends
790 
792  config* txt_ptr,
793  const point& pos,
794  const int max_width,
795  const t_string& text)
796 {
797  if(txt_ptr != nullptr) {
798  (*txt_ptr)["text"] = text;
799  (*txt_ptr)["color"] = text_color_enabled_.to_rgba_string();
800  (*txt_ptr)["font_family"] = font_family_;
801  (*txt_ptr)["font_size"] = font_size_;
802  (*txt_ptr)["font_style"] = font_style_;
803  (*txt_ptr)["text_alignment"] = encode_text_alignment(get_text_alignment());
804  (*txt_ptr)["line_spacing"] = 0;
805  (*txt_ptr)["x"] = pos.x;
806  (*txt_ptr)["y"] = pos.y;
807  (*txt_ptr)["w"] = "(text_width)";
808  (*txt_ptr)["h"] = "(text_height)";
809  (*txt_ptr)["maximum_width"] = max_width;
810  (*txt_ptr)["parse_text_as_formula"] = false;
811  add_attribute(*txt_ptr,
812  "line_height",
813  std::to_string(font::get_line_spacing_factor()));
814  }
815 }
816 
818 {
819  for(canvas& tmp : get_canvases()) {
820  tmp.set_shapes(shapes_, true);
821  tmp.set_variable("width", wfl::variant(init_w_));
822  tmp.set_variable("padding", wfl::variant(padding_));
823  // Disable ellipsization so that text wrapping can work
824  tmp.set_variable("text_wrap_mode", wfl::variant(PANGO_ELLIPSIZE_NONE));
825  tmp.set_variable("text_alpha", wfl::variant(text_alpha_));
826  }
827 }
828 
829 void rich_label::set_text_alpha(unsigned short alpha)
830 {
831  if(alpha != text_alpha_) {
832  text_alpha_ = alpha;
833  update_canvas();
834  queue_redraw();
835  }
836 }
837 
838 void rich_label::set_active(const bool active)
839 {
840  if(get_active() != active) {
841  set_state(active ? ENABLED : DISABLED);
842  }
843 }
844 
845 void rich_label::set_link_aware(bool link_aware)
846 {
847  if(link_aware != link_aware_) {
848  link_aware_ = link_aware;
849  update_canvas();
850  queue_redraw();
851  }
852 }
853 
855 {
856  if(color != link_color_) {
857  link_color_ = color;
858  update_canvas();
859  queue_redraw();
860  }
861 }
862 
864 {
865  if(state != state_) {
866  state_ = state;
867  queue_redraw();
868  }
869 }
870 
871 void rich_label::register_link_callback(std::function<void(std::string)> link_handler)
872 {
873  if(!link_aware_) {
874  return;
875  }
876 
877  connect_signal<event::LEFT_BUTTON_CLICK>(
878  std::bind(&rich_label::signal_handler_left_button_click, this, std::placeholders::_3));
879  connect_signal<event::MOUSE_MOTION>(
880  std::bind(&rich_label::signal_handler_mouse_motion, this, std::placeholders::_3, std::placeholders::_5));
881  connect_signal<event::MOUSE_LEAVE>(
882  std::bind(&rich_label::signal_handler_mouse_leave, this, std::placeholders::_3));
883  link_handler_ = std::move(link_handler);
884 }
885 
886 
888 {
889  DBG_GUI_E << "rich_label click";
890 
891  if(!get_link_aware()) {
892  return; // without marking event as "handled"
893  }
894 
895  point mouse = get_mouse_position() - get_origin();
896 
897  DBG_GUI_RL << "(mouse) " << mouse;
898  DBG_GUI_RL << "link count :" << links_.size();
899 
900  std::optional<std::string> click_target;
901  for(const auto& entry : links_) {
902  DBG_GUI_RL << "link " << entry.second;
903 
904  if(entry.first.contains(mouse)) {
905  click_target = entry.second;
906  break;
907  }
908  }
909 
910  if (click_target) {
911  DBG_GUI_RL << "Clicked link! dst = " << *click_target;
913  if(link_handler_) {
914  link_handler_(*click_target);
915  } else {
916  DBG_GUI_RL << "No registered link handler found";
917  }
918  }
919 
920  handled = true;
921 }
922 
924 {
925  DBG_GUI_E << "rich_label mouse motion";
926 
927  if(!get_link_aware()) {
928  return; // without marking event as "handled"
929  }
930 
931  point mouse = coordinate - get_origin();
932 
933  for(const auto& entry : links_) {
934  if(entry.first.contains(mouse)) {
935  update_mouse_cursor(true);
936  handled = true;
937  return;
938  }
939  }
940 
941  update_mouse_cursor(false);
942 }
943 
945 {
946  DBG_GUI_E << "rich_label mouse leave";
947 
948  if(!get_link_aware()) {
949  return; // without marking event as "handled"
950  }
951 
952  // We left the widget, so just unconditionally reset the cursor
953  update_mouse_cursor(false);
954 
955  handled = true;
956 }
957 
959 {
960  // Someone else may set the mouse cursor for us to something unusual (e.g.
961  // the WAIT cursor) so we ought to mess with that only if it's set to
962  // NORMAL or HYPERLINK.
963 
964  if(enable && cursor::get() == cursor::NORMAL) {
966  } else if(!enable && cursor::get() == cursor::HYPERLINK) {
968  }
969 }
970 
971 // }---------- DEFINITION ---------{
972 
975 {
976  DBG_GUI_P << "Parsing rich_label " << id;
977 
978  load_resolutions<resolution>(cfg);
979 }
980 
983  , text_color_enabled(color_t::from_rgba_string(cfg["text_font_color_enabled"].str()))
984  , text_color_disabled(color_t::from_rgba_string(cfg["text_font_color_disabled"].str()))
985  , link_color(cfg["link_color"].empty() ? font::YELLOW_COLOR : color_t::from_rgba_string(cfg["link_color"].str()))
986  , font_family(cfg["text_font_family"].str())
987  , font_size(cfg["text_font_size"].to_int(font::SIZE_NORMAL))
988  , font_style(cfg["text_font_style"].str("normal"))
989  , colors()
990 {
991  if(auto colors_cfg = cfg.optional_child("colors")) {
992  for(const auto& [name, value] : colors_cfg->attribute_range()) {
993  colors.try_emplace(name, color_t::from_rgba_string(value.str()));
994  }
995  }
996 
997  // Note the order should be the same as the enum state_t is rich_label.hpp.
998  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_enabled", missing_mandatory_wml_tag("rich_label_definition][resolution", "state_enabled")));
999  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_disabled", missing_mandatory_wml_tag("rich_label_definition][resolution", "state_disabled")));
1000 }
1001 
1002 // }---------- BUILDER -----------{
1003 
1004 namespace implementation
1005 {
1006 
1007 builder_rich_label::builder_rich_label(const config& cfg)
1009  , text_alignment(decode_text_alignment(cfg["text_alignment"]))
1010  , link_aware(cfg["link_aware"].to_bool(true))
1011  , padding(cfg["padding"].to_int(5))
1012 {
1013 }
1014 
1015 std::unique_ptr<widget> builder_rich_label::build() const
1016 {
1017  DBG_GUI_G << "Window builder: placed rich_label '" << id << "' with definition '"
1018  << definition << "'.";
1019 
1020  return std::make_unique<rich_label>(*this);
1021 }
1022 
1023 } // namespace implementation
1024 
1025 // }------------ END --------------
1026 
1027 } // namespace gui2
Variant for storing WML attributes.
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:157
config & add_child(std::string_view key)
Definition: config.cpp:436
void append(const config &cfg)
Append data from another config object to this one.
Definition: config.cpp:188
optional_config_impl< config > optional_child(std::string_view key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:380
auto all_children_view() const
In-order iteration over all children.
Definition: config.hpp:795
child_itors child_range(std::string_view key)
Definition: config.cpp:268
void append_attributes(const config &cfg)
Adds attributes from cfg.
Definition: config.cpp:174
std::string debug() const
Definition: config.cpp:1214
void append_children(const config &cfg)
Adds children from cfg.
Definition: config.cpp:167
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:38
point calculate_best_size() const override
See widget::calculate_best_size.
Definition: rich_label.cpp:277
void add_link(config &curr_item, const std::string &name, const std::string &dest, const point &origin, int img_width)
Definition: rich_label.cpp:177
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:923
state_t state_
Current state of the widget.
Definition: rich_label.hpp:171
void set_state(const state_t state)
Definition: rich_label.cpp:863
color_t text_color_enabled_
Base text color, enabled state.
Definition: rich_label.hpp:192
virtual bool get_active() const override
Gets the active state of the styled_widget.
Definition: rich_label.hpp:66
point get_text_size(config &text_cfg, unsigned width=0) const
size calculation functions
Definition: rich_label.cpp:111
std::function< void(std::string)> link_handler_
Definition: rich_label.hpp:269
virtual void update_canvas() override
Updates the canvas(ses).
Definition: rich_label.cpp:817
int padding_
Padding.
Definition: rich_label.hpp:244
void signal_handler_mouse_leave(bool &handled)
Mouse leave signal handler: checks if the cursor left a hyperlink.
Definition: rich_label.cpp:944
std::vector< std::pair< rect, std::string > > links_
link variables and functions
Definition: rich_label.hpp:267
void add_attribute(config &curr_item, const std::string &attr_name, const std::string &extra_data="", std::size_t start=0, std::size_t end=0)
Definition: rich_label.cpp:141
void place(const point &origin, const point &size) override
See widget::place.
Definition: rich_label.cpp:282
int font_size_
Base font size.
Definition: rich_label.hpp:217
point get_image_size(config &img_cfg) const
Definition: rich_label.cpp:120
config shapes_
Final list of shapes to be drawn on the canvas.
Definition: rich_label.hpp:235
unsigned short text_alpha_
Definition: rich_label.hpp:226
std::string font_family_
Base font family.
Definition: rich_label.hpp:212
unsigned init_w_
Width and height of the canvas.
Definition: rich_label.hpp:238
void set_dom(const config &dom)
Definition: rich_label.cpp:255
point get_xy_from_offset(const unsigned offset) const
Definition: rich_label.hpp:276
color_t get_color(const std::string &color)
If color is a predefined color set in resolution, return it, otherwise decode using font::string_to_c...
Definition: rich_label.cpp:92
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:291
void request_reduce_width(const unsigned maximum_width) override
See widget::request_reduce_width.
Definition: rich_label.cpp:268
virtual void set_active(const bool active) override
Sets the styled_widget's state.
Definition: rich_label.cpp:838
std::string font_style_
Base font style.
Definition: rich_label.hpp:222
state_t
Possible states of the widget.
Definition: rich_label.hpp:158
void register_link_callback(std::function< void(std::string)> link_handler)
Definition: rich_label.cpp:871
void signal_handler_left_button_click(bool &handled)
Left click signal handler: checks if we clicked on a hyperlink.
Definition: rich_label.cpp:887
int get_offset_from_xy(const point &position) const
Definition: rich_label.hpp:271
bool link_aware_
Whether the rich_label is link aware, rendering links with special formatting and handling click even...
Definition: rich_label.hpp:187
std::pair< std::size_t, std::size_t > add_text(config &curr_item, const std::string &text)
Definition: rich_label.cpp:132
wfl::map_formula_callable setup_text_renderer(config text_cfg, unsigned width=0) const
Definition: rich_label.cpp:98
void set_link_color(const color_t &color)
Definition: rich_label.cpp:854
std::map< std::string, color_t > predef_colors_
Color variables that can be used in place of colors strings, like <row bgcolor=color1>
Definition: rich_label.hpp:207
void set_link_aware(bool l)
Definition: rich_label.cpp:845
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:54
void default_text_config(config *txt_ptr, const point &pos, const int max_width, const t_string &text="")
Create template for text config that can be shown in canvas.
Definition: rich_label.cpp:791
void set_text_alpha(unsigned short alpha)
Definition: rich_label.cpp:829
color_t link_color_
What color links will be rendered in.
Definition: rich_label.hpp:202
void set_label(const t_string &text) override
Definition: rich_label.cpp:263
void update_mouse_cursor(bool enable)
Implementation detail for (re)setting the hyperlink cursor.
Definition: rich_label.cpp:958
std::size_t get_split_location(std::string_view text, const point &pos)
Definition: rich_label.cpp:231
std::pair< std::size_t, std::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:166
PangoAlignment get_text_alignment() const
std::vector< canvas > & get_canvases()
virtual void request_reduce_width(const unsigned maximum_width) override
See widget::request_reduce_width.
virtual void place(const point &origin, const point &size) override
See widget::place.
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
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(int fallback=0) const
Returns the variant's value as an integer.
Definition: variant.cpp:300
constexpr uint8_t ALPHA_OPAQUE
Definition: color.hpp:37
const config * cfg
static std::string _(const char *str)
Definition: gettext.hpp:97
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:206
@ 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:172
void point(int x, int y)
Draw a single point.
Definition: draw.cpp:214
void line(int from_x, int from_y, int to_x, int to_y)
Draw a line.
Definition: draw.cpp:189
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.
const color_t YELLOW_COLOR
constexpr float get_line_spacing_factor()
Definition: text.hpp:570
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:966
const int SIZE_TITLE
Definition: constants.cpp:31
const int SIZE_NORMAL
Definition: constants.cpp:20
color_t string_to_color(const std::string &color_str)
Return the color the string represents.
std::string sound_button_click
Definition: settings.cpp:50
Generic file dialog.
point get_mouse_position()
Returns the current mouse position.
Definition: helper.cpp:168
std::string_view debug_truncate(std::string_view text)
Returns a truncated version of the text.
Definition: helper.cpp:173
PangoAlignment decode_text_alignment(const std::string &alignment)
Converts a text alignment string to a text alignment.
Definition: helper.cpp:84
std::string encode_text_alignment(const PangoAlignment alignment)
Converts a PangoAlignment to its string representation.
Definition: helper.cpp:130
Contains the implementation details for lexical_cast and shouldn't be used directly.
static log_domain dom("general")
config parse_text(const std::string &text)
Parse a xml style marked up text string.
Definition: markup.cpp:444
void play_UI_sound(const std::string &files)
Definition: sound.cpp:1051
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:81
int stoi(std::string_view str)
Same interface as std::stoi and meant as a drop in replacement, except:
Definition: charconv.hpp:156
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition: general.hpp:87
std::vector< std::string > split(const config_attribute_value &val)
int get_pixel_scale()
Get the current active pixel scale multiplier.
Definition: video.cpp:498
int w
Definition: pathfind.cpp:188
#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:45
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:51
std::string to_hex_string() const
Returns the stored color in rrggbb hex format.
Definition: color.cpp:88
static color_t from_rgba_string(std::string_view c)
Creates a new color_t object from a string variable in "R,G,B,A" format.
Definition: color.cpp:23
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
std::string definition
Parameters for the styled_widget.
std::vector< state_definition > state
std::map< std::string, color_t > colors
Definition: rich_label.hpp:334
rich_label_definition(const config &cfg)
Definition: rich_label.cpp:973
Holds a 2D point.
Definition: point.hpp:25
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:49
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