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