// 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(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::instance; void Notifier::init(Rendering::Renderer* renderer) { if (!instance) { instance = std::unique_ptr(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