mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-22 15:19:31 +00:00
403 lines
8.9 KiB
C++
403 lines
8.9 KiB
C++
/*
|
|
Copyright (C) 2003 - 2025
|
|
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 "floating_label.hpp"
|
|
|
|
#include "draw.hpp"
|
|
#include "draw_manager.hpp"
|
|
#include "font/standard_colors.hpp"
|
|
#include "font/text.hpp"
|
|
#include "log.hpp"
|
|
#include "video.hpp"
|
|
|
|
#include <map>
|
|
#include <set>
|
|
#include <stack>
|
|
|
|
static lg::log_domain log_font("font");
|
|
#define DBG_FT LOG_STREAM(debug, log_font)
|
|
#define LOG_FT LOG_STREAM(info, log_font)
|
|
#define WRN_FT LOG_STREAM(warn, log_font)
|
|
#define ERR_FT LOG_STREAM(err, log_font)
|
|
|
|
static lg::log_domain log_display("display");
|
|
#define ERR_DP LOG_STREAM(err, log_display)
|
|
|
|
namespace
|
|
{
|
|
typedef std::map<int, font::floating_label> label_map;
|
|
label_map labels;
|
|
int label_id = 1;
|
|
|
|
std::stack<std::set<int>> label_contexts;
|
|
|
|
}
|
|
|
|
using namespace std::chrono_literals;
|
|
|
|
namespace font
|
|
{
|
|
floating_label::floating_label(const std::string& text)
|
|
: tex_()
|
|
, screen_loc_()
|
|
, alpha_(0)
|
|
, fadeout_(0)
|
|
, time_start_()
|
|
, text_(text)
|
|
, font_size_(SIZE_SMALL)
|
|
, color_(NORMAL_COLOR)
|
|
, bgcolor_(0, 0, 0, SDL_ALPHA_TRANSPARENT)
|
|
, xpos_(0)
|
|
, ypos_(0)
|
|
, xmove_(0)
|
|
, ymove_(0)
|
|
, lifetime_(-1)
|
|
, width_(-1)
|
|
, height_(-1)
|
|
, clip_rect_(video::game_canvas())
|
|
, visible_(true)
|
|
, align_(CENTER_ALIGN)
|
|
, border_(0)
|
|
, scroll_(ANCHOR_LABEL_SCREEN)
|
|
, use_markup_(true)
|
|
{
|
|
}
|
|
|
|
void floating_label::move(double xmove, double ymove)
|
|
{
|
|
xpos_ += xmove;
|
|
ypos_ += ymove;
|
|
}
|
|
|
|
int floating_label::xpos(std::size_t width) const
|
|
{
|
|
int xpos = int(xpos_);
|
|
if(align_ == font::CENTER_ALIGN) {
|
|
xpos -= width / 2;
|
|
} else if(align_ == font::RIGHT_ALIGN) {
|
|
xpos -= width;
|
|
}
|
|
|
|
return xpos;
|
|
}
|
|
|
|
rect floating_label::get_bg_rect(const rect& text_rect) const
|
|
{
|
|
return text_rect.padded_by(border_);
|
|
}
|
|
|
|
void floating_label::clear_texture()
|
|
{
|
|
tex_.reset();
|
|
}
|
|
|
|
bool floating_label::create_texture()
|
|
{
|
|
if(video::headless()) {
|
|
return false;
|
|
}
|
|
|
|
if(tex_ != nullptr) {
|
|
// Already have a texture
|
|
return true;
|
|
}
|
|
|
|
if(text_.empty()) {
|
|
// Empty labels are unfortunately still used sometimes
|
|
return false;
|
|
}
|
|
|
|
DBG_FT << "creating floating label texture, text: " << text_.substr(0,15);
|
|
font::pango_text& text = font::get_text_renderer();
|
|
|
|
text.set_link_aware(false)
|
|
.set_family_class(font::family_class::sans_serif)
|
|
.set_font_size(font_size_)
|
|
.set_font_style(font::pango_text::STYLE_NORMAL)
|
|
.set_alignment(PANGO_ALIGN_LEFT)
|
|
.set_foreground_color(color_)
|
|
.set_maximum_width(width_ < 0 ? clip_rect_.w : width_)
|
|
.set_maximum_height(height_ < 0 ? clip_rect_.h : height_, true)
|
|
.set_ellipse_mode(PANGO_ELLIPSIZE_END)
|
|
.set_characters_per_line(0)
|
|
.set_add_outline(bgcolor_.a == 0);
|
|
|
|
// ignore last '\n'
|
|
if(!text_.empty() && *(text_.rbegin()) == '\n') {
|
|
text.set_text(std::string(text_.begin(), text_.end() - 1), use_markup_);
|
|
} else {
|
|
text.set_text(text_, use_markup_);
|
|
}
|
|
|
|
tex_ = text.render_and_get_texture();
|
|
if(!tex_) {
|
|
ERR_FT << "could not create floating label's text";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void floating_label::undraw()
|
|
{
|
|
DBG_FT << "undrawing floating label from " << screen_loc_;
|
|
draw_manager::invalidate_region(get_bg_rect(screen_loc_));
|
|
screen_loc_ = {};
|
|
}
|
|
|
|
void floating_label::update(const clock::time_point& time)
|
|
{
|
|
if(video::headless() || text_.empty()) {
|
|
return;
|
|
}
|
|
|
|
if(!create_texture()) {
|
|
ERR_FT << "failed to create texture for floating label";
|
|
return;
|
|
}
|
|
|
|
point new_pos = get_pos(time);
|
|
rect draw_loc {new_pos.x, new_pos.y, tex_.w(), tex_.h()};
|
|
|
|
uint8_t new_alpha = get_alpha(time);
|
|
|
|
// Invalidate former draw loc
|
|
draw_manager::invalidate_region(get_bg_rect(screen_loc_));
|
|
|
|
// Invalidate new draw loc in preparation
|
|
draw_manager::invalidate_region(get_bg_rect(draw_loc));
|
|
|
|
DBG_FT << "updating floating label from " << screen_loc_ << " to " << draw_loc;
|
|
|
|
screen_loc_ = draw_loc;
|
|
alpha_ = new_alpha;
|
|
}
|
|
|
|
void floating_label::draw()
|
|
{
|
|
if(!visible_) {
|
|
screen_loc_ = {};
|
|
return;
|
|
}
|
|
|
|
if(screen_loc_.empty()) {
|
|
return;
|
|
}
|
|
|
|
if(!tex_) {
|
|
ERR_DP << "trying to draw floating label with no texture!";
|
|
return;
|
|
}
|
|
|
|
if(!screen_loc_.overlaps(draw::get_clip().intersect(clip_rect_))) {
|
|
return;
|
|
}
|
|
|
|
DBG_FT << "drawing floating label to " << screen_loc_;
|
|
|
|
// Clip if appropriate.
|
|
auto clipper = draw::reduce_clip(clip_rect_);
|
|
|
|
// Draw background, if appropriate
|
|
if(bgcolor_.a != 0) {
|
|
draw::fill(get_bg_rect(screen_loc_), bgcolor_);
|
|
}
|
|
|
|
// Apply the label texture to the screen.
|
|
tex_.set_alpha_mod(alpha_);
|
|
draw::blit(tex_, screen_loc_);
|
|
}
|
|
|
|
void floating_label::set_lifetime(const std::chrono::milliseconds& lifetime, const std::chrono::milliseconds& fadeout)
|
|
{
|
|
lifetime_ = lifetime;
|
|
fadeout_ = fadeout;
|
|
time_start_ = std::chrono::steady_clock::now();
|
|
}
|
|
|
|
std::chrono::milliseconds floating_label::get_time_alive(const clock::time_point& current_time) const
|
|
{
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(current_time - time_start_);
|
|
}
|
|
|
|
point floating_label::get_pos(const clock::time_point& time)
|
|
{
|
|
auto time_alive = get_time_alive(time);
|
|
return {
|
|
static_cast<int>(time_alive.count() * xmove_ + xpos(tex_.w())),
|
|
static_cast<int>(time_alive.count() * ymove_ + ypos_)
|
|
};
|
|
}
|
|
|
|
uint8_t floating_label::get_alpha(const clock::time_point& time)
|
|
{
|
|
if(lifetime_ >= 0ms && fadeout_ > 0ms) {
|
|
auto time_alive = get_time_alive(time);
|
|
if(time_alive >= lifetime_ && tex_ != nullptr) {
|
|
// fade out moving floating labels
|
|
int alpha_sub = 255 * (time_alive - lifetime_) / fadeout_;
|
|
if (alpha_sub >= 255) {
|
|
return 0;
|
|
} else {
|
|
return 255 - alpha_sub;
|
|
}
|
|
}
|
|
}
|
|
return 255;
|
|
}
|
|
|
|
int add_floating_label(const floating_label& flabel)
|
|
{
|
|
if(label_contexts.empty()) {
|
|
return 0;
|
|
}
|
|
|
|
++label_id;
|
|
labels.emplace(label_id, flabel);
|
|
label_contexts.top().insert(label_id);
|
|
return label_id;
|
|
}
|
|
|
|
void move_floating_label(int handle, double xmove, double ymove)
|
|
{
|
|
const label_map::iterator i = labels.find(handle);
|
|
if(i != labels.end()) {
|
|
i->second.move(xmove, ymove);
|
|
}
|
|
}
|
|
|
|
void scroll_floating_labels(double xmove, double ymove)
|
|
{
|
|
for(label_map::iterator i = labels.begin(); i != labels.end(); ++i) {
|
|
if(i->second.scroll() == ANCHOR_LABEL_MAP) {
|
|
i->second.move(xmove, ymove);
|
|
}
|
|
}
|
|
}
|
|
|
|
void remove_floating_label(int handle, const std::chrono::milliseconds& fadeout)
|
|
{
|
|
const label_map::iterator i = labels.find(handle);
|
|
if(i != labels.end()) {
|
|
if(fadeout > 0ms) {
|
|
i->second.set_lifetime(0ms, fadeout);
|
|
return;
|
|
} else if(fadeout < 0ms) {
|
|
i->second.set_lifetime(0ms, i->second.get_fade_time());
|
|
return;
|
|
}
|
|
// Queue a redraw of where the label was.
|
|
i->second.undraw();
|
|
labels.erase(i);
|
|
}
|
|
|
|
if(!label_contexts.empty()) {
|
|
label_contexts.top().erase(handle);
|
|
}
|
|
}
|
|
|
|
void show_floating_label(int handle, bool value)
|
|
{
|
|
const label_map::iterator i = labels.find(handle);
|
|
if(i != labels.end()) {
|
|
i->second.show(value);
|
|
}
|
|
}
|
|
|
|
SDL_Rect get_floating_label_rect(int handle)
|
|
{
|
|
const label_map::iterator i = labels.find(handle);
|
|
if(i != labels.end()) {
|
|
if (i->second.create_texture()) {
|
|
SDL_Point size = i->second.get_draw_size();
|
|
return {0, 0, size.x, size.y};
|
|
}
|
|
}
|
|
return sdl::empty_rect;
|
|
}
|
|
|
|
floating_label_context::floating_label_context()
|
|
{
|
|
// hacky but the whole floating label system needs to be redesigned...
|
|
for(auto& [id, label] : labels) {
|
|
if(label_contexts.top().count(id) > 0) {
|
|
label.undraw();
|
|
}
|
|
}
|
|
|
|
//TODO: 'pause' floating labels in other contexrs
|
|
label_contexts.emplace();
|
|
}
|
|
|
|
floating_label_context::~floating_label_context()
|
|
{
|
|
//TODO: 'pause' floating labels in other contexrs
|
|
const std::set<int>& context = label_contexts.top();
|
|
|
|
while(!context.empty()) {
|
|
// Remove_floating_label removes the passed label from the context.
|
|
// This loop removes a different label in every iteration.
|
|
remove_floating_label(*context.begin());
|
|
}
|
|
|
|
label_contexts.pop();
|
|
}
|
|
|
|
void draw_floating_labels()
|
|
{
|
|
if(label_contexts.empty()) {
|
|
return;
|
|
}
|
|
|
|
const std::set<int>& context = label_contexts.top();
|
|
|
|
// draw the labels in the order they were added, so later added labels (likely to be tooltips)
|
|
// are displayed over earlier added labels.
|
|
for(auto& [id, label] : labels) {
|
|
if(context.count(id) > 0) {
|
|
label.draw();
|
|
}
|
|
}
|
|
}
|
|
|
|
void update_floating_labels()
|
|
{
|
|
if(label_contexts.empty()) {
|
|
return;
|
|
}
|
|
auto time = std::chrono::steady_clock::now();
|
|
|
|
std::set<int>& context = label_contexts.top();
|
|
|
|
for(auto& [id, label] : labels) {
|
|
if(context.count(id) > 0) {
|
|
label.update(time);
|
|
}
|
|
}
|
|
|
|
//remove expired labels
|
|
for(label_map::iterator j = labels.begin(); j != labels.end(); ) {
|
|
if(context.count(j->first) > 0 && j->second.expired(time)) {
|
|
DBG_FT << "removing expired floating label " << j->first;
|
|
context.erase(j->first);
|
|
labels.erase(j++);
|
|
} else {
|
|
++j;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|