module; #include export module ra3.ui; import std; export import ra3.core; import ra3.render; import ra3.client; /** * The SDL3 presentation backend. * * SDL3 is the platform layer: it owns the window, the input, and (here) a * low-end `SDL_Renderer` blit path. Only the low-level display primitives live * in this module; the interactive loops are shared in `ra3::client::display`. * The Vulkan backend implements the same interface. */ export namespace ra3::ui { /** * An SDL_Renderer-backed display: cheap, dependency-light, and used as the * fallback when Vulkan is unavailable. */ class sdl_display final : public ra3::client::display { public: sdl_display() = default; sdl_display(const sdl_display &) = delete; auto operator=(const sdl_display &) -> sdl_display & = delete; ~sdl_display() override { this->shutdown(); } [[nodiscard]] auto init(const ra3::client::display_options &options) -> bool override { if (!SDL_Init(SDL_INIT_VIDEO)) return false; this->fps_limit_ = options.fps_limit; window_ = SDL_CreateWindow(options.title.c_str(), options.width, options.height, SDL_WINDOW_RESIZABLE | (options.fullscreen ? SDL_WINDOW_FULLSCREEN : 0)); if (window_ == nullptr) { SDL_Quit(); return false; } SDL_RaiseWindow(window_); renderer_ = SDL_CreateRenderer(window_, nullptr); if (renderer_ == nullptr) { this->shutdown(); return false; } SDL_SetRenderVSync(renderer_, options.fps_limit == 0 ? 1 : 0); SDL_StartTextInput(window_); return true; } [[nodiscard]] auto present(const ra3::render::image &frame, const ra3::render::view_rect &dest, bool changed) -> bool override { if (frame.empty() || renderer_ == nullptr) return false; if (changed || texture_ == nullptr || frame.width() != texture_w_ || frame.height() != texture_h_) { if (texture_ != nullptr) SDL_DestroyTexture(texture_); texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, static_cast(frame.width()), static_cast(frame.height())); if (texture_ == nullptr) return false; texture_w_ = frame.width(); texture_h_ = frame.height(); SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_LINEAR); SDL_UpdateTexture(texture_, nullptr, frame.data(), static_cast(frame.width()) * 4); } SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); SDL_RenderClear(renderer_); const SDL_FRect dst{dest.x, dest.y, dest.w, dest.h}; SDL_RenderTexture(renderer_, texture_, nullptr, &dst); SDL_RenderPresent(renderer_); return true; } [[nodiscard]] auto poll_event(ra3::render::ui_event &out) -> bool override { SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_EVENT_QUIT: out = {}; out.type = ra3::render::ui_event_type::quit; return true; case SDL_EVENT_KEY_DOWN: if (const auto key = map_key(event.key.key); key != ra3::render::ui_key::none) { out = {}; out.type = ra3::render::ui_event_type::key; out.key = key; return true; } break; case SDL_EVENT_TEXT_INPUT: if (event.text.text[0] != '\0') { out = {}; out.type = ra3::render::ui_event_type::text; out.character = event.text.text[0]; return true; } break; case SDL_EVENT_MOUSE_MOTION: out = {}; out.type = ra3::render::ui_event_type::mouse_move; out.x = event.motion.x; out.y = event.motion.y; out.dx = event.motion.xrel; out.dy = event.motion.yrel; out.left = (event.motion.state & SDL_BUTTON_LMASK) != 0U; out.middle = (event.motion.state & SDL_BUTTON_MMASK) != 0U; out.right = (event.motion.state & SDL_BUTTON_RMASK) != 0U; return true; case SDL_EVENT_MOUSE_BUTTON_DOWN: out = {}; out.type = ra3::render::ui_event_type::mouse_button; out.x = event.button.x; out.y = event.button.y; out.left = event.button.button == SDL_BUTTON_LEFT; out.middle = event.button.button == SDL_BUTTON_MIDDLE; out.right = event.button.button == SDL_BUTTON_RIGHT; return true; case SDL_EVENT_MOUSE_BUTTON_UP: out = {}; out.type = ra3::render::ui_event_type::mouse_button; out.x = event.button.x; out.y = event.button.y; out.released = true; out.left = event.button.button == SDL_BUTTON_LEFT; out.middle = event.button.button == SDL_BUTTON_MIDDLE; out.right = event.button.button == SDL_BUTTON_RIGHT; return true; case SDL_EVENT_MOUSE_WHEEL: out = {}; out.type = ra3::render::ui_event_type::wheel; out.wheel = event.wheel.y; return true; default: break; } } return false; } [[nodiscard]] auto window_size() const -> std::pair override { int w = 0; int h = 0; SDL_GetWindowSizeInPixels(window_, &w, &h); return {w, h}; } [[nodiscard]] auto key_down(ra3::render::ui_key key) const -> bool override { const bool *keys = SDL_GetKeyboardState(nullptr); if (keys == nullptr) return false; switch (key) { case ra3::render::ui_key::up: return keys[SDL_SCANCODE_UP]; case ra3::render::ui_key::down: return keys[SDL_SCANCODE_DOWN]; case ra3::render::ui_key::left: return keys[SDL_SCANCODE_LEFT]; case ra3::render::ui_key::right: return keys[SDL_SCANCODE_RIGHT]; default: return false; } } auto shutdown() -> void override { if (window_ != nullptr) SDL_StopTextInput(window_); if (texture_ != nullptr) SDL_DestroyTexture(texture_); if (renderer_ != nullptr) SDL_DestroyRenderer(renderer_); if (window_ != nullptr) SDL_DestroyWindow(window_); SDL_Quit(); texture_ = nullptr; renderer_ = nullptr; window_ = nullptr; texture_w_ = 0; texture_h_ = 0; } [[nodiscard]] auto name() const -> std::string_view override { return "sdl"; } private: [[nodiscard]] static auto map_key(SDL_Keycode key) -> ra3::render::ui_key { using ra3::render::ui_key; switch (key) { case SDLK_UP: return ui_key::up; case SDLK_DOWN: return ui_key::down; case SDLK_LEFT: return ui_key::left; case SDLK_RIGHT: return ui_key::right; case SDLK_PAGEUP: return ui_key::page_up; case SDLK_PAGEDOWN: return ui_key::page_down; case SDLK_RETURN: case SDLK_KP_ENTER: return ui_key::confirm; case SDLK_ESCAPE: return ui_key::cancel; case SDLK_TAB: return ui_key::tab; case SDLK_BACKSPACE: return ui_key::backspace; default: return ui_key::none; } } SDL_Window *window_ = nullptr; SDL_Renderer *renderer_ = nullptr; SDL_Texture *texture_ = nullptr; ra3::core::uint32 texture_w_ = 0; ra3::core::uint32 texture_h_ = 0; }; }