Merge branch 'feat/notifications': sistema de notificacions toast

Toasts centrats al centre-superior amb fons semitransparent, slide-in/out
amb easings cubic, integrats als toggles F1-F5 i a la doble pulsació
d'ESC per confirmar la sortida.
This commit is contained in:
2026-05-20 22:30:49 +02:00
10 changed files with 628 additions and 266 deletions
+4 -4
View File
@@ -179,7 +179,7 @@ auto VectorText::isSupported(char c) const -> bool {
return chars_.contains(c);
}
void VectorText::render(const std::string& text, const Vec2& position, float scale, float spacing, float brightness) const {
void VectorText::render(const std::string& text, const Vec2& position, float scale, float spacing, float brightness, SDL_Color color) const {
if (renderer_ == nullptr) {
return;
}
@@ -221,7 +221,7 @@ void VectorText::render(const std::string& text, const Vec2& position, float sca
// Ajustar X e Y para que position represente esquina superior izquierda
// (render_shape espera el centro, así que sumamos la mitad de ancho y altura)
Vec2 char_pos = {.x = current_x + (CHAR_WIDTH_SCALED / 2.0F), .y = position.y + (CHAR_HEIGHT_SCALED / 2.0F)};
Rendering::renderShape(renderer_, it->second, char_pos, 0.0F, scale, 1.0F, brightness);
Rendering::renderShape(renderer_, it->second, char_pos, 0.0F, scale, 1.0F, brightness, color);
// Avanzar posición
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
@@ -234,7 +234,7 @@ void VectorText::render(const std::string& text, const Vec2& position, float sca
}
}
void VectorText::renderCentered(const std::string& text, const Vec2& centre_punt, float scale, float spacing, float brightness) const {
void VectorText::renderCentered(const std::string& text, const Vec2& centre_punt, float scale, float spacing, float brightness, SDL_Color color) const {
// Calcular dimensions del text
float text_width = getTextWidth(text, scale, spacing);
float text_height = getTextHeight(scale);
@@ -246,7 +246,7 @@ void VectorText::renderCentered(const std::string& text, const Vec2& centre_punt
.y = centre_punt.y - (text_height / 2.0F)};
// Delegar al método render() existent
render(text, posicio_esquerra, scale, spacing, brightness);
render(text, posicio_esquerra, scale, spacing, brightness, color);
}
auto VectorText::getTextWidth(const std::string& text, float scale, float spacing) -> float {
+5 -4
View File
@@ -3,8 +3,6 @@
#pragma once
#include "core/rendering/render_context.hpp"
#include <SDL3/SDL.h>
#include <memory>
@@ -12,6 +10,7 @@
#include <unordered_map>
#include "core/graphics/shape.hpp"
#include "core/rendering/render_context.hpp"
#include "core/types.hpp"
namespace Graphics {
@@ -27,7 +26,8 @@ class VectorText {
// - scale: factor de scale (1.0 = 20×40 px por carácter)
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
void render(const std::string& text, const Vec2& position, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F) const;
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
void render(const std::string& text, const Vec2& position, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
// Renderizar string centrado en un punto
// - text: cadena a renderizar
@@ -35,7 +35,8 @@ class VectorText {
// - scale: factor de scale (1.0 = 20×40 px por carácter)
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
void renderCentered(const std::string& text, const Vec2& centre_punt, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F) const;
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
void renderCentered(const std::string& text, const Vec2& centre_punt, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
// Calcular ancho total de un string (útil para centrado).
// Es estático: no depende del estado del VectorText (el ancho viene de
@@ -269,6 +269,31 @@ namespace Rendering::GPU {
indices_.push_back(BASE_INDEX + 2);
}
void GpuFrameRenderer::pushRect(float x, float y, float w, float h, float r, float g, float b, float a) {
if (w <= 0.0F || h <= 0.0F) {
return;
}
const float X1 = x;
const float Y1 = y;
const float X2 = x + w;
const float Y2 = y + h;
const auto BASE_INDEX = static_cast<uint16_t>(vertices_.size());
// edge_dist=0 → el fragment shader dóna alpha plena (no fade).
vertices_.push_back({X1, Y1, r, g, b, a, 0.0F});
vertices_.push_back({X2, Y1, r, g, b, a, 0.0F});
vertices_.push_back({X1, Y2, r, g, b, a, 0.0F});
vertices_.push_back({X2, Y2, r, g, b, a, 0.0F});
indices_.push_back(BASE_INDEX + 0);
indices_.push_back(BASE_INDEX + 1);
indices_.push_back(BASE_INDEX + 2);
indices_.push_back(BASE_INDEX + 1);
indices_.push_back(BASE_INDEX + 3);
indices_.push_back(BASE_INDEX + 2);
}
void GpuFrameRenderer::flushBatch() {
if (vertices_.empty() || indices_.empty()) {
return;
@@ -74,6 +74,13 @@ namespace Rendering::GPU {
// Encola una línea con grosor configurable (px). Color RGBA en [0..1].
void pushLine(float x1, float y1, float x2, float y2, float thickness, float r, float g, float b, float a);
// Encola un rectàngle massís (2 triangles) amb color RGBA [0..1]. Es
// remet pel mateix pipeline de líneas (TRIANGLELIST + alpha blend); els
// vèrtexs es marquen amb edge_dist=0 perquè el fragment shader doni
// alpha completa sense fade geomètric. Útil per a fons semitransparents
// d'UI (notificacions, panels).
void pushRect(float x, float y, float w, float h, float r, float g, float b, float a);
// endFrame: flush del batch de líneas → composite postpro → submit + presenta.
void endFrame();
+17 -7
View File
@@ -13,6 +13,7 @@
#include "core/defaults.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/coordinate_transform.hpp"
#include "core/system/notifier.hpp"
#include "project.h"
namespace {
@@ -215,7 +216,9 @@ void SDLManager::increaseWindowSize() {
}
float new_zoom = zoom_factor_ + Defaults::Window::ZOOM_INCREMENT;
applyZoom(new_zoom);
std::cout << "F2: Zoom aumentat a " << zoom_factor_ << "x" << '\n';
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(std::format("ZOOM: {:.1f}X", zoom_factor_));
}
}
void SDLManager::decreaseWindowSize() {
@@ -224,7 +227,9 @@ void SDLManager::decreaseWindowSize() {
}
float new_zoom = zoom_factor_ - Defaults::Window::ZOOM_INCREMENT;
applyZoom(new_zoom);
std::cout << "F1: Zoom reduït a " << zoom_factor_ << "x" << '\n';
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(std::format("ZOOM: {:.1f}X", zoom_factor_));
}
}
void SDLManager::applyWindowSize(int new_width, int new_height) {
@@ -262,18 +267,18 @@ void SDLManager::toggleFullscreen() {
// comportament depèn del mode que tingués la finestra anteriorment.
SDL_SetWindowFullscreenMode(finestra_, nullptr);
SDL_SetWindowFullscreen(finestra_, true);
std::cout << "F3: Fullscreen activat (guardada: "
<< windowed_width_ << "x" << windowed_height_ << ")" << '\n';
} else {
is_fullscreen_ = false;
SDL_SetWindowFullscreen(finestra_, false);
applyWindowSize(windowed_width_, windowed_height_);
std::cout << "F3: Fullscreen desactivat (restaurada: "
<< windowed_width_ << "x" << windowed_height_ << ")" << '\n';
}
cfg_->window.fullscreen = is_fullscreen_;
Mouse::setForceHidden(is_fullscreen_);
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(is_fullscreen_ ? "PANTALLA COMPLETA" : "MODE FINESTRA");
}
}
auto SDLManager::handleWindowEvent(const SDL_Event& event) -> bool {
@@ -325,6 +330,9 @@ void SDLManager::toggleVSync() {
if (on_persist_) {
on_persist_();
}
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(cfg_->rendering.vsync != 0 ? "VSYNC ACTIU" : "VSYNC INACTIU");
}
}
void SDLManager::toggleAntialias() {
@@ -332,5 +340,7 @@ void SDLManager::toggleAntialias() {
gpu_renderer_.setAntialias(cfg_->rendering.antialias != 0);
// No persistim: l'AA és toggleable runtime però el seu estat no es
// guarda al YAML de moment (decisió volgudament conservadora).
std::cout << "F5: AA " << (cfg_->rendering.antialias != 0 ? "ON" : "OFF") << '\n';
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(cfg_->rendering.antialias != 0 ? "AA ACTIU" : "AA INACTIU");
}
}
+14 -1
View File
@@ -17,6 +17,7 @@
#include "core/rendering/sdl_manager.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/resources/resource_loader.hpp"
#include "core/system/notifier.hpp"
#include "core/utils/path_utils.hpp"
#include "debug_overlay.hpp"
#include "game/scenes/game_scene.hpp"
@@ -264,6 +265,11 @@ auto Director::run() -> int {
// a todas las escenas. Toggle con F11 (visible por defecto en _DEBUG).
System::DebugOverlay debug_overlay(sdl.getRenderer(), cfg_->rendering);
// Sistema de notificacions toast: singleton accessible des d'on calgui
// (F1-F5 a sdl_manager, ESC a global_events). El renderer ha de viure
// tant com el Notifier; el destruim explícitament abans de tornar.
System::Notifier::init(sdl.getRenderer());
// Bucle principal: construir escena → frame loop → destruir → siguiente.
while (context.nextScene() != SceneType::EXIT) {
SceneManager::actual = context.nextScene();
@@ -275,6 +281,7 @@ auto Director::run() -> int {
}
SceneManager::actual = SceneType::EXIT;
System::Notifier::destroy();
return 0;
}
@@ -325,6 +332,9 @@ void Director::runFrameLoop(Scene& scene, SDLManager& sdl, SceneContext& context
scene.update(delta_time);
debug_overlay.update(delta_time);
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->update(delta_time);
}
Audio::update();
// Si la swapchain no está disponible (ventana minimizada, etc.),
@@ -335,7 +345,10 @@ void Director::runFrameLoop(Scene& scene, SDLManager& sdl, SceneContext& context
}
sdl.updateRenderingContext();
scene.draw();
debug_overlay.draw(); // siempre on top de la escena
debug_overlay.draw(); // sempre per damunt de l'escena
if (const auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->draw(); // toast: per damunt de tot
}
sdl.present();
}
}
+16 -1
View File
@@ -8,6 +8,7 @@
#include "core/input/input.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/system/notifier.hpp"
#include "scene_context.hpp"
// Using declarations per simplificar el codi
@@ -57,10 +58,24 @@ namespace GlobalEvents {
sdl.toggleAntialias();
return true;
case SDL_SCANCODE_ESCAPE:
case SDL_SCANCODE_ESCAPE: {
// Doble pulsació per confirmar sortida: la primera ESC
// dispara un toast d'avís; mentre el toast està entrant
// o aguantant (isActiveWindow), la segona ESC tanca el
// joc. Si el toast ha començat a sortir o ja ha
// desaparegut, ESC torna a obrir la finestra de
// confirmació sense tancar.
auto* notifier = System::Notifier::get();
if (notifier != nullptr && !notifier->isActiveWindow()) {
notifier->notifyExit("PREMEU ESC UN ALTRE COP PER EIXIR");
return true;
}
// Notifier inexistent (degradació elegant) o segona ESC
// dins la finestra activa: tanquem el joc.
context.setNextScene(SceneType::EXIT);
SceneManager::actual = SceneType::EXIT;
return true;
}
default:
// Tecla no global
+186
View File
@@ -0,0 +1,186 @@
// 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.55F;
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 = 80, .g = 230, .b = 255, .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
+81
View File
@@ -0,0 +1,81 @@
// notifier.hpp - Sistema de notificacions toast (singleton)
// © 2026 JailDesigner
//
// Mostra missatges curts en un cuadre centrat horitzontalment al centre
// superior de la pantalla. El cuadre entra des de fora amb easing outCubic,
// aguanta el temps demanat i surt amb inCubic. El color del text és
// configurable; el fondo es deriva oscurint el RGB del text i posant alpha
// 0.65 (semitransparent).
//
// API singleton (mateix patró que Audio i Input): Notifier::init() al startup,
// Notifier::get()->notify(...) des d'on calgui, Notifier::destroy() al teardown.
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <memory>
#include <string>
#include "core/graphics/vector_text.hpp"
#include "core/rendering/render_context.hpp"
namespace System {
class Notifier {
public:
// Inicialitza el singleton amb el renderer global. El renderer ha de
// viure tant com el Notifier (és del SDLManager, propietat del Director).
static void init(Rendering::Renderer* renderer);
static void destroy();
[[nodiscard]] static auto get() -> Notifier*;
// Mostra una notificació. Si ja n'hi ha una visible, es sobreescriu
// (reset a l'estat ENTERING des de la Y actual; mai s'apilen).
// - text: cadena a mostrar (sense salts de línia)
// - text_color: color RGBA del text i del borde
// - duration_s: temps que es queda visible (sense comptar entry/exit)
void notify(const std::string& text, SDL_Color text_color, float duration_s);
// Atajos semàntics amb colors i durada predefinits.
void notifyInfo(const std::string& text); // blanc, 2.0s
void notifyWarn(const std::string& text); // àmbar, 3.0s
void notifyExit(const std::string& text); // vermell, EXIT_WINDOW_S
void update(float delta_time);
void draw() const;
// Activa mentre el toast està entrant o aguantant. Quan està sortint
// o ja amagat, retorna false. Útil per a la lògica de doble-pulsació
// d'ESC: la segona pulsació només confirma sortida si encara aguanta.
[[nodiscard]] auto isActiveWindow() const -> bool;
private:
explicit Notifier(Rendering::Renderer* renderer);
enum class Status : std::uint8_t { HIDDEN,
ENTERING,
HOLDING,
EXITING };
Rendering::Renderer* renderer_;
Graphics::VectorText text_;
Status status_{Status::HIDDEN};
std::string current_text_;
SDL_Color current_color_{.r = 255, .g = 255, .b = 255, .a = 255};
float hold_remaining_s_{0.0F};
float slide_elapsed_s_{0.0F};
float y_current_{0.0F};
float y_off_{0.0F}; // posició Y fora de pantalla
float y_on_{0.0F}; // posició Y de descans (visible)
float box_w_{0.0F};
float box_h_{0.0F};
float text_x_{0.0F}; // X esquerre del text dins la caixa
float text_scale_{0.4F};
static std::unique_ptr<Notifier> instance;
};
} // namespace System
+24
View File
@@ -0,0 +1,24 @@
// easing.hpp - Funciones d'interpolació suaus (header-only)
// © 2026 JailDesigner
//
// Conjunt mínim de funcions easing per a animacions d'UI. Totes prenen un
// paràmetre normalitzat t ∈ [0,1] i retornen un valor ∈ [0,1].
#pragma once
namespace Utils::Easing {
// outCubic: ràpid al principi, suau cap al final. Útil per a entrades de
// notificacions (slide-in: arrenc d'impacte i frenada cuidada).
constexpr auto outCubic(float t) -> float {
const float INV = 1.0F - t;
return 1.0F - (INV * INV * INV);
}
// inCubic: arranca suau, accelera cap al final. Útil per a sortides
// (slide-out: comença discretament i desapareix ràpid).
constexpr auto inCubic(float t) -> float {
return t * t * t;
}
} // namespace Utils::Easing