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