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>
This commit is contained in:
JailDesignerandClaude Opus 4.7 committed 2026-05-20 22:07:56 +02:00
1 parent 799a97930c
commit 81330f8432
8 files changed
+595 -258

No files matched your search

+249 -249
View File
@@ -10,274 +10,274 @@
namespace Graphics {
// Constants para mides base dels caràcters
constexpr float BASE_CHAR_WIDTH = 20.0F; // Amplada base del caràcter
constexpr float BASE_CHAR_HEIGHT = 40.0F; // Altura base del caràcter
// Constants para mides base dels caràcters
constexpr float BASE_CHAR_WIDTH = 20.0F; // Amplada base del caràcter
constexpr float BASE_CHAR_HEIGHT = 40.0F; // Altura base del caràcter
VectorText::VectorText(Rendering::Renderer* renderer)
: renderer_(renderer) {
loadCharset();
}
VectorText::VectorText(Rendering::Renderer* renderer)
: renderer_(renderer) {
loadCharset();
}
void VectorText::loadCharset() {
// Cargar dígitos 0-9
for (char c = '0'; c <= '9'; c++) {
std::string filename = getShapeFilename(c);
auto shape = ShapeLoader::load(filename);
void VectorText::loadCharset() {
// Cargar dígitos 0-9
for (char c = '0'; c <= '9'; c++) {
std::string filename = getShapeFilename(c);
auto shape = ShapeLoader::load(filename);
if (shape && shape->isValid()) {
chars_[c] = shape;
} else {
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
<< '\n';
if (shape && shape->isValid()) {
chars_[c] = shape;
} else {
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
<< '\n';
}
}
// Cargar lletres A-Z (majúscules)
for (char c = 'A'; c <= 'Z'; c++) {
std::string filename = getShapeFilename(c);
auto shape = ShapeLoader::load(filename);
if (shape && shape->isValid()) {
chars_[c] = shape;
} else {
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
<< '\n';
}
}
// Cargar símbolos
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?"};
for (const auto& sym : SYMBOLS) {
char c = sym[0];
std::string filename = getShapeFilename(c);
auto shape = ShapeLoader::load(filename);
if (shape && shape->isValid()) {
chars_[c] = shape;
} else {
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
<< '\n';
}
}
// Cargar símbolo de copyright (©) - UTF-8 U+00A9.
// Usamos el segundo byte (0xA9, 169 decimal) como key interna del map.
{
const std::string FILENAME = "font/char_copyright.shp";
auto shape = ShapeLoader::load(FILENAME);
if (shape && shape->isValid()) {
chars_['\xA9'] = shape;
} else {
std::cerr << "[VectorText] Warning: no s'ha pogut load " << FILENAME
<< '\n';
}
}
std::cout << "[VectorText] Carregats " << chars_.size() << " caràcters"
<< '\n';
}
auto VectorText::getShapeFilename(char c) -> std::string {
// Mapeo carácter → nombre de archivo (con prefix "font/").
// Dígitos 0-9 y mayúsculas A-Z comparten el mismo path: la shape se llama
// como el caracter mismo, así que se agrupan en un único case.
switch (c) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
case 'G':
case 'H':
case 'I':
case 'J':
case 'K':
case 'L':
case 'M':
case 'N':
case 'O':
case 'P':
case 'Q':
case 'R':
case 'S':
case 'T':
case 'U':
case 'V':
case 'W':
case 'X':
case 'Y':
case 'Z':
return std::string("font/char_") + c + ".shp";
// Lletres minúscules a-z (convertir a majúscules)
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
case 'g':
case 'h':
case 'i':
case 'j':
case 'k':
case 'l':
case 'm':
case 'n':
case 'o':
case 'p':
case 'q':
case 'r':
case 's':
case 't':
case 'u':
case 'v':
case 'w':
case 'x':
case 'y':
case 'z':
return std::string("font/char_") + char(c - 32) + ".shp";
// Símbols
case '.':
return "font/char_dot.shp";
case ',':
return "font/char_comma.shp";
case '-':
return "font/char_minus.shp";
case ':':
return "font/char_colon.shp";
case '!':
return "font/char_exclamation.shp";
case '?':
return "font/char_question.shp";
case ' ':
return ""; // Espai es maneja sin load shape
case '\xA9': // Copyright symbol (©) - UTF-8 U+00A9
return "font/char_copyright.shp";
default:
return ""; // Caràcter no suportat
}
}
// Cargar lletres A-Z (majúscules)
for (char c = 'A'; c <= 'Z'; c++) {
std::string filename = getShapeFilename(c);
auto shape = ShapeLoader::load(filename);
auto VectorText::isSupported(char c) const -> bool {
return chars_.contains(c);
}
if (shape && shape->isValid()) {
chars_[c] = shape;
} else {
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
<< '\n';
void VectorText::render(const std::string& text, const Vec2& position, float scale, float spacing, float brightness, SDL_Color color) const {
if (renderer_ == nullptr) {
return;
}
// Ancho de un carácter base (20 px a scale 1.0)
const float CHAR_WIDTH_SCALED = BASE_CHAR_WIDTH * scale;
// Spacing escalado
const float SPACING_SCALED = spacing * scale;
// Altura de un carácter escalado (necesario para ajustar Y)
const float CHAR_HEIGHT_SCALED = BASE_CHAR_HEIGHT * scale;
// Posición X del borde izquierdo del carácter actual
// (se ajustará +BASE_CHAR_WIDTH/2 para obtener el centro al renderizar)
float current_x = position.x;
// Iterar sobre cada byte del string (con detecció UTF-8)
for (size_t i = 0; i < text.length(); i++) {
auto c = static_cast<unsigned char>(text[i]);
// Detectar copyright UTF-8 (0xC2 0xA9)
if (c == 0xC2 && i + 1 < text.length() &&
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
c = 0xA9; // Usar segon byte como a key
i++; // Saltar el següent byte
}
// Manejar espacios (avanzar sin dibujar)
if (c == ' ') {
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
continue;
}
// Verificar si el carácter está soportado
auto it = chars_.find(c);
if (it != chars_.end()) {
// Renderizar carácter
// 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, color);
// Avanzar posición
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
} else {
// Carácter no soportado: saltar (o renderizar '?' en el futuro)
std::cerr << "[VectorText] Warning: caràcter no suportat '" << c << "'"
<< '\n';
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
}
}
}
// Cargar símbolos
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?"};
for (const auto& sym : SYMBOLS) {
char c = sym[0];
std::string filename = getShapeFilename(c);
auto shape = ShapeLoader::load(filename);
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);
if (shape && shape->isValid()) {
chars_[c] = shape;
} else {
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
<< '\n';
}
// Calcular posición de l'esquina superior izquierda
// restant la meitat de las dimensions del point central
Vec2 posicio_esquerra = {
.x = centre_punt.x - (text_width / 2.0F),
.y = centre_punt.y - (text_height / 2.0F)};
// Delegar al método render() existent
render(text, posicio_esquerra, scale, spacing, brightness, color);
}
// Cargar símbolo de copyright (©) - UTF-8 U+00A9.
// Usamos el segundo byte (0xA9, 169 decimal) como key interna del map.
{
const std::string FILENAME = "font/char_copyright.shp";
auto shape = ShapeLoader::load(FILENAME);
if (shape && shape->isValid()) {
chars_['\xA9'] = shape;
} else {
std::cerr << "[VectorText] Warning: no s'ha pogut load " << FILENAME
<< '\n';
}
}
std::cout << "[VectorText] Carregats " << chars_.size() << " caràcters"
<< '\n';
}
auto VectorText::getShapeFilename(char c) -> std::string {
// Mapeo carácter → nombre de archivo (con prefix "font/").
// Dígitos 0-9 y mayúsculas A-Z comparten el mismo path: la shape se llama
// como el caracter mismo, así que se agrupan en un único case.
switch (c) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
case 'G':
case 'H':
case 'I':
case 'J':
case 'K':
case 'L':
case 'M':
case 'N':
case 'O':
case 'P':
case 'Q':
case 'R':
case 'S':
case 'T':
case 'U':
case 'V':
case 'W':
case 'X':
case 'Y':
case 'Z':
return std::string("font/char_") + c + ".shp";
// Lletres minúscules a-z (convertir a majúscules)
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
case 'g':
case 'h':
case 'i':
case 'j':
case 'k':
case 'l':
case 'm':
case 'n':
case 'o':
case 'p':
case 'q':
case 'r':
case 's':
case 't':
case 'u':
case 'v':
case 'w':
case 'x':
case 'y':
case 'z':
return std::string("font/char_") + char(c - 32) + ".shp";
// Símbols
case '.':
return "font/char_dot.shp";
case ',':
return "font/char_comma.shp";
case '-':
return "font/char_minus.shp";
case ':':
return "font/char_colon.shp";
case '!':
return "font/char_exclamation.shp";
case '?':
return "font/char_question.shp";
case ' ':
return ""; // Espai es maneja sin load shape
case '\xA9': // Copyright symbol (©) - UTF-8 U+00A9
return "font/char_copyright.shp";
default:
return ""; // Caràcter no suportat
}
}
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 {
if (renderer_ == nullptr) {
return;
}
// Ancho de un carácter base (20 px a scale 1.0)
const float CHAR_WIDTH_SCALED = BASE_CHAR_WIDTH * scale;
// Spacing escalado
const float SPACING_SCALED = spacing * scale;
// Altura de un carácter escalado (necesario para ajustar Y)
const float CHAR_HEIGHT_SCALED = BASE_CHAR_HEIGHT * scale;
// Posición X del borde izquierdo del carácter actual
// (se ajustará +BASE_CHAR_WIDTH/2 para obtener el centro al renderizar)
float current_x = position.x;
// Iterar sobre cada byte del string (con detecció UTF-8)
for (size_t i = 0; i < text.length(); i++) {
auto c = static_cast<unsigned char>(text[i]);
// Detectar copyright UTF-8 (0xC2 0xA9)
if (c == 0xC2 && i + 1 < text.length() &&
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
c = 0xA9; // Usar segon byte como a key
i++; // Saltar el següent byte
auto VectorText::getTextWidth(const std::string& text, float scale, float spacing) -> float {
if (text.empty()) {
return 0.0F;
}
// Manejar espacios (avanzar sin dibujar)
if (c == ' ') {
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
continue;
const float CHAR_WIDTH_SCALED = BASE_CHAR_WIDTH * scale;
const float SPACING_SCALED = spacing * scale;
// Contar caracteres visuals (no bytes) - manejar UTF-8
size_t visual_chars = 0;
for (size_t i = 0; i < text.length(); i++) {
auto c = static_cast<unsigned char>(text[i]);
// Detectar copyright UTF-8 (0xC2 0xA9) - igual que render()
if (c == 0xC2 && i + 1 < text.length() &&
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
visual_chars++; // Un caràcter visual (©)
i++; // Saltar el següent byte
} else {
visual_chars++; // Caràcter normal
}
}
// Verificar si el carácter está soportado
auto it = chars_.find(c);
if (it != chars_.end()) {
// Renderizar carácter
// 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);
// Avanzar posición
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
} else {
// Carácter no soportado: saltar (o renderizar '?' en el futuro)
std::cerr << "[VectorText] Warning: caràcter no suportat '" << c << "'"
<< '\n';
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
}
}
}
void VectorText::renderCentered(const std::string& text, const Vec2& centre_punt, float scale, float spacing, float brightness) const {
// Calcular dimensions del text
float text_width = getTextWidth(text, scale, spacing);
float text_height = getTextHeight(scale);
// Calcular posición de l'esquina superior izquierda
// restant la meitat de las dimensions del point central
Vec2 posicio_esquerra = {
.x = centre_punt.x - (text_width / 2.0F),
.y = centre_punt.y - (text_height / 2.0F)};
// Delegar al método render() existent
render(text, posicio_esquerra, scale, spacing, brightness);
}
auto VectorText::getTextWidth(const std::string& text, float scale, float spacing) -> float {
if (text.empty()) {
return 0.0F;
// Ancho total = todos los caracteres VISUALES + spacing entre ellos
return (visual_chars * CHAR_WIDTH_SCALED) + ((visual_chars - 1) * SPACING_SCALED);
}
const float CHAR_WIDTH_SCALED = BASE_CHAR_WIDTH * scale;
const float SPACING_SCALED = spacing * scale;
// Contar caracteres visuals (no bytes) - manejar UTF-8
size_t visual_chars = 0;
for (size_t i = 0; i < text.length(); i++) {
auto c = static_cast<unsigned char>(text[i]);
// Detectar copyright UTF-8 (0xC2 0xA9) - igual que render()
if (c == 0xC2 && i + 1 < text.length() &&
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
visual_chars++; // Un caràcter visual (©)
i++; // Saltar el següent byte
} else {
visual_chars++; // Caràcter normal
}
auto VectorText::getTextHeight(float scale) -> float {
return BASE_CHAR_HEIGHT * scale;
}
// Ancho total = todos los caracteres VISUALES + spacing entre ellos
return (visual_chars * CHAR_WIDTH_SCALED) + ((visual_chars - 1) * SPACING_SCALED);
}
auto VectorText::getTextHeight(float scale) -> float {
return BASE_CHAR_HEIGHT * scale;
}
} // namespace Graphics
+9 -8
View File
@@ -3,8 +3,6 @@
#pragma once
#include "core/rendering/render_context.hpp"
#include <SDL3/SDL.h>
#include <memory>
@@ -12,12 +10,13 @@
#include <unordered_map>
#include "core/graphics/shape.hpp"
#include "core/rendering/render_context.hpp"
#include "core/types.hpp"
namespace Graphics {
class VectorText {
public:
class VectorText {
public:
explicit VectorText(Rendering::Renderer* renderer);
// Renderizar string completo
@@ -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
@@ -48,12 +49,12 @@ class VectorText {
// Verificar si un carácter está soportado
[[nodiscard]] auto isSupported(char c) const -> bool;
private:
private:
Rendering::Renderer* renderer_;
std::unordered_map<char, std::shared_ptr<Shape>> chars_;
void loadCharset();
[[nodiscard]] static auto getShapeFilename(char c) -> std::string;
};
};
} // namespace Graphics