81330f8432
- Notifier singleton (System::init/get/destroy) que dibuixa un cuadre
centrat al centre-superior amb fons semitransparent (derivat oscur del
color del text) i bordes en línies.
- Màquina d'estats HIDDEN → ENTERING → HOLDING → EXITING amb easing
outCubic (entrada) i inCubic (sortida), slide de 300 ms.
- pushRect() afegit a GpuFrameRenderer (2 triangles, edge_dist=0) per
poder pintar el fons opac/semitransparent reutilitzant el pipeline de
línies — sense afegir cap pipeline nou.
- VectorText::render/renderCentered admeten color RGBA explícit
(default {0,0,0,0} preserva el comportament previ amb oscil·lador
global de color).
- Easing header-only a core/utils/easing.hpp (outCubic, inCubic).
- Director crea Notifier just després del DebugOverlay i el draweja com
a última capa per damunt de l'escena i el debug.
Encara cap consumer el crida; els F1-F5 i la doble pulsació d'ESC
arriben en commits posteriors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
187 lines
6.7 KiB
C++
187 lines
6.7 KiB
C++
// notifier.cpp - Implementació del singleton de notificacions toast
|
||
|
||
#include "core/system/notifier.hpp"
|
||
|
||
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
|
||
#include "core/utils/easing.hpp"
|
||
|
||
namespace System {
|
||
|
||
namespace {
|
||
// Geometria del cuadre en coordenades lògiques (1280×720).
|
||
constexpr float CANVAS_WIDTH = 1280.0F;
|
||
constexpr float MARGIN_TOP = 40.0F;
|
||
constexpr float PADDING_H = 16.0F;
|
||
constexpr float PADDING_V = 10.0F;
|
||
constexpr float BORDER_THICKNESS = 2.0F;
|
||
constexpr float TEXT_SCALE = 0.4F;
|
||
constexpr float TEXT_SPACING = 2.0F;
|
||
constexpr float BORDER_BRIGHTNESS = 1.0F;
|
||
|
||
// Cinemàtica del slide.
|
||
constexpr float SLIDE_DURATION_S = 0.30F;
|
||
|
||
// Conversió color SDL → float [0,1].
|
||
constexpr auto toUnit(Uint8 v) -> float {
|
||
return static_cast<float>(v) / 255.0F;
|
||
}
|
||
|
||
// Color del fons: variant fosca del text (0.25× RGB) amb alpha 0.65.
|
||
struct UnitRGBA {
|
||
float r;
|
||
float g;
|
||
float b;
|
||
float a;
|
||
};
|
||
|
||
constexpr auto textColorFloat(SDL_Color c) -> UnitRGBA {
|
||
return UnitRGBA{.r = toUnit(c.r), .g = toUnit(c.g), .b = toUnit(c.b), .a = toUnit(c.a)};
|
||
}
|
||
|
||
constexpr auto bgColorFloat(SDL_Color c) -> UnitRGBA {
|
||
constexpr float DARKEN = 0.25F;
|
||
constexpr float BG_ALPHA = 0.65F;
|
||
return UnitRGBA{
|
||
.r = toUnit(c.r) * DARKEN,
|
||
.g = toUnit(c.g) * DARKEN,
|
||
.b = toUnit(c.b) * DARKEN,
|
||
.a = BG_ALPHA};
|
||
}
|
||
|
||
// Presets per als atajos semàntics.
|
||
constexpr SDL_Color COLOR_INFO{.r = 230, .g = 230, .b = 230, .a = 255};
|
||
constexpr SDL_Color COLOR_WARN{.r = 255, .g = 180, .b = 40, .a = 255};
|
||
constexpr SDL_Color COLOR_EXIT{.r = 255, .g = 80, .b = 80, .a = 255};
|
||
constexpr float DURATION_INFO = 2.0F;
|
||
constexpr float DURATION_WARN = 3.0F;
|
||
constexpr float DURATION_EXIT = 3.0F;
|
||
} // namespace
|
||
|
||
std::unique_ptr<Notifier> Notifier::instance;
|
||
|
||
void Notifier::init(Rendering::Renderer* renderer) {
|
||
if (!instance) {
|
||
instance = std::unique_ptr<Notifier>(new Notifier(renderer));
|
||
}
|
||
}
|
||
|
||
void Notifier::destroy() { instance.reset(); }
|
||
|
||
auto Notifier::get() -> Notifier* { return instance.get(); }
|
||
|
||
Notifier::Notifier(Rendering::Renderer* renderer)
|
||
: renderer_(renderer),
|
||
text_(renderer) {}
|
||
|
||
void Notifier::notify(const std::string& text, SDL_Color text_color, float duration_s) {
|
||
current_text_ = text;
|
||
current_color_ = text_color;
|
||
hold_remaining_s_ = duration_s;
|
||
|
||
const float TEXT_W = Graphics::VectorText::getTextWidth(text, TEXT_SCALE, TEXT_SPACING);
|
||
const float TEXT_H = Graphics::VectorText::getTextHeight(TEXT_SCALE);
|
||
|
||
box_w_ = TEXT_W + (PADDING_H * 2.0F);
|
||
box_h_ = TEXT_H + (PADDING_V * 2.0F);
|
||
text_x_ = (CANVAS_WIDTH - TEXT_W) * 0.5F;
|
||
|
||
y_on_ = MARGIN_TOP;
|
||
y_off_ = -(box_h_ + BORDER_THICKNESS);
|
||
|
||
// Si ja es veu, reseteja el slide-in des de la posició actual perquè
|
||
// la transició sembli continua. Si està amagat, arrenc des de fora.
|
||
if (status_ == Status::HIDDEN) {
|
||
y_current_ = y_off_;
|
||
}
|
||
status_ = Status::ENTERING;
|
||
slide_elapsed_s_ = 0.0F;
|
||
text_scale_ = TEXT_SCALE;
|
||
}
|
||
|
||
void Notifier::notifyInfo(const std::string& text) { notify(text, COLOR_INFO, DURATION_INFO); }
|
||
void Notifier::notifyWarn(const std::string& text) { notify(text, COLOR_WARN, DURATION_WARN); }
|
||
void Notifier::notifyExit(const std::string& text) { notify(text, COLOR_EXIT, DURATION_EXIT); }
|
||
|
||
void Notifier::update(float delta_time) {
|
||
switch (status_) {
|
||
case Status::ENTERING: {
|
||
slide_elapsed_s_ += delta_time;
|
||
if (slide_elapsed_s_ >= SLIDE_DURATION_S) {
|
||
y_current_ = y_on_;
|
||
status_ = Status::HOLDING;
|
||
slide_elapsed_s_ = 0.0F;
|
||
} else {
|
||
const float T = slide_elapsed_s_ / SLIDE_DURATION_S;
|
||
const float K = Utils::Easing::outCubic(T);
|
||
y_current_ = y_off_ + ((y_on_ - y_off_) * K);
|
||
}
|
||
break;
|
||
}
|
||
case Status::HOLDING: {
|
||
hold_remaining_s_ -= delta_time;
|
||
if (hold_remaining_s_ <= 0.0F) {
|
||
status_ = Status::EXITING;
|
||
slide_elapsed_s_ = 0.0F;
|
||
}
|
||
break;
|
||
}
|
||
case Status::EXITING: {
|
||
slide_elapsed_s_ += delta_time;
|
||
if (slide_elapsed_s_ >= SLIDE_DURATION_S) {
|
||
y_current_ = y_off_;
|
||
status_ = Status::HIDDEN;
|
||
} else {
|
||
const float T = slide_elapsed_s_ / SLIDE_DURATION_S;
|
||
const float K = Utils::Easing::inCubic(T);
|
||
y_current_ = y_on_ + ((y_off_ - y_on_) * K);
|
||
}
|
||
break;
|
||
}
|
||
case Status::HIDDEN:
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
void Notifier::draw() const {
|
||
if (status_ == Status::HIDDEN) {
|
||
return;
|
||
}
|
||
|
||
const float BOX_X = (CANVAS_WIDTH - box_w_) * 0.5F;
|
||
const float BOX_Y = y_current_;
|
||
const UnitRGBA TC = textColorFloat(current_color_);
|
||
const UnitRGBA BG = bgColorFloat(current_color_);
|
||
|
||
auto* gpu = renderer_;
|
||
|
||
// 1. Fons semitransparent.
|
||
gpu->pushRect(BOX_X, BOX_Y, box_w_, box_h_, BG.r, BG.g, BG.b, BG.a);
|
||
|
||
// 2. Bordes (4 línies amb el color del text).
|
||
const float X1 = BOX_X;
|
||
const float Y1 = BOX_Y;
|
||
const float X2 = BOX_X + box_w_;
|
||
const float Y2 = BOX_Y + box_h_;
|
||
gpu->pushLine(X1, Y1, X2, Y1, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // top
|
||
gpu->pushLine(X1, Y2, X2, Y2, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // bottom
|
||
gpu->pushLine(X1, Y1, X1, Y2, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // left
|
||
gpu->pushLine(X2, Y1, X2, Y2, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // right
|
||
|
||
// 3. Text centrat dins la caixa, amb color explícit (l'alpha != 0
|
||
// li diu al renderShape que no agafe l'oscil·lador global de color).
|
||
const float TEXT_Y = BOX_Y + PADDING_V;
|
||
text_.render(current_text_,
|
||
Vec2{.x = text_x_, .y = TEXT_Y},
|
||
text_scale_,
|
||
TEXT_SPACING,
|
||
BORDER_BRIGHTNESS,
|
||
current_color_);
|
||
}
|
||
|
||
auto Notifier::isActiveWindow() const -> bool {
|
||
return status_ == Status::ENTERING || status_ == Status::HOLDING;
|
||
}
|
||
|
||
} // namespace System
|