The Battle for Wesnoth  1.19.11+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  row_paddings = get_padding(row["padding"]);
477  pos.y += row_paddings[0];
478 
479  for(const config& col : row.child_range("col")) {
480  DBG_GUI_RL << "table row " << row_idx << " height: " << row_heights[row_idx]
481  << "col " << col_idx << " width: " << col_widths[col_idx];
482  DBG_GUI_RL << "cell origin: " << pos;
483 
484  config col_cfg;
485  col_cfg.append_children(col);
486  config& col_txt_cfg = col_cfg.add_child("text");
487  col_txt_cfg.append_attributes(col);
488 
489  // order: left padding|right padding
490  std::array<int, 2> col_paddings = get_padding(col["padding"]);
491 
492  pos.x += col_paddings[0];
493 
494  const std::string& valign = row["valign"].str("center");
495  const std::string& halign = col["halign"].str("left");
496 
497  // set position according to alignment keys
498  point text_pos(pos);
499  if (valign == "center" || valign == "middle") {
500  text_pos.y += (row_heights[row_idx] - cell_sizes[row_idx][col_idx].y)/2;
501  } else if (valign == "bottom") {
502  text_pos.y += row_heights[row_idx] - cell_sizes[row_idx][col_idx].y;
503  }
504  if (halign == "center" || halign == "middle") {
505  text_pos.x += (col_widths[col_idx] - cell_sizes[row_idx][col_idx].x)/2;
506  } else if (halign == "right") {
507  text_pos.x += col_widths[col_idx] - cell_sizes[row_idx][col_idx].x;
508  }
509 
510  // attach data
511  auto [table_elem, size] = get_parsed_text(col_cfg, text_pos, col_widths[col_idx]);
512  text_dom.append(std::move(table_elem));
513  pos.x += col_widths[col_idx];
514  pos.x += col_paddings[1];
515 
516  auto [_, end_cfg] = text_dom.all_children_view().back();
517  end_cfg["maximum_width"] = col_widths[col_idx];
518 
519  DBG_GUI_RL << "jump to next column";
520 
521  if(!is_image) {
522  new_text_block = true;
523  }
524  is_image = false;
525  col_idx++;
526  }
527 
528  pos.y += row_heights[row_idx];
529  pos.y += row_paddings[1];
530  DBG_GUI_RL << "row height: " << row_heights[row_idx];
531  row_idx++;
532  }
533 
534  w = std::max(w, static_cast<unsigned>(pos.x));
535  prev_blk_height = pos.y;
536  text_height = 0;
537  pos.x = origin.x;
538 
539  is_image = false;
540  is_text = false;
541 
542  x = origin.x;
543 
544  } else {
545  std::string line = child["text"];
546 
547  if (!finalize && (line.empty() && key == "text")) {
548  continue;
549  }
550 
551  if (curr_item == nullptr || new_text_block) {
552  curr_item = &(text_dom.add_child("text"));
553  default_text_config(curr_item, pos, init_width - pos.x - float_size.x);
554  new_text_block = false;
555  }
556 
557  // }---------- TEXT TAGS -----------{
558  int tmp_h = get_text_size(*curr_item, init_width - (x == 0 ? float_size.x : x)).y;
559 
560  if(is_text && key == "text") {
561  add_text_with_attribute(*curr_item, "\n\n");
562  }
563 
564  is_text = false;
565  is_image = false;
566 
567  if (key == "inline_image") {
568 
569  // Inline image is rendered as a custom text glyph (pango shape attribute)
570  // FIXME: If linebreak (\n) is followed by an inline image
571  // the text size is calculated wrongly as being decreased.
572  // Workaround: append a zero width space always in front of the image.
573  add_text(*curr_item, "\u200b");
574  add_text_with_attribute(*curr_item, "\ufffc", "image", child["src"]);
575 
576  DBG_GUI_RL << key << ": src=" << child["src"];
577 
578  } else if(key == "ref") {
579 
580  add_link(*curr_item, line, child["dst"], point(x + origin.x, prev_blk_height), float_size.x);
581 
582  DBG_GUI_RL << key << ": dst=" << child["dst"];
583 
584  } else if(std::find(format_tags.begin(), format_tags.end(), key) != format_tags.end()) {
585  // TODO only the formatting tags here support nesting
586 
587  add_text_with_attribute(*curr_item, line, key);
588 
589  // Calculate the location of the nested children
590  setup_text_renderer(*curr_item, init_w_ - origin.x - float_size.x);
591  point child_origin = origin + get_xy_from_offset(utf8::size((*curr_item)["text"].str()));
592  child_origin.y += prev_blk_height;
593 
594  config parsed_children = get_parsed_text(child, child_origin, init_width).first;
595 
596  for(const auto [parsed_key, parsed_cfg] : parsed_children.all_children_view()) {
597  if(parsed_key == "text") {
598  const auto [start, end] = add_text(*curr_item, parsed_cfg["text"]);
599  for (const config& attr : parsed_cfg.child_range("attribute")) {
600  add_attribute(*curr_item, attr["name"], attr["value"], start + attr["start"].to_int(), start + attr["end"].to_int());
601  }
602  add_attribute(*curr_item, key, "", start, end);
603  } else {
604  text_dom.add_child(parsed_key, parsed_cfg);
605  }
606  }
607 
608  DBG_GUI_RL << key << ": text=" << gui2::debug_truncate(line);
609 
610  } else if(key == "header" || key == "h") {
611 
612  const auto [start, end] = add_text(*curr_item, line);
613  add_attribute(*curr_item, "weight", "heavy", start, end);
614  add_attribute(*curr_item, "color", "white", start, end);
615  add_attribute(*curr_item, "size", std::to_string(font::SIZE_TITLE - 2), start, end);
616 
617  DBG_GUI_RL << key << ": text=" << line;
618 
619  } else if(key == "character_entity") {
620 
621  line = "&" + child["name"].str() + ";";
622 
623  const auto [start, end] = add_text(*curr_item, line);
624  add_attribute(*curr_item, "face", "monospace", start, end);
625  add_attribute(*curr_item, "color", "red", start, end);
626 
627  DBG_GUI_RL << key << ": text=" << line;
628 
629  } else if(key == "span" || key == "format") {
630 
631  const auto [start, end] = add_text(*curr_item, line);
632  DBG_GUI_RL << "span/format: text=" << line;
633  DBG_GUI_RL << "attributes:";
634 
635  for (const auto& [key, value] : child.attribute_range()) {
636  if (key != "text") {
637  add_attribute(*curr_item, key, value, start, end);
638  DBG_GUI_RL << key << "=" << value;
639  }
640  }
641 
642  } else if (key == "text") {
643 
644  DBG_GUI_RL << "text: text=" << gui2::debug_truncate(line) << "...";
645 
646  add_text(*curr_item, line);
647 
648  point text_size = get_text_size(*curr_item, init_width - (x == 0 ? float_size.x : x));
649 
650  is_text = true;
651 
652  // Text wrapping around floating images
653  if(wrap_mode && (float_size.y > 0) && (text_size.y > float_size.y)) {
654  DBG_GUI_RL << "wrap start";
655 
656  size_t len = get_split_location((*curr_item)["text"].str(), point(init_width - float_size.x, float_size.y * video::get_pixel_scale()));
657  DBG_GUI_RL << "wrap around area: " << float_size;
658 
659  std::string removed_part = (*curr_item)["text"].str().substr(len+1);
660 
661  // first part of the text
662  // get_split_location always splits at word bounds.
663  // substr(len) will include a space, so we skip that.
664  (*curr_item)["text"] = (*curr_item)["text"].str().substr(0, len);
665  (*curr_item)["maximum_width"] = init_width - float_size.x;
666  float_size = point(0,0);
667 
668  // Height update
669  int ah = get_text_size(*curr_item, init_width - float_size.x).y;
670  if(tmp_h > ah) {
671  tmp_h = 0;
672  }
673  text_height += ah - tmp_h;
674  prev_blk_height += text_height;
675  pos = point(origin.x, prev_blk_height);
676 
677  DBG_GUI_RL << "wrap: " << prev_blk_height << "," << text_height;
678  text_height = 0;
679 
680  // New text block
681  x = origin.x;
682  wrap_mode = false;
683 
684  // rest of the text
685  curr_item = &(text_dom.add_child("text"));
686  default_text_config(curr_item, pos, init_width - pos.x);
687  tmp_h = get_text_size(*curr_item, init_width).y;
688  add_text_with_attribute(*curr_item, removed_part);
689 
690  } else if((float_size.y > 0) && (text_size.y < float_size.y)) {
691  //TODO padding?
692  // text height less than floating image's height, don't split
693  DBG_GUI_RL << "no wrap";
694  pos.y += text_size.y;
695  }
696 
697  if(!wrap_mode) {
698  float_size = point(0,0);
699  }
700  }
701 
702  point size = get_text_size(*curr_item, init_width - (x == 0 ? float_size.x : x));
703  // update text size and widget height
704  if(tmp_h > size.y) {
705  tmp_h = 0;
706  }
707  w = std::max(w, x + static_cast<unsigned>(size.x));
708 
709  text_height += size.y - tmp_h;
710  pos.y += size.y - tmp_h;
711  }
712 
713  if(!is_image && !wrap_mode && img_size.y > 0) {
714  img_size = point(0,0);
715  }
716 
717  if(curr_item) {
718  DBG_GUI_RL << "Item:\n" << curr_item->debug();
719  }
720  DBG_GUI_RL << "X: " << x;
721  DBG_GUI_RL << "Prev block height: " << prev_blk_height << " Current text block height: " << text_height;
722  DBG_GUI_RL << "Height: " << h;
723  h = text_height + prev_blk_height;
724  DBG_GUI_RL << "-----------";
725  } // for loop ends
726 
727  if(w == 0) {
728  w = init_width;
729  }
730 
731  // DEBUG: draw boxes around links
732  #if LINK_DEBUG_BORDER
733  if(finalize) {
734  for(const auto& entry : links_) {
735  config& link_rect_cfg = text_dom.add_child("rectangle");
736  link_rect_cfg["x"] = entry.first.x;
737  link_rect_cfg["y"] = entry.first.y;
738  link_rect_cfg["w"] = entry.first.w;
739  link_rect_cfg["h"] = entry.first.h;
740  link_rect_cfg["border_thickness"] = 1;
741  link_rect_cfg["border_color"] = "255, 180, 0, 255";
742  }
743  }
744  #endif
745 
746  // TODO float and a mix of floats and images and tables
747  h = std::max(static_cast<unsigned>(img_size.y), h);
748 
749  DBG_GUI_RL << "[\n" << text_dom.debug() << "]\n";
750 
751  DBG_GUI_RL << "Width: " << w << " Height: " << h << " Origin: " << origin;
752  return { text_dom, point(w, h - origin.y) };
753 } // function ends
754 
756  config* txt_ptr,
757  const point& pos,
758  const int max_width,
759  const t_string& text)
760 {
761  if(txt_ptr != nullptr) {
762  (*txt_ptr)["text"] = text;
763  (*txt_ptr)["color"] = text_color_enabled_.to_rgba_string();
764  (*txt_ptr)["font_family"] = font_family_;
765  (*txt_ptr)["font_size"] = font_size_;
766  (*txt_ptr)["font_style"] = font_style_;
767  (*txt_ptr)["text_alignment"] = encode_text_alignment(get_text_alignment());
768  (*txt_ptr)["line_spacing"] = 0;
769  (*txt_ptr)["x"] = pos.x;
770  (*txt_ptr)["y"] = pos.y;
771  (*txt_ptr)["w"] = "(text_width)";
772  (*txt_ptr)["h"] = "(text_height)";
773  (*txt_ptr)["maximum_width"] = max_width;
774  (*txt_ptr)["parse_text_as_formula"] = false;
775  add_attribute(*txt_ptr,
776  "line_height",
777  std::to_string(font::get_line_spacing_factor()));
778  }
779 }
780 
782 {
783  for(canvas& tmp : get_canvases()) {
784  tmp.set_shapes(shapes_, true);
785  tmp.set_variable("width", wfl::variant(init_w_));
786  tmp.set_variable("padding", wfl::variant(padding_));
787  // Disable ellipsization so that text wrapping can work
788  tmp.set_variable("text_wrap_mode", wfl::variant(PANGO_ELLIPSIZE_NONE));
789  tmp.set_variable("text_alpha", wfl::variant(text_alpha_));
790  }
791 }
792 
793 void rich_label::set_text_alpha(unsigned short alpha)
794 {
795  if(alpha != text_alpha_) {
796  text_alpha_ = alpha;
797  update_canvas();
798  queue_redraw();
799  }
800 }
801 
802 void rich_label::set_active(const bool active)
803 {
804  if(get_active() != active) {
805  set_state(active ? ENABLED : DISABLED);
806  }
807 }
808 
809 void rich_label::set_link_aware(bool link_aware)
810 {
811  if(link_aware != link_aware_) {
812  link_aware_ = link_aware;
813  update_canvas();
814  queue_redraw();
815  }
816 }
817 
819 {
820  if(color != link_color_) {
821  link_color_ = color;
822  update_canvas();
823  queue_redraw();
824  }
825 }
826 
828 {
829  if(state != state_) {
830  state_ = state;
831  queue_redraw();
832  }
833 }
834 
835 void rich_label::register_link_callback(std::function<void(std::string)> link_handler)
836 {
837  if(!link_aware_) {
838  return;
839  }
840 
841  connect_signal<event::LEFT_BUTTON_CLICK>(
842  std::bind(&rich_label::signal_handler_left_button_click, this, std::placeholders::_3));
843  connect_signal<event::MOUSE_MOTION>(
844  std::bind(&rich_label::signal_handler_mouse_motion, this, std::placeholders::_3, std::placeholders::_5));
845  connect_signal<event::MOUSE_LEAVE>(
846  std::bind(&rich_label::signal_handler_mouse_leave, this, std::placeholders::_3));
847  link_handler_ = std::move(link_handler);
848 }
849 
850 
852 {
853  DBG_GUI_E << "rich_label click";
854 
855  if(!get_link_aware()) {
856  return; // without marking event as "handled"
857  }
858 
859  point mouse = get_mouse_position() - get_origin();
860 
861  DBG_GUI_RL << "(mouse) " << mouse;
862  DBG_GUI_RL << "link count :" << links_.size();
863 
864  for(const auto& entry : links_) {
865  DBG_GUI_RL << "link " << entry.first;
866 
867  if(entry.first.contains(mouse)) {
868  DBG_GUI_RL << "Clicked link! dst = " << entry.second;
870  if(link_handler_) {
871  link_handler_(entry.second);
872  } else {
873  DBG_GUI_RL << "No registered link handler found";
874  }
875 
876  }
877  }
878 
879  handled = true;
880 }
881 
883 {
884  DBG_GUI_E << "rich_label mouse motion";
885 
886  if(!get_link_aware()) {
887  return; // without marking event as "handled"
888  }
889 
890  point mouse = coordinate - get_origin();
891 
892  for(const auto& entry : links_) {
893  if(entry.first.contains(mouse)) {
894  update_mouse_cursor(true);
895  handled = true;
896  return;
897  }
898  }
899 
900  update_mouse_cursor(false);
901 }
902 
904 {
905  DBG_GUI_E << "rich_label mouse leave";
906 
907  if(!get_link_aware()) {
908  return; // without marking event as "handled"
909  }
910 
911  // We left the widget, so just unconditionally reset the cursor
912  update_mouse_cursor(false);
913 
914  handled = true;
915 }
916 
918 {
919  // Someone else may set the mouse cursor for us to something unusual (e.g.
920  // the WAIT cursor) so we ought to mess with that only if it's set to
921  // NORMAL or HYPERLINK.
922 
923  if(enable && cursor::get() == cursor::NORMAL) {
925  } else if(!enable && cursor::get() == cursor::HYPERLINK) {
927  }
928 }
929 
930 // }---------- DEFINITION ---------{
931 
934 {
935  DBG_GUI_P << "Parsing rich_label " << id;
936 
937  load_resolutions<resolution>(cfg);
938 }
939 
941  : resolution_definition(cfg)
942  , text_color_enabled(color_t::from_rgba_string(cfg["text_font_color_enabled"].str()))
943  , text_color_disabled(color_t::from_rgba_string(cfg["text_font_color_disabled"].str()))
944  , link_color(cfg["link_color"].empty() ? font::YELLOW_COLOR : color_t::from_rgba_string(cfg["link_color"].str()))
945  , font_family(cfg["text_font_family"].str())
946  , font_size(cfg["text_font_size"].to_int(font::SIZE_NORMAL))
947  , font_style(cfg["text_font_style"].str("normal"))
948  , colors()
949 {
950  if(auto colors_cfg = cfg.optional_child("colors")) {
951  for(const auto& [name, value] : colors_cfg->attribute_range()) {
952  colors.try_emplace(name, color_t::from_rgba_string(value.str()));
953  }
954  }
955 
956  // Note the order should be the same as the enum state_t is rich_label.hpp.
957  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_enabled", missing_mandatory_wml_tag("rich_label_definition][resolution", "state_enabled")));
958  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_disabled", missing_mandatory_wml_tag("rich_label_definition][resolution", "state_disabled")));
959 }
960 
961 // }---------- BUILDER -----------{
962 
963 namespace implementation
964 {
965 
966 builder_rich_label::builder_rich_label(const config& cfg)
967  : builder_styled_widget(cfg)
968  , text_alignment(decode_text_alignment(cfg["text_alignment"]))
969  , link_aware(cfg["link_aware"].to_bool(true))
970  , width(cfg["width"], 500)
971  , padding(cfg["padding"].to_int(5))
972 {
973 }
974 
975 std::unique_ptr<widget> builder_rich_label::build() const
976 {
977  DBG_GUI_G << "Window builder: placed rich_label '" << id << "' with definition '"
978  << definition << "'.";
979 
980  return std::make_unique<rich_label>(*this);
981 }
982 
983 } // namespace implementation
984 
985 // }------------ END --------------
986 
987 } // 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:882
state_t state_
Current state of the widget.
Definition: rich_label.hpp:171
void set_state(const state_t state)
Definition: rich_label.cpp:827
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:781
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:903
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:802
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:835
void signal_handler_left_button_click(bool &handled)
Left click signal handler: checks if we clicked on a hyperlink.
Definition: rich_label.cpp:851
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:818
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:809
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:755
void set_text_alpha(unsigned short alpha)
Definition: rich_label.cpp:793
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:917
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:47
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:971
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: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:975
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:932
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