The Battle for Wesnoth  1.19.14+dev
floating_label.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2025
3  by David White <dave@whitevine.net>
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 #include "floating_label.hpp"
17 
18 #include "draw.hpp"
19 #include "draw_manager.hpp"
20 #include "font/standard_colors.hpp"
21 #include "font/text.hpp"
22 #include "log.hpp"
23 #include "serialization/chrono.hpp"
24 #include "video.hpp"
25 
26 #include <map>
27 #include <set>
28 #include <stack>
29 
30 #include <boost/algorithm/string/trim.hpp>
31 
32 static lg::log_domain log_font("font");
33 #define DBG_FT LOG_STREAM(debug, log_font)
34 #define LOG_FT LOG_STREAM(info, log_font)
35 #define WRN_FT LOG_STREAM(warn, log_font)
36 #define ERR_FT LOG_STREAM(err, log_font)
37 
38 static lg::log_domain log_display("display");
39 #define ERR_DP LOG_STREAM(err, log_display)
40 
41 namespace
42 {
43 typedef std::map<int, font::floating_label> label_map;
44 label_map labels;
45 int label_id = 1;
46 
47 std::stack<std::set<int>> label_contexts;
48 
49 }
50 
51 using namespace std::chrono_literals;
52 
53 namespace font
54 {
55 floating_label::floating_label(const std::string& text)
56  : tex_()
57  , screen_loc_()
58  , alpha_(0)
59  , fadeout_(0)
60  , time_start_()
61  , text_(boost::trim_copy(text))
62  , font_size_(SIZE_SMALL)
63  , color_(NORMAL_COLOR)
64  , bgcolor_(0, 0, 0, SDL_ALPHA_TRANSPARENT)
65  , xpos_(0)
66  , ypos_(0)
67  , xmove_(0)
68  , ymove_(0)
69  , lifetime_(-1)
70  , width_(-1)
71  , height_(-1)
72  , clip_rect_(video::game_canvas())
73  , visible_(true)
74  , align_(CENTER_ALIGN)
75  , border_(0)
76  , scroll_(ANCHOR_LABEL_SCREEN)
77  , use_markup_(true)
78 {
79 }
80 
81 void floating_label::move(double xmove, double ymove)
82 {
83  xpos_ += xmove;
84  ypos_ += ymove;
85 }
86 
87 int floating_label::xpos(std::size_t width) const
88 {
89  int xpos = int(xpos_);
90  if(align_ == font::CENTER_ALIGN) {
91  xpos -= width / 2;
92  } else if(align_ == font::RIGHT_ALIGN) {
93  xpos -= width;
94  }
95 
96  return xpos;
97 }
98 
99 rect floating_label::get_bg_rect(const rect& text_rect) const
100 {
101  return text_rect.padded_by(border_);
102 }
103 
105 {
106  tex_.reset();
107 }
108 
110 {
111  if(video::headless()) {
112  return false;
113  }
114 
115  if(tex_ != nullptr) {
116  // Already have a texture
117  return true;
118  }
119 
120  if(text_.empty()) {
121  // Empty labels are unfortunately still used sometimes
122  return false;
123  }
124 
125  DBG_FT << "creating floating label texture, text: " << text_.substr(0,15);
127 
128  text.set_link_aware(false)
132  .set_alignment(PANGO_ALIGN_LEFT)
135  .set_maximum_height(height_ < 0 ? clip_rect_.h : height_, true)
136  .set_ellipse_mode(PANGO_ELLIPSIZE_END)
138  .set_add_outline(bgcolor_.a == 0);
139 
140  text.set_text(text_, use_markup_);
141 
142  tex_ = text.render_and_get_texture();
143  if(!tex_) {
144  ERR_FT << "could not create floating label's text";
145  return false;
146  }
147 
148  return true;
149 }
150 
152 {
153  DBG_FT << "undrawing floating label from " << screen_loc_;
155  screen_loc_ = {};
156 }
157 
158 void floating_label::update(const clock::time_point& time)
159 {
160  if(video::headless() || text_.empty()) {
161  return;
162  }
163 
164  if(!create_texture()) {
165  ERR_FT << "failed to create texture for floating label";
166  return;
167  }
168 
169  rect draw_loc{get_pos(time), tex_.draw_size()};
170  uint8_t new_alpha = get_alpha(time);
171 
172  // Nothing has changed
173  // FIXME: we consider border too since otherwise we get flickering under floating
174  // labels with a background (see bug #7700). This isn't ideal for chat messages or
175  // tooltips, but it optimizes the usual case (map labels).
176  if(screen_loc_ == draw_loc && alpha_ == new_alpha && border_ == 0) {
177  return;
178  }
179 
180  // Invalidate former draw loc
182 
183  // Invalidate new draw loc in preparation
185 
186  DBG_FT << "updating floating label from " << screen_loc_ << " to " << draw_loc;
187 
188  screen_loc_ = draw_loc;
189  alpha_ = new_alpha;
190 }
191 
193 {
194  if(!visible_) {
195  screen_loc_ = {};
196  return;
197  }
198 
199  if(screen_loc_.empty()) {
200  return;
201  }
202 
203  if(!tex_) {
204  ERR_DP << "trying to draw floating label with no texture!";
205  return;
206  }
207 
209  return;
210  }
211 
212  DBG_FT << "drawing floating label to " << screen_loc_;
213 
214  // Clip if appropriate.
215  auto clipper = draw::reduce_clip(clip_rect_);
216 
217  // Draw background, if appropriate
218  if(bgcolor_.a != 0) {
220  }
221 
222  // Apply the label texture to the screen.
225 }
226 
227 void floating_label::set_lifetime(const std::chrono::milliseconds& lifetime, const std::chrono::milliseconds& fadeout)
228 {
229  lifetime_ = lifetime;
230  fadeout_ = fadeout;
231  time_start_ = std::chrono::steady_clock::now();
232 }
233 
234 auto floating_label::get_time_alive(const clock::time_point& current_time) const -> clock::duration
235 {
236  return current_time - time_start_;
237 }
238 
239 point floating_label::get_pos(const clock::time_point& time) const
240 {
241  double new_x = xpos(tex_.w());
242  double new_y = ypos_;
243 
244  if(xmove_ != 0.0 || ymove_ != 0.0) {
245  auto time_alive = std::chrono::duration_cast<std::chrono::milliseconds>(get_time_alive(time));
246  new_x += time_alive.count() * xmove_;
247  new_y += time_alive.count() * ymove_;
248  }
249 
250  // TODO: return a floating point point
251  return {static_cast<int>(new_x), static_cast<int>(new_y)};
252 }
253 
254 uint8_t floating_label::get_alpha(const clock::time_point& time) const
255 {
256  if(lifetime_ >= 0ms && fadeout_ > 0ms) {
257  auto time_alive = get_time_alive(time);
258  if(time_alive >= lifetime_) {
259  double progress = chrono::normalize_progress(time_alive - lifetime_, fadeout_);
260  return float_to_color(1.0 - progress);
261  }
262  }
263  return ALPHA_OPAQUE;
264 }
265 
267 {
268  if(label_contexts.empty()) {
269  return 0;
270  }
271 
272  ++label_id;
273  labels.emplace(label_id, flabel);
274  label_contexts.top().insert(label_id);
275  return label_id;
276 }
277 
278 void move_floating_label(int handle, double xmove, double ymove)
279 {
280  const label_map::iterator i = labels.find(handle);
281  if(i != labels.end()) {
282  i->second.move(xmove, ymove);
283  }
284 }
285 
286 void scroll_floating_labels(double xmove, double ymove)
287 {
288  for(auto& [id, label] : labels) {
289  if(label.scroll() == ANCHOR_LABEL_MAP) {
290  label.move(xmove, ymove);
291  }
292  }
293 }
294 
295 void remove_floating_label(int handle, const std::chrono::milliseconds& fadeout)
296 {
297  const label_map::iterator i = labels.find(handle);
298  if(i != labels.end()) {
299  if(fadeout > 0ms) {
300  i->second.set_lifetime(0ms, fadeout);
301  return;
302  } else if(fadeout < 0ms) {
303  i->second.set_lifetime(0ms, i->second.get_fade_time());
304  return;
305  }
306  // Queue a redraw of where the label was.
307  i->second.undraw();
308  labels.erase(i);
309  }
310 
311  if(!label_contexts.empty()) {
312  label_contexts.top().erase(handle);
313  }
314 }
315 
316 void show_floating_label(int handle, bool value)
317 {
318  const label_map::iterator i = labels.find(handle);
319  if(i != labels.end()) {
320  i->second.show(value);
321  }
322 }
323 
325 {
326  const label_map::iterator i = labels.find(handle);
327  if(i != labels.end()) {
328  if (i->second.create_texture()) {
329  point size = i->second.get_draw_size();
330  return {0, 0, size.x, size.y};
331  }
332  }
333  return {};
334 }
335 
337 {
338  // hacky but the whole floating label system needs to be redesigned...
339  for(auto& [id, label] : labels) {
340  if(label_contexts.top().count(id) > 0) {
341  label.undraw();
342  }
343  }
344 
345  //TODO: 'pause' floating labels in other contexrs
346  label_contexts.emplace();
347 }
348 
350 {
351  //TODO: 'pause' floating labels in other contexrs
352  const std::set<int>& context = label_contexts.top();
353 
354  while(!context.empty()) {
355  // Remove_floating_label removes the passed label from the context.
356  // This loop removes a different label in every iteration.
357  remove_floating_label(*context.begin());
358  }
359 
360  label_contexts.pop();
361 }
362 
364 {
365  if(label_contexts.empty()) {
366  return;
367  }
368 
369  const std::set<int>& context = label_contexts.top();
370 
371  // draw the labels in the order they were added, so later added labels (likely to be tooltips)
372  // are displayed over earlier added labels.
373  for(auto& [id, label] : labels) {
374  if(context.count(id) > 0) {
375  label.draw();
376  }
377  }
378 }
379 
381 {
382  if(label_contexts.empty()) {
383  return;
384  }
385 
386  auto time = std::chrono::steady_clock::now();
387  std::set<int>& context = label_contexts.top();
388 
389  for(auto iter = labels.begin(); iter!= labels.end();) {
390  auto& [id, label] = *iter;
391 
392  if(context.count(id) > 0) {
393  label.update(time);
394 
395  if(label.expired(time)) {
396  context.erase(id);
397  iter = labels.erase(iter);
398  continue;
399  }
400  }
401 
402  ++iter;
403  }
404 }
405 
406 }
std::chrono::milliseconds lifetime_
uint8_t get_alpha(const clock::time_point &time) const
rect get_bg_rect(const rect &text_rect) const
point get_pos(const clock::time_point &time) const
clock::time_point time_start_
bool create_texture()
Ensure a texture for this floating label exists, creating one if needed.
int xpos(std::size_t width) const
void update(const clock::time_point &time)
Finalize draw position and alpha, and queue redrawing if changed.
void set_lifetime(const std::chrono::milliseconds &lifetime, const std::chrono::milliseconds &fadeout=std::chrono::milliseconds{100})
std::chrono::milliseconds fadeout_
void move(double xmove, double ymove)
Change the floating label's position.
clock::duration get_time_alive(const clock::time_point &current_time) const
void draw()
Draw the label to the screen.
void undraw()
Mark the last drawn location as requiring redraw.
Text class.
Definition: text.hpp:78
pango_text & set_font_style(const FONT_STYLE font_style)
Definition: text.cpp:381
pango_text & set_characters_per_line(const unsigned characters_per_line)
Definition: text.cpp:416
pango_text & set_foreground_color(const color_t &color)
Definition: text.cpp:391
pango_text & set_family_class(font::family_class fclass)
Definition: text.cpp:359
pango_text & set_add_outline(bool do_add)
Definition: text.cpp:514
pango_text & set_ellipse_mode(const PangoEllipsizeMode ellipse_mode)
Definition: text.cpp:452
pango_text & set_alignment(const PangoAlignment alignment)
Definition: text.cpp:472
pango_text & set_font_size(unsigned font_size)
Definition: text.cpp:369
pango_text & set_link_aware(bool b)
Definition: text.cpp:495
bool set_text(const std::string &text, const bool markedup)
Sets the text to render.
Definition: text.cpp:333
pango_text & set_maximum_height(int height, bool multiline)
Definition: text.cpp:427
pango_text & set_maximum_width(int width)
Definition: text.cpp:400
texture render_and_get_texture()
Returns the cached texture, or creates a new one otherwise.
Definition: text.cpp:122
int w() const
The draw-space width of the texture, in pixels.
Definition: texture.hpp:103
void reset()
Releases ownership of the managed texture and resets the ptr to null.
Definition: texture.cpp:184
point draw_size() const
The size of the texture in draw-space.
Definition: texture.hpp:120
void set_alpha_mod(uint8_t alpha)
Alpha modifier.
Definition: texture.cpp:128
constexpr uint8_t ALPHA_OPAQUE
Definition: color.hpp:47
constexpr uint8_t float_to_color(double n)
Convert a double in the range [0.0,1.0] to an 8-bit colour value.
Definition: color.hpp:314
Drawing functions, for drawing things on the screen.
#define DBG_FT
static lg::log_domain log_font("font")
static lg::log_domain log_display("display")
#define ERR_DP
#define ERR_FT
std::size_t i
Definition: function.cpp:1032
std::string label
What to show in the filter's drop-down list.
Definition: manager.cpp:201
std::string id
Text to match against addon_info.tags()
Definition: manager.cpp:199
Standard logging facilities (interface).
Definition: span.hpp:53
constexpr double normalize_progress(const std::chrono::duration< RepE, PeriodE > &elapsed, const std::chrono::duration< RepD, PeriodD > &duration)
Definition: chrono.hpp:77
void invalidate_region(const rect &region)
Mark a region of the screen as requiring redraw.
clip_setter reduce_clip(const ::rect &clip)
Set the clipping area to the intersection of the current clipping area and the given rectangle.
Definition: draw.cpp:586
void fill(const ::rect &rect, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
Fill an area with the given colour.
Definition: draw.cpp:52
::rect get_clip()
Get the current clipping area, in draw coordinates.
Definition: draw.cpp:606
void blit(const texture &tex, const ::rect &dst)
Draws a texture, or part of a texture, at the given location.
Definition: draw.cpp:394
Graphical text output.
pango_text & get_text_renderer()
Returns a reference to a static pango_text object.
Definition: text.cpp:960
int add_floating_label(const floating_label &flabel)
add a label floating on the screen above everything else.
rect get_floating_label_rect(int handle)
void show_floating_label(int handle, bool value)
hides or shows a floating label
const int SIZE_SMALL
Definition: constants.cpp:24
void scroll_floating_labels(double xmove, double ymove)
moves all floating labels that have 'scroll_mode' set to ANCHOR_LABEL_MAP
void remove_floating_label(int handle, const std::chrono::milliseconds &fadeout)
removes the floating label given by 'handle' from the screen
void update_floating_labels()
void move_floating_label(int handle, double xmove, double ymove)
moves the floating label given by 'handle' by (xmove,ymove)
void draw_floating_labels()
const color_t NORMAL_COLOR
@ CENTER_ALIGN
@ ANCHOR_LABEL_SCREEN
@ ANCHOR_LABEL_MAP
std::shared_ptr< halo_record > handle
Definition: halo.hpp:31
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:85
bool headless()
The game is running headless.
Definition: video.cpp:145
rect game_canvas()
The game canvas area, in drawing coordinates.
Definition: video.cpp:441
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
Holds a 2D point.
Definition: point.hpp:25
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:49
bool empty() const
False if both w and h are > 0, true otherwise.
Definition: rect.cpp:49
constexpr rect padded_by(int dx, int dy) const
Returns a new rectangle with dx horizontal padding and dy vertical padding.
Definition: rect.hpp:158
rect intersect(const rect &r) const
Calculates the intersection of this rectangle and another; that is, the maximal rectangle that is con...
Definition: rect.cpp:92
bool overlaps(const rect &r) const
Whether the given rectangle and this rectangle overlap.
Definition: rect.cpp:74