The Battle for Wesnoth  1.19.25+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  , clip_rect_(video::game_canvas())
69  , visible_(true)
70  , align_(CENTER_ALIGN)
71  , border_(0)
72  , scroll_(ANCHOR_LABEL_SCREEN)
73  , use_markup_(true)
74 {
75 }
76 
77 void floating_label::move(double xmove, double ymove)
78 {
79  xpos_ += xmove;
80  ypos_ += ymove;
81 }
82 
83 int floating_label::xpos(std::size_t width) const
84 {
85  int xpos = int(xpos_);
86  if(align_ == font::CENTER_ALIGN) {
87  xpos -= width / 2;
88  } else if(align_ == font::RIGHT_ALIGN) {
89  xpos -= width;
90  }
91 
92  return xpos;
93 }
94 
95 rect floating_label::get_bg_rect(const rect& text_rect) const
96 {
97  return text_rect.padded_by(border_);
98 }
99 
101 {
102  tex_.reset();
103 }
104 
106 {
107  if(video::headless()) {
108  return false;
109  }
110 
111  if(tex_ != nullptr) {
112  // Already have a texture
113  return true;
114  }
115 
116  if(text_.empty()) {
117  // Empty labels are unfortunately still used sometimes
118  return false;
119  }
120 
121  DBG_FT << "creating floating label texture, text: " << text_.substr(0,15);
123 
124  text.set_link_aware(false)
128  .set_alignment(PANGO_ALIGN_LEFT)
130  .set_maximum_width(width_.value_or(clip_rect_.w))
131  .set_maximum_height(height_.value_or(clip_rect_.h), true)
132  .set_ellipse_mode(PANGO_ELLIPSIZE_END)
134  .set_add_outline(bgcolor_.a == 0);
135 
136  text.set_text(text_, use_markup_);
137 
138  tex_ = text.render_and_get_texture();
139  if(!tex_) {
140  ERR_FT << "could not create floating label's text";
141  return false;
142  }
143 
144  return true;
145 }
146 
148 {
149  DBG_FT << "undrawing floating label from " << screen_loc_;
151  screen_loc_ = {};
152 }
153 
154 void floating_label::update(const clock::time_point& time)
155 {
156  if(video::headless() || text_.empty()) {
157  return;
158  }
159 
160  if(!create_texture()) {
161  ERR_FT << "failed to create texture for floating label";
162  return;
163  }
164 
165  rect draw_loc{get_pos(time), tex_.draw_size()};
166  uint8_t new_alpha = get_alpha(time);
167 
168  // Nothing has changed
169  // FIXME: we consider border too since otherwise we get flickering under floating
170  // labels with a background (see bug #7700). This isn't ideal for chat messages or
171  // tooltips, but it optimizes the usual case (map labels).
172  if(screen_loc_ == draw_loc && alpha_ == new_alpha && border_ == 0) {
173  return;
174  }
175 
176  // Invalidate former draw loc
178 
179  // Invalidate new draw loc in preparation
181 
182  DBG_FT << "updating floating label from " << screen_loc_ << " to " << draw_loc;
183 
184  screen_loc_ = draw_loc;
185  alpha_ = new_alpha;
186 }
187 
189 {
190  if(!visible_) {
191  screen_loc_ = {};
192  return;
193  }
194 
195  if(screen_loc_.empty()) {
196  return;
197  }
198 
199  if(!tex_) {
200  ERR_DP << "trying to draw floating label with no texture!";
201  return;
202  }
203 
205  return;
206  }
207 
208  DBG_FT << "drawing floating label to " << screen_loc_;
209 
210  // Clip if appropriate.
211  auto clipper = draw::reduce_clip(clip_rect_);
212 
213  // Draw background, if appropriate
214  if(bgcolor_.a != 0) {
216  }
217 
218  // Apply the label texture to the screen.
221 }
222 
223 void floating_label::set_lifetime(const std::chrono::milliseconds& lifetime, const std::chrono::milliseconds& fadeout)
224 {
225  lifetime_ = lifetime;
226  fadeout_ = fadeout;
227  time_start_ = std::chrono::steady_clock::now();
228 }
229 
230 auto floating_label::get_time_alive(const clock::time_point& current_time) const -> clock::duration
231 {
232  return current_time - time_start_;
233 }
234 
235 point floating_label::get_pos(const clock::time_point& time) const
236 {
237  double new_x = xpos(tex_.w());
238  double new_y = ypos_;
239 
240  if(xmove_ != 0.0 || ymove_ != 0.0) {
241  auto time_alive = std::chrono::duration_cast<std::chrono::milliseconds>(get_time_alive(time));
242  new_x += time_alive.count() * xmove_;
243  new_y += time_alive.count() * ymove_;
244  }
245 
246  // TODO: return a floating point point
247  return {static_cast<int>(new_x), static_cast<int>(new_y)};
248 }
249 
250 uint8_t floating_label::get_alpha(const clock::time_point& time) const
251 {
252  if(lifetime_ >= 0ms && fadeout_ > 0ms) {
253  auto time_alive = get_time_alive(time);
254  if(time_alive >= lifetime_) {
255  double progress = chrono::normalize_progress(time_alive - lifetime_, fadeout_);
256  return float_to_color(1.0 - progress);
257  }
258  }
259  return ALPHA_OPAQUE;
260 }
261 
263 {
264  if(label_contexts.empty()) {
265  return 0;
266  }
267 
268  ++label_id;
269  labels.emplace(label_id, flabel);
270  label_contexts.top().insert(label_id);
271  return label_id;
272 }
273 
274 void move_floating_label(int handle, double xmove, double ymove)
275 {
276  const label_map::iterator i = labels.find(handle);
277  if(i != labels.end()) {
278  i->second.move(xmove, ymove);
279  }
280 }
281 
282 void scroll_floating_labels(double xmove, double ymove)
283 {
284  for(auto& [id, label] : labels) {
285  if(label.scroll() == ANCHOR_LABEL_MAP) {
286  label.move(xmove, ymove);
287  }
288  }
289 }
290 
291 void remove_floating_label(int handle, const std::chrono::milliseconds& fadeout)
292 {
293  const label_map::iterator i = labels.find(handle);
294  if(i != labels.end()) {
295  if(fadeout > 0ms) {
296  i->second.set_lifetime(0ms, fadeout);
297  return;
298  } else if(fadeout < 0ms) {
299  i->second.set_lifetime(0ms, i->second.get_fade_time());
300  return;
301  }
302  // Queue a redraw of where the label was.
303  i->second.undraw();
304  labels.erase(i);
305  }
306 
307  if(!label_contexts.empty()) {
308  label_contexts.top().erase(handle);
309  }
310 }
311 
312 void show_floating_label(int handle, bool value)
313 {
314  const label_map::iterator i = labels.find(handle);
315  if(i != labels.end()) {
316  i->second.show(value);
317  }
318 }
319 
321 {
322  const label_map::iterator i = labels.find(handle);
323  if(i != labels.end()) {
324  if (i->second.create_texture()) {
325  point size = i->second.get_draw_size();
326  return {0, 0, size.x, size.y};
327  }
328  }
329  return {};
330 }
331 
333 {
334  // hacky but the whole floating label system needs to be redesigned...
335  for(auto& [id, label] : labels) {
336  if(label_contexts.top().count(id) > 0) {
337  label.undraw();
338  }
339  }
340 
341  //TODO: 'pause' floating labels in other contexrs
342  label_contexts.emplace();
343 }
344 
346 {
347  //TODO: 'pause' floating labels in other contexrs
348  const std::set<int>& context = label_contexts.top();
349 
350  while(!context.empty()) {
351  // Remove_floating_label removes the passed label from the context.
352  // This loop removes a different label in every iteration.
353  remove_floating_label(*context.begin());
354  }
355 
356  label_contexts.pop();
357 }
358 
360 {
361  if(label_contexts.empty()) {
362  return;
363  }
364 
365  const std::set<int>& context = label_contexts.top();
366 
367  // draw the labels in the order they were added, so later added labels (likely to be tooltips)
368  // are displayed over earlier added labels.
369  for(auto& [id, label] : labels) {
370  if(context.count(id) > 0) {
371  label.draw();
372  }
373  }
374 }
375 
377 {
378  if(label_contexts.empty()) {
379  return;
380  }
381 
382  auto time = std::chrono::steady_clock::now();
383  std::set<int>& context = label_contexts.top();
384 
385  for(auto iter = labels.begin(); iter!= labels.end();) {
386  auto& [id, label] = *iter;
387 
388  if(context.count(id) > 0) {
389  label.update(time);
390 
391  if(label.expired(time)) {
392  context.erase(id);
393  iter = labels.erase(iter);
394  continue;
395  }
396  }
397 
398  ++iter;
399  }
400 }
401 
402 }
utils::optional< int > width_
std::chrono::milliseconds lifetime_
uint8_t get_alpha(const clock::time_point &time) const
rect get_bg_rect(const rect &text_rect) const
utils::optional< int > height_
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:202
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:146
constexpr uint8_t ALPHA_OPAQUE
Definition: color.hpp:37
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:274
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:1031
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:85
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:595
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:62
::rect get_clip()
Get the current clipping area, in draw coordinates.
Definition: draw.cpp:615
void blit(const texture &tex, const ::rect &dst)
Draws a texture, or part of a texture, at the given location.
Definition: draw.cpp:409
Graphical text output.
pango_text & get_text_renderer()
Returns a reference to a static pango_text object.
Definition: text.cpp:959
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:146
rect game_canvas()
The game canvas area, in drawing coordinates.
Definition: video.cpp:427
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