The Battle for Wesnoth  1.19.15+dev
rich_label.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2024 - 2025
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 "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_(builder.width(get_screen_size_variables()))
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  std::size_t len = get_offset_from_xy(pos);
234  if (len >= text.size() - 1) {
235  return text.size() - 1;
236  }
237 
238  // break only at word boundary
239  char c;
240  while(!std::isspace(c = text[len])) {
241  len--;
242  if(len == 0) {
243  break;
244  }
245  }
246 
247  return len;
248 }
249 
251 {
252  std::tie(shapes_, size_) = get_parsed_text(dom, point(0,0), init_w_, true);
253  update_canvas();
254  queue_redraw();
255 }
256 
258 {
260 }
261 
262 std::pair<config, point> rich_label::get_parsed_text(
263  const config& parsed_text,
264  const point& origin,
265  const unsigned init_width,
266  const bool finalize)
267 {
268  // Initial width
269  DBG_GUI_RL << "Initial width: " << init_width;
270 
271  // Initialization
272  unsigned x = 0;
273  unsigned prev_blk_height = origin.y;
274  unsigned text_height = 0;
275  unsigned h = 0;
276  unsigned w = 0;
277 
278  if(finalize) {
279  links_.clear();
280  }
281 
282  config text_dom;
283  config* curr_item = nullptr;
284 
285  bool is_text = false;
286  bool is_image = false;
287  bool wrap_mode = false;
288  bool new_text_block = false;
289 
290  point pos(origin);
291  point float_pos, float_size;
292  point img_size;
293 
294  DBG_GUI_RL << parsed_text.debug();
295 
296  for(const auto [orig_key, child] : parsed_text.all_children_view()) {
297 
298  const std::string key = (orig_key == "img" && !child["float"].to_bool(false)) ? "inline_image" : orig_key;
299 
300  DBG_GUI_RL << "\n Trying to layout tag: " << key;
301 
302  if(key == "img") {
303  prev_blk_height += text_height;
304  text_height = 0;
305 
306  const std::string& align = child["align"].str("left");
307 
308  curr_item = &(text_dom.add_child("image"));
309  (*curr_item)["name"] = child["src"];
310  (*curr_item)["x"] = 0;
311  (*curr_item)["y"] = 0;
312  (*curr_item)["w"] = "(image_width)";
313  (*curr_item)["h"] = "(image_height)";
314 
315  const point& curr_img_size = get_image_size(*curr_item);
316 
317  if (align == "right") {
318  float_pos.x = init_width - curr_img_size.x;
319  } else if (align == "middle" || align == "center") {
320  // works for single image only
321  float_pos.x = float_size.x + (init_width - curr_img_size.x)/2;
322  }
323 
324  if (is_image) {
325  float_pos.y += float_size.y;
326  }
327 
328  (*curr_item)["x"] = float_pos.x;
329  (*curr_item)["y"] = pos.y + float_pos.y;
330 
331  float_size.x = curr_img_size.x + padding_;
332  float_size.y += curr_img_size.y + padding_;
333 
334  x = ((align == "left") ? float_size.x : 0);
335  pos.x += ((align == "left") ? float_size.x : 0);
336 
337  wrap_mode = true;
338 
339  w = std::max(w, x);
340 
341  is_image = true;
342  is_text = false;
343  new_text_block = true;
344 
345  DBG_GUI_RL << key << ": src=" << child["src"] << ", size=" << img_size;
346  DBG_GUI_RL << "wrap turned on.";
347  } else if(key == "clear") {
348  // Moves the text below the preceding floating image and turns off wrapping
349  wrap_mode = false;
350  prev_blk_height += float_size.y;
351  pos.y += float_size.y;
352  float_size = point(0, 0);
353  pos.x = origin.x;
354 
355  DBG_GUI_RL << key;
356  DBG_GUI_RL << "wrap turned off";
357  } else if(key == "table") {
358  if(curr_item == nullptr) {
359  curr_item = &(text_dom.add_child("text"));
360  default_text_config(curr_item, pos, init_width);
361  new_text_block = false;
362  }
363 
364  // table doesn't support floating images alongside
365  img_size = point(0,0);
366  float_size = point(0,0);
367  x = origin.x;
368  prev_blk_height += text_height + padding_;
369  text_height = 0;
370  pos = point(origin.x, prev_blk_height + padding_);
371 
372  // init table vars
373  unsigned col_idx = 0, row_idx = 0;
374  unsigned rows = child.child_count("row");
375  unsigned columns = 1;
376  if(rows > 0) {
377  columns = child.mandatory_child("row").child_count("col");
378  }
379  columns = (columns == 0) ? 1 : columns;
380  int init_cell_width;
381  if(child["width"] == "fill") {
382  init_cell_width = init_width/columns;
383  } else {
384  init_cell_width = child["width"].to_int(init_width)/columns;
385  }
386  std::vector<int> col_widths(columns, 0);
387  std::vector<int> row_heights(rows, 0);
388 
389  is_text = false;
390  new_text_block = true;
391  is_image = false;
392 
393  DBG_GUI_RL << "start table: " << "row=" << rows << " col=" << columns
394  << " width=" << init_cell_width*columns;
395 
396  const auto get_padding = [this](const config::attribute_value& val) {
397  if(val.blank()) {
398  return std::array{ padding_, padding_ };
399  } else {
400  auto paddings = utils::split(val.str(), ' ');
401  if(paddings.size() == 1) {
402  return std::array{ std::stoi(paddings[0]), std::stoi(paddings[0]) };
403  } else {
404  return std::array{ std::stoi(paddings[0]), std::stoi(paddings[1]) };
405  }
406  }
407  };
408 
409  std::array<int, 2> row_paddings;
410  boost::multi_array<point, 2> cell_sizes(boost::extents[rows][columns]);
411 
412  // optimal col width calculation
413  for(const config& row : child.child_range("row")) {
414  pos.x = origin.x;
415  col_idx = 0;
416 
417  // order: top padding|bottom padding
418  row_paddings = get_padding(row["padding"]);
419 
420  pos.y += row_paddings[0];
421  for(const config& col : row.child_range("col")) {
422  DBG_GUI_RL << "table cell origin (pre-layout): " << pos.x << ", " << pos.y;
423  config col_cfg;
424  col_cfg.append_children(col);
425  config& col_txt_cfg = col_cfg.add_child("text");
426  col_txt_cfg.append_attributes(col);
427 
428  // order: left padding|right padding
429  std::array<int, 2> col_paddings = get_padding(col["padding"]);
430  int cell_width = init_cell_width - col_paddings[0] - col_paddings[1];
431 
432  pos.x += col_paddings[0];
433  // attach data
434  auto links = links_;
435  cell_sizes[row_idx][col_idx] = get_parsed_text(col_cfg, pos, init_cell_width).second;
436  links_ = links;
437 
438  // column post-processing
439  row_heights[row_idx] = std::max(row_heights[row_idx], cell_sizes[row_idx][col_idx].y);
440  if(!child["width"].empty()) {
441  col_widths[col_idx] = cell_width;
442  }
443  col_widths[col_idx] = std::max(col_widths[col_idx], cell_sizes[row_idx][col_idx].x);
444  if(child["width"].empty()) {
445  col_widths[col_idx] = std::min(col_widths[col_idx], cell_width);
446  }
447 
448  DBG_GUI_RL << "table row " << row_idx << " height: " << row_heights[row_idx]
449  << "col " << col_idx << " width: " << col_widths[col_idx];
450 
451  pos.x += cell_width;
452  pos.x += col_paddings[1];
453  col_idx++;
454  }
455 
456  pos.y += row_heights[row_idx] + row_paddings[1];
457  row_idx++;
458  }
459 
460  // table layouting
461  row_idx = 0;
462  pos = point(origin.x, prev_blk_height);
463  for(const config& row : child.child_range("row")) {
464  pos.x = origin.x;
465  col_idx = 0;
466 
467  if(!row["bgcolor"].blank()) {
468  config bg_base;
469  config& bgbox = bg_base.add_child("rectangle");
470  bgbox["x"] = origin.x;
471  bgbox["y"] = pos.y;
472  bgbox["w"] = std::accumulate(col_widths.begin(), col_widths.end(), 0) + 2*(row_paddings[0] + row_paddings[1])*columns;
473  bgbox["h"] = row_paddings[0] + row_heights[row_idx] + row_paddings[1];
474  bgbox["fill_color"] = get_color(row["bgcolor"].str()).to_rgba_string();
475  text_dom.append(std::move(bg_base));
476  }
477 
478  row_paddings = get_padding(row["padding"]);
479  pos.y += row_paddings[0];
480 
481  for(const config& col : row.child_range("col")) {
482  DBG_GUI_RL << "table row " << row_idx << " height: " << row_heights[row_idx]
483  << "col " << col_idx << " width: " << col_widths[col_idx];
484  DBG_GUI_RL << "cell origin: " << pos;
485 
486  config col_cfg;
487  col_cfg.append_children(col);
488  config& col_txt_cfg = col_cfg.add_child("text");
489  col_txt_cfg.append_attributes(col);
490 
491  // order: left padding|right padding
492  std::array<int, 2> col_paddings = get_padding(col["padding"]);
493 
494  pos.x += col_paddings[0];
495 
496  const std::string& valign = row["valign"].str("center");
497  const std::string& halign = col["halign"].str("left");
498 
499  // set position according to alignment keys
500  point text_pos(pos);
501  if (valign == "center" || valign == "middle") {
502  text_pos.y += (row_heights[row_idx] - cell_sizes[row_idx][col_idx].y)/2;
503  } else if (valign == "bottom") {
504  text_pos.y += row_heights[row_idx] - cell_sizes[row_idx][col_idx].y;
505  }
506  if (halign == "center" || halign == "middle") {
507  text_pos.x += (col_widths[col_idx] - cell_sizes[row_idx][col_idx].x)/2;
508  } else if (halign == "right") {
509  text_pos.x += col_widths[col_idx] - cell_sizes[row_idx][col_idx].x;
510  }
511 
512  // attach data
513  auto [table_elem, size] = get_parsed_text(col_cfg, text_pos, col_widths[col_idx]);
514  text_dom.append(std::move(table_elem));
515  pos.x += col_widths[col_idx];
516  pos.x += col_paddings[1];
517 
518  auto [_, end_cfg] = text_dom.all_children_view().back();
519  end_cfg["maximum_width"] = col_widths[col_idx];
520 
521  DBG_GUI_RL << "jump to next column";
522 
523  if(!is_image) {
524  new_text_block = true;
525  }
526  is_image = false;
527  col_idx++;
528  }
529 
530  pos.y += row_heights[row_idx];
531  pos.y += row_paddings[1];
532  DBG_GUI_RL << "row height: " << row_heights[row_idx];
533  row_idx++;
534  }
535 
536  w = std::max(w, static_cast<unsigned>(pos.x));
537  prev_blk_height = pos.y;
538  text_height = 0;
539  pos.x = origin.x;
540 
541  is_image = false;
542  is_text = false;
543 
544  x = origin.x;
545 
546  } else {
547  std::string line = child["text"];
548 
549  if (!finalize && (line.empty() && key == "text")) {
550  continue;
551  }
552 
553  if (curr_item == nullptr || new_text_block) {
554  curr_item = &(text_dom.add_child("text"));
555  default_text_config(curr_item, pos, init_width - pos.x - float_size.x);
556  new_text_block = false;
557  }
558 
559  // }---------- TEXT TAGS -----------{
560  int tmp_h = get_text_size(*curr_item, init_width - (x == 0 ? float_size.x : x)).y;
561 
562  if(is_text && key == "text") {
563  add_text_with_attribute(*curr_item, "\n\n");
564  }
565 
566  is_text = false;
567  is_image = false;
568 
569  if (key == "inline_image") {
570 
571  // Inline image is rendered as a custom text glyph (pango shape attribute)
572  // FIXME: If linebreak (\n) is followed by an inline image
573  // the text size is calculated wrongly as being decreased.
574  // Workaround: append a zero width space always in front of the image.
575  add_text(*curr_item, "\u200b");
576  add_text_with_attribute(*curr_item, "\ufffc", "image", child["src"]);
577 
578  DBG_GUI_RL << key << ": src=" << child["src"];
579 
580  } else if(key == "ref") {
581 
582  add_link(*curr_item, line, child["dst"], point(x + origin.x, prev_blk_height), float_size.x);
583 
584  DBG_GUI_RL << key << ": dst=" << child["dst"];
585 
586  } else if(utils::contains(format_tags, key)) {
587  // TODO only the formatting tags here support nesting
588 
589  add_text_with_attribute(*curr_item, line, key);
590 
591  // Calculate the location of the nested children
592  setup_text_renderer(*curr_item, init_w_ - origin.x - float_size.x);
593  point child_origin = origin + get_xy_from_offset(utf8::size((*curr_item)["text"].str()));
594  child_origin.y += prev_blk_height;
595 
596  config parsed_children = get_parsed_text(child, child_origin, init_width).first;
597 
598  for(const auto [parsed_key, parsed_cfg] : parsed_children.all_children_view()) {
599  if(parsed_key == "text") {
600  const auto [start, end] = add_text(*curr_item, parsed_cfg["text"]);
601  for (const config& attr : parsed_cfg.child_range("attribute")) {
602  add_attribute(*curr_item, attr["name"], attr["value"], start + attr["start"].to_int(), start + attr["end"].to_int());
603  }
604  add_attribute(*curr_item, key, "", start, end);
605  } else {
606  text_dom.add_child(parsed_key, parsed_cfg);
607  }
608  }
609 
610  DBG_GUI_RL << key << ": text=" << gui2::debug_truncate(line);
611 
612  } else if(key == "header" || key == "h") {
613 
614  const auto [start, end] = add_text(*curr_item, line);
615  add_attribute(*curr_item, "weight", "heavy", start, end);
616  add_attribute(*curr_item, "color", "white", start, end);
617  add_attribute(*curr_item, "size", std::to_string(font::SIZE_TITLE - 2), start, end);
618 
619  DBG_GUI_RL << key << ": text=" << line;
620 
621  } else if(key == "character_entity") {
622 
623  line = "&" + child["name"].str() + ";";
624 
625  const auto [start, end] = add_text(*curr_item, line);
626  add_attribute(*curr_item, "face", "monospace", start, end);
627  add_attribute(*curr_item, "color", "red", start, end);
628 
629  DBG_GUI_RL << key << ": text=" << line;
630 
631  } else if(key == "span" || key == "format") {
632 
633  const auto [start, end] = add_text(*curr_item, line);
634  DBG_GUI_RL << "span/format: text=" << line;
635  DBG_GUI_RL << "attributes:";
636 
637  for (const auto& [key, value] : child.attribute_range()) {
638  if (key != "text") {
639  add_attribute(*curr_item, key, value, start, end);
640  DBG_GUI_RL << key << "=" << value;
641  }
642  }
643 
644  } else if (key == "text") {
645 
646  DBG_GUI_RL << "text: text=" << gui2::debug_truncate(line) << "...";
647 
648  add_text(*curr_item, line);
649 
650  point text_size = get_text_size(*curr_item, init_width - (x == 0 ? float_size.x : x));
651 
652  is_text = true;
653 
654  // Text wrapping around floating images
655  if(wrap_mode && (float_size.y > 0) && (text_size.y > float_size.y)) {
656  DBG_GUI_RL << "wrap start";
657 
658  std::size_t len = get_split_location((*curr_item)["text"].str(), point(init_width - float_size.x, float_size.y * video::get_pixel_scale()));
659  DBG_GUI_RL << "wrap around area: " << float_size;
660 
661  std::string removed_part = (*curr_item)["text"].str().substr(len+1);
662 
663  // first part of the text
664  // get_split_location always splits at word bounds.
665  // substr(len) will include a space, so we skip that.
666  (*curr_item)["text"] = (*curr_item)["text"].str().substr(0, len);
667  (*curr_item)["maximum_width"] = init_width - float_size.x;
668  float_size = point(0,0);
669 
670  // Height update
671  int ah = get_text_size(*curr_item, init_width - float_size.x).y;
672  if(tmp_h > ah) {
673  tmp_h = 0;
674  }
675  text_height += ah - tmp_h;
676  prev_blk_height += text_height;
677  pos = point(origin.x, prev_blk_height);
678 
679  DBG_GUI_RL << "wrap: " << prev_blk_height << "," << text_height;
680  text_height = 0;
681 
682  // New text block
683  x = origin.x;
684  wrap_mode = false;
685 
686  // rest of the text
687  curr_item = &(text_dom.add_child("text"));
688  default_text_config(curr_item, pos, init_width - pos.x);
689  tmp_h = get_text_size(*curr_item, init_width).y;
690  add_text_with_attribute(*curr_item, removed_part);
691 
692  } else if((float_size.y > 0) && (text_size.y < float_size.y)) {
693  //TODO padding?
694  // text height less than floating image's height, don't split
695  DBG_GUI_RL << "no wrap";
696  pos.y += text_size.y;
697  }
698 
699  if(!wrap_mode) {
700  float_size = point(0,0);
701  }
702  }
703 
704  point size = get_text_size(*curr_item, init_width - (x == 0 ? float_size.x : x));
705  // update text size and widget height
706  if(tmp_h > size.y) {
707  tmp_h = 0;
708  }
709  w = std::max(w, x + static_cast<unsigned>(size.x));
710 
711  text_height += size.y - tmp_h;
712  pos.y += size.y - tmp_h;
713  }
714 
715  if(!is_image && !wrap_mode && img_size.y > 0) {
716  img_size = point(0,0);
717  }
718 
719  if(curr_item) {
720  DBG_GUI_RL << "Item:\n" << curr_item->debug();
721  }
722  DBG_GUI_RL << "X: " << x;
723  DBG_GUI_RL << "Prev block height: " << prev_blk_height << " Current text block height: " << text_height;
724  DBG_GUI_RL << "Height: " << h;
725  h = text_height + prev_blk_height;
726  DBG_GUI_RL << "-----------";
727  } // for loop ends
728 
729  if(w == 0) {
730  w = init_width;
731  }
732 
733  // DEBUG: draw boxes around links
734  #if LINK_DEBUG_BORDER
735  if(finalize) {
736  for(const auto& entry : links_) {
737  config& link_rect_cfg = text_dom.add_child("rectangle");
738  link_rect_cfg["x"] = entry.first.x;
739  link_rect_cfg["y"] = entry.first.y;
740  link_rect_cfg["w"] = entry.first.w;
741  link_rect_cfg["h"] = entry.first.h;
742  link_rect_cfg["border_thickness"] = 1;
743  link_rect_cfg["border_color"] = "255, 180, 0, 255";
744  }
745  }
746  #endif
747 
748  // TODO float and a mix of floats and images and tables
749  h = std::max(static_cast<unsigned>(img_size.y), h);
750 
751  DBG_GUI_RL << "[\n" << text_dom.debug() << "]\n";
752 
753  DBG_GUI_RL << "Width: " << w << " Height: " << h << " Origin: " << origin;
754  return { text_dom, point(w, h - origin.y) };
755 } // function ends
756 
758  config* txt_ptr,
759  const point& pos,
760  const int max_width,
761  const t_string& text)
762 {
763  if(txt_ptr != nullptr) {
764  (*txt_ptr)["text"] = text;
765  (*txt_ptr)["color"] = text_color_enabled_.to_rgba_string();
766  (*txt_ptr)["font_family"] = font_family_;
767  (*txt_ptr)["font_size"] = font_size_;
768  (*txt_ptr)["font_style"] = font_style_;
769  (*txt_ptr)["text_alignment"] = encode_text_alignment(get_text_alignment());
770  (*txt_ptr)["line_spacing"] = 0;
771  (*txt_ptr)["x"] = pos.x;
772  (*txt_ptr)["y"] = pos.y;
773  (*txt_ptr)["w"] = "(text_width)";
774  (*txt_ptr)["h"] = "(text_height)";
775  (*txt_ptr)["maximum_width"] = max_width;
776  (*txt_ptr)["parse_text_as_formula"] = false;
777  add_attribute(*txt_ptr,
778  "line_height",
779  std::to_string(font::get_line_spacing_factor()));
780  }
781 }
782 
784 {
785  for(canvas& tmp : get_canvases()) {
786  tmp.set_shapes(shapes_, true);
787  tmp.set_variable("width", wfl::variant(init_w_));
788  tmp.set_variable("padding", wfl::variant(padding_));
789  // Disable ellipsization so that text wrapping can work
790  tmp.set_variable("text_wrap_mode", wfl::variant(PANGO_ELLIPSIZE_NONE));
791  tmp.set_variable("text_alpha", wfl::variant(text_alpha_));
792  }
793 }
794 
795 void rich_label::set_text_alpha(unsigned short alpha)
796 {
797  if(alpha != text_alpha_) {
798  text_alpha_ = alpha;
799  update_canvas();
800  queue_redraw();
801  }
802 }
803 
804 void rich_label::set_active(const bool active)
805 {
806  if(get_active() != active) {
807  set_state(active ? ENABLED : DISABLED);
808  }
809 }
810 
811 void rich_label::set_link_aware(bool link_aware)
812 {
813  if(link_aware != link_aware_) {
814  link_aware_ = link_aware;
815  update_canvas();
816  queue_redraw();
817  }
818 }
819 
821 {
822  if(color != link_color_) {
823  link_color_ = color;
824  update_canvas();
825  queue_redraw();
826  }
827 }
828 
830 {
831  if(state != state_) {
832  state_ = state;
833  queue_redraw();
834  }
835 }
836 
837 void rich_label::register_link_callback(std::function<void(std::string)> link_handler)
838 {
839  if(!link_aware_) {
840  return;
841  }
842 
843  connect_signal<event::LEFT_BUTTON_CLICK>(
844  std::bind(&rich_label::signal_handler_left_button_click, this, std::placeholders::_3));
845  connect_signal<event::MOUSE_MOTION>(
846  std::bind(&rich_label::signal_handler_mouse_motion, this, std::placeholders::_3, std::placeholders::_5));
847  connect_signal<event::MOUSE_LEAVE>(
848  std::bind(&rich_label::signal_handler_mouse_leave, this, std::placeholders::_3));
849  link_handler_ = std::move(link_handler);
850 }
851 
852 
854 {
855  DBG_GUI_E << "rich_label click";
856 
857  if(!get_link_aware()) {
858  return; // without marking event as "handled"
859  }
860 
861  point mouse = get_mouse_position() - get_origin();
862 
863  DBG_GUI_RL << "(mouse) " << mouse;
864  DBG_GUI_RL << "link count :" << links_.size();
865 
866  for(const auto& entry : links_) {
867  DBG_GUI_RL << "link " << entry.first;
868 
869  if(entry.first.contains(mouse)) {
870  DBG_GUI_RL << "Clicked link! dst = " << entry.second;
872  if(link_handler_) {
873  link_handler_(entry.second);
874  } else {
875  DBG_GUI_RL << "No registered link handler found";
876  }
877 
878  }
879  }
880 
881  handled = true;
882 }
883 
885 {
886  DBG_GUI_E << "rich_label mouse motion";
887 
888  if(!get_link_aware()) {
889  return; // without marking event as "handled"
890  }
891 
892  point mouse = coordinate - get_origin();
893 
894  for(const auto& entry : links_) {
895  if(entry.first.contains(mouse)) {
896  update_mouse_cursor(true);
897  handled = true;
898  return;
899  }
900  }
901 
902  update_mouse_cursor(false);
903 }
904 
906 {
907  DBG_GUI_E << "rich_label mouse leave";
908 
909  if(!get_link_aware()) {
910  return; // without marking event as "handled"
911  }
912 
913  // We left the widget, so just unconditionally reset the cursor
914  update_mouse_cursor(false);
915 
916  handled = true;
917 }
918 
920 {
921  // Someone else may set the mouse cursor for us to something unusual (e.g.
922  // the WAIT cursor) so we ought to mess with that only if it's set to
923  // NORMAL or HYPERLINK.
924 
925  if(enable && cursor::get() == cursor::NORMAL) {
927  } else if(!enable && cursor::get() == cursor::HYPERLINK) {
929  }
930 }
931 
932 // }---------- DEFINITION ---------{
933 
936 {
937  DBG_GUI_P << "Parsing rich_label " << id;
938 
939  load_resolutions<resolution>(cfg);
940 }
941 
944  , text_color_enabled(color_t::from_rgba_string(cfg["text_font_color_enabled"].str()))
945  , text_color_disabled(color_t::from_rgba_string(cfg["text_font_color_disabled"].str()))
946  , link_color(cfg["link_color"].empty() ? font::YELLOW_COLOR : color_t::from_rgba_string(cfg["link_color"].str()))
947  , font_family(cfg["text_font_family"].str())
948  , font_size(cfg["text_font_size"].to_int(font::SIZE_NORMAL))
949  , font_style(cfg["text_font_style"].str("normal"))
950  , colors()
951 {
952  if(auto colors_cfg = cfg.optional_child("colors")) {
953  for(const auto& [name, value] : colors_cfg->attribute_range()) {
954  colors.try_emplace(name, color_t::from_rgba_string(value.str()));
955  }
956  }
957 
958  // Note the order should be the same as the enum state_t is rich_label.hpp.
959  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_enabled", missing_mandatory_wml_tag("rich_label_definition][resolution", "state_enabled")));
960  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_disabled", missing_mandatory_wml_tag("rich_label_definition][resolution", "state_disabled")));
961 }
962 
963 // }---------- BUILDER -----------{
964 
965 namespace implementation
966 {
967 
968 builder_rich_label::builder_rich_label(const config& cfg)
970  , text_alignment(decode_text_alignment(cfg["text_alignment"]))
971  , link_aware(cfg["link_aware"].to_bool(true))
972  , width(cfg["width"], 500)
973  , padding(cfg["padding"].to_int(5))
974 {
975 }
976 
977 std::unique_ptr<widget> builder_rich_label::build() const
978 {
979  DBG_GUI_G << "Window builder: placed rich_label '" << id << "' with definition '"
980  << definition << "'.";
981 
982  return std::make_unique<rich_label>(*this);
983 }
984 
985 } // namespace implementation
986 
987 // }------------ END --------------
988 
989 } // 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:158
void append(const config &cfg)
Append data from another config object to this one.
Definition: config.cpp:188
auto all_children_view() const
In-order iteration over all children.
Definition: config.hpp:796
child_itors child_range(config_key_type 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:1230
void append_children(const config &cfg)
Adds children from cfg.
Definition: config.cpp:167
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:380
config & add_child(config_key_type key)
Definition: config.cpp:436
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
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:884
state_t state_
Current state of the widget.
Definition: rich_label.hpp:171
void set_state(const state_t state)
Definition: rich_label.cpp:829
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:267
virtual void update_canvas() override
Updates the canvas(ses).
Definition: rich_label.cpp:783
int padding_
Padding.
Definition: rich_label.hpp:242
void signal_handler_mouse_leave(bool &handled)
Mouse leave signal handler: checks if the cursor left a hyperlink.
Definition: rich_label.cpp:905
std::vector< std::pair< rect, std::string > > links_
link variables and functions
Definition: rich_label.hpp:265
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
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:250
point get_xy_from_offset(const unsigned offset) const
Definition: rich_label.hpp:274
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:262
virtual void set_active(const bool active) override
Sets the styled_widget's state.
Definition: rich_label.cpp:804
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:837
void signal_handler_left_button_click(bool &handled)
Left click signal handler: checks if we clicked on a hyperlink.
Definition: rich_label.cpp:853
int get_offset_from_xy(const point &position) const
Definition: rich_label.hpp:269
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:820
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:811
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:757
void set_text_alpha(unsigned short alpha)
Definition: rich_label.cpp:795
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:257
void update_mouse_cursor(bool enable)
Implementation detail for (re)setting the hyperlink cursor.
Definition: rich_label.cpp:919
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()
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:291
constexpr uint8_t ALPHA_OPAQUE
Definition: color.hpp:47
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:218
@ 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:178
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.
void get_screen_size_variables(wfl::map_formula_callable &variable)
Gets a formula object with the screen size.
Definition: helper.cpp:151
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:403
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:85
int stoi(std::string_view str)
Same interface as std::stoi and meant as a drop in replacement, except:
Definition: charconv.hpp:155
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:61
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
Definition: rich_label.cpp:977
std::string definition
Parameters for the styled_widget.
std::vector< state_definition > state
std::map< std::string, color_t > colors
Definition: rich_label.hpp:328
rich_label_definition(const config &cfg)
Definition: rich_label.cpp:934
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