Files
orni-attack/source/core/system/notifier.cpp
T
JailDesigner 81330f8432 feat(notifier): infrastructura del sistema de notificacions toast
- 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>
2026-05-20 22:07:56 +02:00

187 lines
6.7 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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