The Battle for Wesnoth  1.19.12+dev
tooltips.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 "tooltips.hpp"
17 
18 #include "draw_manager.hpp"
19 #include "floating_label.hpp"
20 #include "font/standard_colors.hpp"
21 #include "game_display.hpp"
22 #include "help/help.hpp"
23 #include "log.hpp"
24 #include "video.hpp"
25 
26 #include <SDL2/SDL_rect.h>
27 
28 static lg::log_domain log_font("font");
29 #define DBG_FT LOG_STREAM(debug, log_font)
30 #define LOG_FT LOG_STREAM(info, log_font)
31 
32 namespace {
33 
34 static const int font_size = font::SIZE_SMALL;
35 static const int text_width = 400;
36 static const double height_fudge = 0.95; // An artificial "border" to keep tip text from crowding lower edge of viewing area
37 
38 struct tooltip
39 {
40  tooltip(const SDL_Rect& r, const std::string& msg, const std::string& act = "");
41  rect origin;
42  rect loc = {};
43  std::string message;
44  std::string action;
46  bool label_initialized = false;
47 
48  void init_label();
49  void update_label_pos();
50 };
51 
52 tooltip::tooltip(const SDL_Rect& r, const std::string& msg, const std::string& act)
53  : origin(r), message(msg), action(act), label(msg)
54 {
55  DBG_FT << "created tooltip for " << origin << " at " << loc;
56 }
57 
58 void tooltip::init_label()
59 {
60  const color_t bgcolor {0,0,0,192};
62  unsigned int border = 10;
63 
64  label.set_font_size(font_size);
65  label.set_color(font::NORMAL_COLOR);
66  label.set_width(text_width); // If tooltip will be too tall for game_canvas, this could be scaled up appropriately
67  label.set_height(1000000); // Functionally unbounded on first pass
68  label.set_alignment(font::LEFT_ALIGN);
69  label.set_bg_color(bgcolor);
70  label.set_border_size(border);
71 
72  label.clear_texture();
73  label.create_texture();
74 
75  point lsize = label.get_draw_size();
76  int new_text_width = text_width * static_cast<float>(lsize.y)/game_canvas.h; // If necessary, scale width to reduce height while preserving area of label
77  while((lsize.y > game_canvas.h*height_fudge) && (lsize.x < game_canvas.w)) {
78  // Scaling the tip to reduce height is hard, since making a texture wider is no guarantee that there will be fewer lines of text:
79  //
80  // This block of text is just
81  // as tall as the other one.
82  //
83  // This block of text is just as tall as the other
84  // one.
85  //
86  // Creating this over and over may not be the most efficient route, but it will work and will be quite rare (tip taller than screen).
87  bool wont_fit = false;
88  if(new_text_width>game_canvas.w) {
89  new_text_width=game_canvas.w;
90  wont_fit = true;
91  }
92  DBG_FT << "lsize.x,y = " << lsize.x << "," << lsize.y << ", new_text_width = " << new_text_width;
93 
94  label.set_width(new_text_width);
95  label.clear_texture();
96  label.create_texture();
97 
98  lsize = label.get_draw_size();
99  DBG_FT << "new label lsize.x,y = " << lsize.x << "," << lsize.y;
100  if(wont_fit) {
101  break;
102  }
103  new_text_width *= 1.3;
104  }
105 
106  update_label_pos();
107  label_initialized = true;
108 }
109 
110 void tooltip::update_label_pos()
111 {
113 
114  point lsize = label.get_draw_size();
115  loc = {0, 0, lsize.x, lsize.y};
116 
117  DBG_FT << "\nupdate_label_pos() Start: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
118 
119  if(origin.y > loc.h) {
120  // There is enough room to fit it above the tip area
121  loc.y = origin.y - loc.h;
122  DBG_FT << "\tAbove: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
123  } else if((origin.y + origin.h + loc.h) <= game_canvas.h*height_fudge) {
124  // There is enough room to fit it below the tip area
125  loc.y = origin.y + origin.h;
126  DBG_FT << "\tBelow: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
127  } else if(((origin.y + origin.h/2 - loc.h/2) >= 0) &&
128  ((origin.y + origin.h/2 + loc.h/2) <= game_canvas.h*height_fudge)) {
129  // There is enough room to center it at the tip area
130  loc.y = origin.y + origin.h/2 - loc.h/2;
131  DBG_FT << "\tCenter: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
132  } else if(loc.h <= game_canvas.h*0.95) {
133  // There is enough room to center it
134  loc.y = game_canvas.h/2 - loc.h/2;
135  DBG_FT << "\tScreen Center: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
136  } else {
137  // It doesn't fit
138  loc.y = 0;
139  DBG_FT << "\tToo big: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
140  }
141 
142  DBG_FT << "\tBefore x adjust: loc.x,y,w,h = " << loc.x << "," << loc.y << "," << loc.w << "," << loc.h << " origin = " << origin.x << "," << origin.y;
143  // Try to keep it within the screen
144  loc.x = origin.x;
145  if(loc.x + loc.w > game_canvas.w) {
146  loc.x = game_canvas.w - loc.w;
147  }
148  if(loc.x < 0) {
149  loc.x = 0;
150  }
151 
152  DBG_FT << "\tFinal: loc.x,y,w,h = " << loc.x << "," << loc.y << "," << loc.w << "," << loc.h << " origin = " << origin.x << "," << origin.y;
153  label.set_position(loc.x, loc.y);
154 }
155 
156 
157 std::map<int, tooltip> tips;
158 int active_tooltip = 0;
159 
160 int tooltip_id = 1;
161 
162 // Is this a freaking singleton or is it not?
163 // This is horrible, but that's how the usage elsewhere is.
164 // If you want to fix this, either make it an actual singleton,
165 // or ensure that tooltips:: functions are called on an instance.
166 tooltips::manager* current_manager = nullptr;
167 
168 } // anon namespace
169 
170 /** Clear/hide the active tooltip. */
171 static void clear_active()
172 {
173  if(!active_tooltip) {
174  return;
175  }
176  DBG_FT << "clearing active tooltip " << active_tooltip;
177  tips.at(active_tooltip).label.undraw();
178  active_tooltip = 0;
179 }
180 
181 namespace tooltips
182 {
183 
185 {
186  clear_tooltips();
187  current_manager = this;
188 }
189 
191 {
192  try {
193  clear_tooltips();
194  } catch (...) {}
195  current_manager = nullptr;
196 }
197 
199 {
200  if(!active_tooltip) {
201  return;
202  }
203  tooltip& tip = tips.at(active_tooltip);
204  if(!tip.label_initialized) {
205  tip.init_label();
206  }
207  // Update the active tooltip's draw state.
208  // This will trigger redraws if necessary.
209  tip.label.update(std::chrono::steady_clock::now());
210 }
211 
212 bool manager::expose(const rect& region)
213 {
214  // Only the active tip is shown.
215  if(!active_tooltip) {
216  return false;
217  }
218  tooltip& tip = tips.at(active_tooltip);
219  if(!tip.loc.overlaps(region)) {
220  return false;
221  }
222  tip.label.draw();
223  return true;
224 }
225 
227 {
228  // Only the active tip, if any, should be visible.
229  if(!active_tooltip) {
230  return {};
231  } else {
232  return tips.at(active_tooltip).loc;
233  }
234 }
235 
237 {
238  LOG_FT << "clearing all tooltips";
239  clear_active();
240  tips.clear();
241 }
242 
243 void clear_tooltips(const SDL_Rect& r)
244 {
245  for(auto i = tips.begin(); i != tips.end(); ) {
246  if(i->second.origin.overlaps(r)) {
247  DBG_FT << "clearing tip " << i->first << " at "
248  << i->second.origin << " overlapping " << r;
249 
250  if (i->first == active_tooltip) {
251  i->second.label.undraw();
252  active_tooltip = 0;
253  }
254 
255  i = tips.erase(i);
256  } else {
257  ++i;
258  }
259  }
260 }
261 
262 bool update_tooltip(int id, const SDL_Rect& origin, const std::string& message)
263 {
265  if (it == tips.end() ) return false;
266  tooltip& tip = it->second;
267  if(tip.message == message && tip.origin == origin) {
268  return false;
269  }
270  if(tip.message != message) {
271  LOG_FT << "updating tooltip " << id << " message";
272  tip.message = message;
273  tip.label = font::floating_label(message);
274  tip.init_label();
275  }
276  if(tip.origin != origin) {
277  DBG_FT << "updating tooltip " << id << " origin " << origin;
278  tip.origin = origin;
279  tip.update_label_pos();
280  }
281  return true;
282 }
283 
284 void remove_tooltip(int id)
285 {
286  if(!id) { return; }
287  DBG_FT << "removing tooltip " << id;
288  if(id == active_tooltip) {
289  clear_active();
290  }
291  tips.erase(id);
292 }
293 
294 int add_tooltip(const SDL_Rect& origin, const std::string& message, const std::string& action)
295 {
296  // Because some other things are braindead, we have to check we're not
297  // just adding the same tooltip over and over every time the mouse moves.
298  for(auto& [id, tip] : tips) {
299  if(tip.origin == origin && tip.message == message && tip.action == action) {
300  return id;
301  }
302  }
303  DBG_FT << "adding tooltip for " << origin;
304 
305  // Clear any existing tooltips for this origin
306  clear_tooltips(origin);
307  // Create and add a new tooltip
308  int id = tooltip_id++;
309  tips.try_emplace(id, origin, message, action);
310  return id;
311 }
312 
313 static void raise_to_top()
314 {
315  // Raise the current manager so it will display on top of everything.
316  if(!current_manager) {
317  throw game::error("trying to show tooltip with no tooltip manager");
318  }
319  draw_manager::raise_drawable(current_manager);
320 }
321 
322 static void select_active(int id)
323 {
324  if(active_tooltip == id) {
325  return;
326  }
327  tooltip& tip = tips.at(id);
328  LOG_FT << "showing tip " << id << " for " << tip.origin;
329  clear_active();
330  active_tooltip = id;
331  tip.label.update(std::chrono::steady_clock::now());
332  raise_to_top();
333 }
334 
335 void process(int mousex, int mousey)
336 {
337  point mouseloc{mousex, mousey};
338  for(auto& [id, tip] : tips) {
339  if(tip.origin.contains(mouseloc)) {
340  select_active(id);
341  return;
342  }
343  }
344 
345  if(active_tooltip) {
346  LOG_FT << "clearing tooltip because none hovered";
347  clear_active();
348  }
349 }
350 
351 bool click(int mousex, int mousey)
352 {
353  for(auto& [id, tip] : tips) { (void)id;
354  if(!tip.action.empty() && tip.origin.contains(mousex, mousey)) {
355  help::show_help(tip.action);
356  return true;
357  }
358  }
359  return false;
360 }
361 
362 } // namespace tooltips
map_location loc
Definition: move.cpp:172
virtual bool expose(const rect &region) override
Draw the portion of the drawable intersecting region to the screen.
Definition: tooltips.cpp:212
virtual void layout() override
Finalize the size and position of the drawable and its children, and invalidate any regions requiring...
Definition: tooltips.cpp:198
virtual rect screen_location() override
The location of the TLD on the screen, in drawing coordinates.
Definition: tooltips.cpp:226
@ border
The border of the map.
std::size_t i
Definition: function.cpp:1030
std::string label
What to show in the filter's drop-down list.
Definition: manager.cpp:201
std::string tooltip
Shown when hovering over an entry in the filter's drop-down list.
Definition: manager.cpp:203
std::string id
Text to match against addon_info.tags()
Definition: manager.cpp:199
Standard logging facilities (interface).
void raise_drawable(top_level_drawable *tld)
Raise a TLD to the top of the drawing stack.
const int SIZE_SMALL
Definition: constants.cpp:24
const color_t NORMAL_COLOR
static std::unique_ptr< tooltip > tip
Definition: tooltip.cpp:62
std::vector< game_tip > tips
Definition: settings.cpp:47
void show_help(const std::string &show_topic)
Open the help browser, show topic with id show_topic.
Definition: help.cpp:139
tooltips.
Definition: tooltips.cpp:182
bool click(int mousex, int mousey)
Definition: tooltips.cpp:351
int add_tooltip(const SDL_Rect &origin, const std::string &message, const std::string &action)
Definition: tooltips.cpp:294
void clear_tooltips()
Definition: tooltips.cpp:236
void remove_tooltip(int id)
Definition: tooltips.cpp:284
static void select_active(int id)
Definition: tooltips.cpp:322
void process(int mousex, int mousey)
Definition: tooltips.cpp:335
static void raise_to_top()
Definition: tooltips.cpp:313
bool update_tooltip(int id, const SDL_Rect &origin, const std::string &message)
Definition: tooltips.cpp:262
rect game_canvas()
The game canvas area, in drawing coordinates.
Definition: video.cpp:427
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:109
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:61
Base class for all the errors encountered by the engine.
Definition: exceptions.hpp:29
Holds a 2D point.
Definition: point.hpp:25
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:49
#define DBG_FT
Definition: tooltips.cpp:29
static void clear_active()
Clear/hide the active tooltip.
Definition: tooltips.cpp:171
#define LOG_FT
Definition: tooltips.cpp:30
static lg::log_domain log_font("font")