The Battle for Wesnoth  1.19.15+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 static lg::log_domain log_font("font");
31 #define DBG_FT LOG_STREAM(debug, log_font)
32 #define LOG_FT LOG_STREAM(info, log_font)
33 #define WRN_FT LOG_STREAM(warn, log_font)
34 #define ERR_FT LOG_STREAM(err, log_font)
35 
36 static lg::log_domain log_display("display");
37 #define ERR_DP LOG_STREAM(err, log_display)
38 
39 namespace
40 {
41 typedef std::map<int, font::floating_label> label_map;
42 label_map labels;
43 int label_id = 1;
44 
45 std::stack<std::set<int>> label_contexts;
46 
47 }
48 
49 using namespace std::chrono_literals;
50 
51 namespace font
52 {
53 floating_label::floating_label(const std::string& text)
54  : tex_()
55  , screen_loc_()
56  , alpha_(0)
57  , fadeout_(0)
58  , time_start_()
59  , text_((!text.empty() && text.back() == '\n') ? std::string(text.begin(), text.end() - 1) : text) // remove unwanted trailing newline
60  , font_size_(SIZE_SMALL)
61  , color_(NORMAL_COLOR)
62  , bgcolor_(0, 0, 0, SDL_ALPHA_TRANSPARENT)
63  , xpos_(0)
64  , ypos_(0)
65  , xmove_(0)
66  , ymove_(0)
67  , lifetime_(-1)
68  , width_(-1)
69  , height_(-1)
70  , clip_rect_(video::game_canvas())
71  , visible_(true)
72  , align_(CENTER_ALIGN)
73  , border_(0)
74  , scroll_(ANCHOR_LABEL_SCREEN)
75  , use_markup_(true)
76 {
77 }
78 
79 void floating_label::move(double xmove, double ymove)
80 {
81  xpos_ += xmove;
82  ypos_ += ymove;
83 }
84 
85 int floating_label::xpos(std::size_t width) const
86 {
87  int xpos = int(xpos_);
88  if(align_ == font::CENTER_ALIGN) {
89  xpos -= width / 2;
90  } else if(align_ == font::RIGHT_ALIGN) {
91  xpos -= width;
92  }
93 
94  return xpos;
95 }
96 
97 rect floating_label::get_bg_rect(const rect& text_rect) const
98 {
99  return text_rect.padded_by(border_);
100 }
101 
103 {
104  tex_.reset();
105 }
106 
108 {
109  if(video::headless()) {
110  return false;
111  }
112 
113  if(tex_ != nullptr) {
114  // Already have a texture
115  return true;
116  }
117 
118  if(text_.empty()) {
119  // Empty labels are unfortunately still used sometimes
120  return false;
121  }
122 
123  DBG_FT << "creating floating label texture, text: " << text_.substr(0,15);
125 
126  text.set_link_aware(false)
130  .set_alignment(PANGO_ALIGN_LEFT)
133  .set_maximum_height(height_ < 0 ? clip_rect_.h : height_, true)
134  .set_ellipse_mode(PANGO_ELLIPSIZE_END)
136  .set_add_outline(bgcolor_.a == 0);
137 
138  text.set_text(text_, use_markup_);
139 
140  tex_ = text.render_and_get_texture();
141  if(!tex_) {
142  ERR_FT << "could not create floating label's text";
143  return false;
144  }
145 
146  return true;
147 }
148 
150 {
151  DBG_FT << "undrawing floating label from " << screen_loc_;
153  screen_loc_ = {};
154 }
155 
156 void floating_label::update(const clock::time_point& time)
157 {
158  if(video::headless() || text_.empty()) {
159  return;
160  }
161 
162  if(!create_texture()) {
163  ERR_FT << "failed to create texture for floating label";
164  return;
165  }
166 
167  rect draw_loc{get_pos(time), tex_.draw_size()};
168  uint8_t new_alpha = get_alpha(time);
169 
170  // Nothing has changed
171  // FIXME: we consider border too since otherwise we get flickering under floating
172  // labels with a background (see bug #7700). This isn't ideal for chat messages or
173  // tooltips, but it optimizes the usual case (map labels).
174  if(screen_loc_ == draw_loc && alpha_ == new_alpha && border_ == 0) {
175  return;
176  }
177 
178  // Invalidate former draw loc
180 
181  // Invalidate new draw loc in preparation
183 
184  DBG_FT << "updating floating label from " << screen_loc_ << " to " << draw_loc;
185 
186  screen_loc_ = draw_loc;
187  alpha_ = new_alpha;
188 }
189 
191 {
192  if(!visible_) {
193  screen_loc_ = {};
194  return;
195  }
196 
197  if(screen_loc_.empty()) {
198  return;
199  }
200 
201  if(!tex_) {
202  ERR_DP << "trying to draw floating label with no texture!";
203  return;
204  }
205 
207  return;
208  }
209 
210  DBG_FT << "drawing floating label to " << screen_loc_;
211 
212  // Clip if appropriate.
213  auto clipper = draw::reduce_clip(clip_rect_);
214 
215  // Draw background, if appropriate
216  if(bgcolor_.a != 0) {
218  }
219 
220  // Apply the label texture to the screen.
223 }
224 
225 void floating_label::set_lifetime(const std::chrono::milliseconds& lifetime, const std::chrono::milliseconds& fadeout)
226 {
227  lifetime_ = lifetime;
228  fadeout_ = fadeout;
229  time_start_ = std::chrono::steady_clock::now();
230 }
231 
232 auto floating_label::get_time_alive(const clock::time_point& current_time) const -> clock::duration
233 {
234  return current_time - time_start_;
235 }
236 
237 point floating_label::get_pos(const clock::time_point& time) const
238 {
239  double new_x = xpos(tex_.w());
240  double new_y = ypos_;
241 
242  if(xmove_ != 0.0 || ymove_ != 0.0) {
243  auto time_alive = std::chrono::duration_cast<std::chrono::milliseconds>(get_time_alive(time));
244  new_x += time_alive.count() * xmove_;
245  new_y += time_alive.count() * ymove_;
246  }
247 
248  // TODO: return a floating point point
249  return {static_cast<int>(new_x), static_cast<int>(new_y)};
250 }
251 
252 uint8_t floating_label::get_alpha(const clock::time_point& time) const
253 {
254  if(lifetime_ >= 0ms && fadeout_ > 0ms) {
255  auto time_alive = get_time_alive(time);
256  if(time_alive >= lifetime_) {
257  double progress = chrono::normalize_progress(time_alive - lifetime_, fadeout_);
258  return float_to_color(1.0 - progress);
259  }
260  }
261  return ALPHA_OPAQUE;
262 }
263 
265 {
266  if(label_contexts.empty()) {
267  return 0;
268  }
269 
270  ++label_id;
271  labels.emplace(label_id, flabel);
272  label_contexts.top().insert(label_id);
273  return label_id;
274 }
275 
276 void move_floating_label(int handle, double xmove, double ymove)
277 {
278  const label_map::iterator i = labels.find(handle);
279  if(i != labels.end()) {
280  i->second.move(xmove, ymove);
281  }
282 }
283 
284 void scroll_floating_labels(double xmove, double ymove)
285 {
286  for(auto& [id, label] : labels) {
287  if(label.scroll() == ANCHOR_LABEL_MAP) {
288  label.move(xmove, ymove);
289  }
290  }
291 }
292 
293 void remove_floating_label(int handle, const std::chrono::milliseconds& fadeout)
294 {
295  const label_map::iterator i = labels.find(handle);
296  if(i != labels.end()) {
297  if(fadeout > 0ms) {
298  i->second.set_lifetime(0ms, fadeout);
299  return;
300  } else if(fadeout < 0ms) {
301  i->second.set_lifetime(0ms, i->second.get_fade_time());
302  return;
303  }
304  // Queue a redraw of where the label was.
305  i->second.undraw();
306  labels.erase(i);
307  }
308 
309  if(!label_contexts.empty()) {
310  label_contexts.top().erase(handle);
311  }
312 }
313 
314 void show_floating_label(int handle, bool value)
315 {
316  const label_map::iterator i = labels.find(handle);
317  if(i != labels.end()) {
318  i->second.show(value);
319  }
320 }
321 
323 {
324  const label_map::iterator i = labels.find(handle);
325  if(i != labels.end()) {
326  if (i->second.create_texture()) {
327  point size = i->second.get_draw_size();
328  return {0, 0, size.x, size.y};
329  }
330  }
331  return {};
332 }
333 
335 {
336  // hacky but the whole floating label system needs to be redesigned...
337  for(auto& [id, label] : labels) {
338  if(label_contexts.top().count(id) > 0) {
339  label.undraw();
340  }
341  }
342 
343  //TODO: 'pause' floating labels in other contexrs
344  label_contexts.emplace();
345 }
346 
348 {
349  //TODO: 'pause' floating labels in other contexrs
350  const std::set<int>& context = label_contexts.top();
351 
352  while(!context.empty()) {
353  // Remove_floating_label removes the passed label from the context.
354  // This loop removes a different label in every iteration.
355  remove_floating_label(*context.begin());
356  }
357 
358  label_contexts.pop();
359 }
360 
362 {
363  if(label_contexts.empty()) {
364  return;
365  }
366 
367  const std::set<int>& context = label_contexts.top();
368 
369  // draw the labels in the order they were added, so later added labels (likely to be tooltips)
370  // are displayed over earlier added labels.
371  for(auto& [id, label] : labels) {
372  if(context.count(id) > 0) {
373  label.draw();
374  }
375  }
376 }
377 
379 {
380  if(label_contexts.empty()) {
381  return;
382  }
383 
384  auto time = std::chrono::steady_clock::now();
385  std::set<int>& context = label_contexts.top();
386 
387  for(auto iter = labels.begin(); iter!= labels.end();) {
388  auto& [id, label] = *iter;
389 
390  if(context.count(id) > 0) {
391  label.update(time);
392 
393  if(label.expired(time)) {
394  context.erase(id);
395  iter = labels.erase(iter);
396  continue;
397  }
398  }
399 
400  ++iter;
401  }
402 }
403 
404 }
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).
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:81
bool headless()
The game is running headless.
Definition: video.cpp:147
rect game_canvas()
The game canvas area, in drawing coordinates.
Definition: video.cpp:444
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