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