diff --git a/include/asw/modules/ui/button.h b/include/asw/modules/ui/button.h index 8812832..a674290 100644 --- a/include/asw/modules/ui/button.h +++ b/include/asw/modules/ui/button.h @@ -53,16 +53,32 @@ class Button : public Widget { /// void draw(Context& ctx) override; + /// @brief Padding applied inside the button on all sides. + float padding = 0.0f; + /// @brief The font to use for the button text. asw::Font font; /// @brief The button text. std::string text; -private: - bool hovered_ = false; - bool pressed_ = false; - bool focused_ = false; + /// @brief The texture to display on the button. + asw::Texture texture; + + /// @brief Set the texture, optionally resizing the button to match. + /// + /// @param tex The texture to set. + /// @param auto_size If true, resizes the button to the texture dimensions. + /// + void set_texture(const asw::Texture& tex, bool auto_size = false); + + /// @brief Set the text, optionally resizing the button to match. + /// + /// @param t The text to set. + /// @param auto_size If true, resizes the button to fit the text. + /// + void set_text(const std::string& t, bool auto_size = false); + }; } // namespace asw::ui diff --git a/include/asw/modules/ui/context.h b/include/asw/modules/ui/context.h index 890ddbe..765a5bf 100644 --- a/include/asw/modules/ui/context.h +++ b/include/asw/modules/ui/context.h @@ -33,7 +33,7 @@ class FocusManager { /// Widget* focused() const { - return focused_; + return _focused; } /// @brief Set focus to a specific widget. @@ -66,8 +66,8 @@ class FocusManager { private: void dfs(Widget& w); - std::vector focusables_; - Widget* focused_ = nullptr; + std::vector _focusables; + Widget* _focused = nullptr; }; /// @brief Shared state for the UI system. diff --git a/include/asw/modules/ui/input_box.h b/include/asw/modules/ui/input_box.h index 28cd7ea..3673fe1 100644 --- a/include/asw/modules/ui/input_box.h +++ b/include/asw/modules/ui/input_box.h @@ -63,9 +63,7 @@ class InputBox : public Widget { std::string placeholder; private: - bool hovered_ = false; - bool focused_ = false; - std::size_t cursor_pos_ = 0; + std::size_t _cursor_pos = 0; }; } // namespace asw::ui diff --git a/include/asw/modules/ui/widget.h b/include/asw/modules/ui/widget.h index 4634865..6e7cc25 100644 --- a/include/asw/modules/ui/widget.h +++ b/include/asw/modules/ui/widget.h @@ -30,7 +30,7 @@ class Widget { /// @brief Default constructor for Widget. /// Widget() - : id_(generate_id()) { }; + : _id(generate_id()) { }; /// @brief Default virtual destructor. /// @@ -53,7 +53,7 @@ class Widget { /// WidgetId id() const { - return id_; + return _id; } /// @brief Whether the widget is visible. @@ -118,15 +118,29 @@ class Widget { /// @brief The transform (position and size) of the widget. asw::Quad transform; + /// @brief Whether the pointer is currently over this widget. + bool is_hovered() const { return _hovered; } + + /// @brief Whether this widget is currently being pressed. + bool is_pressed() const { return _pressed; } + + /// @brief Whether this widget currently holds focus. + bool is_focused() const { return _focused; } + +protected: + bool _hovered = false; + bool _pressed = false; + bool _focused = false; + private: - static inline int id_counter_ { 1 }; + static inline int _id_counter { 1 }; static int generate_id() { - return id_counter_++; + return _id_counter++; } - WidgetId id_; + WidgetId _id; }; } // namespace asw::ui diff --git a/src/modules/core.cpp b/src/modules/core.cpp index e7b20db..f6a6dce 100644 --- a/src/modules/core.cpp +++ b/src/modules/core.cpp @@ -209,6 +209,7 @@ void asw::core::cleanup() SDL_DestroyWindow(asw::display::window); asw::display::window = nullptr; } + MIX_Quit(); TTF_Quit(); SDL_Quit(); diff --git a/src/modules/draw.cpp b/src/modules/draw.cpp index ce0f903..6b20fc6 100644 --- a/src/modules/draw.cpp +++ b/src/modules/draw.cpp @@ -156,11 +156,11 @@ void asw::draw::text(const asw::Font& font, const std::string& text, } const auto sdlColor = SDL_Color { color.r, color.g, color.b, color.a }; - SDL_Surface* textSurface = TTF_RenderText_Solid(font.get(), text.c_str(), 0, sdlColor); + SDL_Surface* textSurface = TTF_RenderText_Blended(font.get(), text.c_str(), 0, sdlColor); SDL_Texture* textTexture = SDL_CreateTextureFromSurface(asw::display::renderer, textSurface); SDL_SetTextureBlendMode(textTexture, SDL_BLENDMODE_BLEND); - SDL_SetTextureScaleMode(textTexture, SDL_SCALEMODE_NEAREST); + SDL_SetTextureScaleMode(textTexture, SDL_SCALEMODE_LINEAR); SDL_FRect dest; dest.x = position.x; diff --git a/src/modules/ui/button.cpp b/src/modules/ui/button.cpp index 77fb55e..fc57539 100644 --- a/src/modules/ui/button.cpp +++ b/src/modules/ui/button.cpp @@ -5,7 +5,7 @@ void asw::ui::Button::on_focus_changed(Context& ctx, bool focused) { - focused_ = focused; + _focused = focused; (void)ctx; } @@ -17,12 +17,12 @@ bool asw::ui::Button::on_event(Context& ctx, const UIEvent& e) switch (e.type) { case UIEvent::Type::PointerEnter: { - hovered_ = true; + _hovered = true; return false; } case UIEvent::Type::PointerLeave: { - hovered_ = false; - pressed_ = false; + _hovered = false; + _pressed = false; return false; } case UIEvent::Type::PointerMove: { @@ -30,7 +30,7 @@ bool asw::ui::Button::on_event(Context& ctx, const UIEvent& e) } case UIEvent::Type::PointerDown: { if (transform.contains(e.pointer_pos)) { - pressed_ = true; + _pressed = true; ctx.pointer_capture = this; ctx.focus.set_focus(ctx, this); return true; @@ -39,8 +39,8 @@ bool asw::ui::Button::on_event(Context& ctx, const UIEvent& e) } case UIEvent::Type::PointerUp: { const bool in = transform.contains(e.pointer_pos); - const bool wasPressed = pressed_; - pressed_ = false; + const bool wasPressed = _pressed; + _pressed = false; if (ctx.pointer_capture == this) { ctx.pointer_capture = nullptr; } @@ -64,28 +64,57 @@ bool asw::ui::Button::on_event(Context& ctx, const UIEvent& e) return false; } +void asw::ui::Button::set_texture(const asw::Texture& tex, bool auto_size) +{ + texture = tex; + if (auto_size && texture != nullptr) { + const auto tex_size = asw::util::get_texture_size(texture); + transform.size = tex_size + asw::Vec2(padding * 2.0f, padding * 2.0f); + } +} + +void asw::ui::Button::set_text(const std::string& t, bool auto_size) +{ + text = t; + if (auto_size && font != nullptr && !text.empty()) { + const auto size = asw::util::get_text_size(font, text); + transform.size = asw::Vec2( + size.x + padding * 2.0f, + size.y + padding * 2.0f); + } +} + void asw::ui::Button::draw(Context& ctx) { asw::Color bg = ctx.theme.btn_bg; if (!enabled) { bg = ctx.theme.panel_bg; - } else if (pressed_) { + } else if (_pressed) { bg = ctx.theme.btn_pressed; - } else if (hovered_) { + } else if (_hovered) { bg = ctx.theme.btn_hover; } asw::draw::rect_fill(transform, bg); + const asw::Quad inner { + { transform.position.x + padding, transform.position.y + padding }, + { transform.size.x - padding * 2.0f, transform.size.y - padding * 2.0f } + }; + + if (texture != nullptr) { + asw::draw::stretch_sprite(texture, inner); + } + if (!text.empty() && font != nullptr) { const auto text_size = asw::util::get_text_size(font, text); const auto text_pos - = transform.get_center() - asw::Vec2(text_size.x / 2.0f, text_size.y / 2.0f); + = inner.get_center() - asw::Vec2(text_size.x / 2.0f, text_size.y / 2.0f); asw::draw::text(font, text, text_pos, ctx.theme.text, asw::TextJustify::Left); } - if (focused_ && ctx.theme.show_focus) { + if (_focused && ctx.theme.show_focus) { // Focus ring auto ring = asw::Quad(transform); ring.position.x -= 2; diff --git a/src/modules/ui/context.cpp b/src/modules/ui/context.cpp index f8549e0..fb9c49e 100644 --- a/src/modules/ui/context.cpp +++ b/src/modules/ui/context.cpp @@ -5,75 +5,75 @@ void asw::ui::FocusManager::rebuild(Context& ctx, Widget& root) { - focusables_.clear(); + _focusables.clear(); dfs(root); // Keep current focus if still exists - if (focused_ != nullptr) { - auto it = std::ranges::find(focusables_, focused_); - if (it == focusables_.end()) { + if (_focused != nullptr) { + auto it = std::ranges::find(_focusables, _focused); + if (it == _focusables.end()) { set_focus(ctx, nullptr); } } - if (focused_ == nullptr && !focusables_.empty()) { - set_focus(ctx, focusables_.front()); + if (_focused == nullptr && !_focusables.empty()) { + set_focus(ctx, _focusables.front()); } } void asw::ui::FocusManager::set_focus(Context& ctx, Widget* w) { - if (focused_ == w) { + if (_focused == w) { return; } - if (focused_ != nullptr) { - focused_->on_focus_changed(ctx, false); + if (_focused != nullptr) { + _focused->on_focus_changed(ctx, false); } - focused_ = w; - if (focused_ != nullptr) { - focused_->on_focus_changed(ctx, true); + _focused = w; + if (_focused != nullptr) { + _focused->on_focus_changed(ctx, true); } } void asw::ui::FocusManager::focus_next(Context& ctx) { - if (focusables_.empty()) { + if (_focusables.empty()) { return; } - if (focused_ == nullptr) { - set_focus(ctx, focusables_.front()); + if (_focused == nullptr) { + set_focus(ctx, _focusables.front()); return; } - auto it = std::ranges::find(focusables_, focused_); - if (it == focusables_.end()) { - set_focus(ctx, focusables_.front()); + auto it = std::ranges::find(_focusables, _focused); + if (it == _focusables.end()) { + set_focus(ctx, _focusables.front()); return; } ++it; - if (it == focusables_.end()) { - it = focusables_.begin(); + if (it == _focusables.end()) { + it = _focusables.begin(); } set_focus(ctx, *it); } void asw::ui::FocusManager::focus_prev(Context& ctx) { - if (focusables_.empty()) { + if (_focusables.empty()) { return; } - if (focused_ == nullptr) { - set_focus(ctx, focusables_.front()); + if (_focused == nullptr) { + set_focus(ctx, _focusables.front()); return; } - auto it = std::ranges::find(focusables_, focused_); - if (it == focusables_.end()) { - set_focus(ctx, focusables_.front()); + auto it = std::ranges::find(_focusables, _focused); + if (it == _focusables.end()) { + set_focus(ctx, _focusables.front()); return; } - if (it == focusables_.begin()) { - it = focusables_.end(); + if (it == _focusables.begin()) { + it = _focusables.end(); } --it; set_focus(ctx, *it); @@ -81,24 +81,24 @@ void asw::ui::FocusManager::focus_prev(Context& ctx) void asw::ui::FocusManager::focus_dir(Context& ctx, int dx, int dy) { - if (focusables_.empty()) { + if (_focusables.empty()) { return; } - if (focused_ == nullptr) { - set_focus(ctx, focusables_.front()); + if (_focused == nullptr) { + set_focus(ctx, _focusables.front()); return; } - const auto& from = focused_->transform; + const auto& from = _focused->transform; const float fx = from.get_center().x; const float fy = from.get_center().y; Widget* best = nullptr; float bestScore = 1e30f; - for (Widget* w : focusables_) { - if (w == focused_) { + for (Widget* w : _focusables) { + if (w == _focused) { continue; } const auto& to = w->transform; @@ -138,7 +138,7 @@ void asw::ui::FocusManager::focus_dir(Context& ctx, int dx, int dy) void asw::ui::FocusManager::dfs(Widget& w) { if (w.visible && w.enabled && w.focusable) { - focusables_.push_back(&w); + _focusables.push_back(&w); } for (auto const& c : w.children) { dfs(*c); diff --git a/src/modules/ui/input_box.cpp b/src/modules/ui/input_box.cpp index 6eb131a..057dd29 100644 --- a/src/modules/ui/input_box.cpp +++ b/src/modules/ui/input_box.cpp @@ -9,12 +9,12 @@ void asw::ui::InputBox::on_focus_changed(Context& ctx, bool focused) { - focused_ = focused; + _focused = focused; (void)ctx; if (focused) { SDL_StartTextInput(asw::display::window); - cursor_pos_ = value.size(); + _cursor_pos = value.size(); } else { SDL_StopTextInput(asw::display::window); } @@ -28,18 +28,18 @@ bool asw::ui::InputBox::on_event(Context& ctx, const UIEvent& e) switch (e.type) { case UIEvent::Type::PointerEnter: { - hovered_ = true; + _hovered = true; return false; } case UIEvent::Type::PointerLeave: { - hovered_ = false; + _hovered = false; return false; } case UIEvent::Type::PointerDown: { if (transform.contains(e.pointer_pos)) { ctx.pointer_capture = this; ctx.focus.set_focus(ctx, this); - cursor_pos_ = value.size(); + _cursor_pos = value.size(); return true; } return false; @@ -51,8 +51,8 @@ bool asw::ui::InputBox::on_event(Context& ctx, const UIEvent& e) return false; } case UIEvent::Type::TextInput: { - value.insert(cursor_pos_, e.text); - cursor_pos_ += e.text.size(); + value.insert(_cursor_pos, e.text); + _cursor_pos += e.text.size(); if (on_change) { on_change(value); } @@ -60,9 +60,9 @@ bool asw::ui::InputBox::on_event(Context& ctx, const UIEvent& e) } case UIEvent::Type::KeyDown: { if (e.key == asw::input::Key::Backspace) { - if (cursor_pos_ > 0) { - value.erase(cursor_pos_ - 1, 1); - cursor_pos_--; + if (_cursor_pos > 0) { + value.erase(_cursor_pos - 1, 1); + _cursor_pos--; if (on_change) { on_change(value); } @@ -70,8 +70,8 @@ bool asw::ui::InputBox::on_event(Context& ctx, const UIEvent& e) return true; } if (e.key == asw::input::Key::Delete) { - if (cursor_pos_ < value.size()) { - value.erase(cursor_pos_, 1); + if (_cursor_pos < value.size()) { + value.erase(_cursor_pos, 1); if (on_change) { on_change(value); } @@ -79,23 +79,23 @@ bool asw::ui::InputBox::on_event(Context& ctx, const UIEvent& e) return true; } if (e.key == asw::input::Key::Left) { - if (cursor_pos_ > 0) { - cursor_pos_--; + if (_cursor_pos > 0) { + _cursor_pos--; } return true; } if (e.key == asw::input::Key::Right) { - if (cursor_pos_ < value.size()) { - cursor_pos_++; + if (_cursor_pos < value.size()) { + _cursor_pos++; } return true; } if (e.key == asw::input::Key::Home) { - cursor_pos_ = 0; + _cursor_pos = 0; return true; } if (e.key == asw::input::Key::End) { - cursor_pos_ = value.size(); + _cursor_pos = value.size(); return true; } return false; @@ -123,7 +123,7 @@ void asw::ui::InputBox::draw(Context& ctx) // Border asw::Color border = ctx.theme.btn_bg; - if (hovered_ && enabled) { + if (_hovered && enabled) { border = ctx.theme.btn_hover; } asw::draw::rect(transform, border); @@ -150,8 +150,8 @@ void asw::ui::InputBox::draw(Context& ctx) } // Cursor - if (focused_ && font != nullptr) { - const auto before_cursor = value.substr(0, cursor_pos_); + if (_focused && font != nullptr) { + const auto before_cursor = value.substr(0, _cursor_pos); float cursor_x = transform.position.x + text_padding; if (!before_cursor.empty()) { @@ -171,7 +171,7 @@ void asw::ui::InputBox::draw(Context& ctx) SDL_SetRenderClipRect(asw::display::renderer, nullptr); // Focus ring - if (focused_ && ctx.theme.show_focus) { + if (_focused && ctx.theme.show_focus) { auto ring = asw::Quad(transform); ring.position.x -= 2; ring.position.y -= 2;