wesnoth/src/tooltips.cpp
Charles Dang 12fa8f2844 Use standard chrono types consistently throughout the codebase
* Functions that return time values now return proper std::chrono::duration units (milliseconds,
  seconds, etc.). This removes the need to do manual unit conversions.
* Simple time-to-execute logging was replaced with `utils::optimer`.
* Most uses of `SDL_GetTicks()` have been replaced with `std::chrono::steady_clock`.
* Uses of events::pump_info::ticks() have been replaced with direct calls to steady_clock::now().
  This made the countdown_clock code significantly simpler. As for the music_tinker, that needs
  to be rethought wholesale.
2024-11-07 11:10:13 -05:00

368 lines
9.6 KiB
C++

/*
Copyright (C) 2003 - 2024
by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "tooltips.hpp"
#include "draw_manager.hpp"
#include "floating_label.hpp"
#include "font/standard_colors.hpp"
#include "game_display.hpp"
#include "help/help.hpp"
#include "log.hpp"
#include "video.hpp"
#include <SDL2/SDL_rect.h>
static lg::log_domain log_font("font");
#define DBG_FT LOG_STREAM(debug, log_font)
#define LOG_FT LOG_STREAM(info, log_font)
namespace {
static const int font_size = font::SIZE_SMALL;
static const int text_width = 400;
static const double height_fudge = 0.95; // An artificial "border" to keep tip text from crowding lower edge of viewing area
struct tooltip
{
tooltip(const SDL_Rect& r, const std::string& msg, const std::string& act = "");
rect origin;
rect loc = {};
std::string message;
std::string action;
font::floating_label label;
void init_label();
void update_label_pos();
};
tooltip::tooltip(const SDL_Rect& r, const std::string& msg, const std::string& act)
: origin(r), message(msg), action(act), label(msg)
{
init_label();
DBG_FT << "created tooltip for " << origin << " at " << loc;
}
void tooltip::init_label()
{
const color_t bgcolor {0,0,0,192};
rect game_canvas = video::game_canvas();
unsigned int border = 10;
rect huge;
huge.h=1000000;
huge.w=1000000;
label.set_font_size(font_size);
label.set_color(font::NORMAL_COLOR);
label.set_clip_rect(huge);
label.set_width(text_width); // If tooltip will be too tall for game_canvas, this could be scaled up appropriately
label.set_alignment(font::LEFT_ALIGN);
label.set_bg_color(bgcolor);
label.set_border_size(border);
label.create_texture();
point lsize = label.get_draw_size();
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
while((lsize.y > game_canvas.h*height_fudge) && (lsize.x < game_canvas.w)) {
// Scaling the tip to reduce height is hard, since making a texture wider is no guarantee that there will be fewer lines of text:
//
// This block of text is just
// as tall as the other one.
//
// This block of text is just as tall as the other
// one.
//
// 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).
bool wont_fit = false;
if(new_text_width>game_canvas.w) {
new_text_width=game_canvas.w;
wont_fit = true;
}
DBG_FT << "lsize.x,y = " << lsize.x << "," << lsize.y << ", new_text_width = " << new_text_width;
label.set_width(new_text_width);
label.clear_texture();
label.create_texture();
lsize = label.get_draw_size();
DBG_FT << "new label lsize.x,y = " << lsize.x << "," << lsize.y;
if(wont_fit) {
break;
}
new_text_width *= 1.3;
}
// I don't know if it's strictly necessary to create the texture yet again just to make sure the clip_rect is set to game_canvas
// but it seems like the safe course of action.
label.set_clip_rect(game_canvas);
label.clear_texture();
label.create_texture();
update_label_pos();
}
void tooltip::update_label_pos()
{
rect game_canvas = video::game_canvas();
point lsize = label.get_draw_size();
loc = {0, 0, lsize.x, lsize.y};
DBG_FT << "\nupdate_label_pos() Start: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
if(origin.y > loc.h) {
// There is enough room to fit it above the tip area
loc.y = origin.y - loc.h;
DBG_FT << "\tAbove: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
} else if((origin.y + origin.h + loc.h) <= game_canvas.h*height_fudge) {
// There is enough room to fit it below the tip area
loc.y = origin.y + origin.h;
DBG_FT << "\tBelow: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
} else if(((origin.y + origin.h/2 - loc.h/2) >= 0) &&
((origin.y + origin.h/2 + loc.h/2) <= game_canvas.h*height_fudge)) {
// There is enough room to center it at the tip area
loc.y = origin.y + origin.h/2 - loc.h/2;
DBG_FT << "\tCenter: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
} else if(loc.h <= game_canvas.h*0.95) {
// There is enough room to center it
loc.y = game_canvas.h/2 - loc.h/2;
DBG_FT << "\tScreen Center: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
} else {
// It doesn't fit
loc.y = 0;
DBG_FT << "\tToo big: loc = " << loc.x << "," << loc.y << " origin = " << origin.x << "," << origin.y;
}
DBG_FT << "\tBefore x adjust: loc.x,y,w,h = " << loc.x << "," << loc.y << "," << loc.w << "," << loc.h << " origin = " << origin.x << "," << origin.y;
// Try to keep it within the screen
loc.x = origin.x;
if(loc.x + loc.w > game_canvas.w) {
loc.x = game_canvas.w - loc.w;
}
if(loc.x < 0) {
loc.x = 0;
}
DBG_FT << "\tFinal: loc.x,y,w,h = " << loc.x << "," << loc.y << "," << loc.w << "," << loc.h << " origin = " << origin.x << "," << origin.y;
label.set_position(loc.x, loc.y);
}
std::map<int, tooltip> tips;
int active_tooltip = 0;
int tooltip_id = 1;
surface current_background = nullptr;
// Is this a freaking singleton or is it not?
// This is horrible, but that's how the usage elsewhere is.
// If you want to fix this, either make it an actual singleton,
// or ensure that tooltips:: functions are called on an instance.
tooltips::manager* current_manager = nullptr;
} // anon namespace
/** Clear/hide the active tooltip. */
static void clear_active()
{
if(!active_tooltip) {
return;
}
DBG_FT << "clearing active tooltip " << active_tooltip;
tips.at(active_tooltip).label.undraw();
active_tooltip = 0;
}
namespace tooltips
{
manager::manager()
{
clear_tooltips();
current_manager = this;
}
manager::~manager()
{
try {
clear_tooltips();
} catch (...) {}
current_manager = nullptr;
}
void manager::layout()
{
if(!active_tooltip) {
return;
}
// Update the active tooltip's draw state.
// This will trigger redraws if necessary.
tips.at(active_tooltip).label.update(std::chrono::steady_clock::now());
}
bool manager::expose(const rect& region)
{
// Only the active tip is shown.
if(!active_tooltip) {
return false;
}
tooltip& tip = tips.at(active_tooltip);
if(!tip.loc.overlaps(region)) {
return false;
}
tip.label.draw();
return true;
}
rect manager::screen_location()
{
// Only the active tip, if any, should be visible.
if(!active_tooltip) {
return {};
} else {
return tips.at(active_tooltip).loc;
}
}
void clear_tooltips()
{
LOG_FT << "clearing all tooltips";
clear_active();
tips.clear();
}
void clear_tooltips(const SDL_Rect& r)
{
for(auto i = tips.begin(); i != tips.end(); ) {
if(i->second.origin.overlaps(r)) {
DBG_FT << "clearing tip " << i->first << " at "
<< i->second.origin << " overlapping " << r;
if (i->first == active_tooltip) {
i->second.label.undraw();
active_tooltip = 0;
}
i = tips.erase(i);
} else {
++i;
}
}
}
bool update_tooltip(int id, const SDL_Rect& origin, const std::string& message)
{
std::map<int, tooltip>::iterator it = tips.find(id);
if (it == tips.end() ) return false;
tooltip& tip = it->second;
if(tip.message == message && tip.origin == origin) {
return false;
}
if(tip.message != message) {
LOG_FT << "updating tooltip " << id << " message";
tip.message = message;
tip.label = font::floating_label(message);
tip.init_label();
}
if(tip.origin != origin) {
DBG_FT << "updating tooltip " << id << " origin " << origin;
tip.origin = origin;
tip.update_label_pos();
}
return true;
}
void remove_tooltip(int id)
{
if(!id) { return; }
DBG_FT << "removing tooltip " << id;
if(id == active_tooltip) {
clear_active();
}
tips.erase(id);
}
int add_tooltip(const SDL_Rect& origin, const std::string& message, const std::string& action)
{
// Because some other things are braindead, we have to check we're not
// just adding the same tooltip over and over every time the mouse moves.
for(auto& [id, tip] : tips) {
if(tip.origin == origin && tip.message == message && tip.action == action) {
return id;
}
}
DBG_FT << "adding tooltip for " << origin;
// Clear any existing tooltips for this origin
clear_tooltips(origin);
// Create and add a new tooltip
int id = tooltip_id++;
tips.emplace(id, tooltip(origin, message, action));
return id;
}
static void raise_to_top()
{
// Raise the current manager so it will display on top of everything.
if(!current_manager) {
throw game::error("trying to show tooltip with no tooltip manager");
}
draw_manager::raise_drawable(current_manager);
}
static void select_active(int id)
{
if(active_tooltip == id) {
return;
}
tooltip& tip = tips.at(id);
LOG_FT << "showing tip " << id << " for " << tip.origin;
clear_active();
active_tooltip = id;
tip.label.update(std::chrono::steady_clock::now());
raise_to_top();
}
void process(int mousex, int mousey)
{
point mouseloc{mousex, mousey};
for(auto& [id, tip] : tips) {
if(tip.origin.contains(mouseloc)) {
select_active(id);
return;
}
}
if(active_tooltip) {
LOG_FT << "clearing tooltip because none hovered";
clear_active();
}
}
bool click(int mousex, int mousey)
{
for(auto& [id, tip] : tips) { (void)id;
if(!tip.action.empty() && tip.origin.contains(mousex, mousey)) {
help::show_help(tip.action);
return true;
}
}
return false;
}
} // namespace tooltips