The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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"
24
25#include "cursor.hpp"
26#include "picture.hpp"
27#include "font/constants.hpp"
29#include "log.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
44static 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
51namespace gui2
52{
53namespace
54{
55using namespace std::string_literals;
56
57/** Possible formatting tags, must be the same as those in gui2::text_shape::draw */
58const std::array format_tags{ "bold"s, "b"s, "italic"s, "i"s, "underline"s, "u"s };
59}
60
61// ------------ WIDGET -----------{
62
63REGISTER_WIDGET(rich_label)
64
65rich_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
92color_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
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
110point 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
119point rich_label::get_image_size(const std::string& path) const
120{
121 return ::image::get_size(::image::locator{path});
122}
123
124std::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
189std::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;
224 queue_redraw();
225}
226
228{
230}
231
232void 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
246void 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
255std::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
739std::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,
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
767void rich_label::set_text_alpha(unsigned short alpha)
768{
769 if(alpha != text_alpha_) {
770 text_alpha_ = alpha;
772 queue_redraw();
773 }
774}
775
776void rich_label::set_active(const bool /*active*/)
777{
778 set_state(ENABLED); // Always enabled
779}
780
781void rich_label::set_link_aware(bool link_aware)
782{
783 if(link_aware != link_aware_) {
784 link_aware_ = link_aware;
786 queue_redraw();
787 }
788}
789
791{
792 if(color != link_color_) {
793 link_color_ = color;
795 queue_redraw();
796 }
797}
798
800{
801 if(state != state_) {
802 state_ = state;
803 queue_redraw();
804 }
805}
806
807void 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 utils::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
859void rich_label::signal_handler_mouse_motion(bool& handled, const point& coordinate)
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)) {
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
939namespace implementation
940{
941
942builder_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
950std::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
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 ...
point calculate_best_size() const override
See widget::calculate_best_size.
void signal_handler_mouse_motion(bool &handled, const point &coordinate)
Mouse motion signal handler: checks if the cursor is on a hyperlink.
state_t state_
Current state of the widget.
virtual void set_active(const bool) override
Sets the styled_widget's state.
void set_state(const state_t state)
std::function< void(std::string)> link_handler_
virtual void update_canvas() override
Updates the canvas(ses).
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)
int padding_
Padding.
void signal_handler_mouse_leave(bool &handled)
Mouse leave signal handler: checks if the cursor left a hyperlink.
std::vector< std::pair< rect, std::string > > links_
link variables and functions
void place(const point &origin, const point &size) override
See widget::place.
int font_size_
Base font size.
unsigned short text_alpha_
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.
std::string font_family_
Base font family.
unsigned init_w_
Width and height of the canvas.
void set_dom(const config &dom)
point get_xy_from_offset(const unsigned offset) const
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...
wfl::map_formula_callable setup_text_renderer(text_shape &tshape, unsigned width) const
size calculation functions
void request_reduce_width(const unsigned maximum_width) override
See widget::request_reduce_width.
std::string font_style_
Base font style.
state_t
Possible states of the widget.
void register_link_callback(std::function< void(std::string)> link_handler)
void signal_handler_left_button_click(bool &handled)
Left click signal handler: checks if we clicked on a hyperlink.
int get_offset_from_xy(const point &position) const
point get_image_size(const std::string &path) const
bool link_aware_
Whether the rich_label is link aware, rendering links with special formatting and handling click even...
void set_link_color(const color_t &color)
std::map< std::string, color_t > predef_colors_
Color variables that can be used in place of colors strings, like <row bgcolor=color1>
void set_link_aware(bool l)
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...
void add_link(text_shape &tshape, const std::string &name, const std::string &dest, const point &origin, int img_width)
void set_text_alpha(unsigned short alpha)
color_t link_color_
What color links will be rendered in.
void set_label(const t_string &text) override
point get_text_size(text_shape &tshape, unsigned width) const
void update_mouse_cursor(bool enable)
Implementation detail for (re)setting the hyperlink cursor.
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="")
std::size_t get_split_location(std::string_view text, const point &pos)
PangoAlignment get_text_alignment() const
canvas & get_canvas(const unsigned index)
virtual void request_reduce_width(const unsigned maximum_width) override
See widget::request_reduce_width.
std::vector< canvas > & get_canvases()
virtual void place(const point &origin, const point &size) override
See widget::place.
t_string get_text() const
Definition canvas.cpp:545
void draw(wfl::map_formula_callable &variables) override
Draws the canvas.
Definition canvas.cpp:616
std::pair< std::size_t, std::size_t > add_text(const t_string &text)
Definition canvas.cpp:553
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:566
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:249
int as_int(int fallback=0) const
Returns the variant's value as an integer.
Definition variant.cpp:336
constexpr uint8_t ALPHA_OPAQUE
Definition color.hpp:37
const config * cfg
static std::string _(const char *str)
Definition gettext.hpp:100
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:185
@ 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:151
Graphical text output.
constexpr float get_line_spacing_factor()
Definition text.hpp:568
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:958
const int SIZE_TITLE
Definition constants.cpp:31
family_class decode_family_class(const std::string &str)
color_t string_to_color(const std::string &color_str)
Return the color the string represents.
std::string sound_button_click
Definition settings.cpp:50
Generic file dialog.
point get_mouse_position()
Returns the current mouse position.
Definition helper.cpp:167
std::string_view debug_truncate(std::string_view text)
Returns a truncated version of the text.
Definition helper.cpp:172
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
Contains the implementation details for lexical_cast and shouldn't be used directly.
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:1009
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition unicode.cpp:81
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition general.hpp:88
std::vector< std::string > split(const config_attribute_value &val)
int get_pixel_scale()
Get the current active pixel scale multiplier.
Definition video.cpp:480
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
Transitional API for porting SDL_ttf-based code to Pango.
This file contains the settings handling of the widget library.
std::unique_ptr< MIX_Audio, decltype(&MIX_DestroyAudio)> value
Definition sound.cpp:139
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
std::string definition
Parameters for the styled_widget.
std::vector< state_definition > state
std::map< std::string, color_t > colors
rich_label_definition(const config &cfg)
Holds a 2D point.
Definition point.hpp:25
An abstract description of a rectangle with integer coordinates.
Definition rect.hpp:49
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
#define c